diff --git a/api/config.py b/api/config.py index 17f7bc65..cd4ee1d6 100644 --- a/api/config.py +++ b/api/config.py @@ -3316,8 +3316,20 @@ def _candidate_supports_reasoning(candidate: str) -> bool: if major >= 4 or (major == 3 and minor >= 7): return True return False + # 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"): - # Restrict to Qwen 3+ (exclude Qwen 2/2.5) 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..2a4e7edb 100644 --- a/tests/test_reasoning_effort_model_capabilities.py +++ b/tests/test_reasoning_effort_model_capabilities.py @@ -514,3 +514,50 @@ 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)" + ) + # 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)" + ) +