diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 947dc307a..bc5fa4606 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,8 @@ jobs: enable-cache: true - name: Install dependencies run: make sync + - name: Verify formatting + run: make format-check - name: Run lint run: make lint diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 6271a1600..7d02e2194 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -326,6 +326,23 @@ async def _fetch_response( ) reasoning_effort = model_settings.reasoning.effort if model_settings.reasoning else None + # Enable developers to pass non-OpenAI compatible reasoning_effort data like "none" + # Priority order: + # 1. model_settings.reasoning.effort + # 2. model_settings.extra_body["reasoning_effort"] + # 3. model_settings.extra_args["reasoning_effort"] + if ( + reasoning_effort is None # Unset in model_settings + and isinstance(model_settings.extra_body, dict) + and "reasoning_effort" in model_settings.extra_body + ): + reasoning_effort = model_settings.extra_body["reasoning_effort"] + if ( + reasoning_effort is None # Unset in both model_settings and model_settings.extra_body + and model_settings.extra_args + and "reasoning_effort" in model_settings.extra_args + ): + reasoning_effort = model_settings.extra_args["reasoning_effort"] stream_options = None if stream and model_settings.include_usage is not None: @@ -343,6 +360,9 @@ async def _fetch_response( if model_settings.extra_args: extra_kwargs.update(model_settings.extra_args) + # Prevent duplicate reasoning_effort kwargs when it was promoted to a top-level argument. + extra_kwargs.pop("reasoning_effort", None) + ret = await litellm.acompletion( model=self.model, messages=converted_messages, diff --git a/tests/models/test_kwargs_functionality.py b/tests/models/test_kwargs_functionality.py index 941fdc68d..31c166ecc 100644 --- a/tests/models/test_kwargs_functionality.py +++ b/tests/models/test_kwargs_functionality.py @@ -176,3 +176,41 @@ async def fake_acompletion(model, messages=None, **kwargs): # Should work without error and include regular parameters assert captured["temperature"] == 0.3 + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_reasoning_effort_falls_back_to_extra_args(monkeypatch): + """ + Ensure reasoning_effort from extra_args is promoted when reasoning settings are missing. + """ + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="test response") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + + # GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764. + settings = ModelSettings( + extra_args={"reasoning_effort": "none", "custom_param": "custom_value"} + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input="test input", + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert captured["reasoning_effort"] == "none" + assert captured["custom_param"] == "custom_value" + assert settings.extra_args == {"reasoning_effort": "none", "custom_param": "custom_value"} diff --git a/tests/models/test_litellm_extra_body.py b/tests/models/test_litellm_extra_body.py index 3c83b0607..c33e09da6 100644 --- a/tests/models/test_litellm_extra_body.py +++ b/tests/models/test_litellm_extra_body.py @@ -42,3 +42,116 @@ async def fake_acompletion(model, messages=None, **kwargs): ) assert {"cached_content": "some_cache", "foo": 123}.items() <= captured.items() + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_body_reasoning_effort_is_promoted(monkeypatch): + """ + Ensure reasoning_effort from extra_body is promoted to the top-level parameter. + """ + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="ok") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + # GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764. + settings = ModelSettings( + extra_body={"reasoning_effort": "none", "cached_content": "some_cache"} + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert captured["reasoning_effort"] == "none" + assert captured["cached_content"] == "some_cache" + assert settings.extra_body == {"reasoning_effort": "none", "cached_content": "some_cache"} + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_reasoning_effort_prefers_model_settings(monkeypatch): + """ + Verify explicit ModelSettings.reasoning takes precedence over extra_body entries. + """ + from openai.types.shared import Reasoning + + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="ok") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + settings = ModelSettings( + reasoning=Reasoning(effort="low"), + extra_body={"reasoning_effort": "high"}, + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert captured["reasoning_effort"] == "low" + assert settings.extra_body == {"reasoning_effort": "high"} + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +async def test_extra_body_reasoning_effort_overrides_extra_args(monkeypatch): + """ + Ensure extra_body reasoning_effort wins over extra_args when both are provided. + """ + captured: dict[str, object] = {} + + async def fake_acompletion(model, messages=None, **kwargs): + captured.update(kwargs) + msg = Message(role="assistant", content="ok") + choice = Choices(index=0, message=msg) + return ModelResponse(choices=[choice], usage=Usage(0, 0, 0)) + + monkeypatch.setattr(litellm, "acompletion", fake_acompletion) + # GitHub issue context: https://github.com/openai/openai-agents-python/issues/1764. + settings = ModelSettings( + extra_body={"reasoning_effort": "none"}, + extra_args={"reasoning_effort": "low", "custom_param": "custom"}, + ) + model = LitellmModel(model="test-model") + + await model.get_response( + system_instructions=None, + input=[], + model_settings=settings, + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + ) + + assert captured["reasoning_effort"] == "none" + assert captured["custom_param"] == "custom" + assert settings.extra_args == {"reasoning_effort": "low", "custom_param": "custom"}