diff --git a/.gitignore b/.gitignore index 07103d0..e0fb6bc 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ ENV/ # OS .DS_Store Thumbs.db + +# Tests +tests/ diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc index 92290f8..840a850 100644 Binary files a/__pycache__/__init__.cpython-313.pyc and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 847fdd8..0000000 --- a/docs/README.md +++ /dev/null @@ -1,241 +0,0 @@ -
- -# 🚀 ComfyUI-DeepSeek-Toolkit - -**为ComfyUI工作流程注入强大的大语言模型力量!** - -[![GitHub Stars](https://img.shields.io/github/stars/HuangYuChuh/ComfyUI-DeepSeek-Toolkit?style=flat-square&logo=github)](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/stargazers) -[![GitHub Forks](https://img.shields.io/github/forks/HuangYuChuh/ComfyUI-DeepSeek-Toolkit?style=flat-square&logo=github)](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/network) -[![License](https://img.shields.io/github/license/HuangYuChuh/ComfyUI-DeepSeek-Toolkit?style=flat-square)](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/blob/main/LICENSE) -[![Python](https://img.shields.io/badge/Python-3.8+-blue?style=flat-square&logo=python)](https://www.python.org/) - -**一个集成中国领先大语言模型的ComfyUI自定义节点套件** - -
- ---- - -## 📖 项目介绍 - -ComfyUI-DeepSeek-Toolkit 是一个专为ComfyUI设计的自定义节点包,致力于将中国最优秀的大语言模型(如DeepSeek、通义千问等)无缝集成到您的AI工作流程中。通过轻量级的API驱动方案,即使是计算资源有限的用户也能轻松体验到前沿AI技术的强大能力。 - -### 🎯 项目愿景 - -> **让AI技术惠及每一个创作者** -> -> 我们相信AI应该是赋能工具,而非不平等的根源。通过提供易于访问的解决方案,我们致力于让AI成为一种更具包容性和公平性的技术。 - ---- - -## ✨ 核心特性 - -### 🤖 支持的大语言模型 - -| 模型提供商 | 模型名称 | 特色功能 | -|-----------|---------|---------| -| 🔥 **DeepSeek** | DeepSeek-V2/V3 | 超强推理能力,代码生成专家 | -| 🌟 **通义千问** | Qwen-Max/Plus | 多模态理解,中文优化 | -| 🚀 **豆包** | Doubao-Pro | 对话生成,创意写作 | -| ⚡ **星火** | Spark-Max | 语言理解,文本分析 | -| 🧠 **智谱清言** | GLM-4 | 知识问答,逻辑推理 | -| 🌙 **月之暗面** | Moonshot-V1 | 长文本处理,上下文理解 | -| 🌊 **百川** | Baichuan-Turbo | 快速响应,高效处理 | -| 🎭 **MiniMax** | ABAB-6.5 | 角色扮演,创意生成 | -| 🔮 **阶跃星辰** | Step-1X | 多任务处理,智能推理 | - -### 🛠️ 功能模块 - -
- -| 模块名称 | 功能描述 | 适用场景 | -|---------|---------|----------| -| 🔧 **LLM加载器** | 配置和加载不同的大语言模型 | 模型切换、参数调优 | -| 🎨 **图像生成** | 基于文本描述生成高质量图像 | 创意设计、内容创作 | -| 👁️ **图像理解** | 智能分析和描述图像内容 | 图像标注、内容审核 | -| 📦 **模型管理** | 统一管理各种AI模型资源 | 模型版本控制、资源优化 | -| 🔌 **OpenAI兼容** | 支持OpenAI格式的API调用 | 兼容性扩展、迁移便利 | -| 🖼️ **图像预处理** | 智能优化图像质量和格式 | 数据清洗、格式转换 | - -
- ---- - -## 🚀 快速开始 - -### 📋 环境要求 - -- **Python**: 3.8 或更高版本 -- **ComfyUI**: 最新稳定版本 -- **操作系统**: Windows / macOS / Linux - -### 🔧 安装指南 - -#### 方法一:通过ComfyUI Manager安装(推荐) - -1. 在ComfyUI中打开 **Manager** 面板 -2. 搜索 \`ComfyUI-DeepSeek-Toolkit\` -3. 点击 **安装** 并重启ComfyUI - -#### 方法二:手动安装 - -\`\`\`bash -# 进入ComfyUI的custom_nodes目录 -cd ComfyUI/custom_nodes/ - -# 克隆项目 -git clone https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit.git - -# 进入项目目录 -cd ComfyUI-DeepSeek-Toolkit - -# 安装依赖 -pip install -r requirements.txt -\`\`\` - -### 🔑 API密钥配置 - -在使用前,您需要获取相应模型的API密钥: - -1. **DeepSeek**: [获取API密钥](https://platform.deepseek.com/) -2. **通义千问**: [获取API密钥](https://dashscope.aliyun.com/) -3. **其他模型**: 请参考 [LLM API调用指南](./LLM%20API%20调用指南(中国版).md) - ---- - -## 📚 使用指南 - -### 🎯 基础工作流程 - -1. **加载LLM节点** - - 在ComfyUI中添加 \`DeepSeek LLM Loader\` 节点 - - 配置API密钥和模型参数 - -2. **连接功能节点** - - 根据需求添加图像生成/理解节点 - - 配置输入输出连接 - -3. **运行工作流程** - - 设置提示词和参数 - - 执行并查看结果 - ---- - -## 🛠️ 技术架构 - -### 📦 依赖项 - -\`\`\`txt -torch>=1.9.0 -numpy>=1.21.0 -Pillow>=8.3.0 -aiohttp>=3.8.0 -transformers>=4.20.0 -git+https://github.com/deepseek-ai/Janus.git -\`\`\` - -### 🏗️ 项目结构 - -\`\`\` -ComfyUI-DeepSeek-Toolkit/ -├── 📁 py/ # 核心模块目录 -│ ├── 🔧 llm_loader.py # LLM加载器 -│ ├── 🎨 image_generation.py # 图像生成模块 -│ ├── 👁️ image_understanding.py # 图像理解模块 -│ ├── 📦 model_loader.py # 模型管理器 -│ ├── 🔌 openai_compatible_loader.py # OpenAI兼容层 -│ └── 🖼️ image_preprocessor.py # 图像预处理器 -├── 📄 __init__.py # 主入口文件 -├── 📋 requirements.txt # 依赖列表 -├── 📖 README.md # 项目文档 -├── 📘 readme_en.md # 英文文档 -├── 📙 LLM API 调用指南(中国版).md # API调用指南 -└── 📜 LICENSE # 开源协议 -\`\`\` - ---- - -## 🤝 贡献指南 - -我们欢迎所有形式的贡献! - -### 🐛 报告问题 - -如果您发现了bug或有功能建议: - -1. 在 [Issues](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/issues) 中搜索相似问题 -2. 如果没有找到,请创建新的issue -3. 请提供详细的描述和复现步骤 - -### 💡 提交代码 - -1. Fork本项目 -2. 创建功能分支 (\`git checkout -b feature/AmazingFeature\`) -3. 提交更改 (\`git commit -m 'Add some AmazingFeature'\`) -4. 推送到分支 (\`git push origin feature/AmazingFeature\`) -5. 创建Pull Request - ---- - -## 📈 发展路线图 - -### 🎯 近期计划 - -- [ ] 🚀 **v1.1.0** - - [ ] 支持更多视频理解模型 - - [ ] 增加批处理功能 - - [ ] 优化内存使用 - -- [ ] 🌟 **v1.2.0** - - [ ] 集成更多中国AI模型 - - [ ] 添加模型微调功能 - - [ ] 提供预构建工作流模板 - -### 🔮 长期规划 - -- 🧠 **智能化升级**: 自适应模型选择和参数优化 -- 🌐 **生态扩展**: 与更多AI平台和工具集成 -- 📊 **性能监控**: 实时性能分析和优化建议 -- 🎓 **教育资源**: 完善的教程和案例库 - ---- - -## 📞 支持与反馈 - -### 💬 社区交流 - -- **GitHub Issues**: [问题反馈](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/issues) -- **GitHub Discussions**: [社区讨论](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/discussions) - -### 📧 联系方式 - -如有商务合作或其他询问,请通过以下方式联系: - -- **项目维护者**: [@HuangYuChuh](https://github.com/HuangYuChuh) -- **项目主页**: [ComfyUI-DeepSeek-Toolkit](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit) - ---- - -## 📄 开源协议 - -本项目采用 [GPL-2.0 License](https://github.com/HuangYuChuh/ComfyUI-DeepSeek-Toolkit/blob/main/LICENSE) 开源协议。 - ---- - -## 🙏 致谢 - -感谢以下项目和社区的支持: - -- 🎨 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) - 强大的AI工作流平台 -- 🤖 [DeepSeek](https://www.deepseek.com/) - 优秀的大语言模型 -- 🌟 [阿里云通义千问](https://tongyi.aliyun.com/) - 多模态AI助手 -- 👥 所有贡献者和用户的支持与反馈 - ---- - -
- -**⭐ 如果这个项目对您有帮助,请给我们一个Star!** - -[![Star History Chart](https://api.star-history.com/svg?repos=HuangYuChuh/ComfyUI-DeepSeek-Toolkit&type=Date)](https://star-history.com/#HuangYuChuh/ComfyUI-DeepSeek-Toolkit&Date) - -
diff --git a/examples/notebooks/README.md b/examples/notebooks/README.md deleted file mode 100644 index e9d26ec..0000000 --- a/examples/notebooks/README.md +++ /dev/null @@ -1 +0,0 @@ -# Jupyter示例将在这里提供 diff --git a/examples/workflows/README.md b/examples/workflows/README.md deleted file mode 100644 index 19c02de..0000000 --- a/examples/workflows/README.md +++ /dev/null @@ -1 +0,0 @@ -# 示例工作流将在这里提供 diff --git a/src/core/openai_compatible.py b/src/core/openai_compatible.py index 9db8ddd..adac536 100644 --- a/src/core/openai_compatible.py +++ b/src/core/openai_compatible.py @@ -1,21 +1,175 @@ -import comfy -import folder_paths -import nodes -import aiohttp -import json import asyncio +import time +import random +from typing import Optional, List, Dict, Tuple, Any +from dataclasses import dataclass from aiohttp import ClientSession, ClientError -from typing import Optional, List, Dict -import time # 添加时间模块 -import random # 添加随机数模块 -import torch + + +@dataclass +class ProviderConfig: + """LLM provider endpoint configuration""" + name: str + base_url: str + requires_string_content: bool = False + + +class ProviderRegistry: + """Centralized registry for LLM API providers""" + + PROVIDERS = { + "Qwen/通义千问": ProviderConfig( + "Qwen", + "https://dashscope.aliyuncs.com/compatible-mode/v1" + ), + "DeepSeek/深度求索": ProviderConfig( + "DeepSeek", + "https://api.deepseek.com/v1/" + ), + "DouBao/豆包": ProviderConfig( + "DouBao", + "https://ark.cn-beijing.volces.com/api/v3/" + ), + "Spark/星火": ProviderConfig( + "Spark", + "https://spark-api-open.xf-yun.com/v1/", + requires_string_content=True + ), + "GLM/智谱清言": ProviderConfig( + "GLM", + "https://open.bigmodel.cn/api/paas/v4/" + ), + "Moonshot/月之暗面": ProviderConfig( + "Moonshot", + "https://api.moonshot.cn/v1" + ), + "Baichuan/百川": ProviderConfig( + "Baichuan", + "https://api.baichuan-ai.com/v1/", + requires_string_content=True + ), + "MiniMax/MiniMax": ProviderConfig( + "MiniMax", + "https://api.minimax.chat/v1/" + ), + "StepFun/阶跃星辰": ProviderConfig( + "StepFun", + "https://api.stepfun.com/v1/" + ), + "SenseChat/日日新": ProviderConfig( + "SenseChat", + "https://api.sensenova.cn/compatible-mode/v1", + requires_string_content=True + ), + } + + @classmethod + def get_provider(cls, provider_key: str) -> ProviderConfig: + """Get provider config or treat key as custom URL""" + return cls.PROVIDERS.get(provider_key.strip(), + ProviderConfig("Custom", provider_key.strip())) + + +class MessageBuilder: + """Builds chat messages with support for multimodal content""" + + @staticmethod + def build_content(prompt: str, image_url: Optional[str] = None) -> List[Dict[str, Any]]: + """Build content array for user message""" + content = [] + + if image_url: + if not image_url.startswith("data:image"): + raise ValueError("Image must be a valid base64 data URL") + content.append({ + "type": "image_url", + "image_url": {"url": image_url} + }) + + if prompt.strip(): + content.append({ + "type": "text", + "text": prompt + }) + + if not content: + raise ValueError("Content cannot be empty") + + return content + + @staticmethod + def build_messages( + content: List[Dict[str, Any]], + system_prompt: Optional[str] = None + ) -> List[Dict[str, Any]]: + """Build complete message array""" + messages = [] + + if system_prompt: + messages.append({ + "role": "system", + "content": system_prompt + }) + + messages.append({ + "role": "user", + "content": content + }) + + return messages + + @staticmethod + def adapt_for_provider( + messages: List[Dict[str, Any]], + provider: ProviderConfig, + prompt: str + ) -> List[Dict[str, Any]]: + """Adapt messages for provider-specific requirements""" + if provider.requires_string_content: + # Some providers require string content, not structured arrays + adapted = [] + for msg in messages: + if msg["role"] == "system": + adapted.append(msg) + elif msg["role"] == "user": + adapted.append({ + "role": "user", + "content": prompt if isinstance(prompt, str) else prompt + }) + return adapted + return messages + + +class TokenEstimator: + """Estimates token usage for billing/logging purposes""" + + @staticmethod + def estimate_input_tokens( + prompt: str, + system_prompt: Optional[str] = None, + image_url: Optional[str] = None + ) -> int: + """Simple character-based token estimation""" + tokens = len(prompt) + + if system_prompt: + tokens += len(system_prompt) + + if image_url: + # Rough estimate: base64 image ~= length/1000 tokens + tokens += len(image_url) // 1000 + + return tokens class OpenAICompatibleLoader: """ - Custom node for OpenAI compatible API integration + OpenAI-compatible API adapter for ComfyUI + + Supports multiple LLM providers with unified interface. + Designed for extensibility and clean architecture. """ - + @classmethod def INPUT_TYPES(cls): return { @@ -29,251 +183,230 @@ class OpenAICompatibleLoader: }, "optional": { "prep_img": ("STRING", {"default": "", "forceInput": True}), - "system_prompt": ("STRING", {"default": "你是一个AI大模型", "multiline": True}), + "system_prompt": ("STRING", { + "default": "你是一个AI大模型", + "multiline": True + }), "prompt": ("STRING", {"multiline": True}), - "temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0}), - "max_tokens": ("INT", {"default": 512, "min": 1, "max": 4096}), - "enable_memory": ("BOOLEAN", {"default": False, "label": "Enable Memory"}), + "temperature": ("FLOAT", { + "default": 0.7, + "min": 0.0, + "max": 2.0 + }), + "max_tokens": ("INT", { + "default": 512, + "min": 1, + "max": 4096 + }), + "enable_memory": ("BOOLEAN", { + "default": False, + "label": "Enable Memory" + }), + "seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xffffffffffffffff + }), "api_key": ("STRING", {"default": ""}) } } - + RETURN_TYPES = ("STRING", "INT", "INT") RETURN_NAMES = ("text", "input_tokens", "output_tokens") FUNCTION = "generate" - CATEGORY = "🚦ComfyUI_LLMs_Toolkit" - - async def async_generate(self, payload: dict, actual_base_url: str, api_key: str): - try: - async with ClientSession() as session: - try: - async with session.post( - f"{actual_base_url}/chat/completions", - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}" - }, - json=payload - ) as response: - # 打印完整的请求内容以便调试 - # Simplified debug log - print(f"[DEBUG] Request sent to model: {payload['model']}, temp: {payload['temperature']}, max_tokens: {payload['max_tokens']}") - response.raise_for_status() - data = await response.json() - # 移除冗长的API响应日志 - response_content = data['choices'][0]['message']['content'] - # 简化后的响应内容日志 - print(f"Output: {response_content[:50]}...") # 只保留前50个字符 - return [response_content, data] # 返回值包括 response_content 和 data - except Exception as e: - print(f"[ERROR] 请求失败: {str(e)}") # 打印错误信息 - raise - except ClientError as e: - raise Exception(f"API request failed: {str(e)}") - - def generate(self, base_url: str, api_key: str, prompt: str, - model: str, temperature: float, - max_tokens: int, system_prompt: Optional[str] = None, prep_img: Optional[str] = None, enable_memory: bool = False, seed: Optional[int] = None): - - content = [] # 初始化内容列表 - - # 移除图像参数类型的日志 - - if prep_img: - # 打印 prep_img 调试信息 - print(f"[DEBUG] Received prep_img: {prep_img[:50]}...") # 只保留前50个字符 - # 验证 prep_img 是否为有效的 base64 编码字符串 - if not prep_img.startswith("data:image"): - raise ValueError("Processed image must be a valid base64 encoded string") - - # 将 prep_img 添加到 content 中 - content.append({"type": "image_url", "image_url": {"url": prep_img}}) - - - - if prompt.strip(): - content.append({"type": "text", "text": prompt}) - - messages = [] # 初始化 messages 列表 - if system_prompt: - messages.append({ - "role": "system", - "content": system_prompt - }) - - if content: # 只有当 content 列表不为空时才添加到 messages - messages.append({ - "role": "user", - "content": content - }) - # 移除带有图像内容的消息日志 - elif not prompt.strip() and not system_prompt and prep_img is None: - raise ValueError("用户输入的 prompt 不能为空") - - # 模型选择逻辑 (保持不变) - selected_model = model if model else "glm-4" # 默认模型为 glm-4 - print(f"[INFO] 使用模型: {selected_model}") - - # base_url 映射表 (保持不变) - base_url_mapping = { - "Qwen/通义千问": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "DeepSeek/深度求索": "https://api.deepseek.com/v1/", - "DouBao/豆包": "https://ark.cn-beijing.volces.com/api/v3/", - "Spark/星火": "https://spark-api-open.xf-yun.com/v1/", - "GLM/智谱清言": "https://open.bigmodel.cn/api/paas/v4/", - "Moonshot/月之暗面": "https://api.moonshot.cn/v1", - "Baichuan/百川": "https://api.baichuan-ai.com/v1/", - "MiniMax/MiniMax": "https://api.minimax.chat/v1/", - "StepFun/阶跃星辰": "https://api.stepfun.com/v1/", - "SenseChat/日日新": "https://api.sensenova.cn/compatible-mode/v1" - } - actual_base_url = base_url_mapping.get(base_url.strip(), base_url) - - # 对话历史和 payload 构建 (保持不变) - if not enable_memory: - self._conversation_history = [] - - if not hasattr(self, "_conversation_history"): - self._conversation_history = [] - - if system_prompt and not any(msg["role"] == "system" for msg in self._conversation_history): - self._conversation_history.append({"role": "system", "content": system_prompt}) - - # 避免重复添加内容 - # 修改对话历史添加逻辑 - if enable_memory: - if not any(msg["role"] == "user" and msg["content"] == content for msg in self._conversation_history): - self._conversation_history.append({"role": "user", "content": content}) - else: - # 当禁用记忆时,总是创建新的对话历史 - self._conversation_history = [{"role": "user", "content": content}] - # 重构消息格式化逻辑 - if enable_memory: - # 使用对话历史生成消息 - formatted_messages = [] - for msg in self._conversation_history: - if isinstance(msg["content"], list): - formatted_messages.append({ - "role": msg["role"], - "content": msg["content"] - }) - else: - formatted_messages.append(msg) - else: - # 当禁用记忆时,直接使用当前消息 - formatted_messages = [ - { - "role": "user", - "content": content - } - ] - # 添加时间戳确保请求唯一性 - # 处理随机种子 - seed_value = seed if seed is not None else random.randint(1, 1000000) + CATEGORY = "🚦ComfyUI_LLMs_Toolkit/Generate" + + def __init__(self): + self._conversation_history: List[Dict[str, Any]] = [] + self._request_start_time: float = 0.0 + + def _log_request_start( + self, + provider: ProviderConfig, + model: str, + system_prompt: Optional[str], + prompt: str, + image_attached: bool + ) -> None: + """Geek-style request logging with timing""" + self._request_start_time = time.time() + ts = time.strftime('%H:%M:%S') - payload = { - "model": selected_model, - "messages": formatted_messages, + # Build context flags + flags = [] + if system_prompt: + flags.append('sys') + if image_attached: + flags.append('img') + + flag_str = f"[{' '.join(flags)}]" if flags else "" + + # Compact header + print(f"[LLMs_Toolkit] {ts} → {provider.name}/{model or 'default'} {flag_str}") + + # Content preview (indented) + if system_prompt: + sys_preview = system_prompt[:60].replace('\n', ' ') + print(f" sys│ {sys_preview}...") + + prompt_preview = prompt[:80].replace('\n', ' ') + print(f" in │ {prompt_preview}...") + + def _log_request_complete( + self, + response: str, + input_tokens: int, + output_tokens: int + ) -> None: + """Geek-style completion logging with metrics""" + # Calculate elapsed time + elapsed_ms = int((time.time() - self._request_start_time) * 1000) + + # Format token counts (k suffix for thousands) + in_tok = f"{input_tokens/1000:.1f}k" if input_tokens >= 1000 else str(input_tokens) + out_tok = f"{output_tokens/1000:.1f}k" if output_tokens >= 1000 else str(output_tokens) + + # Response preview + preview = response[:80].replace('\n', ' ') + + print(f" out│ {preview}...") + print(f"[LLMs_Toolkit] {time.strftime('%H:%M:%S')} ← {in_tok}/{out_tok}t ({elapsed_ms}ms)\n") + + async def _async_api_call( + self, + endpoint: str, + payload: Dict[str, Any], + api_key: str + ) -> Tuple[str, Dict[str, Any]]: + """Core async API call with proper error handling""" + async with ClientSession() as session: + try: + async with session.post( + f"{endpoint}/chat/completions", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + }, + json=payload, + timeout=60 + ) as response: + response.raise_for_status() + data = await response.json() + + content = data["choices"][0]["message"]["content"] + + return content, data + + except ClientError as e: + raise Exception(f"API request failed: {str(e)}") + + def _build_payload( + self, + model: str, + messages: List[Dict[str, Any]], + temperature: float, + max_tokens: int + ) -> Dict[str, Any]: + """Build API request payload""" + return { + "model": model or "glm-4", + "messages": messages, "temperature": temperature, "max_tokens": max_tokens, - "request_id": f"req-{int(time.time() * 1000)}-{hash(str(content))}", # 唯一请求ID - "timestamp": int(time.time() * 1000), # 毫秒级时间戳 - "seed": seed_value # 添加随机种子 + "request_id": f"req-{int(time.time() * 1000)}-{random.randint(1000, 9999)}", + "timestamp": int(time.time() * 1000), + "seed": random.randint(1, 1000000) } + + def _manage_conversation_history( + self, + messages: List[Dict[str, Any]], + enable_memory: bool + ) -> List[Dict[str, Any]]: + """Manage conversation history with memory support""" + if not enable_memory: + self._conversation_history = [] + return messages + + # Append to history and return full context + for msg in messages: + if not any( + h["role"] == msg["role"] and h["content"] == msg["content"] + for h in self._conversation_history + ): + self._conversation_history.append(msg) + + return self._conversation_history + + def generate( + self, + base_url: str, + api_key: str, + prompt: str, + model: str, + temperature: float, + max_tokens: int, + system_prompt: Optional[str] = None, + prep_img: Optional[str] = None, + enable_memory: bool = False, + seed: int = 0 + ) -> Tuple[str, int, int]: + """Main generation entry point""" + + # Get provider configuration + provider = ProviderRegistry.get_provider(base_url) + + # Log request details + self._log_request_start( + provider=provider, + model=model, + system_prompt=system_prompt, + prompt=prompt, + image_attached=bool(prep_img) + ) + + # Build message content + content = MessageBuilder.build_content(prompt, prep_img) + + # Build messages with system prompt + messages = MessageBuilder.build_messages(content, system_prompt) + + # Apply provider-specific adaptations + messages = MessageBuilder.adapt_for_provider(messages, provider, prompt) + + # Manage conversation history + messages = self._manage_conversation_history(messages, enable_memory) + + # Build request payload + payload = self._build_payload(model, messages, temperature, max_tokens) + + # Estimate input tokens + input_tokens = TokenEstimator.estimate_input_tokens( + prompt, system_prompt, prep_img + ) - if "spark-api-open.xf-yun.com" in actual_base_url or "api.baichuan-ai.com" in actual_base_url or "api.sensenova.cn" in actual_base_url: - # 星火大模型需要 content 为字符串 - payload["messages"] = [ - { - "role": "system", - "content": system_prompt - }, - { - "role": "user", - "content": prompt if isinstance(prompt, str) else prompt[0]["text"] - } - ] - else: - # 其他模型保持现有结构 - payload["messages"] = formatted_messages - - # 简化后的调试日志 - print(f"[DEBUG] Using model: {selected_model}") - print(f"[DEBUG] Base URL: {actual_base_url}") - - # 确保 temperature 和 max_tokens 参数符合范围 - if not isinstance(temperature, float): - try: - temperature = float(temperature) - except ValueError: - raise ValueError(f"temperature 参数无法转换为浮点数: {temperature}") - if not (0.0 <= temperature <= 2.0): - raise ValueError(f"temperature 参数超出范围: {temperature}") - - try: - max_tokens = int(max_tokens) - except ValueError: - raise ValueError(f"max_tokens 参数无法转换为整数: {max_tokens}") - if not (1 <= max_tokens <= 4096): - raise ValueError(f"max_tokens 参数超出范围: {max_tokens}") - - # 移除完整的payload日志 - # 简化后的调用日志 - # 使用时间戳代替 uuid 生成唯一标识符 - print(f"[{time.strftime('%Y/%m/%d %H:%M:%S')}] INFO PromptTask {int(time.time())}") - print(f"Input: {prompt}") - print(f"HTTP Request: POST {actual_base_url}/chat/completions \"HTTP/1.1 200 OK\"") - # Token 计算逻辑 - def count_tokens(content): - return sum(len(str(item).split()) for item in content) - - # 按字符分割 - input_tokens = len(prompt) - if system_prompt: - input_tokens += len(system_prompt) - if prep_img: - # 根据 prep_img 计算 token - if isinstance(prep_img, str): # Base64 编码的字符串 - # 估算 token 数量(基于字符串长度) - image_tokens = len(prep_img) // 1000 - else: - image_tokens = 0 # 未知类型,默认为 0 - input_tokens += image_tokens - - try: - time.sleep(1) - import asyncio - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - task = loop.create_task(self.async_generate(payload, actual_base_url, api_key)) - response_content, data = loop.run_until_complete(task) - # Extract completion_tokens from API response - completion_tokens = data.get("usage", {}).get("completion_tokens", 0) - return [response_content, int(input_tokens), int(completion_tokens)] - except Exception as e: - print(f"[ERROR] 异步任务失败: {str(e)}") - raise - finally: - try: - if hasattr(loop, 'shutdown_asyncgens'): - loop.run_until_complete(loop.shutdown_asyncgens()) - # Ensure all tasks are done before closing the loop - pending = asyncio.all_tasks(loop) - if pending: - print(f"[WARNING] There are {len(pending)} pending tasks. Waiting for them to complete...") - loop.run_until_complete(asyncio.gather(*pending)) - finally: - loop.close() + # Use asyncio.run() for clean event loop management + response_content, data = asyncio.run( + self._async_api_call(provider.base_url, payload, api_key) + ) + + # Extract actual token usage from response + output_tokens = data.get("usage", {}).get("completion_tokens", 0) + + # Log completion + self._log_request_complete(response_content, input_tokens, output_tokens) + + return (response_content, int(input_tokens), int(output_tokens)) + except Exception as e: - raise Exception(f"请求失败: {str(e)}") + print(f"[LLMs_Toolkit] ✗ {str(e)}") + raise -# 注册节点 (保持不变) +# ComfyUI node registration NODE_CLASS_MAPPINGS = {"OpenAICompatibleLoader": OpenAICompatibleLoader} NODE_DISPLAY_NAME_MAPPINGS = {"OpenAICompatibleLoader": "OpenAI Compatible Adapter"} WEB_DIRECTORY = "./web" -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/src/processors/image_processor.py b/src/processors/image_processor.py index 7f7344d..6f5edac 100644 --- a/src/processors/image_processor.py +++ b/src/processors/image_processor.py @@ -30,7 +30,7 @@ class ImagePreprocessor: quality_map = {"High": 95, "Medium": 75, "Low": 50} quality_str = quality # Ensure quality is treated as a string quality_val = quality_map.get(quality_str, 95) # Default to High if invalid value - print(f"Selected quality: {quality_val}") # Debugging line + print(f"[LLMs_Toolkit] quality={quality_val}") if image is None: raise ValueError("Image input cannot be None") @@ -50,7 +50,7 @@ class ImagePreprocessor: # Resize image based on quality size_map = {"High": 1024, "Medium": 768, "Low": 512} max_size = size_map.get(quality_str, 1024) - print(f"Resizing image to max dimension: {max_size}") + print(f"[LLMs_Toolkit] resize={max_size}px") image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) # Convert PIL image to base64 string @@ -58,8 +58,8 @@ class ImagePreprocessor: image.save(buffered, format=format, quality=quality_val) img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") image_url = f"data:image/{format.lower()};base64,{img_str}" - print(f"Image size with quality {quality}: {buffered.tell()} bytes") - print(f"Generated image URL: {image_url[:50]}...") # 打印生成的图像 URL 前 50 个字符 + size_kb = buffered.tell() / 1024 + print(f"[LLMs_Toolkit] encoded={size_kb:.1f}KB {format}") return (image_url,)