mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix: use model capabilities for OpenAI sampling parameters
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
ddd162439f
commit
95a0b7d7a6
11 changed files with 581 additions and 44 deletions
|
|
@ -8,11 +8,12 @@ Translations handled by LiteLLM:
|
|||
- Other parameters follow base Azure OpenAI Responses API behavior
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
from litellm.utils import supports_reasoning
|
||||
from litellm.utils import has_catalog_entry, supports_reasoning
|
||||
|
||||
from .transformation import AzureOpenAIResponsesAPIConfig
|
||||
|
||||
|
|
@ -39,7 +40,9 @@ class AzureOpenAIOSeriesResponsesAPIConfig(AzureOpenAIResponsesAPIConfig):
|
|||
O-series models don't support temperature parameter in responses API.
|
||||
"""
|
||||
# Get the base Azure supported params
|
||||
base_supported_params: Final = super().get_supported_openai_params(model)
|
||||
base_supported_params: Final[Sequence[str]] = super().get_supported_openai_params(model)
|
||||
if has_catalog_entry(model=model, custom_llm_provider="azure") and self._sampling_params_allowed(model, None):
|
||||
return list(base_supported_params)
|
||||
|
||||
# O-series models don't support temperature parameter in responses API
|
||||
o_series_unsupported_params: Final = ["temperature"]
|
||||
|
|
@ -63,6 +66,13 @@ class AzureOpenAIOSeriesResponsesAPIConfig(AzureOpenAIResponsesAPIConfig):
|
|||
Drops temperature parameter if drop_params is True since O-series models
|
||||
don't support temperature in the responses API.
|
||||
"""
|
||||
if has_catalog_entry(model=model, custom_llm_provider="azure"):
|
||||
return dict(
|
||||
super().map_openai_params_for_deployment(
|
||||
response_api_optional_params, model, drop_params, declared_reasoning_efforts=None
|
||||
)
|
||||
)
|
||||
|
||||
mapped_params: Final = dict(response_api_optional_params)
|
||||
|
||||
# If drop_params is enabled, remove temperature parameter for O-series models
|
||||
|
|
|
|||
|
|
@ -97,6 +97,11 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.AZURE
|
||||
|
||||
def _sampling_params_allowed(self, model: str, effort: str | None) -> bool:
|
||||
return AzureOpenAIGPT5Config.sampling_params_allowed(
|
||||
model, effort, fallback_allowed=not self._is_gpt_5_model(model)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_gpt_5_model(model: str) -> bool:
|
||||
return AzureOpenAIGPT5Config.is_model_gpt_5_model(model)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from litellm.utils import (
|
|||
has_catalog_entry,
|
||||
is_explicitly_disabled_factory,
|
||||
supports_factory,
|
||||
supports_sampling_params,
|
||||
)
|
||||
|
||||
from .gpt_transformation import OpenAIGPTConfig
|
||||
|
|
@ -205,6 +206,14 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
) -> bool:
|
||||
return cls._supports_reasoning_effort_level(model, level)
|
||||
|
||||
@classmethod
|
||||
def sampling_params_allowed(cls, model: str, effort: str | None, *, fallback_allowed: bool) -> bool:
|
||||
lookup: Final = cls._model_map_lookup_name(model)
|
||||
if cls._supports_reasoning_effort_level(model, "none") and cls.effort_resolves_to_none(model, effort):
|
||||
return True
|
||||
flag: Final = supports_sampling_params(lookup)
|
||||
return fallback_allowed if flag is None else flag
|
||||
|
||||
@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
|
||||
|
|
@ -318,7 +327,9 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
|
||||
# gpt-5.1/5.2 support logprobs, top_p, top_logprobs when reasoning_effort="none"
|
||||
if not self._supports_reasoning_effort_level(model, "none"):
|
||||
non_supported_params.extend(["logprobs", "top_p", "top_logprobs"])
|
||||
non_supported_params.extend(["logprobs", "top_logprobs"])
|
||||
if not self.sampling_params_allowed(model, None, fallback_allowed=False):
|
||||
non_supported_params.append("top_p")
|
||||
|
||||
return [param for param in base_gpt_series_params if param not in non_supported_params]
|
||||
|
||||
|
|
@ -398,7 +409,7 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
# gpt-5.1/5.2 support logprobs, top_p, top_logprobs only when reasoning_effort="none"
|
||||
supports_none: Final = self._supports_reasoning_effort_level(model, "none")
|
||||
if supports_none:
|
||||
sampling_params: Final = ["logprobs", "top_logprobs", "top_p"]
|
||||
sampling_params: Final = ["logprobs", "top_logprobs"]
|
||||
has_sampling: Final = any(p in non_default_params for p in sampling_params)
|
||||
if has_sampling and not self.effort_resolves_to_none(model, effective_effort):
|
||||
if litellm.drop_params or drop_params:
|
||||
|
|
@ -415,12 +426,19 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
sampling_allowed: Final = self.sampling_params_allowed(model, effective_effort, fallback_allowed=False)
|
||||
if "top_p" in non_default_params and not sampling_allowed:
|
||||
if litellm.drop_params or drop_params:
|
||||
non_default_params.pop("top_p", None)
|
||||
else:
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=f"{model} only supports top_p when reasoning_effort resolves to 'none'",
|
||||
status_code=400,
|
||||
)
|
||||
if "temperature" in non_default_params:
|
||||
temperature_value: Final[float | None] = non_default_params.pop("temperature")
|
||||
if temperature_value is not None:
|
||||
# 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:
|
||||
if sampling_allowed or temperature_value == 1:
|
||||
optional_params["temperature"] = temperature_value
|
||||
elif litellm.drop_params or drop_params:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from litellm.utils import (
|
|||
supports_system_messages,
|
||||
)
|
||||
|
||||
from .gpt_reasoning_transformation import OpenAIGPT5Config
|
||||
from .gpt_transformation import OpenAIGPTConfig
|
||||
|
||||
|
||||
|
|
@ -60,7 +61,6 @@ class OpenAIOSeriesConfig(OpenAIGPTConfig):
|
|||
all_openai_params: Final = super().get_supported_openai_params(model=model)
|
||||
non_supported_params: Final = [
|
||||
"logprobs",
|
||||
"top_p",
|
||||
"presence_penalty",
|
||||
"frequency_penalty",
|
||||
"top_logprobs",
|
||||
|
|
@ -68,6 +68,9 @@ class OpenAIOSeriesConfig(OpenAIGPTConfig):
|
|||
|
||||
o_series_only_param: Final = ["reasoning_effort"]
|
||||
|
||||
if not OpenAIGPT5Config.sampling_params_allowed(model, None, fallback_allowed=False):
|
||||
non_supported_params.append("top_p")
|
||||
|
||||
all_openai_params.extend(o_series_only_param)
|
||||
|
||||
try:
|
||||
|
|
@ -105,10 +108,22 @@ class OpenAIOSeriesConfig(OpenAIGPTConfig):
|
|||
):
|
||||
if "max_tokens" in non_default_params:
|
||||
optional_params["max_completion_tokens"] = non_default_params.pop("max_tokens")
|
||||
sampling_params: Final = cast( # cast-ok: legacy mapper accepts an untyped parameter bag
|
||||
"dict[str, object]", non_default_params
|
||||
)
|
||||
reasoning_effort: Final = sampling_params.get("reasoning_effort")
|
||||
sampling_allowed: Final = OpenAIGPT5Config.sampling_params_allowed(
|
||||
model, reasoning_effort if isinstance(reasoning_effort, str) else None, fallback_allowed=False
|
||||
)
|
||||
if "top_p" in non_default_params and not sampling_allowed:
|
||||
if litellm.drop_params or drop_params:
|
||||
sampling_params.pop("top_p", None)
|
||||
else:
|
||||
raise litellm.utils.UnsupportedParamsError(message=f"{model} doesn't support top_p", status_code=400)
|
||||
if "temperature" in non_default_params:
|
||||
temperature_value: Final[float | None] = non_default_params.pop("temperature")
|
||||
if temperature_value is not None:
|
||||
if temperature_value == 1:
|
||||
if sampling_allowed or temperature_value == 1:
|
||||
optional_params["temperature"] = temperature_value
|
||||
else:
|
||||
## UNSUPPORTED TOOL CHOICE VALUE
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
|||
from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base
|
||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.llms.openai.chat.gpt_reasoning_transformation import (
|
||||
OpenAIGPT5Config,
|
||||
is_gpt_reasoning_series_name,
|
||||
refused_reasoning_effort_warning,
|
||||
unsupported_reasoning_effort_message,
|
||||
|
|
@ -373,6 +374,13 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
def _model_map_lookup_name(model: str) -> str:
|
||||
return model
|
||||
|
||||
def _sampling_params_allowed(self, model: str, effort: str | None) -> bool:
|
||||
if self.custom_llm_provider not in (LlmProviders.OPENAI, LlmProviders.AZURE):
|
||||
return not self._is_gpt_5_model(model) or (
|
||||
self._supports_reasoning_effort_none(model) and self._effort_resolves_to_none(model, effort)
|
||||
)
|
||||
return OpenAIGPT5Config.sampling_params_allowed(model, effort, fallback_allowed=not self._is_gpt_5_model(model))
|
||||
|
||||
@staticmethod
|
||||
def _supports_reasoning_effort_none(model: str) -> bool:
|
||||
"""Return True if the model supports reasoning.effort='none'."""
|
||||
|
|
@ -494,7 +502,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
) -> Mapping[str, object]:
|
||||
"""No mapping applied since inputs are in OpenAI spec already.
|
||||
|
||||
GPT-5 models have restrictions on temperature and top_p (only temperature=1
|
||||
Reasoning models have restrictions on temperature and top_p (only temperature=1
|
||||
is accepted, and top_p is rejected, unless reasoning.effort resolves to
|
||||
'none' on models that support it).
|
||||
Apply the same validation used by the chat completions path.
|
||||
|
|
@ -553,42 +561,43 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
|
|||
params["reasoning"] = remaining
|
||||
else:
|
||||
params.pop("reasoning", None)
|
||||
forwarded_reasoning: Final = params.get("reasoning")
|
||||
effort: Final = forwarded_reasoning.get("effort") if isinstance(forwarded_reasoning, dict) else None
|
||||
supports_none: Final = self._supports_reasoning_effort_none(model=lookup_name)
|
||||
effort_is_none: Final = supports_none and self._effort_resolves_to_none(lookup_name, effort)
|
||||
forwarded_reasoning: Final = params.get("reasoning")
|
||||
effort: Final = self._requested_effort(forwarded_reasoning)
|
||||
sampling_allowed: Final = self._sampling_params_allowed(
|
||||
lookup_name, effort if isinstance(effort, str) else None
|
||||
)
|
||||
|
||||
temperature: Final = params.get("temperature")
|
||||
if temperature is not None and temperature != 1:
|
||||
if effort_is_none:
|
||||
pass # flexible temperature allowed
|
||||
elif drop_params or litellm.drop_params:
|
||||
params.pop("temperature", None)
|
||||
else:
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message=(
|
||||
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,
|
||||
)
|
||||
temperature: Final = params.get("temperature")
|
||||
if temperature is not None and temperature != 1:
|
||||
if sampling_allowed:
|
||||
pass
|
||||
elif drop_params or litellm.drop_params:
|
||||
params.pop("temperature", None)
|
||||
else:
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message=(
|
||||
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,
|
||||
)
|
||||
|
||||
if "top_p" in params and not effort_is_none:
|
||||
if drop_params or litellm.drop_params:
|
||||
params.pop("top_p", None)
|
||||
else:
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message=(
|
||||
f"{model} only supports top_p when 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,
|
||||
)
|
||||
if "top_p" in params and not sampling_allowed:
|
||||
if drop_params or litellm.drop_params:
|
||||
params.pop("top_p", None)
|
||||
else:
|
||||
raise litellm.UnsupportedParamsError(
|
||||
message=(
|
||||
f"{model} only supports top_p when 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,
|
||||
)
|
||||
|
||||
return params
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3562,6 +3562,11 @@ def supports_audio_input(model: str, custom_llm_provider: str | None = None) ->
|
|||
return supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_audio_input")
|
||||
|
||||
|
||||
def supports_sampling_params(model: str, custom_llm_provider: str | None = None) -> bool | None:
|
||||
info: Final = lookup_catalog_model_info(model, custom_llm_provider)
|
||||
return None if isinstance(info, ModelInfoLookupMiss) else info.get("supports_sampling_params")
|
||||
|
||||
|
||||
def supports_video_input(model: str, custom_llm_provider: str | None = None) -> bool:
|
||||
return supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_video_input")
|
||||
|
||||
|
|
@ -7580,6 +7585,7 @@ def get_model_info_helper(
|
|||
supports_prompt_caching=_model_info.get("supports_prompt_caching", None),
|
||||
supports_prompt_cache_breakpoint=_model_info.get("supports_prompt_cache_breakpoint", None),
|
||||
supports_thinking_cache_preservation=_model_info.get("supports_thinking_cache_preservation", None),
|
||||
supports_sampling_params=_model_info.get("supports_sampling_params", None),
|
||||
supports_audio_input=_model_info.get("supports_audio_input", None),
|
||||
supports_video_input=_model_info.get("supports_video_input", None),
|
||||
supports_box_questions=_model_info.get("supports_box_questions", None),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1814,3 +1814,12 @@ def test_azure_reasoning_replay_drops_status_always_and_content_under_drop_param
|
|||
|
||||
assert request["input"] == [replayable if drop_params else {**replayable, "content": saved_content}]
|
||||
assert history == original
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["o3", "azure/o3", "azure/us/o3"])
|
||||
@pytest.mark.parametrize("param,value", [("temperature", 0.5), ("top_p", 0.9)])
|
||||
def test_azure_o_series_responses_sampling_cost_map(model: str, param: str, value: float) -> None:
|
||||
config: Final = AzureOpenAIOSeriesResponsesAPIConfig()
|
||||
assert config.map_openai_params({param: value}, model, drop_params=True) == {}
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
config.map_openai_params({param: value}, model, drop_params=False)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from openai.types.responses.response_compaction_item import ResponseCompactionIt
|
|||
from pydantic import JsonValue, TypeAdapter
|
||||
|
||||
import litellm
|
||||
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig
|
||||
from litellm.llms.azure_ai.responses.transformation import AzureAIResponsesAPIConfig
|
||||
|
|
@ -5052,3 +5053,40 @@ def test_compact_request_keeps_stream_under_drop_params(config: OpenAIResponsesA
|
|||
)
|
||||
|
||||
assert {key: data[key] for key in data if key != "input"} == {"model": "gpt-5.4-mini", "stream": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param,value", [("temperature", 0.5), ("top_p", 0.9)])
|
||||
@pytest.mark.parametrize("model", ["o3", "openai/o3", "o4-mini", "o1-pro"])
|
||||
def test_o_series_responses_rejects_or_drops_sampling(model: str, param: str, value: float) -> None:
|
||||
config: Final = OpenAIResponsesAPIConfig()
|
||||
assert config.map_openai_params({param: value}, model, drop_params=True) == {}
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
config.map_openai_params({param: value}, model, drop_params=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,reasoning,expected",
|
||||
[
|
||||
("gpt-5.1", {"effort": "none"}, {"temperature": 0.5, "top_p": 0.9, "reasoning": {"effort": "none"}}),
|
||||
("gpt-5.5", {"effort": "none"}, {"temperature": 0.5, "top_p": 0.9, "reasoning": {"effort": "none"}}),
|
||||
("gpt-5.5", {"effort": "medium"}, {"reasoning": {"effort": "medium"}}),
|
||||
("gpt-5-chat-latest", None, {"temperature": 0.5, "top_p": 0.9}),
|
||||
("openai/custom-sampling-deployment", None, {"temperature": 0.5, "top_p": 0.9}),
|
||||
("openai/gpt-5-custom-sampling-deployment", None, {}),
|
||||
],
|
||||
)
|
||||
def test_responses_sampling_effort_and_unknown_fallback(
|
||||
model: str, reasoning: dict[str, str] | None, expected: dict[str, object]
|
||||
) -> None:
|
||||
params: Final = {"temperature": 0.5, "top_p": 0.9, **({"reasoning": reasoning} if reasoning else {})}
|
||||
assert OpenAIResponsesAPIConfig().map_openai_params(params, model, drop_params=True) == expected
|
||||
|
||||
|
||||
def test_responses_empty_sampling_params() -> None:
|
||||
assert OpenAIResponsesAPIConfig().map_openai_params({}, "o3", drop_params=False) == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drop_params", [True, False])
|
||||
def test_hosted_vllm_responses_keeps_sampling_when_effort_is_none(drop_params: bool) -> None:
|
||||
params: Final = {"temperature": 0.5, "top_p": 0.9, "reasoning": {"effort": "none"}}
|
||||
assert HostedVLLMResponsesAPIConfig().map_openai_params(params, "gpt-5.1", drop_params) == params
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ from litellm.llms.openai.chat.o_series_transformation import OpenAIOSeriesConfig
|
|||
import os
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param,value", [("temperature", 0.5), ("top_p", 0.9)])
|
||||
def test_o_series_chat_sampling_cost_map_override(param: str, value: float) -> None:
|
||||
model: Final = "openai/o3-sampling-enabled-test"
|
||||
with patch.dict(litellm.model_cost, {model: {"litellm_provider": "openai", "mode": "chat", "supports_sampling_params": True}}):
|
||||
assert OpenAIOSeriesConfig().map_openai_params({param: value}, {}, model, False) == {param: value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("param,value", [("temperature", 0.5), ("top_p", 0.9)])
|
||||
def test_o_series_chat_sampling_unknown_fallback(param: str, value: float) -> None:
|
||||
config: Final = OpenAIOSeriesConfig()
|
||||
assert config.map_openai_params({param: value}, {}, "o3-unknown-test", True) == {}
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
config.map_openai_params({param: value}, {}, "o3-unknown-test", False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name,expected",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue