From e3919107abb95530ed81fa78eff65cbd49e5ded9 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Thu, 26 Mar 2026 16:50:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8E=BB=E9=99=A4Dashscope=20SDK?= =?UTF-8?q?=E5=92=8CZ.ai=20SDK=EF=BC=9B=E5=85=A8=E9=83=A8=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=BB=9F=E4=B8=80=E7=9A=84OpenAI=20SDK=EF=BC=8C?= =?UTF-8?q?=E4=B8=BAMCP=E7=9A=84=E8=B0=83=E7=94=A8=E9=A2=84=E7=95=99?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E3=80=82=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=9A=84=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/adapters/dashscope_adapter.py | 743 +++------------------------ server/adapters/glm_adapter.py | 365 ++----------- server/adapters/openai_adapter.py | 381 ++++---------- server/adapters/unified_adapter.py | 382 ++++++++++++++ server/requirements.txt | 164 +++--- start.sh | 4 +- 6 files changed, 673 insertions(+), 1366 deletions(-) create mode 100644 server/adapters/unified_adapter.py diff --git a/server/adapters/dashscope_adapter.py b/server/adapters/dashscope_adapter.py index 3bbdbf7..e3d4c2e 100644 --- a/server/adapters/dashscope_adapter.py +++ b/server/adapters/dashscope_adapter.py @@ -1,26 +1,19 @@ """ 阿里云百炼 DashScope 适配器 -基于 api/chat_routes.py 重构 +使用 OpenAI SDK 调用阿里云 OpenAI 兼容 API """ import json import os -from typing import Dict, List +from typing import Any, Dict, List, Optional -from fastapi.responses import JSONResponse, StreamingResponse - -from .base import BaseAdapter, ChatCompletionRequest, ModelInfo +from .base import ChatCompletionRequest, ModelInfo +from .unified_adapter import UnifiedOpenAIAdapter from .plugins import get_web_search_mode from core import get_logger logger = get_logger() -# 支持深度思考的模型 -THINKING_MODELS = {"qwen3-max", "qwen3.5-plus"} - -# 需要使用多模态接口的模型(qwen3.5 系列) -MULTIMODAL_API_MODELS = {"qwen3.5-plus", "qwen3.5-flash"} - # 百炼模型配置 DASHSCOPE_MODELS = [ ModelInfo( @@ -48,7 +41,18 @@ DASHSCOPE_MODELS = [ ModelInfo( id="qwen3.5-flash", name="Qwen3.5-Flash", - description="千问系列速度最快、成本极低的模型,适合简单任务。千问Flash采用灵活的阶梯定价,相比千问Turbo计费更合理。", + description="千问系列速度最快、成本极低的模型,适合简单任务。", + max_tokens=8192, + provider="Aliyun", + supports_thinking=False, + supports_web_search=True, + supports_vision=False, + supports_files=False, + ), + ModelInfo( + id="qwen-turbo", + name="Qwen-Turbo", + description="快速响应的通用模型", max_tokens=8192, provider="Aliyun", supports_thinking=False, @@ -67,397 +71,49 @@ DASHSCOPE_MODELS = [ supports_vision=True, supports_files=False, ), + ModelInfo( + id="qwen-vl-plus", + name="通义万相 VL-Plus", + description="支持视觉理解的多模态模型", + max_tokens=8192, + provider="Aliyun", + supports_thinking=False, + supports_web_search=False, + supports_vision=True, + supports_files=False, + ), ] +# 从 DASHSCOPE_MODELS 自动计算 +THINKING_MODELS = {m.id.lower() for m in DASHSCOPE_MODELS if m.supports_thinking} +VISION_MODELS = {m.id.lower() for m in DASHSCOPE_MODELS if m.supports_vision} -class DashScopeAdapter(BaseAdapter): + +class DashScopeAdapter(UnifiedOpenAIAdapter): """阿里云百炼 DashScope 平台适配器""" + _provider_type = "dashscope" + @property def provider_name(self) -> str: return "dashscope" - def is_available(self) -> bool: - """检查 API Key 是否配置""" - return bool(os.getenv("ALIYUN_API_KEY") or os.getenv("DASHSCOPE_API_KEY")) - - def _get_api_key(self) -> str: - """获取 API Key""" - return os.getenv("ALIYUN_API_KEY") or os.getenv("DASHSCOPE_API_KEY", "") - - def _needs_multimodal_api(self, model: str) -> bool: - """检查模型是否需要使用多模态 API""" - return model.lower() in MULTIMODAL_API_MODELS + def list_models(self) -> List[ModelInfo]: + return DASHSCOPE_MODELS def _supports_thinking(self, model: str) -> bool: """检查模型是否支持深度思考""" return model.lower() in THINKING_MODELS - def list_models(self) -> List[ModelInfo]: - return DASHSCOPE_MODELS + def _is_vision_model(self, model: str) -> bool: + """检查是否为多模态模型""" + return model.lower() in VISION_MODELS - async def chat(self, request: ChatCompletionRequest): + def _build_messages(self, request: ChatCompletionRequest) -> List[Dict]: """ - 处理 DashScope 聊天请求 - 支持流式/非流式、多模态 + 构建 DashScope 格式的消息 + 处理多模态内容 """ - # 打印请求参数 - logger.info(f"[DashScope] 请求参数:") - logger.info(f" - model: {request.model}") - logger.info(f" - stream: {request.stream}") - logger.info(f" - temperature: {request.temperature}") - logger.info(f" - max_tokens: {request.max_tokens}") - logger.info(f" - files: {request.files}") - logger.info(f" - deep_thinking: {request.deep_thinking}") - logger.info( - f" - messages: {json.dumps(request.messages, ensure_ascii=False, indent=2)}" - ) - - # 检测是否包含多模态内容 - has_multimodal = self._has_multimodal_content(request) - logger.info(f" - has_multimodal: {has_multimodal}") - - # 检查是否需要使用多模态接口(qwen3.5 系列) - needs_multimodal_api = self._needs_multimodal_api(request.model) - logger.info(f" - needs_multimodal_api: {needs_multimodal_api}") - - if has_multimodal or needs_multimodal_api: - return await self._multimodal_chat(request) - else: - return await self._text_chat(request) - - def _has_multimodal_content(self, request: ChatCompletionRequest) -> bool: - """检查是否包含多模态内容""" - for msg in request.messages: - content = msg.get("content", "") - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and item.get("type") == "image_url": - return True - return bool(request.files) - - async def _text_chat(self, request: ChatCompletionRequest): - """纯文本聊天""" - import dashscope - from dashscope import Generation - - dashscope.api_key = self._get_api_key() - - # 转换消息格式 - messages = self._build_text_messages(request) - logger.info(f"[DashScope] 文本聊天 - 转换后的消息:") - logger.info(f" - messages_count: {len(messages)}") - logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}") - - if request.stream: - return self._stream_text_chat(messages, request) - else: - return self._sync_text_chat(messages, request) - - def _build_text_messages(self, request: ChatCompletionRequest) -> List[Dict]: - """构建文本消息""" - messages = [] - for msg in request.messages: - role = msg.get("role", "user") - content = msg.get("content", "") - if isinstance(content, str) and content.strip(): - messages.append({"role": role, "content": content}) - elif isinstance(content, list): - text = "" - for item in content: - if isinstance(item, dict) and item.get("type") == "text": - text += item.get("text", "") - if text.strip(): - messages.append({"role": role, "content": text}) - return messages - - def _stream_text_chat(self, messages: List[Dict], request: ChatCompletionRequest): - """流式文本聊天""" - logger.info(f"[DashScope] 开始流式文本响应...") - - # 检查是否启用深度思考 - thinking_enabled = request.deep_thinking and self._supports_thinking(request.model) - logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(request.model)})") - - def generator(): - from utils.helpers import generate_unique_id, get_current_timestamp - - from dashscope import Generation - - full_content = "" - full_reasoning = "" - chunk_count = 0 - error_occurred = False - - # 打印 API 调用参数 - api_params = { - "model": request.model, - "messages": messages, - "stream": True, - "temperature": request.temperature, - "max_tokens": request.max_tokens, - "result_format": "message", - } - - # 使用统一网络搜索配置 - web_search_mode = get_web_search_mode(request) - if web_search_mode: - api_params["enable_search"] = True - if web_search_mode == "deep": - api_params["search_options"] = {"enable_search_extension": True} - - # 添加深度思考参数 - if thinking_enabled: - api_params["enable_thinking"] = True - - # 打印 API 调用参数 - logger.info(f"[DashScope] API 调用参数:") - logger.info(f" - model: {api_params['model']}") - logger.info(f" - stream: {api_params['stream']}") - logger.info(f" - temperature: {api_params['temperature']}") - logger.info(f" - max_tokens: {api_params['max_tokens']}") - logger.info(f" - result_format: {api_params['result_format']}") - if thinking_enabled: - logger.info(f" - enable_thinking: True") - - try: - responses = Generation.call(**api_params) - except Exception as e: - error_occurred = True - logger.error(f"[DashScope] API 调用异常: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - # 返回错误响应 - error_data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": request.model, - "choices": [{ - "index": 0, - "delta": {"content": f"API 调用失败: {str(e)}"}, - "finish_reason": "stop", - }], - } - yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - return - - for resp in responses: - if resp.status_code == 200: - chunk_count += 1 - choice = resp.output.choices[0] - - # 处理深度思考内容(reasoning_content) - reasoning_content = getattr(choice.message, "reasoning_content", None) - if reasoning_content: - # 计算增量 - if len(reasoning_content) > len(full_reasoning): - delta_reasoning = reasoning_content[len(full_reasoning):] - full_reasoning = reasoning_content - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": request.model, - "choices": [ - { - "index": 0, - "delta": {"reasoning_content": delta_reasoning}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - continue - - # 处理普通内容 - content = choice.message.content - if content and len(content) > len(full_content): - # DashScope 流式响应返回完整内容,计算增量 - delta = content[len(full_content) :] - full_content = content - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": request.model, - "choices": [ - { - "index": 0, - "delta": {"content": delta}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - else: - # 记录非200响应 - logger.warning(f"[DashScope] 非200响应: status_code={resp.status_code}, code={resp.code}, message={resp.message}") - - finish = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": request.model, - "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], - } - yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - - # 打印流式响应结果 - logger.info(f"[DashScope] 流式文本响应完成:") - logger.info(f" - chunks: {chunk_count}") - logger.info(f" - content_length: {len(full_content)} 字符") - if full_reasoning: - logger.info(f" - reasoning_length: {len(full_reasoning)} 字符") - logger.info( - f" - content_preview: {full_content[:200]}..." - if len(full_content) > 200 - else f" - content: {full_content}" - ) - - return StreamingResponse(generator(), media_type="text/event-stream") - - def _sync_text_chat(self, messages: List[Dict], request: ChatCompletionRequest): - """非流式文本聊天""" - from utils.helpers import generate_unique_id, get_current_timestamp - - from dashscope import Generation - - # 检查是否启用深度思考 - thinking_enabled = request.deep_thinking and self._supports_thinking(request.model) - logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(request.model)})") - - # 构建 API 调用参数 - api_params = { - "model": request.model, - "messages": messages, - "stream": False, - "temperature": request.temperature, - "max_tokens": request.max_tokens, - "result_format": "message", - } - - # 使用统一网络搜索配置 - web_search_mode = get_web_search_mode(request) - if web_search_mode: - api_params["enable_search"] = True - if web_search_mode == "deep": - api_params["search_options"] = {"enable_search_extension": True} - - # 添加深度思考参数 - if thinking_enabled: - api_params["enable_thinking"] = True - - # 打印 API 调用参数 - logger.info(f"[DashScope] API 调用参数:") - logger.info(f" - model: {api_params['model']}") - logger.info(f" - stream: {api_params['stream']}") - logger.info(f" - temperature: {api_params['temperature']}") - logger.info(f" - max_tokens: {api_params['max_tokens']}") - logger.info(f" - result_format: {api_params['result_format']}") - if thinking_enabled: - logger.info(f" - enable_thinking: True") - - try: - resp = Generation.call(**api_params) - except Exception as e: - logger.error(f"[DashScope] API 调用异常: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - return JSONResponse( - status_code=500, - content={"error": f"DashScope API 调用异常: {str(e)}"}, - ) - - if resp.status_code == 200: - message = resp.output.choices[0].message - content = message.content or "" - - # 构建响应消息 - response_message = {"role": "assistant", "content": content} - - # 处理深度思考内容 - reasoning_content = getattr(message, "reasoning_content", None) - if reasoning_content: - response_message["reasoning_content"] = reasoning_content - - response = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion", - "created": get_current_timestamp(), - "model": request.model, - "choices": [ - { - "index": 0, - "message": response_message, - "finish_reason": "stop", - } - ], - } - - if hasattr(resp, "usage") and resp.usage: - response["usage"] = { - "prompt_tokens": resp.usage.input_tokens, - "completion_tokens": resp.usage.output_tokens, - "total_tokens": resp.usage.total_tokens, - } - - # 打印响应结果 - logger.info(f"[DashScope] 响应成功:") - logger.info(f" - status_code: {resp.status_code}") - logger.info(f" - content_length: {len(content)} 字符") - if reasoning_content: - logger.info(f" - reasoning_length: {len(reasoning_content)} 字符") - logger.info( - f" - content_preview: {content[:200]}..." - if len(content) > 200 - else f" - content: {content}" - ) - if hasattr(resp, "usage") and resp.usage: - logger.info(f" - usage: {response['usage']}") - - return JSONResponse(content=response) - - logger.error(f"[DashScope] 请求失败:") - logger.error(f" - status_code: {resp.status_code}") - logger.error(f" - code: {resp.code}") - logger.error(f" - message: {resp.message}") - return JSONResponse( - status_code=500, - content={"error": f"DashScope Error: {resp.code} - {resp.message}"}, - ) - - async def _multimodal_chat(self, request: ChatCompletionRequest): - """多模态聊天""" - import dashscope - from dashscope import MultiModalConversation - - dashscope.api_key = self._get_api_key() - - logger.info(f"[DashScope] 开始多模态聊天...") - - # 转换消息格式 - messages = self._build_multimodal_messages(request) - logger.info(f"[DashScope] 多模态消息转换完成:") - logger.info(f" - messages_count: {len(messages)}") - logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}") - - # 选择多模态模型 - model = request.model - if "qwen-" in model and "vl" not in model: - original_model = model - model = model.replace("qwen-", "qwen-vl-") - logger.info(f"[DashScope] 模型自动切换: {original_model} -> {model}") - - if request.stream: - return self._stream_multimodal_chat(messages, model, request) - else: - return self._sync_multimodal_chat(messages, model, request) - - def _build_multimodal_messages(self, request: ChatCompletionRequest) -> List[Dict]: - """构建多模态消息""" messages = [] for msg in request.messages: @@ -466,39 +122,43 @@ class DashScopeAdapter(BaseAdapter): if isinstance(content, str): if content.strip(): - messages.append({"role": role, "content": [{"text": content}]}) + messages.append({"role": role, "content": content}) elif isinstance(content, list): + # 多模态内容 - OpenAI 格式兼容 ds_content = [] for item in content: if isinstance(item, dict): if item.get("type") == "text": - ds_content.append({"text": item.get("text", "")}) + ds_content.append({"type": "text", "text": item.get("text", "")}) elif item.get("type") == "image_url": img_url = self._extract_image_url(item) if img_url: - ds_content.append({"image": img_url}) + ds_content.append({ + "type": "image_url", + "image_url": {"url": img_url} + }) if ds_content: messages.append({"role": role, "content": ds_content}) return messages - def _extract_image_url(self, item: Dict) -> str: - """提取并转换图片 URL""" + def _extract_image_url(self, item: Dict) -> Optional[str]: + """提取图片 URL""" img_val = item.get("image_url", "") if isinstance(img_val, str): img_url = img_val elif isinstance(img_val, dict): img_url = img_val.get("url", "") else: - img_url = "" + return None - logger.info(f"[DashScope] 原始图片URL: {img_url}") + # 记录图片 URL 转换 + logger.info(f"[DashScope] 图片URL: {img_url}") - # 转换 http URL 为 file:// 格式(如果是本地文件) + # 处理本地文件 URL if img_url.startswith(("http://", "https://")): from urllib.parse import urlparse - parsed = urlparse(img_url) if "localhost" in parsed.netloc or "127.0.0.1" in parsed.netloc: path_parts = parsed.path.split("/") @@ -510,285 +170,38 @@ class DashScopeAdapter(BaseAdapter): elif not img_url.startswith("file://") and not img_url.startswith(("http://", "https://")): img_url = f"file://{img_url}" - logger.info(f"[DashScope] 转换后图片URL: {img_url}") - return img_url - def _stream_multimodal_chat( - self, messages: List[Dict], model: str, request: ChatCompletionRequest - ): - """流式多模态聊天""" - logger.info(f"[DashScope] 开始流式多模态响应...") - logger.info(f" - model: {model}") - logger.info(f" - max_tokens: {request.max_tokens}") - logger.info(f" - temperature: {request.temperature}") + def _get_extra_params(self, request: ChatCompletionRequest) -> Dict[str, Any]: + """ + 获取 DashScope 特殊参数 + - 深度思考: extra_body={"enable_thinking": True/False} + - 联网搜索: extra_body={"enable_search": True} + """ + extra_params = {} + extra_body = {} - # 检查是否启用深度思考 - thinking_enabled = request.deep_thinking and self._supports_thinking(model) - logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(model)})") + model = request.model - def generator(): - from utils.helpers import generate_unique_id, get_current_timestamp + # 深度思考 - 始终传递,明确启用或禁用 + logger.info(f"[DashScope] 深度思考请求: deep_thinking={request.deep_thinking}, model={model}") - from dashscope import MultiModalConversation + supports_thinking = self._supports_thinking(model) + logger.info(f"[DashScope] 模型 {model} 支持深度思考: {supports_thinking}") - full_content = "" - full_reasoning = "" - chunk_count = 0 - error_occurred = False + thinking_enabled = request.deep_thinking and supports_thinking + extra_body["enable_thinking"] = thinking_enabled + logger.info(f"[DashScope] 深度思考最终状态: {thinking_enabled}") - # 打印 API 调用参数 - api_params = { - "model": model, - "messages": messages, - "stream": True, - "enable_thinking": False, - "max_tokens": request.max_tokens, - "temperature": request.temperature, - } - - # 使用统一网络搜索配置 - web_search_mode = get_web_search_mode(request) - if web_search_mode: - api_params["enable_search"] = True - if web_search_mode == "deep": - api_params["search_options"] = {"enable_search_extension": True} - - # 添加深度思考参数 - if thinking_enabled: - api_params["enable_thinking"] = True - - logger.info(f"[DashScope] 流式多模态 API 调用参数:") - logger.info(f" - model: {api_params['model']}") - logger.info(f" - stream: {api_params['stream']}") - logger.info(f" - max_tokens: {api_params['max_tokens']}") - logger.info(f" - temperature: {api_params['temperature']}") - logger.info(f" - enable_thinking: {api_params['enable_thinking']}") - logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}") - - try: - responses = MultiModalConversation.call(**api_params) - except Exception as e: - error_occurred = True - logger.error(f"[DashScope] 多模态 API 调用异常: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - error_data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [{ - "index": 0, - "delta": {"content": f"API 调用失败: {str(e)}"}, - "finish_reason": "stop", - }], - } - yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - return - - for resp in responses: - chunk_count += 1 - - if resp.status_code == 200: - try: - choice = resp.output.choices[0] - message = choice["message"] - - # 处理深度思考内容(reasoning_content) - # 多模态 API 返回的 reasoning_content 也是独立的片段 - reasoning_content = message.get("reasoning_content", "") - if reasoning_content: - delta_reasoning = reasoning_content - full_reasoning += reasoning_content - - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "delta": {"reasoning_content": delta_reasoning}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - continue - - # 处理普通内容 - content_items = message.get("content", []) - text = "" - for item in content_items: - if isinstance(item, dict) and "text" in item: - text += item["text"] - - # 多模态 API 返回的 content 是独立的片段(不是累积的),直接作为 delta - if text: - delta = text - full_content += text - - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "delta": {"content": delta}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - except (KeyError, IndexError, TypeError) as e: - logger.warning(f"[DashScope] 解析多模态响应异常: {str(e)}") - else: - logger.warning(f"[DashScope] 非200响应: status_code={resp.status_code}, code={resp.code}, message={resp.message}") - - finish = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], - } - yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - - # 打印流式响应结果 - logger.info(f"[DashScope] 流式多模态响应完成:") - logger.info(f" - chunks: {chunk_count}") - if full_reasoning: - logger.info(f" - reasoning_length: {len(full_reasoning)} 字符") - logger.info(f" - reasoning: {full_reasoning[:500]}..." if len(full_reasoning) > 500 else f" - reasoning: {full_reasoning}") - logger.info(f" - content_length: {len(full_content)} 字符") - logger.info( - f" - content: {full_content[:500]}..." - if len(full_content) > 500 - else f" - content: {full_content}" - ) - - return StreamingResponse(generator(), media_type="text/event-stream") - - def _sync_multimodal_chat( - self, messages: List[Dict], model: str, request: ChatCompletionRequest - ): - """非流式多模态聊天""" - from utils.helpers import generate_unique_id, get_current_timestamp - - from dashscope import MultiModalConversation - - # 检查是否启用深度思考 - thinking_enabled = request.deep_thinking and self._supports_thinking(model) - logger.info(f"[DashScope] 深度思考: {thinking_enabled} (request={request.deep_thinking}, supports={self._supports_thinking(model)})") - - logger.info(f"[DashScope] 开始非流式多模态响应...") - logger.info(f" - model: {model}") - logger.info(f" - max_tokens: {request.max_tokens}") - logger.info(f" - temperature: {request.temperature}") - - # 打印 API 调用参数 - api_params = { - "model": model, - "messages": messages, - "stream": False, - "max_tokens": request.max_tokens, - "enable_thinking": False, - "temperature": request.temperature, - } - - # 使用统一网络搜索配置 + # 联网搜索 web_search_mode = get_web_search_mode(request) if web_search_mode: - api_params["enable_search"] = True + extra_body["enable_search"] = True if web_search_mode == "deep": - api_params["search_options"] = {"enable_search_extension": True} + extra_body["search_options"] = {"enable_search_extension": True} + logger.info(f"[DashScope] 联网搜索已启用: mode={web_search_mode}") - # 添加深度思考参数 - if thinking_enabled: - api_params["enable_thinking"] = True + # 添加 extra_body 到参数 + extra_params["extra_body"] = extra_body - logger.info(f"[DashScope] 非流式多模态 API 调用参数:") - logger.info(f" - model: {api_params['model']}") - logger.info(f" - stream: {api_params['stream']}") - logger.info(f" - max_tokens: {api_params['max_tokens']}") - logger.info(f" - temperature: {api_params['temperature']}") - logger.info(f" - enable_thinking: {api_params['enable_thinking']}") - - try: - resp = MultiModalConversation.call(**api_params) - except Exception as e: - logger.error(f"[DashScope] 多模态 API 调用异常: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - return JSONResponse( - status_code=500, - content={"error": f"DashScope API 调用异常: {str(e)}"}, - ) - - if resp.status_code == 200: - try: - message = resp.output.choices[0]["message"] - content_items = message.get("content", []) - text = "" - for item in content_items: - if isinstance(item, dict) and "text" in item: - text += item["text"] - - # 构建响应消息 - response_message = {"role": "assistant", "content": text} - - # 处理深度思考内容 - reasoning_content = message.get("reasoning_content") - if reasoning_content: - response_message["reasoning_content"] = reasoning_content - - response = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "message": response_message, - "finish_reason": "stop", - } - ], - } - - # 打印响应结果 - logger.info(f"[DashScope] 多模态响应成功:") - logger.info(f" - status_code: {resp.status_code}") - logger.info(f" - content_length: {len(text)} 字符") - if reasoning_content: - logger.info(f" - reasoning_length: {len(reasoning_content)} 字符") - logger.info( - f" - content_preview: {text[:200]}..." - if len(text) > 200 - else f" - content: {text}" - ) - - return JSONResponse(content=response) - except (KeyError, IndexError, TypeError) as e: - logger.error(f"[DashScope] 解析多模态响应异常: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - return JSONResponse( - status_code=500, - content={"error": f"Parse error: {str(e)}"}, - ) - - logger.error(f"[DashScope] 多模态请求失败:") - logger.error(f" - status_code: {resp.status_code}") - logger.error(f" - code: {resp.code}") - logger.error(f" - message: {resp.message}") - return JSONResponse( - status_code=500, - content={"error": f"DashScope Error: {resp.code} - {resp.message}"}, - ) + return extra_params \ No newline at end of file diff --git a/server/adapters/glm_adapter.py b/server/adapters/glm_adapter.py index 0169594..3d20c78 100644 --- a/server/adapters/glm_adapter.py +++ b/server/adapters/glm_adapter.py @@ -1,16 +1,14 @@ """ 智谱 GLM 适配器 -基于 utils/glm_adapter.py 重构 -使用zai-sdk。因为已经完成这一部分的整套逻辑,如果更换OpenAI-SDK会花很多时间调试。 +使用 OpenAI SDK 调用智谱 OpenAI 兼容 API """ import json import os -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional -from fastapi.responses import JSONResponse, StreamingResponse - -from .base import BaseAdapter, ChatCompletionRequest, ModelInfo +from .base import ChatCompletionRequest, ModelInfo +from .unified_adapter import UnifiedOpenAIAdapter from .plugins import get_web_search_mode, build_glm_search_tool from core import get_logger @@ -75,122 +73,44 @@ GLM_MODELS = [ ), ] -# 视觉模型列表(用于自动切换) -VISION_MODELS = {"glm-4v", "glm-4v-plus", "glm-4v-plus-0111", "glm-4.6v"} - -# 支持深度思考的模型 -THINKING_MODELS = {"glm-z1-flash", "glm-z1-air", "glm-4.6v", "glm-4.6"} +# 从 GLM_MODELS 自动计算 +VISION_MODELS = {m.id.lower() for m in GLM_MODELS if m.supports_vision} +THINKING_MODELS = {m.id.lower() for m in GLM_MODELS if m.supports_thinking} -class GLMAdapter(BaseAdapter): +class GLMAdapter(UnifiedOpenAIAdapter): """智谱 GLM 平台适配器""" - _client = None + _provider_type = "zhipu" @property def provider_name(self) -> str: return "glm" - def is_available(self) -> bool: - """检查 API Key 是否配置""" - return bool(os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY")) - - def _get_client(self): - """获取 GLM 客户端(懒加载)""" - if self._client is None: - from zhipuai import ZhipuAI - - api_key = os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY") - self._client = ZhipuAI(api_key=api_key) - return self._client - def list_models(self) -> List[ModelInfo]: return GLM_MODELS - async def chat(self, request: ChatCompletionRequest): - """ - 处理 GLM 聊天请求 - 支持流式/非流式、图像、文档、联网搜索、深度思考 - """ - client = self._get_client() + def _supports_thinking(self, model: str) -> bool: + """检查模型是否支持深度思考""" + return model.lower() in THINKING_MODELS - # 构建消息 - glm_messages, has_vision, has_files = self._build_messages(request) - actual_model = self._resolve_model(request.model, has_vision, has_files) - - # 调试:打印原始请求参数 - logger.info(f"[GLM] 原始请求参数:") - logger.info( - f" - request.deep_thinking: {request.deep_thinking} (type: {type(request.deep_thinking)})" - ) - logger.info(f" - request.web_search: {request.web_search}") - logger.info(f" - request.deep_search: {request.deep_search}") - logger.info(f" - actual_model: {actual_model}") - logger.info(f" - supports_thinking: {self._supports_thinking(actual_model)}") - - # 构建额外参数 - extra_kwargs = {} - web_search_mode = get_web_search_mode(request) - - if web_search_mode: - extra_kwargs["tools"] = [build_glm_search_tool(web_search_mode)] - extra_kwargs["tool_choice"] = "auto" - - # 深度思考:正向选择(True 时启用,False 时禁用) - # 注意:只有特定模型支持深度思考(如 glm-z1-flash) - thinking_enabled = request.deep_thinking and self._supports_thinking( - actual_model - ) - logger.info( - f"[GLM] 深度思考判断: {request.deep_thinking} and {self._supports_thinking(actual_model)} = {thinking_enabled}" - ) - - if thinking_enabled: - extra_kwargs["thinking"] = {"type": "enabled"} - logger.info( - f"[GLM] 深度思考已启用: extra_kwargs['thinking'] = {extra_kwargs['thinking']}" - ) - else: - extra_kwargs["thinking"] = {"type": "disabled"} - logger.info( - f"[GLM] 深度思考已禁用: extra_kwargs['thinking'] = {extra_kwargs['thinking']}" - ) - - if extra_kwargs: - logger.info( - f"[GLM] 最终 extra_kwargs: {json.dumps(extra_kwargs, ensure_ascii=False)}" - ) - - if request.stream: - return self._stream_chat( - client, glm_messages, actual_model, request, extra_kwargs - ) - else: - return self._sync_chat( - client, glm_messages, actual_model, request, extra_kwargs - ) - - def _build_messages( - self, request: ChatCompletionRequest - ) -> tuple[List[Dict], bool, bool]: + def _build_messages(self, request: ChatCompletionRequest) -> List[Dict]: """ 构建 GLM 格式的消息 - 返回:(消息列表, 是否包含图片, 是否包含文件附件) + 处理文件附件和多模态内容 """ messages = [] has_vision = False - has_files = bool(request.files) # 检查是否有文件附件 + has_files = bool(request.files) for msg in request.messages: role = msg.get("role", "user") content = msg.get("content", "") if isinstance(content, str): - # 纯文本 if content.strip(): messages.append({"role": role, "content": content}) elif isinstance(content, list): - # 多模态内容 glm_content = [] for item in content: if isinstance(item, dict): @@ -214,7 +134,6 @@ class GLMAdapter(BaseAdapter): if request.files: file_content = self._build_file_content(request.files) if messages and messages[-1]["role"] == "user": - # 追加到最后一个用户消息 if isinstance(messages[-1]["content"], list): messages[-1]["content"].extend(file_content) else: @@ -225,7 +144,7 @@ class GLMAdapter(BaseAdapter): else: messages.append({"role": "user", "content": file_content}) - return messages, has_vision, has_files + return messages def _extract_image_url(self, item: Dict) -> Optional[str]: """提取图片 URL""" @@ -244,232 +163,54 @@ class GLMAdapter(BaseAdapter): content.append({"type": "file_url", "file_url": {"url": file_url}}) return content - def _resolve_model( - self, model: str, has_vision: bool, has_files: bool = False - ) -> str: + def _resolve_model(self, model: str, has_vision: bool, has_files: bool = False) -> str: """解析实际使用的模型""" model_lower = model.lower() - # 如果有图片或文件附件,强制使用 glm-4.6v(支持多模态) if (has_vision or has_files) and model_lower not in VISION_MODELS: - logger.info( - f"[GLM] 检测到图片或文件附件,强制切换模型: {model} -> glm-4.6v" - ) + logger.info(f"[GLM] 检测到图片或文件附件,切换模型: {model} -> glm-4.6v") return "glm-4.6v" return model - def _supports_thinking(self, model: str) -> bool: - """检查模型是否支持深度思考""" - return model.lower() in THINKING_MODELS + def _get_extra_params(self, request: ChatCompletionRequest) -> Dict[str, Any]: + """ + 获取 GLM 特殊参数 + - 深度思考: extra_body={"thinking": {"type": "enabled/disabled"}} + - 联网搜索: tools=[{"type": "web_search", ...}] + """ + extra_params = {} - def _stream_chat( - self, client, messages, model, request, extra_kwargs - ) -> StreamingResponse: - """流式聊天""" - logger.info(f"[GLM] 开始流式响应...") - - # 提取深度思考配置 - thinking_config = extra_kwargs.get("thinking") - tools_config = extra_kwargs.get("tools") - - def generator(): - from utils.helpers import generate_unique_id, get_current_timestamp - - full_content = "" - - # 构建 API 调用参数 - api_params = { - "model": model, - "messages": messages, - "stream": True, - "temperature": request.temperature, - "max_tokens": request.max_tokens, - } - - # 深度思考:使用 extra_body 传递 - if thinking_config: - api_params["extra_body"] = {"thinking": thinking_config} - - # 联网搜索:使用 tools 参数 - if tools_config: - api_params["tools"] = tools_config - api_params["tool_choice"] = "auto" - - # 打印请求参数 - logger.info(f"[GLM] API 调用参数:") - logger.info(f" - model: {model}") - logger.info(f" - stream: True") - logger.info(f" - temperature: {request.temperature}") - logger.info(f" - max_tokens: {request.max_tokens}") - if thinking_config: - logger.info(f" - extra_body: {{'thinking': {thinking_config}}}") - if tools_config: - logger.info( - f" - tools: {json.dumps(tools_config, ensure_ascii=False)}" - ) - logger.info(f" - tool_choice: auto") - logger.info( - f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}" - ) - - chunk_count = 0 - resp = client.chat.completions.create(**api_params) - - for chunk in resp: - chunk_count += 1 - - # 检查 delta 是否存在 - if not hasattr(chunk.choices[0], "delta"): - continue - - delta = chunk.choices[0].delta - - # 处理深度思考内容(reasoning_content) - reasoning_content = getattr(delta, "reasoning_content", None) - if reasoning_content: - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "delta": {"reasoning_content": reasoning_content}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - continue - - # 处理普通内容 - content = getattr(delta, "content", None) - if content: - full_content += content - data = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "delta": {"content": content}, - "finish_reason": None, - } - ], - } - yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - - # 结束标记 - finish = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion.chunk", - "created": get_current_timestamp(), - "model": model, - "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], - } - yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n" - yield "data: [DONE]\n\n" - - # 打印流式响应结果 - logger.info(f"[GLM] 流式响应完成:") - logger.info(f" - chunks: {chunk_count}") - logger.info(f" - content_length: {len(full_content)} 字符") - logger.info( - f" - content_preview: {full_content[:200]}..." - if len(full_content) > 200 - else f" - content: {full_content}" - ) - - return StreamingResponse(generator(), media_type="text/event-stream") - - def _sync_chat( - self, client, messages, model, request, extra_kwargs - ) -> JSONResponse: - """非流式聊天""" - from utils.helpers import generate_unique_id, get_current_timestamp - - # 提取深度思考配置 - thinking_config = extra_kwargs.get("thinking") - tools_config = extra_kwargs.get("tools") - - # 构建 API 调用参数 - api_params = { - "model": model, - "messages": messages, - "stream": False, - "temperature": request.temperature, - "max_tokens": request.max_tokens, - } - - # 深度思考:使用 extra_body 传递 - if thinking_config: - api_params["extra_body"] = {"thinking": thinking_config} - - # 联网搜索:使用 tools 参数 - if tools_config: - api_params["tools"] = tools_config - api_params["tool_choice"] = "auto" - - # 打印请求参数 - logger.info(f"[GLM] API 调用参数:") - logger.info(f" - model: {model}") - logger.info(f" - stream: {request.stream}") - logger.info(f" - temperature: {request.temperature}") - logger.info(f" - max_tokens: {request.max_tokens}") - if thinking_config: - logger.info(f" - extra_body: {{'thinking': {thinking_config}}}") - if tools_config: - logger.info(f" - tools: {json.dumps(tools_config, ensure_ascii=False)}") - logger.info(f" - tool_choice: auto") - logger.info( - f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}" + # 检测是否有多模态内容,决定最终使用的模型 + messages = self._build_messages(request) + has_vision = any( + isinstance(m.get("content"), list) and + any(c.get("type") == "image_url" for c in m.get("content", [])) + for m in messages ) + has_files = bool(request.files) + actual_model = self._resolve_model(request.model, has_vision, has_files) - resp = client.chat.completions.create(**api_params) + # 更新请求中的模型(如果有变化) + if actual_model != request.model: + extra_params["model"] = actual_model - message = resp.choices[0].message - content = message.content or "" + # 联网搜索 + web_search_mode = get_web_search_mode(request) + if web_search_mode: + extra_params["tools"] = [build_glm_search_tool(web_search_mode)] + extra_params["tool_choice"] = "auto" + logger.info(f"[GLM] 联网搜索已启用: mode={web_search_mode}") - # 构建响应 - response_message = {"role": "assistant", "content": content} + # 深度思考 - 始终传递,明确启用或禁用 + logger.info(f"[GLM] 深度思考请求: deep_thinking={request.deep_thinking}, actual_model={actual_model}") - # 处理深度思考内容 - reasoning_content = getattr(message, "reasoning_content", None) - if reasoning_content: - response_message["reasoning_content"] = reasoning_content + # 判断是否支持深度思考 + supports_thinking = self._supports_thinking(actual_model) + logger.info(f"[GLM] 模型 {actual_model} 支持深度思考: {supports_thinking}") - response = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion", - "created": get_current_timestamp(), - "model": model, - "choices": [ - { - "index": 0, - "message": response_message, - "finish_reason": "stop", - } - ], - } + # 只有前端请求启用 且 模型支持时才启用 + thinking_enabled = request.deep_thinking and supports_thinking + thinking_type = "enabled" if thinking_enabled else "disabled" + extra_params["extra_body"] = {"thinking": {"type": thinking_type}} + logger.info(f"[GLM] 深度思考最终状态: {thinking_type}") - if hasattr(resp, "usage") and resp.usage: - response["usage"] = { - "prompt_tokens": resp.usage.prompt_tokens, - "completion_tokens": resp.usage.completion_tokens, - "total_tokens": resp.usage.total_tokens, - } - - # 打印响应结果 - logger.info(f"[GLM] 响应结果:") - logger.info(f" - content_length: {len(content)} 字符") - logger.info( - f" - content_preview: {content[:200]}..." - if len(content) > 200 - else f" - content: {content}" - ) - if hasattr(resp, "usage") and resp.usage: - logger.info(f" - usage: {response['usage']}") - - return JSONResponse(content=response) + return extra_params \ No newline at end of file diff --git a/server/adapters/openai_adapter.py b/server/adapters/openai_adapter.py index 5421a10..52e3778 100644 --- a/server/adapters/openai_adapter.py +++ b/server/adapters/openai_adapter.py @@ -5,12 +5,18 @@ OpenAI 适配器 import json import os -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import StreamingResponse -from .base import BaseAdapter, ChatCompletionRequest, ModelInfo -from .plugins import get_web_search_mode, build_openai_search_tool, execute_tavily_search, get_current_time_info +from .base import ChatCompletionRequest, ModelInfo +from .unified_adapter import UnifiedOpenAIAdapter +from .plugins import ( + get_web_search_mode, + build_openai_search_tool, + execute_tavily_search, + get_current_time_info, +) from core import get_logger logger = get_logger() @@ -83,183 +89,67 @@ DEEPSEEK_MODELS = [ max_tokens=64000, provider="Deepseek", supports_thinking=True, - supports_web_search=True, # 注:通过内置检索增强实现 + supports_web_search=True, supports_vision=False, supports_files=False, ), ] -# DeepSeek 支持深度思考的模型 -DEEPSEEK_THINKING_MODELS = {"deepseek-reasoner"} +# 从 DEEPSEEK_MODELS 自动计算 +DEEPSEEK_THINKING_MODELS = {m.id.lower() for m in DEEPSEEK_MODELS if m.supports_thinking} -class OpenAIAdapter(BaseAdapter): +class OpenAIAdapter(UnifiedOpenAIAdapter): """OpenAI 平台适配器""" - _client = None - _provider_type: str = "openai" # openai 或 deepseek - - def __init__(self, provider_type: str = "openai"): - self._provider_type = provider_type + _provider_type = "openai" @property def provider_name(self) -> str: - return self._provider_type - - def is_available(self) -> bool: - """检查 API Key 是否配置""" - if self._provider_type == "deepseek": - return bool(os.getenv("DEEPSEEK_API_KEY")) - return bool(os.getenv("OPENAI_API_KEY")) - - def _get_client(self): - """获取 OpenAI 客户端(懒加载)""" - if self._client is None: - from openai import OpenAI - - if self._provider_type == "deepseek": - api_key = os.getenv("DEEPSEEK_API_KEY", "") - base_url = os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") - else: - api_key = os.getenv("OPENAI_API_KEY", "") - base_url = os.getenv("OPENAI_BASE_URL") # 可选自定义端点 - - kwargs = {"api_key": api_key} - if base_url: - kwargs["base_url"] = base_url - - self._client = OpenAI(**kwargs) - return self._client + return "openai" def list_models(self) -> List[ModelInfo]: - if self._provider_type == "deepseek": - return DEEPSEEK_MODELS return OPENAI_MODELS - async def chat(self, request: ChatCompletionRequest): - """ - 处理 OpenAI 聊天请求 - 直接使用 OpenAI SDK,支持流式/非流式 - """ - client = self._get_client() + def _get_extra_params(self, request: ChatCompletionRequest) -> Dict[str, Any]: + """获取 OpenAI 特殊参数""" + extra_params = {} - # 打印请求参数 - provider_name = self._provider_type.upper() - logger.info(f"[{provider_name}] 请求参数:") - logger.info(f" - model: {request.model}") - logger.info(f" - stream: {request.stream}") - logger.info(f" - temperature: {request.temperature}") - logger.info(f" - max_tokens: {request.max_tokens}") - logger.info(f" - provider_type: {self._provider_type}") - if self._provider_type == "deepseek": - logger.info(f" - deep_thinking: {request.deep_thinking}") - - # 构建消息 - messages = self._build_messages(request) - - # 统一添加联网搜索插件参数 + # 联网搜索 - 使用 Function Calling web_search_mode = get_web_search_mode(request) if web_search_mode: - # 注入当前时间信息到 System Prompt 中,以便模型拥有时间感知能力 - time_info = get_current_time_info() - has_system = False - for msg in messages: - if msg.get("role") == "system": - msg["content"] = f"当前系统时间:{time_info}\n" + str(msg.get("content", "")) - has_system = True - break - if not has_system: - messages.insert(0, {"role": "system", "content": f"当前系统时间:{time_info}"}) + extra_params["tools"] = [build_openai_search_tool(web_search_mode)] + logger.info(f"[OpenAI] 联网搜索已启用: mode={web_search_mode}") - logger.info( - f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}" - ) + return extra_params - # 构建请求参数 - kwargs = { - "model": request.model, - "messages": messages, - "temperature": request.temperature, - "max_tokens": request.max_tokens, - "stream": request.stream, - } - - if web_search_mode: - search_tool = build_openai_search_tool(web_search_mode) - kwargs["tools"] = [search_tool] - - # DeepSeek 深度思考支持 - extra_body = None - if self._provider_type == "deepseek" and request.deep_thinking: - if self._supports_thinking(request.model): - extra_body = {"thinking": {"type": "enabled"}} - kwargs["extra_body"] = extra_body - logger.info( - f"[{provider_name}] 深度思考已启用: extra_body = {extra_body}" - ) - - if request.stream: - return self._stream_chat(client, kwargs, extra_body) - else: - return self._sync_chat(client, kwargs, extra_body) - - def _supports_thinking(self, model: str) -> bool: - """检查模型是否支持深度思考""" - return model.lower() in DEEPSEEK_THINKING_MODELS - - def _build_messages(self, request: ChatCompletionRequest) -> List[Dict]: - """构建 OpenAI 格式消息""" - messages = [] - - for msg in request.messages: - role = msg.get("role", "user") - content = msg.get("content", "") - - # OpenAI 直接支持标准格式 - if isinstance(content, str): - if content.strip(): - messages.append({"role": role, "content": content}) - elif isinstance(content, list): - # 多模态内容 - openai_content = [] - for item in content: - if isinstance(item, dict): - openai_content.append(item) - if openai_content: - messages.append({"role": role, "content": openai_content}) - - return messages - - def _stream_chat( - self, client, kwargs: Dict, extra_body: Optional[Dict] = None - ) -> StreamingResponse: - """流式聊天""" - provider_name = self._provider_type.upper() - logger.info(f"[{provider_name}] 开始流式响应...") + def _stream_chat(self, client, kwargs: Dict) -> StreamingResponse: + """ + 流式聊天 - 处理联网搜索的 Function Calling + """ + logger.info(f"[OpenAI] 开始流式响应...") def generator(): from utils.helpers import generate_unique_id, get_current_timestamp - - nonlocal kwargs - # 可能需要执行多轮对话(当发生工具调用时) + # 可能需要多轮对话(当发生工具调用时) while True: resp = client.chat.completions.create(**kwargs) full_content = "" full_reasoning = "" chunk_count = 0 - + tool_calls = [] current_tool_call = None - + for chunk in resp: if not chunk.choices: continue - + chunk_count += 1 delta = chunk.choices[0].delta - # 1. 收集可能有内容/推理 + # 收集内容 delta_content = {} if hasattr(delta, "content") and delta.content: delta_content["content"] = delta.content @@ -268,18 +158,20 @@ class OpenAIAdapter(BaseAdapter): delta_content["reasoning_content"] = delta.reasoning_content full_reasoning += delta.reasoning_content - # 2. 收集可能产生的 tool_calls (流式) + # 收集 tool_calls(流式) if hasattr(delta, "tool_calls") and delta.tool_calls: for tool_call_chunk in delta.tool_calls: idx = tool_call_chunk.index - # 确保 tool_calls 列表足够长 while len(tool_calls) <= idx: - tool_calls.append({"id": "", "type": "function", "function": {"name": "", "arguments": ""}}) - + tool_calls.append({ + "id": "", + "type": "function", + "function": {"name": "", "arguments": ""} + }) + if tool_call_chunk.id: tool_calls[idx]["id"] += tool_call_chunk.id if tool_call_chunk.type: - # 对于 type, 因为 OpenAI 可能会传 chunks, 但通常只在第一块或者每块传, 为了避免 functionfunction, 使用赋值而非累加 tool_calls[idx]["type"] = tool_call_chunk.type if tool_call_chunk.function: if tool_call_chunk.function.name: @@ -287,64 +179,59 @@ class OpenAIAdapter(BaseAdapter): if tool_call_chunk.function.arguments: tool_calls[idx]["function"]["arguments"] += tool_call_chunk.function.arguments - # 3. 输出给前端普通文本 + # 输出普通内容 if delta_content and not tool_calls: data = { "id": f"chatcmpl-{generate_unique_id()}", "object": "chat.completion.chunk", "created": get_current_timestamp(), "model": kwargs["model"], - "choices": [ - { - "index": 0, - "delta": delta_content, - "finish_reason": None, - } - ], + "choices": [{ + "index": 0, + "delta": delta_content, + "finish_reason": None, + }], } yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" - - # 检查此轮请求是否收到了完整工具调用,若是则执行搜索逻辑并追加继续请求,不再让外部函数退出 + + # 检查是否有完整的工具调用 if tool_calls: - logger.info(f"[{provider_name}] 检测到流式中包含了工具调用进行拦截并处理: {json.dumps(tool_calls, ensure_ascii=False)}") - - # 把大模型的工具调用请求也追加进去 + logger.info(f"[OpenAI] 检测到工具调用: {json.dumps(tool_calls, ensure_ascii=False)}") + + # 添加助手消息 assistant_msg = { "role": "assistant", - "content": full_content or None, # 如果工具和普通内容同时存在也保留 + "content": full_content or None, "tool_calls": tool_calls } if full_reasoning: assistant_msg["reasoning_content"] = full_reasoning - elif self._provider_type == "deepseek" and self._supports_thinking(kwargs["model"]): - # DeepSeek 推理模型在有工具调用时必须有 reasoning_content 字段 - assistant_msg["reasoning_content"] = "" kwargs["messages"].append(assistant_msg) - + + # 执行搜索工具 for tc in tool_calls: if tc["function"]["name"] == "web_search": try: args = json.loads(tc["function"]["arguments"]) query = args.get("query", "") mode = "deep" if "advanced" in str(kwargs.get("tools", [])) else "simple" - logger.info(f"[{provider_name}] 执行搜索插件: {query}") + logger.info(f"[OpenAI] 执行搜索: {query}") search_result = execute_tavily_search(query, mode=mode) except Exception as e: - search_result = f"获取搜索参数或执行搜索失败: {str(e)}" + search_result = f"搜索失败: {str(e)}" logger.error(search_result) - - # 把执行结果告诉大模型 + kwargs["messages"].append({ "role": "tool", "tool_call_id": tc["id"], "name": "web_search", "content": search_result }) - - # 工具执行完毕,继续发起下一轮请求大模型归纳总结输出 + + # 继续请求归纳答案 continue - # 如果没有工具调用或者全部分发完毕,正常结束给前端 + # 没有工具调用,结束 finish = { "id": f"chatcmpl-{generate_unique_id()}", "object": "chat.completion.chunk", @@ -355,130 +242,52 @@ class OpenAIAdapter(BaseAdapter): yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n" yield "data: [DONE]\n\n" - # 打印流式响应结果 - logger.info(f"[{provider_name}] 流式响应完成:") - logger.info(f" - chunks: {chunk_count}") - logger.info(f" - content_length: {len(full_content)} 字符") - if full_reasoning: - logger.info(f" - reasoning_length: {len(full_reasoning)} 字符") - logger.info( - f" - content_preview: {full_content[:200]}..." - if len(full_content) > 200 - else f" - content: {full_content}" - ) - - # 结束外层循环退出生成器 + logger.info(f"[OpenAI] 流式响应完成: chunks={chunk_count}, content_len={len(full_content)}") break return StreamingResponse(generator(), media_type="text/event-stream") - def _sync_chat( - self, client, kwargs: Dict, extra_body: Optional[Dict] = None - ) -> JSONResponse: - """非流式聊天""" - from utils.helpers import generate_unique_id, get_current_timestamp - while True: - resp = client.chat.completions.create(**kwargs) +class DeepseekAdapter(UnifiedOpenAIAdapter): + """Deepseek 平台适配器""" - message = resp.choices[0].message - - # 判断是否涉及工具调用 - if hasattr(message, "tool_calls") and message.tool_calls: - # 记录这轮的助手回复 - assistant_msg = {"role": "assistant", "content": message.content or None} - # openai sdk 对象转 dict 存储 tool_calls - tool_calls_dict = [] - for tc in message.tool_calls: - tc_dict = { - "id": tc.id, - "type": tc.type, - "function": { - "name": tc.function.name, - "arguments": tc.function.arguments - } - } - tool_calls_dict.append(tc_dict) - assistant_msg["tool_calls"] = tool_calls_dict - if hasattr(message, "reasoning_content") and message.reasoning_content: - assistant_msg["reasoning_content"] = message.reasoning_content - elif self._provider_type == "deepseek" and self._supports_thinking(kwargs["model"]): - # DeepSeek 推理模型在有工具调用时必须有 reasoning_content 字段 - assistant_msg["reasoning_content"] = "" - kwargs["messages"].append(assistant_msg) - - # 执行所有的工具调用 - for tc in tool_calls_dict: - if tc["function"]["name"] == "web_search": - try: - args = json.loads(tc["function"]["arguments"]) - query = args.get("query", "") - mode = "deep" if "advanced" in str(kwargs.get("tools", [])) else "simple" - search_result = execute_tavily_search(query, mode=mode) - except Exception as e: - search_result = f"执行搜索失败: {str(e)}" - - # 把执行结果追加到消息中 - kwargs["messages"].append({ - "role": "tool", - "tool_call_id": tc["id"], - "name": "web_search", - "content": search_result - }) - # 工具调用完成,发起下一轮请求获取归纳答案 - continue + _provider_type = "deepseek" - # 处理普通的文本回复 - content = message.content or "" - response = { - "id": f"chatcmpl-{generate_unique_id()}", - "object": "chat.completion", - "created": get_current_timestamp(), - "model": kwargs["model"], - "choices": [ - { - "index": 0, - "message": { - "role": message.role, - "content": content, - }, - "finish_reason": resp.choices[0].finish_reason, - } - ], - } + @property + def provider_name(self) -> str: + return "deepseek" - # 添加推理内容(如有) - if hasattr(message, "reasoning_content") and message.reasoning_content: - response["choices"][0]["message"][ - "reasoning_content" - ] = message.reasoning_content + def list_models(self) -> List[ModelInfo]: + return DEEPSEEK_MODELS - if resp.usage: - response["usage"] = { - "prompt_tokens": resp.usage.prompt_tokens, - "completion_tokens": resp.usage.completion_tokens, - "total_tokens": resp.usage.total_tokens, - } + def _supports_thinking(self, model: str) -> bool: + """检查模型是否支持深度思考""" + return model.lower() in DEEPSEEK_THINKING_MODELS - # 打印响应结果 - provider_name = self._provider_type.upper() - logger.info(f"[{provider_name}] 响应结果:") - logger.info(f" - content_length: {len(content)} 字符") - if hasattr(message, "reasoning_content") and message.reasoning_content: - logger.info(f" - reasoning_length: {len(message.reasoning_content)} 字符") - logger.info( - f" - content_preview: {content[:200]}..." - if len(content) > 200 - else f" - content: {content}" - ) - if resp.usage: - logger.info(f" - usage: {response['usage']}") + def _get_extra_params(self, request: ChatCompletionRequest) -> Dict[str, Any]: + """获取 Deepseek 特殊参数""" + extra_params = {} - return JSONResponse(content=response) + # 深度思考 - 始终传递,明确启用或禁用 + logger.info(f"[Deepseek] 深度思考请求: deep_thinking={request.deep_thinking}, model={request.model}") + supports_thinking = self._supports_thinking(request.model) + logger.info(f"[Deepseek] 模型 {request.model} 支持深度思考: {supports_thinking}") -class DeepseekAdapter(OpenAIAdapter): - """Deepseek 平台适配器(继承 OpenAI 适配器)""" + thinking_enabled = request.deep_thinking and supports_thinking + thinking_type = "enabled" if thinking_enabled else "disabled" + extra_params["extra_body"] = {"thinking": {"type": thinking_type}} + logger.info(f"[Deepseek] 深度思考最终状态: {thinking_type}") - def __init__(self): - super().__init__(provider_type="deepseek") + # 联网搜索 - 使用 Function Calling + web_search_mode = get_web_search_mode(request) + if web_search_mode: + extra_params["tools"] = [build_openai_search_tool(web_search_mode)] + logger.info(f"[Deepseek] 联网搜索已启用: mode={web_search_mode}") + + return extra_params + + def _stream_chat(self, client, kwargs: Dict) -> StreamingResponse: + """流式聊天 - 复用 OpenAI 的工具调用逻辑""" + # DeepSeek 使用相同的工具调用处理逻辑 + return OpenAIAdapter._stream_chat(self, client, kwargs) \ No newline at end of file diff --git a/server/adapters/unified_adapter.py b/server/adapters/unified_adapter.py new file mode 100644 index 0000000..2cecad5 --- /dev/null +++ b/server/adapters/unified_adapter.py @@ -0,0 +1,382 @@ +""" +统一 OpenAI SDK 适配器基类 + +所有平台适配器继承此类,通过配置区分不同平台。 + +MCP (Model Context Protocol) 支持: +- 子类可覆盖 _get_mcp_tools() 返回 MCP 工具定义 +- 子类可覆盖 _handle_mcp_tool_call() 处理 MCP 工具调用 +""" + +import json +import os +from abc import abstractmethod +from typing import Any, Dict, List, Optional + +from fastapi.responses import JSONResponse, StreamingResponse +from openai import OpenAI + +from .base import BaseAdapter, ChatCompletionRequest, ModelInfo +from core import get_logger + +logger = get_logger() + + +# 平台配置 +PROVIDER_CONFIGS = { + "zhipu": { + "base_url": "https://open.bigmodel.cn/api/paas/v4/", + "api_key_env": "ZHIPU_API_KEY", + "alias_env": ["GLM_API_KEY"], # 备选环境变量 + }, + "dashscope": { + "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "api_key_env": "DASHSCOPE_API_KEY", + "alias_env": ["ALIYUN_API_KEY"], + }, + "deepseek": { + "base_url": "https://api.deepseek.com/v1", + "api_key_env": "DEEPSEEK_API_KEY", + "alias_env": [], + }, + "openai": { + "base_url": None, # 使用 OpenAI 默认值 + "api_key_env": "OPENAI_API_KEY", + "alias_env": [], + }, +} + + +class UnifiedOpenAIAdapter(BaseAdapter): + """ + 基于 OpenAI SDK 的统一适配器基类 + + 子类只需提供: + - provider_name: 平台名称 + - list_models(): 支持的模型列表 + - _get_extra_params(): 特殊参数(可选) + + MCP 扩展点: + - _get_mcp_tools(): 返回 MCP 工具定义 + - _handle_mcp_tool_call(): 处理 MCP 工具调用 + """ + + _client: Optional[OpenAI] = None + _provider_type: str = "openai" + + def _get_api_key(self) -> Optional[str]: + """获取 API Key""" + config = PROVIDER_CONFIGS.get(self._provider_type, {}) + api_key_env = config.get("api_key_env", "") + alias_env = config.get("alias_env", []) + + # 优先使用主环境变量 + api_key = os.getenv(api_key_env) + if api_key: + return api_key + + # 尝试备选环境变量 + for env_name in alias_env: + api_key = os.getenv(env_name) + if api_key: + return api_key + + return None + + def _get_base_url(self) -> Optional[str]: + """获取 Base URL""" + config = PROVIDER_CONFIGS.get(self._provider_type, {}) + return config.get("base_url") + + def _get_client(self) -> OpenAI: + """获取 OpenAI 客户端(懒加载)""" + if self._client is None: + api_key = self._get_api_key() + base_url = self._get_base_url() + + kwargs = {"api_key": api_key or ""} + if base_url: + kwargs["base_url"] = base_url + + self._client = OpenAI(**kwargs) + logger.info(f"[{self.provider_name}] 创建 OpenAI 客户端: base_url={base_url or 'default'}") + + return self._client + + def is_available(self) -> bool: + """检查适配器是否可用""" + return bool(self._get_api_key()) + + def _get_extra_params(self, request: ChatCompletionRequest) -> Dict[str, Any]: + """ + 获取额外参数(子类可覆盖) + + Returns: + 传递给 OpenAI API 的额外参数,如 extra_body + """ + return {} + + # ============================================================ + # MCP 扩展点(子类可覆盖) + # ============================================================ + + def _get_mcp_tools(self, request: ChatCompletionRequest) -> List[Dict]: + """ + 获取 MCP 工具定义(子类可覆盖) + + Returns: + MCP 工具列表,格式与 OpenAI tools 相同 + 例如: [{"type": "function", "function": {...}}] + + 示例: + return [{ + "type": "function", + "function": { + "name": "mcp_search", + "description": "通过 MCP 协议搜索", + "parameters": {...} + } + }] + """ + return [] + + def _handle_mcp_tool_call( + self, + tool_name: str, + tool_args: Dict, + request: ChatCompletionRequest + ) -> Optional[str]: + """ + 处理 MCP 工具调用(子类可覆盖) + + Args: + tool_name: 工具名称 + tool_args: 工具参数 + request: 原始请求 + + Returns: + 工具执行结果(字符串),返回 None 表示不是 MCP 工具 + + 示例: + if tool_name == "mcp_search": + # 调用 MCP 客户端 + result = await mcp_client.call(tool_name, tool_args) + return result + return None + """ + return None + + # ============================================================ + # 聊天处理 + # ============================================================ + + async def chat(self, request: ChatCompletionRequest): + """ + 处理聊天请求(统一流程) + """ + client = self._get_client() + + # 打印请求参数 + logger.info(f"[{self.provider_name}] 请求参数:") + logger.info(f" - model: {request.model}") + logger.info(f" - stream: {request.stream}") + logger.info(f" - temperature: {request.temperature}") + logger.info(f" - max_tokens: {request.max_tokens}") + logger.info(f" - deep_thinking: {request.deep_thinking}") + logger.info(f" - web_search: {request.web_search}") + logger.info(f" - deep_search: {request.deep_search}") + + # 构建消息 + messages = self._build_messages(request) + + # 构建请求参数 + kwargs: Dict[str, Any] = { + "model": request.model, + "messages": messages, + "temperature": request.temperature, + "max_tokens": request.max_tokens, + "stream": request.stream, + } + + # 添加特殊参数(由子类实现) + extra_params = self._get_extra_params(request) + + # 分离 extra_body 和其他参数 + # extra_body 需要作为 OpenAI SDK 的单独参数传递 + extra_body = None + if extra_params: + if "extra_body" in extra_params: + extra_body = extra_params.pop("extra_body") + kwargs.update(extra_params) + logger.info(f" - extra_params: {json.dumps(extra_params, ensure_ascii=False)}") + if extra_body: + logger.info(f" - extra_body: {json.dumps(extra_body, ensure_ascii=False)}") + + # 添加 MCP 工具(由子类实现) + mcp_tools = self._get_mcp_tools(request) + if mcp_tools: + if "tools" not in kwargs: + kwargs["tools"] = [] + kwargs["tools"].extend(mcp_tools) + logger.info(f" - mcp_tools: {len(mcp_tools)} 个工具") + + # 单独传递 extra_body + if extra_body: + kwargs["extra_body"] = extra_body + + logger.info(f" - messages: {json.dumps(messages, ensure_ascii=False, indent=2)}") + + if request.stream: + return self._stream_chat(client, kwargs) + else: + return self._sync_chat(client, kwargs) + + def _build_messages(self, request: ChatCompletionRequest) -> List[Dict]: + """ + 构建 OpenAI 格式消息 + + 子类可覆盖以处理特殊格式(如多模态) + """ + messages = [] + + for msg in request.messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if isinstance(content, str): + if content.strip(): + messages.append({"role": role, "content": content}) + elif isinstance(content, list): + # 多模态内容 + openai_content = [] + for item in content: + if isinstance(item, dict): + openai_content.append(item) + if openai_content: + messages.append({"role": role, "content": openai_content}) + + return messages + + def _stream_chat(self, client: OpenAI, kwargs: Dict) -> StreamingResponse: + """流式聊天""" + logger.info(f"[{self.provider_name}] 开始流式响应...") + + # 调试:打印最终传给 API 的参数 + logger.info(f"[{self.provider_name}] API 调用参数:") + for key, value in kwargs.items(): + if key == "messages": + logger.info(f" - {key}: [{len(value)} 条消息]") + elif key == "extra_body": + logger.info(f" - {key}: {json.dumps(value, ensure_ascii=False)}") + elif key == "tools": + logger.info(f" - {key}: {json.dumps(value, ensure_ascii=False)}") + else: + logger.info(f" - {key}: {value}") + + def generator(): + from utils.helpers import generate_unique_id, get_current_timestamp + + full_content = "" + full_reasoning = "" + chunk_count = 0 + + resp = client.chat.completions.create(**kwargs) + + for chunk in resp: + if not chunk.choices: + continue + + chunk_count += 1 + delta = chunk.choices[0].delta + + # 处理深度思考内容 + reasoning_content = getattr(delta, "reasoning_content", None) + if reasoning_content: + full_reasoning += reasoning_content + data = { + "id": f"chatcmpl-{generate_unique_id()}", + "object": "chat.completion.chunk", + "created": get_current_timestamp(), + "model": kwargs["model"], + "choices": [{ + "index": 0, + "delta": {"reasoning_content": reasoning_content}, + "finish_reason": None, + }], + } + yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" + continue + + # 处理普通内容 + content = getattr(delta, "content", None) + if content: + full_content += content + data = { + "id": f"chatcmpl-{generate_unique_id()}", + "object": "chat.completion.chunk", + "created": get_current_timestamp(), + "model": kwargs["model"], + "choices": [{ + "index": 0, + "delta": {"content": content}, + "finish_reason": None, + }], + } + yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n" + + # 结束标记 + finish = { + "id": f"chatcmpl-{generate_unique_id()}", + "object": "chat.completion.chunk", + "created": get_current_timestamp(), + "model": kwargs["model"], + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], + } + yield f"data: {json.dumps(finish, ensure_ascii=False)}\n\n" + yield "data: [DONE]\n\n" + + logger.info(f"[{self.provider_name}] 流式响应完成: chunks={chunk_count}, content_len={len(full_content)}") + + return StreamingResponse(generator(), media_type="text/event-stream") + + def _sync_chat(self, client: OpenAI, kwargs: Dict) -> JSONResponse: + """非流式聊天""" + from utils.helpers import generate_unique_id, get_current_timestamp + + resp = client.chat.completions.create(**kwargs) + + message = resp.choices[0].message + content = message.content or "" + + # 构建响应消息 + response_message = {"role": message.role, "content": content} + + # 处理深度思考内容 + reasoning_content = getattr(message, "reasoning_content", None) + if reasoning_content: + response_message["reasoning_content"] = reasoning_content + + response = { + "id": f"chatcmpl-{generate_unique_id()}", + "object": "chat.completion", + "created": get_current_timestamp(), + "model": kwargs["model"], + "choices": [{ + "index": 0, + "message": response_message, + "finish_reason": resp.choices[0].finish_reason, + }], + } + + if resp.usage: + response["usage"] = { + "prompt_tokens": resp.usage.prompt_tokens, + "completion_tokens": resp.usage.completion_tokens, + "total_tokens": resp.usage.total_tokens, + } + + logger.info(f"[{self.provider_name}] 响应完成: content_len={len(content)}") + if reasoning_content: + logger.info(f"[{self.provider_name}] reasoning_len={len(reasoning_content)}") + + return JSONResponse(content=response) \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt index 881e4e5..4c87754 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,103 +1,65 @@ -aiofiles==24.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 -aiosignal==1.4.0 -aiosqlite==0.22.1 -alibabacloud-oss-v2==1.2.4 -aliyun-python-sdk-core==2.16.0 -aliyun-python-sdk-kms==2.16.5 -annotated-types==0.7.0 -anyio==4.12.1 -argcomplete==3.6.3 -attrs==25.4.0 -banks==2.4.1 -black==26.1.0 -cachetools==7.0.2 -certifi==2026.2.25 -cffi==2.0.0 -charset-normalizer==3.4.4 -click==8.3.1 -colorama==0.4.6 -colorlog==6.10.1 -crcmod==1.7 -crcmod-plus==2.3.1 -cryptography==46.0.5 -dashscope==1.20.12 -dataclasses-json==0.6.7 -dependency-groups==1.3.1 -Deprecated==1.3.1 -dirtyjson==1.0.8 -distlib==0.4.0 -distro==1.9.0 -fastapi==0.115.4 -filelock==3.25.0 -filetype==1.2.0 -frozenlist==1.8.0 -fsspec==2026.2.0 -greenlet==3.3.2 -griffe==2.0.0 -griffecli==2.0.0 -griffelib==2.0.0 -h11==0.16.0 -httpcore==1.0.9 -httpx==0.28.1 -humanize==4.15.0 -idna==3.11 -isort==8.0.1 -Jinja2==3.1.6 -jiter==0.13.0 -jmespath==0.10.0 -joblib==1.5.3 -llama-index-core==0.14.15 -llama-index-instrumentation==0.4.2 -llama-index-readers-dashscope==0.4.1 -llama-index-workflows==2.15.0 -MarkupSafe==3.0.3 -marshmallow==3.26.2 -multidict==6.7.1 -mypy_extensions==1.1.0 -nest-asyncio==1.6.0 -networkx==3.6.1 -nltk==3.9.3 -nox==2026.2.9 -numpy==2.4.2 +# ============================================================ +# 核心依赖 +# ============================================================ openai==2.26.0 -oss2==2.19.1 -packaging==26.0 -pathspec==1.0.4 -pillow==12.1.1 -platformdirs==4.9.2 -propcache==0.4.1 -pycparser==3.0 -pycryptodome==3.23.0 -pydantic==2.12.5 -pydantic_core==2.41.5 -PyJWT==2.11.0 -python-discovery==1.1.0 -python-dotenv==1.0.1 -python-multipart==0.0.18 -pytokens==0.4.1 -PyYAML==6.0.3 -regex==2026.2.28 -requests==2.32.5 -retrying==1.4.2 -setuptools==82.0.0 -six==1.17.0 -sniffio==1.3.1 -SQLAlchemy==2.0.48 -starlette==0.41.3 -tenacity==9.1.4 -tiktoken==0.12.0 -tinytag==2.2.0 -tqdm==4.67.3 -typing-inspect==0.9.0 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -urllib3==2.6.3 +fastapi==0.115.4 uvicorn==0.32.0 -virtualenv==21.1.0 -websocket-client==1.9.0 -wrapt==2.1.1 -yarl==1.23.0 -# zai-sdk==0.2.2 -zhipuai==2.1.5.20250825 +pydantic==2.12.5 +python-dotenv==1.0.1 + +# ============================================================ +# 数据库 +# ============================================================ +SQLAlchemy==2.0.48 +aiosqlite==0.22.1 + +# ============================================================ +# 文件上传 +# ============================================================ +python-multipart==0.0.18 + +# ============================================================ +# 阿里云 OSS +# ============================================================ +alibabacloud-oss-v2==1.2.4 +oss2==2.19.1 + +# ============================================================ +# Token 计算 +# ============================================================ +tiktoken==0.12.0 + +# ============================================================ +# 间接依赖(由上述包自动安装,但显式声明版本) +# ============================================================ +starlette==0.41.3 +httpx==0.28.1 +httpcore==1.0.9 +h11==0.16.0 +anyio==4.12.1 +sniffio==1.3.1 +certifi==2026.2.25 +idna==3.11 +charset-normalizer==3.4.4 +urllib3==2.6.3 +requests==2.32.5 +jiter==0.13.0 +distro==1.9.0 +pydantic_core==2.41.5 +annotated-types==0.7.0 +typing_extensions==4.15.0 +typing-inspect==0.9.0 +tenacity==9.1.4 + +# ============================================================ +# 异步/网络 +# ============================================================ +aiohttp==3.13.3 +aiofiles==24.1.0 + +# ============================================================ +# 其他工具 +# ============================================================ +PyJWT==2.11.0 +PyYAML==6.0.3 +pillow==12.1.1 \ No newline at end of file diff --git a/start.sh b/start.sh index cfd81bc..7072e6e 100644 --- a/start.sh +++ b/start.sh @@ -20,7 +20,7 @@ trap cleanup SIGINT SIGTERM EXIT # 启动后端 echo "[系统] 正在启动后端服务器..." -cd /home/mt/project/ai-chat-ui/server +cd /home/mt/Project/ai-chat-ui/server if [ -d ".venv" ]; then source .venv/bin/activate # 使用 -u 参数强制不缓冲输出,实时显示日志 @@ -34,7 +34,7 @@ sleep 2 # 启动前端 echo "[系统] 正在启动前端服务器..." -cd /home/mt/project/ai-chat-ui +cd /home/mt/Project/ai-chat-ui # 启动 vite 开发服务器 npm run dev &