Skip to content
Merged
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
4 changes: 3 additions & 1 deletion src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
75 changes: 66 additions & 9 deletions tests/google-signature-history-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +96,18 @@ function modelParts(body: string): Record<string, unknown>[] {
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<Uint8Array>({
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;
Expand Down Expand Up @@ -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<Uint8Array>({
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);
}

Expand All @@ -199,6 +210,52 @@ 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<AdapterEvent, { type: "tool_call_start" }> =>
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({
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`,
];

const events: AdapterEvent[] = [];
for await (const event of adapter.parseStream(sseResponse(frames))) events.push(event);

const start = events.find((event): event is Extract<AdapterEvent, { type: "tool_call_start" }> =>
event.type === "tool_call_start");
expect(start?.providerMetadata?.google?.thoughtSignature).toBe(SIGNATURE);
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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