Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions litellm/llms/openai/responses/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

from ..common_utils import OpenAIError

OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS = 16

if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj

Expand Down Expand Up @@ -59,6 +61,19 @@ def _supports_reasoning_effort_none(model: str) -> bool:
key="supports_none_reasoning_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.

OpenAI's Responses API rejects max_output_tokens below 16 for every model
(not gpt-5 specific), so a client like Claude Code that sends a max_tokens=1
warmup probe on model switch would otherwise 400. Values that are None or
already at/above the minimum are returned unchanged.
"""
if isinstance(max_output_tokens, int) and max_output_tokens < OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS:
return OPENAI_RESPONSES_API_MIN_MAX_OUTPUT_TOKENS
return max_output_tokens

def get_supported_openai_params(self, model: str) -> list:
"""
All OpenAI Responses API params are supported
Expand Down Expand Up @@ -92,6 +107,9 @@ def map_openai_params(
"""
params = dict(response_api_optional_params)

if "max_output_tokens" in params:
params["max_output_tokens"] = self._enforce_min_max_output_tokens(params.get("max_output_tokens"))

if self._is_gpt_5_model(model=model):
temperature = params.get("temperature")
if temperature is not None and temperature != 1:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,60 @@ def test_map_openai_params(self):
# The function should return the params unchanged
assert result == test_params

@pytest.mark.parametrize("max_output_tokens", [1, 15])
def test_map_openai_params_clamps_max_output_tokens_below_minimum(self, max_output_tokens):
"""OpenAI's Responses API rejects max_output_tokens < 16.

Claude Code (via the Anthropic Messages -> Responses adapter) sends a
max_tokens=1 warmup probe when running `/model`, which produced:
"Invalid 'max_output_tokens': integer below minimum value.
Expected a value >= 16, but got 1 instead."
Clamp anything below the minimum up to 16 instead of erroring.
"""
result = self.config.map_openai_params(
response_api_optional_params={"max_output_tokens": max_output_tokens},
model=self.model,
drop_params=False,
)

assert result["max_output_tokens"] == 16

def test_map_openai_params_preserves_max_output_tokens_at_or_above_minimum(self):
"""Values already >= 16 must pass through untouched."""
result = self.config.map_openai_params(
response_api_optional_params={"max_output_tokens": 256},
model=self.model,
drop_params=False,
)

assert result["max_output_tokens"] == 256

def test_map_openai_params_leaves_max_output_tokens_absent(self):
"""A request without max_output_tokens must not gain the key."""
result = self.config.map_openai_params(
response_api_optional_params={"input": "hi"},
model=self.model,
drop_params=False,
)

assert "max_output_tokens" not in result

@pytest.mark.parametrize(
"value, expected",
[
(1, 16),
(15, 16),
(16, 16),
(17, 17),
(256, 256),
(None, None),
],
)
def test_enforce_min_max_output_tokens(self, value, expected):
"""Below the minimum clamps to 16; the boundary, larger values, and None
are returned unchanged so no previously-valid request regresses."""
assert self.config._enforce_min_max_output_tokens(value) == expected

def validate_responses_api_request_params(self, params, expected_fields):
"""
Validate that the params dict has the expected structure of ResponsesAPIRequestParams
Expand Down
Loading