diff --git a/Autotests/unit/test_llm_budget.py b/Autotests/unit/test_llm_budget.py index 95904e74..2cca1fb1 100644 --- a/Autotests/unit/test_llm_budget.py +++ b/Autotests/unit/test_llm_budget.py @@ -26,12 +26,17 @@ def _exec(name, path, register): return module +# Values the stubbed config module returns for keys the tests control; +# anything else falls back to the caller's default. +CONFIG_STUB = {} + + def _load_modules(): openai_stub = types.ModuleType("openai") openai_stub.OpenAI = object config_stub = types.ModuleType("config") - config_stub.config_get_by_key = lambda key, default=None: default + config_stub.config_get_by_key = lambda key, default=None: CONFIG_STUB.get(key, default) stubs = {"openai": openai_stub, "config": config_stub} saved = {name: sys.modules.get(name) for name in list(stubs) + ["providers", "lib_llm_ext"]} @@ -232,6 +237,46 @@ def test_openrouter_reasoning_body(effort, want): assert create.calls[0]["extra_body"]["reasoning"] == want +@pytest.mark.parametrize("effort, want", [ + ("medium", "medium"), + ("none", "none"), +]) +def test_base_provider_forwards_reasoning_effort(effort, want): + create = FakeCreate(chat_response([tool_call("call_1", "send", '{"content": "hi"}')], "tool_calls", + completion_tokens=40, reasoning_tokens=30)) + make_anthropic(create).chat(make_request(reasoning=effort)) + assert create.calls[0]["extra_body"]["reasoning_effort"] == want + + +def test_base_provider_omits_reasoning_when_unset(): + create = FakeCreate(chat_response([tool_call("call_1", "send", '{"content": "hi"}')], "tool_calls", + completion_tokens=40, reasoning_tokens=30)) + make_anthropic(create).chat(make_request(reasoning=None)) + assert "extra_body" not in create.calls[0] + + +def test_base_provider_omits_reasoning_when_effort_unsupported(): + CONFIG_STUB["supportsReasoningEffort"] = False + try: + create = FakeCreate(chat_response([tool_call("call_1", "send", '{"content": "hi"}')], "tool_calls", + completion_tokens=40, reasoning_tokens=30)) + make_anthropic(create).chat(make_request(reasoning="medium")) + assert "extra_body" not in create.calls[0] + finally: + del CONFIG_STUB["supportsReasoningEffort"] + + +def test_base_provider_sends_reasoning_when_effort_supported(): + CONFIG_STUB["supportsReasoningEffort"] = True + try: + create = FakeCreate(chat_response([tool_call("call_1", "send", '{"content": "hi"}')], "tool_calls", + completion_tokens=40, reasoning_tokens=30)) + make_anthropic(create).chat(make_request(reasoning="medium")) + assert create.calls[0]["extra_body"]["reasoning_effort"] == "medium" + finally: + del CONFIG_STUB["supportsReasoningEffort"] + + def test_usage_is_logged_at_info(caplog): caplog.set_level(logging.INFO) create = FakeCreate(chat_response([tool_call("call_1", "send", '{"content": "hi"}')], "tool_calls", diff --git a/config/config.yaml b/config/config.yaml index 8cbf65da..0cc50b71 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -14,6 +14,10 @@ provider: Anthropic maxOutputToken: 6000 # Reasoning-effort hint passed to the provider reasoningMode: medium +# Set false for models that reject the reasoning_effort request parameter +# (e.g. non-reasoning OpenAI models served through the OpenAIAPI provider): +# the hint is then omitted from provider requests. +supportsReasoningEffort: true # How long idle before the next scheduled wake-up (seconds) wakeupInterval: 600 # Path to the logger configuration file diff --git a/providers/lib_llm_ext.py b/providers/lib_llm_ext.py index bd1b216f..70636c59 100644 --- a/providers/lib_llm_ext.py +++ b/providers/lib_llm_ext.py @@ -338,13 +338,22 @@ def convert_tool(self, tool: LLMTool) -> Dict: } def convert_request(self, request: LLMRequest) -> Dict[str, Any]: - return { + result = { "model": self._model_name, "messages": [self.convert_message(msg) for msg in request.messages], "max_tokens": request.max_tokens, "tools": [self.convert_tool(tool) for tool in request.tools], "tool_choice": "required", } + reasoning = getattr(request, "reasoning_mode", None) + if reasoning and config_get_by_key("supportsReasoningEffort", True): + # OpenAI-compatible servers (llama.cpp, vLLM, Strata, ...) accept + # reasoning_effort; "none" disables thinking. Plain OpenAI chat + # completions endpoints ignore unknown fields. Models that reject + # the parameter (e.g. non-reasoning OpenAI models) need it omitted: + # set supportsReasoningEffort: false for those. + result["extra_body"] = {"reasoning_effort": str(reasoning).lower()} + return result def convert_response(self, raw): response = LLMResponse()