From d24066369b977a7a1447d573b5d996b6d7ca4d94 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Mon, 3 Aug 2026 23:03:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(aibox):=20=E8=A7=A3=E8=80=A6=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=B0=83=E7=94=A8=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20llm=5Fclient=20=E6=8A=BD=E8=B1=A1=E5=B1=82=E4=BB=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E5=8E=82=E5=95=86=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 think_and_respond 与 _compress_via_llm 中的直接 DeepSeek API 调用 收敛到 LLMClient 抽象层(DeepSeekClient 实现 + create_llm_client 工厂), 统一流式事件格式,为子代理/多模型切换预留扩展点;配置新增 api.provider。 --- aibox/README.md | 3 + aibox/README.robot.md | 1 + aibox/aibox.py | 278 ++++++++++++++---------------------------- aibox/llm_client.py | 265 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 363 insertions(+), 184 deletions(-) create mode 100644 aibox/llm_client.py diff --git a/aibox/README.md b/aibox/README.md index 6a71a1b..f60beb5 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -437,6 +437,7 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i "broadcast_enabled": true }, "api": { + "provider": "deepseek", "deepseek_api_url": "https://api.deepseek.com/chat/completions", "deepseek_model": "deepseek-v4-flash", "reasoning_enabled": false, @@ -508,6 +509,8 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i > 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。设置 `api.reasoning_enabled: true` 可启用思考模式,此时会在请求中添加 `thinking: {type: "enabled"}` 参数,模型会先进行思考再回答,实时展示思考过程(灰色斜体区域),回答质量更高且**支持工具调用**(文件操作、命令执行、记忆搜索等工具均可在思考模式下正常使用)。 > > API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。 +> +> **多厂商切换**:大模型调用已通过 `aibox/llm_client.py` 抽象层解耦,业务层不再直接依赖具体厂商。配置 `api.provider`(默认 `deepseek`)即可切换厂商,当前内置 `DeepSeekClient`(兼容 OpenAI Chat Completions 协议,Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。新增厂商只需在此文件新增一个 `LLMClient` 子类并在 `create_llm_client` 工厂中注册,无需改动业务代码,为后续子代理/多模型共发预留了扩展点。 ## 🛠️ 命令行参数 diff --git a/aibox/README.robot.md b/aibox/README.robot.md index 278cece..f0d90e2 100644 --- a/aibox/README.robot.md +++ b/aibox/README.robot.md @@ -507,6 +507,7 @@ CREATE TABLE memos ( | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| +| `provider` | str | `deepseek` | 模型厂商,用于 `llm_client` 工厂路由 | | `deepseek_api_url` | str | `https://api.deepseek.com/chat/completions` | API URL | | `deepseek_model` | str | `deepseek-v4-flash` | 模型名 | | `api_key_env` | str | `DEEPSEEK_API_KEY` | API Key 环境变量名 | diff --git a/aibox/aibox.py b/aibox/aibox.py index 55fe75e..bc6f710 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -35,6 +35,9 @@ from logging.handlers import RotatingFileHandler import requests +# 大模型客户端抽象层(支持多厂商切换) +from llm_client import create_llm_client + # ==================== 语言检测和多语言支持 ==================== @@ -2013,7 +2016,7 @@ class ConversationCompressor: return content def _compress_via_llm(self, messages: List[Dict]) -> Optional[str]: - """通过语言模型压缩消息""" + """通过语言模型压缩消息(经由客户端抽象层调用,支持多厂商)""" if not messages: return None @@ -2022,36 +2025,15 @@ class ConversationCompressor: print(f"📦 {i18n.get('conversation_compress')}: 正在压缩 {len(messages)} 条消息...") try: - headers = { - "Authorization": f"Bearer {self.ai.api_key}", - "Content-Type": "application/json" - } - - payload = { - "model": constants.DEEPSEEK_MODEL, - "messages": [ + summary = self.ai.client.chat_completion( + [ {"role": "system", "content": "你是一个专业的对话摘要助手,擅长提取对话核心信息并进行结构化总结。请保留工具调用的关键结果。"}, {"role": "user", "content": prompt} ], - "temperature": 0.3, - "max_tokens": constants.COMPRESSOR_LLM_MAX_TOKENS - } - - response = requests.post( - constants.DEEPSEEK_API_URL, - headers=headers, - json=payload, - timeout=30 + temperature=0.3, + max_tokens=constants.COMPRESSOR_LLM_MAX_TOKENS ) - - if response.status_code == 200: - result = response.json() - summary = result["choices"][0]["message"]["content"].strip() - return summary - else: - print(f" ❌ 语言模型压缩失败: HTTP {response.status_code}") - return None - + return summary except Exception as e: self.logger.error(f"语言模型压缩异常: {e}") return None @@ -6580,10 +6562,13 @@ class DeepSeekChat: history_manager: ConversationHistory = None, memory_db: MemoryDatabase = None, memo_db: MemoDatabase = None, command_executor: CommandExecutor = None, prompt_manager: PromptManager = None): - self.name = "DeepSeek" - self.debug_level = debug_level if debug_level is not None else Logger("").level - self.logger = Logger(self.name, self.debug_level) self.config = ConfigManager() + self.logger = Logger("DeepSeek", debug_level if debug_level is not None else Logger("").level) + self.debug_level = self.logger.level + # 大模型客户端(抽象层,支持多厂商切换) + self.client = create_llm_client(self.config, api_key=api_key, logger=self.logger) + self.api_key = self.client.api_key + self.name = self.client.name self.history_manager = history_manager self.memory_db = memory_db self.memo_db = memo_db @@ -6595,24 +6580,12 @@ class DeepSeekChat: # 添加对话压缩器 self.compressor = ConversationCompressor(self, self.logger) - api_key_env = self.config.get("api.api_key_env") - self.api_key = api_key or os.environ.get(api_key_env) - if not self.api_key: - try: - home = os.environ.get("HOME", os.environ["USERPROFILE"]) - key_path = os.path.join(home, ".cert", "deepseek.key") - with open(f"{key_path}", "r") as f: - self.api_key = f.read().strip() - except Exception as e: - raise RuntimeError(f"请设置环境变量 {api_key_env} 或在配置文件中设置API Key") - self.max_history = max_history or constants.DEFAULT_MAX_HISTORY self.preserve_system = constants.PRESERVE_SYSTEM_PROMPTS self.stats = {"api_calls": 0, "tool_calls": 0, "errors": []} - # 取消机制:threading.Event 用于跨线程信号,_current_response 用于中断HTTP请求 + # 取消机制:threading.Event 用于跨线程信号,HTTP 中断由客户端抽象层负责 self._cancel_event = threading.Event() - self._current_response = None self._command_executor = command_executor # 保存命令执行器引用,用于取消时kill子进程 self._init_system_prompts(system_prompt) @@ -6624,19 +6597,10 @@ class DeepSeekChat: """中断当前正在进行的AI思考/工具执行。线程安全,可从任意线程调用。""" self.logger.info("收到中断信号,正在取消当前操作...") self._cancel_event.set() - # 强制关闭当前HTTP响应,立即中断 iter_lines() 的阻塞读取 - # 仅调用 response.close() 可能无法中断底层socket recv(),需要先关闭raw连接 - if self._current_response is not None: + # 关闭当前流式HTTP响应,立即中断阻塞读取(由客户端抽象层负责) + if self.client is not None: try: - resp = self._current_response - self._current_response = None - # 先关闭底层raw socket连接,强制阻塞的recv()立即抛出异常 - if hasattr(resp, 'raw') and resp.raw is not None: - try: - resp.raw.close() - except Exception: - pass - resp.close() + self.client.cancel() except Exception: pass # 终止正在运行的子进程 @@ -7039,11 +7003,6 @@ class DeepSeekChat: if self.history_manager: self.history_manager.add_message("user", enhanced_input, "用户") - headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - # 构建消息列表,确保包含 reasoning_content enhanced_messages = [] @@ -7081,25 +7040,8 @@ class DeepSeekChat: else: enhanced_messages.append(msg) - payload = { - "model": constants.DEEPSEEK_MODEL, - "messages": enhanced_messages, - "temperature": constants.DEFAULT_TEMPERATURE, - "max_tokens": constants.DEFAULT_MAX_TOKENS, - "stream": True - } - - # 思考模式:启用 thinking 参数 - if constants.REASONING_ENABLED: - payload["thinking"] = { - "type": "enabled" - } - # 工具调用(v4-flash 支持思考+工具调用) - tools = self.tool_registry.get_tools_schemas() - if tools: - payload["tools"] = tools - payload["tool_choice"] = "auto" + tools = self.tool_registry.get_tools_schemas() or None current_time = datetime.now().strftime("%H:%M:%S") if output_callback and not is_continuation: @@ -7125,113 +7067,81 @@ class DeepSeekChat: MAX_CHUNK_DELAY = 0.1 try: - proxies = self.config.get_proxy() - response = requests.post( - constants.DEEPSEEK_API_URL, - headers=headers, - json=payload, - timeout=constants.DEFAULT_TIMEOUT, - stream=True, - proxies=proxies # 动态代理 - ) - self._current_response = response # 保存引用以便cancel()关闭连接 - - if response.status_code != 200: - error_msg = f"\n错误状态码: {response.status_code}\n错误响应: {response.text}" - if output_callback: - output_callback("error", error_msg) - response.raise_for_status() - - for line in response.iter_lines(): + # 通过客户端抽象层发起流式请求(统一事件格式,支持多厂商切换) + for event in self.client.stream_chat( + enhanced_messages, + temperature=constants.DEFAULT_TEMPERATURE, + max_tokens=constants.DEFAULT_MAX_TOKENS, + tools=tools, + reasoning_enabled=constants.REASONING_ENABLED, + ): # 流式循环中检查取消信号 if self._is_cancelled(): break - if line: - line = line.decode('utf-8') - if line.startswith('data: '): - data = line[6:] - if data == '[DONE]': - if chunk_buffer and output_callback: - output_callback("chunk", chunk_buffer) - chunk_buffer = "" - if output_callback: - output_callback("line", "\n") - break - - try: - chunk = json.loads(data) - choices = chunk.get('choices', []) - if not choices: - continue - - delta = choices[0].get('delta', {}) - - # 【关键】处理 reasoning_content(思考内容),仅当配置启用时才展示 - if constants.REASONING_ENABLED and 'reasoning_content' in delta and delta['reasoning_content']: - reasoning = delta['reasoning_content'] - full_reasoning += reasoning - - # 发送思考开始信号(仅第一次) - if not reasoning_started and output_callback: - reasoning_started = True - - # 实时发送思考内容给前端 - if output_callback: - output_callback("reasoning", reasoning) - - # 思考结束:当开始收到content且之前有reasoning时,发送结束信号 - if 'content' in delta and delta['content'] and reasoning_started and not reasoning_ended: - reasoning_ended = True - if output_callback: - output_callback("reasoning_end", "") - output_callback("line", i18n.get('assistant_prefix')) - - # 处理正常内容 - if 'content' in delta and delta['content']: - content = delta['content'] - chunk_buffer += content - full_response += content - - if (len(chunk_buffer) >= MIN_CHUNK_SIZE or - time.time() - last_send_time >= MAX_CHUNK_DELAY): - if output_callback: - output_callback("chunk", chunk_buffer) - chunk_buffer = "" - last_send_time = time.time() - - # 处理工具调用 - if 'tool_calls' in delta: - tool_calls = delta['tool_calls'] - for tc in tool_calls: - index = tc.get('index', 0) - - if index not in current_tool_calls: - # 确保 tool_call id 非空,空 id 会导致 API 400 错误污染对话历史 - tc_id = tc.get('id', '') or f"call_{self.stats['tool_calls']}_{index}_{int(time.time()*1000)}" - current_tool_calls[index] = { - 'id': tc_id, - 'type': 'function', - 'function': { - 'name': '', - 'arguments': '' - } - } - else: - # 流式传输中 id 可能分片到达,后续补充 - existing_id = current_tool_calls[index].get('id', '') - new_id = tc.get('id', '') - if new_id and (not existing_id or existing_id.startswith('call_')): - current_tool_calls[index]['id'] = new_id - - if 'function' in tc: - if 'name' in tc['function']: - current_tool_calls[index]['function']['name'] = tc['function']['name'] - if 'arguments' in tc['function']: - current_tool_calls[index]['function']['arguments'] += tc['function']['arguments'] - - except json.JSONDecodeError as e: - self.logger.error(f"JSON解析错误: {e}, 数据: {data}") - continue + + etype = event.get("type") + + if etype == "reasoning": + # 处理思考内容(仅当配置启用时才会收到该事件) + reasoning = event.get("text", "") + full_reasoning += reasoning + if not reasoning_started and output_callback: + reasoning_started = True + if output_callback: + output_callback("reasoning", reasoning) + + elif etype == "content": + # 思考结束:当开始收到content且之前有reasoning时,发送结束信号 + if reasoning_started and not reasoning_ended: + reasoning_ended = True + if output_callback: + output_callback("reasoning_end", "") + output_callback("line", i18n.get('assistant_prefix')) + content = event.get("text", "") + chunk_buffer += content + full_response += content + + if (len(chunk_buffer) >= MIN_CHUNK_SIZE or + time.time() - last_send_time >= MAX_CHUNK_DELAY): + if output_callback: + output_callback("chunk", chunk_buffer) + chunk_buffer = "" + last_send_time = time.time() + + elif etype == "tool_call": + # 处理工具调用(流式增量) + index = event.get("index", 0) + if index not in current_tool_calls: + # 确保 tool_call id 非空,空 id 会导致 API 400 错误污染对话历史 + tc_id = event.get("id") or f"call_{self.stats['tool_calls']}_{index}_{int(time.time()*1000)}" + current_tool_calls[index] = { + 'id': tc_id, + 'type': 'function', + 'function': { + 'name': '', + 'arguments': '' + } + } + else: + # 流式传输中 id 可能分片到达,后续补充 + existing_id = current_tool_calls[index].get('id', '') + new_id = event.get("id", "") + if new_id and (not existing_id or existing_id.startswith('call_')): + current_tool_calls[index]['id'] = new_id + + if event.get("name"): + current_tool_calls[index]['function']['name'] = event["name"] + if event.get("arguments"): + current_tool_calls[index]['function']['arguments'] += event["arguments"] + + elif etype == "done": + # 流结束:flush 剩余缓冲 + if chunk_buffer and output_callback: + output_callback("chunk", chunk_buffer) + chunk_buffer = "" + if output_callback: + output_callback("line", "\n") + break # 流式循环结束,检查是否被取消 if self._is_cancelled(): @@ -7373,8 +7283,8 @@ class DeepSeekChat: self.logger.error(f"未知错误,已回滚消息到位置 {msgs_before_turn}: {e}") return error_msg finally: - # 清除当前响应引用 - self._current_response = None + # 客户端抽象层在流结束时已自行清理响应引用 + pass def _rollback_messages(self, to_length: int): """回滚 self.messages 到指定长度,用于 API 失败时恢复对话状态。""" diff --git a/aibox/llm_client.py b/aibox/llm_client.py new file mode 100644 index 0000000..049b511 --- /dev/null +++ b/aibox/llm_client.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +""" +大模型客户端抽象层 + +为 aibox 提供统一的 LLM 调用接口,将「调用哪个厂商 / 哪个模型」与业务逻辑解耦。 +业务层只依赖 `LLMClient` 抽象方法与统一的流式事件格式,切换厂商只需: + 1. 新增一个 `LLMClient` 子类实现; + 2. 在 `create_llm_client` 工厂里按 `api.provider` 进行路由。 + +当前内置实现: + - `DeepSeekClient`:兼容 OpenAI Chat Completions 协议(DeepSeek / Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。 + +统一流式事件格式(`stream_chat` 逐条 yield 的 dict): + {"type": "content", "text": str} # 增量正文 + {"type": "reasoning", "text": str} # 增量思考内容(可选,厂商支持时) + {"type": "tool_call", "index": int, "id": str, "name": str, "arguments": str} # 工具调用增量 + {"type": "done"} # 流结束 +""" + +from abc import ABC, abstractmethod +from typing import Dict, Iterator, List, Optional + +import json +import os +import requests + + +class LLMClient(ABC): + """大模型客户端抽象基类。子类需实现流式/非流式两种调用,并归一化为统一事件格式。""" + + def __init__(self, api_key: str, model: str, base_url: str, + timeout: int, config=None, logger=None): + self.api_key = api_key + self.model = model + self.base_url = base_url + self.timeout = timeout + self.config = config # 可选,用于读取代理等动态配置 + self.logger = logger # 可选,用于日志输出 + # 当前活动的 HTTP 响应引用,供 cancel() 强制关闭底层连接以中断阻塞读取 + self._current_response = None + + @property + def name(self) -> str: + """厂商/模型展示名,业务层可用于上下文(如 history 里的 assistant 名)。""" + return "LLM" + + # ---------- 工具方法 ---------- + + def _resolve_proxies(self) -> Optional[Dict[str, str]]: + """从 config 解析 requests 可用的 proxies 字典(动态代理)。""" + if self.config is not None and hasattr(self.config, "get_proxy"): + return self.config.get_proxy() + return None + + def _log(self, level: str, msg: str): + if self.logger is not None and hasattr(self.logger, level): + try: + getattr(self.logger, level)(msg) + except Exception: + pass + + def cancel(self): + """中断当前流式请求。线程安全,可从任意线程调用。""" + resp = self._current_response + self._current_response = None + if resp is not None: + try: + # 先关闭底层 raw socket,强制阻塞的 recv() 立即抛出异常 + if getattr(resp, "raw", None) is not None: + resp.raw.close() + except Exception: + pass + try: + resp.close() + except Exception: + pass + + # ---------- 抽象接口 ---------- + + @abstractmethod + def chat_completion(self, messages: List[Dict], temperature: float = 0.3, + max_tokens: int = 1024, **kwargs) -> str: + """非流式对话,返回完整文本。用于对话压缩等离线摘要场景。""" + + @abstractmethod + def stream_chat(self, messages: List[Dict], temperature: float = 0.7, + max_tokens: int = 8192, tools: Optional[List[Dict]] = None, + tool_choice: Optional[str] = None, + reasoning_enabled: bool = False, **extra) -> Iterator[Dict]: + """流式对话,逐条 yield 统一事件格式的 dict。业务层据此驱动输出/工具调用。""" + + +class DeepSeekClient(LLMClient): + """DeepSeek 客户端(OpenAI Chat Completions 协议兼容)。 + + 思考模式通过 `thinking: {"type": "enabled"}` 参数控制,思考内容字段为 + `delta.reasoning_content`。二者均为 DeepSeek 特色,其他 OpenAI 兼容厂商 + 若字段不同,可在子类中覆写 `_parse_delta` 归一化。 + """ + + def __init__(self, api_key: str, model: str, base_url: str, + timeout: int = 30, config=None, logger=None): + super().__init__(api_key, model, base_url, timeout, config, logger) + + @property + def name(self) -> str: + return "DeepSeek" + + def _headers(self) -> Dict[str, str]: + return { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + def chat_completion(self, messages: List[Dict], temperature: float = 0.3, + max_tokens: int = 1024, **kwargs) -> str: + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + } + payload.update(kwargs) + try: + response = requests.post( + self.base_url, + headers=self._headers(), + json=payload, + timeout=30, + proxies=self._resolve_proxies(), + ) + if response.status_code == 200: + result = response.json() + return result["choices"][0]["message"]["content"].strip() + self._log("error", f"LLM 压缩请求失败: HTTP {response.status_code}") + return None + except Exception as e: + self._log("error", f"LLM 压缩请求异常: {e}") + return None + + def stream_chat(self, messages: List[Dict], temperature: float = 0.7, + max_tokens: int = 8192, tools: Optional[List[Dict]] = None, + tool_choice: Optional[str] = None, + reasoning_enabled: bool = False, **extra) -> Iterator[Dict]: + payload = { + "model": self.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + "stream": True, + } + # 思考模式 + if reasoning_enabled: + payload["thinking"] = {"type": "enabled"} + # 工具调用 + if tools: + payload["tools"] = tools + payload["tool_choice"] = tool_choice or "auto" + payload.update(extra) + + proxies = self._resolve_proxies() + response = requests.post( + self.base_url, + headers=self._headers(), + json=payload, + timeout=self.timeout, + stream=True, + proxies=proxies, + ) + self._current_response = response + + if response.status_code != 200: + raise RuntimeError( + f"HTTP {response.status_code}: {response.text}") + + try: + for line in response.iter_lines(): + if not line: + continue + line_text = line.decode("utf-8") + if not line_text.startswith("data: "): + continue + data = line_text[6:] + if data == "[DONE]": + yield {"type": "done"} + break + try: + chunk = json.loads(data) + except json.JSONDecodeError as e: + self._log("error", f"JSON 解析错误: {e}, 数据: {data}") + continue + choices = chunk.get("choices", []) + if not choices: + continue + delta = choices[0].get("delta", {}) + for event in self._parse_delta(delta, reasoning_enabled): + yield event + finally: + self._current_response = None + + def _parse_delta(self, delta: Dict, reasoning_enabled: bool) -> Iterator[Dict]: + """将单条 delta 归一化为统一事件。子类覆写以适配不同厂商字段。""" + # 思考内容 + if reasoning_enabled and delta.get("reasoning_content"): + yield {"type": "reasoning", "text": delta["reasoning_content"]} + # 正文 + if delta.get("content"): + yield {"type": "content", "text": delta["content"]} + # 工具调用(增量) + for tc in delta.get("tool_calls", []): + index = tc.get("index", 0) + function = tc.get("function", {}) + yield { + "type": "tool_call", + "index": index, + "id": tc.get("id", ""), + "name": function.get("name", ""), + "arguments": function.get("arguments", ""), + } + + +def resolve_api_key(api_key: Optional[str], config=None) -> str: + """解析 API Key:优先显式传入,其次环境变量,最后 ~/.cert/deepseek.key。""" + if api_key: + return api_key + env_name = None + if config is not None and hasattr(config, "get"): + env_name = config.get("api.api_key_env") + env_name = env_name or "DEEPSEEK_API_KEY" + env_key = os.environ.get(env_name) + if env_key: + return env_key + try: + home = os.environ.get("HOME", os.environ["USERPROFILE"]) + key_path = os.path.join(home, ".cert", "deepseek.key") + with open(key_path, "r") as f: + return f.read().strip() + except Exception: + raise RuntimeError( + f"请设置环境变量 {env_name} 或在配置文件中设置 API Key") + + +def create_llm_client(config, api_key: Optional[str] = None, + logger=None) -> LLMClient: + """根据配置创建对应厂商的 LLM 客户端。可通过 `api.provider` 切换厂商。""" + provider = (config.get("api.provider", "deepseek") or "deepseek").lower() + base_url = config.get("api.deepseek_api_url") + model = config.get("api.deepseek_model", "deepseek-v4-flash") + timeout = config.get("api.timeout", 30) + + resolved_key = resolve_api_key(api_key, config) + + if provider in ("deepseek", "deepseek-chat"): + return DeepSeekClient( + api_key=resolved_key, + model=model, + base_url=base_url, + timeout=timeout, + config=config, + logger=logger, + ) + + # 其他 OpenAI 兼容厂商:可直接复用 DeepSeekClient(继承其协议解析), + # 或在此接入专有实现。新增厂商时在此扩展即可。 + raise ValueError(f"未知的模型提供商: {provider}") \ No newline at end of file