From e724c63a35cf2559cc2ae49239af0913ede39eda Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:02:48 +0900 Subject: [PATCH 1/2] fix(google): preserve stream signature source order --- src/adapters/google.ts | 4 +- ...google-signature-history-roundtrip.test.ts | 70 ++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 746f9490a4..d004b9bf87 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -964,7 +964,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession; if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex") && parts && replayModel && replaySession) { - pendingStreamThoughtSig = observeAntigravityReplay( + // Observation may scan the whole frame, so use it only for replay-cache side effects. + // The source-order loop below exclusively owns stream carry and cannot pair backwards. + observeAntigravityReplay( replayModel, replaySession, parts as unknown[], diff --git a/tests/google-signature-history-roundtrip.test.ts b/tests/google-signature-history-roundtrip.test.ts index 571ce27c1a..495925f450 100644 --- a/tests/google-signature-history-roundtrip.test.ts +++ b/tests/google-signature-history-roundtrip.test.ts @@ -33,6 +33,13 @@ const provider = { apiKey: "vertex-test-key", } as OcxProviderConfig; +const aiStudioProvider = { + adapter: "google", + googleMode: "ai-studio", + baseUrl: "https://generativelanguage.googleapis.com", + apiKey: "ai-studio-test-key", +} as OcxProviderConfig; + /** * A replay scope is now REQUIRED for the store to remember or return anything: a @@ -89,6 +96,18 @@ function modelParts(body: string): Record[] { return parsed.contents.find(content => content.role === "model")?.parts ?? []; } +/** Build a streaming response whose SSE frames remain distinct transport chunks. */ +function sseResponse(frames: string[]): Response { + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + for (const frame of frames) controller.enqueue(encoder.encode(frame)); + controller.close(); + }, + }); + return new Response(stream); +} + describe("#1735 thought signature survives history replay", () => { let previousHome: string | undefined; let testDir: string; @@ -175,16 +194,8 @@ describe("#1735 thought signature survives history replay", () => { `data: ${JSON.stringify({ usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 2 } })}\n\n`, ]; - const stream = new ReadableStream({ - start(controller) { - const encoder = new TextEncoder(); - for (const frame of frames) controller.enqueue(encoder.encode(frame)); - controller.close(); - }, - }); - const events: AdapterEvent[] = []; - for await (const event of adapter.parseStream(new Response(stream))) { + for await (const event of adapter.parseStream(sseResponse(frames))) { events.push(event); } @@ -199,6 +210,47 @@ describe("#1735 thought signature survives history replay", () => { .toBe(SIGNATURE); }); + test("streaming signatures only attach to function calls that follow them in the same frame", async () => { + const adapter = createGoogleAdapter(provider); + await adapter.buildRequest(firstTurn()); + const frames = [ + `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(sseResponse(frames))) events.push(event); + + const signatures = events + .filter((event): event is Extract => + event.type === "tool_call_start") + .map(event => event.providerMetadata?.google?.thoughtSignature); + expect(signatures).toEqual([undefined, SIGNATURE]); + }); + + test("AI Studio keeps source-order thought signature carry across stream frames", async () => { + const adapter = createGoogleAdapter(aiStudioProvider); + await adapter.buildRequest(firstTurn()); + const frames = [ + `data: ${JSON.stringify(googleBody([ + { text: "thinking...", thought: true, thought_signature: SIGNATURE }, + ]))}\n\n`, + `data: ${JSON.stringify(googleBody([ + { functionCall: { name: "shell_command", args: { command: "pwd" } } }, + ]))}\n\n`, + ]; + + const events: AdapterEvent[] = []; + for await (const event of adapter.parseStream(sseResponse(frames))) events.push(event); + + const start = events.find((event): event is Extract => + event.type === "tool_call_start"); + expect(start?.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({ From 4fb942d4d5231bc678d47846356c6b7321d1ece8 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:16:42 +0900 Subject: [PATCH 2/2] test(google): keep carry fixture non-terminal --- tests/google-signature-history-roundtrip.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/google-signature-history-roundtrip.test.ts b/tests/google-signature-history-roundtrip.test.ts index 495925f450..40f23154e8 100644 --- a/tests/google-signature-history-roundtrip.test.ts +++ b/tests/google-signature-history-roundtrip.test.ts @@ -235,9 +235,14 @@ describe("#1735 thought signature survives history replay", () => { const adapter = createGoogleAdapter(aiStudioProvider); await adapter.buildRequest(firstTurn()); const frames = [ - `data: ${JSON.stringify(googleBody([ - { text: "thinking...", thought: true, thought_signature: SIGNATURE }, - ]))}\n\n`, + `data: ${JSON.stringify({ + candidates: [{ + content: { + role: "model", + parts: [{ text: "thinking...", thought: true, thought_signature: SIGNATURE }], + }, + }], + })}\n\n`, `data: ${JSON.stringify(googleBody([ { functionCall: { name: "shell_command", args: { command: "pwd" } } }, ]))}\n\n`,