Files
rui40000andClaude Opus 4.8 cc6a6e3400 feat: 新增越光 API 连接节点(OpenAI 兼容,25 个模型内置价格)
参照 ZenMux 节点的架构编写,参数、输出与容错行为保持一致,便于互换。
端点 https://llm.ai-nebula.com/v1/chat/completions

与 ZenMux 的三点差异:
- 模型清单内置在 yueguang/model_registry.py,不做在线快照:越光没有
  可枚举的模型接口,清单与价格来自官方规范文档。少一个联网环节,
  也不会因拉取失败导致下拉变空
- model id 不带厂商前缀(gpt-4o 而非 openai/gpt-4o),下拉靠排序聚类,
  搜索时输 gpt/claude/deepseek/kimi 过滤
- 默认 deepseek-v4-flash(全表最便宜 $0.1/$0.3,官方示例也用它),
  默认值便宜可避免误触发产生意外费用

25 个模型的价格已按文档录入并逐项校验(标签解析、价格、厂商三项一致性
全部通过)。usage_stats 比 ZenMux 多一行厂商,价格换算经手算对照无误。

沿用的实战经验:base_url 默认值不带 ://(前端会吞协议片段)、
自适应参数重试(弃用 temperature / 需 max_completion_tokens 的模型)、
VALIDATE_INPUTS 宽松放行避免价格变动使旧工作流失效。
所有 19 个参数均带中文 tooltip。

验证:链路已打通(认证被接受、响应能正确解析),但文档提供的 key
余额为 0,返回 403「用户额度不足」,故未取得实际回复内容。
换有额度的 key 即可使用。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:19:39 +08:00

137 lines
4.9 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 有可枚举的模型接口),
因此这里直接内置清单,不做在线快照 —— 少一个联网环节,也不会因为
拉取失败而让下拉变空。价格有变动时改这张表即可。
对外接口与 zenmux/model_registry.py 保持一致,便于两个节点共用同一套写法:
- all_model_labels() 带价格的下拉标签,按厂商分组、组内保持官方顺序
- default_model_label() 默认模型对应的标签
- label_to_model_id() 从标签解析真实 model id(对旧标签/纯 id 也兼容)
- model_prices() 按 model id 查(输入单价, 输出单价),USD/百万 token
- model_label_by_id() 按 model id 生成带价签标签
标签格式(价格单位:USD / 百万 token):
gpt-4o [入$2.5/M 出$10/M]
标签第一个空格前恒为 model id,后端解析依赖这一点。
注意:越光的 model id **不带厂商前缀**(是 gpt-4o 而非 openai/gpt-4o),
这点与 ZenMux 不同。下拉里同厂商的模型是靠排序聚在一起的,
搜索时输 gpt / claude / deepseek / kimi 即可过滤。
"""
# (厂商, model id, 输入单价, 输出单价) 单价单位:USD / 百万 token
# 数据来源:越光 API 接入规范(2026-07-29),docs.stars-cloud.com/cn
_MODELS_RAW = [
# ---- OpenAI ----
("OpenAI", "gpt-5.6-sol", 4.75, 5.00),
("OpenAI", "gpt-5.6-terra", 2.375, 2.50),
("OpenAI", "gpt-5.6-luna", 0.95, 1.00),
("OpenAI", "gpt-4.1", 2.00, 8.00),
("OpenAI", "gpt-4.1-mini", 0.40, 1.60),
("OpenAI", "gpt-4o", 2.50, 10.00),
("OpenAI", "gpt-4o-mini", 0.15, 0.60),
("OpenAI", "o4-mini", 1.10, 4.40),
("OpenAI", "o3-mini", 1.10, 4.40),
# ---- Anthropic ----
("Anthropic", "claude-opus-5", 5.00, 5.00),
("Anthropic", "claude-opus-4-7", 15.00, 75.00),
("Anthropic", "claude-opus-4-6", 15.00, 75.00),
("Anthropic", "claude-sonnet-5", 2.00, 2.00),
("Anthropic", "claude-sonnet-4-6", 3.00, 15.00),
("Anthropic", "claude-haiku-4-5-20251001", 0.80, 4.00),
("Anthropic", "claude-fable-5", 3.00, 15.00),
# ---- DeepSeek ----
("DeepSeek", "deepseek-v4-pro", 2.19, 8.76),
("DeepSeek", "deepseek-v4-flash", 0.10, 0.30),
("DeepSeek", "deepseek-r1-250528", 0.55, 2.19),
("DeepSeek", "deepseek-v3-250324", 0.27, 1.10),
# ---- Kimi (Moonshot) ----
("Kimi", "kimi-k3", 2.86, 2.86),
("Kimi", "kimi-k2.7-code", 1.00, 4.00),
("Kimi", "kimi-k2.6", 1.00, 4.00),
("Kimi", "kimi-k2.5", 1.00, 4.00),
("Kimi", "kimi-k2-thinking", 1.00, 4.00),
]
# 默认用最便宜的 deepseek-v4-flash($0.1/$0.3):官方示例也用它,
# 且默认值便宜可以避免误触发时产生意外费用。
DEFAULT_MODEL_ID = "deepseek-v4-flash"
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 "?"
_MODELS = [{"vendor": v, "id": i, "input_price": ip, "output_price": op}
for v, i, ip, op in _MODELS_RAW]
def _make_label(m):
return (f"{m['id']} "
f"[入${_fmt_price(m.get('input_price'))}/M "
f"出${_fmt_price(m.get('output_price'))}/M]")
_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():
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 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 model_vendor(model_id):
m = _ID_TO_MODEL.get(model_id)
return m["vendor"] if m else "?"
def label_to_model_id(label):
"""
从下拉标签解析 model id。三层兼容:
1. 当前标签精确命中;
2. 旧工作流里存的标签(价格已变动)→ 取第一个空格前的 id 段;
3. 用户直接填了纯 model id → 原样返回(未收录也放行,交服务端判定)。
"""
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
return head if head else None