Skip to content
Draft
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
17 changes: 17 additions & 0 deletions src/responses/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,23 @@ export function parseRequest(
};
}

/**
* Restore provider-opaque signatures after routing has supplied the complete replay scope.
* The initial request parse necessarily happens before provider and credential selection, so
* production callers use this once the route-bound scope has been populated.
*/
export function hydrateReplayThoughtSignatures(parsed: OcxParsedRequest): void {
const scope = parsed._reasoningReplayScope;
for (const message of parsed.context.messages) {
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
for (const part of message.content) {
if (part.type !== "toolCall" || part.providerMetadata || !part.id) continue;
const remembered = replayThoughtSignatureMetadata(part.id, scope);
if (remembered) part.providerMetadata = remembered;
}
}
}

/**
* The Responses `text.format` object when it requests structured output (json_schema or
* json_object), undefined otherwise. Acceptance is identical to the boolean detector this
Expand Down
3 changes: 2 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
multiAgentGuidanceEnabled,
resolveEnvValue,
} from "../../config";
import { parseRequest } from "../../responses/parser";
import { hydrateReplayThoughtSignatures, parseRequest } from "../../responses/parser";
import {
bindReasoningReplayScope,
reasoningReplayCodexCredentialIdentity,
Expand Down Expand Up @@ -2148,6 +2148,7 @@ async function handleResponsesInner(
codexAuthContext: authCtx,
forwardHeaders: selectedForwardHeaders,
});
hydrateReplayThoughtSignatures(parsed);
logCtx.providerAdapter = adapter.name;
// Ordinary requests receive one durable attempt only after their final initial
// adapter is resolved. Combo children own their attempt and retries keep it.
Expand Down
21 changes: 20 additions & 1 deletion tests/google-signature-history-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { createGoogleAdapter as createGoogleAdapterProduction } from "../src/adapters/google";
import { __resetAntigravityReplayCache } from "../src/adapters/google-antigravity-replay";
import { parseRequest } from "../src/responses/parser";
import { hydrateReplayThoughtSignatures, parseRequest } from "../src/responses/parser";
import {
flushThoughtSignatureReplayForTests,
lookupReplayThoughtSignature,
Expand Down Expand Up @@ -179,6 +179,25 @@ describe("#1735 thought signature survives history replay", () => {
expect(part?.thoughtSignature).toBe(SIGNATURE);
});

test("the server can restore a signature after its route scope is bound", async () => {
const scope = scopeFor();
rememberThoughtSignatureForReplay("call_shell_late", SIGNATURE, scope);
const parsed = parseRequest({
model: MODEL,
input: [
{ type: "function_call", call_id: "call_shell_late", name: "shell_command", arguments: "{}" },
{ type: "function_call_output", call_id: "call_shell_late", output: "/workspace" },
],
});

parsed._reasoningReplayScope = scope;
hydrateReplayThoughtSignatures(parsed);

const request = await createGoogleAdapter(provider).buildRequest(parsed);
const part = modelParts(request.body as string).find(candidate => "functionCall" in candidate);
expect(part?.thoughtSignature).toBe(SIGNATURE);
});

test("a custom_tool_call replay is re-signed from the proxy-side store", async () => {
rememberThoughtSignatureForReplay("call_custom_1", SIGNATURE_B, scopeFor());
const parsed = parseRequestScoped({
Expand Down
Loading