fix(core): preserve functionCall thoughtSignature when stripping thought parts (#28607)

Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
Mpider-San
2026-08-06 03:25:32 +05:30
committed by GitHub
parent 348fc35f17
commit 63c5b74770
2 changed files with 93 additions and 1 deletions
+69
View File
@@ -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', () => {
+24 -1
View File
@@ -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,
},
};
})