mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
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:
@@ -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'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -112,6 +112,7 @@ const ThinkModelTypes = [
|
||||
'gemini2_pro',
|
||||
'gemini3_flash',
|
||||
'gemini3_pro',
|
||||
'gemini3_1_pro',
|
||||
'qwen',
|
||||
'qwen_thinking',
|
||||
'doubao',
|
||||
|
||||
Reference in New Issue
Block a user