From 4ef5db7c91ccfb4b690d811baf7cfad4129ab7ae Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:36:05 -0700 Subject: [PATCH 1/5] fix(responses): drop unsupported reasoning param for openai non-reasoning models --- .../llms/openai/responses/transformation.py | 29 ++++++++++++ .../test_openai_responses_transformation.py | 46 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 2fa44cfc2e3..99ce158c4e2 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -75,6 +75,19 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return OpenAIGPT5Config.effort_resolves_to_none(model, effort) + @staticmethod + def _is_o_series_name(model: str) -> bool: + base: Final = model.split("/")[-1] + return len(base) > 1 and base[0] == "o" and base[1].isdigit() + + def _supports_reasoning_param(self, model: str) -> bool: + if self._is_gpt_5_model(model=model) or self._is_o_series_name(model=model): + return True + base: Final = model.split("/")[-1] + if base not in litellm.open_ai_chat_completion_models: + return True + return litellm.supports_reasoning(model=base, custom_llm_provider=self.custom_llm_provider.value) + @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. @@ -124,6 +137,22 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): if "max_output_tokens" in params: params["max_output_tokens"] = self._enforce_min_max_output_tokens(params.get("max_output_tokens")) + if ( + self.custom_llm_provider == LlmProviders.OPENAI + and params.get("reasoning") is not None + and not self._supports_reasoning_param(model=model) + ): + if drop_params or litellm.drop_params: + params.pop("reasoning", None) + else: + raise litellm.UnsupportedParamsError( + message=( + f"{model} doesn't support the `reasoning` parameter. " + "To drop unsupported params set `litellm.drop_params = True`" + ), + status_code=400, + ) + if self._is_gpt_5_model(model=model): temperature: Final = params.get("temperature") if temperature is not None and temperature != 1: 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 e314b94444b..66d22cf8fb0 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 @@ -1626,3 +1626,49 @@ class TestResponsesSurfaceSharesTheEffortRule: drop_params=True, ) assert ("temperature" in mapped) is temperature_survives + + +class TestReasoningFollowsModelSupport: + """Responses API clients like Codex send `reasoning` on every request, and OpenAI 400s it + on non-reasoning models like gpt-4o. drop_params must strip it there, the same way the + chat completions surface already strips reasoning_effort for those models. + """ + + @pytest.mark.parametrize( + "model, reasoning_survives", + [ + ("gpt-4o", False), + ("gpt-4.1", False), + ("gpt-4o-mini", False), + ("gpt-5.6", True), + ("o3", True), + ("o3-deep-research", True), + ("codex-mini-latest", True), + ("computer-use-preview", True), + ], + ) + def test_drop_params_strips_reasoning_by_model(self, local_model_cost_map, model, reasoning_survives): + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium", "summary": "auto"}}, + model=model, + drop_params=True, + ) + assert ("reasoning" in mapped) is reasoning_survives + + def test_without_drop_params_the_error_is_litellms_400(self, local_model_cost_map, monkeypatch): + monkeypatch.setattr(litellm, "drop_params", False) + with pytest.raises(litellm.UnsupportedParamsError) as excinfo: + OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium"}}, + model="gpt-4o", + drop_params=False, + ) + assert excinfo.value.status_code == 400 + + def test_azure_deployments_keep_reasoning(self, local_model_cost_map): + mapped = AzureOpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium"}}, + model="my-o3-deployment", + drop_params=True, + ) + assert mapped["reasoning"] == {"effort": "medium"} From d9929379003a2b5ea2d6c584fb9c1088a7e6aab7 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 5 Sep 2026 03:17:11 -0700 Subject: [PATCH 2/5] fix(responses): read reasoning support from the cost map instead of model-name rules --- .../llms/openai/responses/transformation.py | 7 ------- ...odel_prices_and_context_window_backup.json | 4 ++++ model_prices_and_context_window.json | 4 ++++ .../test_openai_responses_transformation.py | 2 ++ .../test_litellm/test_model_prices_schema.py | 21 +++++++++++++++++++ 5 files changed, 31 insertions(+), 7 deletions(-) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index cdfbbb5be5c..123e9a1dda4 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -117,14 +117,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return OpenAIGPT5Config.effort_resolves_to_none(model, effort) - @staticmethod - def _is_o_series_name(model: str) -> bool: - base: Final = model.split("/")[-1] - return len(base) > 1 and base[0] == "o" and base[1].isdigit() - def _supports_reasoning_param(self, model: str) -> bool: - if self._is_gpt_5_model(model=model) or self._is_o_series_name(model=model): - return True base: Final = model.split("/")[-1] if base not in litellm.open_ai_chat_completion_models: return True diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 2459ed940e0..99e728a30bc 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -37115,6 +37115,7 @@ "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, @@ -37155,6 +37156,7 @@ "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, @@ -37366,6 +37368,7 @@ "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, @@ -37406,6 +37409,7 @@ "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, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 2459ed940e0..99e728a30bc 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -37115,6 +37115,7 @@ "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, @@ -37155,6 +37156,7 @@ "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, @@ -37366,6 +37368,7 @@ "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, @@ -37406,6 +37409,7 @@ "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, 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 bf382d4d8ce..cc884fd7dc1 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 @@ -2034,9 +2034,11 @@ class TestReasoningFollowsModelSupport: ("gpt-4o", False), ("gpt-4.1", False), ("gpt-4o-mini", False), + ("gpt-5-search-api", False), ("gpt-5.6", True), ("o3", True), ("o3-deep-research", True), + ("o4-mini-deep-research", True), ("codex-mini-latest", True), ("computer-use-preview", True), ], diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 6114d1d8aba..79609032fbd 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -173,3 +173,24 @@ def test_dated_variants_carry_base_alias_service_tier_pricing(prices: dict): "sync the tier keys so service-tier requests against pinned snapshots are not " "billed at standard rates:\n" + "\n".join(drifted) ) + + +def is_openai_o_series(name: str) -> bool: + base = name.split("/")[-1] + return len(base) > 1 and base[0] == "o" and base[1].isdigit() + + +def test_openai_o_series_entries_carry_supports_reasoning(prices: dict): + unflagged = [ + name + for name, entry in prices.items() + if isinstance(entry, dict) + and entry.get("litellm_provider") == "openai" + and is_openai_o_series(name) + and entry.get("supports_reasoning") is not True + ] + assert unflagged == [], ( + "OpenAI o-series models are reasoning models, and the Responses API drops the " + "`reasoning` param for any mapped OpenAI model whose entry lacks supports_reasoning; " + "flag these entries:\n" + "\n".join(unflagged) + ) From f62130a479bf8c95fd5b364ee60cc2be0a57cdff Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:24:36 -0700 Subject: [PATCH 3/5] fix(responses): floor reasoning support on the bundled cost map and resolve fine-tuned ids A live cost map older than this release, or a proxy whose map fetch lags, could strip `reasoning` from a model this release knows accepts it. The bundled map is now the floor: any OpenAI entry it flags as reasoning keeps the param whatever the live map says. Fine-tuned ids with an empty suffix (`ft:gpt-4o-2024-08-06:org::id`) now resolve to their base entry instead of failing open, `chat-latest` carries the flag, and the schema test keeps every codex, deep-research, and chat-latest entry flagged. The none-effort check goes through a public wrapper so the responses config stops importing a private helper. --- .../litellm_core_utils/get_model_cost_map.py | 8 ++- .../llms/openai/responses/transformation.py | 57 ++++++++++++++----- ...odel_prices_and_context_window_backup.json | 1 + litellm/utils.py | 12 +++- model_prices_and_context_window.json | 1 + .../test_openai_responses_transformation.py | 29 +++++++++- .../test_litellm/test_model_prices_schema.py | 29 +++++++--- tests/test_litellm/test_utils.py | 5 ++ 8 files changed, 116 insertions(+), 26 deletions(-) diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 9cba5db8ab7..ba8738c8de0 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -53,12 +53,14 @@ class GetModelCostMap: _backup_model_count: int = -1 # -1 = not yet loaded + @staticmethod + def read_local_model_cost_map_text() -> str: + return files("litellm").joinpath("model_prices_and_context_window_backup.json").read_text(encoding="utf-8") + @staticmethod def load_local_model_cost_map() -> dict: """Load the local backup model cost map bundled with the package.""" - content: Final = json.loads( - files("litellm").joinpath("model_prices_and_context_window_backup.json").read_text(encoding="utf-8") - ) + content: Final = json.loads(GetModelCostMap.read_local_model_cost_map_text()) return content @classmethod diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 123e9a1dda4..189cc7fe956 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -1,15 +1,17 @@ from collections.abc import Mapping, Sequence +from functools import lru_cache from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final, Protocol, cast, get_type_hints import httpx from openai.types.responses import ResponseReasoningItem -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, TypeAdapter, ValidationError from typing_extensions import ReadOnly, TypedDict import litellm from litellm._logging import verbose_logger from litellm.litellm_core_utils.core_helpers import process_response_headers +from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( _safe_convert_created_field, ) @@ -42,6 +44,30 @@ _PROVIDERS_WITH_COMBINATOR_REJECTING_VALIDATOR: Final = frozenset({LlmProviders. _PROVIDERS_VALIDATING_TOOL_CALL_ITEM_IDS: Final = frozenset({LlmProviders.AZURE, LlmProviders.OPENAI}) +class _ReasoningSupportEntry(BaseModel): + litellm_provider: str | None = None + supports_reasoning: bool | None = None + + +_BUNDLED_COST_MAP: Final = TypeAdapter(dict[str, _ReasoningSupportEntry]) + + +@lru_cache(maxsize=1) +def _bundled_openai_reasoning_models() -> frozenset[str]: + """OpenAI models the cost map shipped with this release flags as reasoning models. + + The live map can lag this release (a pinned mirror, or a proxy on newer code than the + map it fetches), and a lagging entry must never strip `reasoning` from a model this + release knows accepts it. + """ + bundled: Final = _BUNDLED_COST_MAP.validate_json(GetModelCostMap.read_local_model_cost_map_text()) + return frozenset( + name + for name, entry in bundled.items() + if entry.litellm_provider == LlmProviders.OPENAI.value and entry.supports_reasoning is True + ) + + class _DeleteResponseBody(TypedDict): """Decoded body of the Responses API delete call.""" @@ -95,13 +121,9 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): @staticmethod def _supports_reasoning_effort_none(model: str) -> bool: """Return True if the model supports reasoning.effort='none'.""" - from litellm.utils import _supports_factory + from litellm.utils import supports_none_reasoning_effort - return _supports_factory( - model=model, - custom_llm_provider=None, - key="supports_none_reasoning_effort", - ) + return supports_none_reasoning_effort(model=model, custom_llm_provider=None) @staticmethod def _effort_resolves_to_none(model: str, effort: str | None) -> bool: @@ -117,11 +139,17 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return OpenAIGPT5Config.effort_resolves_to_none(model, effort) - def _supports_reasoning_param(self, model: str) -> bool: - base: Final = model.split("/")[-1] - if base not in litellm.open_ai_chat_completion_models: + @staticmethod + def _supports_reasoning_param(model: str) -> bool: + from litellm.utils import _get_model_info_helper + + try: + info: Final = _get_model_info_helper( + model=model.split("/")[-1], custom_llm_provider=LlmProviders.OPENAI.value + ) + except Exception: return True - return litellm.supports_reasoning(model=base, custom_llm_provider=self.custom_llm_provider.value) + return info["key"] in _bundled_openai_reasoning_models() or info.get("supports_reasoning") is True @staticmethod def _enforce_min_max_output_tokens(max_output_tokens: "int | None") -> "int | None": @@ -182,7 +210,8 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): else: raise litellm.UnsupportedParamsError( message=( - f"{model} doesn't support the `reasoning` parameter. " + f"{model} doesn't support the `reasoning` parameter " + "(its model cost map entry lacks `supports_reasoning`). " "To drop unsupported params set `litellm.drop_params = True`" ), status_code=400, @@ -500,7 +529,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): processed_headers: Final = process_response_headers(raw_response_headers) try: response = ResponsesAPIResponse.model_validate(raw_response_json) - except Exception: + except ValidationError: verbose_logger.debug( "Error constructing ResponsesAPIResponse: %s, using model_construct", raw_response_json ) @@ -892,7 +921,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): try: response = ResponsesAPIResponse.model_validate(raw_response_json) - except Exception: + except ValidationError: verbose_logger.debug( "Error constructing ResponsesAPIResponse: %s, using model_construct", raw_response_json ) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 99e728a30bc..c97858b4e29 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -30698,6 +30698,7 @@ "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, diff --git a/litellm/utils.py b/litellm/utils.py index 9d20d32d147..07735cc1f87 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2805,6 +2805,13 @@ def supports_reasoning(model: str, custom_llm_provider: str | None = None) -> bo return _supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_reasoning") +def supports_none_reasoning_effort(model: str, custom_llm_provider: str | None = None) -> bool: + """ + Check if the given model accepts reasoning effort "none" and return a boolean value. + """ + return _supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_none_reasoning_effort") + + def supports_native_structured_output(model: str, custom_llm_provider: str | None = None) -> bool: """ Check if the given model supports native structured outputs and return a boolean value. @@ -5210,13 +5217,16 @@ def _strip_openai_finetune_model_name(model_name: str) -> str: input: ft:gpt-3.5-turbo:my-org:custom_suffix:id output: ft:gpt-3.5-turbo + input: ft:gpt-4o-2024-08-06:my-org::id (OpenAI leaves the suffix empty when none was set) + output: ft:gpt-4o-2024-08-06 + Args: model_name (str): The full model name Returns: str: The stripped model name """ - return re.sub(r"(:[^:]+){3}$", "", model_name) + return re.sub(r"(:[^:]*){3}$", "", model_name) def _strip_model_name(model: str, custom_llm_provider: str | None) -> str: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 99e728a30bc..c97858b4e29 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -30698,6 +30698,7 @@ "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, 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 cc884fd7dc1..66271de7d04 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 @@ -2034,12 +2034,14 @@ class TestReasoningFollowsModelSupport: ("gpt-4o", False), ("gpt-4.1", False), ("gpt-4o-mini", False), - ("gpt-5-search-api", False), + ("ft:gpt-4o-2024-08-06:my-org::abc123", False), + ("chat-latest", True), ("gpt-5.6", True), ("o3", True), ("o3-deep-research", True), ("o4-mini-deep-research", True), ("codex-mini-latest", True), + ("ft:o4-mini-2025-04-16:my-org::abc123", True), ("computer-use-preview", True), ], ) @@ -2051,6 +2053,30 @@ class TestReasoningFollowsModelSupport: ) assert ("reasoning" in mapped) is reasoning_survives + @pytest.mark.parametrize( + "model, reasoning_survives", + [ + ("gpt-4o", False), + ("chat-latest", True), + ("o3", True), + ("o3-deep-research", True), + ], + ) + def test_a_cost_map_older_than_this_release_never_strips_a_known_reasoning_model( + self, local_model_cost_map, monkeypatch, model, reasoning_survives + ): + lagging = { + name: {field: value for field, value in entry.items() if field != "supports_reasoning"} + for name, entry in litellm.model_cost.items() + } + monkeypatch.setattr(litellm, "model_cost", lagging) + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium"}}, + model=model, + drop_params=True, + ) + assert ("reasoning" in mapped) is reasoning_survives + def test_without_drop_params_the_error_is_litellms_400(self, local_model_cost_map, monkeypatch): monkeypatch.setattr(litellm, "drop_params", False) with pytest.raises(litellm.UnsupportedParamsError) as excinfo: @@ -2060,6 +2086,7 @@ class TestReasoningFollowsModelSupport: drop_params=False, ) assert excinfo.value.status_code == 400 + assert "cost map" in str(excinfo.value) def test_azure_deployments_keep_reasoning(self, local_model_cost_map): mapped = AzureOpenAIResponsesAPIConfig().map_openai_params( diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 79609032fbd..3f0992275eb 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -8,6 +8,8 @@ from pathlib import Path import jsonschema import pytest +from litellm.llms.openai.chat.gpt_5_transformation import is_gpt_reasoning_series_name + REPO_ROOT = Path(__file__).parents[2] GENERATOR_PATH = REPO_ROOT / "ci_cd" / "generate_model_prices_schema.py" PRICES_PATH = REPO_ROOT / "model_prices_and_context_window.json" @@ -175,22 +177,35 @@ def test_dated_variants_carry_base_alias_service_tier_pricing(prices: dict): ) +OPENAI_REASONING_FAMILY_MARKERS = ("codex", "deep-research", "chat-latest") + + def is_openai_o_series(name: str) -> bool: - base = name.split("/")[-1] - return len(base) > 1 and base[0] == "o" and base[1].isdigit() + return len(name) > 1 and name[0] == "o" and name[1].isdigit() -def test_openai_o_series_entries_carry_supports_reasoning(prices: dict): +def is_openai_reasoning_family(name: str) -> bool: + base = name.split("/")[-1].removeprefix("ft:") + if "search-api" in base: + return False + return ( + is_openai_o_series(base) + or is_gpt_reasoning_series_name(base) + or any(marker in base for marker in OPENAI_REASONING_FAMILY_MARKERS) + ) + + +def test_openai_reasoning_family_entries_carry_supports_reasoning(prices: dict): unflagged = [ name for name, entry in prices.items() if isinstance(entry, dict) and entry.get("litellm_provider") == "openai" - and is_openai_o_series(name) + and is_openai_reasoning_family(name) and entry.get("supports_reasoning") is not True ] assert unflagged == [], ( - "OpenAI o-series models are reasoning models, and the Responses API drops the " - "`reasoning` param for any mapped OpenAI model whose entry lacks supports_reasoning; " - "flag these entries:\n" + "\n".join(unflagged) + "OpenAI o-series, gpt-5+, codex, deep-research, and chat-latest models are reasoning " + "models, and the Responses API drops the `reasoning` param for any mapped OpenAI model " + "whose entry lacks supports_reasoning; flag these entries:\n" + "\n".join(unflagged) ) diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 14907e17b1b..6a90593bea9 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -198,6 +198,11 @@ def test_get_model_info_resolves_provider_prefixed_model_ids(local_model_cost_ma assert via_provider["mode"] == "responses" +def test_get_model_info_strips_openai_finetune_ids_without_a_custom_suffix(local_model_cost_map): + info = litellm.get_model_info(model="ft:gpt-4o-2024-08-06:my-org::abc123", custom_llm_provider="openai") + assert info["key"] == "ft:gpt-4o-2024-08-06" + + def test_provider_prefixed_lookup_never_outranks_an_existing_row(local_model_cost_map): """The provider-prefixed candidate is tried last, after every candidate that already existed, so no model that resolves today can change answer. `perplexity/sonar` From 1975a54b0418d37f89aba149730e7b6b94729629 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:49:00 -0700 Subject: [PATCH 4/5] fix: declare medium as the only reasoning effort chat-latest accepts OpenAI rejects every reasoning.effort on chat-latest except medium. With supports_reasoning set and no declared levels the entry resolved to None, so /model_group/info and the dashboard effort pickers had nothing to narrow the offered levels with --- litellm/model_prices_and_context_window_backup.json | 3 +++ model_prices_and_context_window.json | 3 +++ tests/test_litellm/test_model_prices_schema.py | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index c97858b4e29..1ce5d08ccbc 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -30681,6 +30681,9 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3e-05, + "reasoning_effort_levels": [ + "medium" + ], "source": "https://developers.openai.com/api/docs/models/chat-latest", "supported_endpoints": [ "/v1/chat/completions", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index c97858b4e29..1ce5d08ccbc 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -30681,6 +30681,9 @@ "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3e-05, + "reasoning_effort_levels": [ + "medium" + ], "source": "https://developers.openai.com/api/docs/models/chat-latest", "supported_endpoints": [ "/v1/chat/completions", diff --git a/tests/test_litellm/test_model_prices_schema.py b/tests/test_litellm/test_model_prices_schema.py index 3f0992275eb..c2c22c25998 100644 --- a/tests/test_litellm/test_model_prices_schema.py +++ b/tests/test_litellm/test_model_prices_schema.py @@ -9,6 +9,7 @@ import jsonschema import pytest from litellm.llms.openai.chat.gpt_5_transformation import is_gpt_reasoning_series_name +from litellm.router_utils.reasoning_effort_capability import resolve_supported_reasoning_efforts REPO_ROOT = Path(__file__).parents[2] GENERATOR_PATH = REPO_ROOT / "ci_cd" / "generate_model_prices_schema.py" @@ -209,3 +210,10 @@ def test_openai_reasoning_family_entries_carry_supports_reasoning(prices: dict): "models, and the Responses API drops the `reasoning` param for any mapped OpenAI model " "whose entry lacks supports_reasoning; flag these entries:\n" + "\n".join(unflagged) ) + + +def test_chat_latest_declares_the_one_effort_openai_accepts(prices: dict): + """OpenAI rejects every reasoning.effort on chat-latest except medium, and a reasoning entry + with no declared levels resolves to None, which lets /model_group/info and the dashboard offer + levels the upstream will 400 on.""" + assert resolve_supported_reasoning_efforts(prices["chat-latest"], deployment_is_mapped=True) == ("medium",) From d748cf40b7c989a344fe1f59335c3b93b2285a14 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:53:37 -0700 Subject: [PATCH 5/5] fix(responses): only drop reasoning when it carries an effort and let an explicit map flag win --- .../llms/openai/responses/transformation.py | 16 +++++++-- .../test_openai_responses_transformation.py | 34 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 189cc7fe956..926de3e8854 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -149,7 +149,17 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) except Exception: return True - return info["key"] in _bundled_openai_reasoning_models() or info.get("supports_reasoning") is True + declared: Final = info.get("supports_reasoning") + if declared is not None: + return declared + return info["key"] in _bundled_openai_reasoning_models() + + @staticmethod + def _requests_reasoning_effort(reasoning: object) -> bool: + effort: Final = ( + reasoning.get("effort") if isinstance(reasoning, Mapping) else getattr(reasoning, "effort", None) + ) + return effort is not None @staticmethod def _enforce_min_max_output_tokens(max_output_tokens: "int | None") -> "int | None": @@ -202,7 +212,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): if ( self.custom_llm_provider == LlmProviders.OPENAI - and params.get("reasoning") is not None + and self._requests_reasoning_effort(params.get("reasoning")) and not self._supports_reasoning_param(model=model) ): if drop_params or litellm.drop_params: @@ -210,7 +220,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): else: raise litellm.UnsupportedParamsError( message=( - f"{model} doesn't support the `reasoning` parameter " + f"{model} doesn't support `reasoning.effort` " "(its model cost map entry lacks `supports_reasoning`). " "To drop unsupported params set `litellm.drop_params = True`" ), 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 66271de7d04..c5902b32a06 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 @@ -2086,12 +2086,42 @@ class TestReasoningFollowsModelSupport: drop_params=False, ) assert excinfo.value.status_code == 400 + assert "reasoning.effort" in str(excinfo.value) assert "cost map" in str(excinfo.value) - def test_azure_deployments_keep_reasoning(self, local_model_cost_map): + @pytest.mark.parametrize("drop_params", [True, False]) + @pytest.mark.parametrize( + "reasoning", + [{"summary": "auto"}, {"effort": None, "summary": "auto"}, {}], + ) + def test_reasoning_without_an_effort_passes_through_on_non_reasoning_models( + self, local_model_cost_map, monkeypatch, drop_params, reasoning + ): + monkeypatch.setattr(litellm, "drop_params", drop_params) + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": dict(reasoning)}, + model="gpt-4o", + drop_params=drop_params, + ) + assert mapped["reasoning"] == reasoning + + def test_an_explicit_supports_reasoning_false_beats_the_bundled_floor(self, local_model_cost_map, monkeypatch): + overridden = { + name: ({**entry, "supports_reasoning": False} if name == "o3" else entry) + for name, entry in litellm.model_cost.items() + } + monkeypatch.setattr(litellm, "model_cost", overridden) + mapped = OpenAIResponsesAPIConfig().map_openai_params( + response_api_optional_params={"reasoning": {"effort": "medium"}}, + model="o3", + drop_params=True, + ) + assert "reasoning" not in mapped + + def test_azure_deployments_keep_reasoning_even_on_a_non_reasoning_model_name(self, local_model_cost_map): mapped = AzureOpenAIResponsesAPIConfig().map_openai_params( response_api_optional_params={"reasoning": {"effort": "medium"}}, - model="my-o3-deployment", + model="gpt-4o", drop_params=True, ) assert mapped["reasoning"] == {"effort": "medium"}