diff --git a/src/renderer/src/config/models/__tests__/reasoning.test.ts b/src/renderer/src/config/models/__tests__/reasoning.test.ts index ded3f9cc77..af9567b80e 100644 --- a/src/renderer/src/config/models/__tests__/reasoning.test.ts +++ b/src/renderer/src/config/models/__tests__/reasoning.test.ts @@ -949,6 +949,14 @@ describe('Gemini Models', () => { group: '' }) ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-flash-preview', + name: '', + provider: '', + group: '' + }) + ).toBe(true) expect( isSupportedThinkingTokenGeminiModel({ id: 'google/gemini-3-pro-preview', @@ -990,6 +998,31 @@ describe('Gemini Models', () => { group: '' }) ).toBe(true) + // Version with date suffixes + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-flash-preview-09-2025', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-pro-preview-09-2025', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-flash-exp-1234', + name: '', + provider: '', + group: '' + }) + ).toBe(true) // Version with decimals expect( isSupportedThinkingTokenGeminiModel({ @@ -1009,7 +1042,8 @@ describe('Gemini Models', () => { ).toBe(true) }) - it('should return true for gemini-3 image models', () => { + it('should return true for gemini-3-pro-image models only', () => { + // Only gemini-3-pro-image models should return true expect( isSupportedThinkingTokenGeminiModel({ id: 'gemini-3-pro-image-preview', @@ -1018,6 +1052,17 @@ describe('Gemini Models', () => { group: '' }) ).toBe(true) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-pro-image', + name: '', + provider: '', + group: '' + }) + ).toBe(true) + }) + + it('should return false for other gemini-3 image models', () => { expect( isSupportedThinkingTokenGeminiModel({ id: 'gemini-3.0-flash-image-preview', @@ -1080,6 +1125,22 @@ describe('Gemini Models', () => { group: '' }) ).toBe(false) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-flash-preview-tts', + name: '', + provider: '', + group: '' + }) + ).toBe(false) + expect( + isSupportedThinkingTokenGeminiModel({ + id: 'gemini-3-pro-tts', + name: '', + provider: '', + group: '' + }) + ).toBe(false) }) it('should return false for older gemini models', () => { @@ -2072,7 +2133,7 @@ describe('getModelSupportedReasoningEffortOptions', () => { const geminiModel = createModel({ id: 'gemini-2.5-flash-latest' }) const geminiResult = getModelSupportedReasoningEffortOptions(geminiModel) - expect(geminiResult).toEqual(MODEL_SUPPORTED_OPTIONS.gemini) + expect(geminiResult).toEqual(MODEL_SUPPORTED_OPTIONS.gemini2_flash) }) }) }) diff --git a/src/renderer/src/config/models/__tests__/utils.test.ts b/src/renderer/src/config/models/__tests__/utils.test.ts index 042673b75d..602b0737a8 100644 --- a/src/renderer/src/config/models/__tests__/utils.test.ts +++ b/src/renderer/src/config/models/__tests__/utils.test.ts @@ -20,6 +20,8 @@ import { getModelSupportedVerbosity, groupQwenModels, isAnthropicModel, + isGemini3FlashModel, + isGemini3ProModel, isGeminiModel, isGemmaModel, isGenerateImageModels, @@ -432,6 +434,101 @@ describe('model utils', () => { }) }) + describe('isGemini3FlashModel', () => { + it('detects gemini-3-flash model', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash' }))).toBe(true) + }) + + it('detects gemini-3-flash-preview model', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-preview' }))).toBe(true) + }) + + it('detects gemini-3-flash with version suffixes', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-latest' }))).toBe(true) + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-preview-09-2025' }))).toBe(true) + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-exp-1234' }))).toBe(true) + }) + + it('detects gemini-flash-latest alias', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-flash-latest' }))).toBe(true) + expect(isGemini3FlashModel(createModel({ id: 'Gemini-Flash-Latest' }))).toBe(true) + }) + + it('detects gemini-3-flash with uppercase', () => { + expect(isGemini3FlashModel(createModel({ id: 'Gemini-3-Flash' }))).toBe(true) + expect(isGemini3FlashModel(createModel({ id: 'GEMINI-3-FLASH-PREVIEW' }))).toBe(true) + }) + + it('excludes gemini-3-flash-image models', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-image-preview' }))).toBe(false) + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-flash-image' }))).toBe(false) + }) + + it('returns false for non-flash gemini-3 models', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro' }))).toBe(false) + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro-preview' }))).toBe(false) + expect(isGemini3FlashModel(createModel({ id: 'gemini-3-pro-image-preview' }))).toBe(false) + }) + + it('returns false for other gemini models', () => { + expect(isGemini3FlashModel(createModel({ id: 'gemini-2-flash' }))).toBe(false) + expect(isGemini3FlashModel(createModel({ id: 'gemini-2-flash-preview' }))).toBe(false) + expect(isGemini3FlashModel(createModel({ id: 'gemini-2.5-flash-preview-09-2025' }))).toBe(false) + }) + + it('returns false for null/undefined models', () => { + expect(isGemini3FlashModel(null)).toBe(false) + expect(isGemini3FlashModel(undefined)).toBe(false) + }) + }) + + describe('isGemini3ProModel', () => { + it('detects gemini-3-pro model', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro' }))).toBe(true) + }) + + it('detects gemini-3-pro-preview model', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-preview' }))).toBe(true) + }) + + it('detects gemini-3-pro with version suffixes', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-latest' }))).toBe(true) + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-preview-09-2025' }))).toBe(true) + 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) + }) + + it('excludes gemini-3-pro-image models', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image-preview' }))).toBe(false) + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image' }))).toBe(false) + expect(isGemini3ProModel(createModel({ id: 'gemini-3-pro-image-latest' }))).toBe(false) + }) + + it('returns false for non-pro gemini-3 models', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-3-flash' }))).toBe(false) + expect(isGemini3ProModel(createModel({ id: 'gemini-3-flash-preview' }))).toBe(false) + }) + + it('returns false for other gemini models', () => { + expect(isGemini3ProModel(createModel({ id: 'gemini-2-pro' }))).toBe(false) + expect(isGemini3ProModel(createModel({ id: 'gemini-2.5-pro-preview-09-2025' }))).toBe(false) + }) + + it('returns false for null/undefined models', () => { + expect(isGemini3ProModel(null)).toBe(false) + expect(isGemini3ProModel(undefined)).toBe(false) + }) + }) + describe('isZhipuModel', () => { it('detects Zhipu models by provider', () => { expect(isZhipuModel(createModel({ provider: 'zhipu' }))).toBe(true) diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 4b0e293f40..0c4c201202 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -20,7 +20,7 @@ import { isOpenAIReasoningModel, isSupportedReasoningEffortOpenAIModel } from './openai' -import { GEMINI_FLASH_MODEL_REGEX, isGemini3ThinkingTokenModel } from './utils' +import { GEMINI_FLASH_MODEL_REGEX, isGemini3FlashModel, isGemini3ProModel } from './utils' import { isTextToImageModel } from './vision' // Reasoning models @@ -43,9 +43,10 @@ export const MODEL_SUPPORTED_REASONING_EFFORT = { gpt52pro: ['medium', 'high', 'xhigh'] as const, grok: ['low', 'high'] as const, grok4_fast: ['auto'] as const, - gemini: ['low', 'medium', 'high', 'auto'] as const, - gemini3: ['low', 'medium', 'high'] as const, - gemini_pro: ['low', 'medium', 'high', 'auto'] as const, + gemini2_flash: ['low', 'medium', 'high', 'auto'] as const, + gemini2_pro: ['low', 'medium', 'high', 'auto'] as const, + gemini3_flash: ['minimal', 'low', 'medium', 'high'] as const, + gemini3_pro: ['low', 'high'] as const, qwen: ['low', 'medium', 'high'] as const, qwen_thinking: ['low', 'medium', 'high'] as const, doubao: ['auto', 'high'] as const, @@ -72,9 +73,10 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = { gpt52pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gpt52pro] as const, grok: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.grok] as const, grok4_fast: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.grok4_fast] as const, - gemini: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const, - gemini_pro: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro] as const, - gemini3: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini3] as const, + gemini2_flash: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini2_flash] as const, + 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, 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, @@ -100,8 +102,7 @@ const _getThinkModelType = (model: Model): ThinkingModelType => { const modelId = getLowerBaseModelName(model.id) if (isOpenAIDeepResearchModel(model)) { return 'openai_deep_research' - } - if (isGPT51SeriesModel(model)) { + } else if (isGPT51SeriesModel(model)) { if (modelId.includes('codex')) { thinkingModelType = 'gpt5_1_codex' if (isGPT51CodexMaxModel(model)) { @@ -129,16 +130,18 @@ const _getThinkModelType = (model: Model): ThinkingModelType => { } else if (isGrok4FastReasoningModel(model)) { thinkingModelType = 'grok4_fast' } else if (isSupportedThinkingTokenGeminiModel(model)) { - if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) { - thinkingModelType = 'gemini' + if (isGemini3FlashModel(model)) { + thinkingModelType = 'gemini3_flash' + } else if (isGemini3ProModel(model)) { + thinkingModelType = 'gemini3_pro' + } else if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) { + thinkingModelType = 'gemini2_flash' } else { - thinkingModelType = 'gemini_pro' + thinkingModelType = 'gemini2_pro' } - if (isGemini3ThinkingTokenModel(model)) { - thinkingModelType = 'gemini3' - } - } else if (isSupportedReasoningEffortGrokModel(model)) thinkingModelType = 'grok' - else if (isSupportedThinkingTokenQwenModel(model)) { + } else if (isSupportedReasoningEffortGrokModel(model)) { + thinkingModelType = 'grok' + } else if (isSupportedThinkingTokenQwenModel(model)) { if (isQwenAlwaysThinkModel(model)) { thinkingModelType = 'qwen_thinking' } @@ -151,10 +154,15 @@ const _getThinkModelType = (model: Model): ThinkingModelType => { } else { thinkingModelType = 'doubao_no_auto' } - } else if (isSupportedThinkingTokenHunyuanModel(model)) thinkingModelType = 'hunyuan' - else if (isSupportedReasoningEffortPerplexityModel(model)) thinkingModelType = 'perplexity' - else if (isSupportedThinkingTokenZhipuModel(model)) thinkingModelType = 'zhipu' - else if (isDeepSeekHybridInferenceModel(model)) thinkingModelType = 'deepseek_hybrid' + } else if (isSupportedThinkingTokenHunyuanModel(model)) { + thinkingModelType = 'hunyuan' + } else if (isSupportedReasoningEffortPerplexityModel(model)) { + thinkingModelType = 'perplexity' + } else if (isSupportedThinkingTokenZhipuModel(model)) { + thinkingModelType = 'zhipu' + } else if (isDeepSeekHybridInferenceModel(model)) { + thinkingModelType = 'deepseek_hybrid' + } return thinkingModelType } diff --git a/src/renderer/src/config/models/utils.ts b/src/renderer/src/config/models/utils.ts index 6516550937..12e85326cd 100644 --- a/src/renderer/src/config/models/utils.ts +++ b/src/renderer/src/config/models/utils.ts @@ -267,3 +267,43 @@ export const isGemini3ThinkingTokenModel = (model: Model) => { const modelId = getLowerBaseModelName(model.id) return isGemini3Model(model) && !modelId.includes('image') } + +/** + * 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 + * @param model - The model to check + * @returns true if the model is a Gemini 3 Flash model + */ +export const isGemini3FlashModel = (model: Model | undefined | null): boolean => { + if (!model) { + return false + } + const modelId = getLowerBaseModelName(model.id) + // Check for gemini-flash-latest alias (currently points to gemini-3-flash, may change in future) + if (modelId === 'gemini-flash-latest') { + return true + } + // Check for gemini-3-flash with optional suffixes, excluding image variants + return /gemini-3-flash(?!-image)(?:-[\w-]+)*$/i.test(modelId) +} + +/** + * 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 + * @param model - The model to check + * @returns true if the model is a Gemini 3 Pro model + */ +export const isGemini3ProModel = (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-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) +} diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c9dc647acd..bc01e2da74 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -94,9 +94,10 @@ const ThinkModelTypes = [ 'gpt52pro', 'grok', 'grok4_fast', - 'gemini', - 'gemini_pro', - 'gemini3', + 'gemini2_flash', + 'gemini2_pro', + 'gemini3_flash', + 'gemini3_pro', 'qwen', 'qwen_thinking', 'doubao',