From 5da28834009aba8d727f4bf31ddccc2fcdc1dcf9 Mon Sep 17 00:00:00 2001 From: maosisheng Date: Sun, 20 Sep 2026 10:27:25 -0700 Subject: [PATCH 1/5] fix(responses): lower undeclared historical custom tools when the destination denies them Routed lowering collected only current custom declarations, so a compacted or replayed custom_tool_call leaked to xAI-like gateways as the native item type and came back as a misleading 422 missing id. Convert protocol-history items from the top-level input without expanding the live catalog, request full replay for orphan results, and fail closed before serializing leftovers. Co-authored-by: Cursor --- src/adapters/openai-responses/passthrough.ts | 5 +- src/responses/custom-tool-compat.ts | 165 ++++++++++++- src/server/responses/passthrough-dispatch.ts | 8 +- tests/responses/custom-tool-compat.test.ts | 161 ++++++++++++- .../openai-responses-passthrough.test.ts | 219 ++++++++++++++++++ 5 files changed, 549 insertions(+), 9 deletions(-) diff --git a/src/adapters/openai-responses/passthrough.ts b/src/adapters/openai-responses/passthrough.ts index cadcb2a38c2..c35553448c0 100644 --- a/src/adapters/openai-responses/passthrough.ts +++ b/src/adapters/openai-responses/passthrough.ts @@ -15,7 +15,7 @@ import { isOpenAiOperatedResponsesDestination, } from "../../providers/openai-tiers"; import type { TranslatorBudget } from "../../lib/translator-budget"; -import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat"; +import { rewriteRoutedCustomToolsForUpstream, validateFinalCustomToolCompatibility } from "../../responses/custom-tool-compat"; import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat"; import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat"; import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair"; @@ -503,6 +503,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): // HTTP and the WebSocket outbound, because the WS path transports this same request // instead of rebuilding it. observeOutbound(parsed._rawBody, finalBody, headers); + if (!isCanonicalOpenAiForwardProvider(provider)) { + validateFinalCustomToolCompatibility(finalBody, provider.supportsResponsesCustomTools); + } const body = JSON.stringify(finalBody); const releaseBodyObservation = translatorBudget.observeExternallyCapped( "passthrough_serialization", diff --git a/src/responses/custom-tool-compat.ts b/src/responses/custom-tool-compat.ts index 399dba6b2e2..e01aa2aad2e 100644 --- a/src/responses/custom-tool-compat.ts +++ b/src/responses/custom-tool-compat.ts @@ -241,6 +241,147 @@ function rewriteForUpstream( return changed ? next : value; } +/** Request-layer compatibility failure. Callers map this to HTTP 400, never an unhandled 500. */ +export class RoutedCustomToolCompatError extends Error { + readonly code = "custom_tool_compat"; + constructor( + readonly stage: string, + readonly itemType: string, + ) { + super(`custom_tool_compat: ${stage}: ${itemType}`); + this.name = "RoutedCustomToolCompatError"; + } +} + +function collectDeclaredFunctionWireNames(body: unknown): Set { + const names = new Set(); + const register = (tool: unknown, namespace?: string): void => { + if (!isPlainObject(tool) || tool.type !== "function" || typeof tool.name !== "string") return; + names.add(customToolWireName(namespace, tool.name)); + }; + for (const group of collectResponsesToolGroups(body)) { + for (const tool of group) { + if (!isPlainObject(tool)) continue; + if (tool.type === "namespace" && typeof tool.name === "string" && Array.isArray(tool.tools)) { + for (const child of tool.tools) register(child, tool.name); + continue; + } + register(tool); + } + } + return names; +} + +function historicalCallIdentity( + item: Record, +): { name: string; namespace?: string } | undefined { + if (typeof item.name !== "string" || item.name.length === 0) return undefined; + return { + name: item.name, + ...(typeof item.namespace === "string" ? { namespace: item.namespace } : {}), + }; +} + +function sameHistoricalIdentity( + left: { name: string; namespace?: string }, + right: { name: string; namespace?: string }, +): boolean { + return left.name === right.name && left.namespace === right.namespace; +} + +/** + * Convert remaining protocol-history custom items when the destination has denied native custom + * tools. Walks only the top-level `input` array so tool-output JSON cannot be rewritten, and does + * not merge historical names into the live declaration / restore sets. + */ +function rewriteHistoricalCustomItems( + body: unknown, + declaredFunctionWireNames: ReadonlySet, +): unknown { + if (!isPlainObject(body) || !Array.isArray(body.input)) return body; + + const calls = new Map(); + for (const item of body.input) { + if (!isPlainObject(item)) continue; + if ( + (item.type !== "custom_tool_call" && item.type !== "function_call") + || typeof item.call_id !== "string" + || item.call_id.length === 0 + ) continue; + const identity = historicalCallIdentity(item); + if (!identity) { + if (item.type === "custom_tool_call") { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + } + continue; + } + const existing = calls.get(item.call_id); + if (existing && !sameHistoricalIdentity(existing, identity)) { + throw new RoutedCustomToolCompatError("historical_item", "call_id"); + } + calls.set(item.call_id, identity); + } + + let changed = false; + const input = body.input.map(item => { + if (!isPlainObject(item)) return item; + if (item.type === "custom_tool_call") { + if (typeof item.name !== "string" || item.name.length === 0 || typeof item.input !== "string") { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + } + const wireName = customToolWireName( + typeof item.namespace === "string" ? item.namespace : undefined, + item.name, + ); + if (declaredFunctionWireNames.has(wireName)) { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + } + const { input: rawInput, id: _id, ...rest } = item; + changed = true; + return { + ...rest, + type: "function_call", + arguments: JSON.stringify({ input: rawInput }), + }; + } + if ( + item.type === "custom_tool_call_output" + && typeof item.call_id === "string" + && calls.has(item.call_id) + ) { + changed = true; + return { ...item, type: "function_call_output" }; + } + return item; + }); + return changed ? { ...body, input } : body; +} + +export function validateFinalCustomToolCompatibility( + body: unknown, + supportsResponsesCustomTools?: boolean, +): void { + if (supportsResponsesCustomTools !== false || !isPlainObject(body)) return; + + const rejectCustomDeclaration = (tool: unknown): void => { + if (!isPlainObject(tool)) return; + if (tool.type === "custom") throw new RoutedCustomToolCompatError("final_guard", "custom"); + if (tool.type === "namespace" && Array.isArray(tool.tools)) { + for (const child of tool.tools) rejectCustomDeclaration(child); + } + }; + for (const group of collectResponsesToolGroups(body)) { + for (const tool of group) rejectCustomDeclaration(tool); + } + if (!Array.isArray(body.input)) return; + for (const item of body.input) { + if (!isPlainObject(item) || typeof item.type !== "string") continue; + if (item.type === "custom_tool_call" || item.type === "custom_tool_call_output") { + throw new RoutedCustomToolCompatError("final_guard", item.type); + } + } +} + export function rewriteRoutedCustomToolsForUpstream( body: unknown, supportsResponsesCustomTools?: boolean, @@ -255,22 +396,36 @@ export function rewriteRoutedCustomToolsForUpstream( for (const name of repairNames) { if (!toolChoiceAllowsRoutedCustomTool(body, name, repairNames)) repairNames.delete(name); } - if (conversionNames.size === 0) return { body, names, repairNames }; - const callIds = new Set(); - collectConvertedCallIds(body, conversionNames, callIds); - return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames }; + if (conversionNames.size === 0 && supportsResponsesCustomTools !== false) { + return { body, names, repairNames }; + } + let next = body; + if (conversionNames.size > 0) { + const callIds = new Set(); + collectConvertedCallIds(body, conversionNames, callIds); + next = rewriteForUpstream(body, conversionNames, callIds); + } + if (supportsResponsesCustomTools === false) { + next = rewriteHistoricalCustomItems(next, collectDeclaredFunctionWireNames(body)); + } + return { body: next, names, repairNames }; } /** * A delta result has no tool name. Without its call, lowering cannot tell whether it belongs * to a converted function or a native custom tool. Request full replay instead of guessing. + * A destination that has denied custom tools also cannot map an orphan result when the current + * catalog is empty, so that case must request replay rather than forwarding the native type. */ export function hasUnmappedRoutedCustomToolOutput( body: unknown, supportsResponsesCustomTools?: boolean, ): boolean { if (!isPlainObject(body) || !Array.isArray(body.input)) return false; - if (collectRoutedCustomToolNames(body, supportsResponsesCustomTools).size === 0) return false; + if ( + supportsResponsesCustomTools !== false + && collectRoutedCustomToolNames(body, supportsResponsesCustomTools).size === 0 + ) return false; const callIds = new Set(); for (const item of body.input) { if (isPlainObject(item) diff --git a/src/server/responses/passthrough-dispatch.ts b/src/server/responses/passthrough-dispatch.ts index 7bd9b64253c..53fa41563f1 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -41,6 +41,7 @@ import { NamespaceToolCollisionError, restoreRoutedNamespaceCalls, } from "../../responses/namespace-tool-compat"; +import { restoreRoutedCustomCalls, RoutedCustomToolCompatError } from "../../responses/custom-tool-compat"; import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema"; import { formatErrorResponse } from "../../bridge"; import { redactSecretString } from "../../lib/redact"; @@ -61,7 +62,6 @@ import { parseMuseSubscriptionUsage, } from "../../providers/muse-subscription-usage"; import { restoreMuseToolNames } from "../../responses/muse-tool-name-alias"; -import { restoreRoutedCustomCalls } from "../../responses/custom-tool-compat"; import { restorePlaintextV2AgentMessageCalls } from "../../responses/plaintext-v2-agent-messages"; import { recordAdapterReasoning, @@ -328,7 +328,11 @@ export async function preparePassthroughExchange( // unstructured 500 — and no request log — depending only on whether a rotation ran first. // Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the // catalog had to drop, so the selector naming it is a client input error, not a 500. - if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) { + if ( + error instanceof NamespaceToolCollisionError + || error instanceof XaiToolSchemaCompatibilityError + || error instanceof RoutedCustomToolCompatError + ) { return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message)); } throw error; diff --git a/tests/responses/custom-tool-compat.test.ts b/tests/responses/custom-tool-compat.test.ts index adfb2105c06..7ba5a18debd 100644 --- a/tests/responses/custom-tool-compat.test.ts +++ b/tests/responses/custom-tool-compat.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { hasUnmappedRoutedCustomToolOutput, rewriteRoutedCustomToolsForUpstream } from "../../src/responses/custom-tool-compat"; +import { + hasUnmappedRoutedCustomToolOutput, + rewriteRoutedCustomToolsForUpstream, + RoutedCustomToolCompatError, + validateFinalCustomToolCompatibility, +} from "../../src/responses/custom-tool-compat"; function convertedInputDescription(name: string): string | undefined { const result = rewriteRoutedCustomToolsForUpstream({ @@ -197,3 +202,157 @@ describe("routed custom-tool compatibility", () => { .toBe("Raw input for this client-executed custom tool."); }); }); + +describe("undeclared historical custom-tool replay", () => { + const awkwardInput = 'say "hi"\npath\\file'; + const execCall = { + type: "custom_tool_call", + id: "ctc_exec", + call_id: "call_exec", + name: "exec", + input: awkwardInput, + }; + const execOutput = { + type: "custom_tool_call_output", + call_id: "call_exec", + output: "ok", + }; + + test("lowers a complete undeclared history pair without expanding the live catalog", () => { + const raw = { + tools: [], + tool_choice: "none", + input: [execCall, execOutput], + }; + const before = JSON.stringify(raw); + + const rewritten = rewriteRoutedCustomToolsForUpstream(raw, false); + const body = rewritten.body as typeof raw; + + expect(JSON.stringify(raw)).toBe(before); + expect(rewritten.body).not.toBe(raw); + expect(rewritten.names).toEqual(new Set()); + expect(rewritten.repairNames).toEqual(new Set()); + expect(body.tools).toEqual([]); + expect(body.tool_choice).toBe("none"); + expect(body.input[0]).toMatchObject({ + type: "function_call", + call_id: "call_exec", + name: "exec", + arguments: JSON.stringify({ input: awkwardInput }), + }); + expect(JSON.parse(String((body.input[0] as { arguments: string }).arguments)).input).toBe(awkwardInput); + expect(body.input[1]).toMatchObject({ + type: "function_call_output", + call_id: "call_exec", + output: "ok", + }); + expect(body.input[0]).not.toHaveProperty("id"); + validateFinalCustomToolCompatibility(body, false); + }); + + test.each([undefined, true] as const)("leaves undeclared history unchanged when custom-tool support is %p", support => { + const raw = { input: [execCall, execOutput] }; + const rewritten = rewriteRoutedCustomToolsForUpstream(raw, support); + expect(rewritten.body).toBe(raw); + expect(rewritten.names).toEqual(new Set()); + }); + + test("does not depend on store and keeps a legal empty input string", () => { + const raw = { + store: false, + input: [ + { type: "custom_tool_call", call_id: "call_empty", name: "exec", input: "" }, + { type: "custom_tool_call_output", call_id: "call_empty", output: { type: "custom_tool_call", name: "exec", input: "nested" } }, + ], + }; + const rewritten = rewriteRoutedCustomToolsForUpstream(raw, false); + const body = rewritten.body as typeof raw; + expect(body.input[0]).toMatchObject({ + type: "function_call", + arguments: JSON.stringify({ input: "" }), + }); + expect(body.input[1]).toMatchObject({ + type: "function_call_output", + output: { type: "custom_tool_call", name: "exec", input: "nested" }, + }); + const stored = rewriteRoutedCustomToolsForUpstream({ ...raw, store: true }, false); + expect((stored.body as typeof raw).input[0]).toMatchObject({ type: "function_call", call_id: "call_empty" }); + }); + + test("converts an in-request output-only pair without requiring a second replay", () => { + const raw = { + input: [ + { type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }, + execOutput, + ], + }; + expect(hasUnmappedRoutedCustomToolOutput(raw, false)).toBe(false); + const body = rewriteRoutedCustomToolsForUpstream(raw, false).body as typeof raw; + expect(body.input.map(item => item.type)).toEqual(["function_call", "function_call_output"]); + }); + + test("requests full replay for an unmapped result when the destination denies custom tools", () => { + const orphan = { input: [execOutput] }; + expect(hasUnmappedRoutedCustomToolOutput(orphan)).toBe(false); + expect(hasUnmappedRoutedCustomToolOutput(orphan, true)).toBe(false); + expect(hasUnmappedRoutedCustomToolOutput(orphan, false)).toBe(true); + const rewritten = rewriteRoutedCustomToolsForUpstream(orphan, false); + expect((rewritten.body as typeof orphan).input[0]).toEqual(execOutput); + expect(() => validateFinalCustomToolCompatibility(rewritten.body, false)).toThrow(RoutedCustomToolCompatError); + }); + + test("does not re-wrap existing function calls and is idempotent", () => { + const raw = { + input: [ + { type: "function_call", call_id: "call_fn", name: "lookup", arguments: "{\"q\":1}" }, + { type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "{\"already\":true}" }, + { type: "custom_tool_call_output", call_id: "call_exec", output: "done" }, + ], + }; + const first = rewriteRoutedCustomToolsForUpstream(raw, false); + const second = rewriteRoutedCustomToolsForUpstream(first.body, false); + const body = first.body as typeof raw; + expect(body.input[0]).toEqual(raw.input[0]); + expect(body.input[1]).toMatchObject({ + type: "function_call", + arguments: JSON.stringify({ input: "{\"already\":true}" }), + }); + expect(second.body).toEqual(first.body); + }); + + test("refuses illegal historical input and call_id identity collisions", () => { + expect(() => rewriteRoutedCustomToolsForUpstream({ + input: [{ type: "custom_tool_call", call_id: "call_exec", name: "exec", input: { nested: true } }], + }, false)).toThrow(RoutedCustomToolCompatError); + expect(() => rewriteRoutedCustomToolsForUpstream({ + input: [ + { type: "custom_tool_call", call_id: "call_dup", name: "exec", input: "a" }, + { type: "custom_tool_call", call_id: "call_dup", name: "apply_patch", input: "b" }, + ], + }, false)).toThrow(RoutedCustomToolCompatError); + expect(() => rewriteRoutedCustomToolsForUpstream({ + tools: [{ type: "function", name: "exec", parameters: { type: "object" } }], + input: [{ type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }], + }, false)).toThrow(RoutedCustomToolCompatError); + }); + + test("final guard reports leftover protocol items and ignores tool-output JSON", () => { + expect(() => validateFinalCustomToolCompatibility({ + input: [{ type: "custom_tool_call", call_id: "call_x", name: "exec", input: "x" }], + }, false)).toThrow(/final_guard: custom_tool_call/); + expect(() => validateFinalCustomToolCompatibility({ + tools: [{ type: "custom", name: "exec" }], + }, false)).toThrow(/final_guard: custom/); + expect(() => validateFinalCustomToolCompatibility({ + input: [{ + type: "function_call_output", + call_id: "call_x", + output: { type: "custom_tool_call", name: "exec", input: "x" }, + }], + }, false)).not.toThrow(); + expect(() => validateFinalCustomToolCompatibility({ + input: [{ type: "custom_tool_call", call_id: "call_x", name: "exec", input: "x" }], + }, true)).not.toThrow(); + }); +}); diff --git a/tests/responses/openai-responses-passthrough.test.ts b/tests/responses/openai-responses-passthrough.test.ts index f9c2029f38a..0e27a0694f2 100644 --- a/tests/responses/openai-responses-passthrough.test.ts +++ b/tests/responses/openai-responses-passthrough.test.ts @@ -995,6 +995,225 @@ describe("Responses custom-tool destination capability", () => { expect(body.input[0]).toMatchObject({ type: "custom_tool_call", call_id: "c1", name: "apply_patch" }); expect(request.convertedRoutedCustomToolNames ?? []).toEqual([]); }); + + test("serialized outbound JSON lowers undeclared historical custom calls on a denying destination", () => { + const awkwardInput = 'say "hi"\npath\\file'; + const rawBody = { + model: "routed-model", + store: false, + input: [ + { type: "custom_tool_call", id: "ctc_exec", call_id: "call_exec", name: "exec", input: awkwardInput }, + { type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }, + ], + }; + const before = JSON.stringify(rawBody); + const request = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://provider.example/v1", + authMode: "key", + apiKey: "test-key", + supportsResponsesCustomTools: false, + }).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: rawBody, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { + store: boolean; + input: Array>; + tools?: unknown; + }; + + expect(JSON.stringify(rawBody)).toBe(before); + expect(body).not.toHaveProperty("tools"); + expect(body.store).toBe(false); + expect(body.input[0]).toMatchObject({ + type: "function_call", + call_id: "call_exec", + name: "exec", + arguments: JSON.stringify({ input: awkwardInput }), + }); + expect(body.input[0]).not.toHaveProperty("id"); + expect(JSON.parse(String(body.input[0]!.arguments)).input).toBe(awkwardInput); + expect(body.input[1]).toMatchObject({ + type: "function_call_output", + call_id: "call_exec", + output: "ok", + }); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); + }); + + test("namespaced historical custom calls keep distinct wire identities after flattening", () => { + const request = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://provider.example/v1", + authMode: "key", + apiKey: "test-key", + supportsResponsesCustomTools: false, + }).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: { + model: "routed-model", + input: [ + { type: "custom_tool_call", call_id: "c1", namespace: "alpha", name: "read", input: "a" }, + { type: "custom_tool_call_output", call_id: "c1", output: "A" }, + { type: "custom_tool_call", call_id: "c2", namespace: "beta", name: "read", input: "b" }, + { type: "custom_tool_call_output", call_id: "c2", output: "B" }, + ], + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { input: Array> }; + expect(body.input[0]).toMatchObject({ + type: "function_call", + call_id: "c1", + name: "alpha__read", + arguments: JSON.stringify({ input: "a" }), + }); + expect(body.input[0]).not.toHaveProperty("namespace"); + expect(body.input[2]).toMatchObject({ + type: "function_call", + call_id: "c2", + name: "beta__read", + }); + }); + + test("compaction with no live tools still lowers historical custom replay items", () => { + const request = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://gateway.example/v1", + authMode: "key", + apiKey: "test-key", + supportsResponsesCustomTools: false, + }).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _compactionRequest: true, + _rawBody: { + model: "routed-model", + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "earlier" }] }, + { type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }, + { type: "custom_tool_call_output", call_id: "call_exec", output: "1" }, + { type: "compaction_trigger" }, + ], + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { input: Array> }; + expect(body).not.toHaveProperty("tools"); + expect(body.input.some(item => item.type === "compaction_trigger")).toBe(false); + expect(body.input).toEqual(expect.arrayContaining([ + { + type: "function_call", + call_id: "call_exec", + name: "exec", + arguments: JSON.stringify({ input: "text(1)" }), + }, + { + type: "function_call_output", + call_id: "call_exec", + output: "1", + }, + ])); + expect(body.input.at(-1)).toEqual({ + type: "message", + role: "user", + content: [{ + type: "input_text", + text: expect.stringContaining("CONTEXT CHECKPOINT COMPACTION"), + }], + }); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); + }); + + test("unmapped custom results fail closed before a denying destination is contacted", () => { + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://provider.example/v1", + authMode: "key", + apiKey: "test-key", + supportsResponsesCustomTools: false, + }); + expect(() => adapter.buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: { + model: "routed-model", + input: [{ type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }], + }, + }, { headers: new Headers() })).toThrow("custom_tool_compat: final_guard: custom_tool_call_output"); + }); + + test("historical exec replay does not re-authorize a new undeclared exec call", async () => { + const outbound: Array> = []; + const leakedCall = { + type: "function_call", + id: "fc_new", + call_id: "call_new", + name: "exec", + arguments: JSON.stringify({ input: "text(2)" }), + status: "completed", + }; + const savedFetch = globalThis.fetch; + globalThis.fetch = (async (_input, init) => { + outbound.push(JSON.parse(String(init?.body))); + return new Response(JSON.stringify({ id: "resp_1", status: "completed", output: [leakedCall] }), { + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + try { + takeSpendHome(); + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "fixture/model", + stream: false, + tools: [{ type: "function", name: "wait", parameters: { type: "object" } }], + input: [ + { type: "custom_tool_call", call_id: "call_old", name: "exec", input: "text(1)" }, + { type: "custom_tool_call_output", call_id: "call_old", output: "1" }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ], + }), + }), { + port: 0, + defaultProvider: "fixture", + providers: { + fixture: { + adapter: "openai-responses", + baseUrl: "https://fixture.test/v1", + authMode: "key", + apiKey: "fixture-key", + supportsResponsesCustomTools: false, + }, + }, + } as OcxConfig, { model: "", provider: "" }); + expect(outbound).toHaveLength(1); + expect(outbound[0]!.input).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: "function_call", + call_id: "call_old", + name: "exec", + arguments: JSON.stringify({ input: "text(1)" }), + }), + ])); + const body = await response.text(); + expect(body).toContain("undeclared client tool"); + expect(body).toContain("exec"); + expect(body).not.toContain("\"type\":\"custom_tool_call\""); + } finally { + globalThis.fetch = savedFetch; + } + }); }); describe("routed compaction lowering order", () => { From 6f437947e6f8fd992863d10bc5d659a6727ca899 Mon Sep 17 00:00:00 2001 From: maosisheng Date: Sun, 20 Sep 2026 10:45:08 -0700 Subject: [PATCH 2/5] test(responses): split historical custom-tool replay coverage off the passthrough ratchet cap openai-responses-passthrough.test.ts is already at its 4809-line ceiling. Keep the new wire fixtures in a responses-prefixed file so the layout seed resolves it without raising a cap. Co-authored-by: Cursor --- .../openai-responses-passthrough.test.ts | 219 ----------------- ...nses-custom-tool-historical-replay.test.ts | 229 ++++++++++++++++++ 2 files changed, 229 insertions(+), 219 deletions(-) create mode 100644 tests/responses/responses-custom-tool-historical-replay.test.ts diff --git a/tests/responses/openai-responses-passthrough.test.ts b/tests/responses/openai-responses-passthrough.test.ts index 0e27a0694f2..f9c2029f38a 100644 --- a/tests/responses/openai-responses-passthrough.test.ts +++ b/tests/responses/openai-responses-passthrough.test.ts @@ -995,225 +995,6 @@ describe("Responses custom-tool destination capability", () => { expect(body.input[0]).toMatchObject({ type: "custom_tool_call", call_id: "c1", name: "apply_patch" }); expect(request.convertedRoutedCustomToolNames ?? []).toEqual([]); }); - - test("serialized outbound JSON lowers undeclared historical custom calls on a denying destination", () => { - const awkwardInput = 'say "hi"\npath\\file'; - const rawBody = { - model: "routed-model", - store: false, - input: [ - { type: "custom_tool_call", id: "ctc_exec", call_id: "call_exec", name: "exec", input: awkwardInput }, - { type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }, - ], - }; - const before = JSON.stringify(rawBody); - const request = createResponsesPassthroughAdapter({ - adapter: "openai-responses", - baseUrl: "https://provider.example/v1", - authMode: "key", - apiKey: "test-key", - supportsResponsesCustomTools: false, - }).buildRequest({ - modelId: "routed-model", - context: { messages: [] }, - stream: false, - options: {}, - _rawBody: rawBody, - }, { headers: new Headers() }); - const body = JSON.parse(request.body) as { - store: boolean; - input: Array>; - tools?: unknown; - }; - - expect(JSON.stringify(rawBody)).toBe(before); - expect(body).not.toHaveProperty("tools"); - expect(body.store).toBe(false); - expect(body.input[0]).toMatchObject({ - type: "function_call", - call_id: "call_exec", - name: "exec", - arguments: JSON.stringify({ input: awkwardInput }), - }); - expect(body.input[0]).not.toHaveProperty("id"); - expect(JSON.parse(String(body.input[0]!.arguments)).input).toBe(awkwardInput); - expect(body.input[1]).toMatchObject({ - type: "function_call_output", - call_id: "call_exec", - output: "ok", - }); - expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); - }); - - test("namespaced historical custom calls keep distinct wire identities after flattening", () => { - const request = createResponsesPassthroughAdapter({ - adapter: "openai-responses", - baseUrl: "https://provider.example/v1", - authMode: "key", - apiKey: "test-key", - supportsResponsesCustomTools: false, - }).buildRequest({ - modelId: "routed-model", - context: { messages: [] }, - stream: false, - options: {}, - _rawBody: { - model: "routed-model", - input: [ - { type: "custom_tool_call", call_id: "c1", namespace: "alpha", name: "read", input: "a" }, - { type: "custom_tool_call_output", call_id: "c1", output: "A" }, - { type: "custom_tool_call", call_id: "c2", namespace: "beta", name: "read", input: "b" }, - { type: "custom_tool_call_output", call_id: "c2", output: "B" }, - ], - }, - }, { headers: new Headers() }); - const body = JSON.parse(request.body) as { input: Array> }; - expect(body.input[0]).toMatchObject({ - type: "function_call", - call_id: "c1", - name: "alpha__read", - arguments: JSON.stringify({ input: "a" }), - }); - expect(body.input[0]).not.toHaveProperty("namespace"); - expect(body.input[2]).toMatchObject({ - type: "function_call", - call_id: "c2", - name: "beta__read", - }); - }); - - test("compaction with no live tools still lowers historical custom replay items", () => { - const request = createResponsesPassthroughAdapter({ - adapter: "openai-responses", - baseUrl: "https://gateway.example/v1", - authMode: "key", - apiKey: "test-key", - supportsResponsesCustomTools: false, - }).buildRequest({ - modelId: "routed-model", - context: { messages: [] }, - stream: false, - options: {}, - _compactionRequest: true, - _rawBody: { - model: "routed-model", - input: [ - { type: "message", role: "user", content: [{ type: "input_text", text: "earlier" }] }, - { type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }, - { type: "custom_tool_call_output", call_id: "call_exec", output: "1" }, - { type: "compaction_trigger" }, - ], - }, - }, { headers: new Headers() }); - const body = JSON.parse(request.body) as { input: Array> }; - expect(body).not.toHaveProperty("tools"); - expect(body.input.some(item => item.type === "compaction_trigger")).toBe(false); - expect(body.input).toEqual(expect.arrayContaining([ - { - type: "function_call", - call_id: "call_exec", - name: "exec", - arguments: JSON.stringify({ input: "text(1)" }), - }, - { - type: "function_call_output", - call_id: "call_exec", - output: "1", - }, - ])); - expect(body.input.at(-1)).toEqual({ - type: "message", - role: "user", - content: [{ - type: "input_text", - text: expect.stringContaining("CONTEXT CHECKPOINT COMPACTION"), - }], - }); - expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); - }); - - test("unmapped custom results fail closed before a denying destination is contacted", () => { - const adapter = createResponsesPassthroughAdapter({ - adapter: "openai-responses", - baseUrl: "https://provider.example/v1", - authMode: "key", - apiKey: "test-key", - supportsResponsesCustomTools: false, - }); - expect(() => adapter.buildRequest({ - modelId: "routed-model", - context: { messages: [] }, - stream: false, - options: {}, - _rawBody: { - model: "routed-model", - input: [{ type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }], - }, - }, { headers: new Headers() })).toThrow("custom_tool_compat: final_guard: custom_tool_call_output"); - }); - - test("historical exec replay does not re-authorize a new undeclared exec call", async () => { - const outbound: Array> = []; - const leakedCall = { - type: "function_call", - id: "fc_new", - call_id: "call_new", - name: "exec", - arguments: JSON.stringify({ input: "text(2)" }), - status: "completed", - }; - const savedFetch = globalThis.fetch; - globalThis.fetch = (async (_input, init) => { - outbound.push(JSON.parse(String(init?.body))); - return new Response(JSON.stringify({ id: "resp_1", status: "completed", output: [leakedCall] }), { - headers: { "content-type": "application/json" }, - }); - }) as typeof fetch; - try { - takeSpendHome(); - const response = await handleResponses(new Request("http://localhost/v1/responses", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - model: "fixture/model", - stream: false, - tools: [{ type: "function", name: "wait", parameters: { type: "object" } }], - input: [ - { type: "custom_tool_call", call_id: "call_old", name: "exec", input: "text(1)" }, - { type: "custom_tool_call_output", call_id: "call_old", output: "1" }, - { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, - ], - }), - }), { - port: 0, - defaultProvider: "fixture", - providers: { - fixture: { - adapter: "openai-responses", - baseUrl: "https://fixture.test/v1", - authMode: "key", - apiKey: "fixture-key", - supportsResponsesCustomTools: false, - }, - }, - } as OcxConfig, { model: "", provider: "" }); - expect(outbound).toHaveLength(1); - expect(outbound[0]!.input).toEqual(expect.arrayContaining([ - expect.objectContaining({ - type: "function_call", - call_id: "call_old", - name: "exec", - arguments: JSON.stringify({ input: "text(1)" }), - }), - ])); - const body = await response.text(); - expect(body).toContain("undeclared client tool"); - expect(body).toContain("exec"); - expect(body).not.toContain("\"type\":\"custom_tool_call\""); - } finally { - globalThis.fetch = savedFetch; - } - }); }); describe("routed compaction lowering order", () => { diff --git a/tests/responses/responses-custom-tool-historical-replay.test.ts b/tests/responses/responses-custom-tool-historical-replay.test.ts new file mode 100644 index 00000000000..997c3fded8f --- /dev/null +++ b/tests/responses/responses-custom-tool-historical-replay.test.ts @@ -0,0 +1,229 @@ +/** + * Undeclared historical custom-tool replay for destinations that deny native custom tools. + * + * Lives in its own file rather than in openai-responses-passthrough.test.ts: that file is + * exactly at its file-size ratchet cap (4,809 lines in tests/fixtures/file-size-baseline.json), + * and the cap only ever moves downward. + */ +import { afterEach, describe, expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter as createResponsesPassthroughAdapterProduction } from "../../src/adapters/openai-responses"; +import { handleResponses } from "../../src/server/responses"; +import type { OcxConfig } from "../../src/types"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; +import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; + +let releaseSpendHome: (() => void) | undefined; +const takeSpendHome = (): void => { releaseSpendHome ??= acquireOwnedSpendHome(); }; +afterEach(() => { releaseSpendHome?.(); releaseSpendHome = undefined; }); + +const createResponsesPassthroughAdapter = ( + ...args: Parameters +) => withTestTranslatorBudget(createResponsesPassthroughAdapterProduction(...args)); + +const denyingProvider = { + adapter: "openai-responses" as const, + baseUrl: "https://provider.example/v1", + authMode: "key" as const, + apiKey: "test-key", + supportsResponsesCustomTools: false as const, +}; + +describe("undeclared historical custom-tool replay on the passthrough wire", () => { + test("serialized outbound JSON lowers undeclared historical custom calls on a denying destination", () => { + const awkwardInput = 'say "hi"\npath\\file'; + const rawBody = { + model: "routed-model", + store: false, + input: [ + { type: "custom_tool_call", id: "ctc_exec", call_id: "call_exec", name: "exec", input: awkwardInput }, + { type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }, + ], + }; + const before = JSON.stringify(rawBody); + const request = createResponsesPassthroughAdapter(denyingProvider).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: rawBody, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { + store: boolean; + input: Array>; + tools?: unknown; + }; + + expect(JSON.stringify(rawBody)).toBe(before); + expect(body).not.toHaveProperty("tools"); + expect(body.store).toBe(false); + expect(body.input[0]).toMatchObject({ + type: "function_call", + call_id: "call_exec", + name: "exec", + arguments: JSON.stringify({ input: awkwardInput }), + }); + expect(body.input[0]).not.toHaveProperty("id"); + expect(JSON.parse(String(body.input[0]!.arguments)).input).toBe(awkwardInput); + expect(body.input[1]).toMatchObject({ + type: "function_call_output", + call_id: "call_exec", + output: "ok", + }); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); + }); + + test("namespaced historical custom calls keep distinct wire identities after flattening", () => { + const request = createResponsesPassthroughAdapter(denyingProvider).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: { + model: "routed-model", + input: [ + { type: "custom_tool_call", call_id: "c1", namespace: "alpha", name: "read", input: "a" }, + { type: "custom_tool_call_output", call_id: "c1", output: "A" }, + { type: "custom_tool_call", call_id: "c2", namespace: "beta", name: "read", input: "b" }, + { type: "custom_tool_call_output", call_id: "c2", output: "B" }, + ], + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { input: Array> }; + expect(body.input[0]).toMatchObject({ + type: "function_call", + call_id: "c1", + name: "alpha__read", + arguments: JSON.stringify({ input: "a" }), + }); + expect(body.input[0]).not.toHaveProperty("namespace"); + expect(body.input[2]).toMatchObject({ + type: "function_call", + call_id: "c2", + name: "beta__read", + }); + }); + + test("compaction with no live tools still lowers historical custom replay items", () => { + const request = createResponsesPassthroughAdapter({ + ...denyingProvider, + baseUrl: "https://gateway.example/v1", + }).buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _compactionRequest: true, + _rawBody: { + model: "routed-model", + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "earlier" }] }, + { type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }, + { type: "custom_tool_call_output", call_id: "call_exec", output: "1" }, + { type: "compaction_trigger" }, + ], + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { input: Array> }; + expect(body).not.toHaveProperty("tools"); + expect(body.input.some(item => item.type === "compaction_trigger")).toBe(false); + expect(body.input).toEqual(expect.arrayContaining([ + { + type: "function_call", + call_id: "call_exec", + name: "exec", + arguments: JSON.stringify({ input: "text(1)" }), + }, + { + type: "function_call_output", + call_id: "call_exec", + output: "1", + }, + ])); + expect(body.input.at(-1)).toEqual({ + type: "message", + role: "user", + content: [{ + type: "input_text", + text: expect.stringContaining("CONTEXT CHECKPOINT COMPACTION"), + }], + }); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).toEqual([]); + }); + + test("unmapped custom results fail closed before a denying destination is contacted", () => { + const adapter = createResponsesPassthroughAdapter(denyingProvider); + expect(() => adapter.buildRequest({ + modelId: "routed-model", + context: { messages: [] }, + stream: false, + options: {}, + _rawBody: { + model: "routed-model", + input: [{ type: "custom_tool_call_output", call_id: "call_exec", output: "ok" }], + }, + }, { headers: new Headers() })).toThrow("custom_tool_compat: final_guard: custom_tool_call_output"); + }); + + test("historical exec replay does not re-authorize a new undeclared exec call", async () => { + const outbound: Array> = []; + const leakedCall = { + type: "function_call", + id: "fc_new", + call_id: "call_new", + name: "exec", + arguments: JSON.stringify({ input: "text(2)" }), + status: "completed", + }; + const savedFetch = globalThis.fetch; + globalThis.fetch = (async (_input, init) => { + outbound.push(JSON.parse(String(init?.body))); + return new Response(JSON.stringify({ id: "resp_1", status: "completed", output: [leakedCall] }), { + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + try { + takeSpendHome(); + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "fixture/model", + stream: false, + tools: [{ type: "function", name: "wait", parameters: { type: "object" } }], + input: [ + { type: "custom_tool_call", call_id: "call_old", name: "exec", input: "text(1)" }, + { type: "custom_tool_call_output", call_id: "call_old", output: "1" }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ], + }), + }), { + port: 0, + defaultProvider: "fixture", + providers: { + fixture: { + adapter: "openai-responses", + baseUrl: "https://fixture.test/v1", + authMode: "key", + apiKey: "fixture-key", + supportsResponsesCustomTools: false, + }, + }, + } as OcxConfig, { model: "", provider: "" }); + expect(outbound).toHaveLength(1); + expect(outbound[0]!.input).toEqual(expect.arrayContaining([ + expect.objectContaining({ + type: "function_call", + call_id: "call_old", + name: "exec", + arguments: JSON.stringify({ input: "text(1)" }), + }), + ])); + const body = await response.text(); + expect(body).toContain("undeclared client tool"); + expect(body).toContain("exec"); + expect(body).not.toContain("\"type\":\"custom_tool_call\""); + } finally { + globalThis.fetch = savedFetch; + } + }); +}); From 52f74488f7c0002945c174d394451ffc2300a215 Mon Sep 17 00:00:00 2001 From: maosisheng Date: Mon, 21 Sep 2026 00:40:20 -0700 Subject: [PATCH 3/5] fix(responses): reject malformed historical custom calls --- src/responses/custom-tool-compat.ts | 26 ++++++++++++++-------- tests/responses/custom-tool-compat.test.ts | 26 +++++++++++++++++----- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/responses/custom-tool-compat.ts b/src/responses/custom-tool-compat.ts index e01aa2aad2e..e4378f51184 100644 --- a/src/responses/custom-tool-compat.ts +++ b/src/responses/custom-tool-compat.ts @@ -303,15 +303,17 @@ function rewriteHistoricalCustomItems( const calls = new Map(); for (const item of body.input) { if (!isPlainObject(item)) continue; - if ( - (item.type !== "custom_tool_call" && item.type !== "function_call") - || typeof item.call_id !== "string" - || item.call_id.length === 0 - ) continue; + if (item.type !== "custom_tool_call" && item.type !== "function_call") continue; + if (typeof item.call_id !== "string" || item.call_id.length === 0) { + if (item.type === "custom_tool_call") { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.call_id"); + } + continue; + } const identity = historicalCallIdentity(item); if (!identity) { if (item.type === "custom_tool_call") { - throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.name"); } continue; } @@ -326,15 +328,21 @@ function rewriteHistoricalCustomItems( const input = body.input.map(item => { if (!isPlainObject(item)) return item; if (item.type === "custom_tool_call") { - if (typeof item.name !== "string" || item.name.length === 0 || typeof item.input !== "string") { - throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + if (typeof item.call_id !== "string" || item.call_id.length === 0) { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.call_id"); + } + if (typeof item.name !== "string" || item.name.length === 0) { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.name"); + } + if (typeof item.input !== "string") { + throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call.input"); } const wireName = customToolWireName( typeof item.namespace === "string" ? item.namespace : undefined, item.name, ); if (declaredFunctionWireNames.has(wireName)) { - throw new RoutedCustomToolCompatError("historical_item", "custom_tool_call"); + throw new RoutedCustomToolCompatError("historical_collision", "declared_function_name"); } const { input: rawInput, id: _id, ...rest } = item; changed = true; diff --git a/tests/responses/custom-tool-compat.test.ts b/tests/responses/custom-tool-compat.test.ts index 7ba5a18debd..fb402875097 100644 --- a/tests/responses/custom-tool-compat.test.ts +++ b/tests/responses/custom-tool-compat.test.ts @@ -321,20 +321,34 @@ describe("undeclared historical custom-tool replay", () => { expect(second.body).toEqual(first.body); }); - test("refuses illegal historical input and call_id identity collisions", () => { + test.each([ + ["missing", { type: "custom_tool_call", name: "exec", input: "text(1)" }], + ["empty", { type: "custom_tool_call", call_id: "", name: "exec", input: "text(1)" }], + ] as const)("rejects historical custom calls with a %s call_id before lowering", (_label, item) => { + expect(() => rewriteRoutedCustomToolsForUpstream({ input: [item] }, false)) + .toThrow(/historical_item: custom_tool_call\.call_id/); + }); + + test("reports malformed historical fields separately from live-name collisions", () => { + expect(() => rewriteRoutedCustomToolsForUpstream({ + input: [{ type: "custom_tool_call", call_id: "call_exec", name: "", input: "text(1)" }], + }, false)).toThrow(/historical_item: custom_tool_call\.name/); expect(() => rewriteRoutedCustomToolsForUpstream({ input: [{ type: "custom_tool_call", call_id: "call_exec", name: "exec", input: { nested: true } }], - }, false)).toThrow(RoutedCustomToolCompatError); + }, false)).toThrow(/historical_item: custom_tool_call\.input/); + expect(() => rewriteRoutedCustomToolsForUpstream({ + tools: [{ type: "function", name: "exec", parameters: { type: "object" } }], + input: [{ type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }], + }, false)).toThrow(/historical_collision: declared_function_name/); + }); + + test("refuses call_id identity collisions", () => { expect(() => rewriteRoutedCustomToolsForUpstream({ input: [ { type: "custom_tool_call", call_id: "call_dup", name: "exec", input: "a" }, { type: "custom_tool_call", call_id: "call_dup", name: "apply_patch", input: "b" }, ], }, false)).toThrow(RoutedCustomToolCompatError); - expect(() => rewriteRoutedCustomToolsForUpstream({ - tools: [{ type: "function", name: "exec", parameters: { type: "object" } }], - input: [{ type: "custom_tool_call", call_id: "call_exec", name: "exec", input: "text(1)" }], - }, false)).toThrow(RoutedCustomToolCompatError); }); test("final guard reports leftover protocol items and ignores tool-output JSON", () => { From 5654b41938cf3fbf7634668dc5ad2e3ad06adac1 Mon Sep 17 00:00:00 2001 From: maosisheng Date: Mon, 21 Sep 2026 01:53:41 -0700 Subject: [PATCH 4/5] fix(responses): bind historical outputs to custom calls --- src/responses/custom-tool-compat.ts | 4 +++- tests/responses/custom-tool-compat.test.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/responses/custom-tool-compat.ts b/src/responses/custom-tool-compat.ts index e4378f51184..c6b0912efba 100644 --- a/src/responses/custom-tool-compat.ts +++ b/src/responses/custom-tool-compat.ts @@ -301,6 +301,7 @@ function rewriteHistoricalCustomItems( if (!isPlainObject(body) || !Array.isArray(body.input)) return body; const calls = new Map(); + const historicalCustomCallIds = new Set(); for (const item of body.input) { if (!isPlainObject(item)) continue; if (item.type !== "custom_tool_call" && item.type !== "function_call") continue; @@ -322,6 +323,7 @@ function rewriteHistoricalCustomItems( throw new RoutedCustomToolCompatError("historical_item", "call_id"); } calls.set(item.call_id, identity); + if (item.type === "custom_tool_call") historicalCustomCallIds.add(item.call_id); } let changed = false; @@ -355,7 +357,7 @@ function rewriteHistoricalCustomItems( if ( item.type === "custom_tool_call_output" && typeof item.call_id === "string" - && calls.has(item.call_id) + && historicalCustomCallIds.has(item.call_id) ) { changed = true; return { ...item, type: "function_call_output" }; diff --git a/tests/responses/custom-tool-compat.test.ts b/tests/responses/custom-tool-compat.test.ts index fb402875097..15ee9840352 100644 --- a/tests/responses/custom-tool-compat.test.ts +++ b/tests/responses/custom-tool-compat.test.ts @@ -351,6 +351,20 @@ describe("undeclared historical custom-tool replay", () => { }, false)).toThrow(RoutedCustomToolCompatError); }); + test("does not let a native function call claim a historical custom-tool output", () => { + const raw = { + input: [ + { type: "function_call", call_id: "call_shared", name: "exec", arguments: "{}" }, + { type: "custom_tool_call_output", call_id: "call_shared", output: "ok" }, + ], + }; + const rewritten = rewriteRoutedCustomToolsForUpstream(raw, false); + expect(rewritten.body).toBe(raw); + expect((rewritten.body as typeof raw).input[1]).toEqual(raw.input[1]); + expect(() => validateFinalCustomToolCompatibility(rewritten.body, false)) + .toThrow(/final_guard: custom_tool_call_output/); + }); + test("final guard reports leftover protocol items and ignores tool-output JSON", () => { expect(() => validateFinalCustomToolCompatibility({ input: [{ type: "custom_tool_call", call_id: "call_x", name: "exec", input: "x" }], From dc948dcff592fa2577edb8a6222aa20ef5af4e80 Mon Sep 17 00:00:00 2001 From: maosisheng Date: Tue, 22 Sep 2026 01:40:25 -0700 Subject: [PATCH 5/5] fix(responses): reject duplicate historical call ids --- src/responses/custom-tool-compat.ts | 7 +++++-- tests/responses/custom-tool-compat.test.ts | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/responses/custom-tool-compat.ts b/src/responses/custom-tool-compat.ts index c6b0912efba..989af132974 100644 --- a/src/responses/custom-tool-compat.ts +++ b/src/responses/custom-tool-compat.ts @@ -319,8 +319,11 @@ function rewriteHistoricalCustomItems( continue; } const existing = calls.get(item.call_id); - if (existing && !sameHistoricalIdentity(existing, identity)) { - throw new RoutedCustomToolCompatError("historical_item", "call_id"); + if (existing) { + throw new RoutedCustomToolCompatError( + "historical_item", + sameHistoricalIdentity(existing, identity) ? "duplicate_call_id" : "call_id", + ); } calls.set(item.call_id, identity); if (item.type === "custom_tool_call") historicalCustomCallIds.add(item.call_id); diff --git a/tests/responses/custom-tool-compat.test.ts b/tests/responses/custom-tool-compat.test.ts index 15ee9840352..9351651c924 100644 --- a/tests/responses/custom-tool-compat.test.ts +++ b/tests/responses/custom-tool-compat.test.ts @@ -342,6 +342,15 @@ describe("undeclared historical custom-tool replay", () => { }, false)).toThrow(/historical_collision: declared_function_name/); }); + test("rejects duplicate call IDs even when the historical call identity matches", () => { + expect(() => rewriteRoutedCustomToolsForUpstream({ + input: [ + { type: "custom_tool_call", call_id: "call_dup", name: "exec", input: "a" }, + { type: "custom_tool_call", call_id: "call_dup", name: "exec", input: "a" }, + ], + }, false)).toThrow(/historical_item: duplicate_call_id/); + }); + test("refuses call_id identity collisions", () => { expect(() => rewriteRoutedCustomToolsForUpstream({ input: [