diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index ee44333a6..5b766edf6 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -93,6 +93,14 @@ def _model_slug(model_name: str) -> str: return model_name.lower().rsplit("/", 1)[-1] +def _requires_max_completion_tokens(model_name: str) -> bool: + """Return True for models that reject ``max_tokens`` (GPT-5 family, o3/o4).""" + slug = _model_slug(model_name) + return "gpt-5" in slug or any( + slug == p or slug.startswith((p + "-", p + ".")) for p in ("o3", "o4") + ) + + def _model_thinking_style(model_name: str) -> str: return _MODEL_THINKING_STYLES.get(_model_slug(model_name), "") @@ -630,7 +638,9 @@ class OpenAICompatProvider(LLMProvider): if self._supports_temperature(model_name, reasoning_effort): kwargs["temperature"] = temperature - if spec and getattr(spec, "supports_max_completion_tokens", False): + if ( + spec and getattr(spec, "supports_max_completion_tokens", False) + ) or _requires_max_completion_tokens(model_name): kwargs["max_completion_tokens"] = max(1, max_tokens) else: kwargs["max_tokens"] = max(1, max_tokens) diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index 0a1b85f70..81e5f5d0a 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -929,6 +929,43 @@ def test_openai_compat_build_kwargs_uses_gpt5_safe_parameters() -> None: assert "temperature" not in kwargs +@pytest.mark.parametrize( + ("model_name", "expected_key"), + [ + ("gpt-5.4", "max_completion_tokens"), + ("o3-mini", "max_completion_tokens"), + ("gpt-4", "max_tokens"), + ], +) +def test_openai_compat_build_kwargs_max_completion_tokens_by_model_name( + model_name: str, + expected_key: str, +) -> None: + spec = find_by_name("custom") + with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): + provider = OpenAICompatProvider( + api_key="sk-test-key", + default_model=model_name, + spec=spec, + ) + + kwargs = provider._build_kwargs( + messages=[{"role": "user", "content": "hello"}], + tools=None, + model=model_name, + max_tokens=2048, + temperature=0.7, + reasoning_effort=None, + tool_choice=None, + ) + + other_key = ( + "max_tokens" if expected_key == "max_completion_tokens" else "max_completion_tokens" + ) + assert kwargs[expected_key] == 2048 + assert other_key not in kwargs + + def test_openai_compat_preserves_message_level_reasoning_fields() -> None: with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): provider = OpenAICompatProvider()