Merge pull request #39065 from BerriAI/litellm_fix_openai_alias_reasoning_effort

fix(openai): forward reasoning_effort for unknown model aliases instead of failing closed
This commit is contained in:
Mateo Wang 2026-08-31 22:56:38 -07:00 committed by GitHub
commit d83d9645fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 95 additions and 7 deletions

View file

@ -170,16 +170,20 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
if model != "gpt-3.5-turbo-16k" and model != "gpt-4": # gpt-4 does not support 'response_format'
model_specific_params.append("response_format")
# Normalize model name for responses API (e.g., "responses/gpt-4.1" -> "gpt-4.1")
model_for_check: Final = model.split("responses/", 1)[1] if "responses/" in model else model
if (
model_for_check in litellm.open_ai_chat_completion_models
) or model_for_check in litellm.open_ai_text_completion_models:
if OpenAIGPTConfig.is_openai_catalog_model(model):
model_specific_params.append(
"user"
) # user is not a param supported by all openai-compatible endpoints - e.g. azure ai
return base_params + model_specific_params
@staticmethod
def is_openai_catalog_model(model: str) -> bool:
model_for_check: Final = model.split("responses/", 1)[1] if "responses/" in model else model
return (
model_for_check in litellm.open_ai_chat_completion_models
or model_for_check in litellm.open_ai_text_completion_models
)
def _map_openai_params(
self,
non_default_params: dict,
@ -755,6 +759,14 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
)
class OpenAIUnknownModelConfig(OpenAIGPTConfig):
"""A model the openai provider does not recognize is typically a LiteLLM proxy alias, so
forward reasoning_effort and let the server decide whether it is supported."""
def get_supported_openai_params(self, model: str) -> list: # mutable-ok: inherited contract
return super().get_supported_openai_params(model) + ["reasoning_effort"] # mutable-ok: inherited contract
class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator):
def _map_reasoning_to_reasoning_content(self, choices: list) -> list:
"""

View file

@ -43,6 +43,7 @@ from litellm.utils import (
from ...types.llms.openai import *
from ..base import BaseLLM
from .chat.gpt_5_transformation import OpenAIGPT5Config
from .chat.gpt_transformation import OpenAIGPTConfig, OpenAIUnknownModelConfig
from .chat.o_series_transformation import OpenAIOSeriesConfig
from .common_utils import (
BaseOpenAILLM,
@ -189,7 +190,12 @@ class OpenAIConfig(BaseConfig):
elif litellm.openAIGPTAudioConfig.is_model_gpt_audio_model(model=model):
return litellm.openAIGPTAudioConfig.get_supported_openai_params(model=model)
else:
return litellm.openAIGPTConfig.get_supported_openai_params(model=model)
return self._gpt_config_for_model(model).get_supported_openai_params(model=model)
def _gpt_config_for_model(self, model: str) -> OpenAIGPTConfig:
if type(self) is OpenAIConfig and not OpenAIGPTConfig.is_openai_catalog_model(model):
return OpenAIUnknownModelConfig()
return litellm.openAIGPTConfig
def _map_openai_params(self, non_default_params: dict, optional_params: dict, model: str) -> dict:
supported_openai_params: Final = self.get_supported_openai_params(model)
@ -231,7 +237,7 @@ class OpenAIConfig(BaseConfig):
drop_params=drop_params,
)
return litellm.openAIGPTConfig.map_openai_params(
return self._gpt_config_for_model(model).map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,

View file

@ -8278,10 +8278,17 @@ class ProviderConfigManager:
"""
# Handle OpenAI special cases (O-series and GPT-5 models)
if provider == LlmProviders.OPENAI:
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIGPTConfig,
OpenAIUnknownModelConfig,
)
if litellm.openaiOSeriesConfig.is_model_o_series_model(model=model):
return litellm.openaiOSeriesConfig
if litellm.OpenAIGPT5Config.is_model_gpt_5_model(model=model):
return litellm.OpenAIGPT5Config()
if not OpenAIGPTConfig.is_openai_catalog_model(model):
return OpenAIUnknownModelConfig()
# Handle Azure before the generic map so base_model can be threaded through
if provider == LlmProviders.AZURE:

View file

@ -145,6 +145,69 @@ class TestGetOptionalParamsIntegration:
assert regular_params.get("user") == "my-end-user"
assert responses_params.get("user") == "my-end-user"
def test_reasoning_effort_supported_for_unknown_model_alias(self):
"""An openai/-routed model litellm doesn't recognize is likely a proxy alias:
reasoning_effort must be forwarded so the server decides support."""
from litellm.llms.openai.openai import OpenAIConfig
supported_params = OpenAIConfig().get_supported_openai_params(
"my-claude-alias"
)
assert "reasoning_effort" in supported_params
def test_reasoning_effort_not_supported_for_known_non_reasoning_models(self):
"""Known OpenAI models keep failing closed client-side."""
from litellm.llms.openai.openai import OpenAIConfig
config = OpenAIConfig()
assert "reasoning_effort" not in config.get_supported_openai_params("gpt-4o")
assert "reasoning_effort" not in config.get_supported_openai_params(
"responses/gpt-4.1-mini"
)
def test_reasoning_effort_not_inherited_by_openai_compatible_subclasses(self):
"""Providers subclassing either openai config keep their own reasoning_effort gating
for their models, which are all unknown to the openai catalog."""
from litellm.llms.openai.openai import OpenAIConfig
class InheritingDispatcherConfig(OpenAIConfig):
pass
class InheritingGPTConfig(OpenAIGPTConfig):
pass
assert "reasoning_effort" not in InheritingDispatcherConfig().get_supported_openai_params(
"some-unknown-model"
)
assert "reasoning_effort" not in InheritingGPTConfig().get_supported_openai_params(
"some-unknown-model"
)
def test_reasoning_effort_forwarded_in_optional_params_for_unknown_model_alias(
self,
):
"""Regression test for reasoning_effort raising UnsupportedParamsError
client-side for openai/-prefixed proxy aliases before any HTTP request."""
from litellm.utils import get_optional_params
optional_params = get_optional_params(
model="my-claude-alias",
custom_llm_provider="openai",
reasoning_effort="low",
)
assert optional_params.get("reasoning_effort") == "low"
def test_reasoning_effort_still_rejected_for_known_non_reasoning_model(self):
"""A real OpenAI model that doesn't reason still rejects the param client-side."""
from litellm.utils import get_optional_params
with pytest.raises(litellm.utils.UnsupportedParamsError):
get_optional_params(
model="gpt-4o",
custom_llm_provider="openai",
reasoning_effort="low",
)
class TestOpenAIChatCompletionStreamingHandler:
"""Tests for OpenAIChatCompletionStreamingHandler.chunk_parser()"""