From 041e311f1ee05c3138f4922c5a2260f23f0e803b Mon Sep 17 00:00:00 2001 From: happy5318 Date: Sat, 25 Jul 2026 02:40:13 +0800 Subject: [PATCH 1/3] fix: Qwen reasoning heuristic fails for prefixed model IDs The Qwen branch in _candidate_supports_reasoning() checks "qwen" in token_set, but model IDs like "al-qwen3.8-max-preview" (New-API alias prefixes) normalize to tokens ["al", "qwen3", "8", ...] where "qwen" is not a standalone token. This causes the reasoning effort toggle to disappear for any prefixed Qwen model. Switch to token-prefix matching (token.startswith("qwen")), consistent with the position-independent approach already used for DeepSeek (tokens.index). This handles arbitrary vendor/alias prefixes while still correctly excluding Qwen 2.x via the existing version gate. --- api/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/config.py b/api/config.py index 17f7bc65..9f5d8451 100644 --- a/api/config.py +++ b/api/config.py @@ -3316,8 +3316,11 @@ def _candidate_supports_reasoning(candidate: str) -> bool: if major >= 4 or (major == 3 and minor >= 7): return True return False - if "qwen" in token_set or normalized.startswith("qwen"): + if any(token.startswith("qwen") for token in tokens) or normalized.startswith("qwen"): # Restrict to Qwen 3+ (exclude Qwen 2/2.5) + # Use token-prefix matching (position-independent) so prefixed ids like + # "al-qwen3-8-max-preview" (tokens: ["al","qwen3","8",...]) still match, + # even though "qwen" is not a standalone token. match = re.search(r"qwen.*?(\d+)(?:\D+(\d+))?", normalized) if match: major = int(match.group(1)) From 90250adfb77242f4a8c539a2d8628f7c9064eb43 Mon Sep 17 00:00:00 2001 From: happy5318 Date: Sat, 25 Jul 2026 02:45:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20remov?= =?UTF-8?q?e=20redundant=20guard,=20add=20regression=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the now-subsumed `normalized.startswith("qwen")` guard (token-prefix matching already covers the bare case) - Add test_qwen_prefixed_alias_reasoning_detection covering: prefixed Qwen 3+ (al-/sn-), bare Qwen 3+, and Qwen 2.x exclusion --- api/config.py | 7 ++-- ...est_reasoning_effort_model_capabilities.py | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/api/config.py b/api/config.py index 9f5d8451..447ebf62 100644 --- a/api/config.py +++ b/api/config.py @@ -3316,11 +3316,12 @@ def _candidate_supports_reasoning(candidate: str) -> bool: if major >= 4 or (major == 3 and minor >= 7): return True return False - if any(token.startswith("qwen") for token in tokens) or normalized.startswith("qwen"): + if any(token.startswith("qwen") for token in tokens): # Restrict to Qwen 3+ (exclude Qwen 2/2.5) - # Use token-prefix matching (position-independent) so prefixed ids like + # Token-prefix matching (position-independent) so prefixed ids like # "al-qwen3-8-max-preview" (tokens: ["al","qwen3","8",...]) still match, - # even though "qwen" is not a standalone token. + # even though "qwen" is not a standalone token. The bare + # normalized.startswith("qwen") case is subsumed by this check. match = re.search(r"qwen.*?(\d+)(?:\D+(\d+))?", normalized) if match: major = int(match.group(1)) diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 6ba82ebc..798d7114 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -514,3 +514,40 @@ def test_datestamped_claude3_not_reasoning_capable_heuristic(): f"{model} must remain reasoning-capable" ) + +def test_qwen_prefixed_alias_reasoning_detection(): + """Prefixed Qwen IDs (e.g. New-API aliases) must still be detected. + + Regression: "al-qwen3.8-max-preview" normalizes to tokens + ["al", "qwen3", "8", ...] where "qwen" is NOT a standalone token, + so the old exact-membership check silently failed. + """ + # Prefixed Qwen 3+ → reasoning-capable + for model in ( + "al-qwen3.8-max-preview", + "al-qwen3.7-max", + "al-qwen3.7-plus", + "al-qwen3.6-flash", + "sn-qwen3-235b-a22b", + ): + assert cfg._candidate_supports_reasoning(model) is True, ( + f"{model} must be reasoning-capable (prefixed Qwen 3+)" + ) + # Bare Qwen 3+ → still works + for model in ( + "qwen3-235b-a22b", + "qwen3-32b", + ): + assert cfg._candidate_supports_reasoning(model) is True, ( + f"{model} must be reasoning-capable (bare Qwen 3+)" + ) + # Qwen 2.x → excluded regardless of prefix + for model in ( + "al-qwen2.5-72b-instruct", + "qwen2.5-7b-instruct", + "qwen2-72b", + ): + assert cfg._candidate_supports_reasoning(model) is False, ( + f"{model} must NOT be reasoning-capable (Qwen 2.x excluded)" + ) + From a57d576805ed30ac9d6f4800ea46161697ee850f Mon Sep 17 00:00:00 2001 From: happy5318 Date: Sat, 25 Jul 2026 16:27:50 +0800 Subject: [PATCH 3/3] fix: make prefixed Qwen detection positive-only to avoid shadowing hybrid IDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate review found that the broadened token-prefix check caused deepseek-r1-distill-qwen2.5-bakeneko-32b to enter the Qwen branch (qwen2 token), return False, and prevent the DeepSeek detector from running — a silent regression. Fix: split into two phases: 1. Positive-only scan: any token matching qwen(\d+) with major >= 3 immediately returns True. No False path — Qwen 2.x in hybrid IDs falls through to subsequent detectors. 2. Terminal guard (original behavior): standalone "qwen" token or normalized.startswith("qwen") still applies the 3+ gate with return False for 2.x, since those ARE pure Qwen models. Add regression test for deepseek-r1-distill-qwen2.5-bakeneko-32b. --- api/config.py | 20 +++++++++++++------ ...est_reasoning_effort_model_capabilities.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/api/config.py b/api/config.py index 447ebf62..cd4ee1d6 100644 --- a/api/config.py +++ b/api/config.py @@ -3316,12 +3316,20 @@ def _candidate_supports_reasoning(candidate: str) -> bool: if major >= 4 or (major == 3 and minor >= 7): return True return False - if any(token.startswith("qwen") for token in tokens): - # Restrict to Qwen 3+ (exclude Qwen 2/2.5) - # Token-prefix matching (position-independent) so prefixed ids like - # "al-qwen3-8-max-preview" (tokens: ["al","qwen3","8",...]) still match, - # even though "qwen" is not a standalone token. The bare - # normalized.startswith("qwen") case is subsumed by this check. + # Positive-only prefixed Qwen 3+ detection (e.g. "al-qwen3-8-max-preview" + # → tokens ["al","qwen3","8",...]). Scan for any token starting with + # "qwen" followed by version >= 3 and immediately allow. Do NOT return + # False here — Qwen 2.x embedded in hybrid IDs like + # "deepseek-r1-distill-qwen2.5-bakeneko-32b" must fall through to the + # DeepSeek detector below. + for token in tokens: + m = re.match(r"qwen(\d+)", token) + if m and int(m.group(1)) >= 3: + return True + # Terminal guard for standalone/bare Qwen IDs (original behavior): + # "qwen" as a standalone token or normalized starting with "qwen" means + # this IS a Qwen model — apply the 3+ gate and block 2.x. + if "qwen" in token_set or normalized.startswith("qwen"): match = re.search(r"qwen.*?(\d+)(?:\D+(\d+))?", normalized) if match: major = int(match.group(1)) diff --git a/tests/test_reasoning_effort_model_capabilities.py b/tests/test_reasoning_effort_model_capabilities.py index 798d7114..2a4e7edb 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -550,4 +550,14 @@ def test_qwen_prefixed_alias_reasoning_detection(): assert cfg._candidate_supports_reasoning(model) is False, ( f"{model} must NOT be reasoning-capable (Qwen 2.x excluded)" ) + # Hybrid IDs with embedded Qwen 2.x must NOT be shadowed by the Qwen + # branch — they must fall through to the DeepSeek detector. + for model in ( + "deepseek-r1-distill-qwen2.5-bakeneko-32b", + "rinna/deepseek-r1-distill-qwen2.5-bakeneko-32b", + ): + assert cfg._candidate_supports_reasoning(model) is True, ( + f"{model} must remain reasoning-capable (DeepSeek-R1 hybrid, " + f"Qwen 2.x must not shadow the DeepSeek detector)" + )