From 8cea418f459457185a939775b30a310ff3204ab8 Mon Sep 17 00:00:00 2001 From: Erez Date: Tue, 7 Apr 2026 19:52:19 +0300 Subject: [PATCH 1/3] fix: use action.query for web_search_call items to match OpenAI API format The web_search_call items were placing `query` directly on the item object, but the real OpenAI Responses API nests it at `item.action.query`. This caused consumers to crash with `Cannot read properties of undefined (reading 'query')` when accessing `event.item.action.query`. --- src/__tests__/reasoning-web-search.test.ts | 16 ++++++++-------- src/__tests__/stream-collapse.test.ts | 4 ++-- src/responses.ts | 6 +++--- src/stream-collapse.ts | 9 ++++++--- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/__tests__/reasoning-web-search.test.ts b/src/__tests__/reasoning-web-search.test.ts index 93cf588..384bd57 100644 --- a/src/__tests__/reasoning-web-search.test.ts +++ b/src/__tests__/reasoning-web-search.test.ts @@ -235,10 +235,10 @@ describe("POST /v1/responses (web search streaming)", () => { (e) => e.type === "response.output_item.done" && (e.item as { type: string })?.type === "web_search_call", - ) as (SSEEvent & { item: { query: string } })[]; + ) as (SSEEvent & { item: { action: { query: string } } })[]; - expect(searchDone[0].item.query).toBe("latest news"); - expect(searchDone[1].item.query).toBe("weather forecast"); + expect(searchDone[0].item.action.query).toBe("latest news"); + expect(searchDone[1].item.action.query).toBe("weather forecast"); }); it("response.completed includes web search output items", async () => { @@ -251,14 +251,14 @@ describe("POST /v1/responses (web search streaming)", () => { const events = parseResponsesSSEEvents(res.body); const completed = events.find((e) => e.type === "response.completed") as SSEEvent & { - response: { output: { type: string; query?: string }[] }; + response: { output: { type: string; action?: { query: string } }[] }; }; expect(completed).toBeDefined(); const searchOutputs = completed.response.output.filter((o) => o.type === "web_search_call"); expect(searchOutputs).toHaveLength(2); - expect(searchOutputs[0].query).toBe("latest news"); - expect(searchOutputs[1].query).toBe("weather forecast"); + expect(searchOutputs[0].action!.query).toBe("latest news"); + expect(searchOutputs[1].action!.query).toBe("weather forecast"); }); }); @@ -355,8 +355,8 @@ describe("POST /v1/responses (non-streaming with reasoning)", () => { const searchOutputs = body.output.filter((o: { type: string }) => o.type === "web_search_call"); expect(searchOutputs).toHaveLength(2); - expect(searchOutputs[0].query).toBe("latest news"); - expect(searchOutputs[1].query).toBe("weather forecast"); + expect(searchOutputs[0].action.query).toBe("latest news"); + expect(searchOutputs[1].action.query).toBe("weather forecast"); }); it("combined non-streaming response has correct output order", async () => { diff --git a/src/__tests__/stream-collapse.test.ts b/src/__tests__/stream-collapse.test.ts index fc154c4..b5b304a 100644 --- a/src/__tests__/stream-collapse.test.ts +++ b/src/__tests__/stream-collapse.test.ts @@ -1715,12 +1715,12 @@ describe("collapseOpenAISSE with reasoning", () => { "", `data: ${JSON.stringify({ type: "response.output_item.done", - item: { type: "web_search_call", status: "completed", query: "test query" }, + item: { type: "web_search_call", status: "completed", action: { query: "test query" } }, })}`, "", `data: ${JSON.stringify({ type: "response.output_item.done", - item: { type: "web_search_call", status: "completed", query: "another query" }, + item: { type: "web_search_call", status: "completed", action: { query: "another query" } }, })}`, "", `data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Result" })}`, diff --git a/src/responses.ts b/src/responses.ts index d7fa30d..5b7e18f 100644 --- a/src/responses.ts +++ b/src/responses.ts @@ -505,7 +505,7 @@ function buildWebSearchStreamEvents( type: "web_search_call", id: searchId, status: "in_progress", - query: queries[i], + action: { query: queries[i] }, }, }); @@ -516,7 +516,7 @@ function buildWebSearchStreamEvents( type: "web_search_call", id: searchId, status: "completed", - query: queries[i], + action: { query: queries[i] }, }, }); } @@ -550,7 +550,7 @@ function buildTextResponse( type: "web_search_call", id: generateId("ws"), status: "completed", - query, + action: { query }, }); } } diff --git a/src/stream-collapse.ts b/src/stream-collapse.ts index 01c5d87..1ecbe59 100644 --- a/src/stream-collapse.ts +++ b/src/stream-collapse.ts @@ -72,9 +72,12 @@ export function collapseOpenAISSE(body: string): CollapseResult { // Responses API web search events if (parsed.type === "response.output_item.done") { const item = parsed.item as Record | undefined; - if (item?.type === "web_search_call" && typeof item.query === "string") { - webSearchQueries.push(item.query); - continue; + if (item?.type === "web_search_call") { + const action = item.action as Record | undefined; + if (action && typeof action.query === "string") { + webSearchQueries.push(action.query); + continue; + } } } From 0ffe42f8c85a7abce60fa10dda094f7c147d73d3 Mon Sep 17 00:00:00 2001 From: Erez Date: Tue, 7 Apr 2026 20:01:14 +0300 Subject: [PATCH 2/3] fix: remove unnecessary non-null assertions in tests --- src/__tests__/reasoning-web-search.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/reasoning-web-search.test.ts b/src/__tests__/reasoning-web-search.test.ts index 384bd57..99b9895 100644 --- a/src/__tests__/reasoning-web-search.test.ts +++ b/src/__tests__/reasoning-web-search.test.ts @@ -251,14 +251,14 @@ describe("POST /v1/responses (web search streaming)", () => { const events = parseResponsesSSEEvents(res.body); const completed = events.find((e) => e.type === "response.completed") as SSEEvent & { - response: { output: { type: string; action?: { query: string } }[] }; + response: { output: { type: string; action: { query: string } }[] }; }; expect(completed).toBeDefined(); const searchOutputs = completed.response.output.filter((o) => o.type === "web_search_call"); expect(searchOutputs).toHaveLength(2); - expect(searchOutputs[0].action!.query).toBe("latest news"); - expect(searchOutputs[1].action!.query).toBe("weather forecast"); + expect(searchOutputs[0].action.query).toBe("latest news"); + expect(searchOutputs[1].action.query).toBe("weather forecast"); }); }); From d126d3c11658cd30df7aabf83f55a65fe8319132 Mon Sep 17 00:00:00 2001 From: Erez Date: Tue, 7 Apr 2026 20:01:36 +0300 Subject: [PATCH 3/3] docs: add reasoning and webSearches to Response Types table --- docs/fixtures.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fixtures.html b/docs/fixtures.html index c913956..2f292ab 100644 --- a/docs/fixtures.html +++ b/docs/fixtures.html @@ -146,7 +146,7 @@

Response Types

Text - content, role?, finishReason? + content, role?, finishReason?, reasoning?, webSearches? Plain text response