Skip to content
Closed
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
16 changes: 13 additions & 3 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { ...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;
}
Expand Down
22 changes: 9 additions & 13 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<first> ..." 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 "<first> ..." 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<string, unknown> {
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 {
Expand Down
9 changes: 5 additions & 4 deletions tests/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
Expand All @@ -1085,9 +1085,10 @@ describe("Responses bridge web_search_call native item", () => {

const output = json.output as Record<string, unknown>[];
const action = (output[0] as Record<string, unknown>).action as Record<string, unknown>;
// Native renders "<first> ..." 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)", () => {
Expand Down
13 changes: 6 additions & 7 deletions tests/openai-responses-passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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" });
});
Expand Down
Loading