diff --git a/plugins/model-providers/commandcode/__init__.py b/plugins/model-providers/commandcode/__init__.py index cf031c6813..1b737644ac 100644 --- a/plugins/model-providers/commandcode/__init__.py +++ b/plugins/model-providers/commandcode/__init__.py @@ -48,6 +48,32 @@ class CommandCodeAnthropicProfile(CommandCodeProfile): all_models = super().fetch_models(api_key=api_key, base_url=base_url, timeout=timeout) return None if all_models is None else [m for m in all_models if m.startswith("claude-")] + def build_api_kwargs_extras( + self, *, reasoning_config: dict | None = None, model: str | None = None, **context + ) -> tuple[dict, dict]: + """Apply the native DeepSeek reasoning controls for DeepSeek-family ids. + + CommandCode fronts DeepSeek with vendor-prefixed ids + (``deepseek/deepseek-v4-flash``). DeepSeek V4+ defaults to thinking + mode when the ``thinking`` field is omitted, so without an explicit + wire control a Hermes ``/reasoning none`` changes the session state + but not the actual request — the turn sits in + ``reflecting.../brainstorming...`` for minutes (#95232). Strip the + vendor prefix and delegate to the native DeepSeek profile's logic + (extra_body.thinking + reasoning_effort mapping); other CommandCode + model families keep the base no-op behavior. + """ + m = (model or "").strip() + if not m.lower().startswith("deepseek/") or len(m) <= len("deepseek/"): + return {}, {} + from plugins.model_providers.deepseek import deepseek as _deepseek_profile + + return _deepseek_profile.build_api_kwargs_extras( + reasoning_config=reasoning_config, + model=m.split("/", 1)[1], + **context, + ) + commandcode = CommandCodeProfile( name="commandcode", aliases=("commandcode-chat",), api_mode="chat_completions", diff --git a/tests/plugins/model_providers/test_commandcode_profile.py b/tests/plugins/model_providers/test_commandcode_profile.py index f7ce6b0244..36d3efb625 100644 --- a/tests/plugins/model_providers/test_commandcode_profile.py +++ b/tests/plugins/model_providers/test_commandcode_profile.py @@ -86,28 +86,64 @@ class TestCommandCodeProfileIdentity: class TestCommandCodeProfileNoThinkingInterference: - """Chat completions profile is a no-op for thinking config — it delegates - to the underlying model's provider (DeepSeek, Qwen, etc.) for wire format. + """Reasoning wire controls for the chat-completions profile. + + DeepSeek-family ids get the native DeepSeek controls (DeepSeek V4+ + defaults to thinking when the field is omitted, so an explicit wire + control is required for ``/reasoning none`` to reach the request, + #95232); every other CommandCode model family keeps the base no-op. """ - def test_passthrough_no_reasoning_config(self, commandcode_profile): + def test_deepseek_disabled_reasoning_sends_thinking_disabled( + self, commandcode_profile + ): extra_body, top_level = commandcode_profile.build_api_kwargs_extras( - reasoning_config=None, model="deepseek/deepseek-v4-pro" + reasoning_config={"enabled": False}, + model="deepseek/deepseek-v4-flash", ) - # Chat completions profile doesn't inject thinking params — that's - # the DeepSeek provider's job when routed through DeepSeek's own profile. - # When routed through CommandCode, the underlying model API handles it. - assert isinstance(extra_body, dict) - assert isinstance(top_level, dict) - # Default ProviderProfile returns ({}, {}). + assert extra_body.get("thinking") == {"type": "disabled"} + assert top_level == {} - def test_passthrough_with_reasoning_config(self, commandcode_profile): + def test_deepseek_enabled_reasoning_maps_effort(self, commandcode_profile): extra_body, top_level = commandcode_profile.build_api_kwargs_extras( reasoning_config={"enabled": True, "effort": "high"}, model="deepseek/deepseek-v4-pro", ) - assert isinstance(extra_body, dict) - assert isinstance(top_level, dict) + assert extra_body.get("thinking") == {"type": "enabled"} + assert top_level.get("reasoning_effort") == "high" + + def test_deepseek_no_config_defaults_to_enabled(self, commandcode_profile): + # Matches DeepSeek's API default, applied explicitly so the field is + # never omitted for thinking-capable models. + extra_body, _ = commandcode_profile.build_api_kwargs_extras( + reasoning_config=None, model="deepseek/deepseek-v4-flash" + ) + assert extra_body.get("thinking") == {"type": "enabled"} + + def test_deepseek_v3_stays_noop(self, commandcode_profile): + extra_body, top_level = commandcode_profile.build_api_kwargs_extras( + reasoning_config={"enabled": False}, + model="deepseek/deepseek-v3", + ) + assert extra_body == {} + assert top_level == {} + + def test_passthrough_non_deepseek_family(self, commandcode_profile): + extra_body, top_level = commandcode_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model="Qwen/Qwen3.7-Max", + ) + assert extra_body == {} + assert top_level == {} + + def test_passthrough_no_reasoning_config_non_deepseek( + self, commandcode_profile + ): + extra_body, top_level = commandcode_profile.build_api_kwargs_extras( + reasoning_config=None, model="gpt-5.5" + ) + assert extra_body == {} + assert top_level == {} # ── Anthropic Messages profile ────────────────────────────────────────────────