Merge branch 'main' into flaky-test-report

This commit is contained in:
Giles Odigwe
2026-04-30 10:28:28 -07:00
Unverified
31 changed files with 2403 additions and 798 deletions
@@ -1296,6 +1296,12 @@ class RawOpenAIChatClient( # type: ignore[misc]
"type": "function",
"name": func_name,
}
elif mode == "auto" and (allowed := tool_mode.get("allowed_tools")) is not None:
run_options["tool_choice"] = {
"type": "allowed_tools",
"mode": "auto",
"tools": [{"type": "function", "name": name} for name in allowed],
}
else:
run_options["tool_choice"] = mode
else:
@@ -662,6 +662,12 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
"type": "function",
"function": {"name": func_name},
}
elif mode in ("auto", "required") and tool_mode.get("allowed_tools") is not None:
logger.warning(
"allowed_tools is not supported by the Chat Completions API; "
"the setting will be ignored. Use OpenAIChatClient (Responses API) instead."
)
run_options["tool_choice"] = mode
else:
run_options["tool_choice"] = mode
@@ -4272,6 +4272,12 @@ def test_with_callable_api_key() -> None:
True,
id="tool_choice_required",
),
param(
"tool_choice",
{"mode": "auto", "allowed_tools": ["get_weather"]},
True,
id="tool_choice_allowed_tools",
),
param("response_format", OutputStruct, True, id="response_format_pydantic"),
param(
"response_format",
@@ -4822,6 +4828,90 @@ async def test_prepare_options_excludes_continuation_token() -> None:
assert run_options["background"] is True
async def test_prepare_options_allowed_tools() -> None:
"""Test that _prepare_options converts allowed_tools to OpenAI API format."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
@tool
def search_docs(query: str) -> str:
"""Search documentation."""
return f"Results for {query}"
messages = [Message(role="user", contents=[Content.from_text(text="Hello")])]
options: dict[str, Any] = {
"model": "test-model",
"tools": [get_weather, search_docs],
"tool_choice": {"mode": "auto", "allowed_tools": ["get_weather"]},
}
run_options = await client._prepare_options(messages, options)
assert run_options["tool_choice"] == {
"type": "allowed_tools",
"mode": "auto",
"tools": [{"type": "function", "name": "get_weather"}],
}
async def test_prepare_options_allowed_tools_multiple() -> None:
"""Test that _prepare_options converts multiple allowed_tools correctly."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
@tool
def search_docs(query: str) -> str:
"""Search documentation."""
return f"Results for {query}"
messages = [Message(role="user", contents=[Content.from_text(text="Hello")])]
options: dict[str, Any] = {
"model": "test-model",
"tools": [get_weather, search_docs],
"tool_choice": {"mode": "auto", "allowed_tools": ["get_weather", "search_docs"]},
}
run_options = await client._prepare_options(messages, options)
assert run_options["tool_choice"] == {
"type": "allowed_tools",
"mode": "auto",
"tools": [
{"type": "function", "name": "get_weather"},
{"type": "function", "name": "search_docs"},
],
}
async def test_prepare_options_auto_without_allowed_tools() -> None:
"""Test that auto mode without allowed_tools still returns plain 'auto' string."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
return f"Sunny in {city}"
messages = [Message(role="user", contents=[Content.from_text(text="Hello")])]
options: dict[str, Any] = {
"model": "test-model",
"tools": [get_weather],
"tool_choice": {"mode": "auto"},
}
run_options = await client._prepare_options(messages, options)
assert run_options["tool_choice"] == "auto"
# endregion
@@ -1430,6 +1430,57 @@ def test_tool_choice_required_with_function_name(
assert prepared_options["tool_choice"]["function"]["name"] == "get_weather"
def test_tool_choice_allowed_tools_falls_back_to_mode(
openai_unit_test_env: dict[str, str],
) -> None:
"""Test that tool_choice with allowed_tools falls back to plain mode (Chat Completions API unsupported)."""
client = OpenAIChatCompletionClient()
messages = [Message(role="user", contents=["test"])]
options = {
"tools": [get_weather],
"tool_choice": {"mode": "auto", "allowed_tools": ["get_weather"]},
}
prepared_options = client._prepare_options(messages, options)
assert prepared_options["tool_choice"] == "auto"
def test_tool_choice_allowed_tools_required_mode_falls_back(
openai_unit_test_env: dict[str, str],
) -> None:
"""Test that tool_choice with allowed_tools and required mode falls back to 'required'."""
client = OpenAIChatCompletionClient()
messages = [Message(role="user", contents=["test"])]
options = {
"tools": [get_weather],
"tool_choice": {"mode": "required", "allowed_tools": ["get_weather"]},
}
prepared_options = client._prepare_options(messages, options)
assert prepared_options["tool_choice"] == "required"
def test_tool_choice_auto_dict_without_allowed_tools(
openai_unit_test_env: dict[str, str],
) -> None:
"""Test that tool_choice dict with mode auto and no allowed_tools falls through to plain 'auto'."""
client = OpenAIChatCompletionClient()
messages = [Message(role="user", contents=["test"])]
options = {
"tools": [get_weather],
"tool_choice": {"mode": "auto"},
}
prepared_options = client._prepare_options(messages, options)
assert prepared_options["tool_choice"] == "auto"
def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) -> None:
"""Test that response_format as dict is passed through directly."""
client = OpenAIChatCompletionClient()
@@ -1590,6 +1641,12 @@ class OutputStruct(BaseModel):
False,
id="tool_choice_required",
),
param(
"tool_choice",
{"mode": "auto", "allowed_tools": ["get_weather"]},
False,
id="tool_choice_allowed_tools",
),
param("response_format", OutputStruct, True, id="response_format_pydantic"),
param(
"response_format",