feat: ZenMux 节点新增 usage_stats 输出,统计单次运行 token 消耗与费用
- 第三输出 usage_stats(STRING),三行格式: token消耗,输入:XXX,输出:XXX 模型类型:openai/gpt-5.4-nano [入$0.2/M 出$1.25/M] 价格换算,美元:XXX,人民币:XXX - 费用 = 响应 usage 的 token 数 × 快照单价(USD/百万token) - 新增可选参数 usd_to_cny(默认 7.2)供人民币换算,可按当日牌价调整 - 请求失败记 0 消耗;usage 缺失或单价未知的项显示 "?" Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cc5852dc18
commit
0a87ba7813
@@ -723,6 +723,13 @@ ComfyUI-SDMatte 传 `aux_input="trimap"`,把模型推到了没训练过的输
|
||||
- **快速筛选**: 模型列表按「厂商/模型名」排序聚类,同厂商模型天然相邻;在下拉的搜索框输入厂商前缀(如 `qwen/`、`anthropic/`)即可只看该厂商的模型
|
||||
- **离线可用的模型清单**: 模型与价格来自随包分发的 `zenmux/models_snapshot.json`;价格有变动时运行 `python zenmux/build_snapshot.py` 即可重新拉取更新
|
||||
- **旧工作流兼容**: 价格快照更新后,旧工作流里保存的带旧价格标签仍能正确解析出模型 id,不会失效
|
||||
- **单次消耗统计**: `usage_stats` 输出本次运行的 token 用量与费用换算(按快照单价计算,汇率可调),格式:
|
||||
|
||||
```
|
||||
token消耗,输入:1234,输出:567
|
||||
模型类型:openai/gpt-5.4-nano [入$0.2/M 出$1.25/M]
|
||||
价格换算,美元:0.000955,人民币:0.006876
|
||||
```
|
||||
|
||||
**输入参数**:
|
||||
- `api_key` (STRING): ZenMux 平台的 API Key(在 zenmux.ai 控制台获取)
|
||||
@@ -738,10 +745,12 @@ ComfyUI-SDMatte 传 `aux_input="trimap"`,把模型推到了没训练过的输
|
||||
- `image_max_size` (INT, 可选): 发送前图像最长边缩放上限,默认 1024
|
||||
- `base_url` (STRING, 可选): API 地址,默认 `zenmux.ai/api/v1`(无需写 `https://`,节点会自动补全)
|
||||
- `proxy_url` (STRING, 可选): HTTP/HTTPS 代理地址,如 `127.0.0.1:7890`
|
||||
- `usd_to_cny` (FLOAT, 可选): 美元兑人民币汇率,默认 7.2,用于 `usage_stats` 的人民币换算,可按当日牌价调整
|
||||
|
||||
**输出**:
|
||||
- `text` (STRING): 模型生成的文本内容
|
||||
- `model_id` (STRING): 实际调用的模型 id(如 `openai/gpt-5.4-nano`),便于下游记录
|
||||
- `usage_stats` (STRING): 单次运行的 token 消耗与费用统计(三行文本,格式见上);请求失败时记为 0 消耗,token 数缺失或单价未知的项显示 `?`
|
||||
|
||||
**使用场景**:
|
||||
- 一个 Key 试遍多家厂商的模型,横向对比效果与成本
|
||||
|
||||
@@ -7,6 +7,8 @@ ZenMux 模型注册表
|
||||
- all_model_labels() 全量「带价格」下拉标签,按厂商 → 模型排序聚类
|
||||
- default_model_label() 默认模型 openai/gpt-5.4-nano 对应的标签
|
||||
- label_to_model_id() 从下拉标签解析真实 model id(对旧标签/纯 id 也兼容)
|
||||
- model_prices() 按 model id 查(输入单价, 输出单价),USD/百万 token
|
||||
- model_label_by_id() 按 model id 生成当前快照的带价签标签
|
||||
|
||||
标签格式(价格单位:USD / 百万 token):
|
||||
openai/gpt-5.4-nano [入$0.04/M 出$0.32/M]
|
||||
@@ -63,6 +65,7 @@ _MODELS = _load_models()
|
||||
_LABELS = [_make_label(m) for m in _MODELS]
|
||||
_LABEL_TO_ID = {lb: m["id"] for lb, m in zip(_LABELS, _MODELS)}
|
||||
_KNOWN_IDS = {m["id"] for m in _MODELS}
|
||||
_ID_TO_MODEL = {m["id"]: m for m in _MODELS}
|
||||
|
||||
|
||||
def all_model_labels():
|
||||
@@ -76,6 +79,20 @@ def default_model_label():
|
||||
return _LABELS[0]
|
||||
|
||||
|
||||
def model_prices(model_id):
|
||||
"""按 model id 查(输入单价, 输出单价),单位 USD/百万 token;未知模型 (None, None)。"""
|
||||
m = _ID_TO_MODEL.get(model_id)
|
||||
if not m:
|
||||
return (None, None)
|
||||
return (m.get("input_price"), m.get("output_price"))
|
||||
|
||||
|
||||
def model_label_by_id(model_id):
|
||||
"""按 model id 生成当前快照的带价签标签;未收录的 id 原样返回。"""
|
||||
m = _ID_TO_MODEL.get(model_id)
|
||||
return _make_label(m) if m else (model_id or "?")
|
||||
|
||||
|
||||
def label_to_model_id(label):
|
||||
"""
|
||||
从下拉标签解析 model id。三层兼容:
|
||||
|
||||
+65
-10
@@ -8,6 +8,8 @@ ZenMux API 连接节点
|
||||
- 模型下拉覆盖 ZenMux 全部文本模型,按「厂商/模型名」排序聚类;
|
||||
ComfyUI 下拉自带搜索,输入厂商前缀(如 "anthropic/")即可快速过滤。
|
||||
- 每个模型选项后面直接标注输入/输出价格(USD / 百万 token)。
|
||||
- usage_stats 输出单次运行的 token 消耗与费用(按快照单价折算,
|
||||
汇率可用 usd_to_cny 参数调整)。
|
||||
- 具备常规 API 节点的完整参数:api_key、system/user prompt、seed、
|
||||
temperature、top_p、max_tokens、以及可选的多模态图像输入与代理。
|
||||
- 默认模型 openai/gpt-5.4-nano。
|
||||
@@ -32,6 +34,8 @@ from .model_registry import (
|
||||
default_model_label,
|
||||
all_model_labels,
|
||||
label_to_model_id,
|
||||
model_label_by_id,
|
||||
model_prices,
|
||||
)
|
||||
|
||||
# ZenMux 平台固定地址(OpenAI 兼容)
|
||||
@@ -152,11 +156,18 @@ class ZenMuxNode:
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
}),
|
||||
# usage_stats 里人民币换算用的汇率,可按当日牌价自行调整
|
||||
"usd_to_cny": ("FLOAT", {
|
||||
"default": 7.2,
|
||||
"min": 0.1,
|
||||
"max": 100.0,
|
||||
"step": 0.01,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("text", "model_id")
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("text", "model_id", "usage_stats")
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "Rui-Node🐶/AI模型🤖"
|
||||
|
||||
@@ -187,6 +198,41 @@ class ZenMuxNode:
|
||||
pil.save(buf, format='JPEG', quality=85)
|
||||
return base64.b64encode(buf.getvalue()).decode('utf-8')
|
||||
|
||||
# ────────── 消耗统计 ──────────
|
||||
@staticmethod
|
||||
def _fmt_cost(v):
|
||||
"""费用格式化:最多 6 位小数并去尾零;未知为 '?'。"""
|
||||
if v is None:
|
||||
return "?"
|
||||
s = f"{v:.6f}".rstrip("0").rstrip(".")
|
||||
return s if s else "0"
|
||||
|
||||
@staticmethod
|
||||
def _build_usage_stats(model_id, usage, usd_to_cny):
|
||||
"""
|
||||
由 API 响应的 usage 与快照单价生成三行消耗统计文本:
|
||||
token消耗,输入:XXX,输出:XXX
|
||||
模型类型:openai/gpt-5.4-nano [入$0.2/M 出$1.25/M]
|
||||
价格换算,美元:XXX,人民币:XXX
|
||||
usage 缺失/单价未知的项以 '?' 呈现;请求未发生时传 usage=None 记为 0 消耗。
|
||||
"""
|
||||
if not isinstance(usage, dict):
|
||||
usage = {"prompt_tokens": 0, "completion_tokens": 0}
|
||||
in_tok = usage.get("prompt_tokens")
|
||||
out_tok = usage.get("completion_tokens")
|
||||
in_price, out_price = model_prices(model_id)
|
||||
|
||||
usd = None
|
||||
if (isinstance(in_tok, (int, float)) and isinstance(out_tok, (int, float))
|
||||
and in_price is not None and out_price is not None):
|
||||
usd = in_tok / 1e6 * in_price + out_tok / 1e6 * out_price
|
||||
cny = usd * usd_to_cny if usd is not None else None
|
||||
|
||||
tok = lambda t: str(int(t)) if isinstance(t, (int, float)) else "?" # noqa: E731
|
||||
return (f"token消耗,输入:{tok(in_tok)},输出:{tok(out_tok)}\n"
|
||||
f"模型类型:{model_label_by_id(model_id)}\n"
|
||||
f"价格换算,美元:{ZenMuxNode._fmt_cost(usd)},人民币:{ZenMuxNode._fmt_cost(cny)}")
|
||||
|
||||
# ────────── 主函数 ──────────
|
||||
def generate(
|
||||
self,
|
||||
@@ -208,14 +254,19 @@ class ZenMuxNode:
|
||||
image_max_size=1024,
|
||||
base_url=DEFAULT_BASE_URL,
|
||||
proxy_url="",
|
||||
usd_to_cny=7.2,
|
||||
):
|
||||
# ---- 1. 从下拉标签解析真实 model id ----
|
||||
model_id = label_to_model_id(model) or DEFAULT_MODEL_ID
|
||||
chat_url = _build_chat_url(base_url)
|
||||
print(f"[Rui-Node] ZenMux -> {chat_url} model={model_id}")
|
||||
|
||||
# 请求未成功前的兜底统计(0 消耗)
|
||||
zero_stats = self._build_usage_stats(model_id, None, usd_to_cny)
|
||||
|
||||
if not (api_key or "").strip():
|
||||
return ("(错误:未填写 api_key,请在节点里填入 ZenMux 的 API Key)", model_id)
|
||||
return ("(错误:未填写 api_key,请在节点里填入 ZenMux 的 API Key)",
|
||||
model_id, zero_stats)
|
||||
|
||||
# ---- 2. 收集图像 ----
|
||||
images = [
|
||||
@@ -247,7 +298,8 @@ class ZenMuxNode:
|
||||
else:
|
||||
text = (user_prompt or "").strip()
|
||||
if not text:
|
||||
return ("(错误:未提供图片也未提供提示词,请至少填写 user_prompt)", model_id)
|
||||
return ("(错误:未提供图片也未提供提示词,请至少填写 user_prompt)",
|
||||
model_id, zero_stats)
|
||||
messages.append({"role": "user", "content": text})
|
||||
|
||||
# ---- 4. 请求 ----
|
||||
@@ -275,6 +327,8 @@ class ZenMuxNode:
|
||||
proxies=proxies, timeout=180)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
# 消耗统计(成功与格式异常场景都尽量取真实 usage)
|
||||
stats = self._build_usage_stats(model_id, data.get("usage"), usd_to_cny)
|
||||
if data.get("choices"):
|
||||
msg = data["choices"][0].get("message", {})
|
||||
content = msg.get("content", "")
|
||||
@@ -282,18 +336,19 @@ class ZenMuxNode:
|
||||
content = "".join(
|
||||
seg.get("text", "") for seg in content if isinstance(seg, dict)
|
||||
)
|
||||
return (content or "", model_id)
|
||||
return (f"API 返回格式异常: {json.dumps(data, ensure_ascii=False)[:800]}", model_id)
|
||||
return (content or "", model_id, stats)
|
||||
return (f"API 返回格式异常: {json.dumps(data, ensure_ascii=False)[:800]}",
|
||||
model_id, stats)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
return (f"连接失败(请检查网络/代理): {e}", model_id)
|
||||
return (f"连接失败(请检查网络/代理): {e}", model_id, zero_stats)
|
||||
except requests.exceptions.Timeout:
|
||||
return ("请求超时(180s),请检查网络或 ZenMux 服务状态。", model_id)
|
||||
return ("请求超时(180s),请检查网络或 ZenMux 服务状态。", model_id, zero_stats)
|
||||
except requests.exceptions.HTTPError:
|
||||
code = resp.status_code if resp is not None else "?"
|
||||
body = resp.text[:600] if resp is not None else ""
|
||||
return (f"HTTP 错误 {code}: {body}", model_id)
|
||||
return (f"HTTP 错误 {code}: {body}", model_id, zero_stats)
|
||||
except Exception as e:
|
||||
return (f"请求异常: {type(e).__name__}: {e}", model_id)
|
||||
return (f"请求异常: {type(e).__name__}: {e}", model_id, zero_stats)
|
||||
|
||||
|
||||
# ────────── ComfyUI 注册 ──────────
|
||||
|
||||
Reference in New Issue
Block a user