From d47c03175bba41a39c4c3f8a947e28938708eef2 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:33:27 +0000 Subject: [PATCH] fix(responses): keep unset-effort gpt-5.4/5.5 tool calls on chat completions (#50968) * fix(responses): keep unset-effort gpt-5.4/5.5 tool calls on chat completions Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(responses): explicitly route bridge fixtures to responses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kenneth Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ...odel_prices_and_context_window_backup.json | 4 + litellm/responses/bridge_check.py | 43 ++---- model_prices_and_context_window.json | 4 + .../sdk/test_unset_effort_tool_bridge_wire.py | 146 ++++++++++++++++++ ...hropic_messages_adapters_transformation.py | 26 ++-- .../test_deferred_guardrail_logging.py | 3 +- .../test_responses_api_bridge_flag.py | 65 +++++++- tests/unit/test_main.py | 23 +-- 8 files changed, 249 insertions(+), 65 deletions(-) create mode 100644 tests/integration/sdk/test_unset_effort_tool_bridge_wire.py diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3145ca4a181..01a32ee7cdb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -72516,6 +72516,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/eu/gpt-5.4-mini": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 8.25e-08, @@ -72537,6 +72538,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/eu/gpt-5.4-nano": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 2.2e-08, @@ -73140,6 +73142,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/us/gpt-5.4-mini": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 8.25e-08, @@ -73161,6 +73164,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/us/gpt-5.4-nano": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 2.2e-08, diff --git a/litellm/responses/bridge_check.py b/litellm/responses/bridge_check.py index 683a51fc6b6..6a55302027d 100644 --- a/litellm/responses/bridge_check.py +++ b/litellm/responses/bridge_check.py @@ -192,35 +192,11 @@ def responses_api_bridge_check( model_info["mode"] = "responses" model = model.replace("responses/", "") - # OpenAI/Azure GPT-5 chat-completions that need Responses-only fields (e.g. - # ``reasoningSummary`` in ``extra_body``) must be bridged; Chat Completions rejects - # those keys. - # - # - gpt-5.4+: FUNCTION tools with reasoning active must be bridged. OpenAI enables - # reasoning by default for these models (unset reasoning_effort means medium - # server-side), and Chat Completions rejects function tools whenever reasoning is - # on ("Function tools with reasoning_effort are not supported ... use - # /v1/responses or set reasoning_effort to 'none'"), so only an explicit - # ``"none"`` the chat gate forwards keeps the request chat-servable: a ``none`` - # the model's map row refuses is dropped (or refused) before the call, which - # leaves the provider's default effort in force. Custom (grammar) tools are served - # natively by Chat Completions with reasoning on, so custom-only requests stay on - # chat and keep their native custom tool_call response shape. - # - The UNSET-effort arm only fires against endpoints known to enforce that - # constraint (any api.openai.com host, or Azure OpenAI where api_base is - # always set): chat-only OpenAI-compatible backends registered under the openai - # provider with a custom api_base and gpt-5.4+ model names serve tools without - # reasoning fine and have no /responses route, so they keep pre-existing - # behavior (bridge only on an explicit reasoning_effort). - # - Azure AI Foundry's OpenAI v1 hosts (azure_ai provider) enforce it later in the series: - # an explicit effort with function tools is rejected from gpt-5.6 on, and the unset - # effort only from gpt-6 on (gpt-5.6 serves tools with reasoning silently off), so the - # azure_ai gate keys on those measured boundaries instead of gpt-5.4+. - # - Bedrock Mantle serves the same API and enforces the gpt-5.4+ rule even with the effort - # unset (measured on gpt-5.6-terra), so a Mantle row that also serves Responses follows - # these rules, which keeps a deployment pinned to ``mode: chat`` from a 400 on them. - # - Older GPT-5 names (e.g. ``gpt-5``, ``gpt-5.1``): bridge only when a reasoning - # summary alias is present with ``reasoning_effort`` (tools alone stay on chat). + # Function tools with explicit active reasoning bridge on GPT-5.4+. With effort unset, + # OpenAI/Azure and Responses-capable Mantle endpoints bridge only GPT-5.6+ whose map + # default is not "none". Custom OpenAI-compatible endpoints keep unset effort on chat. + # Foundry uses its separate measured boundaries. Custom-only tools remain chat-native; + # reasoning summaries and tool_search retain their independent Responses requirements. has_function_tool: Final = any( ( tool.get("type") == "function" and (isinstance(tool.get("function"), dict) or "name" in tool) @@ -274,7 +250,14 @@ def responses_api_bridge_check( if on_foundry_openai_endpoint else ( OpenAIGPT5Config.is_model_gpt_5_4_plus_model(gpt_series_name) - and (reasoning_effort is not None or on_constraint_enforcing_endpoint) + and ( + reasoning_effort is not None + or ( + on_constraint_enforcing_endpoint + and OpenAIGPT5Config.is_model_gpt_5_6_plus_model(gpt_series_name) + and model_info.get("default_reasoning_effort") != "none" + ) + ) ) ) ) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3145ca4a181..01a32ee7cdb 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -72516,6 +72516,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/eu/gpt-5.4-mini": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 8.25e-08, @@ -72537,6 +72538,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/eu/gpt-5.4-nano": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 2.2e-08, @@ -73140,6 +73142,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/us/gpt-5.4-mini": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 8.25e-08, @@ -73161,6 +73164,7 @@ "source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'" }, "azure/us/gpt-5.4-nano": { + "default_reasoning_effort": "none", "deprecation_date": "2027-09-21", "deprecation_source": "https://learn.microsoft.com/en-us/azure/foundry/openai/concepts/model-retirement-schedule", "cache_read_input_token_cost": 2.2e-08, diff --git a/tests/integration/sdk/test_unset_effort_tool_bridge_wire.py b/tests/integration/sdk/test_unset_effort_tool_bridge_wire.py new file mode 100644 index 00000000000..11740f1d163 --- /dev/null +++ b/tests/integration/sdk/test_unset_effort_tool_bridge_wire.py @@ -0,0 +1,146 @@ +import json +from typing import Final + +import pytest +from pydantic import JsonValue, TypeAdapter + +import litellm +from litellm import Router +from tests.integration._support.wire import Reply, Request, wire_server + +_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) +_TOOL: Final = { + "type": "function", + "function": { + "name": "get_weather", + "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, + }, +} +_ARGUMENTS: Final = '{"city":"Paris"}' + + +def _tool_reply(request: Request) -> Reply: + body: Final = _OBJECT.validate_json(request.body) + if "/responses" in request.target: + return Reply( + body=json.dumps( + { + "id": "resp_weather", + "object": "response", + "created_at": 1789788253, + "model": "gpt-5.6", + "status": "completed", + "output": [ + { + "type": "function_call", + "id": "fc_weather", + "call_id": "call_weather", + "name": "get_weather", + "arguments": _ARGUMENTS, + "status": "completed", + } + ], + "usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}, + } + ).encode() + ) + return Reply( + body=json.dumps( + { + "id": "chatcmpl-weather", + "object": "chat.completion", + "created": 1789788253, + "model": body.get("model"), + "choices": [ + { + "index": 0, + "finish_reason": "tool_calls", + "message": { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_weather", + "type": "function", + "function": {"name": "get_weather", "arguments": _ARGUMENTS}, + } + ], + }, + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + } + ).encode() + ) + + +@pytest.mark.parametrize( + ("model", "placement", "endpoint"), + ( + ("gpt-5.4-mini", "unset", "chat/completions"), + ("gpt-5.5", "unset", "chat/completions"), + ("gpt-5.6", "unset", "responses"), + ("gpt-5.4-mini", "extra-body", "responses"), + ("gpt-5.4-mini", "none", "chat/completions"), + ("gpt-5.4-mini", "deployment", "responses"), + ), +) +def test_function_tools_reach_the_chat_servable_endpoint_and_keep_tool_call_shape( + model: str, placement: str, endpoint: str +) -> None: + with wire_server(_tool_reply) as wire: + parameters: Final = { + "model": f"azure/{model}", + "api_base": wire.url, + "api_key": "synthetic-azure-key", + "api_version": "2025-04-01-preview", + } + fields: Final = ( + {"extra_body": {"reasoning_effort": "medium"}} + if placement == "extra-body" + else {"reasoning_effort": "none"} + if placement == "none" + else {} + ) + messages: Final = [{"role": "user", "content": "What's the weather in Paris? Use the tool."}] + if placement == "deployment": + router: Final = Router( + model_list=[ + { + "model_name": "weather", + "litellm_params": {**parameters, "reasoning_effort": "medium"}, + } + ], + num_retries=0, + ) + response: Final = router.completion(model="weather", messages=messages, tools=[_TOOL]) + else: + response_direct: Final = litellm.completion( + **parameters, + **fields, + messages=messages, + tools=[_TOOL], + num_retries=0, + ) + answer: Final = response if placement == "deployment" else response_direct + (sent,) = tuple(request for request in wire.drain() if request.method == "POST") + expected_path: Final = ( + "/openai/responses" if endpoint == "responses" else f"/openai/deployments/{model}/chat/completions" + ) + assert sent.target.split("?")[0] == expected_path, sent.target + assert answer.choices[0].finish_reason == "tool_calls" + assert answer.choices[0].message.tool_calls[0].function.name == "get_weather" + assert json.loads(answer.choices[0].message.tool_calls[0].function.arguments) == {"city": "Paris"} + outbound: Final = _OBJECT.validate_json(sent.body) + if endpoint == "responses": + assert outbound["tools"] == [ + { + "type": "function", + "name": "get_weather", + "parameters": _TOOL["function"]["parameters"], + "description": None, + "strict": None, + } + ] + else: + assert outbound["tools"] == [_TOOL] diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index bad7b3f1750..a76d5e82a37 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -759,7 +759,7 @@ def test_translate_openai_reasoning_items_are_not_duplicated_when_thinking_block def _replayed_reasoning_request(tools: list[dict[str, object]]) -> AnthropicMessagesRequest: return AnthropicMessagesRequest( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", max_tokens=64, messages=[ AnthropicMessagesUserMessageParam(role="user", content="Which city is colder?"), @@ -5820,7 +5820,7 @@ _RETRY_REPLY: Final = { "object": "response", "created_at": 1700000000, "status": "completed", - "model": "gpt-5.4-nano", + "model": "gpt-5.6", "output": [ { "type": "message", @@ -5868,7 +5868,7 @@ async def test_async_messages_retry_once_without_reasoning_the_backend_cannot_de with caplog.at_level(logging.WARNING, logger="LiteLLM"): response: Final = await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -5890,7 +5890,7 @@ async def test_async_messages_retry_once_without_reasoning_the_backend_cannot_de assert retry_warnings == ( "Backend rejected replayed encrypted reasoning, retrying once without it. " "Enable the encrypted_content_affinity pre-call check to drop such blocks before the call. " - "provider=azure model=azure/gpt-5.4-nano status=400 code=invalid_encrypted_content", + "provider=azure model=azure/gpt-5.6 status=400 code=invalid_encrypted_content", ) assert all( "gAAA_foreign" not in message and "litellm_encrypted_reasoning:" not in message for message in warnings @@ -5906,7 +5906,7 @@ def test_sync_messages_retry_once_without_reasoning_the_backend_cannot_decrypt( ) response: Final = litellm.anthropic.messages.create( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -5930,7 +5930,7 @@ async def test_async_messages_with_num_retries_send_the_rejected_reasoning_only_ ) await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -5953,7 +5953,7 @@ def test_sync_messages_with_global_num_retries_send_the_rejected_reasoning_once_ ) litellm.anthropic.messages.create( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -5981,7 +5981,7 @@ async def test_async_messages_retry_a_server_error_with_the_reasoning_and_drop_i ) response: Final = await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -6004,7 +6004,7 @@ def test_sync_messages_retry_a_server_error_with_the_reasoning_and_drop_it_only_ ) response: Final = litellm.anthropic.messages.create( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -6063,7 +6063,7 @@ async def test_async_messages_log_only_the_outcome_of_the_turn_sent_again( with pytest.raises(litellm.BadRequestError) if logged_rejections else nullcontext(): await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -6089,7 +6089,7 @@ def test_sync_messages_log_only_the_outcome_of_the_turn_sent_again( with pytest.raises(litellm.BadRequestError) if logged_rejections else nullcontext(): litellm.anthropic.messages.create( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -6124,7 +6124,7 @@ async def test_messages_do_not_retry_other_rejections( with pytest.raises(litellm.BadRequestError): await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", @@ -6277,7 +6277,7 @@ async def test_streamed_reasoning_summaries_stay_with_their_own_encrypted_item( ) stream: Final = await litellm.anthropic.messages.acreate( - model="azure/gpt-5.4-nano", + model="azure/gpt-5.6", api_base=_RETRY_API_BASE, api_key="fake-key", api_version="2025-04-01-preview", diff --git a/tests/unit/proxy/guardrails/test_deferred_guardrail_logging.py b/tests/unit/proxy/guardrails/test_deferred_guardrail_logging.py index d2161fddeb0..bfbf7318d52 100644 --- a/tests/unit/proxy/guardrails/test_deferred_guardrail_logging.py +++ b/tests/unit/proxy/guardrails/test_deferred_guardrail_logging.py @@ -330,7 +330,7 @@ async def test_deferred_slot_keeps_the_innermost_wrapper_result(): async def test_deferred_anthropic_messages_bridged_to_the_responses_api_logs_the_provider_usage( respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch ): - """/v1/messages on an Azure gpt-5.4+ deployment with function tools runs three nested + """/v1/messages on an Azure deployment with function tools and explicit reasoning runs three nested wrappers: anthropic_messages, the chat adapter's acompletion, and the Responses bridge acompletion hands the call to, which retags the call as ``responses``. With logging deferred for a post-call guardrail the stored closure must carry the innermost provider @@ -382,6 +382,7 @@ async def test_deferred_anthropic_messages_bridged_to_the_responses_api_logs_the response: Final = await litellm.anthropic_messages( model="azure/gpt-5.4-nano", + reasoning_effort="medium", messages=[{"role": "user", "content": "hi"}], max_tokens=16, tools=[ diff --git a/tests/unit/responses/test_responses_api_bridge_flag.py b/tests/unit/responses/test_responses_api_bridge_flag.py index 210488f0888..f6084529ed6 100644 --- a/tests/unit/responses/test_responses_api_bridge_flag.py +++ b/tests/unit/responses/test_responses_api_bridge_flag.py @@ -786,17 +786,74 @@ _FUNCTION_TOOL: Final[dict[str, JsonValue]] = { @pytest.mark.parametrize("custom_llm_provider", ["openai", "azure"]) -def test_a_chat_pin_cannot_pull_gpt_5_4_function_tool_calls_off_the_responses_api(custom_llm_provider: str) -> None: - """Chat Completions rejects function tools while reasoning is on for gpt-5.4+, so the capability rule - must still bridge the request even when the deployment pins ``mode: chat``.""" +@pytest.mark.parametrize(("effort", "mode"), ((None, "chat"), ("medium", "responses"))) +def test_a_chat_pin_routes_gpt_5_4_function_tools_by_explicit_effort( + custom_llm_provider: str, effort: str | None, mode: str +) -> None: info, model = responses_api_bridge_check( model="gpt-5.4", custom_llm_provider=custom_llm_provider, tools=[_FUNCTION_TOOL], + reasoning_effort=effort, api_base="https://example.openai.azure.com" if custom_llm_provider == "azure" else None, deployment_model_info=_CHAT, ) - assert (info["mode"], model) == ("responses", "gpt-5.4") + assert (info["mode"], model) == (mode, "gpt-5.4") + + +@pytest.mark.parametrize("provider", ("openai", "azure")) +@pytest.mark.parametrize( + ("model", "effort", "extra_body", "expected"), + ( + ("gpt-5.4-mini", None, None, "chat"), + ("gpt-5.4-nano", None, None, "chat"), + ("gpt-5.5", None, None, "chat"), + ("gpt-5.6", None, None, "responses"), + ("gpt-6.1-sol", None, None, "responses"), + ("gpt-5.4-mini", "none", None, "chat"), + ("gpt-5.4-mini", "medium", None, "responses"), + ("gpt-5.4-mini", {"effort": "medium"}, None, "responses"), + ("gpt-5.4-mini", None, {"reasoning_effort": "medium"}, "responses"), + ), +) +def test_function_tools_bridge_only_when_chat_rejects_the_effective_effort( + provider: str, + model: str, + effort: str | Mapping[str, object] | None, + extra_body: Mapping[str, object] | None, + expected: str, +) -> None: + info, _ = responses_api_bridge_check( + model=model, + custom_llm_provider=provider, + tools=[_FUNCTION_TOOL], + reasoning_effort=bridge_reasoning_effort(effort, extra_body), + api_base="https://api.openai.com/v1", + ) + assert info.get("mode") == expected + + +@pytest.mark.parametrize("region", ("us", "eu")) +@pytest.mark.parametrize("model", ("gpt-5.4-mini", "gpt-5.4-nano")) +def test_regional_azure_default_none_function_tools_stay_on_chat(region: str, model: str) -> None: + info, _ = responses_api_bridge_check( + model=f"{region}/{model}", custom_llm_provider="azure", tools=[_FUNCTION_TOOL] + ) + assert info.get("mode") == "chat" + assert info.get("default_reasoning_effort") == litellm.model_cost[f"azure/{model}"]["default_reasoning_effort"] + + +def test_registered_gpt_5_6_default_none_function_tools_stay_on_chat(monkeypatch: pytest.MonkeyPatch) -> None: + model: Final = "gpt-5.6-test-default-none" + monkeypatch.setitem( + litellm.model_cost, + model, + {**litellm.model_cost["gpt-5.6"], "default_reasoning_effort": "none"}, + ) + info, _ = responses_api_bridge_check( + model=model, custom_llm_provider="openai", tools=[_FUNCTION_TOOL], api_base="https://api.openai.com/v1" + ) + assert info.get("mode") == "chat" @pytest.mark.parametrize("model", ["openai.gpt-5.6-terra", "us-east-1/openai.gpt-5.6-terra"]) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index dbd9ba8a32e..d2035b6a682 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1418,12 +1418,7 @@ def test_responses_api_bridge_check_azure_gpt_5_4_tools_plus_reasoning_routes_to assert model_info.get("mode") == "responses" -def test_responses_api_bridge_check_azure_gpt_5_4_tools_with_default_reasoning_routes_to_responses(): - """ - Azure gpt-5.4 with tools and UNSET reasoning_effort must bridge: OpenAI enables - reasoning by default for gpt-5.4+, and Chat Completions rejects function tools - whenever reasoning is on. - """ +def test_responses_api_bridge_check_azure_gpt_5_4_tools_with_default_reasoning_stays_chat(): from litellm.main import responses_api_bridge_check with patch("litellm.responses.bridge_check.get_model_info_helper") as mock_get_model_info: @@ -1436,15 +1431,10 @@ def test_responses_api_bridge_check_azure_gpt_5_4_tools_with_default_reasoning_r ) assert model == "gpt-5.4" - assert model_info.get("mode") == "responses" + assert model_info.get("mode") != "responses" -def test_responses_api_bridge_check_gpt_5_4_tools_with_default_reasoning_routes_to_responses(): - """ - gpt-5.4 with tools and UNSET reasoning_effort must bridge: OpenAI enables reasoning - by default for gpt-5.4+, and Chat Completions rejects function tools whenever - reasoning is on ("use /v1/responses or set reasoning_effort to 'none'"). - """ +def test_responses_api_bridge_check_gpt_5_4_tools_with_default_reasoning_stays_chat(): from litellm.main import responses_api_bridge_check with patch("litellm.responses.bridge_check.get_model_info_helper") as mock_get_model_info: @@ -1457,7 +1447,7 @@ def test_responses_api_bridge_check_gpt_5_4_tools_with_default_reasoning_routes_ ) assert model == "gpt-5.4" - assert model_info.get("mode") == "responses" + assert model_info.get("mode") != "responses" @pytest.mark.parametrize("region", ("us", "eu")) @@ -1972,8 +1962,7 @@ def test_responses_api_bridge_check_custom_api_base_with_explicit_effort_still_r assert model_info.get("mode") == "responses" -def test_responses_api_bridge_check_azure_with_api_base_and_unset_effort_routes(): - """Azure OpenAI always sets api_base and does enforce the constraint; keep bridging.""" +def test_responses_api_bridge_check_azure_gpt_5_4_with_api_base_and_unset_effort_stays_chat(): from litellm.main import responses_api_bridge_check with patch("litellm.responses.bridge_check.get_model_info_helper") as mock_get_model_info: @@ -1987,7 +1976,7 @@ def test_responses_api_bridge_check_azure_with_api_base_and_unset_effort_routes( ) assert model == "gpt-5.4" - assert model_info.get("mode") == "responses" + assert model_info.get("mode") != "responses" _FOUNDRY_API_BASE: Final = "https://myproject.services.ai.azure.com"