refactor: overhaul LLM toolkit architecture and standardize observability

- Core Refactoring: Migrated OpenAICompatibleLoader to a structured class design (ProviderRegistry, MessageBuilder, TokenEstimator) for enhanced maintainability.
- AsyncIO Fix: Resolved 'Event loop is closed' RuntimeError by transitioning to asyncio.run() for robust event loop lifecycle management within ComfyUI's threaded environment.
- Node Enhancements:
    - Added 'seed' parameter for deterministic cache invalidation control.
    - Fixed silent drop of system_prompt in stateless mode (enable_memory=False).
    - Refined category hierarchy: [Generate, Loader, Image] under a unified toolkit brand.
- Observability: Implemented project-wide geek-style logging with [LLMs_Toolkit] branding, execution latency tracking, and high-density token reporting.
- Cleanup: Removed legacy DeepSeek local model vertical, dependencies, and redundant utility nodes.
- Documentation: Simplified README and updated .gitignore for developer hygiene.
This commit is contained in:
科林_Cyril Pilgrim
2026-02-09 15:26:02 +08:00
parent d04fff29e2
commit 056cf7ecc4
7 changed files with 379 additions and 486 deletions
+3
View File
@@ -31,3 +31,6 @@ ENV/
# OS
.DS_Store
Thumbs.db
# Tests
tests/
Binary file not shown.
-241
View File
@@ -1,241 +0,0 @@
<div align="center">
# 🚀 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自定义节点套件**
</div>
---
## 📖 项目介绍
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 | 多任务处理,智能推理 |
### 🛠️ 功能模块
<div align="center">
| 模块名称 | 功能描述 | 适用场景 |
|---------|---------|----------|
| 🔧 **LLM加载器** | 配置和加载不同的大语言模型 | 模型切换、参数调优 |
| 🎨 **图像生成** | 基于文本描述生成高质量图像 | 创意设计、内容创作 |
| 👁️ **图像理解** | 智能分析和描述图像内容 | 图像标注、内容审核 |
| 📦 **模型管理** | 统一管理各种AI模型资源 | 模型版本控制、资源优化 |
| 🔌 **OpenAI兼容** | 支持OpenAI格式的API调用 | 兼容性扩展、迁移便利 |
| 🖼️ **图像预处理** | 智能优化图像质量和格式 | 数据清洗、格式转换 |
</div>
---
## 🚀 快速开始
### 📋 环境要求
- **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助手
- 👥 所有贡献者和用户的支持与反馈
---
<div align="center">
**⭐ 如果这个项目对您有帮助,请给我们一个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)
</div>
-1
View File
@@ -1 +0,0 @@
# Jupyter示例将在这里提供
-1
View File
@@ -1 +0,0 @@
# 示例工作流将在这里提供
+372 -239
View File
@@ -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"]
+4 -4
View File
@@ -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,)