diff --git a/astrbot/core/agent/message.py b/astrbot/core/agent/message.py index d69bc6a81..9354ae3a5 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -174,6 +174,8 @@ class AssistantMessageSegment(Message): """A message segment from the assistant.""" role: Literal["assistant"] = "assistant" + reasoning_content: str | None = None + """The reasoning content from the assistant, if available (e.g., DeepSeek thinking mode).""" class ToolCallMessageSegment(Message): diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 7eb90f3fc..5cc50016a 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -217,6 +217,7 @@ async def step(self): tool_calls_info=AssistantMessageSegment( tool_calls=llm_resp.to_openai_to_calls_model(), content=llm_resp.completion_text, + reasoning_content=llm_resp.reasoning_content or "", ), tool_calls_result=tool_call_result_blocks, ) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index b45917026..c607ec583 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -932,13 +932,14 @@ class ChatProviderTemplate(TypedDict): "DeepSeek": { "id": "deepseek", "provider": "deepseek", - "type": "openai_chat_completion", + "type": "deepseek_chat_completion", "provider_type": "chat_completion", "enable": True, "key": [], "api_base": "https://api.deepseek.com/v1", "timeout": 120, "custom_headers": {}, + "ds_thinking_tool_call": False, }, "Zhipu": { "id": "zhipu", @@ -1709,6 +1710,11 @@ class ChatProviderTemplate(TypedDict): "type": "bool", "hint": "启用后所有函数工具将全部失效", }, + "ds_thinking_tool_call": { + "description": "思考中调用工具", + "type": "bool", + "hint": "启用后,DeepSeek 模型可以在思考过程中调用工具,实现多轮思考-工具调用循环。", + }, "gm_url_context": { "description": "启用URL上下文功能", "type": "bool", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 0dff2c8ed..9384723ca 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -275,6 +275,10 @@ def dynamic_import_provider(self, type: str): from .sources.openai_source import ( ProviderOpenAIOfficial as ProviderOpenAIOfficial, ) + case "deepseek_chat_completion": + from .sources.deepseek_source import ( + ProviderDeepSeek as ProviderDeepSeek, + ) case "zhipu_chat_completion": from .sources.zhipu_source import ProviderZhipu as ProviderZhipu case "groq_chat_completion": diff --git a/astrbot/core/provider/sources/deepseek_source.py b/astrbot/core/provider/sources/deepseek_source.py new file mode 100644 index 000000000..ad02ee076 --- /dev/null +++ b/astrbot/core/provider/sources/deepseek_source.py @@ -0,0 +1,112 @@ +from astrbot.core.agent.tool import ToolSet +from astrbot.core.provider.entities import LLMResponse + +from ..entities import ProviderType +from ..register import register_provider_adapter +from .openai_source import ProviderOpenAIOfficial + + +@register_provider_adapter( + "deepseek_chat_completion", + "DeepSeek API Chat Completion 提供商适配器", + ProviderType.CHAT_COMPLETION, + default_config_tmpl={ + "id": "deepseek", + "provider": "deepseek", + "type": "deepseek_chat_completion", + "provider_type": "chat_completion", + "enable": True, + "key": [], + "api_base": "https://api.deepseek.com/v1", + "timeout": 120, + "custom_headers": {}, + "hint": "DeepSeek 官方 API,支持思考模式。", + }, + provider_display_name="DeepSeek", +) +class ProviderDeepSeek(ProviderOpenAIOfficial): + def _should_enable_thinking(self, payloads: dict) -> bool: + """判断是否应该启用思考模式 + + 规则: + 1. deepseek-chat 模型无条件关闭思维链 + 2. deepseek-reasoner 模型无条件开启思维链 + 3. 其他模型根据 ds_thinking_tool_call 配置决定 + """ + model = payloads.get("model", "").lower() + + # deepseek-chat 强制关闭 + if "deepseek-chat" in model: + return False + + # deepseek-reasoner 强制开启 + if "deepseek-reasoner" in model or "reasoner" in model: + return True + + # 其他模型根据配置决定 + return self.provider_config.get("ds_thinking_tool_call", False) + + async def _query_stream( + self, + payloads: dict, + tools: ToolSet | None, + ): + # 判断是否启用思考模式 + ds_thinking_enabled = self._should_enable_thinking(payloads) + + if ds_thinking_enabled: + messages = payloads.get("messages", []) + + # DeepSeek API 要求:思考模式下每条助手消息必须有 reasoning_content + # 先确保每条助手消息都有这个字段 + for msg in messages: + if msg.get("role") == "assistant": + if "reasoning_content" not in msg: + # 工具调用消息等特殊消息初始化为空字符串 + msg["reasoning_content"] = "" + + # 清理历史消息中的 reasoning_content + # 只有最后一条是 user 时才清空(说明是新问题) + if messages and messages[-1].get("role") == "user": + for msg in messages: + if msg.get("role") == "assistant" and "reasoning_content" in msg: + msg["reasoning_content"] = "" + + # 添加 thinking 参数(父类会自动把它放到 extra_body) + payloads["thinking"] = {"type": "enabled"} + + # 调用父类方法 + async for response in super()._query_stream(payloads, tools): + yield response + + async def _query( + self, + payloads: dict, + tools: ToolSet | None, + ) -> LLMResponse: + # 判断是否启用思考模式 + ds_thinking_enabled = self._should_enable_thinking(payloads) + + if ds_thinking_enabled: + messages = payloads.get("messages", []) + + # DeepSeek API 要求:思考模式下每条助手消息必须有 reasoning_content + # 先确保每条助手消息都有这个字段 + for msg in messages: + if msg.get("role") == "assistant": + if "reasoning_content" not in msg: + # 工具调用消息等特殊消息初始化为空字符串 + msg["reasoning_content"] = "" + + # 清理历史消息中的 reasoning_content + # 只有最后一条是 user 时才清空(说明是新问题) + if messages and messages[-1].get("role") == "user": + for msg in messages: + if msg.get("role") == "assistant" and "reasoning_content" in msg: + msg["reasoning_content"] = "" + + # 添加 thinking 参数 + payloads["thinking"] = {"type": "enabled"} + + # 调用父类方法 + return await super()._query(payloads, tools)