Files
rui40000andClaude Fable 5 ca61c48dbd feat: 新增 ZenMux API 连接节点(厂商→模型级联选择,选项含价格标注)
- 一个节点调用 ZenMux 聚合平台全部文本类模型(20 厂商 / 138 模型)
- vendor→model 二级下拉:前端 web/zenmux_cascade.js 按厂商实时收窄模型列表
- 模型选项后缀标注输入/输出价格(USD/百万token),如 [入$0.2/M 出$1.25/M]
- 默认模型 openai/gpt-5.4-nano;具备 api_key/system/user prompt/seed/
  temperature/top_p/max_tokens/多模态图像×6/detail/base_url/proxy 等完整参数
- 模型清单随包分发(models_snapshot.json),离线可列出;
  build_snapshot.py 可随时重拉最新价格
- VALIDATE_INPUTS 宽松校验:价格快照更新后旧工作流的模型标签仍可解析执行
- base_url 默认值沿用仓库约定不含 "://",规避 ComfyUI 前端吞协议片段的老问题
- 导出 WEB_DIRECTORY 以装载前端级联脚本

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 11:37:00 +08:00

92 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
从 ZenMux 拉取模型列表,提炼成节点需要的精简快照。
运行:python build_snapshot.py
产出:models_snapshot.json(随节点分发,保证离线可用)
只保留文本类模型(输出模态含 text),并预先算好基础输入/输出价格,
避免节点运行时每次都解析 ZenMux 那套分段定价结构。
"""
import json
import os
import sys
import requests
# Windows GBK 控制台打印中文/特殊字符会崩,强制 UTF-8
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
API = "https://zenmux.ai/api/v1/models"
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "models_snapshot.json")
def base_price(pricings, key):
"""取某项价格的基础段(prompt_tokens 从 0 起的那一段),单位 USD / 百万 token。"""
seg = (pricings or {}).get(key)
if not seg:
return None
vals = [s for s in seg if isinstance(s, dict) and "value" in s]
if not vals:
return None
base = None
for s in vals:
cond = s.get("conditions", {}).get("prompt_tokens", {})
if cond.get("gte", 0) in (0, None):
base = s
break
base = base or vals[0]
try:
return float(base["value"])
except (TypeError, ValueError, KeyError):
return None
def build(raw_models):
out = []
for m in raw_models:
out_mod = m.get("output_modalities", [])
if "text" not in out_mod: # 只要文本类模型
continue
pr = m.get("pricings") or {}
out.append({
"id": m["id"],
"display_name": m.get("display_name", m["id"]),
# 厂商一律取模型 id 的前缀("openai/gpt-5.4-nano" -> "openai"),
# 与前端级联 JS 的过滤规则(label.split("/")[0])保持同一真源。
"vendor": m["id"].split("/")[0] if "/" in m["id"] else m.get("owned_by", "other"),
"input_price": base_price(pr, "prompt"),
"output_price": base_price(pr, "completion"),
"context_length": m.get("context_length"),
"input_modalities": m.get("input_modalities", []),
})
# 按厂商、再按 id 排序,方便前端级联展示
out.sort(key=lambda x: (x["vendor"].lower(), x["id"].lower()))
return out
def main():
print(f"拉取 {API} ...")
r = requests.get(API, timeout=30)
r.raise_for_status()
data = r.json()["data"]
models = build(data)
payload = {
"source": API,
"count": len(models),
"models": models,
}
with open(OUT, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=1)
print(f"写入 {OUT}:{len(models)} 个文本模型")
vendors = sorted(set(m["vendor"] for m in models))
print(f"厂商 {len(vendors)} 个:{', '.join(vendors)}")
assert any(m["id"] == "openai/gpt-5.4-nano" for m in models), "默认模型缺失!"
print("默认模型 openai/gpt-5.4-nano 存在 ✓")
if __name__ == "__main__":
sys.exit(main())