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
8 changes: 8 additions & 0 deletions src/server/responses/responses-field-backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const ITEM_ID_PREFIXES: Readonly<Record<string, string>> = {
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_",
Expand Down
30 changes: 30 additions & 0 deletions tests/responses-field-backfill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading