From e5c3df2da2de304fd662707f07b0c09f597bb097 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Thu, 27 Aug 2026 16:44:30 -0700 Subject: [PATCH] fix(gpt-5): resolve temperature support from the model's default reasoning effort A gpt-5 model accepts a non-default temperature only while its effective reasoning effort resolves to "none". litellm had no representation of the effort a model applies when the request omits reasoning_effort, so it substituted supports_none_reasoning_effort, which is a different fact. Every model that supports "none" without defaulting to it therefore had temperature forwarded and rejected upstream, and because the carve-out returned before the drop_params branch, drop_params: true could not save it. Declare the fact instead. A new cost-map key, default_reasoning_effort, states the effort the provider applies when the request omits one, and one shared predicate resolves the effective effort from it: an explicit reasoning_effort wins, otherwise the declared default, otherwise the catalogue decides. That last step matters because the cost map is fetched from the published branch at import time, so it can be OLDER than the code reading it. On such a map every model looks undeclared, and reading that as "reasoning is active" would strip temperature from the 39 gpt-5.1/5.2/5.4 entries that accept it, a regression caused by data lag rather than by anything about the model. So an absent declaration is only meaningful once the catalogue carries the key at all; a map that predates the feature keeps the answer litellm gave before it existed, and the conservative answer applies from the moment the data lands. The top_p/logprobs/top_logprobs gate carried the same assumption spelled differently and now shares the predicate, as does the Responses API, which reimplemented the rule and is what the default /v1/messages bridge routes openai models through. Azure normalises its routing names in one resolver that every capability lookup goes through, which replaces its bespoke per-lookup rewrite. Declared on the 37 gpt-5.1/5.2/5.4 entries measured to accept temperature=0 today, so their behaviour is unchanged. The 23 gpt-5.5/5.6 entries that reject it stay undeclared and are fixed once the catalogue carries the key. Resolves LIT-3797 Resolves LIT-5028 --- ci_cd/generate_model_prices_schema.py | 9 ++ .../llms/azure/chat/gpt_5_transformation.py | 20 +-- .../llms/openai/chat/gpt_5_transformation.py | 80 ++++++++++-- .../llms/openai/responses/transformation.py | 24 +++- ...odel_prices_and_context_window_backup.json | 55 ++++++-- litellm/types/utils.py | 1 + litellm/utils.py | 41 ++++++ model_prices_and_context_window.json | 55 ++++++-- model_prices_and_context_window.schema.json | 12 ++ .../chat/test_azure_gpt5_transformation.py | 37 ++++++ .../test_openai_responses_transformation.py | 33 +++++ .../llms/openai/test_gpt5_transformation.py | 122 +++++++++++++++++- tests/test_litellm/test_utils.py | 28 ++++ 13 files changed, 473 insertions(+), 44 deletions(-) diff --git a/ci_cd/generate_model_prices_schema.py b/ci_cd/generate_model_prices_schema.py index 5e1c4b0dcd9..57cc742d5c4 100644 --- a/ci_cd/generate_model_prices_schema.py +++ b/ci_cd/generate_model_prices_schema.py @@ -220,6 +220,15 @@ def string_key_schemas(modes: tuple) -> dict[str, JsonSchema]: "description": "Highest reasoning effort the Bedrock output_config accepts for this model.", "enum": ["low", "medium", "high", "max", "xhigh"], }, + "default_reasoning_effort": { + "type": "string", + "description": ( + "Reasoning effort the provider applies when the request omits reasoning_effort. " + "Gates whether a non-default temperature or the top_p/logprobs sampling params are " + "accepted, which hold only when the effort resolves to 'none'." + ), + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + }, "comment": STRING, "audio_transcription_config": STRING, } diff --git a/litellm/llms/azure/chat/gpt_5_transformation.py b/litellm/llms/azure/chat/gpt_5_transformation.py index d7584083327..6fdd277a04f 100644 --- a/litellm/llms/azure/chat/gpt_5_transformation.py +++ b/litellm/llms/azure/chat/gpt_5_transformation.py @@ -19,19 +19,19 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): GPT5_SERIES_ROUTE = "gpt5_series/" @classmethod - def _supports_reasoning_effort_level(cls, model: str, level: str) -> bool: - """Override to handle gpt5_series/ prefix used for Azure routing. + def _model_map_lookup_name(cls, model: str) -> str: + """Normalise an Azure routing name to its cost-map key. - The parent class calls ``_supports_factory(model, custom_llm_provider=None)`` - which fails to resolve ``gpt5_series/gpt-5.1`` to the correct Azure model - entry. Strip the prefix and prepend ``azure/`` so the lookup finds - ``azure/gpt-5.1`` in model_prices_and_context_window.json. + Neither ``gpt5_series/gpt-5.1`` nor a bare ``gpt-5.1`` is a key in + model_prices_and_context_window.json; ``azure/gpt-5.1`` is. Overriding the shared + resolver rather than one lookup means the supports, explicitly-disabled and + default-effort answers all read the same entry. """ if model.startswith(cls.GPT5_SERIES_ROUTE): - model = "azure/" + model[len(cls.GPT5_SERIES_ROUTE) :] - elif not model.startswith("azure/"): - model = "azure/" + model - return super()._supports_reasoning_effort_level(model, level) + return "azure/" + model[len(cls.GPT5_SERIES_ROUTE) :] + if model.startswith("azure/"): + return model + return "azure/" + model @classmethod def is_model_gpt_5_model(cls, model: str) -> bool: diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 3a65e4a9426..0223be300b0 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -6,11 +6,28 @@ import litellm from litellm.utils import ( _is_explicitly_disabled_factory, _supports_factory, + declared_value_factory, ) from .gpt_transformation import OpenAIGPTConfig +def _catalogue_declares_default_effort() -> bool: + """Whether the loaded cost map carries default_reasoning_effort for ANY entry. + + The map is fetched from the published branch at import time, so it can be OLDER than the + code reading it. On such a map every model looks undeclared, and treating that as "reasoning + is active" would silently strip temperature from the gpt-5.1/5.2/5.4 deployments that accept + it - a regression caused purely by data lag rather than by anything about the model. + + So the absence of the key is only meaningful once the catalogue is known to carry it at all. + A map that has never heard of the key predates the feature, and the honest answer there is + the one litellm gave before it existed. Scanning costs ~80us on the largest published map and + only on the fallback path, which is noise beside the request it precedes. + """ + return any(isinstance(entry, dict) and "default_reasoning_effort" in entry for entry in litellm.model_cost.values()) + + def _normalize_reasoning_effort_for_chat_completion( value: str | dict | None, ) -> str | None: @@ -114,6 +131,17 @@ class OpenAIGPT5Config(OpenAIGPTConfig): except (ValueError, IndexError): return False + @classmethod + def _model_map_lookup_name(cls, model: str) -> str: + """The name this model is looked up by in the cost map. + + Identity here, because an OpenAI model name is already its map key. Azure overrides + it: its routing prefixes are not map keys, so every capability lookup has to + normalise the name the same way, and doing that in ONE place is what keeps the + supports/disabled/default answers from disagreeing about which entry they read. + """ + return model + @classmethod def _supports_reasoning_effort_level(cls, model: str, level: str) -> bool: """Check if the model supports a specific reasoning_effort level. @@ -123,11 +151,40 @@ class OpenAIGPT5Config(OpenAIGPTConfig): Returns False for unknown models (safe fallback). """ return _supports_factory( - model=model, + model=cls._model_map_lookup_name(model), custom_llm_provider=None, key=f"supports_{level}_reasoning_effort", ) + @classmethod + def effort_resolves_to_none(cls, model: str, effective_effort: str | None) -> bool: + """Whether this request's reasoning effort ends up as "none", which is the single + condition under which the provider accepts a non-default temperature or the + top_p/logprobs sampling params. + + An explicit reasoning_effort answers outright. When the request omits it the answer + is the model's DEFAULT effort, which only the map can state: supporting "none" is a + different fact from defaulting to it, and reading the former as the latter is what + forwarded temperature=0 to every gpt-5.5/5.6 deployment. + + An undeclared default resolves to False. The map not saying is not the model + saying no, so the gate takes the conservative branch: a param the provider would + have rejected gets dropped or refused with an actionable error, and a model + released before its map entry declares a default needs no code change to be safe. + """ + if effective_effort is not None: + return effective_effort == "none" + declared: Final = declared_value_factory( + model=cls._model_map_lookup_name(model), + custom_llm_provider=None, + key="default_reasoning_effort", + ) + if declared is not None: + return declared == "none" + if not _catalogue_declares_default_effort(): + return cls._supports_reasoning_effort_level(model, "none") + return False + @classmethod 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. @@ -140,7 +197,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig): Use this for opt-out checks where unknown models should be allowed through. """ return _is_explicitly_disabled_factory( - model=model, + model=cls._model_map_lookup_name(model), custom_llm_provider=None, key=f"supports_{level}_reasoning_effort", ) @@ -260,15 +317,16 @@ class OpenAIGPT5Config(OpenAIGPTConfig): if supports_none: sampling_params: Final = ["logprobs", "top_logprobs", "top_p"] has_sampling: Final = any(p in non_default_params for p in sampling_params) - if has_sampling and effective_effort not in (None, "none"): + if has_sampling and not self.effort_resolves_to_none(model, effective_effort): if litellm.drop_params or drop_params: for p in sampling_params: non_default_params.pop(p, None) else: raise litellm.utils.UnsupportedParamsError( message=( - "gpt-5.1/5.2/5.4 only support logprobs, top_p, top_logprobs when " - f"reasoning_effort='none'. Current reasoning_effort='{effective_effort}'. " + f"{model} only supports logprobs, top_p, top_logprobs when reasoning_effort " + "resolves to 'none', either set explicitly on the request or declared as the " + f"model's default_reasoning_effort. Current reasoning_effort={effective_effort!r}. " "To drop unsupported params set `litellm.drop_params = True`" ), status_code=400, @@ -277,17 +335,19 @@ class OpenAIGPT5Config(OpenAIGPTConfig): if "temperature" in non_default_params: temperature_value: Final[float | None] = non_default_params.pop("temperature") if temperature_value is not None: - # models supporting reasoning_effort="none" also support flexible temperature - if supports_none and (effective_effort == "none" or effective_effort is None) or temperature_value == 1: + # a non-default temperature rides on the effort resolving to "none", not on + # the model merely supporting it + if (supports_none and self.effort_resolves_to_none(model, effective_effort)) or temperature_value == 1: optional_params["temperature"] = temperature_value elif litellm.drop_params or drop_params: pass else: raise litellm.utils.UnsupportedParamsError( message=( - f"gpt-5 models (including gpt-5-codex) don't support temperature={temperature_value}. " - "Only temperature=1 is supported. " - "For gpt-5.1, temperature is supported when reasoning_effort='none' (or not specified, as it defaults to 'none'). " + f"{model} doesn't support temperature={temperature_value} while reasoning is " + "active. Only temperature=1 is supported unless reasoning_effort resolves to " + "'none', either set explicitly on the request or declared as the model's " + "default_reasoning_effort. " "To drop unsupported params set `litellm.drop_params = True`" ), status_code=400, diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index b2a69564908..2fa44cfc2e3 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -61,6 +61,20 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): key="supports_none_reasoning_effort", ) + @staticmethod + def _effort_resolves_to_none(model: str, effort: str | None) -> bool: + """Whether this request's reasoning effort ends up as "none", the one condition + under which a non-default temperature is accepted. + + Delegates to the chat-completions gpt-5 config so both surfaces answer from one + rule: the Responses API reaches the same models over a different wire, and a second + copy of the rule here is what let this surface keep forwarding temperature after the + chat surface stopped. + """ + from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config + + return OpenAIGPT5Config.effort_resolves_to_none(model, effort) + @staticmethod def _enforce_min_max_output_tokens(max_output_tokens: "int | None") -> "int | None": """Raise sub-minimum max_output_tokens up to the OpenAI Responses API minimum. @@ -116,17 +130,17 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): reasoning: Final = params.get("reasoning") or {} effort: Final = reasoning.get("effort") if isinstance(reasoning, dict) else None supports_none: Final = self._supports_reasoning_effort_none(model=model) - if supports_none and (effort == "none" or effort is None): + if supports_none and self._effort_resolves_to_none(model, effort): pass # flexible temperature allowed elif drop_params or litellm.drop_params: params.pop("temperature", None) else: raise litellm.UnsupportedParamsError( message=( - f"gpt-5 models don't support temperature={temperature}. " - "Only temperature=1 is supported. " - "For models like gpt-5.1/5.4, temperature is supported " - "when reasoning.effort='none' (or not specified). " + f"{model} doesn't support temperature={temperature} while reasoning is " + "active. Only temperature=1 is supported unless reasoning.effort resolves " + "to 'none', either set explicitly on the request or declared as the " + "model's default_reasoning_effort. " "To drop unsupported params set `litellm.drop_params = True`" ), status_code=400, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 77572f69b8b..895cc6c884f 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -3409,6 +3409,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -3456,6 +3457,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -3589,6 +3591,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3630,6 +3633,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3671,6 +3675,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3712,6 +3717,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3937,7 +3943,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/eu/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -3972,7 +3979,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/eu/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -4247,7 +4255,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/global/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -4282,7 +4291,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/global/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -5367,6 +5377,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_minimal_reasoning_effort": true }, "azure/gpt-5.1-chat-2025-11-13": { @@ -5404,7 +5415,8 @@ "supports_system_messages": true, "supports_tool_choice": false, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-codex-2025-11-13": { "cache_read_input_token_cost": 1.25e-07, @@ -5833,7 +5845,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -5868,7 +5881,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -6315,6 +6329,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6354,6 +6369,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6393,6 +6409,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6438,6 +6455,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6477,6 +6495,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6516,6 +6535,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -7663,6 +7683,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-mini-2026-03-17": { @@ -7704,6 +7725,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-nano": { @@ -7745,6 +7767,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-nano-2026-03-17": { @@ -7786,6 +7809,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-image-1": { @@ -8856,7 +8880,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/us/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -8891,7 +8916,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/us/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -26292,6 +26318,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26336,6 +26363,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26381,6 +26409,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26426,6 +26455,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -26471,6 +26501,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27295,6 +27326,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27343,6 +27375,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27492,6 +27525,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27543,6 +27577,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27591,6 +27626,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27639,6 +27675,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 95429e899c9..9b2f20c2259 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -165,6 +165,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False): supports_xhigh_reasoning_effort: bool | None supports_max_reasoning_effort: bool | None reasoning_effort_levels: ReadOnly[Sequence[str] | None] + default_reasoning_effort: ReadOnly[Literal["none", "minimal", "low", "medium", "high", "xhigh"] | None] supports_output_config: bool | None supports_image_size: bool | None bedrock_output_config_effort_ceiling: Literal["low", "medium", "high", "max", "xhigh"] | None diff --git a/litellm/utils.py b/litellm/utils.py index 520c40f67c0..37c7098813e 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2596,6 +2596,46 @@ def _supports_factory(model: str, custom_llm_provider: str | None, key: str) -> return False +def declared_value_factory(model: str, custom_llm_provider: str | None, key: str) -> str | None: + """Return a string value the model map declares for *key*, or ``None`` when it says nothing. + + The string-valued sibling of :func:`_supports_factory` and + :func:`_is_explicitly_disabled_factory`, public where those two are not because it is read + from the provider configs rather than from this module, sharing their + ``get_llm_provider`` -> ``_get_model_info_helper`` chain and their unprefixed-twin + fallback (#20885), so a provider-prefixed entry that omits the key still answers + from the bare entry that carries it. + + ``None`` means "the map does not say", never "the map says no" - callers decide what + an unknown declaration implies, and for a capability gate that decision must be the + conservative one. + """ + try: + resolved: Final = litellm.get_llm_provider(model=model, custom_llm_provider=custom_llm_provider) + resolved_model: Final = resolved[0] + resolved_provider: Final = resolved[1] + model_info: Final = _get_model_info_helper(model=resolved_model, custom_llm_provider=resolved_provider) + declared: Final = model_info.get(key) + if isinstance(declared, str): + return declared + bare_model_key: Final = _get_model_cost_key(resolved_model) + bare_entry: Final = litellm.model_cost.get(bare_model_key) if bare_model_key is not None else None + if isinstance(bare_entry, dict): + bare_declared: Final = bare_entry.get(key) + if isinstance(bare_declared, str): + return bare_declared + return None + except Exception as e: # noqa: BLE001 # an unreadable map entry means "not declared", never a failed call + verbose_logger.debug( + "Model not found or error in reading %s. You passed model=%s, custom_llm_provider=%s. Error: %s", + key, + model, + custom_llm_provider, + e, + ) + return None + + def _is_explicitly_disabled_factory(model: str, custom_llm_provider: str | None, key: str) -> bool: """Return True only when the model map explicitly sets *key* to ``False``. @@ -5890,6 +5930,7 @@ def _get_model_info_helper( supports_xhigh_reasoning_effort=_model_info.get("supports_xhigh_reasoning_effort", None), supports_max_reasoning_effort=_model_info.get("supports_max_reasoning_effort", None), reasoning_effort_levels=_model_info.get("reasoning_effort_levels", None), + default_reasoning_effort=_model_info.get("default_reasoning_effort", None), bedrock_output_config_effort_ceiling=_model_info.get("bedrock_output_config_effort_ceiling", None), bedrock_converse_supports_strict_tools=_model_info.get("bedrock_converse_supports_strict_tools", None), supports_computer_use=_model_info.get("supports_computer_use", None), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 77572f69b8b..895cc6c884f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -3409,6 +3409,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -3456,6 +3457,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -3589,6 +3591,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3630,6 +3633,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3671,6 +3675,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3712,6 +3717,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -3937,7 +3943,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/eu/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -3972,7 +3979,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/eu/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -4247,7 +4255,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/global/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -4282,7 +4291,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/global/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -5367,6 +5377,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_minimal_reasoning_effort": true }, "azure/gpt-5.1-chat-2025-11-13": { @@ -5404,7 +5415,8 @@ "supports_system_messages": true, "supports_tool_choice": false, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-codex-2025-11-13": { "cache_read_input_token_cost": 1.25e-07, @@ -5833,7 +5845,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-chat": { "cache_read_input_token_cost": 1.25e-07, @@ -5868,7 +5881,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -6315,6 +6329,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6354,6 +6369,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6393,6 +6409,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6438,6 +6455,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6477,6 +6495,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -6516,6 +6535,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -7663,6 +7683,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-mini-2026-03-17": { @@ -7704,6 +7725,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-nano": { @@ -7745,6 +7767,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-5.4-nano-2026-03-17": { @@ -7786,6 +7809,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true }, "azure/gpt-image-1": { @@ -8856,7 +8880,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/us/gpt-5.1-chat": { "cache_read_input_token_cost": 1.4e-07, @@ -8891,7 +8916,8 @@ "supports_system_messages": true, "supports_tool_choice": true, "supports_vision": true, - "supports_none_reasoning_effort": true + "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none" }, "azure/us/gpt-5.1-codex": { "deprecation_date": "2027-05-15", @@ -26292,6 +26318,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26336,6 +26363,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26381,6 +26409,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": false, "supports_minimal_reasoning_effort": true }, @@ -26426,6 +26455,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -26471,6 +26501,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27295,6 +27326,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27343,6 +27375,7 @@ "supports_tool_choice": true, "supports_vision": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, @@ -27492,6 +27525,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27543,6 +27577,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27591,6 +27626,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, @@ -27639,6 +27675,7 @@ "supports_vision": true, "supports_web_search": true, "supports_none_reasoning_effort": true, + "default_reasoning_effort": "none", "supports_xhigh_reasoning_effort": true, "supports_minimal_reasoning_effort": false }, diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index 6e837354c60..3f6d3b4f910 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -179,6 +179,18 @@ "comment": { "type": "string" }, + "default_reasoning_effort": { + "type": "string", + "description": "Reasoning effort the provider applies when the request omits reasoning_effort. Gates whether a non-default temperature or the top_p/logprobs sampling params are accepted, which hold only when the effort resolves to 'none'.", + "enum": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, "deprecation_date": { "type": "string", "description": "Date the provider deprecates the model, YYYY-MM-DD.", diff --git a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py index 83562331b9a..e06cae97283 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py @@ -1,6 +1,7 @@ import pytest import litellm +from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map from litellm.llms.azure.chat.gpt_5_transformation import AzureOpenAIGPT5Config @@ -9,6 +10,15 @@ def config() -> AzureOpenAIGPT5Config: return AzureOpenAIGPT5Config() +@pytest.fixture(autouse=True) +def use_local_model_cost_map(monkeypatch: pytest.MonkeyPatch): + """Pin the bundled cost map: these gates read model-map capability keys, and the default + import path fetches the published map, which lags a key added in this repo.""" + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr(litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url)) + litellm.add_known_models(model_cost_map=litellm.model_cost) + + def test_azure_gpt5_supports_reasoning_effort(config: AzureOpenAIGPT5Config): assert "reasoning_effort" in config.get_supported_openai_params(model="gpt-5") assert "reasoning_effort" in config.get_supported_openai_params( @@ -299,3 +309,30 @@ def test_azure_gpt5_1_does_not_support_logprobs(config: AzureOpenAIGPT5Config): supported_params = config.get_supported_openai_params(model="gpt-5.1") assert "logprobs" not in supported_params assert "top_logprobs" not in supported_params + + +class TestAzureResolvesTheDeclaredDefaultEffort: + """Azure reaches the same models under names that are not cost-map keys. Every capability + lookup therefore has to normalise the name identically, which is why the normalisation is + one overridden resolver rather than a rewrite inside a single lookup. + """ + + @pytest.mark.parametrize( + "model, temperature_survives", + [ + ("azure/gpt-5.1", True), + ("gpt5_series/gpt-5.1", True), + ("gpt-5.1", True), + ("azure/gpt-5.6-terra", False), + ("gpt5_series/gpt-5.6-terra", False), + ("azure/gpt-5.5", False), + ], + ) + def test_every_azure_name_shape_reads_the_same_entry(self, config, model, temperature_survives): + mapped = config.map_openai_params( + non_default_params={"temperature": 0}, + optional_params={}, + model=model, + drop_params=True, + ) + assert ("temperature" in mapped) is temperature_survives diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index c03c632363d..e314b94444b 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -1593,3 +1593,36 @@ class TestPromptCacheOptionsOnResponsesPath: "text": "hi", "prompt_cache_breakpoint": {"mode": "explicit"}, } + + +class TestResponsesSurfaceSharesTheEffortRule: + """The Responses API reaches the same gpt-5 models over a different wire, and the default + /v1/messages bridge for openai models routes through it. It carried its own copy of the + temperature rule, so fixing chat completions alone left this surface still forwarding + temperature to a model that rejects it. + """ + + @pytest.mark.parametrize( + "model, effort, temperature_survives", + [ + ("gpt-5.1", None, True), + ("gpt-5.4", None, True), + ("gpt-5.5", None, False), + ("gpt-5.6-terra", None, False), + ("gpt-5.6-sol", None, False), + ("gpt-5.6-terra", "none", True), + ("gpt-5.6-terra", "medium", False), + ], + ) + def test_temperature_follows_the_resolved_effort( + self, local_model_cost_map, model, effort, temperature_survives + ): + params = {"temperature": 0} + if effort is not None: + params["reasoning"] = {"effort": effort} + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params=params, + model=model, + drop_params=True, + ) + assert ("temperature" in mapped) is temperature_survives diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index a35b75a6106..9c5bd34d59a 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -1,3 +1,5 @@ +import re + import pytest import litellm @@ -137,7 +139,7 @@ def test_gpt5_codex_temperature_error(config: OpenAIConfig): """Test that GPT-5-Codex raises error for unsupported temperature when drop_params=False.""" with pytest.raises( litellm.utils.UnsupportedParamsError, - match="gpt-5 models \\(including gpt-5-codex\\)", + match=re.escape("gpt-5-codex doesn't support temperature=0.7 while reasoning is active"), ): config.map_openai_params( non_default_params={"temperature": 0.7}, @@ -1385,3 +1387,121 @@ def test_gpt5_drops_xhigh_when_requested(config: OpenAIConfig): drop_params=True, ) assert "reasoning_effort" not in params + + +class TestDefaultReasoningEffortGatesSamplingParams: + """A non-default temperature rides on the effort RESOLVING to "none", which for a request + that omits reasoning_effort is the model's declared default_reasoning_effort - not on the + model merely supporting "none". gpt-5.5 and gpt-5.6 support it and do not default to it, + so reading one fact as the other forwarded temperature=0 and the provider rejected it. + + Every expectation below was measured against the live provider before being pinned here. + """ + + @pytest.mark.parametrize( + "model, effort, temperature_survives", + [ + # declares default_reasoning_effort="none": reasoning is off, sampling is free + ("gpt-5.1", None, True), + ("gpt-5.2", None, True), + ("gpt-5.4", None, True), + ("gpt-5.4-nano", None, True), + # declares no default: reasoning is active, so the provider takes only temperature=1 + ("gpt-5.5", None, False), + ("gpt-5.6", None, False), + ("gpt-5.6-terra", None, False), + ("gpt-5.6-sol", None, False), + # an explicit effort always wins over the declared default, both ways + ("gpt-5.6-terra", "none", True), + ("gpt-5.6-terra", "medium", False), + ("gpt-5.1", "medium", False), + ], + ) + def test_temperature_follows_the_resolved_effort(self, model, effort, temperature_survives): + params = {"temperature": 0} if effort is None else {"temperature": 0, "reasoning_effort": effort} + mapped = OpenAIGPT5Config().map_openai_params( + non_default_params=params, + optional_params={}, + model=model, + drop_params=True, + ) + assert ("temperature" in mapped) is temperature_survives + + @pytest.mark.parametrize("model, top_p_survives", [("gpt-5.1", True), ("gpt-5.6-terra", False)]) + def test_the_same_rule_gates_top_p(self, model, top_p_survives): + """top_p/logprobs are gated by the identical condition, so they were identically wrong.""" + mapped = OpenAIGPT5Config().map_openai_params( + non_default_params={"top_p": 0.5}, + optional_params={}, + model=model, + drop_params=True, + ) + assert ("top_p" in mapped) is top_p_survives + + def test_an_undeclared_model_is_refused_rather_than_forwarded(self): + """Without drop_params the caller gets an actionable 400 naming the remedy, instead of + the provider's own rejection arriving from an upstream it did not address.""" + with pytest.raises(litellm.utils.UnsupportedParamsError, match="default_reasoning_effort"): + OpenAIGPT5Config().map_openai_params( + non_default_params={"temperature": 0}, + optional_params={}, + model="gpt-5.6-terra", + drop_params=False, + ) + + +class TestACatalogueOlderThanTheCodeDoesNotStripTemperature: + """The cost map is fetched from the published branch at import time, so it can be OLDER than + the code reading it. On such a map every model looks undeclared, and reading that as + "reasoning is active" silently stripped temperature from the gpt-5.1/5.2/5.4 deployments that + accept it - a regression caused by data lag rather than by anything about the model. + + Absence of the key only means something once the catalogue is known to carry it at all. + """ + + @staticmethod + def _map_without_the_key(monkeypatch: pytest.MonkeyPatch) -> None: + stripped = { + name: {k: v for k, v in entry.items() if k != "default_reasoning_effort"} + if isinstance(entry, dict) + else entry + for name, entry in litellm.model_cost.items() + } + monkeypatch.setattr(litellm, "model_cost", stripped) + + @pytest.mark.parametrize("model", ["gpt-5.1", "gpt-5.2", "gpt-5.4", "gpt-5.4-nano"]) + def test_a_pre_feature_catalogue_keeps_the_answer_it_gave_before(self, monkeypatch, model): + """These models accept temperature=0, verified against the provider. On a map that predates + the key they must keep it, exactly as they did before this feature existed.""" + self._map_without_the_key(monkeypatch) + + mapped = OpenAIGPT5Config().map_openai_params( + non_default_params={"temperature": 0}, + optional_params={}, + model=model, + drop_params=True, + ) + assert mapped.get("temperature") == 0 + + @pytest.mark.parametrize("model", ["gpt-5.1", "gpt-5.4"]) + def test_the_same_holds_for_the_sampling_params(self, monkeypatch, model): + self._map_without_the_key(monkeypatch) + + mapped = OpenAIGPT5Config().map_openai_params( + non_default_params={"top_p": 0.5}, + optional_params={}, + model=model, + drop_params=True, + ) + assert mapped.get("top_p") == 0.5 + + def test_once_the_catalogue_declares_the_key_the_conservative_answer_returns(self): + """The bundled map DOES carry the key, so an undeclared model there is a real statement + that its default is not none, and temperature is dropped.""" + mapped = OpenAIGPT5Config().map_openai_params( + non_default_params={"temperature": 0}, + optional_params={}, + model="gpt-5.6-terra", + drop_params=True, + ) + assert "temperature" not in mapped diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index e9b2851f717..362e55680a3 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1018,6 +1018,10 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "type": "array", "items": {"type": "string", "enum": ["none", "minimal", "low", "medium", "high", "xhigh", "max"]}, }, + "default_reasoning_effort": { + "type": "string", + "enum": ["none", "minimal", "low", "medium", "high", "xhigh"], + }, "supports_adaptive_thinking": {"type": "boolean"}, "supports_legacy_thinking": {"type": "boolean"}, "thinking_always_on": {"type": "boolean"}, @@ -5639,3 +5643,27 @@ def test_snapshot_exception_for_hook_preserves_suppress_context_flag() -> None: snapshot = _snapshot_exception_for_hook(e) assert snapshot.__suppress_context__ is False assert snapshot.__context__ is e.__context__ + + +class TestDefaultReasoningEffortHydration: + """`get_model_info` is the public shape every other capability key is readable through, so + the declared default has to survive hydration too, not only the raw-map fallback the + request-path gate happens to reach it by. + """ + + @pytest.mark.parametrize( + "model, provider", + [("gpt-5.1", "openai"), ("gpt-5.4", "openai"), ("azure/gpt-5.1", "azure")], + ) + def test_the_declared_default_survives_model_info_hydration(self, local_model_cost_map, model, provider): + from litellm.utils import _get_model_info_helper + + model_info = dict(_get_model_info_helper(model=model, custom_llm_provider=provider)) + assert model_info["default_reasoning_effort"] == "none" + + def test_a_model_that_declares_nothing_hydrates_to_none(self, local_model_cost_map): + """Absent means "the map does not say", which the gate reads as reasoning being active.""" + from litellm.utils import _get_model_info_helper + + model_info = dict(_get_model_info_helper(model="gpt-5.6-terra", custom_llm_provider="openai")) + assert model_info.get("default_reasoning_effort") is None