feat(gemini): Add support for Gemini 3.1 Pro models (#13015)

<!-- Template from
https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft:
https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->

<!--

⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On
Hold ⚠️

Please note: For our current development cycle, we are not accepting
feature Pull Requests that introduce changes to Redux data models or
IndexedDB schemas.

While we value your contributions, PRs of this nature will be blocked
without merge. We welcome all other contributions (bug fixes, perf
enhancements, docs, etc.). Thank you!

Once version 2.0.0 is released, we will resume reviewing feature PRs.

-->

### What this PR does

This PR adds support for Google Gemini 3.1 Pro models, including model
detection utilities, reasoning effort configuration, and vision model
support.

**Before this PR:**
- No dedicated detection for Gemini 3.1 Pro models
- No reasoning effort support for Gemini 3.1 models

**After this PR:**
- Added `isGemini31ProModel()` utility function for model detection
- Added reasoning effort support (`low`, `medium`, `high`) for Gemini
3.1 Pro models
- Added vision model support for `gemini-3.1-pro`,
`gemini-3.1-pro-preview`, and `gemini-3.1-flash` (future)
- Added default model configuration for Gemini 3.1 Pro Preview
- Added comprehensive test coverage for all new functionality

Reference: https://ai.google.dev/gemini-api/docs/gemini-3

### Why we need it and why it was done in this way

Google Gemini 3.1 Pro is the latest version in the Gemini model series
with improved reasoning capabilities and vision support. This PR
integrates these new models into Cherry Studio.

The following tradeoffs were made:
- Used regex pattern matching for model detection consistency with
existing Gemini model utilities
- Leveraged the existing reasoning effort infrastructure for Gemini
models

The following alternatives were considered:
- Separate utilities for each Gemini version (rejected in favor of
unified approach)

Links to places where the discussion took place: N/A

### Breaking changes

<!-- optional -->

NONE - This is an additive feature that does not affect existing
functionality.

### Special notes for your reviewer

<!-- optional -->

The test cases added in `utils.test.ts` and `vision.test.ts` follow the
existing patterns for similar model detection functions.

### Checklist

This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and keep it simple
- [x] Refactor: Code is cleaner than before (test coverage added)
- [x] Upgrade: No impact on upgrade flows
- [x] Documentation: User-facing feature documentation will be handled
separately if needed

### Release note

```release-note
feat(gemini): Add support for Gemini 3.1 Pro models including reasoning effort and vision capabilities
```
This commit is contained in:
Phantom
2026-02-25 10:04:08 +08:00
committed by GitHub
parent 7e7277153a
commit 72eb065f69
8 changed files with 122 additions and 12 deletions

View File

@@ -719,7 +719,10 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
})
it('should return gemini3_pro for Gemini 3 Pro models', () => {
expect(getThinkModelType(createModel({ id: 'gemini-3-pro-preview' }))).toBe('gemini3_pro')
expect(getThinkModelType(createModel({ id: 'gemini-pro-latest' }))).toBe('gemini3_pro')
})
it('should return gemini3_1_pro for Gemini 3.1 Pro models', () => {
expect(getThinkModelType(createModel({ id: 'gemini-3.1-pro-preview' }))).toBe('gemini3_1_pro')
expect(getThinkModelType(createModel({ id: 'gemini-pro-latest' }))).toBe('gemini3_1_pro')
})
})
@@ -1963,6 +1966,7 @@ describe('getModelSupportedReasoningEffortOptions', () => {
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'gemini-pro-latest' }))).toEqual([
'default',
'low',
'medium',
'high'
])
})

View File

@@ -23,6 +23,7 @@ import {
isClaude46SeriesModel,
isGemini3FlashModel,
isGemini3ProModel,
isGemini31ProModel,
isGeminiModel,
isGemmaModel,
isGenerateImageModels,
@@ -498,11 +499,6 @@ describe('model utils', () => {
expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-exp-1234' }))).toBe(true)
})
it('detects gemini-pro-latest alias', () => {
expect(isGemini3ProModel(createModel({ id: 'gemini-pro-latest' }))).toBe(true)
expect(isGemini3ProModel(createModel({ id: 'Gemini-Pro-Latest' }))).toBe(true)
})
it('detects gemini-3-pro with uppercase', () => {
expect(isGemini3ProModel(createModel({ id: 'Gemini-3-Pro' }))).toBe(true)
expect(isGemini3ProModel(createModel({ id: 'GEMINI-3-PRO-PREVIEW' }))).toBe(true)
@@ -530,6 +526,56 @@ describe('model utils', () => {
})
})
describe('isGemini31ProModel', () => {
it('detects gemini-3.1-pro model', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro' }))).toBe(true)
})
it('detects gemini-3.1-pro-preview model', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-preview' }))).toBe(true)
})
it('detects gemini-3.1-pro with version suffixes', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-latest' }))).toBe(true)
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-preview-09-2025' }))).toBe(true)
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-exp-1234' }))).toBe(true)
})
it('detects gemini-pro-latest alias', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-pro-latest' }))).toBe(true)
expect(isGemini31ProModel(createModel({ id: 'Gemini-Pro-Latest' }))).toBe(true)
})
it('detects gemini-3.1-pro with uppercase', () => {
expect(isGemini31ProModel(createModel({ id: 'Gemini-3.1-Pro' }))).toBe(true)
expect(isGemini31ProModel(createModel({ id: 'GEMINI-3.1-PRO-PREVIEW' }))).toBe(true)
})
it('excludes gemini-3.1-pro-image models', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-image-preview' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-image' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3.1-pro-image-latest' }))).toBe(false)
})
it('returns false for non-3.1 gemini-3-pro models', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-3-pro' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3-pro-preview' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3-pro-latest' }))).toBe(false)
})
it('returns false for other gemini models', () => {
expect(isGemini31ProModel(createModel({ id: 'gemini-2-pro' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-2.5-pro-preview-09-2025' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3-flash' }))).toBe(false)
expect(isGemini31ProModel(createModel({ id: 'gemini-3-flash-preview' }))).toBe(false)
})
it('returns false for null/undefined models', () => {
expect(isGemini31ProModel(null)).toBe(false)
expect(isGemini31ProModel(undefined)).toBe(false)
})
})
describe('isZhipuModel', () => {
it('detects Zhipu models by provider', () => {
expect(isZhipuModel(createModel({ provider: 'zhipu' }))).toBe(true)

View File

@@ -287,6 +287,35 @@ describe('isVisionModel', () => {
).toBe(true)
})
it('should return true for gemini 3.1 models', () => {
// Preview versions
expect(
isVisionModel({
id: 'gemini-3.1-pro-preview',
name: '',
provider: '',
group: ''
})
).toBe(true)
// Stable versions
expect(
isVisionModel({
id: 'gemini-3.1-pro',
name: '',
provider: '',
group: ''
})
).toBe(true)
expect(
isVisionModel({
id: 'gemini-3.1-flash',
name: '',
provider: '',
group: ''
})
).toBe(true)
})
it('should return true for gemini exp models', () => {
expect(
isVisionModel({

View File

@@ -370,6 +370,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
provider: 'gemini',
name: 'Gemini 3 Pro Preview',
group: 'Gemini 3'
},
{
id: 'gemini-3.1-pro-preview',
provider: 'gemini',
name: 'Gemini 3.1 Pro Preview',
group: 'Gemini 3'
}
],
anthropic: [

View File

@@ -25,6 +25,7 @@ import {
isClaude46SeriesModel,
isGemini3FlashModel,
isGemini3ProModel,
isGemini31ProModel,
isKimi25Model,
withModelIdAndNameAsId
} from './utils'
@@ -55,6 +56,7 @@ export const MODEL_SUPPORTED_REASONING_EFFORT = {
gemini2_pro: ['low', 'medium', 'high', 'auto'] as const,
gemini3_flash: ['minimal', 'low', 'medium', 'high'] as const,
gemini3_pro: ['low', 'high'] as const,
gemini3_1_pro: ['low', 'medium', 'high'] as const,
qwen: ['low', 'medium', 'high'] as const,
qwen_thinking: ['low', 'medium', 'high'] as const,
doubao: ['auto', 'high'] as const,
@@ -90,6 +92,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
gemini2_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini2_pro] as const,
gemini3_flash: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3_flash] as const,
gemini3_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3_pro] as const,
gemini3_1_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3_1_pro] as const,
qwen: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
qwen_thinking: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen_thinking] as const,
doubao: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.doubao] as const,
@@ -144,6 +147,8 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
thinkingModelType = 'gemini3_flash'
} else if (isGemini3ProModel(model)) {
thinkingModelType = 'gemini3_pro'
} else if (isGemini31ProModel(model)) {
thinkingModelType = 'gemini3_1_pro'
} else if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
thinkingModelType = 'gemini2_flash'
} else {

View File

@@ -284,11 +284,13 @@ export const isMaxTemperatureOneModel = (model: Model): boolean => {
return false
}
// major version, including 3.x
export const isGemini3Model = (model: Model) => {
const modelId = getLowerBaseModelName(model.id)
return modelId.includes('gemini-3')
}
// major version, including 3.x
export const isGemini3ThinkingTokenModel = (model: Model) => {
const modelId = getLowerBaseModelName(model.id)
return isGemini3Model(model) && !modelId.includes('image')
@@ -297,7 +299,7 @@ export const isGemini3ThinkingTokenModel = (model: Model) => {
/**
* Check if the model is a Gemini 3 Flash model
* Matches: gemini-3-flash, gemini-3-flash-preview, gemini-3-flash-preview-09-2025, gemini-flash-latest (alias)
* Excludes: gemini-3-flash-image-preview
* Excludes: gemini-3-flash-image-preview, 3.x flash versions
* @param model - The model to check
* @returns true if the model is a Gemini 3 Flash model
*/
@@ -317,7 +319,7 @@ export const isGemini3FlashModel = (model: Model | undefined | null): boolean =>
/**
* Check if the model is a Gemini 3 Pro model
* Matches: gemini-3-pro, gemini-3-pro-preview, gemini-3-pro-preview-09-2025, gemini-pro-latest (alias)
* Excludes: gemini-3-pro-image-preview
* Excludes: gemini-3-pro-image-preview, 3.x pro versions
* @param model - The model to check
* @returns true if the model is a Gemini 3 Pro model
*/
@@ -326,12 +328,29 @@ export const isGemini3ProModel = (model: Model | undefined | null): boolean => {
return false
}
const modelId = getLowerBaseModelName(model.id)
// Check for gemini-pro-latest alias (currently points to gemini-3-pro, may change in future)
// Check for gemini-3-pro with optional suffixes, excluding image variants
return /gemini-3-pro(?!-image)(?:-[\w-]+)*$/i.test(modelId)
}
/**
* Check if the model is a Gemini 3.1 Pro model
* Matches: gemini-3.1-pro, gemini-3.1-pro-preview, gemini-3.1-pro-preview-09-2025, gemini-3.1-pro-latest (alias)
* Excludes: gemini-3.1-pro-image-preview
* @param model - The model to check
* @returns
*/
export const isGemini31ProModel = (model: Model | undefined | null): boolean => {
if (!model) {
return false
}
const modelId = getLowerBaseModelName(model.id)
// Check for gemini-pro-latest alias (currently points to gemini-3.1-pro, may change in future)
if (modelId === 'gemini-pro-latest') {
return true
}
// Check for gemini-3-pro with optional suffixes, excluding image variants
return /gemini-3-pro(?!-image)(?:-[\w-]+)*$/i.test(modelId)
// Check for gemini-3.1-pro with optional suffixes, excluding image variants
return /gemini-3.1-pro(?!-image)(?:-[\w-]+)*$/i.test(modelId)
}
/**

View File

@@ -13,7 +13,7 @@ const visionAllowedModels = [
'gemini-1\\.5',
'gemini-2\\.0',
'gemini-2\\.5',
'gemini-3-(?:flash|pro)(?:-preview)?',
'gemini-3(?:\\.\\d)?-(?:flash|pro)(?:-preview)?',
'gemini-(flash|pro|flash-lite)-latest',
'gemini-exp',
'claude-3',

View File

@@ -112,6 +112,7 @@ const ThinkModelTypes = [
'gemini2_pro',
'gemini3_flash',
'gemini3_pro',
'gemini3_1_pro',
'qwen',
'qwen_thinking',
'doubao',