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.104.x.
Cherry-picked from d7620c77a1 (main).
Adapted to the line: the bridge check lives in litellm/main.py rather than litellm/responses/bridge_check.py, the effort helpers keep the line's private names, and the azure gpt-6.1-sol rows get supports_none_reasoning_effort=false (from #46377 on main) since this fix and its tests key on it. Test files keep the line's content plus this PR's own hunks.
This commit is contained in:
parent
93faf0a1d5
commit
54f9259b40
18 changed files with 910 additions and 179 deletions
|
|
@ -1,12 +1,10 @@
|
||||||
"""Support for Azure OpenAI gpt-5 model family."""
|
"""Support for Azure OpenAI gpt-5 model family."""
|
||||||
|
|
||||||
from typing import Final
|
from typing import ClassVar, Final
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm.exceptions import UnsupportedParamsError
|
|
||||||
from litellm.llms.openai.chat.gpt_5_transformation import (
|
from litellm.llms.openai.chat.gpt_5_transformation import (
|
||||||
OpenAIGPT5Config,
|
OpenAIGPT5Config,
|
||||||
_get_effort_level,
|
|
||||||
is_gpt_reasoning_series_name,
|
is_gpt_reasoning_series_name,
|
||||||
)
|
)
|
||||||
from litellm.types.llms.openai import AllMessageValues
|
from litellm.types.llms.openai import AllMessageValues
|
||||||
|
|
@ -15,9 +13,16 @@ from .gpt_transformation import AzureOpenAIConfig
|
||||||
|
|
||||||
|
|
||||||
class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
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/"
|
GPT5_SERIES_ROUTE = "gpt5_series/"
|
||||||
|
OPT_IN_REASONING_EFFORTS: ClassVar[frozenset[str]] = frozenset(("xhigh", "none"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _model_map_lookup_name(cls, model: str) -> str:
|
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
|
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
|
resolver rather than one lookup means the supports, explicitly-disabled and
|
||||||
default-effort answers all read the same entry.
|
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):
|
if model.startswith(cls.GPT5_SERIES_ROUTE):
|
||||||
return "azure/" + model[len(cls.GPT5_SERIES_ROUTE) :]
|
return "azure/" + model[len(cls.GPT5_SERIES_ROUTE) :]
|
||||||
if model.startswith("azure/"):
|
if model.startswith("azure/"):
|
||||||
return model
|
return model
|
||||||
|
if model.startswith("azure_ai/"):
|
||||||
|
return model if model in litellm.model_cost else "azure/" + model.removeprefix("azure_ai/")
|
||||||
return "azure/" + model
|
return "azure/" + model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -75,34 +87,7 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
||||||
drop_params: bool,
|
drop_params: bool,
|
||||||
api_version: str = "",
|
api_version: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
reasoning_effort_value = non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
return OpenAIGPT5Config.map_openai_params(
|
||||||
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(
|
|
||||||
self,
|
self,
|
||||||
non_default_params=non_default_params,
|
non_default_params=non_default_params,
|
||||||
optional_params=optional_params,
|
optional_params=optional_params,
|
||||||
|
|
@ -110,17 +95,6 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config):
|
||||||
drop_params=drop_params,
|
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(
|
def transform_request(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,18 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||||
def custom_llm_provider(self) -> LlmProviders:
|
def custom_llm_provider(self) -> LlmProviders:
|
||||||
return LlmProviders.AZURE
|
return LlmProviders.AZURE
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_gpt_5_model(model: str) -> bool:
|
||||||
|
return AzureOpenAIGPT5Config.is_model_gpt_5_model(model)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _supports_reasoning_effort_none(model: str) -> bool:
|
def _supports_reasoning_effort_none(model: str) -> bool:
|
||||||
return AzureOpenAIGPT5Config._supports_reasoning_effort_level(model, "none")
|
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
|
@staticmethod
|
||||||
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
||||||
return AzureOpenAIGPT5Config.effort_resolves_to_none(model, effort)
|
return AzureOpenAIGPT5Config.effort_resolves_to_none(model, effort)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Final
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
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 (
|
from litellm.llms.azure_ai.common_utils import (
|
||||||
AzureFoundryModelInfo,
|
AzureFoundryModelInfo,
|
||||||
api_key_header_for_base,
|
api_key_header_for_base,
|
||||||
|
|
@ -27,6 +28,18 @@ class AzureAIResponsesAPIConfig(AzureOpenAIResponsesAPIConfig):
|
||||||
def custom_llm_provider(self) -> LlmProviders:
|
def custom_llm_provider(self) -> LlmProviders:
|
||||||
return LlmProviders.AZURE_AI
|
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:
|
def validate_environment(self, headers: dict, model: str, litellm_params: GenericLiteLLMParams | None) -> dict:
|
||||||
params: Final = litellm_params or GenericLiteLLMParams()
|
params: Final = litellm_params or GenericLiteLLMParams()
|
||||||
auth_headers: Final = get_azure_ai_auth_headers(
|
auth_headers: Final = get_azure_ai_auth_headers(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""Support for OpenAI gpt-5 model family."""
|
"""Support for OpenAI gpt-5 model family."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Final
|
from typing import ClassVar, Final
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm.utils import (
|
from litellm.utils import (
|
||||||
|
|
@ -67,6 +67,10 @@ def _get_effort_level(value: str | dict | None) -> str | None:
|
||||||
GPT_REASONING_SERIES_MARKERS: Final = ("gpt-5", "gpt-6")
|
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:
|
def is_gpt_reasoning_series_name(model: str) -> bool:
|
||||||
normalized: Final = model.split("/")[-1]
|
normalized: Final = model.split("/")[-1]
|
||||||
return any(marker in model for marker in GPT_REASONING_SERIES_MARKERS) and not normalized.startswith("gpt-5-chat")
|
return any(marker in model for marker in GPT_REASONING_SERIES_MARKERS) and not normalized.startswith("gpt-5-chat")
|
||||||
|
|
@ -82,6 +86,9 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
||||||
- Support for GPT-5-Codex models optimized for code generation.
|
- 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
|
@classmethod
|
||||||
def is_model_gpt_5_model(cls, model: str) -> bool:
|
def is_model_gpt_5_model(cls, model: str) -> bool:
|
||||||
return is_gpt_reasoning_series_name(model)
|
return is_gpt_reasoning_series_name(model)
|
||||||
|
|
@ -193,15 +200,10 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@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.
|
"""Return True only when the model map explicitly sets the capability to False.
|
||||||
|
|
||||||
Unlike ``_supports_reasoning_effort_level`` (which requires an explicit True),
|
A missing key is not a disable. Unknown models pass through.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
return is_explicitly_disabled_factory(
|
return is_explicitly_disabled_factory(
|
||||||
model=cls._model_map_lookup_name(model),
|
model=cls._model_map_lookup_name(model),
|
||||||
|
|
@ -209,6 +211,39 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
||||||
key=f"supports_{level}_reasoning_effort",
|
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:
|
def get_supported_openai_params(self, model: str) -> list:
|
||||||
if self.is_model_gpt_5_search_model(model):
|
if self.is_model_gpt_5_search_model(model):
|
||||||
return [
|
return [
|
||||||
|
|
@ -269,12 +304,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
||||||
drop_params=drop_params,
|
drop_params=drop_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get raw reasoning_effort and effective effort level for all guards.
|
# Dict inputs like {"effort": "none", "summary": "detailed"} are judged by their effort
|
||||||
# Use effective_effort (extracted string) for xhigh validation, "none" checks, and
|
# string so the gate, the sampling guard, and the temperature guard all see one level.
|
||||||
# tool/sampling guards — dict inputs like {"effort": "none", "summary": "detailed"}
|
raw_reasoning_effort: Final = non_default_params.get("reasoning_effort") or optional_params.get(
|
||||||
# must be treated as effort="none" to avoid incorrect tool-drop or sampling errors.
|
"reasoning_effort"
|
||||||
raw_reasoning_effort = non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
)
|
||||||
effective_effort: Final = _get_effort_level(raw_reasoning_effort)
|
requested_effort: Final = _get_effort_level(raw_reasoning_effort)
|
||||||
|
|
||||||
# Normalize dict reasoning_effort to string for Chat Completions API.
|
# Normalize dict reasoning_effort to string for Chat Completions API.
|
||||||
# Example: {"effort": "high", "summary": "detailed"} -> "high"
|
# Example: {"effort": "high", "summary": "detailed"} -> "high"
|
||||||
|
|
@ -286,31 +321,20 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
||||||
if "reasoning_effort" in optional_params:
|
if "reasoning_effort" in optional_params:
|
||||||
optional_params["reasoning_effort"] = normalized
|
optional_params["reasoning_effort"] = normalized
|
||||||
|
|
||||||
if effective_effort == "xhigh":
|
if requested_effort is not None and self.refuses_reasoning_effort_level(model, requested_effort):
|
||||||
# xhigh is an opt-in capability: only allow if model explicitly supports it.
|
if litellm.drop_params or drop_params:
|
||||||
if not self._supports_reasoning_effort_level(model, effective_effort):
|
non_default_params.pop("reasoning_effort", None)
|
||||||
if litellm.drop_params or drop_params:
|
optional_params.pop("reasoning_effort", None)
|
||||||
non_default_params.pop("reasoning_effort", None)
|
else:
|
||||||
optional_params.pop("reasoning_effort", None)
|
raise litellm.utils.UnsupportedParamsError(
|
||||||
else:
|
message=unsupported_reasoning_effort_message("reasoning_effort", requested_effort, model),
|
||||||
raise litellm.utils.UnsupportedParamsError(
|
status_code=400,
|
||||||
message=(f"reasoning_effort={effective_effort} is not supported for this model."),
|
)
|
||||||
status_code=400,
|
|
||||||
)
|
# The level the provider will see: None once dropped, so the guards below judge by the model's default effort
|
||||||
elif effective_effort in ("minimal", "low"):
|
effective_effort: Final = _get_effort_level(
|
||||||
# minimal/low are opt-out: unknown models pass through; only block when
|
non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort")
|
||||||
# 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,
|
|
||||||
)
|
|
||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
# max_tokens is not supported for gpt-5 models on OpenAI API
|
# 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.safe_json_loads import safe_json_loads
|
||||||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
|
||||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
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.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.secret_managers.main import get_secret_str
|
||||||
from litellm.types.llms.openai import *
|
from litellm.types.llms.openai import *
|
||||||
|
|
@ -136,6 +139,20 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
||||||
|
|
||||||
return supports_none_reasoning_effort(model=model, custom_llm_provider=None)
|
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
|
@staticmethod
|
||||||
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
def _effort_resolves_to_none(model: str, effort: str | None) -> bool:
|
||||||
"""Whether this request's reasoning effort ends up as "none", the one condition
|
"""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)
|
lookup_name: Final = self._model_map_lookup_name(model)
|
||||||
if self._is_gpt_5_model(model=lookup_name):
|
if self._is_gpt_5_model(model=lookup_name):
|
||||||
reasoning: Final = params.get("reasoning") or {}
|
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)
|
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)
|
effort_is_none: Final = supports_none and self._effort_resolves_to_none(lookup_name, effort)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1057,6 +1057,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(
|
def responses_api_bridge_check(
|
||||||
model: str,
|
model: str,
|
||||||
custom_llm_provider: str,
|
custom_llm_provider: str,
|
||||||
|
|
@ -1129,10 +1141,13 @@ def responses_api_bridge_check(
|
||||||
)
|
)
|
||||||
for tool in (tools or ())
|
for tool in (tools or ())
|
||||||
)
|
)
|
||||||
if isinstance(reasoning_effort, dict):
|
requested_effort: Final = reasoning_effort.get("effort") if isinstance(reasoning_effort, dict) else reasoning_effort
|
||||||
reasoning_active = reasoning_effort.get("effort") != "none" or reasoning_effort.get("summary") is not None
|
requested_summary: Final = reasoning_effort.get("summary") if isinstance(reasoning_effort, dict) else None
|
||||||
else:
|
reasoning_active: Final = (
|
||||||
reasoning_active = reasoning_effort != "none"
|
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
|
# 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
|
# host (the default URL or a PrivateLink hostname such as <region>.privatelink.api.openai.com) and
|
||||||
# by Azure OpenAI through the azure provider. Resolve the effective OpenAI base arg>global>env>default
|
# by Azure OpenAI through the azure provider. Resolve the effective OpenAI base arg>global>env>default
|
||||||
|
|
|
||||||
|
|
@ -3805,7 +3805,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_cache_breakpoint": true,
|
"supports_prompt_cache_breakpoint": true,
|
||||||
|
|
@ -8251,7 +8251,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -8300,7 +8300,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -8841,7 +8841,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -30766,9 +30766,9 @@
|
||||||
"supports_response_schema": true,
|
"supports_response_schema": true,
|
||||||
"supports_vision": true,
|
"supports_vision": true,
|
||||||
"supports_reasoning": true,
|
"supports_reasoning": true,
|
||||||
"supports_none_reasoning_effort": false,
|
"supports_none_reasoning_effort": true,
|
||||||
"supports_xhigh_reasoning_effort": false,
|
"supports_xhigh_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": true
|
"supports_minimal_reasoning_effort": false
|
||||||
},
|
},
|
||||||
"chatgpt/gpt-5.3-codex-spark": {
|
"chatgpt/gpt-5.3-codex-spark": {
|
||||||
"litellm_provider": "chatgpt",
|
"litellm_provider": "chatgpt",
|
||||||
|
|
@ -35018,11 +35018,13 @@
|
||||||
"supports_response_schema": true,
|
"supports_response_schema": true,
|
||||||
"supports_system_messages": false,
|
"supports_system_messages": false,
|
||||||
"supports_tool_choice": true,
|
"supports_tool_choice": true,
|
||||||
|
"supports_tool_search": true,
|
||||||
"supports_vision": true,
|
"supports_vision": true,
|
||||||
"supports_web_search": true,
|
"supports_web_search": true,
|
||||||
"supports_none_reasoning_effort": false,
|
"supports_none_reasoning_effort": true,
|
||||||
"supports_xhigh_reasoning_effort": false,
|
"supports_xhigh_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": true
|
"supports_minimal_reasoning_effort": false,
|
||||||
|
"deprecation_date": "2027-04-01"
|
||||||
},
|
},
|
||||||
"gpt-5.3-chat-latest": {
|
"gpt-5.3-chat-latest": {
|
||||||
"cache_read_input_token_cost": 1.75e-07,
|
"cache_read_input_token_cost": 1.75e-07,
|
||||||
|
|
@ -69878,18 +69880,47 @@
|
||||||
},
|
},
|
||||||
"azure/eu/gpt-6-astra": {
|
"azure/eu/gpt-6-astra": {
|
||||||
"deprecation_date": "2028-01-11",
|
"deprecation_date": "2028-01-11",
|
||||||
"cache_creation_input_token_cost": 1.375e-05,
|
"cache_creation_input_token_cost": 1.5e-05,
|
||||||
"cache_creation_input_token_cost_above_272k_tokens": 2.75e-05,
|
"cache_creation_input_token_cost_above_272k_tokens": 3e-05,
|
||||||
"cache_read_input_token_cost": 1.1e-06,
|
"cache_read_input_token_cost": 1.2e-06,
|
||||||
"cache_read_input_token_cost_above_272k_tokens": 2.2e-06,
|
"cache_read_input_token_cost_above_272k_tokens": 2.4e-06,
|
||||||
"input_cost_per_token": 1.1e-05,
|
"input_cost_per_token": 1.2e-05,
|
||||||
"input_cost_per_token_above_272k_tokens": 2.2e-05,
|
"input_cost_per_token_above_272k_tokens": 2.4e-05,
|
||||||
"litellm_provider": "azure",
|
"litellm_provider": "azure",
|
||||||
|
"max_input_tokens": 922000,
|
||||||
|
"max_output_tokens": 128000,
|
||||||
|
"max_tokens": 128000,
|
||||||
"mode": "chat",
|
"mode": "chat",
|
||||||
"output_cost_per_token": 5.5e-05,
|
"output_cost_per_token": 6e-05,
|
||||||
"output_cost_per_token_above_272k_tokens": 8.25e-05,
|
"output_cost_per_token_above_272k_tokens": 9e-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://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/models-sold-directly-by-azure",
|
||||||
"supports_reasoning": true
|
"supported_endpoints": [
|
||||||
|
"/v1/chat/completions",
|
||||||
|
"/v1/responses"
|
||||||
|
],
|
||||||
|
"supported_modalities": [
|
||||||
|
"text",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"supported_output_modalities": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"supports_computer_use": true,
|
||||||
|
"supports_function_calling": true,
|
||||||
|
"supports_max_reasoning_effort": false,
|
||||||
|
"supports_minimal_reasoning_effort": false,
|
||||||
|
"supports_native_streaming": true,
|
||||||
|
"supports_none_reasoning_effort": false,
|
||||||
|
"supports_parallel_function_calling": true,
|
||||||
|
"supports_pdf_input": true,
|
||||||
|
"supports_prompt_caching": true,
|
||||||
|
"supports_reasoning": true,
|
||||||
|
"supports_response_schema": true,
|
||||||
|
"supports_system_messages": true,
|
||||||
|
"supports_tool_choice": true,
|
||||||
|
"supports_vision": true,
|
||||||
|
"supports_web_search": true,
|
||||||
|
"supports_xhigh_reasoning_effort": true
|
||||||
},
|
},
|
||||||
"azure/eu/gpt-6-luna": {
|
"azure/eu/gpt-6-luna": {
|
||||||
"deprecation_date": "2028-03-11",
|
"deprecation_date": "2028-03-11",
|
||||||
|
|
@ -78374,7 +78405,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_cache_breakpoint": true,
|
"supports_prompt_cache_breakpoint": true,
|
||||||
|
|
@ -78423,7 +78454,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -78471,7 +78502,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
"""Resolve which reasoning_effort values a deployment, and by intersection a model group, accepts.
|
"""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
|
An entry that states its levels outright in reasoning_effort_levels wins whole, for a model whose set
|
||||||
whole, for a model whose set the per-level flags cannot express: Kimi K3 takes low, high and max,
|
the per-level flags cannot express (Kimi K3 takes low, high and max, and no flag can drop medium).
|
||||||
and no flag can drop medium because medium has none. Every other entry answers through the
|
Every other entry answers through its supports_*_reasoning_effort flags, read with the polarity the
|
||||||
supports_*_reasoning_effort flags below, whose polarity mirrors how a request path reads that same
|
chat gate uses: medium and high are unconditional, minimal and low are opt-out (refused only on an
|
||||||
flag. medium and high are unconditional for a reasoning model. minimal and low are opt-out:
|
explicit false), xhigh is opt-in, and none is opt-out except on the azure gpt-5 family, whose chat
|
||||||
openai/chat/gpt_5_transformation.py refuses them only when the map says false. xhigh and max are
|
config refuses it without an explicit true.
|
||||||
opt-in. none is opt-out everywhere except the azure gpt-5 family, whose config raises
|
|
||||||
UnsupportedParamsError 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
|
The Responses gate reads none, minimal, low and xhigh as opt-out. Neither gate judges max, which
|
||||||
all: outside the gpt-6-astra rows every entry carrying supports_max_reasoning_effort is Claude-family,
|
Chat Completions refuses and Responses honors on the gpt-6 family, so its flag is advisory and
|
||||||
and anthropic/chat/transformation.py gates max on the output_config path while its reasoning_effort
|
advertised opt-in: outside the gpt-6 rows every entry carrying supports_max_reasoning_effort is
|
||||||
path maps any level to a thinking budget. Making max opt-in is a deliberate trade, then, since an
|
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 a real one rather than litellm rounding the level
|
explicit flag is the only signal that the tier is real.
|
||||||
to a budget, and a missing flag costs advisory metadata rather than a rejected request.
|
|
||||||
|
|
||||||
A deployment the map describes with no effort flags at all resolves to None rather than to the
|
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
|
opt-out defaults. 689 of the map's 854 reasoning entries carry no flag, and the o-series, xai and
|
||||||
|
|
|
||||||
|
|
@ -3805,7 +3805,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_cache_breakpoint": true,
|
"supports_prompt_cache_breakpoint": true,
|
||||||
|
|
@ -8251,7 +8251,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -8300,7 +8300,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -8841,7 +8841,7 @@
|
||||||
"supports_max_reasoning_effort": false,
|
"supports_max_reasoning_effort": false,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -30766,9 +30766,9 @@
|
||||||
"supports_response_schema": true,
|
"supports_response_schema": true,
|
||||||
"supports_vision": true,
|
"supports_vision": true,
|
||||||
"supports_reasoning": true,
|
"supports_reasoning": true,
|
||||||
"supports_none_reasoning_effort": false,
|
"supports_none_reasoning_effort": true,
|
||||||
"supports_xhigh_reasoning_effort": false,
|
"supports_xhigh_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": true
|
"supports_minimal_reasoning_effort": false
|
||||||
},
|
},
|
||||||
"chatgpt/gpt-5.3-codex-spark": {
|
"chatgpt/gpt-5.3-codex-spark": {
|
||||||
"litellm_provider": "chatgpt",
|
"litellm_provider": "chatgpt",
|
||||||
|
|
@ -35018,11 +35018,13 @@
|
||||||
"supports_response_schema": true,
|
"supports_response_schema": true,
|
||||||
"supports_system_messages": false,
|
"supports_system_messages": false,
|
||||||
"supports_tool_choice": true,
|
"supports_tool_choice": true,
|
||||||
|
"supports_tool_search": true,
|
||||||
"supports_vision": true,
|
"supports_vision": true,
|
||||||
"supports_web_search": true,
|
"supports_web_search": true,
|
||||||
"supports_none_reasoning_effort": false,
|
"supports_none_reasoning_effort": true,
|
||||||
"supports_xhigh_reasoning_effort": false,
|
"supports_xhigh_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": true
|
"supports_minimal_reasoning_effort": false,
|
||||||
|
"deprecation_date": "2027-04-01"
|
||||||
},
|
},
|
||||||
"gpt-5.3-chat-latest": {
|
"gpt-5.3-chat-latest": {
|
||||||
"cache_read_input_token_cost": 1.75e-07,
|
"cache_read_input_token_cost": 1.75e-07,
|
||||||
|
|
@ -69878,18 +69880,47 @@
|
||||||
},
|
},
|
||||||
"azure/eu/gpt-6-astra": {
|
"azure/eu/gpt-6-astra": {
|
||||||
"deprecation_date": "2028-01-11",
|
"deprecation_date": "2028-01-11",
|
||||||
"cache_creation_input_token_cost": 1.375e-05,
|
"cache_creation_input_token_cost": 1.5e-05,
|
||||||
"cache_creation_input_token_cost_above_272k_tokens": 2.75e-05,
|
"cache_creation_input_token_cost_above_272k_tokens": 3e-05,
|
||||||
"cache_read_input_token_cost": 1.1e-06,
|
"cache_read_input_token_cost": 1.2e-06,
|
||||||
"cache_read_input_token_cost_above_272k_tokens": 2.2e-06,
|
"cache_read_input_token_cost_above_272k_tokens": 2.4e-06,
|
||||||
"input_cost_per_token": 1.1e-05,
|
"input_cost_per_token": 1.2e-05,
|
||||||
"input_cost_per_token_above_272k_tokens": 2.2e-05,
|
"input_cost_per_token_above_272k_tokens": 2.4e-05,
|
||||||
"litellm_provider": "azure",
|
"litellm_provider": "azure",
|
||||||
|
"max_input_tokens": 922000,
|
||||||
|
"max_output_tokens": 128000,
|
||||||
|
"max_tokens": 128000,
|
||||||
"mode": "chat",
|
"mode": "chat",
|
||||||
"output_cost_per_token": 5.5e-05,
|
"output_cost_per_token": 6e-05,
|
||||||
"output_cost_per_token_above_272k_tokens": 8.25e-05,
|
"output_cost_per_token_above_272k_tokens": 9e-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://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/models-sold-directly-by-azure",
|
||||||
"supports_reasoning": true
|
"supported_endpoints": [
|
||||||
|
"/v1/chat/completions",
|
||||||
|
"/v1/responses"
|
||||||
|
],
|
||||||
|
"supported_modalities": [
|
||||||
|
"text",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"supported_output_modalities": [
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"supports_computer_use": true,
|
||||||
|
"supports_function_calling": true,
|
||||||
|
"supports_max_reasoning_effort": false,
|
||||||
|
"supports_minimal_reasoning_effort": false,
|
||||||
|
"supports_native_streaming": true,
|
||||||
|
"supports_none_reasoning_effort": false,
|
||||||
|
"supports_parallel_function_calling": true,
|
||||||
|
"supports_pdf_input": true,
|
||||||
|
"supports_prompt_caching": true,
|
||||||
|
"supports_reasoning": true,
|
||||||
|
"supports_response_schema": true,
|
||||||
|
"supports_system_messages": true,
|
||||||
|
"supports_tool_choice": true,
|
||||||
|
"supports_vision": true,
|
||||||
|
"supports_web_search": true,
|
||||||
|
"supports_xhigh_reasoning_effort": true
|
||||||
},
|
},
|
||||||
"azure/eu/gpt-6-luna": {
|
"azure/eu/gpt-6-luna": {
|
||||||
"deprecation_date": "2028-03-11",
|
"deprecation_date": "2028-03-11",
|
||||||
|
|
@ -78374,7 +78405,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_cache_breakpoint": true,
|
"supports_prompt_cache_breakpoint": true,
|
||||||
|
|
@ -78423,7 +78454,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
@ -78471,7 +78502,7 @@
|
||||||
"supports_max_reasoning_effort": true,
|
"supports_max_reasoning_effort": true,
|
||||||
"supports_minimal_reasoning_effort": false,
|
"supports_minimal_reasoning_effort": false,
|
||||||
"supports_native_streaming": true,
|
"supports_native_streaming": true,
|
||||||
"supports_none_reasoning_effort": true,
|
"supports_none_reasoning_effort": false,
|
||||||
"supports_parallel_function_calling": true,
|
"supports_parallel_function_calling": true,
|
||||||
"supports_pdf_input": true,
|
"supports_pdf_input": true,
|
||||||
"supports_prompt_caching": true,
|
"supports_prompt_caching": true,
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,23 @@ class TestTheNormalizedTierIsTheTierSent:
|
||||||
):
|
):
|
||||||
assert _reasoning_effort_sent(model, provider, effort) == expected
|
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(
|
@pytest.mark.parametrize(
|
||||||
"model, provider",
|
"model, provider",
|
||||||
[
|
[
|
||||||
|
|
@ -212,13 +229,16 @@ class TestTheNormalizedTierIsTheTierSent:
|
||||||
("azure_ai/gpt-6-astra", "azure_ai"),
|
("azure_ai/gpt-6-astra", "azure_ai"),
|
||||||
("gpt-6-astra", "azure"),
|
("gpt-6-astra", "azure"),
|
||||||
("us/gpt-6-astra", "azure"),
|
("us/gpt-6-astra", "azure"),
|
||||||
|
("eu/gpt-6-astra", "azure"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_an_azure_hosted_astra_deployment_drops_to_the_tier_it_accepts(
|
def test_an_azure_hosted_astra_deployment_drops_to_the_tier_its_chat_route_accepts(
|
||||||
self, local_model_cost_map, model, provider
|
self, local_model_cost_map: None, model: str, provider: str
|
||||||
):
|
) -> None:
|
||||||
"""The deployment answers ``max`` with a 400 naming ``none`` through ``xhigh``, so the rows
|
"""A Foundry gpt-6-astra answers a Chat Completions ``max`` with a 400 naming low through
|
||||||
say so and the adapter sends the tier below instead of the rejected one."""
|
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"
|
assert _reasoning_effort_sent(model, provider, "max") == "xhigh"
|
||||||
|
|
||||||
def test_the_openai_hosted_twin_still_sends_max(self, local_model_cost_map):
|
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 pytest
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
@ -343,17 +346,22 @@ def test_azure_gpt_6_astra_takes_the_reasoning_series_request_shape():
|
||||||
model="gpt-6-astra",
|
model="gpt-6-astra",
|
||||||
custom_llm_provider="azure",
|
custom_llm_provider="azure",
|
||||||
max_tokens=100,
|
max_tokens=100,
|
||||||
reasoning_effort="max",
|
reasoning_effort="xhigh",
|
||||||
)
|
)
|
||||||
assert params["max_completion_tokens"] == 100
|
assert params["max_completion_tokens"] == 100
|
||||||
assert "max_tokens" not in params
|
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"])
|
@pytest.mark.parametrize("model", ["azure/gpt-6-sol", "azure/us/gpt-6-sol"])
|
||||||
def test_azure_gpt6_astra_reasoning_effort_none_unlocks_temperature(config: AzureOpenAIGPT5Config, model: str):
|
def test_azure_reasoning_effort_none_unlocks_temperature_off_the_azure_row(
|
||||||
"""Foundry's gpt-6-astra accepts reasoning_effort='none' and, only then, a non-default
|
config: AzureOpenAIGPT5Config, monkeypatch: pytest.MonkeyPatch, model: str
|
||||||
temperature (verified live against a Foundry deployment), unlike OpenAI's gpt-6-astra."""
|
):
|
||||||
|
"""A deployment whose azure/ row takes none accepts a non-default temperature with it, read off
|
||||||
|
that row even when OpenAI's twin row says the model refuses none."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost, "gpt-6-sol", {**litellm.model_cost["gpt-6-sol"], "supports_none_reasoning_effort": False}
|
||||||
|
)
|
||||||
params = config.map_openai_params(
|
params = config.map_openai_params(
|
||||||
non_default_params={"temperature": 0.2, "reasoning_effort": "none"},
|
non_default_params={"temperature": 0.2, "reasoning_effort": "none"},
|
||||||
optional_params={},
|
optional_params={},
|
||||||
|
|
@ -365,14 +373,127 @@ def test_azure_gpt6_astra_reasoning_effort_none_unlocks_temperature(config: Azur
|
||||||
assert params["reasoning_effort"] == "none"
|
assert params["reasoning_effort"] == "none"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("model", ["azure/gpt-6-astra", "azure/us/gpt-6-astra"])
|
def _azure_chat_row_forwards(row: Mapping[str, object], level: str) -> bool:
|
||||||
def test_azure_gpt6_astra_rejects_reasoning_effort_minimal(config: AzureOpenAIGPT5Config, model: str):
|
"""Azure chat polarity: max is never gated, xhigh and none need an explicit true, every other
|
||||||
"""Foundry's gpt-6-astra lists none, low, medium, high, xhigh and max but not minimal."""
|
level only has to not be false."""
|
||||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
if level == "max":
|
||||||
config.map_openai_params(
|
return True
|
||||||
non_default_params={"reasoning_effort": "minimal"},
|
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=True,
|
||||||
|
api_version="2025-04-01-preview",
|
||||||
|
)
|
||||||
|
assert ("reasoning_effort" in dropped) is forwarded
|
||||||
|
if forwarded:
|
||||||
|
kept: Final = config.map_openai_params(
|
||||||
|
non_default_params={"reasoning_effort": level},
|
||||||
optional_params={},
|
optional_params={},
|
||||||
model=model,
|
model=model,
|
||||||
drop_params=False,
|
drop_params=False,
|
||||||
api_version="2025-04-01-preview",
|
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", "azure/eu/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
|
||||||
|
|
||||||
|
|
||||||
|
def test_azure_chat_gate_reads_the_foundry_row_when_azure_ai_prefix_survives_provider_remap(
|
||||||
|
config: AzureOpenAIGPT5Config, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
"""A Foundry deployment on an OpenAI-v1 host is re-routed to the azure provider with its
|
||||||
|
azure_ai/ prefix intact, and this config then gates it. The rows are made to disagree on low,
|
||||||
|
a level neither real row flags, so the prefixed name has to read azure_ai/gpt-6-sol while the
|
||||||
|
bare deployment name keeps reading azure/gpt-6-sol."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure_ai/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure_ai/gpt-6-sol"], "supports_low_reasoning_effort": False},
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure/gpt-6-sol"], "supports_low_reasoning_effort": True},
|
||||||
|
)
|
||||||
|
prefixed: Final = config.map_openai_params(
|
||||||
|
non_default_params={"reasoning_effort": "low"},
|
||||||
|
optional_params={},
|
||||||
|
model="azure_ai/gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
api_version="2025-04-01-preview",
|
||||||
|
)
|
||||||
|
assert "reasoning_effort" not in prefixed
|
||||||
|
bare: Final = config.map_openai_params(
|
||||||
|
non_default_params={"reasoning_effort": "low"},
|
||||||
|
optional_params={},
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
api_version="2025-04-01-preview",
|
||||||
|
)
|
||||||
|
assert bare["reasoning_effort"] == "low"
|
||||||
|
|
||||||
|
|
||||||
|
def test_azure_chat_gate_falls_back_to_the_azure_row_for_an_azure_ai_name_the_map_lacks(
|
||||||
|
config: AzureOpenAIGPT5Config, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
monkeypatch.delitem(litellm.model_cost, "azure_ai/gpt-6-sol")
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure/gpt-6-sol"], "supports_low_reasoning_effort": False},
|
||||||
|
)
|
||||||
|
dropped: Final = config.map_openai_params(
|
||||||
|
non_default_params={"reasoning_effort": "low"},
|
||||||
|
optional_params={},
|
||||||
|
model="azure_ai/gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
api_version="2025-04-01-preview",
|
||||||
|
)
|
||||||
|
assert "reasoning_effort" not in dropped
|
||||||
|
|
||||||
|
|
||||||
|
@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,16 +651,21 @@ def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
litellm.add_known_models(model_cost_map=litellm.model_cost)
|
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):
|
def test_azure_responses_reasoning_effort_none_unlocks_temperature_off_the_azure_row(
|
||||||
"""Foundry's gpt-6-astra accepts reasoning.effort='none' with a non-default temperature
|
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
|
||||||
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."""
|
"""The gate reads the azure/ cost-map entry for the bare deployment name rather than OpenAI's
|
||||||
|
twin: with OpenAI's gpt-6-sol row turned off for none, azure/gpt-6-sol's row still lets none
|
||||||
|
through and, only then, a non-default temperature."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost, "gpt-6-sol", {**litellm.model_cost["gpt-6-sol"], "supports_none_reasoning_effort": False}
|
||||||
|
)
|
||||||
params = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
params = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||||
response_api_optional_params=ResponsesAPIOptionalRequestParams(
|
response_api_optional_params=ResponsesAPIOptionalRequestParams(
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
reasoning={"effort": "none"},
|
reasoning={"effort": "none"},
|
||||||
),
|
),
|
||||||
model="gpt-6-astra",
|
model="gpt-6-sol",
|
||||||
drop_params=False,
|
drop_params=False,
|
||||||
)
|
)
|
||||||
assert params["temperature"] == 0.2
|
assert params["temperature"] == 0.2
|
||||||
|
|
@ -679,6 +684,116 @@ 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_prefixed_name_reads_the_foundry_row_not_the_azure_twin(
|
||||||
|
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
"""The rows are made to disagree on low, a level neither real row flags, so the prefixed name
|
||||||
|
proving it reads azure_ai/gpt-6-sol cannot pass by reading azure/gpt-6-sol, which the bare
|
||||||
|
deployment name keeps reading."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure_ai/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure_ai/gpt-6-sol"], "supports_low_reasoning_effort": False},
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure/gpt-6-sol"], "supports_low_reasoning_effort": True},
|
||||||
|
)
|
||||||
|
prefixed = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": "low"}),
|
||||||
|
model="azure_ai/gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
)
|
||||||
|
assert "reasoning" not in prefixed
|
||||||
|
bare = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=ResponsesAPIOptionalRequestParams(reasoning={"effort": "low"}),
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
)
|
||||||
|
assert bare["reasoning"] == {"effort": "low"}
|
||||||
|
|
||||||
|
|
||||||
def test_azure_responses_sends_the_deployment_name_when_azure_ai_prefix_survives_provider_remap():
|
def test_azure_responses_sends_the_deployment_name_when_azure_ai_prefix_survives_provider_remap():
|
||||||
request = AzureOpenAIResponsesAPIConfig().transform_responses_api_request(
|
request = AzureOpenAIResponsesAPIConfig().transform_responses_api_request(
|
||||||
model="azure_ai/gpt-5.4-nano",
|
model="azure_ai/gpt-5.4-nano",
|
||||||
|
|
|
||||||
|
|
@ -146,17 +146,30 @@ def _local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url))
|
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):
|
def test_foundry_gpt_6_sol_keeps_sampling_params_when_reasoning_effort_is_none(_local_model_cost_map):
|
||||||
|
"""gpt-6-sol's Foundry row still says none, so the sampling params that ride on it stay. gpt-6-astra
|
||||||
|
used to be the model here until its row turned none off (a live Foundry deployment answered none
|
||||||
|
with a 400 naming low through xhigh on 2026-10-01); the test below pins that flip."""
|
||||||
optional_params = AzureAIStudioConfig().map_openai_params(
|
optional_params = AzureAIStudioConfig().map_openai_params(
|
||||||
non_default_params={"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9},
|
non_default_params={"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9},
|
||||||
optional_params={},
|
optional_params={},
|
||||||
model="gpt-6-astra",
|
model="gpt-6-sol",
|
||||||
drop_params=False,
|
drop_params=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert optional_params == {"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9}
|
assert optional_params == {"reasoning_effort": "none", "temperature": 0.2, "top_p": 0.9}
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
def test_azure_ai_grok_stop_parameter_handling():
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import json
|
import json
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
import respx
|
import respx
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
||||||
from litellm.llms.azure_ai.responses.transformation import AzureAIResponsesAPIConfig
|
from litellm.llms.azure_ai.responses.transformation import AzureAIResponsesAPIConfig
|
||||||
from litellm.responses.main import _will_bridge_to_chat_completions
|
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.types.router import GenericLiteLLMParams
|
||||||
from litellm.utils import ProviderConfigManager
|
from litellm.utils import ProviderConfigManager
|
||||||
|
|
||||||
|
|
@ -309,3 +312,74 @@ async def test_aresponses_serverless_host_stays_on_chat_bridge():
|
||||||
assert chat_route.called
|
assert chat_route.called
|
||||||
assert not responses_route.called
|
assert not responses_route.called
|
||||||
assert chat_route.calls.last.request.headers["Authorization"] == "Bearer fake-key"
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_foundry_responses_effort_gate_reads_the_azure_ai_row(
|
||||||
|
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
"""A Foundry deployment and its Azure OpenAI namesake are different products, so the Foundry
|
||||||
|
Responses route answers from the azure_ai/ row the Foundry chat route already reads, never from
|
||||||
|
the azure/ twin. The rows are made to disagree on low, a level neither real row flags, so a
|
||||||
|
leak from the wrong row cannot hide behind rows that happen to agree."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure_ai/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure_ai/gpt-6-sol"], "supports_low_reasoning_effort": False},
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure/gpt-6-sol"], "supports_low_reasoning_effort": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
dropped: Final = AzureAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=_responses_params(reasoning={"effort": "low"}),
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
)
|
||||||
|
assert "reasoning" not in dropped
|
||||||
|
|
||||||
|
with pytest.raises(litellm.UnsupportedParamsError):
|
||||||
|
AzureAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=_responses_params(reasoning={"effort": "low"}),
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
kept_by_azure_openai: Final = AzureOpenAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=_responses_params(reasoning={"effort": "low"}),
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=True,
|
||||||
|
)
|
||||||
|
assert kept_by_azure_openai["reasoning"] == {"effort": "low"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_foundry_responses_none_unlocks_temperature_off_the_azure_ai_row(
|
||||||
|
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
"""The sampling-param unlock that rides on none reads the same azure_ai/ row as the effort gate:
|
||||||
|
with the azure/ twin turned off for none, the Foundry row still lets none through and, only
|
||||||
|
then, a non-default temperature."""
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure_ai/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure_ai/gpt-6-sol"], "supports_none_reasoning_effort": True},
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
litellm.model_cost,
|
||||||
|
"azure/gpt-6-sol",
|
||||||
|
{**litellm.model_cost["azure/gpt-6-sol"], "supports_none_reasoning_effort": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
params: Final = AzureAIResponsesAPIConfig().map_openai_params(
|
||||||
|
response_api_optional_params=_responses_params(temperature=0.2, reasoning={"effort": "none"}),
|
||||||
|
model="gpt-6-sol",
|
||||||
|
drop_params=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert params["temperature"] == 0.2
|
||||||
|
assert params["reasoning"] == {"effort": "none"}
|
||||||
|
|
|
||||||
|
|
@ -1858,6 +1858,115 @@ class TestResponsesSurfaceSharesTheEffortRule:
|
||||||
)
|
)
|
||||||
assert ("top_p" in mapped) is top_p_survives
|
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):
|
def test_top_p_raises_without_drop_params(self, local_model_cost_map):
|
||||||
with pytest.raises(litellm.UnsupportedParamsError):
|
with pytest.raises(litellm.UnsupportedParamsError):
|
||||||
OpenAIResponsesAPIConfig().map_openai_params(
|
OpenAIResponsesAPIConfig().map_openai_params(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -431,6 +433,105 @@ def test_gpt5_drops_reasoning_effort_minimal_when_requested(config: OpenAIConfig
|
||||||
assert "reasoning_effort" not in params
|
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):
|
def test_gpt5_minimal_dict_triggers_validation(config: OpenAIConfig):
|
||||||
"""Dict with effort='minimal' triggers minimal model-support validation."""
|
"""Dict with effort='minimal' triggers minimal model-support validation."""
|
||||||
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
with pytest.raises(litellm.utils.UnsupportedParamsError):
|
||||||
|
|
|
||||||
|
|
@ -395,22 +395,25 @@ class TestGpt6AstraAdvertisesItsDocumentedLevels:
|
||||||
[
|
[
|
||||||
("azure/gpt-6-astra", "azure"),
|
("azure/gpt-6-astra", "azure"),
|
||||||
("azure/us/gpt-6-astra", "azure"),
|
("azure/us/gpt-6-astra", "azure"),
|
||||||
|
("azure/eu/gpt-6-astra", "azure"),
|
||||||
("azure_ai/gpt-6-astra", "azure_ai"),
|
("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
|
self, local_model_cost_map, model, custom_llm_provider
|
||||||
):
|
):
|
||||||
"""Microsoft hosts the same model with a different level set than OpenAI does. Verified live
|
"""Verified live on 2026-10-01 against a Foundry gpt-6-astra deployment (model
|
||||||
on both Azure routes: none returns 200 with zero reasoning tokens and unlocks temperature,
|
gpt-6-astra-2026-09-03): none returns 400 unsupported_value on chat and on Responses, naming
|
||||||
which OpenAI's API rejects, while max returns 400 unsupported_value naming none through
|
low, medium, high and xhigh as the levels it takes; Responses also honors max with reasoning
|
||||||
xhigh as the levels it does take."""
|
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
|
from litellm.utils import _get_model_info_helper
|
||||||
|
|
||||||
model_info = dict(_get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider))
|
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) == (
|
assert resolve_supported_reasoning_efforts(model_info, deployment_is_mapped=True) == (
|
||||||
"none",
|
|
||||||
"low",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
|
|
|
||||||
|
|
@ -986,6 +986,50 @@ def test_responses_api_bridge_check_gpt_5_4_tools_with_reasoning_none_stays_chat
|
||||||
assert model_info.get("mode") != "responses"
|
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():
|
def test_responses_api_bridge_check_reasoning_none_with_summary_still_routes_to_responses():
|
||||||
"""A reasoning summary is Responses-only regardless of effort value."""
|
"""A reasoning summary is Responses-only regardless of effort value."""
|
||||||
from litellm.main import responses_api_bridge_check
|
from litellm.main import responses_api_bridge_check
|
||||||
|
|
@ -1362,11 +1406,15 @@ _FOUNDRY_FUNCTION_TOOL: Final = ({"type": "function", "function": {"name": "get_
|
||||||
pytest.param("gpt-6-astra", "https://myresource.openai.azure.com", None, id="gpt-6-azure-openai-host"),
|
pytest.param("gpt-6-astra", "https://myresource.openai.azure.com", None, id="gpt-6-azure-openai-host"),
|
||||||
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, "low", id="gpt-5.6-explicit-effort"),
|
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, "low", id="gpt-5.6-explicit-effort"),
|
||||||
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, {"effort": "high"}, id="gpt-5.6-explicit-effort-dict"),
|
pytest.param("gpt-5.6-sol", _FOUNDRY_API_BASE, {"effort": "high"}, id="gpt-5.6-explicit-effort-dict"),
|
||||||
|
pytest.param("gpt-6-astra", _FOUNDRY_API_BASE, "none", id="gpt-6-row-refused-none"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_responses_api_bridge_check_azure_ai_foundry_rejected_tools_route_to_responses(
|
def test_responses_api_bridge_check_azure_ai_foundry_rejected_tools_route_to_responses(
|
||||||
model_name, api_base, reasoning_effort
|
model_name, api_base, reasoning_effort
|
||||||
):
|
):
|
||||||
|
"""The explicit ``none`` only keeps a Foundry gpt-6 request on chat while the deployment takes
|
||||||
|
it; the ``azure_ai/gpt-6-astra`` row says it does not, so the gate drops it and the call runs at
|
||||||
|
the default effort, under which Foundry rejects function tools from gpt-6 on."""
|
||||||
from litellm.main import responses_api_bridge_check
|
from litellm.main import responses_api_bridge_check
|
||||||
|
|
||||||
model_info, model = responses_api_bridge_check(
|
model_info, model = responses_api_bridge_check(
|
||||||
|
|
@ -1384,7 +1432,6 @@ def test_responses_api_bridge_check_azure_ai_foundry_rejected_tools_route_to_res
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"model_name, api_base, reasoning_effort",
|
"model_name, api_base, reasoning_effort",
|
||||||
[
|
[
|
||||||
pytest.param("gpt-6-astra", _FOUNDRY_API_BASE, "none", id="explicit-none-stays-chat"),
|
|
||||||
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-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.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.5", _FOUNDRY_API_BASE, "high", id="gpt-5.5-explicit-effort-stays-chat"),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue