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 <kenneth@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-11-10 18:33:27 +00:00 • committed by GitHub
parent 5299914964
commit d47c03175b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 249 additions and 65 deletions

View file

@ -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,

View file

@ -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"
)
)
)
)
)

View file

@ -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,

View file

@ -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]

View file

@ -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",

View file

@ -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=[

View file

@ -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"])

View file

@ -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"