From a70657148b2d2b12688bdc45f9b816126fa9fa32 Mon Sep 17 00:00:00 2001 From: watchme35 Date: Tue, 20 Oct 2026 16:36:51 -0400 Subject: [PATCH 1/2] fix(providers): forward reasoning_mode in base AIProvider The base AIProvider.convert_request in providers/lib_llm_ext.py ignored request.reasoning_mode, so the OpenAIAPI provider sent no reasoning hint to OpenAI-compatible backends (llama.cpp server, vLLM, Strata, ...). Thinking-capable models then ran at the server's default thinking level and could exhaust the shared output budget, producing finish_reason=length with empty content and the misleading "reasoning exceeded the token limit" reply, which advises lowering a setting that had no effect. Forward the mode as extra_body.reasoning_effort, the parameter these servers accept ("none" disables thinking). Plain OpenAI chat-completions endpoints ignore unknown fields. The OpenAI, OpenRouter and ASIonE providers already map the mode themselves and are unaffected: OpenRouter merges the base extra_body with its own reasoning object, ASIonE replaces it with its thinking budget as before. Adds unit tests for the base provider (forward medium/none, omit when reasoning_mode is unset). Fixes #402 --- Autotests/unit/test_llm_budget.py | 18 ++++++++++++++++++ providers/lib_llm_ext.py | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Autotests/unit/test_llm_budget.py b/Autotests/unit/test_llm_budget.py index 3cd57be41..da4ede30f 100644 --- a/Autotests/unit/test_llm_budget.py +++ b/Autotests/unit/test_llm_budget.py @@ -232,6 +232,24 @@ 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_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/providers/lib_llm_ext.py b/providers/lib_llm_ext.py index bd1b216f7..f41105c69 100644 --- a/providers/lib_llm_ext.py +++ b/providers/lib_llm_ext.py @@ -338,13 +338,20 @@ 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: + # OpenAI-compatible servers (llama.cpp, vLLM, Strata, ...) accept + # reasoning_effort; "none" disables thinking. Plain OpenAI chat + # completions endpoints ignore unknown fields. + result["extra_body"] = {"reasoning_effort": str(reasoning).lower()} + return result def convert_response(self, raw): response = LLMResponse() From 0e37a6b64da4f03f24f5904f88a04b74e163322c Mon Sep 17 00:00:00 2001 From: Claudiu Tatomirescu <90481+watchme35@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:04:50 -0400 Subject: [PATCH 2/2] feat(providers): add supportsReasoningEffort opt-out for reasoning_effort With the default reasoningMode: medium, the base provider sends reasoning_effort to every model, including non-thinking models that reject the parameter (non-reasoning OpenAI models answer 400). Add a supportsReasoningEffort config flag (default true): set to false, the parameter is omitted, keeping the pre-PR request shape for those deployments. OpenRouter and ASIOne are unaffected. Two unit tests cover the omitted and sent cases. --- Autotests/unit/test_llm_budget.py | 29 ++++++++++++++++++++++++++++- config/config.yaml | 4 ++++ providers/lib_llm_ext.py | 6 ++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Autotests/unit/test_llm_budget.py b/Autotests/unit/test_llm_budget.py index 42d5f8f47..2cca1fb10 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"]} @@ -250,6 +255,28 @@ def test_base_provider_omits_reasoning_when_unset(): 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 8cbf65da8..0cc50b716 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 f41105c69..70636c59a 100644 --- a/providers/lib_llm_ext.py +++ b/providers/lib_llm_ext.py @@ -346,10 +346,12 @@ def convert_request(self, request: LLMRequest) -> Dict[str, Any]: "tool_choice": "required", } reasoning = getattr(request, "reasoning_mode", None) - if reasoning: + 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. + # 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