mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
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:
commit
d83d9645fe
4 changed files with 95 additions and 7 deletions
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue