♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)

* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统

- 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理
- 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册
- 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器
- 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签
- 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据
- 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度
- 重构执行分发器,支持并发限制、串行间隔和随机延迟打散
- 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入
- 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法
- 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出
- 将各模块的全局 logger 替换为对应的模块专属日志代理
- 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题
- 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token

* ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性

- 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构
- 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型
- 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义
- 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑
- 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构

* ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入

- 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性
- 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费
- 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号

* 🚨 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:
Rumio
2026-07-10 09:14:06 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent 0b32d69c9c
commit 922d092650
132 changed files with 2372 additions and 1972 deletions
+2 -1
View File
@@ -13,7 +13,7 @@ from .glm import GLMAdapter
from .jina import JinaAdapter
from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter, OpenAICompatAdapter
from .openai import OpenAIAdapter, OpenAICompatAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter
LLMAdapterFactory.initialize()
@@ -30,6 +30,7 @@ __all__ = [
"MiniMaxAdapter",
"OpenAIAdapter",
"OpenAICompatAdapter",
"OpenAIResponsesAdapter",
"OpenRouterAdapter",
"RequestData",
"ResponseData",
+3 -4
View File
@@ -47,11 +47,11 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart,
)
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.log_sanitizer import sanitize_for_logging
if TYPE_CHECKING:
from zhenxun.services.ai.llm.adapters.handlers.base import (
from .handlers.base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -496,8 +496,7 @@ class BaseAdapter(ABC):
async def parse_speech_response(
self, identity: ModelIdentity, raw_response: httpx.Response
) -> AudioResponse:
"""解析语音响应并委派给 `audio_handler`。
注意传入的是 httpx.Response 的 raw 对象"""
"""解析语音响应并委派给 `audio_handler`"""
if self.audio_handler:
return await self.audio_handler.parse_speech_response(
adapter=self, identity=identity, raw_response=raw_response
+3 -17
View File
@@ -6,12 +6,13 @@ from zhenxun.services.ai.core.models import (
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAITextHandler,
OpenAIToolSerializer,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class DeepSeekToolSerializer(OpenAIToolSerializer):
@@ -84,21 +85,6 @@ class DeepSeekConfigMapper(OpenAIConfigMapper):
if isinstance(rf, dict) and rf.get("type") == "json_schema":
params["response_format"] = {"type": "json_object"}
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params
+2 -1
View File
@@ -34,10 +34,11 @@ class LLMAdapterFactory:
from .jina import JinaAdapter
from .mimo import MiMoAdapter
from .minimax import MiniMaxAdapter
from .openai import OpenAIAdapter
from .openai import OpenAIAdapter, OpenAIResponsesAdapter
from .openrouter import OpenRouterAdapter
cls.register_adapter(OpenAIAdapter())
cls.register_adapter(OpenAIResponsesAdapter())
cls.register_adapter(OpenRouterAdapter())
cls.register_adapter(DeepSeekAdapter())
cls.register_adapter(JinaAdapter())
+4 -3
View File
@@ -1,13 +1,14 @@
from zhenxun.services.ai.core.messages import RerankRequest
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAIEmbeddingHandler,
OpenAIRerankHandler,
OpenAITextHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class GLMRerankHandler(OpenAIRerankHandler):
@@ -56,7 +56,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -66,7 +68,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser,
ToolSerializer,
)
from zhenxun.services.log import logger
class GeminiConfigMapper(ConfigMapper):
@@ -121,22 +122,19 @@ class GeminiConfigMapper(ConfigMapper):
fc_config["allowedFunctionNames"] = user_funcs
params["toolConfig"] = {"functionCallingConfig": fc_config}
has_effort = bool(
config.common.reasoning_effort
and str(config.common.reasoning_effort).lower() != "none"
)
if (
has_effort
and capabilities
and capabilities.reasoning_mode == ReasoningMode.LEVEL
):
thinking_config = params.setdefault("thinkingConfig", {})
if config.common.reasoning_effort and capabilities:
effort = str(config.common.reasoning_effort).lower()
if capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort)
thinking_config["thinkingLevel"] = effort
if capabilities.reasoning_mode == ReasoningMode.LEVEL and effort != "none":
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingLevel"] = effort
elif (
capabilities.reasoning_mode == ReasoningMode.BUDGET and effort == "none"
):
thinking_config = params.setdefault("thinkingConfig", {})
thinking_config["thinkingBudget"] = 0
if config.gemini_options.include_thoughts is not None:
thinking_config = params.setdefault("thinkingConfig", {})
@@ -829,10 +827,6 @@ class GeminiEmbeddingHandler(BaseEmbeddingHandler):
url = f"{base_url}/v1beta/{api_model_name}:batchEmbedContents"
headers = adapter.get_base_headers(api_key)
from zhenxun.services.ai.llm.adapters.handlers.gemini_handlers import (
GeminiMessageConverter,
)
converter = GeminiMessageConverter()
requests_payload = []
@@ -16,13 +16,13 @@ from zhenxun.services.ai.core.messages import (
)
from zhenxun.services.ai.core.models import (
ModelCapabilities,
ModelDetail,
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig
from zhenxun.services.ai.core.options import TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .base import BaseAudioHandler
from .openai_handlers import (
OpenAIConfigMapper,
OpenAIMessageConverter,
OpenAITextHandler,
@@ -53,35 +53,6 @@ class MiMoToolSerializer(OpenAIToolSerializer):
return res
class MiMoConfigMapper(OpenAIConfigMapper):
"""MiMo 配置映射器,处理深度思考参数差异"""
def map_config(
self,
config: GenerationConfig,
model_detail: ModelDetail | None = None,
capabilities: ModelCapabilities | None = None,
) -> dict[str, Any]:
params = super().map_config(config, model_detail, capabilities)
if config.common.reasoning_effort:
effort = str(config.common.reasoning_effort).lower()
if effort == "none":
params["thinking"] = {"type": "disabled"}
else:
params["thinking"] = {"type": "enabled"}
elif (
hasattr(config, "deepseek_options")
and config.deepseek_options.thinking is not None
):
if config.deepseek_options.thinking is True:
params["thinking"] = {"type": "enabled"}
elif config.deepseek_options.thinking is False:
params["thinking"] = {"type": "disabled"}
return params
class MiMoMessageConverter(OpenAIMessageConverter):
"""MiMo 消息转换器,拦截处理特有的音视频多模态结构"""
@@ -141,7 +112,7 @@ class MiMoTextHandler(OpenAITextHandler):
super().__init__(api_type=api_type)
self.converter = MiMoMessageConverter(api_type=api_type)
self.serializer = MiMoToolSerializer(api_type=api_type)
self.mapper = MiMoConfigMapper(api_type=api_type)
self.mapper = OpenAIConfigMapper(api_type=api_type)
class MiMoAudioHandler(BaseAudioHandler):
@@ -53,7 +53,9 @@ from zhenxun.services.ai.llm.adapters.base import (
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import (
BaseAudioHandler,
BaseEmbeddingHandler,
BaseImageHandler,
@@ -64,7 +66,6 @@ from zhenxun.services.ai.llm.adapters.handlers.base import (
ResponseParser,
ToolSerializer,
)
from zhenxun.services.log import logger
class OpenAIConfigMapper(ConfigMapper):
@@ -114,7 +115,13 @@ class OpenAIConfigMapper(ConfigMapper):
if capabilities and capabilities.reasoning_effort_map:
effort = capabilities.reasoning_effort_map.get(effort, effort)
if effort != "none":
if (
effort == "none"
and capabilities
and capabilities.supports_thinking_toggle
):
params["thinking"] = {"type": "disabled"}
else:
params["reasoning_effort"] = effort
if isinstance(config.output.response_format, dict):
@@ -1165,45 +1172,3 @@ class OpenAIAudioHandler(BaseAudioHandler):
usage=UsageInfo(),
model_name=identity.model_name,
)
class CompositeOpenAITextHandler(BaseTextHandler):
"""
OpenAI 复合文本对话处理器 (Composite Pattern)。
内部包装标准协议与 responses 协议 Handler,根据模型配置在请求时动态路由
"""
def __init__(self, api_type: str = "openai"):
self.api_type = api_type
self._standard_handler = OpenAITextHandler(api_type=api_type)
self._responses_handler = OpenAIResponsesTextHandler(
api_type="openai_responses"
)
def _get_active_handler(self, identity: ModelIdentity) -> BaseTextHandler:
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return self._responses_handler
return self._standard_handler
async def prepare_text_request(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
api_key: str,
request: ChatRequest,
) -> RequestData:
handler = self._get_active_handler(identity)
return await handler.prepare_text_request(adapter, identity, api_key, request)
def parse_text_response(
self,
adapter: BaseAdapter,
identity: ModelIdentity,
response_json: dict[str, Any],
is_advanced: bool = False,
) -> ResponseData:
handler = self._get_active_handler(identity)
return handler.parse_text_response(
adapter, identity, response_json, is_advanced
)
+5 -4
View File
@@ -1,12 +1,14 @@
from zhenxun.services.ai.core.messages import EmbeddingRequest
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.core.options import LLMEmbeddingConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from zhenxun.services.ai.utils.logger import log_llm as logger
from .base import BaseAdapter, RequestData
from .handlers.openai_handlers import (
OpenAIEmbeddingHandler,
OpenAIRerankHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
@@ -36,7 +38,6 @@ class JinaEmbeddingHandler(OpenAIEmbeddingHandler):
TextPart,
VideoPart,
)
from zhenxun.services.log import logger
for payload in batch.payloads:
jina_content = []
+4 -3
View File
@@ -1,12 +1,13 @@
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.handlers.mimo_handlers import (
from .handlers.mimo_handlers import (
MiMoAudioHandler,
MiMoTextHandler,
)
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
from .handlers.openai_handlers import (
OpenAIImageHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAICompatAdapter
from .openai import OpenAICompatAdapter
class MiMoAdapter(OpenAICompatAdapter):
+2 -2
View File
@@ -19,9 +19,9 @@ from zhenxun.services.ai.core.models import (
ModelIdentity,
)
from zhenxun.services.ai.core.options import GenerationConfig, TTSConfig
from zhenxun.services.ai.llm.adapters.base import BaseAdapter, RequestData
from zhenxun.services.ai.llm.adapters.handlers.base import BaseAudioHandler
from .base import BaseAdapter, RequestData
from .handlers.base import BaseAudioHandler
from .handlers.openai_handlers import (
OpenAIConfigMapper,
OpenAIMessageConverter,
+29 -14
View File
@@ -15,11 +15,12 @@ from .base import (
RequestData,
)
from .handlers.openai_handlers import (
CompositeOpenAITextHandler,
OpenAIAudioHandler,
OpenAIEmbeddingHandler,
OpenAIImageHandler,
OpenAIRerankHandler,
OpenAIResponsesTextHandler,
OpenAITextHandler,
)
@@ -83,12 +84,12 @@ class OpenAICompatAdapter(BaseAdapter):
class OpenAIAdapter(OpenAICompatAdapter):
"""OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。"""
"""标准 OpenAI 系列适配器,统一装配文本/图像/嵌入/重排处理链。"""
def __init__(self):
"""初始化并挂载复合文本处理器与通用多模态处理器。"""
"""初始化并挂载标准文本处理器与通用多模态处理器。"""
super().__init__()
self.text_handler = CompositeOpenAITextHandler(api_type=self.api_type)
self.text_handler = OpenAITextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler()
@@ -102,21 +103,35 @@ class OpenAIAdapter(OpenAICompatAdapter):
@property
def supported_api_types(self) -> list[str]:
"""支持的 API 类型及别名。"""
return [
"openai",
"openai_responses",
]
return ["openai"]
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
"""返回聊天完成端点"""
current_api_type = identity.api_type
if current_api_type == "openai_responses":
return "/v1/responses"
if current_api_type == "doubao":
return "/api/v3/chat/completions"
return "/v1/chat/completions"
def get_embedding_endpoint(self, identity: ModelIdentity) -> str:
"""返回嵌入端点。"""
return "/v1/embeddings"
class OpenAIResponsesAdapter(OpenAICompatAdapter):
"""OpenAI v1/responses 协议专用适配器"""
def __init__(self):
super().__init__()
self.text_handler = OpenAIResponsesTextHandler(api_type=self.api_type)
self.image_handler = OpenAIImageHandler()
self.embedding_handler = OpenAIEmbeddingHandler()
self.rerank_handler = OpenAIRerankHandler()
self.audio_handler = OpenAIAudioHandler()
@property
def api_type(self) -> str:
return "openai_responses"
@property
def supported_api_types(self) -> list[str]:
return ["openai_responses"]
def get_chat_endpoint(self, identity: ModelIdentity) -> str:
return "/v1/responses"
@@ -10,18 +10,19 @@ from zhenxun.services.ai.core.messages import (
ThoughtPart,
)
from zhenxun.services.ai.core.models import ModelIdentity
from zhenxun.services.ai.llm.adapters.base import (
from .base import (
BaseAdapter,
RequestData,
ResponseData,
process_image_data,
)
from zhenxun.services.ai.llm.adapters.handlers.base import BaseImageHandler
from zhenxun.services.ai.llm.adapters.handlers.openai_handlers import (
CompositeOpenAITextHandler,
from .handlers.base import BaseImageHandler
from .handlers.openai_handlers import (
OpenAIMessageConverter,
OpenAITextHandler,
)
from zhenxun.services.ai.llm.adapters.openai import OpenAIAdapter
from .openai import OpenAIAdapter
class OpenRouterMessageConverter(OpenAIMessageConverter):
@@ -60,12 +61,12 @@ class OpenRouterMessageConverter(OpenAIMessageConverter):
return openai_messages
class OpenRouterTextHandler(CompositeOpenAITextHandler):
class OpenRouterTextHandler(OpenAITextHandler):
"""OpenRouter 专有文本处理器,挂载专有 Converter"""
def __init__(self, api_type: str = "openrouter"):
super().__init__(api_type=api_type)
self._standard_handler.converter = OpenRouterMessageConverter(api_type=api_type)
self.converter = OpenRouterMessageConverter(api_type=api_type)
class OpenRouterImageHandler(BaseImageHandler):
+2 -2
View File
@@ -35,10 +35,10 @@ from zhenxun.services.ai.core.options import (
TTSConfig,
)
from zhenxun.services.ai.guardrails import GuardrailSource
from zhenxun.services.ai.llm.engine.router import LLMOrchestrator
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from .builder import IntentBuilder
from .engine.router import LLMOrchestrator
T = TypeVar("T", bound=BaseModel)
+1 -20
View File
@@ -10,7 +10,7 @@ from zhenxun.services.ai.core.options import (
GenerationConfig,
ResponseFormat,
)
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.pydantic_compat import model_json_schema, model_validate
@@ -43,18 +43,6 @@ class OpenAIIntentNamespace:
return self._builder
class DeepSeekIntentNamespace:
"""DeepSeek 专属高级参数构建域"""
def __init__(self, builder: "IntentBuilder"):
self._builder = builder
def disable_thinking(self) -> "IntentBuilder":
"""显式关闭 DeepSeek 的思维链"""
self._builder._config.deepseek_options.thinking = False
return self._builder
class IntentBuilder:
"""
基于能力意图声明的构建器 (Intent-Driven Builder)。
@@ -72,10 +60,6 @@ class IntentBuilder:
def openai(self) -> OpenAIIntentNamespace:
return OpenAIIntentNamespace(self)
@property
def deepseek(self) -> DeepSeekIntentNamespace:
return DeepSeekIntentNamespace(self)
def with_reasoning(self, level: str | None = None) -> Self:
"""
跨厂商统一的思考/推理等级意图声明。
@@ -85,9 +69,6 @@ class IntentBuilder:
self._config.common.reasoning_effort = level
if level.lower() != "none":
self._config.gemini_options.include_thoughts = True
self._config.deepseek_options.thinking = True
else:
self._config.deepseek_options.thinking = False
return self
def with_local_cache(self, ttl: int = 3600) -> Self:
@@ -51,7 +51,7 @@ from zhenxun.services.ai.llm.system.network import (
HealthManager,
LLMHttpClient,
)
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.log_sanitizer import sanitize_for_logging
from zhenxun.utils.pydantic_compat import (
@@ -136,7 +136,7 @@ class LLMCacheMiddleware:
if cached_data is not None:
logger.debug(
f"⚡ [LLMCache] 命中本地极速缓存 - "
f"命中本地极速缓存 - "
f"model: {self.model_name}, type: {type(context.request).__name__}"
)
+7 -10
View File
@@ -16,7 +16,7 @@ from zhenxun.services.ai.llm.manager import (
)
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
class BaseModelRouter(ABC):
@@ -57,7 +57,7 @@ class FallbackRouter(BaseModelRouter):
m_name = model_names[idx]
if not health_manager.is_route_healthy(m_name, strict_mode=is_routed_call):
logger.debug(f"👉 [Orchestrator] 节点 '{m_name}' 熔断中,已跳过")
logger.debug(f"👉 节点 '{m_name}' 熔断中,已跳过")
errors.append(f"{m_name}(熔断中)")
continue
@@ -70,7 +70,7 @@ class FallbackRouter(BaseModelRouter):
try:
if len(model_names) > 1:
if idx != start_idx:
logger.debug(f"🔄 [Orchestrator] 切换至备用节点: '{m_name}'...")
logger.debug(f"🔄 切换至备用节点: '{m_name}'...")
async with await get_model_instance(
m_name, override_config, task=task
@@ -83,26 +83,23 @@ class FallbackRouter(BaseModelRouter):
except LLMException as e:
if not e.should_failover:
logger.warning(
f"🚫 [Orchestrator] 节点 '{m_name}' "
f"🚫 节点 '{m_name}' "
f"返回不可恢复错误 ({e.__class__.__name__}),停止故障转移。"
)
raise e
logger.warning(
f"⚠️ [Orchestrator] 节点 '{m_name}' "
f"⚠️ 节点 '{m_name}' "
f"错误 ({e.__class__.__name__}),触发故障转移..."
)
errors.append(f"{m_name}({e.__class__.__name__})")
except Exception as e:
logger.warning(
f"⚠️ [Orchestrator] 节点 '{m_name}' 发生未知异常,触发故障转移: {e}"
)
logger.warning(f"⚠️ 节点 '{m_name}' 发生未知异常,触发故障转移: {e}")
errors.append(f"{m_name}(Error)")
if all_nodes_bypassed and len(model_names) > 1:
fallback_model = health_manager.get_best_fallback_route(model_names)
logger.warning(
f"⚠️ [Orchestrator] 路由组所有节点均已宕机!"
f"强制放行 '{fallback_model}' 探活..."
f"⚠️ 路由组所有节点均已宕机!" f"强制放行 '{fallback_model}' 探活..."
)
try:
async with await get_model_instance(
+15 -19
View File
@@ -43,9 +43,22 @@ from zhenxun.services.ai.core.protocols.llm import (
SupportsTextEmbedding,
)
from zhenxun.services.ai.core.protocols.middleware import LLMMiddleware
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.system.models import RetryConfig
from zhenxun.services.ai.llm.system.network import HealthManager, LLMHttpClient
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from .middlewares import (
ConfigMergeMiddleware,
FailoverAndRetryMiddleware,
HttpExecutionMiddleware,
LLMCacheMiddleware,
LoggingMiddleware,
MiddlewarePipeline,
ModalityFilterMiddleware,
OutputValidationMiddleware,
ResponseRescueMiddleware,
)
T = TypeVar("T", bound=BaseModel)
@@ -86,7 +99,7 @@ class LLMModel(
)
self.model_name = model_detail.model_name
self.temperature = model_detail.temperature
self.generation_max_tokens = model_detail.generation_max_tokens
self.max_output_tokens = model_detail.max_output_tokens
self._is_closed = False
self._ref_count = 0
@@ -100,8 +113,6 @@ class LLMModel(
generation_config=self._generation_config,
)
from zhenxun.services.ai.llm.engine.middlewares import MiddlewarePipeline
self.pipeline = MiddlewarePipeline()
self._setup_default_pipeline()
@@ -110,17 +121,6 @@ class LLMModel(
self.pipeline.add_middleware(middleware)
def _setup_default_pipeline(self) -> None:
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import (
ConfigMergeMiddleware,
FailoverAndRetryMiddleware,
LLMCacheMiddleware,
LoggingMiddleware,
ModalityFilterMiddleware,
OutputValidationMiddleware,
ResponseRescueMiddleware,
)
client_settings = get_llm_config().client_settings
retry_config = RetryConfig(
max_retries=client_settings.max_retries,
@@ -216,10 +216,6 @@ class LLMModel(
request=request,
cancellation_token=cancellation_token,
)
from zhenxun.services.ai.llm.adapters.factory import get_adapter_for_api_type
from zhenxun.services.ai.llm.engine.middlewares import HttpExecutionMiddleware
adapter = get_adapter_for_api_type(self.api_type)
execution_middleware = HttpExecutionMiddleware(
http_client=self.http_client,
+8 -6
View File
@@ -13,12 +13,13 @@ from zhenxun.services.ai.config import (
from zhenxun.services.ai.core.exceptions import ConfigurationException
from zhenxun.services.ai.core.models import ModelDetail
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.pydantic_compat import model_dump
from .system.capabilities import get_model_capabilities
from .system.network import health_manager
_RESOLVED_GROUP_CACHE: dict[str, list[str]] = {}
"""路由组解析缓存,避免每次调用重复打印剔除警告并提升性能"""
@@ -276,7 +277,7 @@ async def get_model_instance(
provider_config_found, model_detail_found = config_tuple_found
from zhenxun.services.ai.llm.system.cache import get_or_create_model
from .system.cache import get_or_create_model
return await get_or_create_model(
provider_config_found, model_detail_found, override_config
@@ -287,7 +288,7 @@ def clear_all_cache() -> None:
"""
清空模型实例缓存与路由组解析缓存。
"""
from zhenxun.services.ai.llm.system.cache import clear_model_cache
from .system.cache import clear_model_cache
clear_model_cache()
clear_resolved_group_cache()
@@ -300,9 +301,10 @@ async def _init_llm_config_on_startup():
logger.info("正在初始化 LLM 配置并加载遥测状态...")
try:
from zhenxun.services.ai.config import get_llm_config
from zhenxun.services.ai.llm.system.network import health_manager
from zhenxun.services.ai.tools.engine.registry import tool_provider_manager
from .system.network import health_manager
get_llm_config()
await health_manager.initialize()
await tool_provider_manager.initialize()
+28 -7
View File
@@ -8,11 +8,12 @@ from zhenxun.services.ai.core.models import ModelDetail, ModelModality
from zhenxun.services.ai.core.options import GenerationConfig
from zhenxun.services.ai.llm.builder import validate_override_params
from zhenxun.services.ai.llm.engine.service import LLMModel
from zhenxun.services.ai.llm.system.capabilities import get_model_capabilities
from zhenxun.services.ai.llm.system.network import health_manager, http_client_manager
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.pydantic_compat import dump_json_safely, model_copy, model_dump
from .capabilities import get_model_capabilities
from .network import health_manager, http_client_manager
_model_cache: dict[str, tuple[LLMModel, float]] = {}
_cache_ttl = 3600
_max_cache_size = 10
@@ -111,6 +112,22 @@ async def get_or_create_model(
capabilities = get_model_capabilities(model_detail_found.model_name)
capabilities = model_copy(capabilities, deep=True)
if model_detail_found.max_input_tokens is not None:
capabilities.max_input_tokens = model_detail_found.max_input_tokens
base_gen_config = GenerationConfig()
if provider_config_found.temperature is not None:
base_gen_config.common.temperature = provider_config_found.temperature
if provider_config_found.max_output_tokens is not None:
base_gen_config.common.max_tokens = provider_config_found.max_output_tokens
if model_detail_found.temperature is not None:
base_gen_config.common.temperature = model_detail_found.temperature
if model_detail_found.max_output_tokens is not None:
base_gen_config.common.max_tokens = model_detail_found.max_output_tokens
if model_detail_found.reasoning_effort is not None:
base_gen_config.common.reasoning_effort = model_detail_found.reasoning_effort
if model_detail_found.task_type == "image_generation":
capabilities.output_modalities.add(ModelModality.IMAGE)
capabilities.supports_tool_calling = False
@@ -130,9 +147,8 @@ async def get_or_create_model(
timeout=default_timeout,
api_base=provider_config_found.api_base,
api_type=provider_config_found.api_type,
openai_compat=provider_config_found.openai_compat,
temperature=provider_config_found.temperature,
generation_max_tokens=provider_config_found.generation_max_tokens,
max_output_tokens=provider_config_found.max_output_tokens,
)
shared_http_client = await http_client_manager.get_client(config_for_http_client)
@@ -144,16 +160,21 @@ async def get_or_create_model(
health_manager=health_manager,
http_client=shared_http_client,
capabilities=capabilities,
config_override=base_gen_config,
)
if override_config:
validated_override_params = validate_override_params(override_config)
model_instance._generation_config = validated_override_params
model_instance.identity.generation_config = validated_override_params
final_config = base_gen_config.merge_with(validated_override_params)
model_instance._generation_config = final_config
model_instance.identity.generation_config = final_config
logger.debug(
f"为新模型 {prov_name_str}/{mod_name_str} 应用配置覆盖: "
f"{_get_clean_log_config(validated_override_params)}"
)
else:
model_instance._generation_config = base_gen_config
model_instance.identity.generation_config = base_gen_config
_cache_model(cache_key, model_instance)
logger.debug(
+14 -1
View File
@@ -125,6 +125,7 @@ CAP_DEEPSEEK_V4 = ModelCapabilities(
input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT,
reasoning_visibility="visible",
reasoning_effort_map={"minimal": "low"},
@@ -133,6 +134,7 @@ CAP_MINIMAX_REASONING = ModelCapabilities(
input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT,
reasoning_visibility="visible",
)
@@ -146,6 +148,15 @@ CAP_GLM_MULTIMODAL = ModelCapabilities(
},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
)
CAP_GLM_REASONING = ModelCapabilities(
input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
reasoning_mode=ReasoningMode.EFFORT,
)
CAP_MINIMAX_MULTIMODAL = ModelCapabilities(
@@ -158,6 +169,7 @@ CAP_MIMO_TEXT = ModelCapabilities(
input_modalities={ModelModality.TEXT},
output_modalities={ModelModality.TEXT},
supports_tool_calling=True,
supports_thinking_toggle=True,
supported_native_tools={"web_search"},
reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"},
)
@@ -171,6 +183,7 @@ CAP_MIMO_MULTIMODAL = ModelCapabilities(
},
output_modalities={ModelModality.TEXT, ModelModality.AUDIO},
supports_tool_calling=True,
supports_thinking_toggle=True,
supported_native_tools={"web_search"},
reasoning_effort_map={"max": "high", "xhigh": "high", "minimal": "low"},
)
@@ -289,7 +302,7 @@ _ROUTING_TABLE: list[tuple[list[str], ModelCapabilities, int]] = [
CTX_256K,
),
(["glm-5v*"], CAP_GLM_MULTIMODAL, CTX_200K),
(["glm-5*", "glm-4.7*", "glm-4.6*"], STANDARD_TEXT_TOOL_CAPABILITIES, CTX_200K),
(["glm-5*", "glm-4.7*", "glm-4.6*"], CAP_GLM_REASONING, CTX_200K),
(["*MiniMax-M2*", "*minimax-m2*"], CAP_MINIMAX_REASONING, CTX_200K),
(["gpt-4*", "gpt-3.5*", "gpt-*"], CAP_OPENAI_MULTIMODAL, CTX_128K),
(["o1-*", "o3-*"], CAP_OPENAI_REASONING, CTX_128K),
+1 -1
View File
@@ -25,7 +25,7 @@ from zhenxun.services.ai.core.exceptions import (
QuotaExceededException,
RateLimitException,
)
from zhenxun.services.log import logger
from zhenxun.services.ai.utils.logger import log_llm as logger
from zhenxun.utils.pydantic_compat import model_dump, parse_as
from zhenxun.utils.user_agent import get_user_agent