From a48f7b2222e1f8bb10a7879aaf7d3fe94facbf01 Mon Sep 17 00:00:00 2001 From: AveryanAlex Date: Sun, 29 Mar 2026 00:14:26 +0300 Subject: [PATCH] fix: translate tool_choice object format between Responses and Chat APIs --- .../translator/request/openai-responses.ts | 31 ++++++++++ .../unit/responses-translation-fixes.test.mjs | 56 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/open-sse/translator/request/openai-responses.ts b/open-sse/translator/request/openai-responses.ts index 715126044b..357c6e6cc4 100644 --- a/open-sse/translator/request/openai-responses.ts +++ b/open-sse/translator/request/openai-responses.ts @@ -244,6 +244,20 @@ export function openaiResponsesToOpenAIRequest( return true; }); + // Translate tool_choice object format: Responses {type,name} → Chat {type,function:{name}} + if (result.tool_choice && typeof result.tool_choice === "object" && !Array.isArray(result.tool_choice)) { + const tc = toRecord(result.tool_choice); + const tcType = toString(tc.type); + if (tcType === "function" && tc.name !== undefined && !tc.function) { + result.tool_choice = { type: "function", function: { name: tc.name } }; + } else if (tcType && tcType !== "function" && tcType !== "allowed_tools") { + // Built-in tool types (web_search_preview, file_search, etc.) have no Chat equivalent + throw unsupportedFeature( + `Unsupported Responses API feature: tool_choice type '${tcType}' is not supported by omniroute` + ); + } + } + // Cleanup Responses API specific fields // Note: prompt_cache_key is intentionally preserved — it is used by Codex and other // providers as a cache-affinity signal. Stripping it breaks prompt caching (#517). @@ -440,6 +454,23 @@ export function openaiToOpenAIResponsesRequest( }); } + // Translate tool_choice: Chat {type,function:{name}} → Responses {type,name} + if (root.tool_choice !== undefined) { + if (typeof root.tool_choice === "string") { + result.tool_choice = root.tool_choice; + } else if (typeof root.tool_choice === "object" && !Array.isArray(root.tool_choice)) { + const tc = toRecord(root.tool_choice); + if (tc.type === "function" && tc.function) { + const fn = toRecord(tc.function); + result.tool_choice = { type: "function", name: fn.name }; + } else { + result.tool_choice = root.tool_choice; + } + } else { + result.tool_choice = root.tool_choice; + } + } + // Pass through relevant fields if (root.service_tier !== undefined) result.service_tier = root.service_tier; if (root.temperature !== undefined) result.temperature = root.temperature; diff --git a/tests/unit/responses-translation-fixes.test.mjs b/tests/unit/responses-translation-fixes.test.mjs index 7c41feb9df..129fc3ceef 100644 --- a/tests/unit/responses-translation-fixes.test.mjs +++ b/tests/unit/responses-translation-fixes.test.mjs @@ -158,3 +158,59 @@ test("Chat→Responses: file content part converted to input_file", () => { assert.equal(filePart.file_id, "file-abc"); assert.equal(filePart.filename, "data.csv"); }); + +test("Responses→Chat: tool_choice {type:'function', name} wrapped to {type:'function', function:{name}}", () => { + const body = { + model: "gpt-4", + input: "hello", + tool_choice: { type: "function", name: "get_weather" }, + tools: [{ type: "function", name: "get_weather", parameters: {} }], + }; + const result = openaiResponsesToOpenAIRequest(null, body, null, null); + assert.deepEqual(result.tool_choice, { + type: "function", + function: { name: "get_weather" }, + }); +}); + +test("Chat→Responses: tool_choice {type:'function', function:{name}} unwrapped to {type:'function', name}", () => { + const body = { + model: "gpt-4", + messages: [{ role: "user", content: "hello" }], + tool_choice: { type: "function", function: { name: "get_weather" } }, + tools: [{ type: "function", function: { name: "get_weather", parameters: {} } }], + }; + const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, null); + assert.deepEqual(result.tool_choice, { + type: "function", + name: "get_weather", + }); +}); + +test("Responses→Chat: string tool_choice passes through unchanged", () => { + const body = { model: "gpt-4", input: "hello", tool_choice: "auto" }; + const result = openaiResponsesToOpenAIRequest(null, body, null, null); + assert.equal(result.tool_choice, "auto"); +}); + +test("Chat→Responses: string tool_choice passes through unchanged", () => { + const body = { + model: "gpt-4", + messages: [{ role: "user", content: "hello" }], + tool_choice: "required", + }; + const result = openaiToOpenAIResponsesRequest("gpt-4", body, true, null); + assert.equal(result.tool_choice, "required"); +}); + +test("Responses→Chat: built-in tool_choice type throws unsupported error", () => { + const body = { + model: "gpt-4", + input: "hello", + tool_choice: { type: "web_search_preview" }, + }; + assert.throws( + () => openaiResponsesToOpenAIRequest(null, body, null, null), + (err) => err.message.includes("web_search_preview") + ); +});