Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,9 +698,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
// Antigravity use separate model namespaces so opaque provider state cannot cross routes.
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
let observedStreamThoughtSig = pendingStreamThoughtSig;
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
&& parts && replayModel && replaySession) {
pendingStreamThoughtSig = observeAntigravityReplay(
observedStreamThoughtSig = observeAntigravityReplay(
replayModel,
replaySession,
parts as unknown[],
Expand Down Expand Up @@ -747,6 +748,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
yield { type: "tool_call_end" };
}
}
// The observer scans the complete frame to populate replay state. Publish its carried
// signature only after emitting parts in source order, so a later thought part cannot
// supply fallback metadata to an earlier function call in the same frame.
pendingStreamThoughtSig = observedStreamThoughtSig;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve signature carry when replay observation is inactive

For streaming AI Studio requests, the replay observer is skipped by the googleMode guard, so observedStreamThoughtSig retains the value from before this frame. After the loop records a standalone thought signature in pendingStreamThoughtSig, this assignment immediately resets it; if the corresponding unsigned function call arrives in a later SSE frame, its tool_call_start lacks the required Google signature metadata and the subsequent tool-result turn can be rejected by Gemini. Keep the source-order value computed by the loop authoritative (and invoke the observer only for its replay-cache side effect), including for modes such as Claude-on-Antigravity where the observer returns without scanning.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

}
return emittedContentEvent ? "content" : "continue";
};
Expand Down
18 changes: 18 additions & 0 deletions tests/google-signature-history-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ describe("#1735 thought signature survives history replay", () => {
.toBe(SIGNATURE);
});

test("streaming signatures only attach to function calls that follow them", async () => {
const adapter = createGoogleAdapter(provider);
await adapter.buildRequest(firstTurn());
const frame = `data: ${JSON.stringify(googleBody([
{ functionCall: { name: "shell_command", args: { command: "pwd" } } },
{ text: "thinking...", thought: true, thought_signature: SIGNATURE },
{ functionCall: { name: "shell_command", args: { command: "ls" } } },
]))}\n\n`;
const events: AdapterEvent[] = [];
for await (const event of adapter.parseStream(new Response(frame))) events.push(event);

const starts = events.filter((event): event is Extract<AdapterEvent, { type: "tool_call_start" }> =>
event.type === "tool_call_start");
expect(starts).toHaveLength(2);
expect(starts[0].providerMetadata?.google?.thoughtSignature).toBeUndefined();
expect(starts[1].providerMetadata?.google?.thoughtSignature).toBe(SIGNATURE);
});

test("a signature replayed through Responses history reaches the rebuilt Google part", async () => {
// No cache is warmed here: this is a cold process replaying client-supplied history.
const parsed = parseRequestScoped({
Expand Down
Loading