From 45c1744b86aecfba747599d21c6aec04362235f8 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Mon, 31 Aug 2026 02:15:32 +0000 Subject: [PATCH] fix(llm): apply DeepSeek capabilities to OpenRouter-namespaced models - OpenRouter exposes DeepSeek as deepseek/, which matched neither the exact IDs nor the patterns, so a thinking model like deepseek/deepseek-v4-flash fell through to _DEFAULT and had object-form tool_choice forced on it - strip the official deepseek/ namespace before lookup so it reuses the native quirks; deepseek/deepseek-chat still keeps tool_choice, and third-party finetunes on other publishers stay on _DEFAULT #1199 --- tests/test_capabilities.py | 31 +++++++++++++++++++++++ tradingagents/llm_clients/capabilities.py | 9 +++++++ 2 files changed, 40 insertions(+) diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index debbb756f..2ab28dc2d 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -116,6 +116,37 @@ class TestDefault: assert caps.supports_tool_choice is True +@pytest.mark.unit +class TestOpenRouterDeepSeekNamespace: + """OpenRouter namespaces DeepSeek as ``deepseek/``; strip it so the + same quirks apply as the native provider (#1199).""" + + def test_prefixed_v4_flash_suppresses_tool_choice(self): + # Was falling through to _DEFAULT (tool_choice on) -> slow object-form call. + assert get_capabilities("deepseek/deepseek-v4-flash").supports_tool_choice is False + + def test_prefixed_reasoner_suppresses_tool_choice(self): + assert get_capabilities("deepseek/deepseek-reasoner").supports_tool_choice is False + + def test_prefixed_chat_selects_deepseek_chat_not_default(self): + # Must resolve to _DEEPSEEK_CHAT, not _DEFAULT: supports_json_schema=False + # is what distinguishes them (both keep tool_choice). + caps = get_capabilities("deepseek/deepseek-chat") + assert caps.supports_tool_choice is True + assert caps.supports_json_schema is False # _DEEPSEEK_CHAT, not _DEFAULT + + def test_only_official_namespace_is_stripped(self): + # A third-party publisher whose model name WOULD match a deepseek pattern + # must stay _DEFAULT: proves we strip only "deepseek/", not any "*/". + caps = get_capabilities("tngtech/deepseek-v4-flash") + assert caps.supports_tool_choice is True # not thinking + assert caps.supports_json_schema is True # _DEFAULT + + def test_native_ids_unchanged(self): + assert get_capabilities("deepseek-v4-flash").supports_tool_choice is False + assert get_capabilities("deepseek-chat").supports_tool_choice is True + + @pytest.mark.unit def test_capabilities_dataclass_is_frozen(): """Capability rows are immutable so they can be safely shared.""" diff --git a/tradingagents/llm_clients/capabilities.py b/tradingagents/llm_clients/capabilities.py index d3e9c5782..a2df7100a 100644 --- a/tradingagents/llm_clients/capabilities.py +++ b/tradingagents/llm_clients/capabilities.py @@ -118,6 +118,15 @@ _BY_PATTERN: list[tuple[re.Pattern[str], ModelCapabilities]] = [ def get_capabilities(model_name: str) -> ModelCapabilities: """Resolve capabilities by exact ID, then pattern, then default.""" + # OpenRouter namespaces official DeepSeek models as ``deepseek/``, so + # strip that prefix to reuse the same quirks as the native provider — e.g. + # ``deepseek/deepseek-v4-flash`` must suppress tool_choice like + # ``deepseek-v4-flash`` does, not fall through to _DEFAULT (#1199). Only the + # official namespace is stripped; third-party finetunes on other publishers + # (e.g. ``tngtech/deepseek-...``) keep _DEFAULT, since their quirks are unknown. + if model_name.startswith("deepseek/"): + model_name = model_name.removeprefix("deepseek/") + if model_name in _BY_ID: return _BY_ID[model_name] for pattern, caps in _BY_PATTERN: