Skip to content

Commit c559111

Browse files
authored
fix: use action.query for web_search_call items to match OpenAI API (#89)
## Summary - `web_search_call` items in streaming and non-streaming Responses API output used `query` directly on the item object, but the real OpenAI Responses API nests it at `item.action.query` - Updated `buildWebSearchStreamEvents` and `buildTextResponse` in `responses.ts` - Updated `collapseOpenAISSE` in `stream-collapse.ts` to read from `item.action.query` - Updated tests to match - Added `reasoning?` and `webSearches?` to the Response Types docs table ## Test plan - [x] All 2068 existing tests pass
2 parents 3bdc79f + d126d3c commit c559111

5 files changed

Lines changed: 20 additions & 17 deletions

File tree

docs/fixtures.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ <h2>Response Types</h2>
146146
<tbody>
147147
<tr>
148148
<td>Text</td>
149-
<td>content, role?, finishReason?</td>
149+
<td>content, role?, finishReason?, reasoning?, webSearches?</td>
150150
<td>Plain text response</td>
151151
</tr>
152152
<tr>

src/__tests__/reasoning-web-search.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,10 @@ describe("POST /v1/responses (web search streaming)", () => {
235235
(e) =>
236236
e.type === "response.output_item.done" &&
237237
(e.item as { type: string })?.type === "web_search_call",
238-
) as (SSEEvent & { item: { query: string } })[];
238+
) as (SSEEvent & { item: { action: { query: string } } })[];
239239

240-
expect(searchDone[0].item.query).toBe("latest news");
241-
expect(searchDone[1].item.query).toBe("weather forecast");
240+
expect(searchDone[0].item.action.query).toBe("latest news");
241+
expect(searchDone[1].item.action.query).toBe("weather forecast");
242242
});
243243

244244
it("response.completed includes web search output items", async () => {
@@ -251,14 +251,14 @@ describe("POST /v1/responses (web search streaming)", () => {
251251

252252
const events = parseResponsesSSEEvents(res.body);
253253
const completed = events.find((e) => e.type === "response.completed") as SSEEvent & {
254-
response: { output: { type: string; query?: string }[] };
254+
response: { output: { type: string; action: { query: string } }[] };
255255
};
256256
expect(completed).toBeDefined();
257257

258258
const searchOutputs = completed.response.output.filter((o) => o.type === "web_search_call");
259259
expect(searchOutputs).toHaveLength(2);
260-
expect(searchOutputs[0].query).toBe("latest news");
261-
expect(searchOutputs[1].query).toBe("weather forecast");
260+
expect(searchOutputs[0].action.query).toBe("latest news");
261+
expect(searchOutputs[1].action.query).toBe("weather forecast");
262262
});
263263
});
264264

@@ -355,8 +355,8 @@ describe("POST /v1/responses (non-streaming with reasoning)", () => {
355355

356356
const searchOutputs = body.output.filter((o: { type: string }) => o.type === "web_search_call");
357357
expect(searchOutputs).toHaveLength(2);
358-
expect(searchOutputs[0].query).toBe("latest news");
359-
expect(searchOutputs[1].query).toBe("weather forecast");
358+
expect(searchOutputs[0].action.query).toBe("latest news");
359+
expect(searchOutputs[1].action.query).toBe("weather forecast");
360360
});
361361

362362
it("combined non-streaming response has correct output order", async () => {

src/__tests__/stream-collapse.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,12 +1715,12 @@ describe("collapseOpenAISSE with reasoning", () => {
17151715
"",
17161716
`data: ${JSON.stringify({
17171717
type: "response.output_item.done",
1718-
item: { type: "web_search_call", status: "completed", query: "test query" },
1718+
item: { type: "web_search_call", status: "completed", action: { query: "test query" } },
17191719
})}`,
17201720
"",
17211721
`data: ${JSON.stringify({
17221722
type: "response.output_item.done",
1723-
item: { type: "web_search_call", status: "completed", query: "another query" },
1723+
item: { type: "web_search_call", status: "completed", action: { query: "another query" } },
17241724
})}`,
17251725
"",
17261726
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Result" })}`,

src/responses.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ function buildWebSearchStreamEvents(
505505
type: "web_search_call",
506506
id: searchId,
507507
status: "in_progress",
508-
query: queries[i],
508+
action: { query: queries[i] },
509509
},
510510
});
511511

@@ -516,7 +516,7 @@ function buildWebSearchStreamEvents(
516516
type: "web_search_call",
517517
id: searchId,
518518
status: "completed",
519-
query: queries[i],
519+
action: { query: queries[i] },
520520
},
521521
});
522522
}
@@ -550,7 +550,7 @@ function buildTextResponse(
550550
type: "web_search_call",
551551
id: generateId("ws"),
552552
status: "completed",
553-
query,
553+
action: { query },
554554
});
555555
}
556556
}

src/stream-collapse.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,12 @@ export function collapseOpenAISSE(body: string): CollapseResult {
7272
// Responses API web search events
7373
if (parsed.type === "response.output_item.done") {
7474
const item = parsed.item as Record<string, unknown> | undefined;
75-
if (item?.type === "web_search_call" && typeof item.query === "string") {
76-
webSearchQueries.push(item.query);
77-
continue;
75+
if (item?.type === "web_search_call") {
76+
const action = item.action as Record<string, unknown> | undefined;
77+
if (action && typeof action.query === "string") {
78+
webSearchQueries.push(action.query);
79+
continue;
80+
}
7881
}
7982
}
8083

0 commit comments

Comments
 (0)