mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
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:
parent
5299914964
commit
d47c03175b
8 changed files with 249 additions and 65 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
146
tests/integration/sdk/test_unset_effort_tool_bridge_wire.py
Normal file
146
tests/integration/sdk/test_unset_effort_tool_bridge_wire.py
Normal 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]
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue