From b001a7aa7a272c88416e36b2397e544325725db6 Mon Sep 17 00:00:00 2001 From: icarus Date: Thu, 18 Dec 2025 13:51:08 +0800 Subject: [PATCH] feat(gemini): update model types and add support for gemini3 variants add new model type identifiers for gemini3 flash and pro variants implement utility functions to detect gemini3 flash and pro models update reasoning configuration and tests for new gemini variants --- .../config/models/__tests__/reasoning.test.ts | 65 ++++++++++++- .../src/config/models/__tests__/utils.test.ts | 97 +++++++++++++++++++ src/renderer/src/config/models/reasoning.ts | 50 ++++++---- src/renderer/src/config/models/utils.ts | 40 ++++++++ src/renderer/src/types/index.ts | 7 +- 5 files changed, 233 insertions(+), 26 deletions(-) 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',