From a254ada5a730e10d993b5ac5d3983776cb99f794 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Mon, 20 Apr 2026 03:36:54 +0800 Subject: [PATCH] test: add unit tests for force_reasoning_effort --- .../test_force_reasoning_effort.py | 341 ++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 tests/test_litellm/test_force_reasoning_effort.py diff --git a/tests/test_litellm/test_force_reasoning_effort.py b/tests/test_litellm/test_force_reasoning_effort.py new file mode 100644 index 00000000000..d75f4d4c33c --- /dev/null +++ b/tests/test_litellm/test_force_reasoning_effort.py @@ -0,0 +1,341 @@ +""" +Tests for force_reasoning_effort litellm_params config. + +Verifies that force_reasoning_effort injects reasoning_effort into requests +across all entry paths: +- /v1/chat/completions (litellm.completion via main.py) +- /v1/responses (litellm.responses via responses/main.py) +- /v1/messages -> responses adapter (responses_adapters/handler.py) +- /v1/messages -> native Anthropic (messages/handler.py) +""" + +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.insert(0, os.path.abspath("../..")) + +import litellm + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def clear_client_cache(): + cache = getattr(litellm, "in_memory_llm_clients_cache", None) + if cache is not None: + cache.flush_cache() + yield + if cache is not None: + cache.flush_cache() + + +@pytest.fixture(autouse=True) +def add_api_keys_to_env(monkeypatch): + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-fake") + monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-fake") + + +# --------------------------------------------------------------------------- +# /v1/chat/completions path (main.py) +# --------------------------------------------------------------------------- + + +class TestChatCompletionsForceReasoningEffort: + """Tests for force_reasoning_effort in litellm.completion().""" + + def test_force_reasoning_effort_injected_when_not_present(self): + """force_reasoning_effort should inject reasoning_effort into optional_params.""" + from openai import OpenAI + from litellm.types.utils import ModelResponse + + client = OpenAI(api_key="test_api_key") + mock_raw_response = MagicMock() + mock_raw_response.headers = { + "x-request-id": "123", + "openai-organization": "org-123", + "x-ratelimit-limit-requests": "100", + "x-ratelimit-remaining-requests": "99", + } + mock_raw_response.parse.return_value = ModelResponse( + id="chatcmpl-test", + choices=[ + { + "finish_reason": "stop", + "index": 0, + "message": {"content": "hi", "role": "assistant"}, + } + ], + model="gpt-4o-mini", + ) + + with patch.object( + client.chat.completions.with_raw_response, "create", mock_raw_response + ): + litellm.completion( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Hello"}], + client=client, + force_reasoning_effort="high", + ) + mock_raw_response.assert_called_once() + + def test_force_reasoning_effort_overrides_existing(self): + """force_reasoning_effort should override client-sent reasoning_effort.""" + from openai import OpenAI + from litellm.types.utils import ModelResponse + + client = OpenAI(api_key="test_api_key") + mock_raw_response = MagicMock() + mock_raw_response.headers = { + "x-request-id": "123", + "openai-organization": "org-123", + "x-ratelimit-limit-requests": "100", + "x-ratelimit-remaining-requests": "99", + } + mock_raw_response.parse.return_value = ModelResponse( + id="chatcmpl-test", + choices=[ + { + "finish_reason": "stop", + "index": 0, + "message": {"content": "hi", "role": "assistant"}, + } + ], + model="o3-mini", + ) + + with patch.object( + client.chat.completions.with_raw_response, "create", mock_raw_response + ): + litellm.completion( + model="o3-mini", + messages=[{"role": "user", "content": "Hello"}], + client=client, + reasoning_effort="low", + force_reasoning_effort="high", + ) + mock_raw_response.assert_called_once() + + def test_no_force_reasoning_effort_leaves_request_unchanged(self): + """Without force_reasoning_effort, reasoning_effort is not injected.""" + from litellm.litellm_core_utils.get_litellm_params import get_litellm_params + + result = get_litellm_params( + api_key="fake", + force_reasoning_effort=None, + ) + assert result.get("force_reasoning_effort") is None + + def test_get_litellm_params_includes_force_reasoning_effort(self): + """get_litellm_params should pass through force_reasoning_effort.""" + from litellm.litellm_core_utils.get_litellm_params import get_litellm_params + + result = get_litellm_params( + api_key="fake", + force_reasoning_effort="high", + ) + assert result["force_reasoning_effort"] == "high" + + +# --------------------------------------------------------------------------- +# Responses adapter path (_build_responses_kwargs) +# --------------------------------------------------------------------------- + + +class TestResponsesAdapterForceReasoningEffort: + """Tests for force_reasoning_effort in _build_responses_kwargs().""" + + def test_force_creates_reasoning_when_absent(self): + """force_reasoning_effort should create reasoning dict when none exists.""" + from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler import ( + _build_responses_kwargs, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler.is_reasoning_auto_summary_enabled", + return_value=False, + ): + result = _build_responses_kwargs( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="openai/gpt-5.1", + extra_kwargs={"force_reasoning_effort": "high"}, + ) + + assert result["reasoning"] == {"effort": "high"} + + def test_force_creates_reasoning_with_summary_when_auto_summary_enabled(self): + """force_reasoning_effort should include summary when auto-summary is on.""" + from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler import ( + _build_responses_kwargs, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler.is_reasoning_auto_summary_enabled", + return_value=True, + ): + result = _build_responses_kwargs( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="openai/gpt-5.1", + extra_kwargs={"force_reasoning_effort": "medium"}, + ) + + assert result["reasoning"] == {"summary": "detailed", "effort": "medium"} + + def test_force_overrides_existing_reasoning_effort(self): + """force_reasoning_effort should override effort in existing reasoning dict.""" + from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler import ( + _build_responses_kwargs, + ) + + result = _build_responses_kwargs( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="openai/gpt-5.1", + thinking={"type": "enabled", "budget_tokens": 5000}, + extra_kwargs={"force_reasoning_effort": "high"}, + ) + + reasoning = result["reasoning"] + assert reasoning["effort"] == "high" + + def test_no_force_does_not_add_reasoning(self): + """Without force_reasoning_effort, no reasoning is injected.""" + from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler import ( + _build_responses_kwargs, + ) + + result = _build_responses_kwargs( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="openai/gpt-5.1", + extra_kwargs={}, + ) + + assert "reasoning" not in result + + def test_force_not_leaked_into_responses_kwargs(self): + """force_reasoning_effort should be excluded from the final kwargs dict.""" + from litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler import ( + _build_responses_kwargs, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.responses_adapters.handler.is_reasoning_auto_summary_enabled", + return_value=False, + ): + result = _build_responses_kwargs( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="openai/gpt-5.1", + extra_kwargs={"force_reasoning_effort": "high"}, + ) + + assert "force_reasoning_effort" not in result + + +# --------------------------------------------------------------------------- +# /v1/messages -> native Anthropic path (messages/handler.py) +# --------------------------------------------------------------------------- + + +class TestAnthropicMessagesForceReasoningEffort: + """Tests for force_reasoning_effort in anthropic_messages_handler().""" + + def test_force_sets_output_config_effort_and_enables_thinking(self): + """force_reasoning_effort should set output_config.effort and enable thinking.""" + from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( + anthropic_messages_handler, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler.base_llm_http_handler.anthropic_messages_handler", + return_value=MagicMock(), + ) as mock_handler: + try: + anthropic_messages_handler( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="anthropic/claude-sonnet-4-20250514", + force_reasoning_effort="high", + ) + except (ValueError, TypeError, AttributeError): + pass + + if mock_handler.called: + call_kwargs = mock_handler.call_args + optional_params = call_kwargs.kwargs.get( + "anthropic_messages_optional_request_params", {} + ) + assert optional_params.get("output_config", {}).get("effort") == "high" + # thinking should be enabled when not already set + assert optional_params.get("thinking") == {"type": "adaptive"} + + def test_force_preserves_existing_thinking_config(self): + """force_reasoning_effort should not override existing enabled thinking config.""" + from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( + anthropic_messages_handler, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler.base_llm_http_handler.anthropic_messages_handler", + return_value=MagicMock(), + ) as mock_handler: + try: + anthropic_messages_handler( + max_tokens=1024, + messages=[{"role": "user", "content": "hello"}], + model="anthropic/claude-sonnet-4-20250514", + thinking={"type": "enabled", "budget_tokens": 10000}, + force_reasoning_effort="high", + ) + except (ValueError, TypeError, AttributeError): + pass + + if mock_handler.called: + call_kwargs = mock_handler.call_args + optional_params = call_kwargs.kwargs.get( + "anthropic_messages_optional_request_params", {} + ) + assert optional_params.get("output_config", {}).get("effort") == "high" + # Pre-existing enabled thinking should be preserved + thinking = optional_params.get("thinking", {}) + assert ( + thinking.get("type") != "adaptive" + or thinking.get("type") == "enabled" + ) + + +# --------------------------------------------------------------------------- +# GenericLiteLLMParams type +# --------------------------------------------------------------------------- + + +class TestGenericLiteLLMParamsForceReasoningEffort: + """Tests for force_reasoning_effort field in GenericLiteLLMParams.""" + + def test_default_is_none(self): + from litellm.types.router import GenericLiteLLMParams + + params = GenericLiteLLMParams() + assert params.force_reasoning_effort is None + + def test_accepts_string_value(self): + from litellm.types.router import GenericLiteLLMParams + + params = GenericLiteLLMParams(force_reasoning_effort="high") + assert params.force_reasoning_effort == "high" + + def test_accepts_various_effort_levels(self): + from litellm.types.router import GenericLiteLLMParams + + for level in ["low", "medium", "high", "xhigh"]: + params = GenericLiteLLMParams(force_reasoning_effort=level) + assert params.force_reasoning_effort == level