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
This commit is contained in:
Bohdan Triapitsyn
2026-07-15 12:45:06 +03:00
parent 2b5e9a0221
commit f45bb05b07
3 changed files with 62 additions and 4 deletions
@@ -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.<id>.options.baseURL`
in the OpenCode config, (2) the hardcoded `https://api.openai.com/v1`
+4 -3
View File
@@ -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),
});
@@ -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 });
});
});