mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-17 18:20:01 +08:00
✨ feat(llm): 优化 DeepSeek 适配器 (#2160)
检查bot是否运行正常 / bot check (push) Canceled after 0s
CodeQL Code Security Analysis / Analyze (python) (push) Canceled after 0s
Ruff Lint / Ruff Lint (push) Canceled after 0s
Sequential Lint and Type Check / ruff-call (push) Canceled after 0s
Release Drafter / Update Release Draft (push) Canceled after 0s
Force Sync to Aliyun / sync (push) Canceled after 0s
Update Version / update-version (push) Canceled after 0s
Sequential Lint and Type Check / pyright-call (push) Canceled after 0s
检查bot是否运行正常 / bot check (push) Canceled after 0s
CodeQL Code Security Analysis / Analyze (python) (push) Canceled after 0s
Ruff Lint / Ruff Lint (push) Canceled after 0s
Sequential Lint and Type Check / ruff-call (push) Canceled after 0s
Release Drafter / Update Release Draft (push) Canceled after 0s
Force Sync to Aliyun / sync (push) Canceled after 0s
Update Version / update-version (push) Canceled after 0s
Sequential Lint and Type Check / pyright-call (push) Canceled after 0s
* ✨ feat(llm): 新增 Grok 适配器及 API 密钥重置指令 - 新增 Grok 适配器并支持原生推特检索工具 `x_search` - 优化 DeepSeek 适配器以迁移至 Responses 协议,增强多模态支持 - 在 LLM 管理插件中新增 `llm reset` 子命令以重置 Key 状态 - 增强 MCP 连接异常处理及流关闭容错能力 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
39ed1ade14
commit
33d6ea1335
@@ -1,24 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.ai.core.messages import ImagePart, LLMMessage
|
||||
from zhenxun.services.ai.core.models import (
|
||||
ModelCapabilities,
|
||||
ModelDetail,
|
||||
ModelIdentity,
|
||||
)
|
||||
from zhenxun.services.ai.core.options import GenerationConfig
|
||||
from zhenxun.services.ai.core.options import GenerationConfig, ResponseFormat
|
||||
|
||||
from .handlers.openai_handlers import (
|
||||
OpenAIConfigMapper,
|
||||
OpenAITextHandler,
|
||||
OpenAIToolSerializer,
|
||||
OpenAIResponsesTextHandler,
|
||||
ResponsesConfigMapper,
|
||||
ResponsesMessageConverter,
|
||||
ResponsesResponseParser,
|
||||
ResponsesToolSerializer,
|
||||
)
|
||||
from .openai import OpenAICompatAdapter
|
||||
from .openai import OpenAIResponsesAdapter
|
||||
|
||||
|
||||
class DeepSeekToolSerializer(OpenAIToolSerializer):
|
||||
class DeepSeekToolSerializer(ResponsesToolSerializer):
|
||||
"""
|
||||
专门针对 DeepSeek 的工具序列化器。
|
||||
负责抹平 Pydantic Schema 与 DeepSeek Strict Mode 之间的差异。
|
||||
由于继承了 ResponsesToolSerializer,已自动获得 web_search 工具的原生格式支持。
|
||||
"""
|
||||
|
||||
def __init__(self, api_type: str = "deepseek"):
|
||||
@@ -68,7 +71,35 @@ class DeepSeekToolSerializer(OpenAIToolSerializer):
|
||||
return pipeline.run(schema)
|
||||
|
||||
|
||||
class DeepSeekConfigMapper(OpenAIConfigMapper):
|
||||
class DeepSeekMessageConverter(ResponsesMessageConverter):
|
||||
"""DeepSeek 专属 Responses 消息转换器"""
|
||||
|
||||
def __init__(self, api_type: str = "deepseek"):
|
||||
super().__init__(api_type=api_type)
|
||||
|
||||
async def convert_messages_async(
|
||||
self, messages: list[LLMMessage]
|
||||
) -> list[dict[str, Any]]:
|
||||
input_items = await super().convert_messages_async(messages)
|
||||
|
||||
image_parts = [
|
||||
p for msg in messages for p in msg.content if isinstance(p, ImagePart)
|
||||
]
|
||||
img_idx = 0
|
||||
|
||||
for item in input_items:
|
||||
if item.get("role") in ("user", "developer") and "content" in item:
|
||||
for c in item["content"]:
|
||||
if c.get("type") == "input_image":
|
||||
if img_idx < len(image_parts):
|
||||
part = image_parts[img_idx]
|
||||
img_idx += 1
|
||||
if getattr(part, "media_resolution", None):
|
||||
c["detail"] = str(part.media_resolution).lower()
|
||||
return input_items
|
||||
|
||||
|
||||
class DeepSeekConfigMapper(ResponsesConfigMapper):
|
||||
"""DeepSeek 的专属配置映射器"""
|
||||
|
||||
def map_config(
|
||||
@@ -77,39 +108,38 @@ class DeepSeekConfigMapper(OpenAIConfigMapper):
|
||||
model_detail: ModelDetail | None = None,
|
||||
capabilities: ModelCapabilities | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""映射生成参数并处理 DeepSeek 专有 `thinking` 与响应格式差异。"""
|
||||
params = super().map_config(config, model_detail, capabilities)
|
||||
"""拦截并处理 DeepSeek 目前无法严格遵循复杂 json_schema 的问题"""
|
||||
if (
|
||||
config.output.response_format == ResponseFormat.JSON
|
||||
and config.output.response_schema
|
||||
):
|
||||
config.output.response_schema = None
|
||||
|
||||
if "response_format" in params:
|
||||
rf = params["response_format"]
|
||||
if isinstance(rf, dict) and rf.get("type") == "json_schema":
|
||||
params["response_format"] = {"type": "json_object"}
|
||||
|
||||
return params
|
||||
return super().map_config(config, model_detail, capabilities)
|
||||
|
||||
|
||||
class DeepSeekTextHandler(OpenAITextHandler):
|
||||
"""DeepSeek 专有文本处理器,替换了特定序列化组件"""
|
||||
class DeepSeekTextHandler(OpenAIResponsesTextHandler):
|
||||
"""DeepSeek 专有 Responses 文本对话处理器,组装所有定制化子件"""
|
||||
|
||||
def __init__(self, api_type: str = "deepseek"):
|
||||
"""替换 OpenAI 默认组件为 DeepSeek 专用实现。"""
|
||||
super().__init__(api_type=api_type)
|
||||
self.converter = DeepSeekMessageConverter(api_type=api_type)
|
||||
self.serializer = DeepSeekToolSerializer(api_type=api_type)
|
||||
self.mapper = DeepSeekConfigMapper(api_type=api_type)
|
||||
self.parser = ResponsesResponseParser()
|
||||
|
||||
|
||||
class DeepSeekAdapter(OpenAICompatAdapter):
|
||||
"""DeepSeek 官方 API 适配器"""
|
||||
class DeepSeekAdapter(OpenAIResponsesAdapter):
|
||||
"""DeepSeek Responses API 适配器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化 DeepSeek 适配器并挂载文本处理器。"""
|
||||
super().__init__()
|
||||
self.text_handler = DeepSeekTextHandler(api_type=self.api_type)
|
||||
|
||||
@property
|
||||
def log_sanitization_context(self) -> str:
|
||||
"""返回 DeepSeek 请求日志清洗上下文。"""
|
||||
return "openai_request"
|
||||
"""复用 Responses API 的日志清洗上下文。"""
|
||||
return "openai_responses_request"
|
||||
|
||||
@property
|
||||
def api_type(self) -> str:
|
||||
@@ -120,7 +150,3 @@ class DeepSeekAdapter(OpenAICompatAdapter):
|
||||
def supported_api_types(self) -> list[str]:
|
||||
"""当前适配器支持的 API 类型列表。"""
|
||||
return ["deepseek"]
|
||||
|
||||
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
|
||||
"""返回对话端点,优先使用模型级自定义端点。"""
|
||||
return "/v1/chat/completions"
|
||||
|
||||
@@ -31,6 +31,7 @@ class LLMAdapterFactory:
|
||||
from .doubao import DoubaoAdapter
|
||||
from .gemini import GeminiAdapter
|
||||
from .glm import GLMAdapter
|
||||
from .grok import GrokAdapter
|
||||
from .jina import JinaAdapter
|
||||
from .mimo import MiMoAdapter
|
||||
from .minimax import MiniMaxAdapter
|
||||
@@ -48,6 +49,7 @@ class LLMAdapterFactory:
|
||||
cls.register_adapter(MiMoAdapter())
|
||||
cls.register_adapter(MiniMaxAdapter())
|
||||
cls.register_adapter(DoubaoAdapter())
|
||||
cls.register_adapter(GrokAdapter())
|
||||
|
||||
@classmethod
|
||||
def register_adapter(cls, adapter: BaseAdapter) -> None:
|
||||
@@ -114,6 +116,7 @@ class SmartAdapter(BaseAdapter):
|
||||
("*gemini*", "gemini"),
|
||||
("*deepseek*", "deepseek"),
|
||||
("*minimax*", "minimax"),
|
||||
("*grok*", "grok"),
|
||||
("*gpt*", "openai_responses"),
|
||||
]
|
||||
_DEFAULT_API_TYPE: ClassVar[str] = "openai"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.ai.core.models import ModelCapabilities
|
||||
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
|
||||
OpenAIResponsesTextHandler,
|
||||
ResponsesToolSerializer,
|
||||
)
|
||||
from zhenxun.services.ai.llm.adapters.openai import OpenAIResponsesAdapter
|
||||
|
||||
|
||||
class GrokToolSerializer(ResponsesToolSerializer):
|
||||
"""Grok 专属工具序列化器"""
|
||||
|
||||
def serialize_server_tools(
|
||||
self, tools: list[Any], capabilities: ModelCapabilities
|
||||
) -> list[dict[str, Any]]:
|
||||
res = []
|
||||
for t in tools:
|
||||
type_id = getattr(t, "type_id", "unknown")
|
||||
if type_id not in capabilities.supported_native_tools:
|
||||
continue
|
||||
|
||||
if type_id == "web_search":
|
||||
res.append({"type": "web_search"})
|
||||
elif type_id == "x_search":
|
||||
payload = {"type": "x_search"}
|
||||
if getattr(t, "allowed_x_handles", None):
|
||||
payload["allowed_x_handles"] = t.allowed_x_handles
|
||||
if getattr(t, "excluded_x_handles", None):
|
||||
payload["excluded_x_handles"] = t.excluded_x_handles
|
||||
res.append(payload)
|
||||
elif type_id == "code_execution":
|
||||
res.append({"type": "code_interpreter"})
|
||||
elif type_id == "file_search":
|
||||
res.append({"type": "file_search"})
|
||||
return res
|
||||
|
||||
|
||||
class GrokTextHandler(OpenAIResponsesTextHandler):
|
||||
"""Grok 文本处理器"""
|
||||
|
||||
def __init__(self, api_type: str = "grok"):
|
||||
super().__init__(api_type=api_type)
|
||||
self.serializer = GrokToolSerializer(api_type=api_type)
|
||||
|
||||
|
||||
class GrokAdapter(OpenAIResponsesAdapter):
|
||||
"""xAI Grok API 适配器"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.text_handler = GrokTextHandler(api_type=self.api_type)
|
||||
|
||||
@property
|
||||
def api_type(self) -> str:
|
||||
return "grok"
|
||||
|
||||
@property
|
||||
def supported_api_types(self) -> list[str]:
|
||||
return ["grok"]
|
||||
@@ -521,6 +521,9 @@ class ResponsesConfigMapper(OpenAIConfigMapper):
|
||||
class ResponsesMessageConverter(MessageConverter):
|
||||
"""针对 OpenAI Responses API 的消息转换器"""
|
||||
|
||||
def __init__(self, api_type: str = "openai_responses"):
|
||||
self.api_type = api_type
|
||||
|
||||
async def convert_messages_async(
|
||||
self, messages: list[LLMMessage]
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -543,11 +546,14 @@ class ResponsesMessageConverter(MessageConverter):
|
||||
continue
|
||||
|
||||
content_list: list[dict[str, Any]] = []
|
||||
thought_text = ""
|
||||
for part in msg.content:
|
||||
if part is None:
|
||||
continue
|
||||
|
||||
if isinstance(part, TextPart):
|
||||
if isinstance(part, ThoughtPart):
|
||||
thought_text += part.thought_text
|
||||
elif isinstance(part, TextPart):
|
||||
c_type = "output_text" if role == "assistant" else "input_text"
|
||||
content_list.append({"type": c_type, "text": part.text})
|
||||
elif isinstance(part, ImagePart):
|
||||
@@ -573,6 +579,26 @@ class ResponsesMessageConverter(MessageConverter):
|
||||
{"type": "input_image", "image_url": image_src}
|
||||
)
|
||||
|
||||
if role == "assistant" and thought_text:
|
||||
if self.api_type == "deepseek":
|
||||
input_items.append(
|
||||
{
|
||||
"type": "reasoning",
|
||||
"content": [
|
||||
{"type": "reasoning_text", "text": thought_text}
|
||||
],
|
||||
"summary": [],
|
||||
}
|
||||
)
|
||||
else:
|
||||
input_items.append(
|
||||
{
|
||||
"type": "reasoning",
|
||||
"content": [],
|
||||
"summary": [{"type": "summary_text", "text": thought_text}],
|
||||
}
|
||||
)
|
||||
|
||||
if content_list:
|
||||
input_items.append({"role": role, "content": content_list})
|
||||
|
||||
@@ -661,6 +687,10 @@ class ResponsesResponseParser(OpenAIResponseParser):
|
||||
)
|
||||
)
|
||||
elif item.get("type") == "reasoning":
|
||||
for r_content in item.get("content", []):
|
||||
if r_content.get("type") == "reasoning_text":
|
||||
thought_content += r_content.get("text", "")
|
||||
|
||||
for summary_item in item.get("summary", []):
|
||||
if summary_item.get("type") == "summary_text":
|
||||
thought_content += summary_item.get("text", "")
|
||||
@@ -868,7 +898,7 @@ class OpenAIResponsesTextHandler(OpenAITextHandler):
|
||||
|
||||
def __init__(self, api_type: str = "openai_responses"):
|
||||
super().__init__(api_type=api_type)
|
||||
self.converter = ResponsesMessageConverter()
|
||||
self.converter = ResponsesMessageConverter(api_type=api_type)
|
||||
self.serializer = ResponsesToolSerializer(api_type=api_type)
|
||||
self.mapper = ResponsesConfigMapper(api_type=api_type)
|
||||
self.parser = ResponsesResponseParser()
|
||||
|
||||
@@ -301,7 +301,7 @@ class LoggingMiddleware:
|
||||
|
||||
smart_adapter = cast(SmartAdapter, self.adapter)
|
||||
delegate_adapter = smart_adapter._get_delegate_adapter(self.identity)
|
||||
sanitizer_req_context = f"{delegate_adapter.api_type}_request"
|
||||
sanitizer_req_context = delegate_adapter.log_sanitization_context
|
||||
else:
|
||||
sanitizer_req_context = self.adapter.log_sanitization_context
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ def get_default_api_base_for_type(api_type: str) -> str | None:
|
||||
"glm": "https://open.bigmodel.cn",
|
||||
"gemini": "https://generativelanguage.googleapis.com",
|
||||
"openrouter": "https://openrouter.ai/api",
|
||||
"grok": "https://api.x.ai/v1",
|
||||
"smart": None,
|
||||
"openai_responses": None,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from zhenxun.utils.pydantic_compat import model_copy
|
||||
|
||||
CTX_1_05M = 1_050_000
|
||||
CTX_1M = 1_000_000
|
||||
CTX_500K = 500_000
|
||||
CTX_400K = 400_000
|
||||
CTX_256K = 256_000
|
||||
CTX_200K = 204_800
|
||||
@@ -122,7 +123,16 @@ CAP_OPENAI_MULTIMODAL = ModelCapabilities(
|
||||
},
|
||||
reasoning_effort_map={"max": "xhigh", "minimal": "none"},
|
||||
)
|
||||
CAP_DEEPSEEK_V4 = ModelCapabilities(
|
||||
CAP_GROK = ModelCapabilities(
|
||||
input_modalities={ModelModality.TEXT, ModelModality.IMAGE},
|
||||
output_modalities={ModelModality.TEXT},
|
||||
supports_tool_calling=True,
|
||||
reasoning_mode=ReasoningMode.EFFORT,
|
||||
reasoning_visibility="visible",
|
||||
supported_native_tools={"web_search", "x_search", "code_execution", "file_search"},
|
||||
)
|
||||
|
||||
CAP_DEEPSEEK_PRO = ModelCapabilities(
|
||||
input_modalities={ModelModality.TEXT},
|
||||
output_modalities={ModelModality.TEXT},
|
||||
supports_tool_calling=True,
|
||||
@@ -130,6 +140,14 @@ CAP_DEEPSEEK_V4 = ModelCapabilities(
|
||||
reasoning_mode=ReasoningMode.EFFORT,
|
||||
reasoning_visibility="visible",
|
||||
reasoning_effort_map={"minimal": "low"},
|
||||
supported_native_tools={"web_search"},
|
||||
)
|
||||
|
||||
CAP_DEEPSEEK_FLASH = model_copy(
|
||||
CAP_DEEPSEEK_PRO,
|
||||
update={
|
||||
"input_modalities": {ModelModality.TEXT, ModelModality.IMAGE},
|
||||
},
|
||||
)
|
||||
CAP_MINIMAX_REASONING = ModelCapabilities(
|
||||
input_modalities={ModelModality.TEXT},
|
||||
@@ -279,7 +297,10 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [
|
||||
(["*gemini*image*", "*nano-banana*"], CAP_GEMINI_IMAGE, CTX_128K),
|
||||
(["glm-4.6v*"], CAP_GLM_MULTIMODAL, CTX_128K),
|
||||
(["glm-4.7-flash*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_128K),
|
||||
(["deepseek-v4-pro*", "deepseek-v4-flash*"], CAP_DEEPSEEK_V4, CTX_1M),
|
||||
(["*grok-4.3*", "*grok-4.20*"], CAP_GROK, CTX_1M),
|
||||
(["*grok*"], CAP_GROK, CTX_500K),
|
||||
(["*deepseek*pro*"], CAP_DEEPSEEK_PRO, CTX_1M),
|
||||
(["*deepseek*flash*"], CAP_DEEPSEEK_FLASH, CTX_1M),
|
||||
(["glm-4-long*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_1M),
|
||||
(["*MiniMax-M3*"], CAP_MINIMAX_MULTIMODAL, CTX_1M),
|
||||
(["mimo-v2.5-pro*", "mimo-v2-pro*", "mimo-v2-flash*"], CAP_MIMO_TEXT, CTX_1M),
|
||||
|
||||
Reference in New Issue
Block a user