mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-08 16:11:58 -07:00
fix(core): preserve functionCall thoughtSignature when stripping thought parts (#28607)
Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
@@ -3433,6 +3433,75 @@ describe('GeminiChat', () => {
|
||||
expect(turns[0].content.parts![0].text).toBe('Question 1');
|
||||
expect(turns[0].content.parts![1].text).toBe('Question 2');
|
||||
});
|
||||
|
||||
it('should inject a synthetic thoughtSignature onto a functionCall left signature-less after stripping a thought part that carried it (regression test for #28604)', () => {
|
||||
vi.mocked(mockConfig.isContextManagementEnabled).mockReturnValue(false);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-2.5-pro');
|
||||
|
||||
chat.setHistory([
|
||||
{ role: 'user', parts: [{ text: 'activate the skill' }] },
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
text: 'internal monologue',
|
||||
thought: true,
|
||||
thoughtSignature: 'real-sig-from-api',
|
||||
} as unknown as Part,
|
||||
{
|
||||
functionCall: { name: 'activate_skill', args: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{ functionResponse: { name: 'activate_skill', response: {} } },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const turns = chat.getHistoryTurns(true);
|
||||
|
||||
const modelTurn = turns[1];
|
||||
expect(modelTurn.content.parts).toHaveLength(1);
|
||||
expect(modelTurn.content.parts![0].functionCall?.name).toBe(
|
||||
'activate_skill',
|
||||
);
|
||||
expect(modelTurn.content.parts![0].thoughtSignature).toBe(
|
||||
SYNTHETIC_THOUGHT_SIGNATURE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should leave an existing thoughtSignature on a functionCall untouched when stripping thoughts', () => {
|
||||
vi.mocked(mockConfig.isContextManagementEnabled).mockReturnValue(false);
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('gemini-2.5-pro');
|
||||
|
||||
chat.setHistory([
|
||||
{ role: 'user', parts: [{ text: 'activate the skill' }] },
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
text: 'internal monologue',
|
||||
thought: true,
|
||||
thoughtSignature: 'real-sig-from-api',
|
||||
} as unknown as Part,
|
||||
{
|
||||
functionCall: { name: 'activate_skill', args: {} },
|
||||
thoughtSignature: 'existing-sig-on-call',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const turns = chat.getHistoryTurns(true);
|
||||
|
||||
const modelTurn = turns[1];
|
||||
expect(modelTurn.content.parts![0].thoughtSignature).toBe(
|
||||
'existing-sig-on-call',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureActiveLoopHasThoughtSignatures', () => {
|
||||
|
||||
@@ -1687,11 +1687,34 @@ export function stripThoughts(history: HistoryTurn[]): HistoryTurn[] {
|
||||
if (!hasThought) return turn;
|
||||
|
||||
const nonThoughtParts = turn.content.parts.filter((p) => p && !p.thought);
|
||||
|
||||
// The thoughtSignature the API requires on the first functionCall of a
|
||||
// model turn is sometimes only carried by the thought part we just
|
||||
// removed, not by the functionCall part itself. Without it, replaying
|
||||
// this turn in a later request gets rejected with a 400 "missing
|
||||
// thought_signature" error, so inject a synthetic one if needed.
|
||||
let patchedFirstCall = false;
|
||||
const finalParts =
|
||||
turn.content.role === 'model'
|
||||
? nonThoughtParts.map((p) => {
|
||||
if (!patchedFirstCall && p.functionCall) {
|
||||
patchedFirstCall = true;
|
||||
if (!p.thoughtSignature) {
|
||||
return {
|
||||
...p,
|
||||
thoughtSignature: SYNTHETIC_THOUGHT_SIGNATURE,
|
||||
};
|
||||
}
|
||||
}
|
||||
return p;
|
||||
})
|
||||
: nonThoughtParts;
|
||||
|
||||
return {
|
||||
...turn,
|
||||
content: {
|
||||
...turn.content,
|
||||
parts: nonThoughtParts,
|
||||
parts: finalParts,
|
||||
},
|
||||
};
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user