From 8e2d42bcd1e57217082b60e86f8897d9548b9842 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 20 Aug 2026 13:15:26 +0900 Subject: [PATCH] fix(responses): give tool_search_call and custom_tool_call their own id prefixes The release audit found this by composing two changes that are each correct alone. A routed tool_search lowering is restored as a tool_search_call with no id (#2145), and the universal output-item id backfill then names it (#2142) -- but the backfill's prefix table had no entry for the type, so it fell through to the generic "item_". That is not cosmetic. stripInvalidItemIds in the Responses adapter deletes any id whose prefix does not match its type, and it lists tsc_ as the only valid prefix for tool_search_call. So the synthesized id survived the turn it was created in and was silently dropped on the next one, leaving the client an item it could not correlate. Neither PR's focused suite caught it because neither composes missing-id restoration with the backfill. custom_tool_call had the same gap and is fixed with it: the serializer enforces ctc_, the backfill did not know the type. --- .../responses/responses-field-backfill.ts | 8 +++++ tests/responses-field-backfill.test.ts | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/server/responses/responses-field-backfill.ts b/src/server/responses/responses-field-backfill.ts index 4d67531020..32cf727891 100644 --- a/src/server/responses/responses-field-backfill.ts +++ b/src/server/responses/responses-field-backfill.ts @@ -34,6 +34,14 @@ const ITEM_ID_PREFIXES: Readonly> = { message: "msg_", reasoning: "rs_", function_call: "fc_", + custom_tool_call: "ctc_", + // A routed tool_search lowering is restored to `tool_search_call` without an id, so this + // backfill is what names it. The generic `item_` fallback is not merely cosmetic here: + // `stripInvalidItemIds` in the Responses adapter deletes any id whose prefix does not match + // the type, so an `item_`-named tool_search_call silently loses its id on the NEXT turn and + // the client sees an item it cannot correlate. The prefixes here must stay a superset of the + // ones that serializer enforces. + tool_search_call: "tsc_", web_search_call: "ws_", file_search_call: "fs_", code_interpreter_call: "ci_", diff --git a/tests/responses-field-backfill.test.ts b/tests/responses-field-backfill.test.ts index f6fb63bae3..f4ac338b69 100644 --- a/tests/responses-field-backfill.test.ts +++ b/tests/responses-field-backfill.test.ts @@ -365,6 +365,36 @@ describe("responses-field-backfill", () => { expect(result.output[0]!.id).toBe("ig_ocx_0"); }); + // A routed tool_search lowering is restored as `tool_search_call` with no id, so this + // backfill names it. The generic `item_` fallback was not cosmetic: `stripInvalidItemIds` + // in the Responses adapter deletes an id whose prefix does not match its type, so the item + // silently lost its id on the NEXT turn and the client could no longer correlate it. + test("tool_search_call gets the prefix the request serializer accepts", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [{ type: "tool_search_call", call_id: "call_x", execution: "client" }], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as { + output: { id: string }[]; + }; + expect(result.output[0]!.id).toBe("tsc_ocx_0"); + }); + + test("custom_tool_call gets its own prefix too", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [{ type: "custom_tool_call", call_id: "call_y" }], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as { + output: { id: string }[]; + }; + expect(result.output[0]!.id).toBe("ctc_ocx_0"); + }); + // A malformed `output_index` falls back to a counter. While that counter lived in the same // numeric namespace as real indexes, a response whose real index reached the counter's base // produced the SAME id as a fallback — a duplicate, which is the one thing this backfill