From 0a87ba7813312ce0a0269e80ce0756070c5680aa Mon Sep 17 00:00:00 2001 From: rui40000 Date: Fri, 24 Jul 2026 10:23:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ZenMux=20=E8=8A=82=E7=82=B9=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20usage=5Fstats=20=E8=BE=93=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=8D=95=E6=AC=A1=E8=BF=90=E8=A1=8C=20token?= =?UTF-8?q?=20=E6=B6=88=E8=80=97=E4=B8=8E=E8=B4=B9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 第三输出 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 --- README.md | 9 +++++ zenmux/model_registry.py | 17 +++++++++ zenmux/zenmux_node.py | 75 ++++++++++++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1042417..21672dc 100644 --- a/README.md +++ b/README.md @@ -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 试遍多家厂商的模型,横向对比效果与成本 diff --git a/zenmux/model_registry.py b/zenmux/model_registry.py index 2bf8440..c753623 100644 --- a/zenmux/model_registry.py +++ b/zenmux/model_registry.py @@ -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。三层兼容: diff --git a/zenmux/zenmux_node.py b/zenmux/zenmux_node.py index f92f92f..4a947dd 100644 --- a/zenmux/zenmux_node.py +++ b/zenmux/zenmux_node.py @@ -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 注册 ──────────