From 8ff77e11ebe7bc6472164d29c89c779986b9469a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 20 Aug 2026 03:50:44 +0900 Subject: [PATCH 1/3] fix(responses): backfill missing ids on output items for strict decoders Some relays omit the required id on message, reasoning, and function_call output items, so strict decoders reject the response even after #1941. Synthesize a stable msg_ocx_N / rs_ocx_N / fc_ocx_N id keyed on output_index, and never overwrite an id the upstream actually sent. Carries @bet4it's #2131 implementation and tests. One correction on top: an absent or malformed output_index collapsed to 0, so two such items both became msg_ocx_0 - duplicate ids, which is the defect this backfill exists to prevent. An unusable index now falls back to a monotonic ordinal based far above any plausible real index, so a synthesized id cannot collide with an index-derived one. The well-formed path is unchanged and still produces the stable index-derived id. Locale docs are limited to the English source here; the translated guides in the original PR were uneven and locale parity is not this change's thesis. Closes #2131 --- .../src/content/docs/guides/grok-build.md | 21 +-- .../responses/responses-field-backfill.ts | 62 +++++- tests/responses-field-backfill.test.ts | 178 ++++++++++++++++++ 3 files changed, 244 insertions(+), 17 deletions(-) diff --git a/docs-site/src/content/docs/guides/grok-build.md b/docs-site/src/content/docs/guides/grok-build.md index 08a1073805..f1192b73e2 100644 --- a/docs-site/src/content/docs/guides/grok-build.md +++ b/docs-site/src/content/docs/guides/grok-build.md @@ -3,10 +3,10 @@ title: Grok Build description: Use any opencodex-routed model from xAI's Grok Build CLI — models are auto-registered into ~/.grok/config.toml while the proxy runs. --- -opencodex serves an OpenAI-compatible `POST /v1/chat/completions` (and `/v1/responses`) on its -local port, and Grok Build supports custom models against OpenAI-compatible servers. Starting -with this integration, opencodex registers its whole visible catalog into Grok Build -automatically — no manual config editing required. +opencodex serves an OpenAI-compatible `POST /v1/responses` on its local port, and Grok Build +supports custom models against OpenAI-compatible servers. Starting with this integration, +opencodex registers its whole visible catalog into Grok Build automatically — no manual config +editing required. ## Auto-registration @@ -18,7 +18,7 @@ into `~/.grok/config.toml`: [model.ocx-gpt-5-6-sol] model = "gpt-5.6-sol" base_url = "http://127.0.0.1:10100/v1" -api_backend = "chat_completions" +api_backend = "responses" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" # ... one [model.ocx-*] table per visible model ... @@ -61,12 +61,11 @@ in Codex. Models with an empty tier list keep no effort control, matching Codex behavior. Native GPT-5.6 entries are separate: they preserve and expose their pinned upstream reasoning ladders rather than provider-configured routed metadata. -Grok Build talks to opencodex over Chat Completions and sends `reasoning_effort` when -the ladder is advertised. The Chat Completions inbound translator defaults the internal -Responses `reasoning.summary` to `auto` in that case, so thinking traces reach Grok as -`delta.reasoning_content` instead of being hidden. Set `include_reasoning: false` (or -`reasoning.summary: "none"`) if a client wants the model to think without returning the -trace. An explicit `reasoning.summary` wins when both knobs are present. +Grok Build talks to opencodex over the Responses API. When the route advertises a reasoning +ladder, the Responses passthrough forwards `reasoning.summary` as configured, so thinking +traces reach Grok natively as Responses reasoning items. Set `reasoning.summary: "none"` if +a client wants the model to think without returning the trace. An explicit `reasoning.summary` +wins over the route default. ## Authentication note diff --git a/src/server/responses/responses-field-backfill.ts b/src/server/responses/responses-field-backfill.ts index dff2da1158..67f6a1b796 100644 --- a/src/server/responses/responses-field-backfill.ts +++ b/src/server/responses/responses-field-backfill.ts @@ -29,6 +29,46 @@ function isPlainObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +/** Wire prefixes for Responses output item ids, matching OpenAI's id shapes. */ +const ITEM_ID_PREFIXES: Readonly> = { + message: "msg_", + reasoning: "rs_", + function_call: "fc_", + web_search_call: "ws_", + file_search_call: "fs_", + code_interpreter_call: "ci_", + computer_call: "cc_", + image_gen_call: "ig_", +}; + +/** + * Backfill a required id on an output item when absent. Strict Responses + * decoders (e.g. grok-build serde types) fail with "missing field id" when a + * message or reasoning item has no id, which some upstream relays omit. The + * generated id is deterministic per (type, output index) so it stays stable + * across streaming events that reference the same item. + */ +function backfillItemId(item: Record, outputIndex: number): Record { + if (typeof item.id === "string" && item.id.length > 0) return item; + const type = typeof item.type === "string" ? item.type : ""; + const prefix = Object.prototype.hasOwnProperty.call(ITEM_ID_PREFIXES, type) ? ITEM_ID_PREFIXES[type] : "item_"; + return { ...item, id: prefix + "ocx_" + outputIndex }; +} + +/** + * Monotonic ordinal for an event whose `output_index` is absent or malformed. + * + * Starts far above any plausible real index so a synthesized id can never collide with an + * index-derived one inside the same response. Process-global rather than per-response because + * this module is stateless by design and the value only has to be unique, not meaningful. + */ +const SYNTHETIC_ITEM_ORDINAL_BASE = 1_000_000; +let syntheticItemOrdinal = 0; +function nextSyntheticItemOrdinal(): number { + syntheticItemOrdinal += 1; + return SYNTHETIC_ITEM_ORDINAL_BASE + syntheticItemOrdinal; +} + /** * Backfill annotations: [] on an output_text content part if missing. * Returns the same object reference if no change is needed. @@ -60,14 +100,16 @@ function backfillContentArray(content: unknown): unknown { /** * Walk an output item and backfill output_text parts in its content. + * Also backfills a missing required id on the item itself. * Returns the same object reference if nothing changed. */ -function backfillOutputItem(item: unknown): unknown { +function backfillOutputItem(item: unknown, outputIndex: number): unknown { if (!isPlainObject(item)) return item; const content = item.content; const repaired = backfillContentArray(content); - if (repaired === content) return item; - return { ...item, content: repaired }; + const withId = backfillItemId(item, outputIndex); + if (repaired === content && withId === item) return item; + return { ...withId, ...(repaired === content ? {} : { content: repaired }) }; } /** @@ -79,9 +121,9 @@ function backfillResponseOutput(response: unknown): unknown { const output = response.output; if (!Array.isArray(output)) return response; let changed = false; - const repaired = output.map((item) => { + const repaired = output.map((item, idx) => { if (!isPlainObject(item)) return item; - const next = backfillOutputItem(item); + const next = backfillOutputItem(item, idx); if (next !== item) changed = true; return next; }); @@ -100,7 +142,15 @@ function rewriteEvent(event: Record): Record { // output_item.added / output_item.done: item.content[] -> output_text parts if ((type === "response.output_item.added" || type === "response.output_item.done") && isPlainObject(event.item)) { - const item = backfillOutputItem(event.item); + const rawIndex = event.output_index; + // A malformed or absent `output_index` must not collapse to 0: two such events would then + // both synthesize `msg_ocx_0`, and duplicate ids are the very thing this backfill exists to + // prevent. Fall back to a per-process counter so the synthesized id stays unique. Position + // is not recoverable in that case, but a unique id is what strict decoders require, and a + // well-formed stream still gets the stable index-derived id. + const item = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 + ? backfillOutputItem(event.item, rawIndex) + : backfillOutputItem(event.item, nextSyntheticItemOrdinal()); if (item !== event.item) { next = { ...next, item }; changed = true; diff --git a/tests/responses-field-backfill.test.ts b/tests/responses-field-backfill.test.ts index 8071323e5a..022ff86e4f 100644 --- a/tests/responses-field-backfill.test.ts +++ b/tests/responses-field-backfill.test.ts @@ -170,4 +170,182 @@ describe("responses-field-backfill", () => { expect(result.output[0].content[3].annotations).toBe("not-an-array"); expect(result.output[0].content[4].annotations).toEqual({ unexpected: true }); }); + + test("backfills missing ids on response.completed output items", () => { + const event = { + type: "response.completed", + sequence_number: 42, + response: { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "reasoning", summary: [{ type: "summary_text", text: "thinking" }] }, + { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hello" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "todo_write", + arguments: "{}", + }, + ], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.response.output[0].id).toBe("rs_ocx_0"); + expect(parsed.response.output[1].id).toBe("msg_ocx_1"); + expect(parsed.response.output[2].id).toBe("fc_ocx_2"); + }); + + test("preserves existing item ids", () => { + const event = { + type: "response.completed", + sequence_number: 42, + response: { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "message", id: "msg_real", role: "assistant", content: [{ type: "output_text", text: "hi" }] }, + ], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.response.output[0].id).toBe("msg_real"); + }); + + test("uses output_index when backfilling output_item.done id", () => { + const event = { + type: "response.output_item.done", + output_index: 3, + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toBe("msg_ocx_3"); + }); + + test("falls back to item_ prefix for inherited type names", () => { + const event = { + type: "response.output_item.done", + output_index: 0, + item: { + type: "toString", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toBe("item_ocx_0"); + }); + + test("an invalid output_index still yields a well-formed synthesized id", () => { + for (const badIndex of [-1, 1.5, NaN, Infinity, "0", null, undefined]) { + const event = { + type: "response.output_item.done", + output_index: badIndex, + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + const [out] = apply(sseBlock(event)); + const parsed = parseData([out])[0]; + expect(parsed.item.id).toMatch(/^msg_ocx_\d+$/); + } + }); + + test("two items with an unusable output_index do not collide on one id", () => { + // Collapsing an unusable index to 0 would synthesize `msg_ocx_0` twice, which is the + // duplicate-id defect this backfill exists to prevent. Position is unrecoverable here; + // uniqueness is not optional. + const event = (text: string) => ({ + type: "response.output_item.done", + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text }], + }, + }); + const first = parseData(apply(sseBlock(event("one"))))[0]; + const second = parseData(apply(sseBlock(event("two"))))[0]; + + expect(first.item.id).toMatch(/^msg_ocx_\d+$/); + expect(second.item.id).toMatch(/^msg_ocx_\d+$/); + expect(first.item.id).not.toBe(second.item.id); + }); + + test("a well-formed output_index still produces the stable index-derived id", () => { + const event = { + type: "response.output_item.done", + output_index: 3, + item: { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hi" }], + }, + }; + // Stability across events referencing the same item is the whole point of index-derivation, + // so the fallback must not leak into the well-formed path. + expect(parseData(apply(sseBlock(event)))[0].item.id).toBe("msg_ocx_3"); + expect(parseData(apply(sseBlock(event)))[0].item.id).toBe("msg_ocx_3"); + }); + + test("backfillResponsesFieldsJson backfills missing ids on output items", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "reasoning", summary: [{ type: "summary_text", text: "thinking" }] }, + { + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "hello" }], + }, + { + type: "function_call", + call_id: "call_1", + name: "todo_write", + arguments: "{}", + }, + ], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as typeof response; + expect(result.output[0].id).toBe("rs_ocx_0"); + expect(result.output[1].id).toBe("msg_ocx_1"); + expect(result.output[2].id).toBe("fc_ocx_2"); + }); + + test("backfillResponsesFieldsJson preserves existing item ids", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [ + { type: "message", id: "msg_real", role: "assistant", content: [{ type: "output_text", text: "hi" }] }, + ], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as typeof response; + expect(result.output[0].id).toBe("msg_real"); + }); }); From 713dce9294fe451d9507579bb28397ff6b34bd00 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 20 Aug 2026 11:23:04 +0900 Subject: [PATCH 2/3] fix(responses): give synthesized ids a namespace that cannot collide Two defects CodeRabbit found on #2142. ITEM_ID_PREFIXES was keyed on image_gen_call, but the Responses wire type is image_generation_call, so every real one fell through to the generic item_ prefix. Both spellings now map to ig_. The malformed-index fallback counter started at 1_000_000 and shared the numeric namespace with real output indexes. That pushed a collision out of reach rather than removing it: a response whose real index reached 1_000_001 produced the same id as the first fallback, and a duplicate id is the one thing this backfill exists to prevent. Index-derived and fallback ids now come from lexically disjoint namespaces, so no integer index can ever produce a fallback id. --- .../responses/responses-field-backfill.ts | 39 ++++++++---- tests/responses-field-backfill.test.ts | 60 ++++++++++++++++++- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/server/responses/responses-field-backfill.ts b/src/server/responses/responses-field-backfill.ts index 67f6a1b796..285ad3d956 100644 --- a/src/server/responses/responses-field-backfill.ts +++ b/src/server/responses/responses-field-backfill.ts @@ -38,6 +38,9 @@ const ITEM_ID_PREFIXES: Readonly> = { file_search_call: "fs_", code_interpreter_call: "ci_", computer_call: "cc_", + // The Responses wire type is `image_generation_call`; `image_gen_call` is kept only so a + // relay that emits the short spelling is not silently demoted to the generic `item_`. + image_generation_call: "ig_", image_gen_call: "ig_", }; @@ -48,25 +51,35 @@ const ITEM_ID_PREFIXES: Readonly> = { * generated id is deterministic per (type, output index) so it stays stable * across streaming events that reference the same item. */ -function backfillItemId(item: Record, outputIndex: number): Record { +function backfillItemId(item: Record, slot: ItemIdSlot): Record { if (typeof item.id === "string" && item.id.length > 0) return item; const type = typeof item.type === "string" ? item.type : ""; const prefix = Object.prototype.hasOwnProperty.call(ITEM_ID_PREFIXES, type) ? ITEM_ID_PREFIXES[type] : "item_"; - return { ...item, id: prefix + "ocx_" + outputIndex }; + return { ...item, id: prefix + "ocx_" + (slot.kind === "index" ? String(slot.index) : "fallback_" + slot.ordinal) }; } +/** + * Which namespace a synthesized id comes from. + * + * Keeping the fallback counter in the SAME numeric namespace as real output indexes only + * pushed the collision out of reach rather than removing it: a response whose real index + * happened to be 1_000_001 would produce the same id as the first malformed-index fallback, + * and a duplicate id is exactly what this backfill exists to prevent. The namespaces are now + * lexically disjoint, so no index value can ever collide with a fallback. + */ +type ItemIdSlot = { kind: "index"; index: number } | { kind: "fallback"; ordinal: number }; + /** * Monotonic ordinal for an event whose `output_index` is absent or malformed. * - * Starts far above any plausible real index so a synthesized id can never collide with an - * index-derived one inside the same response. Process-global rather than per-response because - * this module is stateless by design and the value only has to be unique, not meaningful. + * Process-global rather than per-response because this module is stateless by design and the + * value only has to be unique, not meaningful. It carries its own `fallback_` namespace, so + * uniqueness no longer depends on a real index never reaching some arbitrary ceiling. */ -const SYNTHETIC_ITEM_ORDINAL_BASE = 1_000_000; let syntheticItemOrdinal = 0; -function nextSyntheticItemOrdinal(): number { +function nextSyntheticItemSlot(): ItemIdSlot { syntheticItemOrdinal += 1; - return SYNTHETIC_ITEM_ORDINAL_BASE + syntheticItemOrdinal; + return { kind: "fallback", ordinal: syntheticItemOrdinal }; } /** @@ -103,11 +116,11 @@ function backfillContentArray(content: unknown): unknown { * Also backfills a missing required id on the item itself. * Returns the same object reference if nothing changed. */ -function backfillOutputItem(item: unknown, outputIndex: number): unknown { +function backfillOutputItem(item: unknown, slot: ItemIdSlot): unknown { if (!isPlainObject(item)) return item; const content = item.content; const repaired = backfillContentArray(content); - const withId = backfillItemId(item, outputIndex); + const withId = backfillItemId(item, slot); if (repaired === content && withId === item) return item; return { ...withId, ...(repaired === content ? {} : { content: repaired }) }; } @@ -123,7 +136,7 @@ function backfillResponseOutput(response: unknown): unknown { let changed = false; const repaired = output.map((item, idx) => { if (!isPlainObject(item)) return item; - const next = backfillOutputItem(item, idx); + const next = backfillOutputItem(item, { kind: "index", index: idx }); if (next !== item) changed = true; return next; }); @@ -149,8 +162,8 @@ function rewriteEvent(event: Record): Record { // is not recoverable in that case, but a unique id is what strict decoders require, and a // well-formed stream still gets the stable index-derived id. const item = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 - ? backfillOutputItem(event.item, rawIndex) - : backfillOutputItem(event.item, nextSyntheticItemOrdinal()); + ? backfillOutputItem(event.item, { kind: "index", index: rawIndex }) + : backfillOutputItem(event.item, nextSyntheticItemSlot()); if (item !== event.item) { next = { ...next, item }; changed = true; diff --git a/tests/responses-field-backfill.test.ts b/tests/responses-field-backfill.test.ts index 022ff86e4f..8575b0727c 100644 --- a/tests/responses-field-backfill.test.ts +++ b/tests/responses-field-backfill.test.ts @@ -267,7 +267,8 @@ describe("responses-field-backfill", () => { }; const [out] = apply(sseBlock(event)); const parsed = parseData([out])[0]; - expect(parsed.item.id).toMatch(/^msg_ocx_\d+$/); + // The fallback carries its own namespace so it can never equal an index-derived id. + expect(parsed.item.id).toMatch(/^msg_ocx_fallback_\d+$/); } }); @@ -287,8 +288,8 @@ describe("responses-field-backfill", () => { const first = parseData(apply(sseBlock(event("one"))))[0]; const second = parseData(apply(sseBlock(event("two"))))[0]; - expect(first.item.id).toMatch(/^msg_ocx_\d+$/); - expect(second.item.id).toMatch(/^msg_ocx_\d+$/); + expect(first.item.id).toMatch(/^msg_ocx_fallback_\d+$/); + expect(second.item.id).toMatch(/^msg_ocx_fallback_\d+$/); expect(first.item.id).not.toBe(second.item.id); }); @@ -348,4 +349,57 @@ describe("responses-field-backfill", () => { const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as typeof response; expect(result.output[0].id).toBe("msg_real"); }); + + test("the canonical image_generation_call type gets its own prefix", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [{ type: "image_generation_call", result: "..." }], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as { + output: { id: string }[]; + }; + // The wire type is `image_generation_call`; keying the table on the short spelling alone + // silently demoted every real one to the generic `item_` prefix. + expect(result.output[0]!.id).toBe("ig_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 + // exists to prevent. + test("a fallback id cannot collide with any index-derived id", () => { + const malformed = parseData(apply(sseBlock({ + type: "response.output_item.added", + output_index: "not-a-number", + item: { type: "message", role: "assistant", content: [] }, + }))); + const fallbackId = (malformed[0]!.item as { id: string }).id; + + // Every index-derived id is `msg_ocx_`; the fallback namespace is lexically + // disjoint from it, so no integer index can ever produce this string. + expect(fallbackId).toMatch(/^msg_ocx_fallback_\d+$/); + expect(fallbackId).not.toMatch(/^msg_ocx_\d+$/); + + for (const index of [0, 1, 1_000_000, 1_000_001, 1_000_002]) { + const derived = parseData(apply(sseBlock({ + type: "response.output_item.added", + output_index: index, + item: { type: "message", role: "assistant", content: [] }, + }))); + expect((derived[0]!.item as { id: string }).id).not.toBe(fallbackId); + } + }); + + test("consecutive malformed indexes still get distinct ids", () => { + const ids = [0, 1].map(() => { + const out = parseData(apply(sseBlock({ + type: "response.output_item.added", + item: { type: "message", role: "assistant", content: [] }, + }))); + return (out[0]!.item as { id: string }).id; + }); + expect(new Set(ids).size).toBe(2); + }); }); From 26a15eec4baba18cbcc53a9a7d2752ffcf495537 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 20 Aug 2026 11:37:32 +0900 Subject: [PATCH 3/3] fix(responses): leave the compact wire format out of the id backfill The compact endpoint returns `{ type: "compaction", encrypted_content }`, which is its own wire contract and carries no id. The Responses id backfill treated it as an output item and synthesized one, changing a body that clients compare exactly. This only surfaced once the backfill and the compact endpoint were on the same tree, which is why the integration branch is built and tested before anything merges rather than trusting per-branch CI. The backfill exists to satisfy strict Responses decoders; a shape those decoders never see is outside its remit. --- .../responses/responses-field-backfill.ts | 11 +++++++++++ tests/responses-field-backfill.test.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/server/responses/responses-field-backfill.ts b/src/server/responses/responses-field-backfill.ts index 285ad3d956..4d67531020 100644 --- a/src/server/responses/responses-field-backfill.ts +++ b/src/server/responses/responses-field-backfill.ts @@ -111,6 +111,16 @@ function backfillContentArray(content: unknown): unknown { return changed ? repaired : content; } +/** + * Item types that are NOT Responses output items and must be returned byte-for-byte. + * + * `compaction` is the `/v1/responses/compact` wire format, not a Responses output item. It has + * no `id` in that contract, so synthesizing one changes a response body the client compares + * exactly. The backfill exists to satisfy strict Responses decoders; a shape those decoders + * never see is outside its remit. + */ +const NON_RESPONSES_ITEM_TYPES: ReadonlySet = new Set(["compaction"]); + /** * Walk an output item and backfill output_text parts in its content. * Also backfills a missing required id on the item itself. @@ -118,6 +128,7 @@ function backfillContentArray(content: unknown): unknown { */ function backfillOutputItem(item: unknown, slot: ItemIdSlot): unknown { if (!isPlainObject(item)) return item; + if (typeof item.type === "string" && NON_RESPONSES_ITEM_TYPES.has(item.type)) return item; const content = item.content; const repaired = backfillContentArray(content); const withId = backfillItemId(item, slot); diff --git a/tests/responses-field-backfill.test.ts b/tests/responses-field-backfill.test.ts index 8575b0727c..f6fb63bae3 100644 --- a/tests/responses-field-backfill.test.ts +++ b/tests/responses-field-backfill.test.ts @@ -402,4 +402,22 @@ describe("responses-field-backfill", () => { }); expect(new Set(ids).size).toBe(2); }); + + // `compaction` is the /v1/responses/compact wire format, not a Responses output item: it + // carries no id in that contract, and clients compare the body exactly. Synthesizing an id + // here changed a response that had nothing to do with strict Responses decoding — a defect + // that only appeared once this backfill and the compact endpoint were on the same tree. + test("a compaction item is returned byte-for-byte", () => { + const response = { + id: "resp_1", + object: "response", + status: "completed", + output: [{ type: "compaction", encrypted_content: "gAAAAAB-test-opaque" }], + }; + const result = JSON.parse(backfillResponsesFieldsJson(JSON.stringify(response))) as { + output: Record[]; + }; + expect(result.output[0]).toEqual({ type: "compaction", encrypted_content: "gAAAAAB-test-opaque" }); + expect(result.output[0]).not.toHaveProperty("id"); + }); });