fix(translator): strip neutral tool_choice when tools absent in Responses-to-Chat (#12141) (#12166)

Signed-off-by: Minxi Hou <houminxi@gmail.com>
This commit is contained in:
Bob.Hou
2026-09-01 10:02:35 -04:00
committed by GitHub
parent 0ff164701d
commit 9629d78ece
3 changed files with 104 additions and 4 deletions

View File

@@ -768,6 +768,15 @@ export function openaiResponsesToOpenAIRequest(
}
}
// #12141: When translated Chat tools is empty/absent, strip neutral tool_choice
// ("auto" / "none") so strict Chat endpoints (e.g. vLLM) do not reject with 400
// ("When using tool_choice, tools must be set"). Contradictory choices like "required"
// or forced functions are preserved so the upstream error remains visible.
const finalChatTools = Array.isArray(result.tools) ? result.tools : [];
if (finalChatTools.length === 0 && (result.tool_choice === "auto" || result.tool_choice === "none")) {
delete result.tool_choice;
}
// Cleanup Responses API specific fields
// Note: prompt_cache_key is intentionally preserved for OpenAI destinations — it is
// used by Codex as a cache-affinity signal and stripping it unconditionally broke

View File

@@ -0,0 +1,82 @@
import test from "node:test";
import assert from "node:assert/strict";
const { openaiResponsesToOpenAIRequest } = await import(
"../../open-sse/translator/request/openai-responses.ts"
);
test("#12141: Responses-to-Chat strips tool_choice='auto' when tools is empty array", () => {
const body = {
model: "vllm/qwen-2.5-72b",
input: "hello",
tools: [],
tool_choice: "auto",
stream: false,
};
const result = openaiResponsesToOpenAIRequest(null, body, null, null) as Record<string, unknown>;
assert.equal(
result.tool_choice,
undefined,
"Neutral tool_choice 'auto' must be omitted when no tools exist"
);
});
test("#12141: Responses-to-Chat strips tool_choice='none' when tools is absent or empty", () => {
const body = {
model: "vllm/qwen-2.5-72b",
input: "hello",
tool_choice: "none",
stream: false,
};
const result = openaiResponsesToOpenAIRequest(null, body, null, null) as Record<string, unknown>;
assert.equal(
result.tool_choice,
undefined,
"Neutral tool_choice 'none' must be omitted when no tools exist"
);
});
test("#12141: Responses-to-Chat preserves tools and tool_choice='auto' when valid tools are present", () => {
const body = {
model: "vllm/qwen-2.5-72b",
input: "what is the weather?",
tools: [
{
type: "function",
name: "get_weather",
description: "Get weather",
parameters: { type: "object", properties: { location: { type: "string" } } },
},
],
tool_choice: "auto",
stream: false,
};
const result = openaiResponsesToOpenAIRequest(null, body, null, null) as Record<string, unknown>;
assert.ok(Array.isArray(result.tools), "tools must be preserved as an array");
assert.equal((result.tools as unknown[]).length, 1);
assert.equal(result.tool_choice, "auto", "tool_choice 'auto' must be preserved when tools exist");
});
test("#12141: Responses-to-Chat preserves tool_choice='required' when tools is empty (explicit contradiction)", () => {
const body = {
model: "vllm/qwen-2.5-72b",
input: "hello",
tools: [],
tool_choice: "required",
stream: false,
};
const result = openaiResponsesToOpenAIRequest(null, body, null, null) as Record<string, unknown>;
assert.equal(
result.tool_choice,
"required",
"Contradictory tool_choice 'required' must be preserved so upstream surfaces the error"
);
});

View File

@@ -405,10 +405,19 @@ test("Chat→Responses: tool_choice {type:'function', function:{name}} unwrapped
});
});
test("ResponsesChat: 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 as any).tool_choice, "auto");
test("Responses->Chat: string tool_choice passes through when tools present, stripped when absent (#12141)", () => {
const withTools = {
model: "gpt-4",
input: "hello",
tools: [{ type: "function", name: "f", parameters: {} }],
tool_choice: "auto",
};
const resWith = openaiResponsesToOpenAIRequest(null, withTools, null, null) as Record<string, unknown>;
assert.equal(resWith.tool_choice, "auto");
const noTools = { model: "gpt-4", input: "hello", tool_choice: "auto" };
const resWithout = openaiResponsesToOpenAIRequest(null, noTools, null, null) as Record<string, unknown>;
assert.equal(resWithout.tool_choice, undefined);
});
test("Chat→Responses: string tool_choice passes through unchanged", () => {