diff --git a/src/langbot/pkg/provider/modelmgr/requesters/litellmchat.py b/src/langbot/pkg/provider/modelmgr/requesters/litellmchat.py index ae1904b25..5fc7b8834 100644 --- a/src/langbot/pkg/provider/modelmgr/requesters/litellmchat.py +++ b/src/langbot/pkg/provider/modelmgr/requesters/litellmchat.py @@ -164,6 +164,12 @@ class LiteLLMRequester(requester.ProviderAPIRequester): _EMBEDDING_MODEL_HINTS = ('embedding', 'embed', 'bge-', 'e5-', 'm3e', 'gte-', 'text-embedding') _RERANK_MODEL_HINTS = ('rerank', 're-rank', 're_rank') + _QWEN_DEDICATED_THINKING_MODELS = frozenset( + { + 'qwen3.7-max-preview', + 'qwen3.7-max-2026-05-17', + } + ) _INFERRED_EFFORT_PROVIDERS = frozenset( { 'anthropic', @@ -392,6 +398,14 @@ class LiteLLMRequester(requester.ProviderAPIRequester): return inferred_family return 'volcengine' if requester_name == 'volcark-chat-completions' else '' + # Bailian's compatible endpoint also hosts Kimi models. Keep those + # models on Kimi's ``thinking`` protocol instead of Qwen's + # ``enable_thinking`` protocol. + if requester_name == 'bailian-chat-completions': + inferred_family = self._infer_reasoning_family_from_model_name(model_name) + if inferred_family == 'kimi': + return inferred_family + requester_family = self._REQUESTER_REASONING_FAMILIES.get(requester_name) if requester_family: return requester_family @@ -428,7 +442,11 @@ class LiteLLMRequester(requester.ProviderAPIRequester): @staticmethod def _is_dedicated_qwen_thinking_model(model_name: str) -> bool: normalized_name = model_name.lower().rsplit('/', 1)[-1] - return normalized_name.startswith('qwq') or '-thinking' in normalized_name + return ( + normalized_name in LiteLLMRequester._QWEN_DEDICATED_THINKING_MODELS + or normalized_name.startswith('qwq') + or '-thinking' in normalized_name + ) def _known_reasoning_levels(self, model_name: str, family: str) -> list[str] | None: normalized_name = model_name.lower().rsplit('/', 1)[-1] diff --git a/tests/unit_tests/provider/test_reasoning_control.py b/tests/unit_tests/provider/test_reasoning_control.py index dabc642ad..2281037b1 100644 --- a/tests/unit_tests/provider/test_reasoning_control.py +++ b/tests/unit_tests/provider/test_reasoning_control.py @@ -237,6 +237,43 @@ def test_qwen_mixed_and_dedicated_thinking_profiles(monkeypatch): assert dedicated['levels'] == ['provider_default'] +@pytest.mark.parametrize('model_name', ['qwen3.7-max-preview', 'qwen3.7-max-2026-05-17']) +def test_qwen_dedicated_thinking_release_models_are_not_toggleable(model_name, monkeypatch): + request = _requester('openai', 'bailian-chat-completions') + monkeypatch.setattr(request, '_supports_reasoning', lambda _: False) + + capabilities = request.get_reasoning_capabilities(_runtime_model(request, name=model_name, abilities=[])) + + assert capabilities['levels'] == ['provider_default'] + + +@pytest.mark.parametrize( + ('model_name', 'expected_levels'), + [ + ('kimi-k2.6', ['provider_default', 'disabled', 'enabled']), + ('kimi-k2.5', ['provider_default', 'disabled', 'enabled']), + ('kimi-k2.7-code', ['provider_default']), + ('kimi-k2-thinking', ['provider_default']), + ], +) +def test_bailian_kimi_profiles_use_kimi_model_rules(model_name, expected_levels, monkeypatch): + request = _requester('openai', 'bailian-chat-completions') + monkeypatch.setattr(request, '_supports_reasoning', lambda _: False) + + capabilities = request.get_reasoning_capabilities(_runtime_model(request, name=model_name, abilities=[])) + + assert capabilities['levels'] == expected_levels + + +def test_bailian_kimi_uses_thinking_protocol_instead_of_qwen_protocol(monkeypatch): + request = _requester('openai', 'bailian-chat-completions') + monkeypatch.setattr(request, '_supports_reasoning', lambda _: False) + + assert request._build_reasoning_args(_runtime_model(request, 'disabled', name='kimi-k2.6')) == { + 'extra_body': {'thinking': {'type': 'disabled'}} + } + + def test_doubao_exposes_documented_effort_range(monkeypatch): request = _requester('openai', 'doubao-chat-completions') monkeypatch.setattr(request, '_supports_reasoning', lambda _: False)