fix(llm/openai): normalize tool_choice "any" → "required"
OpenAI rejects tool_choice="any" with a 400 (only accepts none/auto/required), but Anthropic and Gemini backends both treat "any" and "required" as equivalent (anthropic.py:321, gemini.py:568), and tool_loop.py:382 assumes the same. Callers passing "any" — including via env/config overrides not covered by the default-only patch in #630 — would still hit the error. Map "any" → "required" inside the OpenAI backend so callers can use either spelling without provider-specific awareness.
This commit is contained in:
parent
f37338b855
commit
ca96c8a5fa
|
|
@ -309,6 +309,10 @@ class OpenAIBackend:
|
|||
if tools:
|
||||
params["tools"] = self._convert_tools(tools)
|
||||
if tool_choice is not None:
|
||||
# OpenAI accepts only "none" | "auto" | "required" (or a function spec).
|
||||
# Other backends use "any" with the same semantics as "required".
|
||||
if tool_choice == "any":
|
||||
tool_choice = "required"
|
||||
params["tool_choice"] = tool_choice
|
||||
if extra_params:
|
||||
for key in (
|
||||
|
|
|
|||
|
|
@ -230,6 +230,59 @@ async def test_openai_backend_converts_anthropic_style_tools() -> None:
|
|||
assert call["tool_choice"] == "required"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_backend_normalizes_any_tool_choice_to_required() -> None:
|
||||
"""OpenAI rejects tool_choice="any" (Anthropic/Gemini's spelling for the
|
||||
same semantics OpenAI calls "required"). The backend must translate it
|
||||
so callers can pass either spelling without provider-specific awareness.
|
||||
"""
|
||||
client = Mock()
|
||||
client.chat.completions.create = AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
finish_reason="stop",
|
||||
message=SimpleNamespace(
|
||||
content="ok",
|
||||
tool_calls=[],
|
||||
reasoning_details=[],
|
||||
),
|
||||
)
|
||||
],
|
||||
usage=SimpleNamespace(
|
||||
prompt_tokens=10,
|
||||
completion_tokens=5,
|
||||
prompt_tokens_details=None,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
backend = OpenAIBackend(client)
|
||||
await backend.complete(
|
||||
model="gpt-4.1",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
max_tokens=100,
|
||||
tools=[
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Lookup weather",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
"required": ["city"],
|
||||
},
|
||||
}
|
||||
],
|
||||
tool_choice="any",
|
||||
)
|
||||
|
||||
await_args = client.chat.completions.create.await_args
|
||||
if await_args is None:
|
||||
raise AssertionError("Expected OpenAI create call")
|
||||
call = await_args.kwargs
|
||||
assert call["tool_choice"] == "required"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
|
|
|
|||
Loading…
Reference in New Issue