From f45bb05b07e23501fb1f0ecf9d591dc06ed2b59e Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 15 Jul 2026 12:45:06 +0300 Subject: [PATCH] fix: set Google thinking config by Gemini model version Uses thinkingLevel for Gemini 3 Flash models Keeps older Gemini Flash models on thinkingBudget: 0 Updates docs and tests for the new Google request payload --- .../server/lib/small-model/DOCUMENTATION.md | 3 +- packages/web/server/lib/small-model/call.js | 7 ++- .../web/server/lib/small-model/call.test.js | 56 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/web/server/lib/small-model/DOCUMENTATION.md b/packages/web/server/lib/small-model/DOCUMENTATION.md index 7c103a13..56769495 100644 --- a/packages/web/server/lib/small-model/DOCUMENTATION.md +++ b/packages/web/server/lib/small-model/DOCUMENTATION.md @@ -45,7 +45,8 @@ other runtime API. `ChatGPT-Account-Id`; expired tokens are refreshed against `auth.openai.com` (single-flight) and written back to `auth.json`. - **Anthropic** (`type: api`): `/v1/messages` with `x-api-key`. - - **Google** (`type: api`): `generateContent` with `x-goog-api-key`. + - **Google** (`type: api`): `generateContent` with `x-goog-api-key`; Gemini 3 + uses `thinkingLevel` while older Flash models use `thinkingBudget: 0`. - Everything else: OpenAI-compatible `/chat/completions` against the provider's base URL, resolved from (1) `provider..options.baseURL` in the OpenCode config, (2) the hardcoded `https://api.openai.com/v1` diff --git a/packages/web/server/lib/small-model/call.js b/packages/web/server/lib/small-model/call.js index 858fba9b..b67e3343 100644 --- a/packages/web/server/lib/small-model/call.js +++ b/packages/web/server/lib/small-model/call.js @@ -213,6 +213,9 @@ const callAnthropic = async ({ apiKey, modelID, prompt, system, maxOutputTokens const callGoogle = async ({ apiKey, modelID, prompt, system, maxOutputTokens }) => { const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelID)}:generateContent`; + const thinkingConfig = modelID.toLowerCase().startsWith('gemini-3') + ? { thinkingLevel: modelID.toLowerCase().includes('flash') ? 'minimal' : 'low' } + : { thinkingBudget: 0 }; const response = await fetch(url, { method: 'POST', headers: { @@ -223,9 +226,7 @@ const callGoogle = async ({ apiKey, modelID, prompt, system, maxOutputTokens }) body: JSON.stringify({ contents: [{ role: 'user', parts: [{ text: prompt }] }], ...(system ? { systemInstruction: { parts: [{ text: system }] } } : {}), - // thinkingBudget 0 switches Gemini Flash thinking off; Flash is the only - // family the small-model resolver picks for Google. - generationConfig: { maxOutputTokens, thinkingConfig: { thinkingBudget: 0 } }, + generationConfig: { maxOutputTokens, thinkingConfig }, }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); diff --git a/packages/web/server/lib/small-model/call.test.js b/packages/web/server/lib/small-model/call.test.js index b09d29a2..34d73b16 100644 --- a/packages/web/server/lib/small-model/call.test.js +++ b/packages/web/server/lib/small-model/call.test.js @@ -336,3 +336,59 @@ describe('callSmallModel — custom provider config', () => { }); }); }); + +describe('callSmallModel — Google thinking configuration', () => { + let fetchMock; + let originalFetch; + + beforeEach(() => { + fetchMock = vi.fn(); + originalFetch = globalThis.fetch; + globalThis.fetch = fetchMock; + readConfig.mockReset(); + readConfig.mockReturnValue({}); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + const googleResponse = (text) => ({ + ok: true, + status: 200, + json: async () => ({ candidates: [{ content: { parts: [{ text }] } }] }), + }); + + it('uses thinkingLevel for Gemini 3 Flash models', async () => { + fetchMock.mockResolvedValue(googleResponse('generated commit')); + + const text = await callSmallModel({ + auth: { google: { type: 'api', key: 'google-key' } }, + catalog: {}, + workingDirectory: '/proj', + providerID: 'google', + modelID: 'gemini-3.1-flash-lite-preview', + prompt: 'generate', + }); + + expect(text).toBe('generated commit'); + const body = JSON.parse(lastCall(fetchMock).init.body); + expect(body.generationConfig.thinkingConfig).toEqual({ thinkingLevel: 'minimal' }); + }); + + it('keeps thinkingBudget disabled for Gemini 2.5 Flash models', async () => { + fetchMock.mockResolvedValue(googleResponse('generated commit')); + + await callSmallModel({ + auth: { google: { type: 'api', key: 'google-key' } }, + catalog: {}, + workingDirectory: '/proj', + providerID: 'google', + modelID: 'gemini-2.5-flash-lite', + prompt: 'generate', + }); + + const body = JSON.parse(lastCall(fetchMock).init.body); + expect(body.generationConfig.thinkingConfig).toEqual({ thinkingBudget: 0 }); + }); +});