Skip to content

Commit 059929c

Browse files
authored
bug(core): Ensure we use thinking budget on fallback to 2.5 (#13596)
1 parent cdd9f67 commit 059929c

File tree

2 files changed

+151
-9
lines changed

2 files changed

+151
-9
lines changed

packages/core/src/core/geminiChat.test.ts

Lines changed: 142 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { setSimulate429 } from '../utils/testUtils.js';
2020
import {
2121
DEFAULT_GEMINI_FLASH_MODEL,
2222
DEFAULT_GEMINI_MODEL,
23+
DEFAULT_THINKING_MODE,
2324
PREVIEW_GEMINI_MODEL,
2425
} from '../config/models.js';
2526
import { AuthType } from './contentGenerator.js';
@@ -131,15 +132,22 @@ describe('GeminiChat', () => {
131132
getContentGenerator: vi.fn().mockReturnValue(mockContentGenerator),
132133
getRetryFetchErrors: vi.fn().mockReturnValue(false),
133134
modelConfigService: {
134-
getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => ({
135-
model: modelConfigKey.model,
136-
generateContentConfig: {
137-
temperature: 0,
138-
thinkingConfig: {
139-
thinkingBudget: 1000,
135+
getResolvedConfig: vi.fn().mockImplementation((modelConfigKey) => {
136+
const thinkingConfig = modelConfigKey.model.startsWith('gemini-3')
137+
? {
138+
thinkingLevel: ThinkingLevel.HIGH,
139+
}
140+
: {
141+
thinkingBudget: DEFAULT_THINKING_MODE,
142+
};
143+
return {
144+
model: modelConfigKey.model,
145+
generateContentConfig: {
146+
temperature: 0,
147+
thinkingConfig,
140148
},
141-
},
142-
})),
149+
};
150+
}),
143151
},
144152
isPreviewModelBypassMode: vi.fn().mockReturnValue(false),
145153
setPreviewModelBypassMode: vi.fn(),
@@ -976,7 +984,7 @@ describe('GeminiChat', () => {
976984
tools: [],
977985
temperature: 0,
978986
thinkingConfig: {
979-
thinkingBudget: 1000,
987+
thinkingBudget: DEFAULT_THINKING_MODE,
980988
},
981989
abortSignal: expect.any(AbortSignal),
982990
},
@@ -1023,6 +1031,45 @@ describe('GeminiChat', () => {
10231031
'prompt-id-thinking-level',
10241032
);
10251033
});
1034+
1035+
it('should use thinkingBudget and remove thinkingLevel for non-gemini-3 models', async () => {
1036+
const response = (async function* () {
1037+
yield {
1038+
candidates: [
1039+
{
1040+
content: { parts: [{ text: 'response' }], role: 'model' },
1041+
finishReason: 'STOP',
1042+
},
1043+
],
1044+
} as unknown as GenerateContentResponse;
1045+
})();
1046+
vi.mocked(mockContentGenerator.generateContentStream).mockResolvedValue(
1047+
response,
1048+
);
1049+
1050+
const stream = await chat.sendMessageStream(
1051+
{ model: 'gemini-2.0-flash' },
1052+
'hello',
1053+
'prompt-id-thinking-budget',
1054+
new AbortController().signal,
1055+
);
1056+
for await (const _ of stream) {
1057+
// consume stream
1058+
}
1059+
1060+
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledWith(
1061+
expect.objectContaining({
1062+
model: 'gemini-2.0-flash',
1063+
config: expect.objectContaining({
1064+
thinkingConfig: {
1065+
thinkingBudget: DEFAULT_THINKING_MODE,
1066+
thinkingLevel: undefined,
1067+
},
1068+
}),
1069+
}),
1070+
'prompt-id-thinking-budget',
1071+
);
1072+
});
10261073
});
10271074

10281075
describe('addHistory', () => {
@@ -1902,6 +1949,92 @@ describe('GeminiChat', () => {
19021949
expect(modelTurn.parts![0]!.text).toBe('Success on retry');
19031950
});
19041951

1952+
it('should switch to DEFAULT_GEMINI_FLASH_MODEL and use thinkingBudget when falling back from a gemini-3 model', async () => {
1953+
// ARRANGE
1954+
const authType = AuthType.LOGIN_WITH_GOOGLE;
1955+
vi.mocked(mockConfig.getContentGeneratorConfig).mockReturnValue({
1956+
authType,
1957+
});
1958+
1959+
// Initial state: Not in fallback mode
1960+
const isInFallbackModeSpy = vi.spyOn(mockConfig, 'isInFallbackMode');
1961+
isInFallbackModeSpy.mockReturnValue(false);
1962+
1963+
// Mock API calls:
1964+
// 1. Fails with 429 (simulating gemini-3 failure)
1965+
// 2. Succeeds (simulating fallback success)
1966+
vi.mocked(mockContentGenerator.generateContentStream)
1967+
.mockRejectedValueOnce(error429)
1968+
.mockResolvedValueOnce(
1969+
(async function* () {
1970+
yield {
1971+
candidates: [
1972+
{
1973+
content: { parts: [{ text: 'Fallback success' }] },
1974+
finishReason: 'STOP',
1975+
},
1976+
],
1977+
} as unknown as GenerateContentResponse;
1978+
})(),
1979+
);
1980+
1981+
// Mock handleFallback to enable fallback mode and signal retry
1982+
mockHandleFallback.mockImplementation(async () => {
1983+
isInFallbackModeSpy.mockReturnValue(true); // Next call will see fallback mode = true
1984+
return true;
1985+
});
1986+
1987+
// ACT
1988+
const stream = await chat.sendMessageStream(
1989+
{ model: 'gemini-3-test-model' }, // Start with a gemini-3 model
1990+
'test fallback thinking',
1991+
'prompt-id-fb3',
1992+
new AbortController().signal,
1993+
);
1994+
for await (const _ of stream) {
1995+
// consume stream
1996+
}
1997+
1998+
// ASSERT
1999+
expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes(
2000+
2,
2001+
);
2002+
2003+
// First call: gemini-3 model, thinkingLevel set
2004+
expect(
2005+
mockContentGenerator.generateContentStream,
2006+
).toHaveBeenNthCalledWith(
2007+
1,
2008+
expect.objectContaining({
2009+
model: 'gemini-3-test-model',
2010+
config: expect.objectContaining({
2011+
thinkingConfig: {
2012+
thinkingBudget: undefined,
2013+
thinkingLevel: ThinkingLevel.HIGH,
2014+
},
2015+
}),
2016+
}),
2017+
'prompt-id-fb3',
2018+
);
2019+
2020+
// Second call: DEFAULT_GEMINI_FLASH_MODEL (due to fallback), thinkingBudget set (due to fix)
2021+
expect(
2022+
mockContentGenerator.generateContentStream,
2023+
).toHaveBeenNthCalledWith(
2024+
2,
2025+
expect.objectContaining({
2026+
model: DEFAULT_GEMINI_FLASH_MODEL,
2027+
config: expect.objectContaining({
2028+
thinkingConfig: {
2029+
thinkingBudget: DEFAULT_THINKING_MODE,
2030+
thinkingLevel: undefined,
2031+
},
2032+
}),
2033+
}),
2034+
'prompt-id-fb3',
2035+
);
2036+
});
2037+
19052038
it('should stop retrying if handleFallback returns false (e.g., auth intent)', async () => {
19062039
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-pro');
19072040
vi.mocked(mockContentGenerator.generateContentStream).mockRejectedValue(

packages/core/src/core/geminiChat.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { retryWithBackoff } from '../utils/retry.js';
2222
import type { Config } from '../config/config.js';
2323
import {
2424
DEFAULT_GEMINI_MODEL,
25+
DEFAULT_THINKING_MODE,
2526
PREVIEW_GEMINI_MODEL,
2627
getEffectiveModel,
2728
isGemini2Model,
@@ -428,6 +429,14 @@ export class GeminiChat {
428429
thinkingLevel: ThinkingLevel.HIGH,
429430
};
430431
delete config.thinkingConfig?.thinkingBudget;
432+
} else {
433+
// The `gemini-3` configs use thinkingLevel, so we have to invert the
434+
// change above.
435+
config.thinkingConfig = {
436+
...config.thinkingConfig,
437+
thinkingBudget: DEFAULT_THINKING_MODE,
438+
};
439+
delete config.thinkingConfig?.thinkingLevel;
431440
}
432441

433442
return this.config.getContentGenerator().generateContentStream(

0 commit comments

Comments
 (0)