diff --git a/README.md b/README.md index facff5f..7dd1b11 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Rui-Node🐶 是一个功能丰富的 ComfyUI 节点集合,提供图像处理 ### 🤖 AI模型类 - [千问编辑图像生成 / Qwen Edit Image Generation](#4-千问编辑图像生成--qwen-edit-image-generation) - [SDMatte 精细抠图 / SDMatte Interactive Matting](#17-sdmatte-精细抠图--sdmatte-interactive-matting) +- [ZenMux API 连接 / ZenMux API Connector](#18-zenmux-api-连接--zenmux-api-connector) ### 📝 文本处理类 - [镜头分词器 / Shot Splitter](#5-镜头分词器--shot-splitter) @@ -710,6 +711,46 @@ ComfyUI-SDMatte 传 `aux_input="trimap"`,把模型推到了没训练过的输 --- +### 18. ZenMux API 连接 / ZenMux API Connector + +**分类**: `Rui-Node🐶/AI模型🤖` + +**功能描述**: +连接 [ZenMux](https://zenmux.ai) 聚合平台(OpenAI 兼容协议),一个节点即可调用其收录的**所有文本类模型**(Anthropic、OpenAI、Google、DeepSeek、Qwen 等 20 家厂商、130+ 模型)。支持文本生成与多模态图像理解(最多 6 张图)。 + +**特色功能**: +- **厂商 → 模型二级选择**: 先在 `vendor` 下拉选厂商,`model` 下拉会自动收窄为该厂商的模型(由前端脚本 `web/zenmux_cascade.js` 联动) +- **价格直接标在选项上**: 每个模型后缀形如 `[入$0.2/M 出$1.25/M]`,即输入/输出每百万 token 的美元价格,选型时一目了然 +- **离线可用的模型清单**: 模型与价格来自随包分发的 `zenmux/models_snapshot.json`;价格有变动时运行 `python zenmux/build_snapshot.py` 即可重新拉取更新 +- **旧工作流兼容**: 价格快照更新后,旧工作流里保存的带旧价格标签仍能正确解析出模型 id,不会失效 + +**输入参数**: +- `api_key` (STRING): ZenMux 平台的 API Key(在 zenmux.ai 控制台获取) +- `vendor` (选择): 厂商筛选,默认 `openai` +- `model` (选择): 模型(带价格标注),默认 `openai/gpt-5.4-nano` +- `system_prompt` (STRING): 系统提示词 +- `user_prompt` (STRING): 用户提示词 +- `seed` (INT): 随机种子 +- `temperature` (FLOAT, 可选): 采样温度,默认 0.7,范围 0.0 ~ 2.0 +- `top_p` (FLOAT, 可选): 核采样阈值,默认 1.0 +- `max_tokens` (INT, 可选): 最大输出 token 数,默认 1024 +- `image_1` ~ `image_6` (IMAGE, 可选): 多模态图像输入(所选模型需支持 image 输入) +- `detail` (选择, 可选): 图像分析细节等级,auto/low/high +- `image_max_size` (INT, 可选): 发送前图像最长边缩放上限,默认 1024 +- `base_url` (STRING, 可选): API 地址,默认 `zenmux.ai/api/v1`(无需写 `https://`,节点会自动补全) +- `proxy_url` (STRING, 可选): HTTP/HTTPS 代理地址,如 `127.0.0.1:7890` + +**输出**: +- `text` (STRING): 模型生成的文本内容 +- `model_id` (STRING): 实际调用的模型 id(如 `openai/gpt-5.4-nano`),便于下游记录 + +**使用场景**: +- 一个 Key 试遍多家厂商的模型,横向对比效果与成本 +- 按预算选型:价格就写在下拉列表里,直接挑便宜的 +- 调用 Claude / GPT / Gemini / DeepSeek 等做文本生成或图像理解 + +--- + ## 🐕 关于 Rui-Node🐶 Rui-Node🐶 致力于为 ComfyUI 用户提供实用、高效的节点工具集。🐶 是我们的项目标志,代表着忠诚、友好和可靠。 diff --git a/__init__.py b/__init__.py index c3436db..b41ffcb 100644 --- a/__init__.py +++ b/__init__.py @@ -41,6 +41,9 @@ except Exception as _e: print("[Ruinode] 如需使用,请安装:pip install diffusers transformers safetensors scipy opencv-python") SDMATTE_NODE_CLASS_MAPPINGS = {} SDMATTE_NODE_DISPLAY_NAME_MAPPINGS = {} +# 新增:ZenMux API 连接节点(聚合平台,厂商→模型二级选择,标签含价格) +from .zenmux import NODE_CLASS_MAPPINGS as ZENMUX_NODE_CLASS_MAPPINGS +from .zenmux import NODE_DISPLAY_NAME_MAPPINGS as ZENMUX_NODE_DISPLAY_NAME_MAPPINGS # 合并节点映射字典 NODE_CLASS_MAPPINGS = {} @@ -61,6 +64,7 @@ NODE_CLASS_MAPPINGS.update(OPENAI_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(COLORMATCHER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(IMAGESPLITTER_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(SDMATTE_NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(ZENMUX_NODE_CLASS_MAPPINGS) # 合并节点显示名称映射 NODE_DISPLAY_NAME_MAPPINGS = {} @@ -81,5 +85,9 @@ NODE_DISPLAY_NAME_MAPPINGS.update(OPENAI_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(COLORMATCHER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(IMAGESPLITTER_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(SDMATTE_NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(ZENMUX_NODE_DISPLAY_NAME_MAPPINGS) -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +# 前端扩展目录(ZenMux 节点的「厂商→模型」级联脚本等) +WEB_DIRECTORY = "./web" + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/web/zenmux_cascade.js b/web/zenmux_cascade.js new file mode 100644 index 0000000..955af41 --- /dev/null +++ b/web/zenmux_cascade.js @@ -0,0 +1,60 @@ +// ZenMux 节点「厂商 → 模型」二级级联 +// ================================== +// 后端把全量模型标签(含价格)都放进 model 下拉以通过校验; +// 本扩展在前端按 vendor 收窄 model 的候选列表。 +// 约定:模型标签第一个空格前是 "vendor/model" 形式的 id, +// 故 label.split("/")[0] 即厂商名,与 vendor 下拉的取值同源。 +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "Ruinode.ZenMuxCascade", + async beforeRegisterNodeDef(nodeType, nodeData, _app) { + if (nodeData.name !== "ZenMuxAPINode") return; + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated?.apply(this, arguments); + + const vendorW = this.widgets?.find((w) => w.name === "vendor"); + const modelW = this.widgets?.find((w) => w.name === "model"); + if (!vendorW || !modelW) return r; + + // 全量标签快照(此时 options.values 是后端给的完整列表) + const allLabels = (modelW.options.values || []).slice(); + const vendorOf = (label) => String(label).split("/")[0]; + + // resetIfMissing:当前值不在收窄后的列表时是否重置为列表首项。 + // 手动切厂商 → 重置;加载旧工作流 → 保留(旧价格标签后端能解析)。 + const applyFilter = (vendor, resetIfMissing) => { + const filtered = allLabels.filter((l) => vendorOf(l) === vendor); + modelW.options.values = filtered.length ? filtered : allLabels.slice(); + if (resetIfMissing && !modelW.options.values.includes(modelW.value)) { + modelW.value = modelW.options.values[0]; + modelW.callback?.(modelW.value); + } + this.setDirtyCanvas?.(true, true); + }; + + const origCallback = vendorW.callback; + vendorW.callback = function (value) { + const r2 = origCallback?.apply(this, arguments); + applyFilter(value, true); + return r2; + }; + + // 新建节点:按默认厂商先收窄一次 + applyFilter(vendorW.value, false); + + // 加载已保存的工作流:configure 恢复 widget 值后再按真实厂商收窄, + // 但不动用户保存的 model 选择 + const onConfigure = this.onConfigure; + this.onConfigure = function () { + const r3 = onConfigure?.apply(this, arguments); + applyFilter(vendorW.value, false); + return r3; + }; + + return r; + }; + }, +}); diff --git a/zenmux/__init__.py b/zenmux/__init__.py new file mode 100644 index 0000000..aa7b5a6 --- /dev/null +++ b/zenmux/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +"""ZenMux API 连接节点子包。""" +from .zenmux_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/zenmux/build_snapshot.py b/zenmux/build_snapshot.py new file mode 100644 index 0000000..3e90861 --- /dev/null +++ b/zenmux/build_snapshot.py @@ -0,0 +1,91 @@ +# -*- 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()) diff --git a/zenmux/model_registry.py b/zenmux/model_registry.py new file mode 100644 index 0000000..02a4126 --- /dev/null +++ b/zenmux/model_registry.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +ZenMux 模型注册表 +================= +从随包分发的 models_snapshot.json 加载模型清单,为节点提供: + +- all_vendors() 厂商列表(模型 id 的前缀,如 "openai") +- all_model_labels() 全量「带价格」下拉标签,按厂商 → 模型排序 +- default_model_label() 默认模型 openai/gpt-5.4-nano 对应的标签 +- label_to_model_id() 从下拉标签解析真实 model id(对旧标签/纯 id 也兼容) + +标签格式(价格单位:USD / 百万 token): + openai/gpt-5.4-nano [入$0.04/M 出$0.32/M] +标签第一个空格前恒为 model id,前端级联 JS 与后端解析都依赖这一点。 + +快照可用 build_snapshot.py 随时重新拉取更新。 +""" +import json +import os + +DEFAULT_MODEL_ID = "openai/gpt-5.4-nano" + +_SNAPSHOT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "models_snapshot.json") + + +def _fmt_price(v): + """10.0 -> '10',0.04 -> '0.04',None -> '?'。""" + if v is None: + return "?" + try: + s = f"{float(v):.4f}".rstrip("0").rstrip(".") + return s if s else "0" + except (TypeError, ValueError): + return "?" + + +def _make_label(m): + return (f"{m['id']} " + f"[入${_fmt_price(m.get('input_price'))}/M " + f"出${_fmt_price(m.get('output_price'))}/M]") + + +def _load_models(): + """读取快照;文件缺失/损坏时退化为只含默认模型的最小清单。""" + try: + with open(_SNAPSHOT_PATH, "r", encoding="utf-8") as f: + models = json.load(f)["models"] + if models: + return models + except Exception as e: + print(f"[Rui-Node] ZenMux 模型快照加载失败({e}),退化为最小模型清单。" + f"可运行 zenmux/build_snapshot.py 重新生成。") + return [{ + "id": DEFAULT_MODEL_ID, + "vendor": DEFAULT_MODEL_ID.split("/")[0], + "input_price": None, + "output_price": None, + }] + + +# 模块加载时构建一次即可(ComfyUI 启动时构建节点定义) +_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} +_VENDORS = sorted({m.get("vendor") or m["id"].split("/")[0] for m in _MODELS}, + key=str.lower) + + +def all_vendors(): + return list(_VENDORS) + + +def all_model_labels(): + return list(_LABELS) + + +def default_model_label(): + for lb, m in zip(_LABELS, _MODELS): + if m["id"] == DEFAULT_MODEL_ID: + return lb + return _LABELS[0] + + +def label_to_model_id(label): + """ + 从下拉标签解析 model id。三层兼容: + 1. 当前标签精确命中; + 2. 旧版工作流里存的标签(价格已变动)→ 取第一个空格前的 id 段; + 3. 用户直接填了纯 model id → 原样返回。 + 解析不出(空串等)返回 None,由调用方兜底到默认模型。 + """ + s = (label or "").strip() + if not s: + return None + if s in _LABEL_TO_ID: + return _LABEL_TO_ID[s] + head = s.split(" ")[0].split("\t")[0] + if head in _KNOWN_IDS: + return head + # 未收录的 id(快照偏旧但模型真实存在)也放行,交给服务端判定 + return head if head else None diff --git a/zenmux/models_snapshot.json b/zenmux/models_snapshot.json new file mode 100644 index 0000000..0955b90 --- /dev/null +++ b/zenmux/models_snapshot.json @@ -0,0 +1,1700 @@ +{ + "source": "https://zenmux.ai/api/v1/models", + "count": 138, + "models": [ + { + "id": "anthropic/claude-fable-5", + "display_name": "Anthropic: Claude Fable 5", + "vendor": "anthropic", + "input_price": 10.0, + "output_price": 50.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "anthropic/claude-haiku-4.5", + "display_name": "Anthropic: Claude Haiku 4.5", + "vendor": "anthropic", + "input_price": 1.0, + "output_price": 5.0, + "context_length": 200000, + "input_modalities": [ + "image", + "text" + ] + }, + { + "id": "anthropic/claude-opus-4", + "display_name": "Anthropic: Claude Opus 4", + "vendor": "anthropic", + "input_price": 15.0, + "output_price": 75.0, + "context_length": 200000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "anthropic/claude-opus-4.1", + "display_name": "Anthropic: Claude Opus 4.1", + "vendor": "anthropic", + "input_price": 15.0, + "output_price": 75.0, + "context_length": 200000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "anthropic/claude-opus-4.5", + "display_name": "Anthropic: Claude Opus 4.5", + "vendor": "anthropic", + "input_price": 5.0, + "output_price": 25.0, + "context_length": 200000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "anthropic/claude-opus-4.6", + "display_name": "Anthropic: Claude Opus 4.6", + "vendor": "anthropic", + "input_price": 5.0, + "output_price": 25.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "anthropic/claude-opus-4.7", + "display_name": "Anthropic: Claude Opus 4.7", + "vendor": "anthropic", + "input_price": 5.0, + "output_price": 25.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "anthropic/claude-opus-4.8", + "display_name": "Anthropic: Claude Opus 4.8", + "vendor": "anthropic", + "input_price": 5.0, + "output_price": 25.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "anthropic/claude-sonnet-4", + "display_name": "Anthropic: Claude Sonnet 4", + "vendor": "anthropic", + "input_price": 3.0, + "output_price": 15.0, + "context_length": 200000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "anthropic/claude-sonnet-4.5", + "display_name": "Anthropic: Claude Sonnet 4.5", + "vendor": "anthropic", + "input_price": 3.0, + "output_price": 15.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "anthropic/claude-sonnet-4.6", + "display_name": "Anthropic: Claude Sonnet 4.6", + "vendor": "anthropic", + "input_price": 3.0, + "output_price": 15.0, + "context_length": 1000000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "anthropic/claude-sonnet-5", + "display_name": "Anthropic: Claude Sonnet 5", + "vendor": "anthropic", + "input_price": 2.0, + "output_price": 10.0, + "context_length": 1000000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "baidu/ernie-5.0-thinking-preview", + "display_name": "Baidu: ERNIE 5.0", + "vendor": "baidu", + "input_price": 0.84, + "output_price": 3.37, + "context_length": 128000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "baidu/ernie-5.1", + "display_name": "Baidu: ERNIE 5.1", + "vendor": "baidu", + "input_price": 0.424593036, + "output_price": 1.910668662, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "baidu/ernie-x1.1-preview", + "display_name": "Baidu: ERNIE-X1.1-Preview", + "vendor": "baidu", + "input_price": 0.14, + "output_price": 0.56, + "context_length": 65536, + "input_modalities": [ + "text" + ] + }, + { + "id": "bytedance/doubao-seed-1.8", + "display_name": "ByteDance: Doubao-Seed-1.8", + "vendor": "bytedance", + "input_price": 0.11, + "output_price": 0.28, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.0-code", + "display_name": "ByteDance: Doubao-Seed-2.0-Code", + "vendor": "bytedance", + "input_price": 0.45, + "output_price": 2.24, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.0-lite", + "display_name": "ByteDance: Doubao-Seed-2.0-lite", + "vendor": "bytedance", + "input_price": 0.09, + "output_price": 0.51, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.0-mini", + "display_name": "ByteDance: Doubao-Seed-2.0-mini", + "vendor": "bytedance", + "input_price": 0.03, + "output_price": 0.28, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.0-pro", + "display_name": "ByteDance: Doubao-Seed-2.0-pro", + "vendor": "bytedance", + "input_price": 0.45, + "output_price": 2.24, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.1-pro", + "display_name": "ByteDance: Doubao-Seed-2.1-pro", + "vendor": "bytedance", + "input_price": 0.422571632, + "output_price": 2.11285816, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-2.1-turbo", + "display_name": "ByteDance: Doubao-Seed-2.1-turbo", + "vendor": "bytedance", + "input_price": 0.422571632, + "output_price": 2.11285816, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-character", + "display_name": "ByteDance: Doubao-Seed-Character", + "vendor": "bytedance", + "input_price": 0.1179, + "output_price": 0.2947, + "context_length": 128000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "bytedance/doubao-seed-code", + "display_name": "ByteDance: Doubao-Seed-Code", + "vendor": "bytedance", + "input_price": 0.17, + "output_price": 1.12, + "context_length": 256000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "bytedance/doubao-seed-evolving", + "display_name": "ByteDance: Doubao-Seed-Evolving", + "vendor": "bytedance", + "input_price": 0.422571632, + "output_price": 2.11285816, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "deepseek/deepseek-chat", + "display_name": "DeepSeek: DeepSeek-V3.2 (Non-thinking Mode)", + "vendor": "deepseek", + "input_price": 0.14, + "output_price": 0.28, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-chat-v3.1", + "display_name": "DeepSeek: DeepSeek V3.1", + "vendor": "deepseek", + "input_price": 0.28, + "output_price": 1.11, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-r1-0528", + "display_name": "DeepSeek: DeepSeek R1 0528", + "vendor": "deepseek", + "input_price": 0.56, + "output_price": 2.23, + "context_length": 64000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-reasoner", + "display_name": "DeepSeek: DeepSeek-V3.2 (Thinking Mode)", + "vendor": "deepseek", + "input_price": 0.14, + "output_price": 0.28, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-v3.2", + "display_name": "DeepSeek: DeepSeek V3.2", + "vendor": "deepseek", + "input_price": 0.293, + "output_price": 0.4395, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-v3.2-exp", + "display_name": "DeepSeek: DeepSeek-V3.2-Exp", + "vendor": "deepseek", + "input_price": 0.216, + "output_price": 0.328, + "context_length": 163840, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-v4-flash", + "display_name": "DeepSeek: DeepSeek V4 Flash", + "vendor": "deepseek", + "input_price": 0.14, + "output_price": 0.28, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "deepseek/deepseek-v4-pro", + "display_name": "DeepSeek: DeepSeek V4 Pro", + "vendor": "deepseek", + "input_price": 0.435, + "output_price": 0.87, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "google/gemini-2.5-flash", + "display_name": "Google: Gemini 2.5 Flash", + "vendor": "google", + "input_price": 0.3, + "output_price": 2.5, + "context_length": 1048576, + "input_modalities": [ + "file", + "image", + "text", + "audio" + ] + }, + { + "id": "google/gemini-2.5-flash-lite", + "display_name": "Google: Gemini 2.5 Flash Lite", + "vendor": "google", + "input_price": 0.1, + "output_price": 0.4, + "context_length": 1048576, + "input_modalities": [ + "file", + "image", + "text", + "audio" + ] + }, + { + "id": "google/gemini-2.5-pro", + "display_name": "Google: Gemini 2.5 Pro", + "vendor": "google", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 1048576, + "input_modalities": [ + "file", + "image", + "text", + "audio", + "video" + ] + }, + { + "id": "google/gemini-3-flash-preview", + "display_name": "Google: Gemini 3 Flash Preview", + "vendor": "google", + "input_price": 0.5, + "output_price": 3.0, + "context_length": 1048576, + "input_modalities": [ + "text", + "image", + "file", + "audio" + ] + }, + { + "id": "google/gemini-3.1-flash-lite", + "display_name": "Google: Gemini 3.1 Flash Lite", + "vendor": "google", + "input_price": 0.25, + "output_price": 1.5, + "context_length": 1048576, + "input_modalities": [ + "file", + "image", + "text", + "audio" + ] + }, + { + "id": "google/gemini-3.1-pro-preview", + "display_name": "Google: Gemini 3.1 Pro Preview", + "vendor": "google", + "input_price": 2.0, + "output_price": 12.0, + "context_length": 1048576, + "input_modalities": [ + "text", + "image", + "file", + "audio", + "video" + ] + }, + { + "id": "google/gemini-3.5-flash", + "display_name": "Google: Gemini 3.5 Flash", + "vendor": "google", + "input_price": 1.5, + "output_price": 9.0, + "context_length": 1048576, + "input_modalities": [ + "file", + "image", + "audio", + "video", + "text" + ] + }, + { + "id": "inclusionai/ling-2.6-1t", + "display_name": "inclusionAI: Ling-2.6-1T", + "vendor": "inclusionai", + "input_price": 0.1318155, + "output_price": 1.0984625, + "context_length": 262144, + "input_modalities": [ + "text" + ] + }, + { + "id": "inclusionai/ling-2.6-flash", + "display_name": "inclusionAI: Ling-2.6-flash", + "vendor": "inclusionai", + "input_price": null, + "output_price": null, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "inclusionai/llada2.1-flash", + "display_name": "inclusionAI: LLaDA2.1-flash", + "vendor": "inclusionai", + "input_price": 0.28, + "output_price": 2.85, + "context_length": 32000, + "input_modalities": [ + "text" + ] + }, + { + "id": "inclusionai/ring-2.6-1t", + "display_name": "inclusionAI: Ring-2.6-1T", + "vendor": "inclusionai", + "input_price": 0.1318155, + "output_price": 1.0984625, + "context_length": 262144, + "input_modalities": [ + "text" + ] + }, + { + "id": "kuaishou/kat-coder-air-v2.5", + "display_name": "KwaiKAT: KAT-Coder-Air-V2.5", + "vendor": "kuaishou", + "input_price": 0.135, + "output_price": 0.54, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "kuaishou/kat-coder-pro-v2", + "display_name": "KwaiKAT: KAT-Coder-Pro-V2", + "vendor": "kuaishou", + "input_price": 0.1373076, + "output_price": 0.5492304, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "kuaishou/kat-coder-pro-v2.5", + "display_name": "KwaiKAT: KAT-Coder-Pro-V2.5", + "vendor": "kuaishou", + "input_price": 0.444, + "output_price": 1.776, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "meituan/longcat-2.0", + "display_name": "Meituan: LongCat-2.0", + "vendor": "meituan", + "input_price": 0.3, + "output_price": 1.18, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "meta/llama-3.3-70b-instruct", + "display_name": "Meta: Llama 3.3 70B Instruct", + "vendor": "meta", + "input_price": 0.6, + "output_price": 1.2, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "meta/llama-4-scout-17b-16e-instruct", + "display_name": "Meta: Llama 4 Scout Instruct", + "vendor": "meta", + "input_price": 0.08, + "output_price": 0.4, + "context_length": 131072, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "minimax/minimax-m2", + "display_name": "MiniMax: MiniMax M2", + "vendor": "minimax", + "input_price": 0.3, + "output_price": 1.2, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2-her", + "display_name": "MiniMax: MiniMax M2-her", + "vendor": "minimax", + "input_price": 0.3, + "output_price": 1.2, + "context_length": 64000, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2.1", + "display_name": "MiniMax: MiniMax M2.1", + "vendor": "minimax", + "input_price": 0.3, + "output_price": 1.2, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2.5", + "display_name": "MiniMax: MiniMax M2.5", + "vendor": "minimax", + "input_price": 0.3, + "output_price": 1.2, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2.5-lightning", + "display_name": "MiniMax: MiniMax M2.5 highspeed", + "vendor": "minimax", + "input_price": null, + "output_price": null, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2.7", + "display_name": "MiniMax: MiniMax M2.7", + "vendor": "minimax", + "input_price": 0.3, + "output_price": 1.2, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m2.7-highspeed", + "display_name": "MiniMax: MiniMax M2.7 highspeed", + "vendor": "minimax", + "input_price": 0.611, + "output_price": 2.4439, + "context_length": 204800, + "input_modalities": [ + "text" + ] + }, + { + "id": "minimax/minimax-m3", + "display_name": "MiniMax: MiniMax M3", + "vendor": "minimax", + "input_price": 0.1373076, + "output_price": 0.5492304, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "video", + "file" + ] + }, + { + "id": "mistralai/mistral-large-2512", + "display_name": "Mistral: Mistral Large 3", + "vendor": "mistralai", + "input_price": 0.5, + "output_price": 1.5, + "context_length": 256000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "moonshotai/kimi-k2.5", + "display_name": "MoonshotAI: Kimi K2.5", + "vendor": "moonshotai", + "input_price": 0.58, + "output_price": 3.02, + "context_length": 262144, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "moonshotai/kimi-k2.6", + "display_name": "MoonshotAI: Kimi K2.6", + "vendor": "moonshotai", + "input_price": 0.95, + "output_price": 4.0, + "context_length": 262144, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "moonshotai/kimi-k2.7-code", + "display_name": "MoonshotAI: Kimi K2.7 Code", + "vendor": "moonshotai", + "input_price": 0.4257729, + "output_price": 1.792728, + "context_length": 262144, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "moonshotai/kimi-k2.7-code-highspeed", + "display_name": "MoonshotAI: Kimi K2.7 Code HighSpeed", + "vendor": "moonshotai", + "input_price": 1.9, + "output_price": 8.0, + "context_length": 262144, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "moonshotai/kimi-k3", + "display_name": "MoonshotAI: Kimi K3", + "vendor": "moonshotai", + "input_price": 3.0, + "output_price": 15.0, + "context_length": 1048576, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "moonshotai/kimi-k3-free", + "display_name": "MoonshotAI: Kimi K3 (Free)", + "vendor": "moonshotai", + "input_price": 0.0, + "output_price": 0.0, + "context_length": 1048576, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "openai/chat-latest", + "display_name": "OpenAI: Chat Latest (GPT-5.5 Instant)", + "vendor": "openai", + "input_price": 5.0, + "output_price": 30.0, + "context_length": 400000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "openai/gpt-4.1", + "display_name": "OpenAI: GPT-4.1", + "vendor": "openai", + "input_price": 2.0, + "output_price": 8.0, + "context_length": 1047576, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-4.1-mini", + "display_name": "OpenAI: GPT-4.1 Mini", + "vendor": "openai", + "input_price": 0.4, + "output_price": 1.6, + "context_length": 1047576, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-4.1-nano", + "display_name": "OpenAI: GPT-4.1 Nano", + "vendor": "openai", + "input_price": 0.1, + "output_price": 0.4, + "context_length": 1047576, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-4o", + "display_name": "OpenAI: GPT-4o", + "vendor": "openai", + "input_price": 2.5, + "output_price": 10.0, + "context_length": 128000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-4o-mini", + "display_name": "OpenAI: GPT-4o-mini", + "vendor": "openai", + "input_price": 0.15, + "output_price": 0.6, + "context_length": 128000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "openai/gpt-5", + "display_name": "OpenAI: GPT-5", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5-chat", + "display_name": "OpenAI: GPT-5 Chat", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 128000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "openai/gpt-5-codex", + "display_name": "OpenAI: GPT-5 Codex", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "openai/gpt-5-mini", + "display_name": "OpenAI: GPT-5 Mini", + "vendor": "openai", + "input_price": 0.25, + "output_price": 2.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5-nano", + "display_name": "OpenAI: GPT-5 Nano", + "vendor": "openai", + "input_price": 0.05, + "output_price": 0.4, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5-pro", + "display_name": "OpenAI: GPT-5 Pro", + "vendor": "openai", + "input_price": 15.0, + "output_price": 120.0, + "context_length": 400000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-5.1", + "display_name": "OpenAI: GPT-5.1", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 400000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-5.1-chat", + "display_name": "OpenAI: GPT-5.1 Chat", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 128000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "openai/gpt-5.1-codex", + "display_name": "OpenAI: GPT-5.1-Codex", + "vendor": "openai", + "input_price": 1.25, + "output_price": 10.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "openai/gpt-5.1-codex-mini", + "display_name": "OpenAI: GPT-5.1-Codex-Mini", + "vendor": "openai", + "input_price": 0.25, + "output_price": 2.0, + "context_length": 400000, + "input_modalities": [ + "image", + "text" + ] + }, + { + "id": "openai/gpt-5.2", + "display_name": "OpenAI: GPT-5.2", + "vendor": "openai", + "input_price": 1.75, + "output_price": 14.0, + "context_length": 400000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-5.2-chat", + "display_name": "OpenAI: GPT-5.2 Chat", + "vendor": "openai", + "input_price": 1.75, + "output_price": 14.0, + "context_length": 128000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "openai/gpt-5.2-codex", + "display_name": "OpenAI: GPT-5.2-Codex", + "vendor": "openai", + "input_price": 1.75, + "output_price": 14.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.2-pro", + "display_name": "OpenAI: GPT-5.2 Pro", + "vendor": "openai", + "input_price": 21.0, + "output_price": 168.0, + "context_length": 400000, + "input_modalities": [ + "image", + "text", + "file" + ] + }, + { + "id": "openai/gpt-5.3-chat", + "display_name": "OpenAI: GPT-5.3 Chat", + "vendor": "openai", + "input_price": 1.75, + "output_price": 14.0, + "context_length": 128000, + "input_modalities": [ + "file", + "image", + "text" + ] + }, + { + "id": "openai/gpt-5.3-codex", + "display_name": "OpenAI: GPT-5.3-Codex", + "vendor": "openai", + "input_price": 1.75, + "output_price": 14.0, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.4", + "display_name": "OpenAI: GPT-5.4", + "vendor": "openai", + "input_price": 2.5, + "output_price": 15.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.4-mini", + "display_name": "OpenAI: GPT-5.4 Mini", + "vendor": "openai", + "input_price": 0.75, + "output_price": 4.5, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.4-nano", + "display_name": "OpenAI: GPT-5.4 Nano", + "vendor": "openai", + "input_price": 0.2, + "output_price": 1.25, + "context_length": 400000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.4-pro", + "display_name": "OpenAI: GPT-5.4 Pro", + "vendor": "openai", + "input_price": 30.0, + "output_price": 180.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.5", + "display_name": "OpenAI: GPT-5.5", + "vendor": "openai", + "input_price": 5.0, + "output_price": 30.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.5-pro", + "display_name": "OpenAI: GPT-5.5 Pro", + "vendor": "openai", + "input_price": 30.0, + "output_price": 180.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.6-luna", + "display_name": "OpenAI: GPT-5.6 Luna", + "vendor": "openai", + "input_price": 1.0, + "output_price": 6.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.6-sol", + "display_name": "OpenAI: GPT-5.6 Sol", + "vendor": "openai", + "input_price": 5.0, + "output_price": 30.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-5.6-terra", + "display_name": "OpenAI: GPT-5.6 Terra", + "vendor": "openai", + "input_price": 2.5, + "output_price": 15.0, + "context_length": 1050000, + "input_modalities": [ + "text", + "image", + "file" + ] + }, + { + "id": "openai/gpt-image-1.5", + "display_name": "OpenAI: GPT-Image-1.5", + "vendor": "openai", + "input_price": 5.0, + "output_price": 10.0, + "context_length": 10000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "openai/o4-mini", + "display_name": "OpenAI: o4 Mini", + "vendor": "openai", + "input_price": 1.1, + "output_price": 4.4, + "context_length": 200000, + "input_modalities": [ + "image", + "text" + ] + }, + { + "id": "qwen/qwen3-14b", + "display_name": "Qwen: Qwen3-14B", + "vendor": "qwen", + "input_price": 0.14, + "output_price": 1.4, + "context_length": 32000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-235b-a22b-2507", + "display_name": "Qwen: Qwen3 235B A22B Instruct 2507", + "vendor": "qwen", + "input_price": 0.28, + "output_price": 1.11, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-235b-a22b-thinking-2507", + "display_name": "Qwen: Qwen3 235B A22B Thinking 2507", + "vendor": "qwen", + "input_price": 0.28, + "output_price": 2.78, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-coder", + "display_name": "Qwen: Qwen3-Coder", + "vendor": "qwen", + "input_price": 1.25, + "output_price": 5.01, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-coder-plus", + "display_name": "Qwen: Qwen3-Coder-Plus", + "vendor": "qwen", + "input_price": 1.0, + "output_price": 5.0, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-max", + "display_name": "Qwen: Qwen3-Max-Thinking", + "vendor": "qwen", + "input_price": 1.2, + "output_price": 6.0, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3-vl-plus", + "display_name": "Qwen: Qwen3-VL-Plus", + "vendor": "qwen", + "input_price": 0.2, + "output_price": 1.6, + "context_length": 262144, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "qwen/qwen3.5-flash", + "display_name": "Qwen: Qwen3.5-Flash", + "vendor": "qwen", + "input_price": 0.1, + "output_price": 0.4, + "context_length": 1024000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "qwen/qwen3.5-plus", + "display_name": "Qwen: Qwen3.5-Plus", + "vendor": "qwen", + "input_price": 0.4, + "output_price": 2.4, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file", + "video" + ] + }, + { + "id": "qwen/qwen3.6-flash", + "display_name": "Qwen: Qwen3.6 Flash", + "vendor": "qwen", + "input_price": 0.134717, + "output_price": 0.808302, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "qwen/qwen3.6-max-preview", + "display_name": "Qwen: Qwen3.6 Max Preview", + "vendor": "qwen", + "input_price": 1.3, + "output_price": 7.8, + "context_length": 262144, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3.6-plus", + "display_name": "Qwen: Qwen3.6-Plus", + "vendor": "qwen", + "input_price": 0.5, + "output_price": 3.0, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "file", + "video" + ] + }, + { + "id": "qwen/qwen3.7-max", + "display_name": "Qwen: Qwen3.7-Max", + "vendor": "qwen", + "input_price": 0.4307775, + "output_price": 1.2923325, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "qwen/qwen3.7-plus", + "display_name": "Qwen: Qwen3.7-Plus", + "vendor": "qwen", + "input_price": 0.1373076, + "output_price": 0.5492304, + "context_length": 1000000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "sapiens-ai/agnes-2.0-flash", + "display_name": "Sapiens AI: Agnes-2.0-Flash", + "vendor": "sapiens-ai", + "input_price": 0.1, + "output_price": 0.2, + "context_length": 256000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "stepfun/step-3.5-flash", + "display_name": "StepFun: Step 3.5 Flash", + "vendor": "stepfun", + "input_price": 0.1, + "output_price": 0.3, + "context_length": 256000, + "input_modalities": [ + "text" + ] + }, + { + "id": "stepfun/step-3.7-flash", + "display_name": "StepFun: Step 3.7 Flash", + "vendor": "stepfun", + "input_price": 0.1350354, + "output_price": 0.77645355, + "context_length": 256000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "tencent/hy3", + "display_name": "Tencent: Hy3", + "vendor": "tencent", + "input_price": 0.1323, + "output_price": 0.5301, + "context_length": 262144, + "input_modalities": [ + "text" + ] + }, + { + "id": "tencent/hy3-preview", + "display_name": "Tencent: Hy3 preview", + "vendor": "tencent", + "input_price": 0.138203892, + "output_price": 0.459608292, + "context_length": 262144, + "input_modalities": [ + "text" + ] + }, + { + "id": "x-ai/grok-4.2-fast", + "display_name": "xAI: Grok 4.2 Fast", + "vendor": "x-ai", + "input_price": 2.0, + "output_price": 6.0, + "context_length": 2000000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "x-ai/grok-4.2-fast-non-reasoning", + "display_name": "xAI: Grok 4.2 Fast Non Reasoning", + "vendor": "x-ai", + "input_price": 2.0, + "output_price": 6.0, + "context_length": 2000000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "x-ai/grok-4.3", + "display_name": "xAI: Grok 4.3", + "vendor": "x-ai", + "input_price": 1.25, + "output_price": 2.5, + "context_length": 1000000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "x-ai/grok-4.5", + "display_name": "xAI: Grok 4.5", + "vendor": "x-ai", + "input_price": 2.0, + "output_price": 6.0, + "context_length": 500000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "x-ai/grok-build-0.1", + "display_name": "xAI: Grok Build 0.1", + "vendor": "x-ai", + "input_price": 1.0, + "output_price": 2.0, + "context_length": 256000, + "input_modalities": [ + "text", + "image" + ] + }, + { + "id": "xiaomi/mimo-v2.5", + "display_name": "Xiaomi: MiMo-V2.5", + "vendor": "xiaomi", + "input_price": 0.1400916, + "output_price": 0.27084376, + "context_length": 1048576, + "input_modalities": [ + "text", + "audio", + "image", + "video" + ] + }, + { + "id": "xiaomi/mimo-v2.5-pro", + "display_name": "Xiaomi: MiMo-V2.5-Pro", + "vendor": "xiaomi", + "input_price": 0.43499984, + "output_price": 0.86999968, + "context_length": 1048576, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.5", + "display_name": "Z.AI: GLM 4.5", + "vendor": "z-ai", + "input_price": 0.2911, + "output_price": 1.1645, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.5-air", + "display_name": "Z.AI: GLM 4.5 Air", + "vendor": "z-ai", + "input_price": 0.1165, + "output_price": 0.2911, + "context_length": 128000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.6", + "display_name": "Z.AI: GLM 4.6", + "vendor": "z-ai", + "input_price": 0.2911, + "output_price": 1.1645, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.6v", + "display_name": "Z.AI: GLM 4.6V", + "vendor": "z-ai", + "input_price": 0.1456, + "output_price": 0.4367, + "context_length": 200000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "z-ai/glm-4.6v-flash", + "display_name": "Z.AI: GLM 4.6V FlashX", + "vendor": "z-ai", + "input_price": 0.0218, + "output_price": 0.2184, + "context_length": 200000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "z-ai/glm-4.6v-flash-free", + "display_name": "Z.AI: GLM 4.6V Flash (Free)", + "vendor": "z-ai", + "input_price": 0.0, + "output_price": 0.0, + "context_length": 200000, + "input_modalities": [ + "text", + "image", + "video" + ] + }, + { + "id": "z-ai/glm-4.7", + "display_name": "Z.AI: GLM 4.7", + "vendor": "z-ai", + "input_price": 0.2911, + "output_price": 1.1645, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.7-flash-free", + "display_name": "Z.AI: GLM 4.7 Flash (Free)", + "vendor": "z-ai", + "input_price": 0.0, + "output_price": 0.0, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-4.7-flashx", + "display_name": "Z.AI: GLM 4.7 FlashX", + "vendor": "z-ai", + "input_price": 0.0728, + "output_price": 0.4367, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-5", + "display_name": "Z.AI: GLM 5", + "vendor": "z-ai", + "input_price": 0.58, + "output_price": 2.6, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-5-turbo", + "display_name": "Z.AI: GLM 5 Turbo", + "vendor": "z-ai", + "input_price": 0.73, + "output_price": 3.19, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-5.1", + "display_name": "Z.AI: GLM 5.1", + "vendor": "z-ai", + "input_price": 0.8781, + "output_price": 3.5126, + "context_length": 200000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-5.2", + "display_name": "Z.AI: GLM 5.2", + "vendor": "z-ai", + "input_price": 0.98, + "output_price": 3.08, + "context_length": 1000000, + "input_modalities": [ + "text" + ] + }, + { + "id": "z-ai/glm-5v-turbo", + "display_name": "Z.AI: GLM 5V Turbo", + "vendor": "z-ai", + "input_price": 0.726, + "output_price": 3.1946, + "context_length": 200000, + "input_modalities": [ + "text", + "image", + "video", + "file" + ] + } + ] +} \ No newline at end of file diff --git a/zenmux/zenmux_node.py b/zenmux/zenmux_node.py new file mode 100644 index 0000000..68affd1 --- /dev/null +++ b/zenmux/zenmux_node.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +""" +ZenMux API 连接节点 +=================== +通过 ZenMux 聚合平台(https://zenmux.ai)调用其收录的所有文本类模型。 + +特性: +- 模型选择做成「厂商 → 模型」两级级联:先在 vendor 下拉里选厂商, + 再在 model 下拉里选该厂商的模型(联动由配套前端 zenmux_cascade.js 完成)。 +- 每个模型选项后面直接标注输入/输出价格(USD / 百万 token)。 +- 具备常规 API 节点的完整参数:api_key、system/user prompt、seed、 + temperature、top_p、max_tokens、以及可选的多模态图像输入与代理。 +- 默认模型 openai/gpt-5.4-nano。 +- 随节点分发 models_snapshot.json,无网络也能列出模型;价格与列表可用 + build_snapshot.py 重新拉取更新。 + +注:ZenMux 采用 OpenAI 兼容协议,chat 端点为 + https://zenmux.ai/api/v1/chat/completions +""" +import base64 +import io +import json +import os +import re + +import numpy as np +import requests +from PIL import Image + +from .model_registry import ( + DEFAULT_MODEL_ID, + default_model_label, + all_model_labels, + all_vendors, + label_to_model_id, +) + +# ZenMux 平台固定地址(OpenAI 兼容) +DEFAULT_BASE_URL = "https://zenmux.ai/api/v1" +# 输入框默认值不带 "://"——本仓库实测 ComfyUI 前端会吞掉文本框里的 +# 协议片段(见 openai_node.py 的同款处理),后端 _build_chat_url 会自动补 https。 +DEFAULT_BASE_URL_INPUT = "zenmux.ai/api/v1" + + +def _clear_proxy_env(): + """清除可能干扰 requests 的代理环境变量(仅本模块加载时执行一次)。""" + for key in ('HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'): + os.environ.pop(key, None) + + +_clear_proxy_env() + + +def _build_chat_url(base_url: str) -> str: + """由 base_url 拼出 chat/completions 端点,容忍用户填了/没填结尾斜杠。""" + s = (base_url or "").strip() + if not s: + s = DEFAULT_BASE_URL + # 补协议 + if not re.match(r'^https?://', s, flags=re.IGNORECASE): + s = 'https://' + s.lstrip('/') + s = s.rstrip('/') + # 用户可能已经把 /chat/completions 填进去了 + if s.lower().endswith('/chat/completions'): + return s + return s + '/chat/completions' + + +def _build_proxy_url(raw_proxy: str) -> str: + """把 '127.0.0.1:7890' 或 'http://127.0.0.1:7890' 统一成带协议的地址。""" + s = (raw_proxy or "").strip() + if not s: + return '' + if re.match(r'^https?://', s, flags=re.IGNORECASE): + return s + s = re.sub(r'^https?\s*:\s*/*/?\s*', '', s, flags=re.IGNORECASE).strip('/') + return ('http://' + s) if s else '' + + +class ZenMuxNode: + """ZenMux API 连接节点。""" + + # ────────── 输入定义 ────────── + @classmethod + def INPUT_TYPES(cls): + vendors = all_vendors() + model_labels = all_model_labels() + default_label = default_model_label() + return { + "required": { + "api_key": ("STRING", { + "default": "", + "multiline": False, + }), + # vendor 只用于前端级联筛选;后端不依赖它(真实模型从 model 解析)。 + # 默认选中 openai。 + "vendor": (vendors, { + "default": "openai" if "openai" in vendors else (vendors[0] if vendors else "openai"), + }), + # model 的候选是「全量」模型标签(含价格),保证 ComfyUI 后端校验通过; + # 前端 JS 会按 vendor 把可见项收窄到该厂商。 + "model": (model_labels, { + "default": default_label, + }), + "system_prompt": ("STRING", { + "default": "You are a helpful assistant.", + "multiline": True, + }), + "user_prompt": ("STRING", { + "default": "", + "multiline": True, + }), + "seed": ("INT", { + "default": 0, + "min": 0, + "max": 0xffffffffffffffff, + }), + }, + "optional": { + "temperature": ("FLOAT", { + "default": 0.7, + "min": 0.0, + "max": 2.0, + "step": 0.1, + }), + "top_p": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.05, + }), + "max_tokens": ("INT", { + "default": 1024, + "min": 1, + "max": 200000, + }), + # 多模态图像输入(模型需支持 image 输入才有意义) + "image_1": ("IMAGE",), + "image_2": ("IMAGE",), + "image_3": ("IMAGE",), + "image_4": ("IMAGE",), + "image_5": ("IMAGE",), + "image_6": ("IMAGE",), + "detail": (["auto", "low", "high"], { + "default": "auto", + }), + "image_max_size": ("INT", { + "default": 1024, + "min": 256, + "max": 4096, + "step": 64, + }), + # 高级:一般无需改动,留空即用官方地址(无需写 https://,会自动补全) + "base_url": ("STRING", { + "default": DEFAULT_BASE_URL_INPUT, + "multiline": False, + }), + "proxy_url": ("STRING", { + "default": "", + "multiline": False, + }), + }, + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("text", "model_id") + FUNCTION = "generate" + CATEGORY = "Rui-Node🐶/AI模型🤖" + + # ────────── 宽松校验 ────────── + @classmethod + def VALIDATE_INPUTS(cls, vendor, model): + """ + 接管 vendor / model 两个下拉的校验,替代 ComfyUI 内置的 + 「值必须在候选列表里」检查:价格快照更新后,旧工作流里保存的 + 标签(带旧价格)不再逐字匹配新列表,但只要能解析出 model id + 就应放行,避免整个工作流被判为无效。 + """ + if label_to_model_id(model) is None: + return f"无法从 '{model}' 解析出 ZenMux 模型 id" + return True + + # ────────── 图像编码 ────────── + @staticmethod + def _encode_image(img_tensor, max_size): + """把单张图像张量 [H,W,C](0~1)编码为 base64 JPEG。""" + img_np = np.clip(img_tensor.cpu().numpy(), 0, 1) + pil = Image.fromarray((img_np * 255).astype(np.uint8), 'RGB') + w, h = pil.size + if max(w, h) > max_size: + r = max_size / max(w, h) + pil = pil.resize((max(1, int(w * r)), max(1, int(h * r))), Image.LANCZOS) + buf = io.BytesIO() + pil.save(buf, format='JPEG', quality=85) + return base64.b64encode(buf.getvalue()).decode('utf-8') + + # ────────── 主函数 ────────── + def generate( + self, + api_key, + vendor, + model, + system_prompt, + user_prompt, + seed, + temperature=0.7, + top_p=1.0, + max_tokens=1024, + image_1=None, + image_2=None, + image_3=None, + image_4=None, + image_5=None, + image_6=None, + detail="auto", + image_max_size=1024, + base_url=DEFAULT_BASE_URL, + proxy_url="", + ): + # ---- 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}") + + if not (api_key or "").strip(): + return ("(错误:未填写 api_key,请在节点里填入 ZenMux 的 API Key)", model_id) + + # ---- 2. 收集图像 ---- + images = [ + img for img in (image_1, image_2, image_3, image_4, image_5, image_6) + if img is not None + ] + + # ---- 3. 构造 messages ---- + messages = [] + if system_prompt and system_prompt.strip(): + messages.append({"role": "system", "content": system_prompt}) + + if images: + parts = [] + if user_prompt and user_prompt.strip(): + parts.append({"type": "text", "text": user_prompt}) + for img in images: + b64 = self._encode_image(img[0], image_max_size) + parts.append({ + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{b64}", + "detail": detail, + }, + }) + if not parts: + parts.append({"type": "text", "text": " "}) + messages.append({"role": "user", "content": parts}) + else: + text = (user_prompt or "").strip() + if not text: + return ("(错误:未提供图片也未提供提示词,请至少填写 user_prompt)", model_id) + messages.append({"role": "user", "content": text}) + + # ---- 4. 请求 ---- + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key.strip()}", + } + payload = { + "model": model_id, + "messages": messages, + "seed": seed, + "temperature": temperature, + "top_p": top_p, + "max_tokens": max_tokens, + } + + proxies = None + p = _build_proxy_url(proxy_url) + if p: + proxies = {"http": p, "https": p} + + resp = None + try: + resp = requests.post(chat_url, headers=headers, json=payload, + proxies=proxies, timeout=180) + resp.raise_for_status() + data = resp.json() + if data.get("choices"): + msg = data["choices"][0].get("message", {}) + content = msg.get("content", "") + if isinstance(content, list): # 少数模型返回分段内容 + 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) + except requests.exceptions.ConnectionError as e: + return (f"连接失败(请检查网络/代理): {e}", model_id) + except requests.exceptions.Timeout: + return ("请求超时(180s),请检查网络或 ZenMux 服务状态。", model_id) + 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) + except Exception as e: + return (f"请求异常: {type(e).__name__}: {e}", model_id) + + +# ────────── ComfyUI 注册 ────────── +NODE_CLASS_MAPPINGS = { + "ZenMuxAPINode": ZenMuxNode, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "ZenMuxAPINode": "ZenMux API 连接 / ZenMux API Connector", +}