From 79d94553134bee6ccf96a24f6dd5b82459ab0e39 Mon Sep 17 00:00:00 2001 From: seteiro Date: Wed, 22 Jul 2026 10:46:22 +0800 Subject: [PATCH] fix(providers): add Qwen model-level thinking style mapping (#5023) Add _QWEN_THINKING_MODELS to _MODEL_THINKING_STYLES with enable_thinking style. Prevents Qwen 3.5/3.6/3.7 models from exposing raw reasoning content in chat responses. Closes #4934 --- nanobot/providers/openai_compat_provider.py | 11 +++++++++++ tests/providers/test_litellm_kwargs.py | 22 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/nanobot/providers/openai_compat_provider.py b/nanobot/providers/openai_compat_provider.py index 262900e3..c8bad3c8 100644 --- a/nanobot/providers/openai_compat_provider.py +++ b/nanobot/providers/openai_compat_provider.py @@ -99,9 +99,20 @@ _THINKING_STYLE_MAP: dict[str, Any] = { _GATEWAY_REASONING_STYLE_MAP: dict[str, Any] = { "reasoning_effort": lambda effort: {"reasoning": {"effort": effort}}, } +_QWEN_THINKING_MODELS: frozenset[str] = frozenset({ + "qwen3.7-max", + "qwen3.7-plus", + "qwen3.6-max-preview", + "qwen3.6-plus", + "qwen3.6-flash", + "qwen3.5-plus", + "qwen3.5-flash", +}) + _MODEL_THINKING_STYLES: dict[str, str] = { **dict.fromkeys(_KIMI_THINKING_MODELS, "thinking_type"), **dict.fromkeys(_MIMO_THINKING_MODELS, "thinking_type"), + **dict.fromkeys(_QWEN_THINKING_MODELS, "enable_thinking"), } diff --git a/tests/providers/test_litellm_kwargs.py b/tests/providers/test_litellm_kwargs.py index cd5c3770..c62258f2 100644 --- a/tests/providers/test_litellm_kwargs.py +++ b/tests/providers/test_litellm_kwargs.py @@ -1707,6 +1707,28 @@ def test_dashscope_thinking_disabled_for_none_string() -> None: assert "reasoning_effort" not in kw +def test_qwen_thinking_enabled_via_model_level_mapping() -> None: + """Non-DashScope providers (e.g. OpenRouter) must pick up model-level + enable_thinking for Qwen models when reasoning_effort is set.""" + kw = _build_kwargs_for("openrouter", "qwen/qwen3.6-flash", reasoning_effort="medium") + assert kw["extra_body"] == {"enable_thinking": True, "reasoning": {"effort": "medium"}} + + +def test_qwen_thinking_disabled_via_model_level_mapping() -> None: + """reasoning_effort='none' must send enable_thinking: False via model-level + mapping on non-DashScope providers. OpenRouter also emits its own + reasoning.effort alongside the provider-level thinking control.""" + kw = _build_kwargs_for("openrouter", "qwen/qwen3.5-flash", reasoning_effort="none") + assert kw["extra_body"] == {"enable_thinking": False, "reasoning": {"effort": "none"}} + + +def test_qwen_no_extra_body_when_reasoning_effort_omitted() -> None: + """Without reasoning_effort the model-level mapping must not inject extra_body + on its own — the provider default applies.""" + kw = _build_kwargs_for("openrouter", "qwen/qwen3.6-flash", reasoning_effort=None) + assert "extra_body" not in kw + + def test_deepseek_no_backfill_when_reasoning_effort_none_string() -> None: """reasoning_effort='none' must NOT trigger reasoning_content backfill (thinking inactive).""" spec = find_by_name("deepseek")