diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 64243a82f5..34aa27f6f9 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1281,6 +1281,8 @@ class ChatProviderTemplate(TypedDict): "timeout": 120, "proxy": "", "custom_headers": {}, + "deepseek_thinking_enabled": True, + "deepseek_reasoning_effort": "high", }, "Zhipu": { "id": "zhipu", @@ -1983,6 +1985,17 @@ class ChatProviderTemplate(TypedDict): "type": "bool", "hint": "关闭 Ollama 思考模式。", }, + "deepseek_thinking_enabled": { + "description": "开启思考模式", + "type": "bool", + "hint": "控制思考开关。", + }, + "deepseek_reasoning_effort": { + "description": "思考强度", + "type": "string", + "options": ["high", "max"], + "hint": "仅开启思考模式生效,可选 high / max。", + }, "custom_extra_body": { "description": "自定义请求体参数", "type": "dict", diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 512e47233a..bcd37afb1c 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -487,13 +487,62 @@ def __init__(self, provider_config, provider_settings) -> None: self.set_model(model) self.reasoning_key = "reasoning_content" + self._deepseek_reasoning_effort_cached: str | None = None + self._deepseek_reasoning_effort_cached_source: Any = None + self._deepseek_reasoning_effort_cache_ready = False - def _ollama_disable_thinking_enabled(self) -> bool: - value = self.provider_config.get("ollama_disable_thinking", False) + @staticmethod + def _config_flag_enabled(value: Any, default: bool = False) -> bool: + if value is None: + return default if isinstance(value, str): - return value.strip().lower() in {"1", "true", "yes", "on"} + normalized = value.strip().lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + return default return bool(value) + def _ollama_disable_thinking_enabled(self) -> bool: + return self._config_flag_enabled( + self.provider_config.get("ollama_disable_thinking", False) + ) + + def _deepseek_thinking_enabled(self) -> bool: + return self._config_flag_enabled( + self.provider_config.get("deepseek_thinking_enabled", True), + default=True, + ) + + def _deepseek_reasoning_effort(self) -> str: + value = self.provider_config.get("deepseek_reasoning_effort", "high") + if ( + self._deepseek_reasoning_effort_cache_ready + and self._deepseek_reasoning_effort_cached_source == value + and self._deepseek_reasoning_effort_cached is not None + ): + return self._deepseek_reasoning_effort_cached + + normalized_value = "high" + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"high", "max"}: + normalized_value = normalized + elif value not in (None, ""): + logger.warning( + f"Invalid DeepSeek reasoning effort: {value}, falling back to high" + ) + elif value not in (None, ""): + logger.warning( + f"Invalid DeepSeek reasoning effort: {value}, falling back to high" + ) + + self._deepseek_reasoning_effort_cached = normalized_value + self._deepseek_reasoning_effort_cached_source = value + self._deepseek_reasoning_effort_cache_ready = True + return normalized_value + def _apply_provider_specific_extra_body_overrides( self, extra_body: dict[str, Any] ) -> None: @@ -508,6 +557,27 @@ def _apply_provider_specific_extra_body_overrides( extra_body.pop("think", None) extra_body["reasoning_effort"] = "none" + def _apply_provider_specific_request_overrides( + self, + payloads: dict, + extra_body: dict[str, Any], + ) -> None: + if self.provider_config.get("provider") == "deepseek": + thinking_enabled = self._deepseek_thinking_enabled() + extra_body.pop("reasoning", None) + extra_body.pop("think", None) + extra_body.pop("reasoning_effort", None) + extra_body["thinking"] = { + "type": "enabled" if thinking_enabled else "disabled" + } + if thinking_enabled: + # Provider config is the canonical DeepSeek reasoning setting. + payloads["reasoning_effort"] = self._deepseek_reasoning_effort() + else: + payloads.pop("reasoning_effort", None) + + self._apply_provider_specific_extra_body_overrides(extra_body) + async def get_models(self): try: models_str = [] @@ -580,7 +650,7 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: custom_extra_body = self.provider_config.get("custom_extra_body", {}) if isinstance(custom_extra_body, dict): extra_body.update(custom_extra_body) - self._apply_provider_specific_extra_body_overrides(extra_body) + self._apply_provider_specific_request_overrides(payloads, extra_body) model = payloads.get("model", "").lower() @@ -634,7 +704,7 @@ async def _query_stream( to_del.append(key) for key in to_del: del payloads[key] - self._apply_provider_specific_extra_body_overrides(extra_body) + self._apply_provider_specific_request_overrides(payloads, extra_body) self._sanitize_assistant_messages(payloads) diff --git a/dashboard/src/composables/useProviderSources.ts b/dashboard/src/composables/useProviderSources.ts index 1d2548382b..1508e35d94 100644 --- a/dashboard/src/composables/useProviderSources.ts +++ b/dashboard/src/composables/useProviderSources.ts @@ -371,6 +371,15 @@ export function useProviderSources(options: UseProviderSourcesOptions) { source.ollama_disable_thinking = false } + if (source.provider === 'deepseek') { + if (source.deepseek_thinking_enabled === undefined) { + source.deepseek_thinking_enabled = true + } + if (!source.deepseek_reasoning_effort) { + source.deepseek_reasoning_effort = 'high' + } + } + return source } diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index c0796b7f07..fa1d429f04 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1188,6 +1188,14 @@ "description": "Disable thinking mode", "hint": "Close Ollama thinking mode." }, + "deepseek_thinking_enabled": { + "description": "Enable thinking mode", + "hint": "Controls thinking on/off." + }, + "deepseek_reasoning_effort": { + "description": "Reasoning effort", + "hint": "Takes effect only when thinking is enabled. Options: high / max." + }, "custom_extra_body": { "description": "Custom request body parameters", "hint": "Add extra parameters to requests, such as temperature, top_p, max_tokens, etc.", diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 407e9f9f45..26a2990660 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1190,6 +1190,14 @@ "description": "关闭思考模式", "hint": "关闭 Ollama 思考模式。" }, + "deepseek_thinking_enabled": { + "description": "开启思考模式", + "hint": "控制思考开关。" + }, + "deepseek_reasoning_effort": { + "description": "思考强度", + "hint": "仅开启思考模式生效,可选 high / max。" + }, "custom_extra_body": { "description": "自定义请求体参数", "hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。", diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index ec5e79f492..1a707cf02b 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -1724,3 +1724,151 @@ async def fake_create(**kwargs): assert messages[1] == {"role": "user", "content": "again"} finally: await provider.terminate() + + +@pytest.mark.asyncio +async def test_apply_provider_specific_request_overrides_disables_deepseek_thinking(): + provider = _make_provider( + { + "provider": "deepseek", + "deepseek_thinking_enabled": False, + "deepseek_reasoning_effort": "max", + } + ) + try: + payloads = { + "model": "deepseek-v4-pro", + "messages": [{"role": "user", "content": "hello"}], + "reasoning_effort": "max", + } + extra_body = { + "reasoning": {"effort": "high"}, + "reasoning_effort": "high", + "think": True, + } + + provider._apply_provider_specific_request_overrides(payloads, extra_body) + + assert "reasoning_effort" not in payloads + assert extra_body["thinking"] == {"type": "disabled"} + assert "reasoning" not in extra_body + assert "reasoning_effort" not in extra_body + assert "think" not in extra_body + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_apply_provider_specific_request_overrides_prefers_deepseek_config_reasoning_effort(): + provider = _make_provider( + { + "provider": "deepseek", + "deepseek_thinking_enabled": True, + "deepseek_reasoning_effort": "max", + } + ) + try: + payloads = { + "model": "deepseek-v4-pro", + "messages": [{"role": "user", "content": "hello"}], + "reasoning_effort": "high", + } + extra_body = { + "reasoning_effort": "high", + "thinking": {"type": "disabled"}, + } + + provider._apply_provider_specific_request_overrides(payloads, extra_body) + + assert payloads["reasoning_effort"] == "max" + assert extra_body["thinking"] == {"type": "enabled"} + assert "reasoning_effort" not in extra_body + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_deepseek_reasoning_effort_warns_once_for_invalid_config(monkeypatch): + provider = _make_provider( + { + "provider": "deepseek", + "deepseek_reasoning_effort": "invalid", + } + ) + warnings: list[str] = [] + try: + monkeypatch.setattr( + "astrbot.core.provider.sources.openai_source.logger.warning", + warnings.append, + ) + + assert provider._deepseek_reasoning_effort() == "high" + assert provider._deepseek_reasoning_effort() == "high" + assert warnings == [ + "Invalid DeepSeek reasoning effort: invalid, falling back to high" + ] + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_query_injects_deepseek_thinking_and_reasoning_effort(monkeypatch): + provider = _make_provider( + { + "provider": "deepseek", + "deepseek_thinking_enabled": True, + "deepseek_reasoning_effort": "max", + "custom_extra_body": { + "thinking": {"type": "disabled"}, + "reasoning_effort": "high", + "temperature": 0.1, + }, + } + ) + try: + captured_kwargs = {} + + async def fake_create(**kwargs): + captured_kwargs.update(kwargs) + return ChatCompletion.model_validate( + { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "deepseek-v4-pro", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "ok", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 1, + "completion_tokens": 1, + "total_tokens": 2, + }, + } + ) + + monkeypatch.setattr(provider.client.chat.completions, "create", fake_create) + + await provider._query( + payloads={ + "model": "deepseek-v4-pro", + "messages": [{"role": "user", "content": "hello"}], + "reasoning_effort": "high", + }, + tools=None, + ) + + extra_body = captured_kwargs["extra_body"] + assert captured_kwargs["reasoning_effort"] == "max" + assert extra_body["thinking"] == {"type": "enabled"} + assert "reasoning_effort" not in extra_body + assert extra_body["temperature"] == 0.1 + finally: + await provider.terminate()