From 787782fdaaea957dbfaebd1ac007fc896f64ef93 Mon Sep 17 00:00:00 2001 From: MashiroCodfish <106896687+MashiroCodfish@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:11:31 +0000 Subject: [PATCH 1/2] fix: use thinking levels for Gemini 3 models --- .../components/common/SegmentedControl.tsx | 12 +- src/renderer/i18n/locales/ar/translation.json | 1 - src/renderer/i18n/locales/de/translation.json | 1 - src/renderer/i18n/locales/en/translation.json | 7 +- src/renderer/i18n/locales/es/translation.json | 1 - src/renderer/i18n/locales/fr/translation.json | 1 - .../i18n/locales/it-IT/translation.json | 1 - src/renderer/i18n/locales/ja/translation.json | 1 - src/renderer/i18n/locales/ko/translation.json | 1 - .../i18n/locales/nb-NO/translation.json | 1 - .../i18n/locales/pt-PT/translation.json | 1 - src/renderer/i18n/locales/ru/translation.json | 1 - src/renderer/i18n/locales/sv/translation.json | 1 - .../i18n/locales/zh-Hans/translation.json | 7 +- .../i18n/locales/zh-Hant/translation.json | 7 +- src/renderer/modals/SessionSettings.tsx | 125 +++++++++++++++++- .../providers/definitions/models/chatboxai.ts | 4 +- .../definitions/models/custom-gemini.ts | 10 +- .../providers/definitions/models/gemini.ts | 17 ++- src/shared/types/settings.ts | 3 +- src/shared/utils/google-thinking.test.ts | 67 ++++++++++ src/shared/utils/google-thinking.ts | 84 ++++++++++++ 22 files changed, 316 insertions(+), 38 deletions(-) create mode 100644 src/shared/utils/google-thinking.test.ts create mode 100644 src/shared/utils/google-thinking.ts diff --git a/src/renderer/components/common/SegmentedControl.tsx b/src/renderer/components/common/SegmentedControl.tsx index 8151552ce..e18c6c561 100644 --- a/src/renderer/components/common/SegmentedControl.tsx +++ b/src/renderer/components/common/SegmentedControl.tsx @@ -1,15 +1,13 @@ import { SegmentedControl as MantineSegmentedControl } from '@mantine/core' +import type { ComponentProps } from 'react' -export default function SegmentedControl({ - value, - onChange, - data, - ...props -}: { +type SegmentedControlProps = Omit, 'value' | 'onChange' | 'data'> & { value: string onChange: (value: string) => void data: { label: string; value: string }[] -}) { +} + +export default function SegmentedControl({ value, onChange, data, ...props }: SegmentedControlProps) { return ( Settings and enable Chatbox AI document parsing.": "This file type requires a document parser. Please go to Settings and enable Chatbox AI document parsing.", @@ -827,4 +830,4 @@ "Your license has expired.": "Your license has expired.", "Your license has expired. You can continue using your quota pack.": "Your license has expired. You can continue using your quota pack.", "Your rating on the App Store would help make Chatbox even better!": "Your rating on the App Store would help make Chatbox even better!" -} \ No newline at end of file +} diff --git a/src/renderer/i18n/locales/es/translation.json b/src/renderer/i18n/locales/es/translation.json index e627ef45c..516f2cdff 100644 --- a/src/renderer/i18n/locales/es/translation.json +++ b/src/renderer/i18n/locales/es/translation.json @@ -820,7 +820,6 @@ "Theme": "Tema", "Thinking": "Pensando", "Thinking Budget": "Pensando Presupuesto", - "Thinking Budget only works for 2.0 or later models": "Presupuesto de pensamiento solo funciona para modelos 2.0 o posteriores", "Thinking Budget only works for 3.7 or later models": "Presupuesto de pensamiento solo funciona con modelos 3.7 o posteriores", "Thinking Effort": "Esfuerzo de pensamiento", "Thinking Effort only works for OpenAI o-series models": "Esfuerzo de Pensamiento solo funciona para modelos OpenAI de la serie o", diff --git a/src/renderer/i18n/locales/fr/translation.json b/src/renderer/i18n/locales/fr/translation.json index c2516a171..625d6f704 100644 --- a/src/renderer/i18n/locales/fr/translation.json +++ b/src/renderer/i18n/locales/fr/translation.json @@ -821,7 +821,6 @@ "Theme": "Thème", "Thinking": "Réflexion", "Thinking Budget": "Budget de réflexion", - "Thinking Budget only works for 2.0 or later models": "Le budget de réflexion ne fonctionne que pour les modèles 2.0 ou ultérieurs", "Thinking Budget only works for 3.7 or later models": "Le Budget de réflexion ne fonctionne que pour les modèles 3.7 ou ultérieurs", "Thinking Effort": "Effort de réflexion", "Thinking Effort only works for OpenAI o-series models": "L'Effort de réflexion ne fonctionne que pour les modèles OpenAI de la série o.", diff --git a/src/renderer/i18n/locales/it-IT/translation.json b/src/renderer/i18n/locales/it-IT/translation.json index b46d9f265..2bca83704 100644 --- a/src/renderer/i18n/locales/it-IT/translation.json +++ b/src/renderer/i18n/locales/it-IT/translation.json @@ -819,7 +819,6 @@ "Theme": "Tema", "Thinking": "Pensando", "Thinking Budget": "Pensando al Budget", - "Thinking Budget only works for 2.0 or later models": "Thinking Budget funziona solo per modelli 2.0 o successivi", "Thinking Budget only works for 3.7 or later models": "Budget di Pensiero funziona solo per modelli 3.7 o successivi", "Thinking Effort": "Sforzo di Pensiero", "Thinking Effort only works for OpenAI o-series models": "Lo Sforzo di Pensiero funziona solo per i modelli OpenAI della serie o", diff --git a/src/renderer/i18n/locales/ja/translation.json b/src/renderer/i18n/locales/ja/translation.json index d9ec122ef..4f364e669 100644 --- a/src/renderer/i18n/locales/ja/translation.json +++ b/src/renderer/i18n/locales/ja/translation.json @@ -821,7 +821,6 @@ "Theme": "テーマ", "Thinking": "考え中", "Thinking Budget": "思考予算", - "Thinking Budget only works for 2.0 or later models": "思考予算は2.0以降のモデルでのみ動作します", "Thinking Budget only works for 3.7 or later models": "思考予算は3.7以降のモデルにのみ対応しています", "Thinking Effort": "思考労力", "Thinking Effort only works for OpenAI o-series models": "思考努力は OpenAI o-series models にのみ対応しています", diff --git a/src/renderer/i18n/locales/ko/translation.json b/src/renderer/i18n/locales/ko/translation.json index 737cd9869..c43594684 100644 --- a/src/renderer/i18n/locales/ko/translation.json +++ b/src/renderer/i18n/locales/ko/translation.json @@ -821,7 +821,6 @@ "Theme": "테마", "Thinking": "생각 중", "Thinking Budget": "사고 예산", - "Thinking Budget only works for 2.0 or later models": "사고 예산은 2.0 이상 모델에서만 작동합니다", "Thinking Budget only works for 3.7 or later models": "사고 예산은 3.7 이상 모델에서만 작동합니다", "Thinking Effort": "생각 노력", "Thinking Effort only works for OpenAI o-series models": "사고 노력은 OpenAI o-series 모델에서만 작동합니다", diff --git a/src/renderer/i18n/locales/nb-NO/translation.json b/src/renderer/i18n/locales/nb-NO/translation.json index c20d911b3..958eb7aad 100644 --- a/src/renderer/i18n/locales/nb-NO/translation.json +++ b/src/renderer/i18n/locales/nb-NO/translation.json @@ -820,7 +820,6 @@ "Theme": "Tema", "Thinking": "Tenker", "Thinking Budget": "Tenkebudsjett", - "Thinking Budget only works for 2.0 or later models": "Tenkebudsjett fungerer kun for 2.0 eller nyere modeller", "Thinking Budget only works for 3.7 or later models": "Tenkebudsjett fungerer bare for 3.7 eller nyere modeller", "Thinking Effort": "Tankearbeid", "Thinking Effort only works for OpenAI o-series models": "Tenkeinnsats fungerer bare for OpenAI o-series-modeller", diff --git a/src/renderer/i18n/locales/pt-PT/translation.json b/src/renderer/i18n/locales/pt-PT/translation.json index 438537520..d55b34760 100644 --- a/src/renderer/i18n/locales/pt-PT/translation.json +++ b/src/renderer/i18n/locales/pt-PT/translation.json @@ -820,7 +820,6 @@ "Theme": "Tema", "Thinking": "A pensar", "Thinking Budget": "A pensar Orçamento", - "Thinking Budget only works for 2.0 or later models": "O Orçamento de Raciocínio só funciona para modelos 2.0 ou posteriores", "Thinking Budget only works for 3.7 or later models": "Orçamento de Pensamento só funciona para 3.7 ou modelos posteriores", "Thinking Effort": "Esforço de Pensamento", "Thinking Effort only works for OpenAI o-series models": "Esforço de Pensamento só funciona para os modelos OpenAI da série o.", diff --git a/src/renderer/i18n/locales/ru/translation.json b/src/renderer/i18n/locales/ru/translation.json index 10522e2d4..9920011a7 100644 --- a/src/renderer/i18n/locales/ru/translation.json +++ b/src/renderer/i18n/locales/ru/translation.json @@ -821,7 +821,6 @@ "Theme": "Тема", "Thinking": "Думаю", "Thinking Budget": "Бюджет мышления", - "Thinking Budget only works for 2.0 or later models": "Бюджет мышления работает только с моделями 2.0 или более поздних версий", "Thinking Budget only works for 3.7 or later models": "Бюджет мышления работает только с моделями 3.7 или новее", "Thinking Effort": "Мыслительное усилие", "Thinking Effort only works for OpenAI o-series models": "Усилие мышления работает только для моделей OpenAI o-серии", diff --git a/src/renderer/i18n/locales/sv/translation.json b/src/renderer/i18n/locales/sv/translation.json index 136bc4f71..107be130d 100644 --- a/src/renderer/i18n/locales/sv/translation.json +++ b/src/renderer/i18n/locales/sv/translation.json @@ -820,7 +820,6 @@ "Theme": "Tema", "Thinking": "Tänker", "Thinking Budget": "Budgettänkande", - "Thinking Budget only works for 2.0 or later models": "Tänkandebudget fungerar endast för modeller 2.0 eller senare", "Thinking Budget only works for 3.7 or later models": "Thinking Budget fungerar endast för 3.7 eller senare modeller", "Thinking Effort": "Tänkande Ansträngning", "Thinking Effort only works for OpenAI o-series models": "Tankeansträngning fungerar endast för OpenAI o-seriens modeller", diff --git a/src/renderer/i18n/locales/zh-Hans/translation.json b/src/renderer/i18n/locales/zh-Hans/translation.json index ba7aa3bd7..93b3c8061 100644 --- a/src/renderer/i18n/locales/zh-Hans/translation.json +++ b/src/renderer/i18n/locales/zh-Hans/translation.json @@ -492,6 +492,7 @@ "Login timeout. Please try again.": "登录超时。请重试。", "Login to Chatbox AI": "登录 Chatbox AI", "Login to start chatting with AI": "登录后与 AI 聊天", + "Minimal": "极低", "Low": "低", "Make sure you have the following command installed:": "请确保您已安装以下命令:", "Manage License": "管理 License", @@ -829,10 +830,12 @@ "Theme": "主题", "Thinking": "思考中", "Thinking Budget": "思考预算", - "Thinking Budget only works for 2.0 or later models": "思考预算仅适用于 2.0 及更高版本模型", + "Thinking Budget only works for Gemini 2.5 models": "思考预算仅适用于 Gemini 2.5 模型", "Thinking Budget only works for 3.7 or later models": "思考预算仅适用于 3.7 或更高版本模型", "Thinking Effort": "思考程度", "Thinking Effort only works for OpenAI o-series models": "思考仅适用于 OpenAI o 系列模型", + "Thinking Level": "思考级别", + "Thinking Level only works for Gemini 3 models": "思考级别仅适用于 Gemini 3 模型", "Third-party cloud parsing service, supports PDF and most Office files. Requires API token.": "第三方云解析服务,支持 PDF 和大多数 Office 文件。需要 API token。", "This action cannot be undone. All documents and their embeddings will be permanently deleted.": "此操作无法撤销。所有文档及其嵌入将被永久删除。", "This file type requires a document parser. Please go to Settings and enable Chatbox AI document parsing.": "此文件类型需要文档解析器。请前往 设置 并启用 Chatbox AI 文档解析。", @@ -954,4 +957,4 @@ "Your license has expired. Please check your subscription or purchase a new one.": "您的 license 已过期。请检查您的订阅或重新购买。", "Your license has expired. You can continue using your quota pack.": "您的许可证已过期。您可以继续使用您的配额包。", "Your rating on the App Store would help make Chatbox even better!": "您的 App Store 评分将帮助 Chatbox 变得更好!" -} \ No newline at end of file +} diff --git a/src/renderer/i18n/locales/zh-Hant/translation.json b/src/renderer/i18n/locales/zh-Hant/translation.json index 94078fa10..843054a15 100644 --- a/src/renderer/i18n/locales/zh-Hant/translation.json +++ b/src/renderer/i18n/locales/zh-Hant/translation.json @@ -489,6 +489,7 @@ "Login timeout. Please try again.": "登入逾時。請再試一次。", "Login to Chatbox AI": "登入 Chatbox AI", "Login to start chatting with AI": "登入以開始與 AI 對話", + "Minimal": "極低", "Low": "低", "Make sure you have the following command installed:": "請確認您已安裝以下指令:", "Manage License": "管理 License", @@ -821,10 +822,12 @@ "Theme": "主題", "Thinking": "思考中", "Thinking Budget": "思考預算", - "Thinking Budget only works for 2.0 or later models": "思考預算僅適用於 2.0 或更新版本模型", + "Thinking Budget only works for Gemini 2.5 models": "思考預算僅適用於 Gemini 2.5 模型", "Thinking Budget only works for 3.7 or later models": "思考預算僅適用於 3.7 或更新的模型", "Thinking Effort": "思考程度", "Thinking Effort only works for OpenAI o-series models": "思考程度僅適用於 OpenAI o-series 模型", + "Thinking Level": "思考級別", + "Thinking Level only works for Gemini 3 models": "思考級別僅適用於 Gemini 3 模型", "Third-party cloud parsing service, supports PDF and most Office files. Requires API token.": "第三方雲端解析服務,支援 PDF 和大部分 Office 檔案。需要 API 權杖。", "This action cannot be undone. All documents and their embeddings will be permanently deleted.": "此動作無法復原。所有文件及其嵌入將被永久刪除。", "This file type requires a document parser. Please go to Settings and enable Chatbox AI document parsing.": "此檔案類型需要文件解析器。請前往 設定 並啟用 Chatbox AI 文件解析。", @@ -944,4 +947,4 @@ "Your license has expired. Please check your subscription or purchase a new one.": "您的 License 已過期。請檢查您的訂閱或購買新的 License。", "Your license has expired. You can continue using your quota pack.": "您的許可證已過期。您可以繼續使用您的配額包。", "Your rating on the App Store would help make Chatbox even better!": "您的 App Store 評分將幫助 Chatbox 變得更好!" -} \ No newline at end of file +} diff --git a/src/renderer/modals/SessionSettings.tsx b/src/renderer/modals/SessionSettings.tsx index 51cf13f29..ea06555be 100644 --- a/src/renderer/modals/SessionSettings.tsx +++ b/src/renderer/modals/SessionSettings.tsx @@ -22,19 +22,25 @@ import { type Session, type SessionSettings, } from '@shared/types' +import { + type GoogleThinkingLevel, + getDefaultGoogleThinkingLevel, + getGoogleThinkingMode, + getSupportedGoogleThinkingLevels, +} from '@shared/utils/google-thinking' import { IconInfoCircle, IconTrash } from '@tabler/icons-react' import { pick } from 'lodash' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { AssistantAvatar } from '@/components/common/Avatar' import { AdaptiveModal } from '@/components/common/AdaptiveModal' +import { AssistantAvatar } from '@/components/common/Avatar' import LazyNumberInput from '@/components/common/LazyNumberInput' import MaxContextMessageCountSlider from '@/components/common/MaxContextMessageCountSlider' +import { ScalableIcon } from '@/components/common/ScalableIcon' +import SegmentedControl from '@/components/common/SegmentedControl' import SliderWithInput from '@/components/common/SliderWithInput' import { handleImageInputAndSave } from '@/components/Image' import ImageStyleSelect from '@/components/ImageStyleSelect' -import { ScalableIcon } from '@/components/common/ScalableIcon' -import SegmentedControl from '@/components/common/SegmentedControl' import { useIsSmallScreen } from '@/hooks/useScreenChange' import { trackingEvent } from '@/packages/event' import { StorageKeyGenerator } from '@/storage/StoreStorage' @@ -418,6 +424,70 @@ function ThinkingBudgetConfig({ ) } +interface ThinkingLevelConfigProps { + currentLevel: GoogleThinkingLevel + supportedLevels: GoogleThinkingLevel[] + onLevelChange: (thinkingLevel: GoogleThinkingLevel) => void + tooltipText: string +} + +function ThinkingLevelConfig({ currentLevel, supportedLevels, onLevelChange, tooltipText }: ThinkingLevelConfigProps) { + const { t } = useTranslation() + + const thinkingLevelOptions = useMemo( + () => + supportedLevels.map((level) => ({ + label: + level === 'minimal' + ? t('Minimal') + : level === 'low' + ? t('Low') + : level === 'medium' + ? t('Medium') + : t('High'), + value: level, + })), + [supportedLevels, t] + ) + + const handleThinkingLevelChange = useCallback( + (value: string) => { + onLevelChange(value as GoogleThinkingLevel) + }, + [onLevelChange] + ) + + return ( + + + + {t('Thinking Level')} + + + + + + +
+ +
+
+ ) +} + function ClaudeProviderConfig({ settings, onSettingsChange, @@ -528,9 +598,12 @@ function GoogleProviderConfig({ onSettingsChange: (data: Session['settings']) => void }) { const { t } = useTranslation() + const modelId = settings?.modelId || '' const providerOptions = settings?.providerOptions?.google + const thinkingMode = getGoogleThinkingMode(modelId) + const supportedLevels = useMemo(() => getSupportedGoogleThinkingLevels(modelId), [modelId]) - const handleConfigChange = (config: { budgetTokens: number; enabled: boolean }) => { + const handleBudgetConfigChange = (config: { budgetTokens: number; enabled: boolean }) => { onSettingsChange({ providerOptions: { google: { thinkingConfig: { thinkingBudget: config.budgetTokens, includeThoughts: config.enabled } }, @@ -538,12 +611,52 @@ function GoogleProviderConfig({ }) } + const handleLevelChange = useCallback( + (thinkingLevel: GoogleThinkingLevel) => { + onSettingsChange({ + providerOptions: { + google: { thinkingConfig: { thinkingLevel, includeThoughts: true } }, + }, + }) + }, + [onSettingsChange] + ) + + const currentThinkingLevel = useMemo(() => { + const thinkingLevel = providerOptions?.thinkingConfig?.thinkingLevel + + if (supportedLevels.length === 0) { + return undefined + } + + if (thinkingLevel && supportedLevels.includes(thinkingLevel)) { + return thinkingLevel + } + + return getDefaultGoogleThinkingLevel(modelId) + }, [modelId, providerOptions?.thinkingConfig?.thinkingLevel, supportedLevels]) + + if (thinkingMode === 'level' && currentThinkingLevel) { + return ( + + ) + } + + if (thinkingMode !== 'budget') { + return null + } + return ( 0} - onConfigChange={handleConfigChange} - tooltipText={t('Thinking Budget only works for 2.0 or later models')} + onConfigChange={handleBudgetConfigChange} + tooltipText={t('Thinking Budget only works for Gemini 2.5 models')} minValue={0} maxValue={10000} /> diff --git a/src/shared/providers/definitions/models/chatboxai.ts b/src/shared/providers/definitions/models/chatboxai.ts index 5de8a94df..ae657d5dc 100644 --- a/src/shared/providers/definitions/models/chatboxai.ts +++ b/src/shared/providers/definitions/models/chatboxai.ts @@ -30,6 +30,8 @@ interface Config { uuid: string } +type GoogleImageAspectRatio = NonNullable['aspectRatio']> + // 将chatboxAIFetch移到类内部作为私有方法 export default class ChatboxAI extends AbstractAISDKModel implements ModelInterface { @@ -142,7 +144,7 @@ export default class ChatboxAI extends AbstractAISDKModel implements ModelInterf responseModalities: ['TEXT', 'IMAGE'], } if (params.aspectRatio && params.aspectRatio !== 'auto') { - providerOptions.imageConfig = { aspectRatio: params.aspectRatio } + providerOptions.imageConfig = { aspectRatio: params.aspectRatio as GoogleImageAspectRatio } } const result = streamText({ diff --git a/src/shared/providers/definitions/models/custom-gemini.ts b/src/shared/providers/definitions/models/custom-gemini.ts index 27e1eceb4..7365a15f1 100644 --- a/src/shared/providers/definitions/models/custom-gemini.ts +++ b/src/shared/providers/definitions/models/custom-gemini.ts @@ -6,6 +6,7 @@ import { ApiError } from '../../../models/errors' import type { CallChatCompletionOptions } from '../../../models/types' import type { ProviderModelInfo } from '../../../types' import type { ModelDependencies } from '../../../types/adapters' +import { normalizeGoogleThinkingConfig } from '../../../utils/google-thinking' import { normalizeGeminiHost } from '../../../utils/llm_utils' const GEMINI_IMAGE_MODELS = [ @@ -15,6 +16,8 @@ const GEMINI_IMAGE_MODELS = [ 'gemini-3.1-flash-image', ] +type GoogleImageAspectRatio = NonNullable['aspectRatio']> + interface Options { apiKey: string apiHost: string @@ -74,7 +77,10 @@ export default class CustomGemini extends AbstractAISDKModel { ...providerParams, ...(options.providerOptions?.google || {}), thinkingConfig: { - ...(options.providerOptions?.google?.thinkingConfig || {}), + ...(normalizeGoogleThinkingConfig( + this.options.model.modelId, + options.providerOptions?.google?.thinkingConfig + ) || {}), includeThoughts: true, }, } @@ -126,7 +132,7 @@ export default class CustomGemini extends AbstractAISDKModel { responseModalities: ['TEXT', 'IMAGE'], } if (params.aspectRatio && params.aspectRatio !== 'auto') { - providerOptions.imageConfig = { aspectRatio: params.aspectRatio } + providerOptions.imageConfig = { aspectRatio: params.aspectRatio as GoogleImageAspectRatio } } const result = await generateText({ diff --git a/src/shared/providers/definitions/models/gemini.ts b/src/shared/providers/definitions/models/gemini.ts index 9033e1e5e..9838c0dc0 100644 --- a/src/shared/providers/definitions/models/gemini.ts +++ b/src/shared/providers/definitions/models/gemini.ts @@ -6,6 +6,7 @@ import { ApiError } from '../../../models/errors' import type { CallChatCompletionOptions } from '../../../models/types' import type { ProviderModelInfo } from '../../../types' import type { ModelDependencies } from '../../../types/adapters' +import { normalizeGoogleThinkingConfig } from '../../../utils/google-thinking' import { normalizeGeminiHost } from '../../../utils/llm_utils' const GEMINI_IMAGE_MODELS = [ @@ -15,6 +16,8 @@ const GEMINI_IMAGE_MODELS = [ 'gemini-3.1-flash-image', ] +type GoogleImageAspectRatio = NonNullable['aspectRatio']> + interface Options { geminiAPIKey: string geminiAPIHost: string @@ -28,7 +31,10 @@ interface Options { export default class Gemini extends AbstractAISDKModel { public name = 'Google Gemini' - constructor(public options: Options, dependencies: ModelDependencies) { + constructor( + public options: Options, + dependencies: ModelDependencies + ) { super(options, dependencies) this.injectDefaultMetadata = false } @@ -70,7 +76,10 @@ export default class Gemini extends AbstractAISDKModel { ...providerParams, ...(options.providerOptions?.google || {}), thinkingConfig: { - ...(options.providerOptions?.google?.thinkingConfig || {}), + ...(normalizeGoogleThinkingConfig( + this.options.model.modelId, + options.providerOptions?.google?.thinkingConfig + ) || {}), includeThoughts: true, }, } @@ -120,7 +129,7 @@ export default class Gemini extends AbstractAISDKModel { responseModalities: ['TEXT', 'IMAGE'], } if (params.aspectRatio && params.aspectRatio !== 'auto') { - providerOptions.imageConfig = { aspectRatio: params.aspectRatio } + providerOptions.imageConfig = { aspectRatio: params.aspectRatio as GoogleImageAspectRatio } } const result = await generateText({ @@ -161,7 +170,7 @@ export default class Gemini extends AbstractAISDKModel { const res = await this.dependencies.request.apiRequest({ url: `${this.options.geminiAPIHost}/v1beta/models?key=${this.options.geminiAPIKey}`, method: 'GET', - headers: {} + headers: {}, }) const json: Response = await res.json() if (!json.models) { diff --git a/src/shared/types/settings.ts b/src/shared/types/settings.ts index bb14ed7fb..d569ccbdb 100644 --- a/src/shared/types/settings.ts +++ b/src/shared/types/settings.ts @@ -101,7 +101,8 @@ const OpenAIParamsSchema = z.object({ const GoogleParamsSchema = z.object({ thinkingConfig: z.object({ - thinkingBudget: z.number().catch(1024), + thinkingBudget: z.number().optional().catch(undefined), + thinkingLevel: z.enum(['minimal', 'low', 'medium', 'high']).optional().catch(undefined), includeThoughts: z.boolean().catch(true), }), }) diff --git a/src/shared/utils/google-thinking.test.ts b/src/shared/utils/google-thinking.test.ts new file mode 100644 index 000000000..6ca57ebee --- /dev/null +++ b/src/shared/utils/google-thinking.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { + getDefaultGoogleThinkingLevel, + getGoogleThinkingMode, + getSupportedGoogleThinkingLevels, + normalizeGoogleThinkingConfig, +} from './google-thinking' + +describe('google-thinking utils', () => { + it('detects the correct thinking mode for Gemini model families', () => { + expect(getGoogleThinkingMode('gemini-2.5-flash')).toBe('budget') + expect(getGoogleThinkingMode('gemini-3-pro-preview')).toBe('level') + expect(getGoogleThinkingMode('gemini-2.0-flash')).toBe('none') + }) + + it('returns the documented thinking levels for supported Gemini 3 models', () => { + expect(getSupportedGoogleThinkingLevels('gemini-3-pro-preview')).toEqual(['low', 'high']) + expect(getSupportedGoogleThinkingLevels('gemini-3-flash-preview')).toEqual(['minimal', 'low', 'medium', 'high']) + expect(getSupportedGoogleThinkingLevels('gemini-3.1-pro-preview')).toEqual(['low', 'medium', 'high']) + expect(getSupportedGoogleThinkingLevels('gemini-3.1-flash-lite-preview')).toEqual([ + 'minimal', + 'low', + 'medium', + 'high', + ]) + expect(getSupportedGoogleThinkingLevels('gemini-3.1-flash-image-preview')).toEqual([]) + }) + + it('uses the highest supported level as the default Gemini 3 thinking level', () => { + expect(getDefaultGoogleThinkingLevel('gemini-3-pro-preview')).toBe('high') + expect(getDefaultGoogleThinkingLevel('gemini-3-flash-preview')).toBe('high') + expect(getDefaultGoogleThinkingLevel('gemini-3.1-flash-image-preview')).toBeUndefined() + }) + + it('preserves valid Gemini 3 thinking levels and drops legacy budgets', () => { + expect( + normalizeGoogleThinkingConfig('gemini-3-pro-preview', { + thinkingLevel: 'low', + includeThoughts: true, + }) + ).toEqual({ + thinkingLevel: 'low', + includeThoughts: true, + }) + + expect( + normalizeGoogleThinkingConfig('gemini-3-flash-preview', { + thinkingBudget: 5120, + includeThoughts: true, + }) + ).toEqual({ + includeThoughts: true, + }) + }) + + it('preserves Gemini 2.5 thinking budgets', () => { + expect( + normalizeGoogleThinkingConfig('gemini-2.5-flash', { + thinkingBudget: 4096, + includeThoughts: true, + }) + ).toEqual({ + thinkingBudget: 4096, + includeThoughts: true, + }) + }) +}) diff --git a/src/shared/utils/google-thinking.ts b/src/shared/utils/google-thinking.ts new file mode 100644 index 000000000..380221b72 --- /dev/null +++ b/src/shared/utils/google-thinking.ts @@ -0,0 +1,84 @@ +export type GoogleThinkingLevel = 'minimal' | 'low' | 'medium' | 'high' +export type GoogleThinkingMode = 'budget' | 'level' | 'none' + +export interface GoogleThinkingConfig { + thinkingBudget?: number + thinkingLevel?: GoogleThinkingLevel + includeThoughts?: boolean +} + +const GOOGLE_THINKING_LEVELS_BY_MODEL: Array<[RegExp, GoogleThinkingLevel[]]> = [ + // Official Gemini thinking docs cover Gemini 3.1 Pro, Gemini 3.1 Flash-Lite, and Gemini 3 Flash. + // The AI SDK provider docs additionally document Gemini 3 Pro as supporting low/high. + [/^gemini-3\.1-pro/i, ['low', 'medium', 'high']], + [/^gemini-3-pro/i, ['low', 'high']], + [/^gemini-3\.1-flash-lite/i, ['minimal', 'low', 'medium', 'high']], + [/^gemini-3-flash/i, ['minimal', 'low', 'medium', 'high']], +] + +export function getGoogleThinkingMode(modelId: string): GoogleThinkingMode { + if (modelId.startsWith('gemini-3')) { + return 'level' + } + + if (modelId.startsWith('gemini-2.5')) { + return 'budget' + } + + return 'none' +} + +export function getSupportedGoogleThinkingLevels(modelId: string): GoogleThinkingLevel[] { + if (getGoogleThinkingMode(modelId) !== 'level') { + return [] + } + + const match = GOOGLE_THINKING_LEVELS_BY_MODEL.find(([pattern]) => pattern.test(modelId)) + + return match?.[1] || [] +} + +export function getDefaultGoogleThinkingLevel(modelId: string): GoogleThinkingLevel | undefined { + const supportedLevels = getSupportedGoogleThinkingLevels(modelId) + + return supportedLevels.at(-1) +} + +export function normalizeGoogleThinkingConfig( + modelId: string, + thinkingConfig?: GoogleThinkingConfig +): GoogleThinkingConfig | undefined { + if (!thinkingConfig) { + return undefined + } + + const mode = getGoogleThinkingMode(modelId) + + if (mode === 'budget') { + return { + ...(thinkingConfig.thinkingBudget !== undefined ? { thinkingBudget: thinkingConfig.thinkingBudget } : {}), + ...(thinkingConfig.includeThoughts !== undefined ? { includeThoughts: thinkingConfig.includeThoughts } : {}), + } + } + + if (mode === 'level') { + const supportedLevels = getSupportedGoogleThinkingLevels(modelId) + const thinkingLevel = thinkingConfig.thinkingLevel + + if (thinkingLevel && (supportedLevels.length === 0 || supportedLevels.includes(thinkingLevel))) { + return { + thinkingLevel, + ...(thinkingConfig.includeThoughts !== undefined ? { includeThoughts: thinkingConfig.includeThoughts } : {}), + } + } + + // Drop legacy Gemini 3 thinking budgets instead of guessing a level. If the user + // has not explicitly selected a level, we let the Google API fall back to the + // model's documented default level. + return thinkingConfig.includeThoughts !== undefined + ? { includeThoughts: thinkingConfig.includeThoughts } + : undefined + } + + return thinkingConfig +} From 5ac4b78e37dd5f9dc6bc9f7cef88e3dc8956f6dc Mon Sep 17 00:00:00 2001 From: MashiroCodfish <106896687+MashiroCodfish@users.noreply.github.com> Date: Tue, 31 Mar 2026 02:38:40 +0000 Subject: [PATCH 2/2] fix: sync Gemini thinking level settings --- src/renderer/modals/SessionSettings.tsx | 58 ++++++++++++++++++++++++ src/shared/utils/google-thinking.test.ts | 9 ++++ src/shared/utils/google-thinking.ts | 2 +- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/renderer/modals/SessionSettings.tsx b/src/renderer/modals/SessionSettings.tsx index ea06555be..a50e51daf 100644 --- a/src/renderer/modals/SessionSettings.tsx +++ b/src/renderer/modals/SessionSettings.tsx @@ -23,6 +23,7 @@ import { type SessionSettings, } from '@shared/types' import { + type GoogleThinkingConfig, type GoogleThinkingLevel, getDefaultGoogleThinkingLevel, getGoogleThinkingMode, @@ -636,6 +637,63 @@ function GoogleProviderConfig({ return getDefaultGoogleThinkingLevel(modelId) }, [modelId, providerOptions?.thinkingConfig?.thinkingLevel, supportedLevels]) + const canonicalThinkingConfig = useMemo(() => { + const thinkingConfig = providerOptions?.thinkingConfig + + if (thinkingMode === 'level') { + if (supportedLevels.length === 0) { + if (!thinkingConfig) { + return undefined + } + + if ( + thinkingConfig.thinkingBudget === undefined && + thinkingConfig.thinkingLevel === undefined && + thinkingConfig.includeThoughts === undefined + ) { + return undefined + } + + return { + includeThoughts: thinkingConfig.includeThoughts ?? true, + } + } + + if (!currentThinkingLevel) { + return undefined + } + + return { + thinkingLevel: currentThinkingLevel, + includeThoughts: thinkingConfig?.includeThoughts ?? true, + } + } + + return undefined + }, [currentThinkingLevel, providerOptions?.thinkingConfig, supportedLevels, thinkingMode]) + + useEffect(() => { + if (thinkingMode !== 'level' || !canonicalThinkingConfig) { + return + } + + const thinkingConfig = providerOptions?.thinkingConfig + + if ( + thinkingConfig?.thinkingBudget === canonicalThinkingConfig.thinkingBudget && + thinkingConfig?.thinkingLevel === canonicalThinkingConfig.thinkingLevel && + thinkingConfig?.includeThoughts === canonicalThinkingConfig.includeThoughts + ) { + return + } + + onSettingsChange({ + providerOptions: { + google: { thinkingConfig: canonicalThinkingConfig }, + }, + }) + }, [canonicalThinkingConfig, onSettingsChange, providerOptions?.thinkingConfig, thinkingMode]) + if (thinkingMode === 'level' && currentThinkingLevel) { return ( { ).toEqual({ includeThoughts: true, }) + + expect( + normalizeGoogleThinkingConfig('gemini-3.1-flash-image-preview', { + thinkingLevel: 'high', + includeThoughts: true, + }) + ).toEqual({ + includeThoughts: true, + }) }) it('preserves Gemini 2.5 thinking budgets', () => { diff --git a/src/shared/utils/google-thinking.ts b/src/shared/utils/google-thinking.ts index 380221b72..93defb72e 100644 --- a/src/shared/utils/google-thinking.ts +++ b/src/shared/utils/google-thinking.ts @@ -65,7 +65,7 @@ export function normalizeGoogleThinkingConfig( const supportedLevels = getSupportedGoogleThinkingLevels(modelId) const thinkingLevel = thinkingConfig.thinkingLevel - if (thinkingLevel && (supportedLevels.length === 0 || supportedLevels.includes(thinkingLevel))) { + if (thinkingLevel && supportedLevels.includes(thinkingLevel)) { return { thinkingLevel, ...(thinkingConfig.includeThoughts !== undefined ? { includeThoughts: thinkingConfig.includeThoughts } : {}),