From e288d1e6270ec30a88bb756c7e2b73debdb0f882 Mon Sep 17 00:00:00 2001 From: "kunqi.lai" Date: Mon, 31 Aug 2026 17:58:39 +0800 Subject: [PATCH 1/2] fix(responses): always carry query on web_search_call for Console Go DeepSeek's native Responses parser requires 'queries', while Console Go's upstream validator requires 'query'. A multi-query web_search_call emitted by webSearchAction only carried 'queries', so replayed history 400'd on every subsequent turn with: input[N].action missing required field 'query' (sibling of #930). - webSearchAction now always includes action.query (first query) alongside queries, satisfying both strict parsers. - backfillWebSearchQueries repairs pre-existing recorded items in either missing direction (adds queries from query, or query from queries[0]). Verified: a Responses request replaying an old-format multi-query web_search_call now completes instead of 400ing on upstream. --- src/adapters/openai-responses.ts | 16 +++++++++++++--- src/bridge.ts | 22 +++++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 047c60a6a3..a420434336 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -920,9 +920,19 @@ function backfillWebSearchQueries(body: unknown): unknown { if (!isPlainObject(item) || item.type !== "web_search_call") return item; const action = item.action; if (!isPlainObject(action) || action.type !== "search") return item; - if (typeof action.query !== "string" || Array.isArray(action.queries)) return item; - changed = true; - return { ...item, action: { ...action, queries: [action.query] } }; + // Repair whichever side is missing so both strict parsers pass: + // DeepSeek native Responses requires `queries`; Console Go requires `query`. + const rep: Record = { ...action }; + let itemChanged = false; + if (typeof action.query !== "string" && Array.isArray(action.queries) && action.queries.length > 0) { + rep.query = action.queries[0]; // multi-query item recorded before the fix + itemChanged = true; + } else if (typeof action.query === "string" && !Array.isArray(action.queries)) { + rep.queries = [action.query]; // single-query item recorded before the fix + itemChanged = true; + } + if (itemChanged) changed = true; + return itemChanged ? { ...item, action: rep } : item; }); return changed ? { ...body, input } : body; } diff --git a/src/bridge.ts b/src/bridge.ts index dcb163553c..e45fb48ab6 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -149,24 +149,20 @@ export { adapterFailureFromMessage } from "./lib/errors"; * Single query → `{ query, queries: [query] }`. Batch → `{ queries }` with NO singular * `query`. Empty → `{ query: "", queries: [""] }`. * - * The asymmetry is load-bearing in both directions. codex-rs prefers a non-empty `query` - * for the cell label and renders " ..." only when `query` is ABSENT and - * `queries.len() > 1`, so adding `query` to a batch would collapse the plural ellipsis. - * Meanwhile DeepSeek's native Responses parser makes `queries` a required field, so a - * replayed one-term `web_search_call` — carried in the history of every subsequent turn - * — fails deserialization with `missing field 'queries'` and 400s the rest of the - * conversation (#930). Carrying both keys in the single case satisfies the strict parser - * without changing what codex-rs displays. + * The asymmetry is load-bearing in both directions. DeepSeek's native Responses parser + * makes `queries` a required field, and Console Go's upstream validator makes `query` a + * required field — so a replayed `web_search_call` carried in the history of every + * subsequent turn fails deserialization with `missing field 'queries'` (#930) or 400s + * with `missing required field 'query'` unless both keys are present. Carrying both keys + * in every case satisfies both strict parsers; the trade-off is that a multi-query batch + * loses the " ..." ellipsis in codex-rs and shows the first query as the label. * * This fixes items created from here on. History recorded before it is repaired at the * replay boundary by `backfillWebSearchQueries()` in the Responses adapter. */ function webSearchAction(queries: string[]): Record { - if (queries.length <= 1) { - const query = queries[0] ?? ""; - return { type: "search", query, queries: [query] }; - } - return { type: "search", queries }; + const first = queries[0] ?? ""; + return { type: "search", query: first, queries: queries.length > 0 ? queries : [first] }; } interface OutputItem { From 5cf5cc1d23025f3e30c8282b0f524212d3b88731 Mon Sep 17 00:00:00 2001 From: "kunqi.lai" Date: Mon, 31 Aug 2026 18:26:59 +0800 Subject: [PATCH 2/2] test(responses): cover query+queries parity on web_search_call (#3071) - bridge: batched search now asserts both 'query' and 'queries' (was: queries-only), matching the new webSearchAction output. - passthrough: backfill test now asserts the reverse repair direction (multi-query action gains singular 'query'), matching the extended backfillWebSearchQueries. --- tests/bridge.test.ts | 9 +++++---- tests/openai-responses-passthrough.test.ts | 13 ++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/bridge.test.ts b/tests/bridge.test.ts index 444f3d6e77..420b2f6996 100644 --- a/tests/bridge.test.ts +++ b/tests/bridge.test.ts @@ -1075,7 +1075,7 @@ describe("Responses bridge web_search_call native item", () => { }); }); - test("a batched (plural) search emits action.search.queries without a singular query", () => { + test("a batched (plural) search carries both query and queries for Console Go (#3071)", () => { const json = buildResponseJSON([ { type: "web_search_call_begin", id: "ws_3" }, { type: "web_search_call_end", id: "ws_3", queries: ["rust async", "tokio runtime"] }, @@ -1085,9 +1085,10 @@ describe("Responses bridge web_search_call native item", () => { const output = json.output as Record[]; const action = (output[0] as Record).action as Record; - // Native renders " ..." only when `query` is absent and queries.len() > 1. - expect(action).toEqual({ type: "search", queries: ["rust async", "tokio runtime"] }); - expect(action.query).toBeUndefined(); + // Console Go's upstream validator requires singular `query` on the search action, + // and DeepSeek native Responses requires `queries` — so a batch carries both now. + expect(action).toEqual({ type: "search", query: "rust async", queries: ["rust async", "tokio runtime"] }); + expect(action.query).toBe("rust async"); }); test("a single-query search also carries queries so strict parsers accept the replay (#930)", () => { diff --git a/tests/openai-responses-passthrough.test.ts b/tests/openai-responses-passthrough.test.ts index 6687671023..167a679039 100644 --- a/tests/openai-responses-passthrough.test.ts +++ b/tests/openai-responses-passthrough.test.ts @@ -1295,11 +1295,11 @@ describe("OpenAI Responses passthrough sanitization", () => { expect(input[0]).not.toHaveProperty("id"); }); - test("backfills queries on a replayed single-query web_search_call (#930)", () => { + test("backfills web_search_call actions in either missing direction (#930, #3071)", () => { // The bridge fix only helps items created after it. A conversation that already - // recorded {type:"search", query:"..."} replays that stored item every turn, and - // DeepSeek's parser rejects the whole request over it — so upgrading alone would - // leave those threads permanently broken. + // recorded a legacy web_search_call replays that stored item every turn. DeepSeek's + // parser rejects an action without `queries` (#930) and Console Go rejects one + // without `query` (#3071) — so upgrading alone would leave those threads broken. const adapter = createResponsesPassthroughAdapter(provider); const request = adapter.buildRequest({ modelId: "provider-model", @@ -1319,9 +1319,8 @@ describe("OpenAI Responses passthrough sanitization", () => { // Repaired: singular query gains the array the strict parser requires. expect(input[0].action).toEqual({ type: "search", query: "legacy", queries: ["legacy"] }); - // Untouched: a batch already satisfies the parser, and adding `query` would collapse - // the native plural rendering. - expect(input[1].action).toEqual({ type: "search", queries: ["a", "b"] }); + // Repaired: multi-query batch gains the singular `query` Console Go requires. + expect(input[1].action).toEqual({ type: "search", query: "a", queries: ["a", "b"] }); // Untouched: not a search action. expect(input[2].action).toEqual({ type: "open_page", url: "https://example.test" }); });