初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,612 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>魂镜 · 模型网关页设计预览</title>
|
||||
<link rel="stylesheet" href="tokens.css" />
|
||||
<link rel="stylesheet" href="_base.css" />
|
||||
<link rel="stylesheet" href="_chrome.css" />
|
||||
<style>
|
||||
/* ============================================================
|
||||
* 铜镜鉴心 v2.1 — 模型网关页
|
||||
* 能力槽位(默认路由)+ 模型清单(多供应商端点池)+ 定价对照 + 能力矩阵
|
||||
* 数据源:config/models · providers · pricing example
|
||||
* 对齐 docs/architecture/service-boundaries.md §6/§7/§8/§13/§14。
|
||||
* ============================================================ */
|
||||
|
||||
.page-header .spacer { flex: 1; }
|
||||
.stat-pills { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
.stat-pill .v { color: var(--patina); font-weight: 600; }
|
||||
.stat-pill .v.gilt { color: var(--gilt); }
|
||||
|
||||
.section-label .sub { font-size: 12px; letter-spacing: 0.04em; color: var(--fg-dim); opacity: 0.75; }
|
||||
.section-label .glyph { color: var(--gilt); opacity: 0.8; }
|
||||
.section-header { font-family: "JetBrains Mono", monospace; font-size: 12px; letter-spacing: 0.1em; color: var(--fg-faint); margin-bottom: 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.section-header .sub { font-size: 12px; letter-spacing: 0.04em; color: var(--fg-dim); opacity: 0.75; }
|
||||
.section-header .glyph { color: var(--gilt); opacity: 0.8; }
|
||||
.section-caption { font-size: 11.5px; color: var(--fg-faint); margin-top: 8px; line-height: 1.5; }
|
||||
|
||||
/* ── 能力槽位(默认路由)── */
|
||||
.slot-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.slot-card { border: 1px solid var(--rule); border-radius: 8px; background: var(--surface); padding: 13px 15px; display: flex; flex-direction: column; gap: 7px; transition: border-color 0.12s; }
|
||||
.slot-card.set { border-color: var(--patina-dim); background: var(--patina-wash); }
|
||||
.slot-card.unset { border-style: dashed; }
|
||||
.slot-top { display: flex; align-items: baseline; gap: 8px; }
|
||||
.slot-label { font-family: "Songti SC", serif; font-size: 14px; font-weight: 700; color: var(--fg); }
|
||||
.slot-hint { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--fg-faint); letter-spacing: 0.04em; }
|
||||
.slot-current { font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: var(--patina-bright); font-weight: 600; line-height: 1.4; }
|
||||
.slot-card.unset .slot-current { color: var(--fg-faint); font-weight: 400; }
|
||||
.slot-display { font-size: 11.5px; color: var(--fg-dim); font-family: -apple-system, "PingFang SC", sans-serif; font-weight: 400; }
|
||||
.slot-select { font-family: "JetBrains Mono", monospace; font-size: 11.5px; padding: 6px 9px; border-radius: 6px; border: 1px solid var(--rule); background: var(--ink); color: var(--fg); width: 100%; margin-top: auto; }
|
||||
.slot-select:focus { outline: none; border-color: var(--patina-dim); }
|
||||
.slot-empty { font-size: 11px; color: var(--fg-faint); font-family: "JetBrains Mono", monospace; padding: 6px 0; margin-top: auto; line-height: 1.5; }
|
||||
|
||||
/* ── 供应商分组 + 模型卡 ── */
|
||||
.provider-group { display: flex; flex-direction: column; gap: 10px; margin-bottom: 4px; }
|
||||
.provider-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-family: "Songti SC", serif; font-size: 14px; font-weight: 700; color: var(--fg); }
|
||||
.provider-head .ph-key { color: var(--fg-faint); font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 500; }
|
||||
.provider-head .ph-meta { font-family: "JetBrains Mono", monospace; font-size: 10.5px; font-weight: 400; color: var(--fg-faint); letter-spacing: 0.04em; }
|
||||
.provider-head .ph-meta .sep { opacity: 0.5; margin: 0 5px; }
|
||||
.provider-head .ph-line { flex: 1; height: 1px; background: var(--rule); min-width: 20px; }
|
||||
.provider-head .ph-key-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--patina); flex-shrink: 0; align-self: center; }
|
||||
|
||||
.model-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
||||
.model-card { border: 1px solid var(--rule); border-radius: 8px; background: var(--surface); padding: 14px 16px; display: flex; flex-direction: column; gap: 11px; transition: border-color 0.12s; }
|
||||
.model-card:hover { border-color: var(--gilt-dim); }
|
||||
.model-card.is-default { border-color: var(--patina-dim); background: var(--patina-wash); }
|
||||
.model-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.model-star { color: var(--patina); font-size: 13px; line-height: 1; flex-shrink: 0; }
|
||||
.model-star.off { visibility: hidden; }
|
||||
.model-name { font-family: "JetBrains Mono", monospace; font-size: 13.5px; font-weight: 600; color: var(--fg); }
|
||||
.model-real { font-family: "JetBrains Mono", monospace; font-size: 10.5px; color: var(--fg-faint); }
|
||||
.model-real .arr { opacity: 0.6; margin-right: 2px; }
|
||||
.model-tag { font-family: "JetBrains Mono", monospace; font-size: 9.5px; padding: 2px 7px; border-radius: 3px; border: 1px solid var(--rule); color: var(--fg-faint); background: transparent; }
|
||||
.model-tag.fmt { border-color: var(--rule); color: var(--fg-faint); }
|
||||
.model-tag.default-tag { border-color: var(--patina-dim); color: var(--patina); background: var(--patina-wash); }
|
||||
.model-tag.fallback { border-color: var(--gilt-dim); color: var(--gilt); background: var(--gilt-faint); }
|
||||
.model-tag.fast { border-color: var(--cinnabar-dim); color: var(--cinnabar); background: rgba(184, 57, 46, 0.08); }
|
||||
.model-desc { font-size: 12.5px; color: var(--fg-dim); line-height: 1.55; min-height: 30px; }
|
||||
|
||||
/* spec 微网格(全字段,空值显 —) */
|
||||
.model-specs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--rule); border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
|
||||
.model-specs .cell { background: color-mix(in srgb, var(--ink) 55%, transparent); padding: 6px 9px; }
|
||||
.model-specs .cell .k { font-family: "JetBrains Mono", monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-faint); }
|
||||
.model-specs .cell .v { font-family: "JetBrains Mono", monospace; font-size: 12px; color: var(--fg); margin-top: 1px; }
|
||||
.model-specs .cell .v .sub { color: var(--fg-faint); font-size: 9.5px; display: block; margin-top: 1px; }
|
||||
.model-specs .cell .v.na { color: var(--fg-faint); opacity: 0.55; }
|
||||
|
||||
/* 计价条(原值不四舍五入) */
|
||||
.model-price { display: flex; flex-wrap: wrap; gap: 8px 18px; padding: 9px 11px; border: 1px solid var(--gilt-dim); border-radius: 6px; background: var(--gilt-faint); }
|
||||
.model-price .price-cell { display: flex; flex-direction: column; gap: 1px; }
|
||||
.model-price .pk { font-family: "JetBrains Mono", monospace; font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--gilt); opacity: 0.78; }
|
||||
.model-price .pv { font-family: "JetBrains Mono", monospace; font-size: 13px; color: var(--gilt); font-weight: 600; }
|
||||
.model-price .pv .ccy { font-size: 10px; opacity: 0.7; margin-left: 1px; }
|
||||
.model-price .pv.na { color: var(--fg-faint); font-size: 12px; font-weight: 400; opacity: 0.7; }
|
||||
|
||||
/* 能力点阵 */
|
||||
.model-caps { display: flex; flex-wrap: wrap; gap: 5px 11px; }
|
||||
.model-caps .cap { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; color: var(--fg-dim); font-family: "JetBrains Mono", monospace; }
|
||||
.model-caps .cap .d { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.model-caps .cap.on .d { background: var(--patina); }
|
||||
.model-caps .cap.off { color: var(--fg-faint); opacity: 0.6; }
|
||||
.model-caps .cap.off .d { background: var(--fg-faint); opacity: 0.32; }
|
||||
|
||||
/* 多供应商端点池 */
|
||||
.ep-pool { display: flex; flex-direction: column; gap: 4px; border: 1px solid var(--rule); border-radius: 6px; padding: 8px 10px; background: color-mix(in srgb, var(--ink) 40%, transparent); }
|
||||
.ep-pool .ep-title { font-family: "JetBrains Mono", monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--fg-faint); margin-bottom: 3px; }
|
||||
.ep-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--fg-dim); line-height: 1.5; }
|
||||
.ep-row .ep-status { width: 7px; height: 7px; border-radius: 50%; background: var(--patina); flex-shrink: 0; }
|
||||
.ep-row .ep-status.off { background: var(--fg-faint); opacity: 0.4; }
|
||||
.ep-row .ep-name { color: var(--patina); }
|
||||
.ep-row .ep-fmt { color: var(--gilt); font-size: 10px; }
|
||||
.ep-row .ep-env { color: var(--fg-faint); font-size: 10px; }
|
||||
.ep-row .ep-override { color: var(--cinnabar); font-size: 10px; }
|
||||
.ep-row .ep-f { display: inline-flex; align-items: baseline; gap: 2px; }
|
||||
.ep-row .ep-f i { font-style: normal; color: var(--fg-faint); font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.ep-row .ep-f .na { color: var(--fg-faint); opacity: 0.5; }
|
||||
.ep-row .ep-sep { color: var(--rule); }
|
||||
|
||||
/* ── 通用表格 ── */
|
||||
.cap-table { border: 1px solid var(--rule); border-radius: 8px; overflow: hidden; background: var(--surface); }
|
||||
.cap-table table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.cap-table th, .cap-table td { padding: 9px 14px; text-align: left; border-bottom: 1px solid var(--rule); }
|
||||
.cap-table tr:last-child td { border-bottom: none; }
|
||||
.cap-table th { font-family: "JetBrains Mono", monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-faint); font-weight: 600; background: var(--surface-hi); white-space: nowrap; }
|
||||
.cap-table td.model-col { font-family: "JetBrains Mono", monospace; color: var(--fg); font-weight: 500; white-space: nowrap; }
|
||||
.cap-table td.model-col .star { color: var(--patina); margin-right: 4px; }
|
||||
.cap-table td.num, .cap-table th.num { text-align: right; font-family: "JetBrains Mono", monospace; }
|
||||
.cap-table td.num.cost { color: var(--gilt); font-weight: 600; }
|
||||
.cap-table tr.default-row td { background: var(--patina-wash); }
|
||||
.cap-table tr.default-row td.model-col { color: var(--patina); }
|
||||
.cap-table td .ccy { color: var(--fg-faint); font-size: 10.5px; }
|
||||
.cap-table td .na { color: var(--fg-faint); opacity: 0.55; }
|
||||
.cap-dot { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.cap-dot .d { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.cap-dot .d.full { background: var(--patina); }
|
||||
.cap-dot .d.no { background: var(--fg-faint); opacity: 0.4; }
|
||||
.cap-dot span { font-size: 11px; color: var(--fg-dim); }
|
||||
.cap-table .grp td { font-family: "Songti SC", serif; font-size: 12px; font-weight: 700; color: var(--fg-dim); background: color-mix(in srgb, var(--surface-hi) 60%, transparent); padding: 6px 14px; }
|
||||
.cap-table .grp td .gkey { font-family: "JetBrains Mono", monospace; font-size: 10px; color: var(--fg-faint); font-weight: 400; margin-left: 6px; }
|
||||
|
||||
/* ── 语音 / 专用能力 ── */
|
||||
.audio-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||||
.audio-card { border: 1px solid var(--rule); border-radius: 8px; background: var(--surface); padding: 12px 14px; display: flex; flex-direction: column; gap: 5px; }
|
||||
.audio-card.is-default { border-color: var(--patina-dim); background: var(--patina-wash); }
|
||||
.audio-card .a-head { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
||||
.audio-card .a-name { font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 600; color: var(--fg); }
|
||||
.audio-card .a-kind { font-size: 9px; font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.07em; color: var(--cinnabar); border: 1px solid var(--cinnabar-dim); padding: 1px 6px; border-radius: 3px; }
|
||||
.audio-card .a-tag { font-size: 12px; color: var(--fg-dim); }
|
||||
.audio-card .a-spec { font-size: 10.5px; color: var(--fg-faint); font-family: "JetBrains Mono", monospace; display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 2px; }
|
||||
.audio-card .a-spec .na { opacity: 0.5; }
|
||||
|
||||
@media (max-width: 980px) { .slot-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 900px) { .model-grid { grid-template-columns: 1fr; } .audio-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 768px) { .sidebar-wrap { display: none; } .page-body { padding: 18px 16px 40px; } .cap-table { overflow-x: auto; } .cap-table table { min-width: 540px; } .slot-grid, .audio-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<!-- ════════ 侧栏(active=模型网关)════════ -->
|
||||
<div class="sidebar-mount" data-active="模型网关"></div>
|
||||
|
||||
<!-- ════════ 主区 ════════ -->
|
||||
<div class="main-area">
|
||||
<div class="page-header">
|
||||
<span class="page-title">模型网关</span>
|
||||
<span class="page-sub">神识 · 多供应商统一接入与路由</span>
|
||||
<span class="spacer"></span>
|
||||
<div class="stat-pills">
|
||||
<span class="stat-pill">对话模型 <span class="v">7</span></span>
|
||||
<span class="stat-pill">专用槽 <span class="v">4</span></span>
|
||||
<span class="stat-pill">供应商 <span class="v">3</span></span>
|
||||
<span class="stat-pill">已设默认 <span class="v gilt" id="stat-set">4</span>/6</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<!-- 能力槽位(默认路由) -->
|
||||
<div>
|
||||
<div class="section-header"><span class="glyph">⟡</span> 能力槽位 · 默认路由 <span class="sub">model-gateway §13/§14 · 按能力解析为默认模型</span></div>
|
||||
<div class="slot-grid" id="slot-grid"></div>
|
||||
<p class="section-caption">业务层只引用能力槽位(model_hint),网关将其解析为下列默认模型;空槽表示该能力尚未配置可用模型,<code>models.yaml</code> 增配后自动出现。</p>
|
||||
</div>
|
||||
|
||||
<!-- 模型清单 -->
|
||||
<div>
|
||||
<h3 style="font-family:"Songti SC",serif;font-size:14px;font-weight:600;color:var(--fg);margin-bottom:14px">模型清单 <span style="font-size:12px;font-weight:400;color:var(--fg-dim);font-family:"JetBrains Mono",monospace;margin-left:6px">models.yaml + providers.yaml + pricing.yaml · 全字段 · 多供应商端点池</span></h3>
|
||||
<div id="model-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 定价对照 -->
|
||||
<div>
|
||||
<h3 style="font-family:"Songti SC",serif;font-size:14px;font-weight:600;color:var(--fg);margin-bottom:14px">定价对照 <span style="font-size:12px;font-weight:400;color:var(--fg-dim);font-family:"JetBrains Mono",monospace;margin-left:6px">pricing.yaml · 每 1M tokens · 原值不四舍五入</span></h3>
|
||||
<div class="cap-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模型</th>
|
||||
<th class="num">输入 / 1M</th>
|
||||
<th class="num">缓存命中 / 1M</th>
|
||||
<th class="num">输出 / 1M</th>
|
||||
<th class="num">混合(1M 入 + 1M 出)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="price-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="section-caption">混合列为同币种 input + output 之和,便于横向比较;保持厂商原始币种,不做跨币种换算(§7.3)。缓存命中价缺省记 -,按输入价计费。</p>
|
||||
</div>
|
||||
|
||||
<!-- 能力矩阵 -->
|
||||
<div>
|
||||
<h3 style="font-family:"Songti SC",serif;font-size:14px;font-weight:600;color:var(--fg);margin-bottom:14px">能力矩阵 <span style="font-size:12px;font-weight:400;color:var(--fg-dim);font-family:"JetBrains Mono",monospace;margin-left:6px">model-gateway §6/§13 · supports_* 显式声明</span></h3>
|
||||
<div class="cap-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>模型</th>
|
||||
<th class="num">上下文</th>
|
||||
<th>工具</th>
|
||||
<th>思考</th>
|
||||
<th>推理 effort</th>
|
||||
<th>结构化</th>
|
||||
<th>流式</th>
|
||||
<th>视觉</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cap-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 语音 / 专用能力 -->
|
||||
<div>
|
||||
<h3 style="font-family:"Songti SC",serif;font-size:14px;font-weight:600;color:var(--fg);margin-bottom:14px">语音 / 专用能力 <span style="font-size:12px;font-weight:400;color:var(--fg-dim);font-family:"JetBrains Mono",monospace;margin-left:6px">asr · tts · 声纹 · 音色 · 后续 embedding/reranker 槽位预留</span></h3>
|
||||
<div class="audio-grid" id="audio-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ── 数据:取自 config/models · providers · pricing example ── */
|
||||
const PROVIDERS = {
|
||||
deepseek: { label: "深度求索", format: "openai", baseEnv: "DEEPSEEK_BASE_URL", apiKeyEnv: "DEEPSEEK_API_KEY", timeout: 300, retries: 2 },
|
||||
anthropic: { label: "Anthropic", format: "anthropic", baseEnv: "ANTHROPIC_BASE_URL", apiKeyEnv: "ANTHROPIC_API_KEY", timeout: 120, retries: 1 },
|
||||
xiaomi: { label: "小米 MiMo", format: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", apiKeyEnv: "XIAOMI_MIMO_API_KEY", timeout: 300, retries: 2 },
|
||||
};
|
||||
|
||||
// 对话模型(deepseek-v4-pro 已合并 anthropic 格式端点;同模型不同消息格式不重复成卡)
|
||||
const MODELS = [
|
||||
{
|
||||
name: "deepseek-v4-flash", display: "DeepSeek V4 Flash", provider: "deepseek",
|
||||
real: "deepseek-v4-flash", tags: [{ k: "fallback", t: "默认" }],
|
||||
ctx: 1000000, reserve: 8192, maxOut: 384000, timeout: 300,
|
||||
privacy: null, effort: null,
|
||||
routing: "priority_failover", pool: true,
|
||||
caps: { tools: 1, think: 1, effort: 1, struct: 1, stream: 1, vision: 0 },
|
||||
endpoints: [
|
||||
{ name: "primary", model: "deepseek-v4-flash", fmt: "openai", baseEnv: "DEEPSEEK_BASE_URL", status: "active", p: 10, w: 100, rpm: 2500, tpm: null, conc: null },
|
||||
{ name: "fallback", model: "deepseek-v4-pro", fmt: "openai", baseEnv: "DEEPSEEK_BASE_URL", status: "active", p: 20, w: 50, rpm: null, tpm: null, conc: null },
|
||||
],
|
||||
price: { ccy: "CNY", in: 1.0, cache: 0.02, out: 2.0 },
|
||||
desc: "默认对话与生成主模型;长上下文、低成本,工具与思考皆备,端点池带 failover。",
|
||||
},
|
||||
{
|
||||
name: "deepseek-v4-pro", display: "DeepSeek V4 Pro", provider: "deepseek",
|
||||
real: "deepseek-v4-pro", tags: [{ k: "fallback", t: "备选" }],
|
||||
ctx: 1000000, reserve: 8192, maxOut: 384000, timeout: 300,
|
||||
privacy: null, effort: null,
|
||||
routing: "priority_failover", pool: true,
|
||||
caps: { tools: 1, think: 1, effort: 1, struct: 1, stream: 1, vision: 0 },
|
||||
endpoints: [
|
||||
{ name: "primary", model: "deepseek-v4-pro", fmt: "openai", baseEnv: "DEEPSEEK_BASE_URL", status: "active", p: 10, w: 100, rpm: null, tpm: null, conc: null },
|
||||
{ name: "anthropic", model: "deepseek-v4-pro", fmt: "anthropic", baseEnv: "DEEPSEEK_ANTHROPIC_BASE_URL", status: "active", p: 10, w: 100, rpm: null, tpm: null, conc: null },
|
||||
],
|
||||
price: { ccy: "CNY", in: 3.0, cache: 0.025, out: 6.0 },
|
||||
desc: "高难推理与长文写作主力;同一模型提供 openai 与 anthropic 两种消息协议端点,价格与能力一致。",
|
||||
},
|
||||
{
|
||||
name: "claude-sonnet", display: "Claude Sonnet", provider: "anthropic",
|
||||
real: "claude-sonnet-4-6", tags: [],
|
||||
ctx: 1000000, reserve: 16000, maxOut: 16000, timeout: 60,
|
||||
privacy: null, effort: "medium",
|
||||
routing: null, pool: false,
|
||||
caps: { tools: 1, think: 1, effort: 1, struct: 1, stream: 1, vision: 1 },
|
||||
endpoints: null,
|
||||
price: { ccy: "USD", in: 3.0, cache: 0.30, out: 15.0 },
|
||||
desc: "均衡型主力,推理与工具调用稳定;中文叙事自然,可读图。默认 effort: medium。",
|
||||
},
|
||||
{
|
||||
name: "claude-haiku", display: "Claude Haiku", provider: "anthropic",
|
||||
real: "claude-haiku-4-5", tags: [{ k: "fast", t: "轻量" }],
|
||||
ctx: 200000, reserve: 4096, maxOut: 4096, timeout: 60,
|
||||
privacy: null, effort: null,
|
||||
routing: null, pool: false,
|
||||
caps: { tools: 1, think: 0, effort: 0, struct: 1, stream: 1, vision: 1 },
|
||||
endpoints: null,
|
||||
price: { ccy: "USD", in: 1.0, cache: 0.10, out: 5.0 },
|
||||
desc: "低延迟轻量档,适合快速回复与短抽取;不支持思考模式。",
|
||||
},
|
||||
{
|
||||
name: "claude-fable", display: "Claude Fable 5", provider: "anthropic",
|
||||
real: "claude-fable-5", tags: [{ k: "fallback", t: "推理" }],
|
||||
ctx: 1000000, reserve: 16000, maxOut: 16000, timeout: 120,
|
||||
privacy: null, effort: "high",
|
||||
routing: null, pool: false,
|
||||
caps: { tools: 1, think: 1, effort: 1, struct: 1, stream: 1, vision: 1 },
|
||||
endpoints: null,
|
||||
price: { ccy: "USD", in: 10.0, cache: 1.00, out: 50.0 },
|
||||
desc: "最强推理与长程规划,思考常开、effort: high;成本最高,留作关键决策。",
|
||||
},
|
||||
{
|
||||
name: "mimo-v2.5-pro", display: "MiMo v2.5 Pro", provider: "xiaomi",
|
||||
real: "mimo-v2.5-pro", tags: [],
|
||||
ctx: 128000, reserve: 8192, maxOut: 32768, timeout: 300,
|
||||
privacy: null, effort: null,
|
||||
routing: "priority_failover", pool: true,
|
||||
caps: { tools: 1, think: 0, effort: 1, struct: 1, stream: 1, vision: 1 },
|
||||
endpoints: [
|
||||
{ name: "primary", model: "mimo-v2.5-pro", fmt: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", status: "active", p: 10, w: 100, rpm: null, tpm: null, conc: null },
|
||||
],
|
||||
price: { ccy: "CNY", in: 4.0, cache: null, out: 12.0 },
|
||||
desc: "国产多模态主力,视觉与工具兼顾;中文场景性价比高。",
|
||||
},
|
||||
{
|
||||
name: "mimo-v2.5", display: "MiMo v2.5", provider: "xiaomi",
|
||||
real: "mimo-v2.5", tags: [{ k: "fast", t: "轻量" }],
|
||||
ctx: 128000, reserve: 4096, maxOut: 16384, timeout: 180,
|
||||
privacy: null, effort: null,
|
||||
routing: null, pool: false,
|
||||
caps: { tools: 1, think: 0, effort: 0, struct: 1, stream: 1, vision: 1 },
|
||||
endpoints: null,
|
||||
price: { ccy: "CNY", in: 1.0, cache: null, out: 4.0 },
|
||||
desc: "日常对话轻量档,响应快、单价低;视觉可读图。",
|
||||
},
|
||||
];
|
||||
|
||||
// 语音 / 专用能力
|
||||
const AUDIO = [
|
||||
{ name: "mimo-v2.5-asr", display: "MiMo v2.5 ASR", provider: "xiaomi", kind: "asr", kindLabel: "ASR", tag: "语音转文字", stream: 0, timeout: 120, fmt: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", price: null },
|
||||
{ name: "mimo-v2.5-tts", display: "MiMo v2.5 TTS", provider: "xiaomi", kind: "tts", kindLabel: "TTS", tag: "文字转语音", stream: 1, timeout: 120, fmt: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", price: null },
|
||||
{ name: "mimo-v2.5-tts-voiceclone", display: "MiMo v2.5 声纹克隆", provider: "xiaomi", kind: "tts", kindLabel: "TTS", tag: "声纹克隆", stream: 1, timeout: 300, fmt: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", price: null },
|
||||
{ name: "mimo-v2.5-tts-voicedesign", display: "MiMo v2.5 音色设计", provider: "xiaomi", kind: "tts", kindLabel: "TTS", tag: "音色设计", stream: 1, timeout: 300, fmt: "openai", baseEnv: "XIAOMI_MIMO_BASE_URL", price: null },
|
||||
];
|
||||
|
||||
// 能力槽位(默认路由)
|
||||
const SLOTS = [
|
||||
{ key: "chat", label: "默认对话", hint: "default-chat", pick: () => MODELS },
|
||||
{ key: "reasoning", label: "默认推理", hint: "reasoning", pick: () => MODELS.filter((m) => m.caps.think) },
|
||||
{ key: "vision", label: "视觉理解", hint: "vision", pick: () => MODELS.filter((m) => m.caps.vision) },
|
||||
{ key: "tts", label: "语音合成", hint: "tts", pick: () => AUDIO.filter((a) => a.kind === "tts") },
|
||||
{ key: "embedding", label: "向量化", hint: "embedding", pick: () => AUDIO.filter((a) => a.kind === "embedding") },
|
||||
{ key: "reranker", label: "重排序", hint: "reranker", pick: () => AUDIO.filter((a) => a.kind === "reranker") },
|
||||
];
|
||||
|
||||
const state = {
|
||||
defaults: {
|
||||
chat: "deepseek-v4-flash",
|
||||
reasoning: "claude-fable",
|
||||
vision: "mimo-v2.5-pro",
|
||||
tts: "mimo-v2.5-tts",
|
||||
embedding: null,
|
||||
reranker: null,
|
||||
},
|
||||
};
|
||||
|
||||
/* ── 工具函数 ── */
|
||||
const fmt = (n) => (n == null ? "-" : n.toLocaleString("en-US"));
|
||||
const ccySym = (c) => (c === "USD" ? "$" : "¥");
|
||||
// 原值原样,不四舍五入
|
||||
function money(c, v) { return v == null ? "-" : ccySym(c) + v; }
|
||||
function fmtCtx(n) {
|
||||
if (n == null) return "-";
|
||||
if (n >= 1000000) return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1) + "M";
|
||||
if (n >= 1000) return (n / 1000).toFixed(0) + "K";
|
||||
return String(n);
|
||||
}
|
||||
function fmtCtxFull(n) { return n == null ? "-" : n >= 1000 ? n.toLocaleString("en-US") : String(n); }
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||
const orDash = (v) => (v == null || v === "" ? "-" : v);
|
||||
function findChat(name) { return MODELS.find((m) => m.name === name); }
|
||||
function findAudio(name) { return AUDIO.find((a) => a.name === name); }
|
||||
function labelOf(name) { const m = findChat(name) || findAudio(name); return m ? m.display : "-"; }
|
||||
|
||||
// 解析端点池:无 endpoints 时合成隐式单端点(继承 provider 配置)
|
||||
function endpointsOf(m) {
|
||||
const p = PROVIDERS[m.provider];
|
||||
if (m.endpoints && m.endpoints.length) {
|
||||
return m.endpoints.map((e) => ({
|
||||
name: e.name,
|
||||
fmt: e.fmt || p.format,
|
||||
baseEnv: e.baseEnv || p.baseEnv,
|
||||
apiKeyEnv: e.apiKeyEnv || p.apiKeyEnv,
|
||||
model: e.model && e.model !== m.real ? e.model : null,
|
||||
status: e.status || "active",
|
||||
p: e.p, w: e.w, rpm: e.rpm, tpm: e.tpm, conc: e.conc,
|
||||
implicit: false,
|
||||
}));
|
||||
}
|
||||
return [{ name: "default", fmt: p.format, baseEnv: p.baseEnv, apiKeyEnv: p.apiKeyEnv, model: null, status: "active", p: null, w: null, rpm: null, tpm: null, conc: null, implicit: true }];
|
||||
}
|
||||
function modelFmts(m) { return [...new Set(endpointsOf(m).map((e) => e.fmt))]; }
|
||||
|
||||
// 该模型是哪些槽位的默认
|
||||
function defaultSlotsOf(name) {
|
||||
return Object.entries(state.defaults)
|
||||
.filter(([, v]) => v === name)
|
||||
.map(([k]) => k);
|
||||
}
|
||||
const slotLabel = (key) => SLOTS.find((s) => s.key === key).label;
|
||||
|
||||
/* ── 渲染:能力槽位 ── */
|
||||
function renderSlots() {
|
||||
const setCount = Object.values(state.defaults).filter((v) => v).length;
|
||||
document.getElementById("stat-set").textContent = setCount;
|
||||
document.getElementById("slot-grid").innerHTML = SLOTS.map((s) => {
|
||||
const eligible = s.pick();
|
||||
const cur = state.defaults[s.key];
|
||||
const isSet = !!cur;
|
||||
if (!eligible.length) {
|
||||
return `
|
||||
<div class="slot-card unset">
|
||||
<div class="slot-top"><span class="slot-label">${s.label}</span><span class="slot-hint">${s.hint}</span></div>
|
||||
<div class="slot-current">未设置<span class="slot-display">暂无可用模型</span></div>
|
||||
<div class="slot-empty">配置 models.yaml 后自动出现</div>
|
||||
</div>`;
|
||||
}
|
||||
const curLabel = isSet ? labelOf(cur) : "未设置";
|
||||
return `
|
||||
<div class="slot-card ${isSet ? "set" : "unset"}">
|
||||
<div class="slot-top"><span class="slot-label">${s.label}</span><span class="slot-hint">${s.hint}</span></div>
|
||||
<div class="slot-current">${isSet ? esc(cur) : "未设置"}<span class="slot-display">${esc(curLabel)}</span></div>
|
||||
<select class="slot-select" onchange="setSlot('${s.key}', this.value)">
|
||||
<option value="">- 未设置 -</option>
|
||||
${eligible.map((m) => `<option value="${m.name}" ${cur === m.name ? "selected" : ""}>${esc(m.display)} · ${m.name}</option>`).join("")}
|
||||
</select>
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
function setSlot(key, val) {
|
||||
state.defaults[key] = val || null;
|
||||
renderSlots();
|
||||
renderModelList();
|
||||
renderCapMatrix();
|
||||
renderAudio();
|
||||
}
|
||||
|
||||
/* ── 渲染:模型清单 ── */
|
||||
function renderModelList() {
|
||||
const groups = {};
|
||||
for (const m of MODELS) (groups[m.provider] = groups[m.provider] || []).push(m);
|
||||
document.getElementById("model-list").innerHTML = Object.entries(groups).map(([key, list]) => {
|
||||
const p = PROVIDERS[key];
|
||||
return `
|
||||
<div class="provider-group">
|
||||
<div class="provider-head">
|
||||
<span class="ph-key-dot"></span>${esc(p.label)}
|
||||
<span class="ph-key">${key}</span>
|
||||
<span class="ph-meta">${p.format}<span class="sep">·</span>base ${p.baseEnv}<span class="sep">·</span>key ${p.apiKeyEnv}<span class="sep">·</span>超时 ${p.timeout}s<span class="sep">·</span>重试 ${p.retries}</span>
|
||||
<span class="ph-line"></span>
|
||||
</div>
|
||||
<div class="model-grid">${list.map(cardHtml).join("")}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function cardHtml(m) {
|
||||
const defSlots = defaultSlotsOf(m.name);
|
||||
const isAnyDefault = defSlots.length > 0;
|
||||
const usable = m.ctx - m.reserve;
|
||||
const fmts = modelFmts(m);
|
||||
const fmtTag = `<span class="model-tag fmt">${fmts.join(" · ")}</span>`;
|
||||
const defTags = defSlots.map((k) => `<span class="model-tag default-tag">★ ${esc(slotLabel(k))}</span>`).join("");
|
||||
const tags = m.tags.map((t) => `<span class="model-tag ${t.k}">${esc(t.t)}</span>`).join("") + fmtTag + defTags;
|
||||
const effortCell = m.effort || (m.caps.effort ? "可调" : (m.caps.think ? "思考" : null));
|
||||
|
||||
return `
|
||||
<div class="model-card${isAnyDefault ? " is-default" : ""}" data-name="${m.name}">
|
||||
<div class="model-head">
|
||||
<span class="model-star ${isAnyDefault ? "" : "off"}">${isAnyDefault ? "★" : "★"}</span>
|
||||
<span class="model-name">${esc(m.name)}</span>
|
||||
<span class="model-real"><span class="arr">→</span>${esc(m.real)}</span>
|
||||
${tags}
|
||||
</div>
|
||||
<div class="model-desc">${esc(m.desc)}</div>
|
||||
<div class="model-specs">
|
||||
${specCell("上下文", fmtCtxFull(m.ctx), `预留 ${fmt(m.reserve)}`)}
|
||||
${specCell("可用上下文", fmtCtxFull(usable))}
|
||||
${specCell("最大输出", fmtCtxFull(m.maxOut))}
|
||||
${specCell("超时", m.timeout + "s")}
|
||||
${specCell("隐私", orDash(m.privacy), null, m.privacy == null)}
|
||||
${specCell("推理", effortCell, null, effortCell == null)}
|
||||
${specCell("路由", m.routing || "隐式单端点", m.pool ? "端点池已启用" : null, !m.routing)}
|
||||
${specCell("rpm / tpm", (m.endpoints && m.endpoints.find((e) => e.rpm) ? fmt(m.endpoints.find((e) => e.rpm).rpm) : "-") + " / -", null, !(m.endpoints && m.endpoints.find((e) => e.rpm)))}
|
||||
${specCell("并发 / quota", "-", null, true)}
|
||||
</div>
|
||||
<div class="model-price">
|
||||
<div class="price-cell"><span class="pk">输入 / 1M</span><span class="pv">${money(m.price.ccy, m.price.in)}<span class="ccy">${m.price.ccy}</span></span></div>
|
||||
<div class="price-cell"><span class="pk">缓存命中 / 1M</span><span class="pv ${m.price.cache == null ? "na" : ""}">${m.price.cache == null ? "-" : money(m.price.ccy, m.price.cache) + `<span class="ccy">${m.price.ccy}</span>`}</span></div>
|
||||
<div class="price-cell"><span class="pk">输出 / 1M</span><span class="pv">${money(m.price.ccy, m.price.out)}<span class="ccy">${m.price.ccy}</span></span></div>
|
||||
</div>
|
||||
<div class="model-caps">
|
||||
${capDot("工具", m.caps.tools)}${capDot("思考", m.caps.think)}${capDot("推理 effort", m.caps.effort)}${capDot("结构化", m.caps.struct)}${capDot("流式", m.caps.stream)}${capDot("视觉", m.caps.vision)}
|
||||
</div>
|
||||
<div class="ep-pool">
|
||||
<div class="ep-title">多供应商端点池 · ${endpointsOf(m).length} 个端点</div>
|
||||
${endpointsOf(m).map(epRowHtml).join("")}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function specCell(k, v, sub, na) {
|
||||
return `<div class="cell"><div class="k">${k}</div><div class="v ${na ? "na" : ""}">${v == null ? "-" : v}${sub ? `<span class="sub">${sub}</span>` : ""}</div></div>`;
|
||||
}
|
||||
function capDot(label, on) {
|
||||
return `<span class="cap ${on ? "on" : "off"}"><span class="d"></span>${label}</span>`;
|
||||
}
|
||||
function epRowHtml(e) {
|
||||
const f = (label, val) => `<span class="ep-f"><i>${label}</i>${val == null || val === "" ? '<span class="na">-</span>' : val}</span>`;
|
||||
const override = e.model ? `<span class="ep-override">→ ${esc(e.model)}</span>` : "";
|
||||
const env = e.implicit ? `<span class="ep-env">继承 ${esc(e.baseEnv)}</span>` : `<span class="ep-env">${esc(e.baseEnv)}</span>`;
|
||||
return `
|
||||
<div class="ep-row">
|
||||
<span class="ep-status ${e.status === "active" ? "" : "off"}" title="${e.status}"></span>
|
||||
<span class="ep-name">${esc(e.name)}</span>
|
||||
<span class="ep-fmt">${esc(e.fmt)}</span>
|
||||
${env}
|
||||
${override}
|
||||
<span class="ep-sep">│</span>
|
||||
${f("p", e.p)}${f("w", e.w)}${f("rpm", e.rpm == null ? null : fmt(e.rpm))}${f("tpm", e.tpm == null ? null : fmt(e.tpm))}${f("并发", e.conc == null ? null : fmt(e.conc))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ── 渲染:定价对照表 ── */
|
||||
function renderPriceTable() {
|
||||
const rows = [];
|
||||
let curKey = null;
|
||||
for (const m of MODELS) {
|
||||
if (m.provider !== curKey) {
|
||||
const p = PROVIDERS[m.provider];
|
||||
rows.push(`<tr class="grp"><td colspan="5">${esc(p.label)}<span class="gkey">${m.provider} · ${p.format}</span></td></tr>`);
|
||||
curKey = m.provider;
|
||||
}
|
||||
const isDef = defaultSlotsOf(m.name).length > 0;
|
||||
const blend = m.price.in + m.price.out;
|
||||
rows.push(`
|
||||
<tr class="${isDef ? "default-row" : ""}">
|
||||
<td class="model-col">${isDef ? '<span class="star">★</span>' : ""}${esc(m.name)}</td>
|
||||
<td class="num">${money(m.price.ccy, m.price.in)} <span class="ccy">${m.price.ccy}</span></td>
|
||||
<td class="num">${m.price.cache == null ? '<span class="na">-</span>' : money(m.price.ccy, m.price.cache) + ` <span class="ccy">${m.price.ccy}</span>`}</td>
|
||||
<td class="num">${money(m.price.ccy, m.price.out)} <span class="ccy">${m.price.ccy}</span></td>
|
||||
<td class="num cost">${money(m.price.ccy, blend)} <span class="ccy">${m.price.ccy}</span></td>
|
||||
</tr>`);
|
||||
}
|
||||
document.getElementById("price-body").innerHTML = rows.join("");
|
||||
}
|
||||
|
||||
/* ── 渲染:能力矩阵 ── */
|
||||
function renderCapMatrix() {
|
||||
const dot = (on, label) =>
|
||||
on
|
||||
? `<td><span class="cap-dot"><span class="d full"></span><span>${label}</span></span></td>`
|
||||
: `<td><span class="cap-dot"><span class="d no"></span><span>-</span></span></td>`;
|
||||
const rows = MODELS.map((m) => {
|
||||
const isDef = defaultSlotsOf(m.name).length > 0;
|
||||
const effort = m.effort
|
||||
? `<span>${esc(m.effort)}</span>`
|
||||
: m.caps.effort
|
||||
? `<span>可调</span>`
|
||||
: `<span class="na">-</span>`;
|
||||
return `
|
||||
<tr class="${isDef ? "default-row" : ""}">
|
||||
<td class="model-col">${isDef ? '<span class="star">★</span>' : ""}${esc(m.name)}</td>
|
||||
<td class="num">${fmtCtx(m.ctx)}</td>
|
||||
${dot(m.caps.tools, "支持")}
|
||||
${dot(m.caps.think, "支持")}
|
||||
<td><span class="cap-dot"><span class="d ${m.caps.effort ? "full" : "no"}"></span>${effort}</span></td>
|
||||
${dot(m.caps.struct, "支持")}
|
||||
${dot(m.caps.stream, "SSE")}
|
||||
${dot(m.caps.vision, "支持")}
|
||||
</tr>`;
|
||||
}).join("");
|
||||
document.getElementById("cap-body").innerHTML = rows;
|
||||
}
|
||||
|
||||
/* ── 渲染:语音 / 专用能力 ── */
|
||||
function renderAudio() {
|
||||
document.getElementById("audio-grid").innerHTML = AUDIO.map((a) => {
|
||||
const isDef = defaultSlotsOf(a.name).length > 0;
|
||||
const defTags = defaultSlotsOf(a.name).map((k) => `★ ${esc(slotLabel(k))}`).join(" · ");
|
||||
return `
|
||||
<div class="audio-card ${isDef ? "is-default" : ""}">
|
||||
<div class="a-head"><span class="a-name">${esc(a.name)}</span><span class="a-kind">${a.kindLabel}</span></div>
|
||||
<div class="a-tag">${esc(a.tag)}${isDef ? ` <span style="color:var(--patina)">· ${esc(defTags)}</span>` : ""}</div>
|
||||
<div class="a-spec">
|
||||
<span>ctx <span class="na">0</span></span>
|
||||
<span>超时 ${a.timeout}s</span>
|
||||
<span>${a.stream ? "流式" : "非流式"}</span>
|
||||
<span>${esc(a.fmt)}</span>
|
||||
<span class="na">对话计费 -</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
/* ── 初始化 ── */
|
||||
function init() {
|
||||
renderSlots();
|
||||
renderModelList();
|
||||
renderPriceTable();
|
||||
renderCapMatrix();
|
||||
renderAudio();
|
||||
}
|
||||
init();
|
||||
|
||||
</script>
|
||||
<script src="_theme.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user