mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(openai): drop unsupported reasoning effort for gpt-6.1-sol (#43935)
Backport of #43935 to stable/1.103.x. Cherry-picked fromd7620c77a1(main). Ported from the stable/1.104.x adaptation (54f9259b40): the bridge check lives in litellm/main.py and the effort helpers keep the line's private names. Cost-map edits replay only this PR's reasoning-effort flags (gpt-6-astra and gpt-5.3-codex rows), plus supports_none_reasoning_effort=false on the azure gpt-6.1-sol rows from #46377, which the fix's tests key on. Tests built on gpt-6-sol rows (#42515), the Foundry tools-to-Responses bridge, or a full azure/eu/gpt-6-astra row are dropped since none of those are on this line.
This commit is contained in:
parent
de7a2cda10
commit
ab69d8489c
18 changed files with 693 additions and 187 deletions
|
|
@ -1,12 +1,10 @@
|
|||
"""Support for Azure OpenAI gpt-5 model family."""
|
||||
|
||||
from typing import Final
|
||||
from typing import ClassVar, Final
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import (
|
||||
OpenAIGPT5Config,
|
||||
_get_effort_level,
|
||||
is_gpt_reasoning_series_name,
|
||||
)
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
|
|
@ -15,9 +13,16 @@ from .gpt_transformation import AzureOpenAIConfig
|
|||
|
||||
|
||||
class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
||||
"""Azure specific handling for gpt-5 models."""
|
||||
"""Azure specific handling for gpt-5 models.
|
||||
|
||||
reasoning_effort none joins xhigh as opt-in here: Azure rejected it on every gpt-5
|
||||
deployment before gpt-5.1 (https://github.com/BerriAI/litellm/issues/16704), and a deployment
|
||||
name the map never heard of must not forward it. See
|
||||
https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning
|
||||
"""
|
||||
|
||||
GPT5_SERIES_ROUTE = "gpt5_series/"
|
||||
OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh", "none"))
|
||||
|
||||
@classmethod
|
||||
def _model_map_lookup_name(cls, model: str) -> str:
|
||||
|
|
@ -27,11 +32,18 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
model_prices_and_context_window.json; ``azure/gpt-5.1`` is. Overriding the shared
|
||||
resolver rather than one lookup means the supports, explicitly-disabled and
|
||||
default-effort answers all read the same entry.
|
||||
|
||||
An ``azure_ai/`` name reaches this config when ``get_llm_provider`` re-routes a Foundry
|
||||
deployment on an OpenAI-v1 host to the azure provider with the prefix intact. It is the
|
||||
same product the azure_ai config reads ``azure_ai/<model>`` for, so that row answers when
|
||||
the map carries it, and the azure/ twin when it does not.
|
||||
"""
|
||||
if model.startswith(cls.GPT5_SERIES_ROUTE):
|
||||
return "azure/" + model[len(cls.GPT5_SERIES_ROUTE) :]
|
||||
if model.startswith("azure/"):
|
||||
return model
|
||||
if model.startswith("azure_ai/"):
|
||||
return model if model in litellm.model_cost else "azure/" + model.removeprefix("azure_ai/")
|
||||
return "azure/" + model
|
||||
|
||||
@classmethod
|
||||
|
|
@ -75,34 +87,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
drop_params: bool,
|
||||
api_version: str = "",
|
||||
) -> dict:
|
||||
reasoning_effort_value = non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
||||
effective_effort: Final = _get_effort_level(reasoning_effort_value)
|
||||
|
||||
# gpt-5.1/5.2/5.4 support reasoning_effort='none', but other gpt-5 models don't
|
||||
# See: https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/reasoning
|
||||
supports_none: Final = self._supports_reasoning_effort_level(model, "none")
|
||||
|
||||
if effective_effort == "none" and not supports_none:
|
||||
if litellm.drop_params is True or (drop_params is not None and drop_params is True):
|
||||
non_default_params = non_default_params.copy()
|
||||
optional_params = optional_params.copy()
|
||||
if _get_effort_level(non_default_params.get("reasoning_effort")) == "none":
|
||||
non_default_params.pop("reasoning_effort")
|
||||
if _get_effort_level(optional_params.get("reasoning_effort")) == "none":
|
||||
optional_params.pop("reasoning_effort")
|
||||
else:
|
||||
raise UnsupportedParamsError(
|
||||
status_code=400,
|
||||
message=(
|
||||
"Azure OpenAI does not support reasoning_effort='none' for this model. "
|
||||
"Supported values are: 'low', 'medium', and 'high'. "
|
||||
"To drop this parameter, set `litellm.drop_params=True` or for proxy:\n\n"
|
||||
"`litellm_settings:\n drop_params: true`\n"
|
||||
"Issue: https://github.com/BerriAI/litellm/issues/16704"
|
||||
),
|
||||
)
|
||||
|
||||
result: Final = OpenAIGPT5Config.map_openai_params(
|
||||
return OpenAIGPT5Config.map_openai_params(
|
||||
self,
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
|
|
@ -110,17 +95,6 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
|||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
# Only drop reasoning_effort='none' for models that don't support it
|
||||
result_effort: Final = _get_effort_level(result.get("reasoning_effort"))
|
||||
if result_effort == "none" and not supports_none:
|
||||
result.pop("reasoning_effort")
|
||||
|
||||
# Azure gpt-5.4+ with tools + reasoning_effort is now routed to the
|
||||
# Responses API bridge (same as OpenAI), so we no longer need to drop
|
||||
# reasoning_effort here. See: responses_api_bridge_check() in main.py.
|
||||
|
||||
return result
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -30,10 +30,18 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.AZURE
|
||||
|
||||
@staticmethod
|
||||
def _is_gpt_5_model(model: str) -> bool:
|
||||
return AzureOpenAIGPT5Config.is_model_gpt_5_model(model)
|
||||
|
||||
@staticmethod
|
||||
def _supports_reasoning_effort_none(model: str) -> bool:
|
||||
return AzureOpenAIGPT5Config._supports_reasoning_effort_level(model, "none")
|
||||
|
||||
@staticmethod
|
||||
def _effort_level_is_disabled(model: str, level: str) -> bool:
|
||||
return AzureOpenAIGPT5Config.row_disables_reasoning_effort_level(model, level)
|
||||
|
||||
@staticmethod
|
||||
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
||||
return AzureOpenAIGPT5Config.effort_resolves_to_none(model, effort)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from typing import Final
|
|||
import httpx
|
||||
|
||||
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
||||
from litellm.llms.azure_ai.chat.transformation import AzureAIGPT5Config
|
||||
from litellm.llms.azure_ai.common_utils import (
|
||||
AzureFoundryModelInfo,
|
||||
api_key_header_for_base,
|
||||
|
|
@ -27,6 +28,18 @@ class AzureAIResponsesAPIConfig(AzureOpenAIResponsesAPIConfig):
|
|||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.AZURE_AI
|
||||
|
||||
@staticmethod
|
||||
def _supports_reasoning_effort_none(model: str) -> bool:
|
||||
return AzureAIGPT5Config._supports_reasoning_effort_level(model, "none")
|
||||
|
||||
@staticmethod
|
||||
def _effort_level_is_disabled(model: str, level: str) -> bool:
|
||||
return AzureAIGPT5Config.row_disables_reasoning_effort_level(model, level)
|
||||
|
||||
@staticmethod
|
||||
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
||||
return AzureAIGPT5Config.effort_resolves_to_none(model, effort)
|
||||
|
||||
def validate_environment(self, headers: dict, model: str, litellm_params: GenericLiteLLMParams | None) -> dict:
|
||||
params: Final = litellm_params or GenericLiteLLMParams()
|
||||
auth_headers: Final = get_azure_ai_auth_headers(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Support for OpenAI gpt-5 model family."""
|
||||
|
||||
from typing import Final
|
||||
import re
|
||||
from typing import ClassVar, Final
|
||||
|
||||
import litellm
|
||||
from litellm.utils import (
|
||||
|
|
@ -64,6 +65,10 @@ def _get_effort_level(value: str | dict | None) -> str | None:
|
|||
GPT_REASONING_SERIES_MARKERS: Final = ("gpt-5", "gpt-6")
|
||||
|
||||
|
||||
def unsupported_reasoning_effort_message(param: str, level: str, model: str) -> str:
|
||||
return f"{param}={level} is not supported for {model}. To drop unsupported params set `litellm.drop_params = True`"
|
||||
|
||||
|
||||
def is_gpt_reasoning_series_name(model: str) -> bool:
|
||||
normalized: Final = model.split("/")[-1]
|
||||
return any(marker in model for marker in GPT_REASONING_SERIES_MARKERS) and not normalized.startswith("gpt-5-chat")
|
||||
|
|
@ -79,6 +84,9 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
- Support for GPT-5-Codex models optimized for code generation.
|
||||
"""
|
||||
|
||||
OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh",))
|
||||
ROUTE_SPECIFIC_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("max",))
|
||||
|
||||
@classmethod
|
||||
def is_model_gpt_5_model(cls, model: str) -> bool:
|
||||
return is_gpt_reasoning_series_name(model)
|
||||
|
|
@ -182,15 +190,10 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def _is_reasoning_effort_level_explicitly_disabled(cls, model: str, level: str) -> bool:
|
||||
def is_reasoning_effort_level_explicitly_disabled(cls, model: str, level: str) -> bool:
|
||||
"""Return True only when the model map explicitly sets the capability to False.
|
||||
|
||||
Unlike ``_supports_reasoning_effort_level`` (which requires an explicit True),
|
||||
this method returns True only when ``supports_{level}_reasoning_effort`` is
|
||||
explicitly set to ``False`` in the model map. A missing key is treated as
|
||||
supported (i.e. this method returns False = not disabled).
|
||||
|
||||
Use this for opt-out checks where unknown models should be allowed through.
|
||||
A missing key is not a disable. Unknown models pass through.
|
||||
"""
|
||||
return is_explicitly_disabled_factory(
|
||||
model=cls._model_map_lookup_name(model),
|
||||
|
|
@ -198,6 +201,39 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
key=f"supports_{level}_reasoning_effort",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _is_reasoning_effort_level_explicitly_disabled(cls, model: str, level: str) -> bool:
|
||||
return cls.is_reasoning_effort_level_explicitly_disabled(model, level)
|
||||
|
||||
@classmethod
|
||||
def refuses_reasoning_effort_level(cls, model: str, level: str) -> bool:
|
||||
"""The chat gate for any requested effort level, read off the model's map row alone.
|
||||
|
||||
An opt-in level (xhigh here; Azure adds none) is refused unless the row says true, since
|
||||
the providers rejected it on most of the family when it arrived. Every other level is
|
||||
refused only when the row says false: a model the map never heard of, or a row without the
|
||||
flag, keeps forwarding what it was sent, and a level with no flag at all (medium, high) is
|
||||
never refused. So a new model in the family needs its map row and no code change, the
|
||||
row's supports_<level>_reasoning_effort flags being the whole contract.
|
||||
|
||||
A route-specific level (max) is never refused by either gate: the same model honors it
|
||||
on Responses and refuses it on Chat Completions, so one row flag cannot gate it on
|
||||
either route. Chat forwards it for the Responses bridge and a plain chat call gets the
|
||||
provider's own 400, exactly as before the gate read every level.
|
||||
"""
|
||||
if level in cls.ROUTE_SPECIFIC_REASONING_EFFORTS:
|
||||
return False
|
||||
if level in cls.OPT_IN_REASONING_EFFORTS:
|
||||
return not cls._supports_reasoning_effort_level(model, level)
|
||||
return cls.is_reasoning_effort_level_explicitly_disabled(model, level)
|
||||
|
||||
@classmethod
|
||||
def row_disables_reasoning_effort_level(cls, model: str, level: str) -> bool:
|
||||
"""The Responses gate: opt-out for every level except the route-specific ones."""
|
||||
if level in cls.ROUTE_SPECIFIC_REASONING_EFFORTS:
|
||||
return False
|
||||
return cls.is_reasoning_effort_level_explicitly_disabled(model, level)
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
if self.is_model_gpt_5_search_model(model):
|
||||
return [
|
||||
|
|
@ -258,12 +294,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
drop_params=drop_params,
|
||||
)
|
||||
|
||||
# Get raw reasoning_effort and effective effort level for all guards.
|
||||
# Use effective_effort (extracted string) for xhigh validation, "none" checks, and
|
||||
# tool/sampling guards — dict inputs like {"effort": "none", "summary": "detailed"}
|
||||
# must be treated as effort="none" to avoid incorrect tool-drop or sampling errors.
|
||||
raw_reasoning_effort = non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
||||
effective_effort: Final = _get_effort_level(raw_reasoning_effort)
|
||||
# Dict inputs like {"effort": "none", "summary": "detailed"} are judged by their effort
|
||||
# string so the gate, the sampling guard, and the temperature guard all see one level.
|
||||
raw_reasoning_effort: Final = non_default_params.get("reasoning_effort") or optional_params.get(
|
||||
"reasoning_effort"
|
||||
)
|
||||
requested_effort: Final = _get_effort_level(raw_reasoning_effort)
|
||||
|
||||
# Normalize dict reasoning_effort to string for Chat Completions API.
|
||||
# Example: {"effort": "high", "summary": "detailed"} -> "high"
|
||||
|
|
@ -275,31 +311,20 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
if "reasoning_effort" in optional_params:
|
||||
optional_params["reasoning_effort"] = normalized
|
||||
|
||||
if effective_effort == "xhigh":
|
||||
# xhigh is an opt-in capability: only allow if model explicitly supports it.
|
||||
if not self._supports_reasoning_effort_level(model, effective_effort):
|
||||
if litellm.drop_params or drop_params:
|
||||
non_default_params.pop("reasoning_effort", None)
|
||||
optional_params.pop("reasoning_effort", None)
|
||||
else:
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=(f"reasoning_effort={effective_effort} is not supported for this model."),
|
||||
status_code=400,
|
||||
)
|
||||
elif effective_effort in ("minimal", "low"):
|
||||
# minimal/low are opt-out: unknown models pass through; only block when
|
||||
# the model map explicitly sets supports_{level}_reasoning_effort=false.
|
||||
# Example: gpt-5.5-pro only accepts {medium, high, xhigh}, so it sets
|
||||
# supports_low_reasoning_effort=false (and supports_minimal=false).
|
||||
if self._is_reasoning_effort_level_explicitly_disabled(model, effective_effort):
|
||||
if litellm.drop_params or drop_params:
|
||||
non_default_params.pop("reasoning_effort", None)
|
||||
optional_params.pop("reasoning_effort", None)
|
||||
else:
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=(f"reasoning_effort={effective_effort} is not supported for this model."),
|
||||
status_code=400,
|
||||
)
|
||||
if requested_effort is not None and self.refuses_reasoning_effort_level(model, requested_effort):
|
||||
if litellm.drop_params or drop_params:
|
||||
non_default_params.pop("reasoning_effort", None)
|
||||
optional_params.pop("reasoning_effort", None)
|
||||
else:
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=unsupported_reasoning_effort_message("reasoning_effort", requested_effort, model),
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# The level the provider will see: None once dropped, so the guards below judge by the model's default effort
|
||||
effective_effort: Final = _get_effort_level(
|
||||
non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
||||
)
|
||||
|
||||
################################################################
|
||||
# max_tokens is not supported for gpt-5 models on OpenAI API
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
|||
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import is_gpt_reasoning_series_name
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import (
|
||||
is_gpt_reasoning_series_name,
|
||||
unsupported_reasoning_effort_message,
|
||||
)
|
||||
from litellm.responses.litellm_completion_transformation.custom_tools import TOOL_CALL_ITEM_ID_PREFIX_BY_TYPE
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import *
|
||||
|
|
@ -136,6 +139,20 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
|
||||
return supports_none_reasoning_effort(model=model, custom_llm_provider=None)
|
||||
|
||||
@staticmethod
|
||||
def _effort_level_is_disabled(model: str, level: str) -> bool:
|
||||
"""Whether the model's cost-map row turns this effort level off.
|
||||
|
||||
The same row the chat gate reads, so a level the row refuses is refused on both wires
|
||||
and a new model needs its row, not code. This surface stays opt-out for every level,
|
||||
xhigh included, since it never refused a level the row omits; max is the exception, a
|
||||
level the row cannot gate because the same model takes it here and refuses it on chat.
|
||||
Azure overrides this so a bare deployment name reads the azure/ entry.
|
||||
"""
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
|
||||
|
||||
return OpenAIGPT5Config.row_disables_reasoning_effort_level(model, level)
|
||||
|
||||
@staticmethod
|
||||
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
||||
"""Whether this request's reasoning effort ends up as "none", the one condition
|
||||
|
|
@ -242,7 +259,25 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
lookup_name: Final = self._model_map_lookup_name(model)
|
||||
if self._is_gpt_5_model(model=lookup_name):
|
||||
reasoning: Final = params.get("reasoning") or {}
|
||||
effort: Final = reasoning.get("effort") if isinstance(reasoning, dict) else None
|
||||
requested_effort: Final = reasoning.get("effort") if isinstance(reasoning, dict) else None
|
||||
if (
|
||||
isinstance(reasoning, dict)
|
||||
and isinstance(requested_effort, str)
|
||||
and self._effort_level_is_disabled(lookup_name, requested_effort)
|
||||
):
|
||||
if not (drop_params or litellm.drop_params):
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message=unsupported_reasoning_effort_message("reasoning.effort", requested_effort, model),
|
||||
status_code=400,
|
||||
)
|
||||
remaining: Final = reasoning.copy()
|
||||
remaining.pop("effort", None)
|
||||
if remaining:
|
||||
params["reasoning"] = remaining
|
||||
else:
|
||||
params.pop("reasoning", None)
|
||||
forwarded_reasoning: Final = params.get("reasoning")
|
||||
effort: Final = forwarded_reasoning.get("effort") if isinstance(forwarded_reasoning, dict) else None
|
||||
supports_none: Final = self._supports_reasoning_effort_none(model=lookup_name)
|
||||
effort_is_none: Final = supports_none and self._effort_resolves_to_none(lookup_name, effort)
|
||||
|
||||
|
|
|
|||
|
|
@ -1048,6 +1048,18 @@ def _resolve_openai_api_base(api_base: str | None) -> str:
|
|||
)
|
||||
|
||||
|
||||
def _chat_gate_refuses_reasoning_effort_none(model: str, custom_llm_provider: str) -> bool:
|
||||
from litellm.llms.azure_ai.chat.transformation import AzureAIGPT5Config
|
||||
|
||||
match custom_llm_provider:
|
||||
case "azure":
|
||||
return litellm.AzureOpenAIGPT5Config.refuses_reasoning_effort_level(model, "none")
|
||||
case "azure_ai":
|
||||
return AzureAIGPT5Config.refuses_reasoning_effort_level(model, "none")
|
||||
case _:
|
||||
return OpenAIGPT5Config.refuses_reasoning_effort_level(model, "none")
|
||||
|
||||
|
||||
def responses_api_bridge_check(
|
||||
model: str,
|
||||
custom_llm_provider: str,
|
||||
|
|
@ -1112,10 +1124,13 @@ def responses_api_bridge_check(
|
|||
(tool.get("type") == "function" if isinstance(tool, dict) else getattr(tool, "type", None) == "function")
|
||||
for tool in (tools or ())
|
||||
)
|
||||
if isinstance(reasoning_effort, dict):
|
||||
reasoning_active = reasoning_effort.get("effort") != "none" or reasoning_effort.get("summary") is not None
|
||||
else:
|
||||
reasoning_active = reasoning_effort != "none"
|
||||
requested_effort: Final = reasoning_effort.get("effort") if isinstance(reasoning_effort, dict) else reasoning_effort
|
||||
requested_summary: Final = reasoning_effort.get("summary") if isinstance(reasoning_effort, dict) else None
|
||||
reasoning_active: Final = (
|
||||
requested_effort != "none"
|
||||
or requested_summary is not None
|
||||
or _chat_gate_refuses_reasoning_effort_none(model, custom_llm_provider)
|
||||
)
|
||||
# The reasoning+tools constraint is enforced by the real OpenAI backend behind any api.openai.com
|
||||
# host (the default URL or a PrivateLink hostname such as <region>.privatelink.api.openai.com) and
|
||||
# by Azure OpenAI. Resolve the effective base arg>global>env>default exactly as the chat handler
|
||||
|
|
|
|||
|
|
@ -3646,7 +3646,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_cache_breakpoint": true,
|
||||
|
|
@ -7943,7 +7943,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -7991,7 +7991,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -8335,7 +8335,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -29771,8 +29771,11 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"supports_vision": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
"chatgpt/gpt-5.3-codex-spark": {
|
||||
"litellm_provider": "chatgpt",
|
||||
"max_input_tokens": 128000,
|
||||
|
|
@ -34119,9 +34122,9 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": true
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
"cache_read_input_token_cost": 2.5e-08,
|
||||
|
|
@ -69529,8 +69532,9 @@
|
|||
"mode": "chat",
|
||||
"output_cost_per_token": 5.5e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 8.25e-05,
|
||||
"source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'"
|
||||
},
|
||||
"source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'",
|
||||
"supports_none_reasoning_effort": false
|
||||
},
|
||||
"azure/eu/o1-mini": {
|
||||
"cache_read_input_token_cost": 6.05e-07,
|
||||
"input_cost_per_token": 1.21e-06,
|
||||
|
|
@ -75911,7 +75915,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_cache_breakpoint": true,
|
||||
|
|
@ -75960,7 +75964,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -76008,7 +76012,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
"""Resolve which reasoning_effort values a deployment, and by intersection a model group, accepts.
|
||||
|
||||
An entry that states its levels outright in reasoning_effort_levels is read first and wins
|
||||
whole, for a model whose set the per-level flags cannot express: Kimi K3 takes low, high and max,
|
||||
and no flag can drop medium because medium has none. Every other entry answers through the
|
||||
supports_*_reasoning_effort flags below, whose polarity mirrors how a request path reads that same
|
||||
flag. medium and high are unconditional for a reasoning model. minimal and low are opt-out:
|
||||
openai/chat/gpt_5_transformation.py refuses them only when the map says false. xhigh and max are
|
||||
opt-in. none is opt-out everywhere except the azure gpt-5 family, whose config raises
|
||||
UnsupportedParamsError without an explicit true.
|
||||
An entry that states its levels outright in reasoning_effort_levels wins whole, for a model whose set
|
||||
the per-level flags cannot express (Kimi K3 takes low, high and max, and no flag can drop medium).
|
||||
Every other entry answers through its supports_*_reasoning_effort flags, read with the polarity the
|
||||
chat gate uses: medium and high are unconditional, minimal and low are opt-out (refused only on an
|
||||
explicit false), xhigh is opt-in, and none is opt-out except on the azure gpt-5 family, whose chat
|
||||
config refuses it without an explicit true.
|
||||
|
||||
xhigh is gated on the request path by the openai and azure gpt-5 configs. max is not gated there at
|
||||
all: outside the gpt-6-astra rows every entry carrying supports_max_reasoning_effort is Claude-family,
|
||||
and anthropic/chat/transformation.py gates max on the output_config path while its reasoning_effort
|
||||
path maps any level to a thinking budget. Making max opt-in is a deliberate trade, then, since an
|
||||
explicit flag is the only signal that the tier is a real one rather than litellm rounding the level
|
||||
to a budget, and a missing flag costs advisory metadata rather than a rejected request.
|
||||
The Responses gate reads none, minimal, low and xhigh as opt-out. Neither gate judges max, which
|
||||
Chat Completions refuses and Responses honors on the gpt-6 family, so its flag is advisory and
|
||||
advertised opt-in: outside the gpt-6 rows every entry carrying supports_max_reasoning_effort is
|
||||
Claude-family, where anthropic/chat/transformation.py gates max on the output_config path, so an
|
||||
explicit flag is the only signal that the tier is real.
|
||||
|
||||
A deployment the map describes with no effort flags at all resolves to None rather than to the
|
||||
opt-out defaults. 689 of the map's 854 reasoning entries carry no flag, and the o-series, xai and
|
||||
|
|
|
|||
|
|
@ -3646,7 +3646,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_cache_breakpoint": true,
|
||||
|
|
@ -7943,7 +7943,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -7991,7 +7991,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -8335,7 +8335,7 @@
|
|||
"supports_max_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -29771,8 +29771,11 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"supports_vision": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_xhigh_reasoning_effort": true
|
||||
},
|
||||
"chatgpt/gpt-5.3-codex-spark": {
|
||||
"litellm_provider": "chatgpt",
|
||||
"max_input_tokens": 128000,
|
||||
|
|
@ -34119,9 +34122,9 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_xhigh_reasoning_effort": false,
|
||||
"supports_minimal_reasoning_effort": true
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false
|
||||
},
|
||||
"gpt-5-mini": {
|
||||
"cache_read_input_token_cost": 2.5e-08,
|
||||
|
|
@ -69529,8 +69532,9 @@
|
|||
"mode": "chat",
|
||||
"output_cost_per_token": 5.5e-05,
|
||||
"output_cost_per_token_above_272k_tokens": 8.25e-05,
|
||||
"source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'"
|
||||
},
|
||||
"source": "https://prices.azure.com/api/retail/prices?$filter=serviceName%20eq%20'Foundry%20Models'%20and%20armRegionName%20eq%20'eastus'%20and%20priceType%20eq%20'Consumption'",
|
||||
"supports_none_reasoning_effort": false
|
||||
},
|
||||
"azure/eu/o1-mini": {
|
||||
"cache_read_input_token_cost": 6.05e-07,
|
||||
"input_cost_per_token": 1.21e-06,
|
||||
|
|
@ -75911,7 +75915,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_cache_breakpoint": true,
|
||||
|
|
@ -75960,7 +75964,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -76008,7 +76012,7 @@
|
|||
"supports_max_reasoning_effort": true,
|
||||
"supports_minimal_reasoning_effort": false,
|
||||
"supports_native_streaming": true,
|
||||
"supports_none_reasoning_effort": true,
|
||||
"supports_none_reasoning_effort": false,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,23 @@ class TestTheNormalizedTierIsTheTierSent:
|
|||
):
|
||||
assert _reasoning_effort_sent(model, provider, effort) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, provider",
|
||||
[
|
||||
("gpt-5.5", "azure_ai"),
|
||||
("azure_ai/gpt-5.5", "azure_ai"),
|
||||
("gpt-5.6", "azure"),
|
||||
("us/gpt-5.6", "azure"),
|
||||
],
|
||||
)
|
||||
def test_an_azure_hosted_deployment_without_the_flag_drops_to_the_tier_it_accepts(
|
||||
self, local_model_cost_map, model, provider
|
||||
):
|
||||
"""Chat Completions answers ``max`` on gpt-5.6 with a 400 naming ``none`` through ``xhigh``
|
||||
(OpenAI, 2026-10-01) and none of these Azure rows declares the level, so the adapter sends
|
||||
the tier below instead of the rejected one."""
|
||||
assert _reasoning_effort_sent(model, provider, "max") == "xhigh"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, provider",
|
||||
[
|
||||
|
|
@ -92,11 +109,13 @@ class TestTheNormalizedTierIsTheTierSent:
|
|||
("us/gpt-6-astra", "azure"),
|
||||
],
|
||||
)
|
||||
def test_an_azure_hosted_astra_deployment_drops_to_the_tier_it_accepts(
|
||||
self, local_model_cost_map, model, provider
|
||||
):
|
||||
"""The deployment answers ``max`` with a 400 naming ``none`` through ``xhigh``, so the rows
|
||||
say so and the adapter sends the tier below instead of the rejected one."""
|
||||
def test_an_azure_hosted_astra_deployment_drops_to_the_tier_its_chat_route_accepts(
|
||||
self, local_model_cost_map: None, model: str, provider: str
|
||||
) -> None:
|
||||
"""A Foundry gpt-6-astra answers a Chat Completions ``max`` with a 400 naming low through
|
||||
xhigh while honoring the same level on the Responses route (2026-10-01). The row's flag is
|
||||
route-blind, so it keeps max off and the adapter, which cannot know which route the call
|
||||
takes, sends the tier below instead of the level chat rejects."""
|
||||
assert _reasoning_effort_sent(model, provider, "max") == "xhigh"
|
||||
|
||||
def test_the_openai_hosted_twin_still_sends_max(self, local_model_cost_map):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
|
|
@ -343,36 +346,80 @@ def test_azure_gpt_6_astra_takes_the_reasoning_series_request_shape():
|
|||
model="gpt-6-astra",
|
||||
custom_llm_provider="azure",
|
||||
max_tokens=100,
|
||||
reasoning_effort="max",
|
||||
reasoning_effort="xhigh",
|
||||
)
|
||||
assert params["max_completion_tokens"] == 100
|
||||
assert "max_tokens" not in params
|
||||
assert params["reasoning_effort"] == "max"
|
||||
assert params["reasoning_effort"] == "xhigh"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra"])
|
||||
def test_azure_gpt6_astra_reasoning_effort_none_unlocks_temperature(config: AzureOpenAIGPT5Config, model: str):
|
||||
"""Foundry's gpt-6-astra accepts reasoning_effort='none' and, only then, a non-default
|
||||
temperature (verified live against a Foundry deployment), unlike OpenAI's gpt-6-astra."""
|
||||
params = config.map_openai_params(
|
||||
non_default_params={"temperature": 0.2, "reasoning_effort": "none"},
|
||||
def _azure_chat_row_forwards(row: Mapping[str, object], level: str) -> bool:
|
||||
"""Azure chat polarity: max is never gated, xhigh and none need an explicit true, every other
|
||||
level only has to not be false."""
|
||||
if level == "max":
|
||||
return True
|
||||
flag: Final = row.get(f"supports_{level}_reasoning_effort")
|
||||
return flag is True if level in ("xhigh", "none") else flag is not False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra", "azure/eu/gpt-6-astra"])
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_azure_gpt6_astra_forwards_exactly_the_effort_levels_its_map_row_allows(
|
||||
config: AzureOpenAIGPT5Config, model: str, level: str
|
||||
):
|
||||
"""The azure/ row is the whole contract for a Foundry deployment. A live Foundry gpt-6-astra
|
||||
(model gpt-6-astra-2026-09-03) answered reasoning_effort none with a 400 naming low, medium,
|
||||
high and xhigh on 2026-10-01, so its row turns none and minimal off. The row keeps max off too,
|
||||
since chat refuses it, but no gate reads that flag: the Responses route honors max, so chat
|
||||
forwards it for the bridge and a plain chat call gets the provider's own 400."""
|
||||
forwarded: Final = _azure_chat_row_forwards(litellm.model_cost[model], level)
|
||||
dropped: Final = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
drop_params=True,
|
||||
api_version="2025-04-01-preview",
|
||||
)
|
||||
assert params["temperature"] == 0.2
|
||||
assert params["reasoning_effort"] == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra"])
|
||||
def test_azure_gpt6_astra_rejects_reasoning_effort_minimal(config: AzureOpenAIGPT5Config, model: str):
|
||||
"""Foundry's gpt-6-astra lists none, low, medium, high, xhigh and max but not minimal."""
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": "minimal"},
|
||||
assert ("reasoning_effort" in dropped) is forwarded
|
||||
if forwarded:
|
||||
kept: Final = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
api_version="2025-04-01-preview",
|
||||
)
|
||||
assert kept["reasoning_effort"] == level
|
||||
else:
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
api_version="2025-04-01-preview",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra"])
|
||||
def test_azure_gpt6_astra_row_turns_none_and_minimal_off(model: str):
|
||||
"""Pinned on purpose so the row-derived test above cannot go vacuous: a Foundry gpt-6-astra
|
||||
deployment refused none on chat and on Responses on 2026-10-01 (400 unsupported_value naming
|
||||
low, medium, high, xhigh), and minimal the same way; max it refused on chat while honoring it
|
||||
on Responses, so the route-blind flag stays off. When Azure adds a level, flip the row and
|
||||
this assertion together."""
|
||||
row: Final = litellm.model_cost[model]
|
||||
assert row["supports_none_reasoning_effort"] is False
|
||||
assert row["supports_minimal_reasoning_effort"] is False
|
||||
assert row["supports_max_reasoning_effort"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["azure/gpt-6.1-sol", "azure/gpt-6.1-sol-2026-09-29", "azure_ai/gpt-6.1-sol"])
|
||||
def test_azure_gpt61_sol_rows_turn_none_off(key: str):
|
||||
"""Pinned on purpose, as the astra row above: a Foundry gpt-6.1-sol deployment (model
|
||||
gpt-6.1-sol-2026-09-29) refused none on chat and on Responses on 2026-10-02 (400 unsupported_value
|
||||
naming low, medium, high, xhigh, and max on Responses), and minimal the same way, while these rows
|
||||
still said none was on. When Azure adds a level, flip the row and this assertion together."""
|
||||
row: Final = litellm.model_cost[key]
|
||||
assert row["supports_none_reasoning_effort"] is False
|
||||
assert row["supports_minimal_reasoning_effort"] is False
|
||||
|
|
|
|||
|
|
@ -651,22 +651,6 @@ def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
litellm.add_known_models(model_cost_map=litellm.model_cost)
|
||||
|
||||
|
||||
def test_azure_responses_gpt6_astra_reasoning_effort_none_unlocks_temperature(local_model_cost_map: None):
|
||||
"""Foundry's gpt-6-astra accepts reasoning.effort='none' with a non-default temperature
|
||||
while OpenAI's gpt-6-astra does not, so the gate must read the azure/ cost-map entry
|
||||
for the bare deployment name rather than OpenAI's."""
|
||||
params = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(
|
||||
temperature=0.2,
|
||||
reasoning={"effort": "none"},
|
||||
),
|
||||
model="gpt-6-astra",
|
||||
drop_params=False,
|
||||
)
|
||||
assert params["temperature"] == 0.2
|
||||
assert params["reasoning"] == {"effort": "none"}
|
||||
|
||||
|
||||
def test_azure_responses_gpt6_astra_rejects_temperature_while_reasoning(local_model_cost_map: None):
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
|
|
@ -679,6 +663,86 @@ def test_azure_responses_gpt6_astra_rejects_temperature_while_reasoning(local_mo
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["gpt-6-astra", "gpt-6.1-sol"])
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_azure_responses_gpt6_effort_levels_follow_the_azure_row(local_model_cost_map: None, model: str, level: str):
|
||||
"""A bare deployment name reads the azure/ row, opt-out for every level but max, so the none
|
||||
and minimal Foundry refuses on this route (live 400s on 2026-10-01 for gpt-6-astra and 2026-10-02
|
||||
for gpt-6.1-sol) are dropped or refused here while the max it honors goes through whatever the
|
||||
route-blind flag says."""
|
||||
forwarded = (
|
||||
level == "max" or litellm.model_cost[f"azure/{model}"].get(f"supports_{level}_reasoning_effort") is not False
|
||||
)
|
||||
dropped = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": level}),
|
||||
model=model,
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning" in dropped) is forwarded
|
||||
if forwarded:
|
||||
kept = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": level}),
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
assert kept["reasoning"] == {"effort": level}
|
||||
else:
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": level}),
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_azure_gpt6_astra_keeps_max_on_this_route_whatever_its_row_says(local_model_cost_map: None):
|
||||
"""Pinned on purpose: the row-derived test above exempts max by hand, so this one has to fail
|
||||
if the gate ever reads the flag. A Foundry gpt-6-astra deployment answered reasoning.effort max
|
||||
through /v1/responses with 200, the effort echoed back and reasoning tokens billed, on
|
||||
2026-10-01, while chat refuses the same level, as OpenAI's chat does. The row keeps max off for
|
||||
the adapter and the picker, and this gate still forwards it even under drop_params."""
|
||||
assert litellm.model_cost["azure/gpt-6-astra"]["supports_max_reasoning_effort"] is False
|
||||
kept = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": "max"}),
|
||||
model="gpt-6-astra",
|
||||
drop_params=True,
|
||||
)
|
||||
assert kept["reasoning"] == {"effort": "max"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_azure_responses_effort_gate_fires_when_azure_ai_prefix_survives_provider_remap(
|
||||
local_model_cost_map: None, level: str
|
||||
):
|
||||
"""litellm.responses(model="azure_ai/gpt-6.1-sol", api_base=<Foundry host>) is re-routed to the
|
||||
azure provider with the prefix intact, so this config sees the prefixed name. The gate used to
|
||||
skip every prefixed name as another provider's pass-through, and a live Foundry gpt-6.1-sol
|
||||
answered the forwarded none and minimal with 400 on 2026-10-02 even under drop_params. The
|
||||
prefixed name reads the azure_ai/ row and drops or refuses exactly what that row turns off."""
|
||||
forwarded = (
|
||||
level == "max"
|
||||
or litellm.model_cost["azure_ai/gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is not False
|
||||
)
|
||||
dropped = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(
|
||||
max_output_tokens=60, reasoning={"effort": level}
|
||||
),
|
||||
model="azure_ai/gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning" in dropped) is forwarded
|
||||
if forwarded:
|
||||
return
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(
|
||||
max_output_tokens=60, reasoning={"effort": level}
|
||||
),
|
||||
model="azure_ai/gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_azure_responses_sends_the_deployment_name_when_azure_ai_prefix_survives_provider_remap():
|
||||
request = AzureOpenAIResponsesAPIConfig().transform_responses_api_request(
|
||||
model="azure_ai/gpt-5.4-nano",
|
||||
|
|
|
|||
|
|
@ -146,17 +146,6 @@ def _local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
monkeypatch.setattr(litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url))
|
||||
|
||||
|
||||
def test_foundry_gpt_6_astra_keeps_sampling_params_when_reasoning_effort_is_none(_local_model_cost_map):
|
||||
optional_params = AzureAIStudioConfig().map_openai_params(
|
||||
non_default_params={"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9},
|
||||
optional_params={},
|
||||
model="gpt-6-astra",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert optional_params == {"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9}
|
||||
|
||||
|
||||
def test_a_gpt_5_name_without_a_foundry_row_keeps_reading_its_own_entry(
|
||||
monkeypatch: pytest.MonkeyPatch, _local_model_cost_map
|
||||
):
|
||||
|
|
@ -180,6 +169,18 @@ def test_a_gpt_5_name_without_a_foundry_row_keeps_reading_its_own_entry(
|
|||
assert optional_params["logprobs"] is True
|
||||
|
||||
|
||||
def test_foundry_gpt_6_astra_refuses_reasoning_effort_none_before_the_call(_local_model_cost_map):
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
AzureAIStudioConfig().map_openai_params(
|
||||
non_default_params={"reasoning_effort": "none"},
|
||||
optional_params={},
|
||||
model="gpt-6-astra",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_azure_ai_grok_stop_parameter_handling():
|
||||
"""
|
||||
Test that Grok models properly handle stop parameter filtering in Azure AI Studio.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import json
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
||||
from litellm.llms.azure_ai.responses.transformation import AzureAIResponsesAPIConfig
|
||||
from litellm.responses.main import _will_bridge_to_chat_completions
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
|
@ -309,3 +312,7 @@ async def test_aresponses_serverless_host_stays_on_chat_bridge():
|
|||
assert chat_route.called
|
||||
assert not responses_route.called
|
||||
assert chat_route.calls.last.request.headers["Authorization"] == "Bearer fake-key"
|
||||
|
||||
|
||||
def _responses_params(**fields: object) -> ResponsesAPIOptionalRequestParams:
|
||||
return ResponsesAPIOptionalRequestParams(**fields) # pyright: ignore[reportArgumentType] # TypedDict kwargs from a test helper
|
||||
|
|
|
|||
|
|
@ -1858,6 +1858,115 @@ class TestResponsesSurfaceSharesTheEffortRule:
|
|||
)
|
||||
assert ("top_p" in mapped) is top_p_survives
|
||||
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_effort_levels_the_map_row_turns_off_are_dropped_or_refused(self, local_model_cost_map, level):
|
||||
"""Same row as the chat gate, read opt-out for every level but max. gpt-6.1-sol's row turns
|
||||
none and minimal off: OpenAI lists low, medium, high, xhigh and max for it
|
||||
(https://developers.openai.com/api/docs/models/gpt-6.1-sol, via #43910), and a local proxy
|
||||
got OpenAI's 400 for none and minimal through /v1/responses on 2026-09-30."""
|
||||
forwarded = (
|
||||
level == "max" or litellm.model_cost["gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is not False
|
||||
)
|
||||
dropped = OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning" in dropped) is forwarded
|
||||
if forwarded:
|
||||
kept = OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
assert kept["reasoning"] == {"effort": level}
|
||||
else:
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flags", "level", "forwarded"),
|
||||
[
|
||||
({}, "none", True),
|
||||
({"supports_none_reasoning_effort": False}, "none", False),
|
||||
({"supports_none_reasoning_effort": True}, "none", True),
|
||||
({"supports_minimal_reasoning_effort": False}, "minimal", False),
|
||||
({"supports_max_reasoning_effort": False}, "max", True),
|
||||
({}, "xhigh", True),
|
||||
({"supports_xhigh_reasoning_effort": False}, "xhigh", False),
|
||||
({"supports_none_reasoning_effort": False}, "medium", True),
|
||||
],
|
||||
)
|
||||
def test_the_row_flags_alone_decide_whether_a_level_is_forwarded(
|
||||
self, local_model_cost_map, monkeypatch, flags, level, forwarded
|
||||
):
|
||||
"""Same model name, rewritten row: this surface reads every level opt-out, xhigh included,
|
||||
so the gate flips with the flags alone. max is the one level no flag can turn off here,
|
||||
since the same model honors it on this route and refuses it on chat. The chat test module
|
||||
pins the real row's facts."""
|
||||
real_row = litellm.model_cost["gpt-6.1-sol"]
|
||||
without_flags = {
|
||||
field: value
|
||||
for field, value in real_row.items()
|
||||
if not (field.startswith("supports_") and field.endswith("_reasoning_effort"))
|
||||
}
|
||||
monkeypatch.setitem(litellm.model_cost, "gpt-6.1-sol", {**without_flags, **flags})
|
||||
dropped = OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning" in dropped) is forwarded
|
||||
if not forwarded:
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
def test_dropping_a_refused_effort_keeps_the_rest_of_the_reasoning_object(self, local_model_cost_map):
|
||||
refused = next(
|
||||
level
|
||||
for level in ("none", "minimal", "low")
|
||||
if litellm.model_cost["gpt-6.1-sol"].get(f"supports_{level}_reasoning_effort") is False
|
||||
)
|
||||
mapped = OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": refused, "summary": "auto"}},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert mapped["reasoning"] == {"summary": "auto"}
|
||||
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_gpt5_3_codex_forwards_exactly_the_effort_levels_its_map_row_allows(self, local_model_cost_map, level):
|
||||
"""The gate turns a stale false into a local refusal, so the Responses-only codex row has to
|
||||
say what the route does. On 2026-10-01 /v1/responses answered gpt-5.3-codex none and xhigh
|
||||
with 200 (effort echoed back) and minimal and max with 400 unsupported_value naming none,
|
||||
low, medium, high and xhigh, so the row takes none and xhigh and turns minimal off."""
|
||||
forwarded = (
|
||||
level == "max" or litellm.model_cost["gpt-5.3-codex"].get(f"supports_{level}_reasoning_effort") is not False
|
||||
)
|
||||
mapped = OpenAIResponsesAPIConfig().map_openai_params(
|
||||
response_api_optional_params={"reasoning": {"effort": level}},
|
||||
model="gpt-5.3-codex",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning" in mapped) is forwarded
|
||||
|
||||
def test_gpt5_3_codex_row_takes_none_and_xhigh(self, local_model_cost_map):
|
||||
"""Pinned on purpose so the row-derived test above cannot go vacuous: the live answers of
|
||||
2026-10-01 listed above. When OpenAI changes the set, flip the row and this assertion
|
||||
together."""
|
||||
row = litellm.model_cost["gpt-5.3-codex"]
|
||||
assert row["supports_none_reasoning_effort"] is True
|
||||
assert row["supports_xhigh_reasoning_effort"] is True
|
||||
assert row["supports_minimal_reasoning_effort"] is False
|
||||
|
||||
def test_top_p_raises_without_drop_params(self, local_model_cost_map):
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
OpenAIResponsesAPIConfig().map_openai_params(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -447,6 +449,105 @@ def test_gpt5_drops_reasoning_effort_minimal_when_requested(config: OpenAIConfig
|
|||
assert "reasoning_effort" not in params
|
||||
|
||||
|
||||
def _chat_row_forwards(row: Mapping[str, object], level: str) -> bool:
|
||||
"""Chat polarity: max is never gated, xhigh needs an explicit true, every other level only has
|
||||
to not be false."""
|
||||
if level == "max":
|
||||
return True
|
||||
flag: Final = row.get(f"supports_{level}_reasoning_effort")
|
||||
return flag is True if level == "xhigh" else flag is not False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("level", ["none", "minimal", "low", "medium", "high", "xhigh", "max"])
|
||||
def test_gpt6_1_sol_forwards_exactly_the_effort_levels_its_map_row_allows(config: OpenAIConfig, level: str):
|
||||
"""The map row is the whole contract, so a new GPT-6 model is gated by its row with no code
|
||||
change. gpt-6.1-sol's row turns none and minimal off: OpenAI lists low, medium, high, xhigh
|
||||
and max for it (https://developers.openai.com/api/docs/models/gpt-6.1-sol, via #43910), and a
|
||||
local proxy got OpenAI's 400 for none and minimal on both endpoints on 2026-09-30."""
|
||||
forwarded: Final = _chat_row_forwards(litellm.model_cost["gpt-6.1-sol"], level)
|
||||
dropped: Final = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning_effort" in dropped) is forwarded
|
||||
if forwarded:
|
||||
kept: Final = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
assert kept["reasoning_effort"] == level
|
||||
else:
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_gpt6_1_sol_row_turns_none_and_minimal_off():
|
||||
"""Pinned on purpose so the row-derived test above cannot go vacuous: OpenAI lists low, medium,
|
||||
high, xhigh and max for gpt-6.1-sol and neither none nor minimal
|
||||
(https://developers.openai.com/api/docs/models/gpt-6.1-sol, read 2026-09-30 via #43910), and a
|
||||
local proxy got OpenAI's 400 for both on 2026-09-30. When OpenAI adds a level, flip the row and
|
||||
this assertion together."""
|
||||
row: Final = litellm.model_cost["gpt-6.1-sol"]
|
||||
assert row["supports_none_reasoning_effort"] is False
|
||||
assert row["supports_minimal_reasoning_effort"] is False
|
||||
|
||||
|
||||
def _row_without_effort_flags(row: Mapping[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
field: value
|
||||
for field, value in row.items()
|
||||
if not (field.startswith("supports_") and field.endswith("_reasoning_effort"))
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("flags", "level", "forwarded"),
|
||||
[
|
||||
({}, "none", True),
|
||||
({"supports_none_reasoning_effort": False}, "none", False),
|
||||
({"supports_none_reasoning_effort": True}, "none", True),
|
||||
({"supports_minimal_reasoning_effort": False}, "minimal", False),
|
||||
({"supports_max_reasoning_effort": False}, "max", True),
|
||||
({}, "max", True),
|
||||
({}, "xhigh", False),
|
||||
({"supports_xhigh_reasoning_effort": True}, "xhigh", True),
|
||||
({"supports_none_reasoning_effort": False}, "medium", True),
|
||||
],
|
||||
)
|
||||
def test_the_row_flags_alone_decide_whether_a_chat_level_is_forwarded(
|
||||
config: OpenAIConfig, monkeypatch: pytest.MonkeyPatch, flags: dict[str, bool], level: str, forwarded: bool
|
||||
):
|
||||
"""Same model name, rewritten row: the gate flips with the flags and nothing else, so breaking the
|
||||
gate fails here while the pinned test above catches the real row losing a flag."""
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost, "gpt-6.1-sol", {**_row_without_effort_flags(litellm.model_cost["gpt-6.1-sol"]), **flags}
|
||||
)
|
||||
dropped: Final = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=True,
|
||||
)
|
||||
assert ("reasoning_effort" in dropped) is forwarded
|
||||
if not forwarded:
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": level},
|
||||
optional_params={},
|
||||
model="gpt-6.1-sol",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_gpt5_minimal_dict_triggers_validation(config: OpenAIConfig):
|
||||
"""Dict with effort='minimal' triggers minimal model-support validation."""
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||
|
|
|
|||
|
|
@ -398,19 +398,21 @@ class TestGpt6AstraAdvertisesItsDocumentedLevels:
|
|||
("azure_ai/gpt-6-astra", "azure_ai"),
|
||||
],
|
||||
)
|
||||
def test_an_azure_hosted_deployment_advertises_none_but_not_max(
|
||||
def test_an_azure_hosted_deployment_advertises_the_levels_both_its_routes_accept(
|
||||
self, local_model_cost_map, model, custom_llm_provider
|
||||
):
|
||||
"""Microsoft hosts the same model with a different level set than OpenAI does. Verified live
|
||||
on both Azure routes: none returns 200 with zero reasoning tokens and unlocks temperature,
|
||||
which OpenAI's API rejects, while max returns 400 unsupported_value naming none through
|
||||
xhigh as the levels it does take."""
|
||||
"""Verified live on 2026-10-01 against a Foundry gpt-6-astra deployment (model
|
||||
gpt-6-astra-2026-09-03): none returns 400 unsupported_value on chat and on Responses, naming
|
||||
low, medium, high and xhigh as the levels it takes; Responses also honors max with reasoning
|
||||
tokens billed while chat refuses it, as OpenAI's chat does. The flag is route-blind, so the
|
||||
row keeps max off and the advertised set stops at the level every route accepts.
|
||||
Earlier evidence (2026-09-05, commit e79f3ec5205) had none accepted on chat; the deployment
|
||||
no longer behaves that way."""
|
||||
from litellm.utils import _get_model_info_helper
|
||||
|
||||
model_info = dict(_get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider))
|
||||
|
||||
assert resolve_supported_reasoning_efforts(model_info, deployment_is_mapped=True) == (
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
|
|
|
|||
|
|
@ -973,6 +973,50 @@ def test_responses_api_bridge_check_gpt_5_4_tools_with_reasoning_none_stays_chat
|
|||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, custom_llm_provider, reasoning_effort",
|
||||
[
|
||||
("gpt-6.1-sol", "openai", "none"),
|
||||
("gpt-6.1-sol", "openai", {"effort": "none"}),
|
||||
("gpt-6-astra", "azure", "none"),
|
||||
],
|
||||
)
|
||||
def test_responses_api_bridge_check_tools_with_a_none_the_row_refuses_routes_to_responses(
|
||||
local_model_cost_map: None, model: str, custom_llm_provider: str, reasoning_effort: object
|
||||
) -> None:
|
||||
"""The ``none`` escape hatch only holds when the chat gate forwards it. These rows refuse
|
||||
``none``, so the gate drops it (or refuses the call) and the provider runs its default effort,
|
||||
under which Chat Completions rejects function tools; the bridge has to fire the way it does
|
||||
for an unset effort. Row-derived: the test follows the flag, never the vendor's answer."""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
row_key: Final = model if custom_llm_provider == "openai" else f"{custom_llm_provider}/{model}"
|
||||
assert litellm.model_cost[row_key]["supports_none_reasoning_effort"] is False
|
||||
|
||||
model_info, _ = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
|
||||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_a_none_the_row_refuses_stays_chat_without_tools(
|
||||
local_model_cost_map: None,
|
||||
) -> None:
|
||||
"""Without function tools nothing forces the Responses route: the dropped ``none`` leaves a
|
||||
plain chat call at the default effort, as for any other dropped level."""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
model_info, _ = responses_api_bridge_check(
|
||||
model="gpt-6.1-sol", custom_llm_provider="openai", reasoning_effort="none"
|
||||
)
|
||||
|
||||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_reasoning_none_with_summary_still_routes_to_responses():
|
||||
"""A reasoning summary is Responses-only regardless of effort value."""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
|
@ -1308,6 +1352,43 @@ def test_responses_api_bridge_check_azure_with_api_base_and_unset_effort_routes(
|
|||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_FOUNDRY_API_BASE: Final = "https://myproject.services.ai.azure.com"
|
||||
_FOUNDRY_FUNCTION_TOOL: Final = ({"type": "function", "function": {"name": "get_weather"}},)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name, api_base, reasoning_effort",
|
||||
[
|
||||
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, None, id="gpt-5.6-unset-effort-stays-chat"),
|
||||
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, "none", id="gpt-5.6-explicit-none-stays-chat"),
|
||||
pytest.param("gpt-5.5", _FOUNDRY_API_BASE, "high", id="gpt-5.5-explicit-effort-stays-chat"),
|
||||
pytest.param("gpt-5.4-mini", _FOUNDRY_API_BASE, None, id="gpt-5.4-mini-unset-effort-stays-chat"),
|
||||
pytest.param("gpt-5.4-mini", _FOUNDRY_API_BASE, "low", id="gpt-5.4-mini-explicit-effort-stays-chat"),
|
||||
pytest.param("gpt-6-astra", "https://myproject.models.ai.azure.com", None, id="serverless-host-stays-chat"),
|
||||
pytest.param("Mistral-large-2411", _FOUNDRY_API_BASE, None, id="non-gpt-5-model-stays-chat"),
|
||||
pytest.param("claude-opus-4-1", _FOUNDRY_API_BASE, None, id="claude-on-foundry-stays-chat"),
|
||||
],
|
||||
)
|
||||
def test_responses_api_bridge_check_azure_ai_without_foundry_responses_route_stays_chat(
|
||||
model_name, api_base, reasoning_effort
|
||||
):
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
model_info, model = responses_api_bridge_check(
|
||||
model=model_name,
|
||||
custom_llm_provider="azure_ai",
|
||||
tools=_FOUNDRY_FUNCTION_TOOL,
|
||||
reasoning_effort=reasoning_effort,
|
||||
api_base=api_base,
|
||||
)
|
||||
|
||||
assert model == model_name
|
||||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_older_gpt_5_tools_without_reasoning_stays_chat():
|
||||
"""Pre-5.4 GPT-5 names keep the old boundary: tools alone never bridge."""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue