mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
fix(translator): skip replayed web_search_call metadata in Responses-to-Chat
OmniRoute's web-search fallback emits a native web_search_call output item alongside function_call/function_call_output. Responses clients keep that item in conversation history and replay it in the next request's input. When the follow-up turn routes to a Chat Completions target (Claude), the translator hit its default unsupported-feature branch and returned a deterministic HTTP 400: Unsupported Responses API feature: input item type 'web_search_call' cannot be represented in Chat Completions Skip the replayed metadata next to tool_search_call/tool_search_result. The paired function_call_output still carries the search results, so no context is lost and the sources are not duplicated into assistant history.
This commit is contained in:
1
changelog.d/fixes/responses-web-search-call-replay.md
Normal file
1
changelog.d/fixes/responses-web-search-call-replay.md
Normal file
@@ -0,0 +1 @@
|
||||
Responses-to-Chat fallback now skips replayed `web_search_call` metadata while preserving the paired function result, preventing deterministic HTTP 400 failures on follow-up turns routed to Chat Completions providers.
|
||||
@@ -468,14 +468,16 @@ export function openaiResponsesToOpenAIRequest(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip tool_search_call items. These are Responses-API-only metadata items
|
||||
// emitted by Codex's dynamic tool-search optimization: they record that the
|
||||
// model queried a subset of available tools, but carry no content that Chat
|
||||
// Completions can represent. Throwing here would break every multi-turn
|
||||
// conversation where Codex previously used tool_search (the whole session
|
||||
// would carry tool_search_call items forward in `input`). Skipping matches
|
||||
// the reasoning-item policy: display-only metadata, no chat side-effect.
|
||||
if (itemType === "tool_search_call" || itemType === "tool_search_result") {
|
||||
// Skip Responses-only search metadata. tool_search_call/tool_search_result
|
||||
// are Codex's dynamic tool-discovery items; web_search_call is emitted by
|
||||
// OmniRoute's web-search fallback alongside function_call_output, which
|
||||
// already carries the result for Chat Completions. Replayed metadata has no
|
||||
// lossless Chat representation and must not fail a follow-up turn.
|
||||
if (
|
||||
itemType === "tool_search_call" ||
|
||||
itemType === "tool_search_result" ||
|
||||
itemType === "web_search_call"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,6 @@ test("Responses -> Chat rejects input item types without a lossless Chat equival
|
||||
{ type: "item_reference", id: "item_123" },
|
||||
{ type: "computer_call_output", call_id: "call_1", output: {} },
|
||||
{ type: "mcp_call", name: "remote", arguments: "{}" },
|
||||
{ type: "web_search_call", id: "search_1" },
|
||||
{ unexpected: true },
|
||||
]) {
|
||||
assert.throws(
|
||||
|
||||
@@ -58,6 +58,51 @@ test("tool_search_result input item is silently skipped", () => {
|
||||
assert.equal(messages[0].role, "user");
|
||||
});
|
||||
|
||||
test("web_search_call replay is skipped while its function result is preserved", () => {
|
||||
const body = {
|
||||
model: "test-model",
|
||||
input: [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Find docs" }] },
|
||||
{
|
||||
type: "function_call",
|
||||
call_id: "call_search",
|
||||
name: "omniroute_web_search",
|
||||
arguments: '{"query":"OmniRoute docs"}',
|
||||
},
|
||||
{
|
||||
type: "function_call_output",
|
||||
call_id: "call_search",
|
||||
output: '{"success":true,"results":[{"url":"https://example.com","title":"Example"}]}',
|
||||
},
|
||||
{
|
||||
type: "web_search_call",
|
||||
id: "ws_call_search",
|
||||
status: "completed",
|
||||
action: {
|
||||
type: "web_search",
|
||||
query: "OmniRoute docs",
|
||||
sources: [{ title: "Example", url: "https://example.com", caption: "Result" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
};
|
||||
let result;
|
||||
assert.doesNotThrow(() => {
|
||||
result = translateRequest("openai-responses", "openai", "test-model", body, false);
|
||||
}, "web_search_call replay must not throw");
|
||||
const messages = (
|
||||
result as { messages?: Array<{ role?: string; tool_calls?: unknown; content?: unknown }> }
|
||||
).messages;
|
||||
assert.ok(Array.isArray(messages));
|
||||
assert.equal(messages.length, 3, "web_search_call metadata must not create a duplicate message");
|
||||
assert.deepEqual(
|
||||
messages.map((message) => message.role),
|
||||
["user", "assistant", "tool"]
|
||||
);
|
||||
assert.equal(messages[2].content, body.input[2].output);
|
||||
});
|
||||
|
||||
test("multiple tool_search_call items interspersed with messages are skipped in order", () => {
|
||||
const body = {
|
||||
model: "test-model",
|
||||
|
||||
Reference in New Issue
Block a user