Repository navigation
Releases: ZiChuanLan/meta-gateway
Release list
v4.7.3
Fixed
- 控制台实时轨迹的「中断」按钮不再对所有请求返回 404。 请求 ID 的格式是
<容器 ID>/<请求 ID>,作为单个路径段传输时/会被百分号编码成%2F;chi 按URL.RawPath路由、并把路径参数原样交回,处理函数于是拿着编码后的字符串去查内存注册表——永远查不到,每个仍在飞行中的请求都得到404 request not in flight。现在中断接口在查表前先url.PathUnescape还原(internal/httpapi/livetrace_handler.go),并补了一条走真实路由 +%2F的回归测试:回滚修复即复现线上那条 404。
Full Changelog: v4.7.2...v4.7.3
v4.7.2
Fixed
- 插件渠道的「获取模型」不再对被策略拦下的地址报
transport。 模型发现走的是出网 SSRF 策略,而托管插件的监听地址恰好是网关自己分配的127.0.0.1:<port>——策略默认拒绝回环与内网,于是「获取模型」永远 502,而同一地址的转发却一直是通的(relay 不走这条策略)。现在网关把每个已启用插件的端点(托管进程的127.0.0.1:<port>,或注册 sidecar 的host:port)作为例外交给策略,按端点精确匹配:换个端口的回环地址、其他内网地址照旧被拒,插件禁用/卸载后例外随之消失。不需要再配OUTBOUND_ALLOW_CIDRS。
Changed
- 从插件建渠道不再手填模型名。 「创建渠道」弹窗去掉模型输入框:提交时先向插件要
GET /v1/models,把返回的模型直接作为models_csv采纳成路由与成员;插件没有登录任何账号(返回空列表)时提示先登录,而不是静默建一个空渠道。
Full Changelog: v4.7.1...v4.7.2
v4.7.1
Fixed
- 团队模式(cookie 会话)下 sidecar 插件页不再永远 401。 团队模式登录拿到的是 cookie,浏览器里没有 bearer 令牌;而插件页是一次 iframe 导航,只能把令牌放在
?t=里,于是请求带的是空值,网关的插件代理(只认?t=/Authorization)一律回{"error":"unauthorized"}—— 退出重登也不会变,因为重登同样不产生令牌。控制台现在先向新增的POST /admin/plugins/{id}/embed-token要一个10 分钟有效的签名会话令牌再挂 iframe(复用登录那套签名与校验,没有第二条鉴权路径;有效期比 bearer 模式下直接塞 12 小时令牌进 URL 更短)。令牌取不到时页面显示「插件页面加载失败」而不是把后端 JSON 原样铺在窗口里。个人(bearer)模式行为不变。
Full Changelog: v4.7.0...v4.7.1
v4.7.0
Added
- 插件可以不带运行时。 托管插件的
entrypoint现在允许写裸名(如node):包里没有同名文件时按 PATH 找宿主提供的解释器,run_args用{plugin_dir}指向包里真正的程序。包里的同名文件仍然优先,所以既有插件不受影响。一个 JavaScript 插件因此是一个 15 KB 的 zip 通用于所有平台,而不是每个平台一个 39–46 MB 的自包含可执行文件。宿主上没有该运行时会在启动时明确报plugin_entrypoint_unavailable。 - 流式应答可以逐事件改写。 拦截钩子新增第四个点位
event:一个 SSE 事件一个事件地交给插件,可改写 payload 或丢弃该事件;[DONE]、纯文本、畸形 JSON、超过 1 MiB 的帧一律原样通过(fail-open),一条应答最多送 4000 帧,默认超时 100ms。只有声明了event的插件会付这份成本——没有插件声明时一行都不解析。
Changed
- 运行镜像补上
nodejs与tar,去掉libstdc++。 前者是 JS 插件的运行时,后者是插件装 npm 包时解.tgz用的;libstdc++只是 Bun 编译的单文件需要,那种包现在不必再做了。
Fixed
- 托管插件重启后不再换监听地址。 插件的渠道指向
{plugin_url}/v1,而每次重启都重新挑一个临时端口,渠道就一直在打旧端口——表现是全 502 且界面上看不出原因。现在插件记录里的127.0.0.1地址只要仍然空闲就沿用;别人(运维自己注册的 sidecar URL)的地址绝不抢占。
Full Changelog: v4.6.5...v4.7.0
v4.6.5
Added
-
站点观测清单的可用性不再只有「站点自报」一条。 这一行原来只带站点自己公布的那份读数,而模型页对同一个模型显示四条;当站点只公布价格表(不公布健康数据)时,清单上就是一片「—」,看上去像网关没数据。现在两边用同一套证据:实际调用(
proxy_logs,按上游模型名折叠,同一模型的多条渠道合为一条)、主动探测(probe_results)、站点自报(探针源)、第三方参考(目录快照),弹窗里分开列,行上那个百分比取第一条没过期的读数(与模型页同一规则)。判定链没变(仍是站点自报 → 本站实测 → 目录)——展示取哪条与判定采信哪条是两件事。 -
站点自报与第三方参考有了逐时时间格。 以前这两类只有「汇总比例」,格是全空的:站点自报改为按采集折叠(一次采集一格,能回看多久 =
SITE_PROBE_RETENTION_DAYS),第三方参考在每次拉取时另记一行site_probe_external_history(迁移 137;每站点 × 模型 × 小时一行,当小时最新的一份胜出——目录公布的是滚动窗口,把多次拉取相加会把这个窗口乘以拉取次数)。格按读数染色(绿 / 黄 / 红),与其它来源同一套档位。
Changed
- 控制台的 JSON 响应开了 gzip。 站点观测一次要读 ~1.2 MB(2.5 MB 的审计库实测同量级),面板每 60 秒重取一次,而控制台常隔着一条比网关自身窄得多的链路(浏览器 → 反代 → 隧道)。实测同一份响应 2,515,358 B → 44,942 B。只压
application/json:实时追踪与工作台的 SSE 必须原样、逐块送达,白名单就是这条界线。
Fixed
-
只公布百分比的读数终于有格可画。 目录大多数行给的是比例而没有请求数,站点报价行连比例都没有。一格需要「成功 / 样本」两个数,于是:有计数就用计数,只有比例时一次读数算一个样本(≥ 阈值绿、以下红),两样都没有就不出格——「没采到」不是「失败」,弹窗也不会对它说「只有汇总比例」,因为它连汇总都没有。
-
WebDAV「导入方式」的两张卡片不再把标题挤断行。 「增量合并 / 全量覆盖」是四个字、说明是一整句:并排一行时标题被压成窄列、断成「增量合 / 并」,而说明占满宽度。现在每张卡片标题一行、说明一行,两个选项读成一个 2×2 块(两个主题包都已更新)。
-
观测表里那个「收费设置」入口去掉了。 价格列只留价格;成员级计费规则与报价复制仍从模型页 / 路由详情进入。
Full Changelog: v4.6.4...v4.6.5
v4.6.4
Changed
- 收费设置里的报价列表改成以模型目录(models.dev / litellm)打头。 原来的顺序是「本站价 → 他站价 → 目录参考价」,而列表里第一条同时也是上方摘要引用的那条——于是一个用来把各渠道放在同一标尺上比较的参考价,反而排在最后、还最不容易被读到。现在目录参考价在前,本面板所属站点其次,其他站点最后;后端返回顺序不变,排序只作用于展示。
Fixed
-
模型目录里「可用性」读数不再紧贴前一个标签。 这一行是一串内联的徽章,读数接在最后一枚后面,尺寸是0 间距(实测:徽章组结束于 x=290,读数也起于 x=290),而带表盘的读数会让人把第一根柱子读成那个词的组成部分。间距挂在徽章组之后、且只在这一组真有徽章时生效:没有徽章的行走包装元素自身的 8px 边距,无条件加间距会把它们推到 16px —— 把最常见的情况弄歪。
-
「未确认上游分组」不再被说成「这个价看不懂」。 按分组报价的站点会公开一张
group_ratio表;当本账号的分组不在表里,网关能读出这行的单价,但不知道计价用的那个倍数。这条报价以前报「这个价看不懂,没法直接用:未确认上游分组」,还把它已读出的阶梯与时段一起藏起来——两处都是误导:单价解析得很清楚,缺的是那个倍数。现在它直接说「未确认上游分组:单价不含分组倍数,不能采用」,阶梯与时窗照常列在说明旁边(读到了就是读到了),而采用仍被禁用——按 1 倍把这些单价写进计费规则,等于把一份没人量过的价拿去计费。只有真的解析不了的表达式继续走「看不懂」的文案并附上原文。修这条时顺手治了一个不一致:长度阶梯以前会把「未确认分组」这个状态抹掉(于是同一份报价,走阶梯的能采用、走时段的不能),等于让没确认的倍数以 1 倍悄悄写进规则。现在阶梯也保留这个警告,由
TestTieredPriceKeepsAnUnconfirmedGroup钉住。
Full Changelog: v4.6.3...v4.6.4
v4.6.3
Added
-
连接对话框现在说明每个下游协议在这条渠道上实际会以什么形态发出。「这个上游是 OpenAI 形状」回答不了操作员真正的问题:我的 Codex 客户端说 Responses、Claude 客户端说 Messages,它们在这条渠道上各自落到哪?转发层对每个家族都有确定答案,而对话框对此一言不发。端点预览现在返回
surfaces:哪些下游协议不是以上游自己的线格式发出的、以及用什么方式(native_fallback还是translated),对话框按规则各打一行——OpenAI 家族渠道显示「Responses 客户端 → 先按原生 Responses 形状发送,遇 404/405 改写为 chat completions」「Anthropic 客户端 → 改写为 chat completions」,Anthropic 渠道显示「Chat / Responses 客户端 → 改写为 Anthropic Messages」。措辞按形状而非 URL:配了端点覆盖的渠道,原生那次尝试打的是配置的端点,在那里写/v1/responses就是假话;主行继续显示真实解析出的 URL。 -
站点观测清单的最后一列改成「可用性详情」。 原来那里列的是已接入的路由名、给未接入的模型一个「接入」按钮,还把成员的收费设置挤在同一个格子里;而操作员在这张表上真正要读的是这个模型健不健康。现在这一列就是该模型的可用性读数(与模型页同一个卡片),点开是可用性详情弹窗,站点没有读数时显示
—,而它也是追问「为什么没有」的唯一入口——弹窗会说明来源,以及是「没采到」还是「站点不公布」。表格因此从「有读数时四列」变成固定三列(模型|价格|可用性详情),模型名和价格拿回那部分宽度。接入能力没有丢:批量选择里那条「接入路由」走的正是同一套两次调用(建路由 → 挂上提供该名字的渠道),模型页也保留逐成员入口。「已有路由 · 本站未挂载」的提示留在模型名下方——批量接入会跳过这些名字,理由必须留在它适用的那一行。成员的「收费设置」按钮移到价格列下方,紧挨它所覆盖的那份报价。
Changed
- 模型页成员行不再打印「账户余额未知」。 余额读不出来时(上游没报 quota,或没有换算单位)整段文案不再渲染:这一行的用途是选路,一个读不到的数字不值得占地方。读得到的余额照旧显示,
stale/ 刷新失败时仍追加「刷新失败,保留最后一次结果」。
Fixed
-
端点预览按渠道类型回答,不再固定套一种 OpenAI 形状。 连接编辑器只把 Base URL 交给
/admin/endpoint-preview,所以答案永远是那次 OpenAI 拼接:选 Anthropic、base 填https://api.deepseek.com/anthropic会被告知/v1/chat/completions,而这条渠道实际打的是/v1/messages;供应商 profile(TypeSafe)也会被展示一个它从不使用的拼接。一台主机可以同时说 OpenAI chat、OpenAI Responses 或 Anthropic Messages,各自拼法不同——一个忽略类型的预览等于承诺了转发层永远不会调的端点。预览现在接收渠道类型,并按家族用真正服务该请求的那个适配器构造两种 URL(所以它与转发不可能漂移):Anthropic
/v1/messages、Geminimodels/{model}:generateContent、OpenAI/v1/chat/completions、供应商 profile 自己的端点,以及JoinRawPath的显式覆盖。模型清单报的是 API 根下的<root>/v1/models——discovery 真正查询的位置,而不是旧覆盖分支凭空发明的…/chat/completions/models。对话框显示三行(上游协议 / 请求将发往 / 模型清单),类型切换与站点自动识别都会重新解析,并用序列号守卫挡住「慢的旧答案覆盖新答案」。 -
第三方可用性不再拿目录的
observedAt当新鲜度判据。 目录的observedAt是最后一个有样本的桶的开始时间,所以在当前桶为空时它会落在几小时之前;而目录自己的面板恰恰在这些行旁边挂着窗口聚合值与「暂无样本」徽章。网关据此把读数藏起来并打上「已过期」——实测(watchbot.cfd 已发布目录,1,984 行,2026-10-14)薄荷 API /deepseek-v4.1-flash在网关侧显示「已过期」,同一个数字(58,466 次请求上 96.87%)就摆在操作员面前的目录页面里。判据改为读目录自己对采集状态的声明(
acquisitionState)加上聚合所依托的请求量,no_samples不再等于拒绝;快照携带checkedAt与请求/成功/失败计数(迁移 136),由背后请求量最大的分组代表一个 (站点, 模型)。「已过期」此后只表示一件事:本网关刷新这份快照的间隔超过了允许的一轮探测时长。判定仍只作用于我们能续期的快照(actionableExternal)——读数照常显示,但永不因为一份我们无法刷新的副本去禁用成员。「立即采集」改为强制刷新快照、不再等那 15 分钟闸门:面板显示的就是操作员眼前站点的第三方读数,一个「刷新一切除了这些」的按钮是能被现场看到的 bug。采集响应与观测载荷都返回快照状态(读数、上次同步,或失败与原因),面板据此把「网关连不上目录」变成一个可见事实,而不是让读数悄悄变旧、然后所有读它的规则去怪数据。
Maintenance
- 验收栈可以展示可用性读数了。
docker-compose.audit.yml里的站点来自公开价格表,而价格表不发布任何通过/失败判定:观测目录的每一行都渲染成「没有读数」,于是「可用性详情」这一列恰好无法在专为查看控制台而搭的栈里走完整条路。新增tools/audit-mock/seed_beats.py为 mock 站点写入一份心跳史(就是公开状态页会提供的那种行),另把三次失败放在 24 小时之外的 30/55/70 小时前——默认近 24 小时视图读 100%,7 天视图能在时间格里看到那次下陷。可重复执行:它替换自己写入的失败,而不是叠上去。
Full Changelog: v4.6.2...v4.6.3
v4.6.2
Fixed
- 打开「连接 → 站点观测」不再把网关 CPU 打满。 那个面板每次打开都向
/admin/site-probe/report要一份全舰队读数,
而这份读数要为每一条样本回答「这个 (站点, 模型, 分组, 监控器, 来源) 在不在本站最新一轮里」——EXPLAIN QUERY PLAN显示
它在回原表逐行查。生产库实测(21.4 万条样本、2 核机器):一次请求 9.9 秒 CPU,只出 1,359 行;叠加第二处放大——
前端的查询键带的是草稿策略,阈值输入框每敲一个字符就是又一次全舰队查询,而它算出的判定字段这张表根本不渲染。于是
点进去就卡、CPU 打满、转发跟着崩。现在四处一起改:迁移 135 加覆盖索引idx_site_probe_samples_partition
(同一份数据上单站点读 5.15s → 0.30s,全舰队 9.9s → 2.0s;21.4 万行上建索引 0.59s);
GET /admin/site-probe/report?site_id=N按站点取数;自动应用那轮只读它要动的站点;前端按站点做查询键,改阈值不再发请求。
响应新增routes(全部启用路由名)——「这个名字还没被任何路由占用吗」是全局问题,按站点取行会让界面提议创建已存在的路由(409)。 - 类型下限测试在本机 Windows 上不再必然失败。
web/src/styles/typeScale.test.ts用node:fs/node:path读样式表,
却跑在 jsdom 工程里:Vite 对浏览器侧会把 node 内置模块外部化成空模块,join就成了undefined,整个文件在 collect
阶段就炸(同一份代码在 CI 的 Ubuntu runner 上是绿的,所以这条只在本地出现——看起来像自己刚弄坏的,实际白红)。
按仓库里已有的写法加上// @vitest-environment node即可(releasePolicy.test.ts一直这么用),测试逻辑一字未改。
Full Changelog: v4.6.1...v4.6.2
v4.6.1
Added
- 站点用「阶梯 / 时段倍率」写的价,现在能读、能看、能收。 新一代 New-API fork 把价格写成表达式,网关以前只认单档加法式
(tier("base", p * 1.5 + c * 4.5)),其余一律「这个价看不懂」。现在多认两族真实写法:
①tier(…)乘上若干(hour("TZ") >= A && hour("TZ") < B ? 倍数 : 1)→ 翻成时段倍率(price_windows),
边界逐字对齐(>= A && < B与网关窗口同为左闭右开),并连时区一起存;
②len <= N ? tier("短") : tier("长")→ 翻成输入长度阶梯(price_tiers,上限含端点,与<= N一致)。
读到的结构会跟着报价一路走到底:报价卡片显示基础价 + 时段列表 / 分档表;「设为收费价」把它们一起写进计费规则
(price_schedule/price_tiers+ 表达式自己的时区),账单总额按站点真实写法收;已定价的规则读数会多一句
「当前时段 ×N」,由服务端时钟判定。 - 读不懂的仍然不猜:
? :假分支不是1(含免费时段)、两个时段重叠(站点相乘、网关取首个命中,会少收)、
单档区间为空(如>= 10 && < 3)、未知变量/未知时区、多档相加、阶梯里带cw—— 这些继续显示原文,
不会变成一个编出来的数字。无单一数字的阶梯报价标为tiered,不再算「不可用」。 - 日志开始记录「同渠道重发次数」(
proxy_logs.key_retries,迁移 134)。之前控制台把这个开关摆在那里,却没有任何东西能回答“它到底救回过几个请求”:attempt记的是跨渠道轮次,同 key 的重发在日志里完全隐形。现在每次尝试都带上自己的序号(0 = 该 key 的第一次发送,1 = 第一次重发……),于是 200 +key_retries>0就是“靠重发活下来的请求”,而整列都是 0 就说明这个预算只在付延迟。日志页的链路区会写成「第 2 次尝试 · 同渠道重发 1 次」。一次渠道尝试一行:中途的同 key 失败不会各占一行,行上的数字是该次尝试一共用掉的重发预算。
Changed
- 报价卡片右侧那一格改成「钱的符号」:¥ / $ / €。 它原先只是把计价单位胶囊(
站点额度)重复一遍,两个站点的报价看上去完全一样。现在取站点自己声明的货币符号,认不出符号就退化成货币代码(USD),再退化才是协议自己的站点额度;ISO 报价的单位也带上符号(USD →$)。为此账号维度的价格采集补上了站点货币:/api/status的quota_display_type/custom_currency_symbol/usd_exchange_rate本来只有「站点公开价」那条路径读,账号路径只取了quota_per_unit——实测 500 条账号报价里没有一条知道自己是 ¥ 还是 $。读 /api/status 的实现现在收敛到一处(adapters.CreditBasis+adapters.CurrencySymbolFor),公开价那条路径改用同一份符号表(internal/siteprobe/newapi.go不再自己维护一份)。货币仍然只是展示,不改协议里的数字单位。 - 模型层的「沿用旧价格」改名「不在这里定价」。 旧单价列已经空的新模型上,那个名字在骗人:选了它既没有「旧价」可沿用、也不叫未定价。它真正的契约是「取消模型层的定价 → 回落到升级前的旧单价列,没有就是未定价」,现在标签与说明都这么写;渠道层保留「用模型默认价」(那个名字是准的,它真的是跟随模型价)。行为一个字没改——它仍然是唯一的「撤销我在这里的定价」入口。
Fixed
-
「立即采集」不会再和定时轮重叠,把每个站读两遍。 采集轮是串行的(一个站读完再读下一个,代码注释写明了原因:从同一个地址突发请求正是站点会封的),但
CollectNow没拿调度器那把单飞锁——于是手动点采集、或导入目录后自动补一轮,恰好撞上定时轮,就会有两个轮同时扫全网。实测:并发发两个「采集全部」时,第二个现在 105ms 内拿到 409 + 一句人话(“already running, try again when it finishes”),控制台显示成「已有一次采集正在进行,等它结束后再点」,而不是静默再扫一遍。 -
报价列表现在指名道姓:每条价是哪个站点的、哪个账号的。 同一张卡原先只写着「站点标价」+「站点额度」,于是同一个模型下
六个不同数字(各自来自不同站点的账号)读起来像同一个东西被量了六遍——操作员当场问「明明是一个模型,为什么有这么多不一样的
站点额度」。现在卡片标题就是发布者(站点名 + 账户 #id),右侧一枚首字母徽章(拉丁名取两个字母,HongShi与Huan不会都
是 H;中文名取首字),成员面板里本站报价叠一枚「本站」标签并且排在最前面(原先按抄录时间混排,本站的价可能排在第 6 条),
折叠行的摘要也改成「站点名 · 价格 · 抄录时间」,不再是「站点标价 · 价格」。站点名读库时才 join(快照只存 site_id,
站点改名不该改写历史证据)。 -
没有目录参考价时说出原因。 打开「这些价从哪来」若一条参考报价都没有,现在会指明是哪种情况:没配模型目录、目录同步没开
「价格」、目录还没同步过,还是目录里根本没有这个模型名。以前只看到一个空列表,操作员只能猜(实测验收栈里就是
「prices_enabled=false」,界面此前一字未提)。 -
观测页的「接入」此前只建了路由、没挂渠道,得到一个没有渠道的路由。 建路由接口的
auto_match_channel_ids是显式的
(空列表 = 不挂任何渠道),「全部当前匹配」的语义只属于/auto-match接口;此前一次调用被当成后者,于是操作员点了「接入」、
界面报成功、路由却没有任何渠道服务它(实测:路由 9→11 条,成员 0 条)。现在按两步走——建路由,再调 auto-match(空列表 =
全部当前匹配),两步都成功才算接入成功,并且逐个模型串行执行(一次请求 100 个模型只会撞管理面限流)。新增组件测试钉住这两次
调用的顺序与参数。
Full Changelog: v4.6.0...v4.6.1
v4.6.0
Added
-
「连接」多了一页「站点观测」:每个站点公布了哪些模型,以及它真实的历史。 证据按来源标注——真实流量、主动探测、
站点自己的状态页、第三方目录——不会被平均成一个数字。为此转发链路补了三处口径:proxy_logs与探测结果带上
member_id与真实的上游模型名(成员曲线不会借到同一条路由上别的别名的数字);路由的最终结果与尝试分开记录
(恢复的故障转移算一次成功,200 之后断流的流不算);站点心跳按监控器分别保留,来源变了不留旧读数。自动调整按成员生效
(探针可以复测自己禁用的成员,手工禁用的保持关闭),读数收窄到 ~114px,空分组按默认分组归属。 -
报价与收费分家:报价是观测,收费是本地策略。 迁移 132 新增
price_quotes(带expires_at的观测行)与
billing_rules(scope=model|member+revision),usage_records新增billing_status/billing_evidence——
每笔请求把当次生效的规则冻结下来(owner、revision、来源、quote_id、费率、用量来源)。控制台可以分开管理这两件事,
规则写入必须带expected_revision(不匹配返回 409),模型倍率(0~1000)单独在计费面板里编辑;目录同步从此也只写
参考报价(purpose=reference,7 天过期),不再回填计费列。已有的扁平单价列与历史账单金额不变,仍按旧列计费的层标为
legacy。 -
代理日志能解释每笔成本是怎么来的。 日志列表原先只按
usage_recordsjoin 出金额,从不说它怎么算出来。现在管理员行
带上是 relay 记录的计费决策(状态 + 冻结的规则快照),在行展开区里渲染 owner、revision、来源、用量来源与费率(库里存
USD/1k,界面按 1M 显示)。按 400 个request_id一批查询;只填管理员行,成员/me/requests是另一条查询、不带规则
owner;无法解析的 evidence 一律变 null,不交给调用方。 -
成员计费价有了来源徽章。 迁移 131 给
route_members加price_source/price_updated_at:采纳站点报价写
site,手工改价写manual。两列同时进入转发热路径与概览的候选投影(投影守护测试当场抓到第一次读错位——正是这张表摔过
两次的静默错位),所以 relay 池与控制台显示同一个来源。成员行只挂一个安静的徽章「站点价 / 手动价」,站点观测那边的动作
也改名「设为成员计费价」,因为它写的就是这一层。 -
账户财务快照改成「先给你上次的读数,再在后台刷新」。
GET /admin/channels/account/finance不再等上游,慢站点再也
拖不住控制台;新增POST触发单飞的后台刷新。迁移 133 的finance_snapshots把读数落库(每渠道只保留最近 20 条,余额
历史仍在balance_history),重启后第一屏就有数据;读数超过 2 分钟的缓存 TTL 会标成 stale,不再冒充最新。 -
模型页的参考价改成整页一次批量取。
GET /admin/model-pricing/batch(/me同款)一次解析最多 100 个具体模型并去重,
把调用方能看的东西一次答完:没有可用授权候选的模型回到unavailable,而不是让整批失败;成员侧按策略的AllowsModel/
AllowsGrant过滤,不泄露任何成员、渠道或报价身份。modelPriceView增priced_candidates与
priced|partially_priced|unpriced状态(没定价的模型不会被显示成免费),以及cache_known/cache_write_known
——用来区分「声明了缓存价」与「回落到输入价」。 -
新建连接时勾选的模型当场采纳。 在添加对话框里勾模型就是控制台承诺路由的方式(旁边就是「已选」),但
createConnection只把名字存成候选清单:连接读「已选 0」、模型进不了目录,操作员还得在渠道抽屉里再勾一遍。现在手动同步
渠道在创建时就为每个名字建精确路由(已有同名则复用)并挂一个启用成员,用的是控制台自己采纳路径的同一套 store 调用;单个
模型失败在响应里报出来而不是让创建失败;auto 同步渠道不受影响(它服务什么由 discovery 探测决定)。 -
手机端的模型目录改用详情弹窗。 在 760px 的移动层,管理页仍是一套放不下的两栏,而目录被限到 330px(约 123px 的滚动
窗口,勉强一行)。断点以下目录全宽、详情走共享Dialog,并复用成员卡片的打开规则(可选中行 /data-model-details)
以免两者漂移;跨断点只关弹窗、不重挂目录,行自身的点击照旧驱动父级选择。顺带把工作区里过小的字体(行名、提供商行、详情
副标题、成员区标题、策略摘要)提到可读尺寸。 -
手机端的运行设置索引用原生下拉。 760px 以下那条三组标签的索引栏让位给原生
select(用<optgroup>保留分组,并在
「已改动」的条目上标出来);两者驱动同一个选择,CSS 同一时刻只渲染一个,屏幕阅读器不会找到同一个目的地两次。 -
管理员的独立界面用上共享外壳,登录动效不再挟持认证。 独立管理员界面原先只有一个裸页头,现在走共享的
ConsoleShell
(导航、主题切换、搜索面板、退出登录),Dashboard/GuidedTour/SetupWizard/AccountPage改为懒加载。认证成功
立即采用会话——视觉序列由重播(replay)拥有,删掉了GatewayTransition与那 5 个时长常量。日常动效一并缩短:主题切换从
520ms 的clip-path擦除改成 180ms 淡入,仪表盘光晕的飞机与节点停止常驻动画,路由进入的斜切渐变移除;字号整体上调
(--text-xs0.6875rem → 0.75rem,--text-lg0.875rem → 1rem)。
Changed
-
计费面板先答「现在按什么价收费」,术语改回操作员的话。 面板原先开在三个来源的三块并列价格上,「这个要花我多少钱」
的答案散在中间;现在先讲在用的价(「当前按 $X / 1M 输入收费」),规则自己的词汇挪到下一行、收在单个「改价」入口后,来源
变成一行证据(「站点公布的价 · 2.5 site credit / 1M · 14 分钟前」,还有几条)——点开仍是每一行加一个带版本校验的
「用作我的价格」,被复制的那一行会标出来。上游 / 参考 / 报价 / 快照这些流水线词汇换成「站点公布的价格」「目录参考报价」
「你收的价格」;未解析的计费表达式不再当价格打印(单元格直说、原文进 tooltip)。来源列表也从平铺一行行变成卡片:名字与
单位标签、出处作脚注、费率用带标签的网格按显示字号排、动作单独一行,面板的倍率控件骑在价格行上。 -
计费规则的说法改读它回答的问题,并删掉按报价的场景估算。 规则状态从「继承下一层」(方向反了)改成「不在这层设价
(用上一层)」,三个选项各自读成它回答的问题:用模型默认 / 免费 / 在这里设价;读数也明说正在编辑的是哪一层(模型默认给
所有能服务该模型的渠道定价,成员覆盖只给那一个必须不同的渠道)。逐报价的场景估算被删掉了——一个「工作负载 × 价格」
的数字读起来像网关能守住的承诺(共享账号、外部消耗、限流都会打破它),而它从来没进过账单;一个模型在多个渠道上观测到只出
一个计费菜单,不再是每个渠道一个按钮。 -
模型页工具栏与渠道模型行精简,交换页收回一页。 模型页只留路由动作加一个工具菜单(模型探测移进去),「计费规则」排在
「试调」旁边;渠道模型行去掉多余的「已启用」徽章与「移除」按钮(取消勾选本来就是解绑),别名编辑器保留。交换页去掉三个
标签页,回到一页(导入 / 导出 / 云备份堆叠,格式图例折叠)——标签页藏起了每个任务到底要你做什么。 -
控制台不再让内容从吸顶条下面滑过去,页面也不再随列表变长。 经典外观包用
bottom: 0+ 4px padding 覆盖了共享的吸顶
规则,正好漏掉卡片自己 12px 的底边距,于是滚过去的东西在那一条里还看得见(健康行从「检测连接」底下露出来);经典包现在
不动那条共享规则(它的负偏移存在就是为了抵消卡片自身的底边距),并给滑到条下面的内容加阴影。观测看板的筛选行与表头也补上
自己的填充色与发丝线。一个 195 个模型的站点曾把看板撑得跟着列表一起长、还拖着站点栏一起走:两个面板现在都按控制台的定高约定
(封顶的盒子 + 内部滚动,与模型目录同形),页面停在视口里、各自滚动、表头吸顶,手机上仍是一列正常页面。 -
模型卡少了两行 chrome,同样的空间从放得下 1 个成员变成 4 个。 同一张卡从 524px 降到 403px:可用率 chip 与成员分组
范围并进头部(挨着状态徽章,删掉 50px 的「分组 default —」行);分组标签与成员按钮合成一条工具栏(左边「default 4 | +
新建分组」,右边 添加 / 一键挂载 / 批量);小节标题、策略披露与九层深的边距收紧,一键挂载的标签不再复述自己的 tooltip。
没有删掉任何控件,原先卡上的每一个都还在。 -
观测页空白的路由列现在给了动作。 对「还没有任何路由」的模型(只公布价格的来源里的大多数),路由列原先全是空的;现在
点一下就建路由并挂上真正提供这个名字的渠道(模型页的挂载规则)。已经路由到别处的不再重复提供——创建端点对它们回 409,这
正是第一版的写法——只标成「已在别处路由、未挂在这里」。配合「批量选择」,同样两件事可以对一批做:给没路由的挂路由、给已有
的成员开关启停。站点的路由策略移到面板顶部(那才是这一页存在的动作,而不是 126 行列表下的脚注);成员工具栏的标签条与按钮
现在共一条基线,只有整行都是破折号时才不显示的空列也去掉了。
Fixed
-
保活弹窗的「启用保活」开关被拉成了一个方框。
styles.css里的.toolbar input(0,1,1)给工具栏内所有
input 定死height: 38px与padding: 0 14px,而.switch只有 0,1,0——于是 42×22 的开关输给了它,被拉成
42×38,16px 的滑块停在左上角:看上去就是一个空框里停着一个小方块(不是开关,也不是复选框)。保活弹窗的开关
恰好是唯一长在.toolbar里的.switch(另外两处在 Panel / Field 里,不受这条规则影响),所以只有它坏。
现在.toolbar input加了:not(.switch)——同一文件里两条 checkbox 尺寸规则早就这么防了——粗指针的 44px 覆盖
与 classic 主题里的对应规则一并加上。实测(两个主题都测):修复前 42×38、padding0 14px;修复后 42×22、
padding0,滑块 16×16 落在 (2,2),开关与「启用保活」垂直居中对齐,整条工具栏一行。 -
站点探针的「站点模型数据」只列同名冲突,站点公布了 32 个模型时只能看到其中一个子集。 详情面板那块列表读的是
name_only(与我们某条路由同名、但本地还没挂渠道的候选),于是只有同名冲突的那几个模型出现——32 个里只列 6 个,
而且列出来的每一行长得都一样。现在它是这个站点采集到的全部模型(rows∪name_only∪unmatched,按站点公布的
名字去重),一张表、一行一个模型:已接渠道的走原有读数行(可用率、连续、价格、成员、「设为成员计费价」),其余模型在同一张表
里补齐可用率与价格,并在渠道列写明是「本站未挂渠道」(同名候选,想用就把这个站点挂上去)还是「没有同名路由」。
表头给出「本次采集读到 N 个模型 · 已接渠道 M 个」,站点栏与卡片上的数字同口径。这张表可折叠(计数行就是折叠头,
默认展开,不想看的时候收起来);弹窗底部的全站「未匹配模型」汇总本来就是折叠的(在「高级设置」里,默认收起)。
三种情况只在折叠头数一次(「已接渠道 N · 同名未挂渠道 N · 没有同名路由 N」),行内只给能行动的那一行挂标签——
「没有同名路由」原先 32 行里出现 31 次,等于把同一句「无需处理」重复了 31 遍。
(顺带修掉同一块的两个渲染问题:说明文字原先放在会横向滚动的表格容器里,窄面板下被按滚动宽度排版而视觉截断;
只公布价格、没有可用率样本的行不再在「暂无样本」旁边挂一个「站点自报」的来源标签。) -
复核过的报价不再是「冲突」。 复制原先要求它是
price_quotes里最新的一行,而账户刷新每两分钟写一次——于是刚复核完的
报价转眼就成了冲突。现在只要求它是可计算的、且没过expires_at;不可变的载荷与记进规则的quote_id保住证据,之后的观测
不会反过来改已生效的规则。「未知单位」的报价也不再可以被复制(拿不到usd_per_unit就不让复制)。 -
价格归一化不再把「没写」当成「写了 0」。 缺失的
completion_ratio意为「与输入同价」,显式的 0 意为「输出免费」——
旧的<= 0 → 1规则把两者合并,于是免费模型的输出被按输入价计费。quota_type现在是权威的(声明1的按次行不再被旁边
残留的model_ratio拉进 token 计费,声明0的行也不会因残留model_price翻成按次);只带billing_expr的行(价格
只由表达式给出、扁平列都是占位)不再被解析器丢掉;缓存读与缓存写是两个独立维度(缺一个不等于那个免费),负 / NaN / Inf
的比率被拒绝而不是被编成一个正价。 -
上游的缓存语义与按次固定费都按真实用量结算。 OpenAI / Responses / Gemini 这类协议的 prompt / input 计数已经包含
缓存命中(新增PromptIncludesCacheRead:原始计数照旧进日志与 token 配额,只有计费减掉它),修复前缓存读会被重复计一次输入;
成功但没有 usage 的响应不再漏掉按次固定费(失败且无用量仍然不收费);用量在协议 / 字段转换之前计量,字段映射把usage
删掉也不影响账单。 -
未定价的请求也冻结计费决策,报价的作用域也变精确了。 以前只有命中某一层的请求才冻结(「没定价」直接返回 nil),于是没
定价的在途请求会在完成时重新解析一次——中途有人加了一条价,这笔请求就按新价计费;现在只要不是legacy层就冻结(空的未定价
层也冻结),计费不再重解析。成员作用域的报价原先只比对site_id,现在必须属于真正服务这个成员的凭据。另外,已有版本化规则
时,旧的写价路径(/admin/model-metadata的扁平价列、成员 patch 的price_*)返回 409 并指向/admin/billing-rule,
防止两套定价同时生效。 -
账户观测在刷新失败时保留旧读数,也划清了界线。 以前任何一次失败都会复用上一份读数——哪怕它来自另一个账号、另一个站点或
另一个上游分组;现在复用的旧值必须与当前账号、站点、分组都一致。手动报价的校验也补上了:按它的mode给一个 1/1 的
workload 才算得出估算,否则直接回答「不支持的模式或缺费率」,不可用的估算不再带回 cost / calls 两个数。 -
网关自己的错误不再甩给上游渠道。
ApiError带上了来源,控制台自己的/admin与/me调用把 429/5xx 标成
gateway_admin,界面不再怪一个根本没收到请求的上游渠道;这类 429 给控制台专属的原因与Retry-After建议,上游的 429 仍
原样透出。日志未过滤的空态给成员一个令牌与工作台入口(不再是只有员工能开的连接页);删掉「令牌只显示一次、无法找回」的错误
说明——网关加密保存,可重新查看或旋转;模型路由提示改回手动同步这个默认值。 -
抓取模型不再是「全都要」。 抓取列出上游提供什么,不是一个「全都要」的决定;现在什么都不预选,「按需勾选」从空的选择开始。
-
工作台的请求面板与结果面板差 18px。 皮肤里那条垂直的
.panel + .panel规则顺带命中了并排工作台网格的第二列,把结果面板
压到请求面板下面 18px(.cockpit-dual-grid与.exchange-grid早就在覆盖同一个坑)。 -
未保存草稿不再被随手丢掉,也不再拦错页面。 OAuth provider 的编辑原先在经侧边栏、命令面板或程序化跳转离开时丢失;现在路由
的导航器被包一层共享的UnsavedChangesBoundary,脏表面按 pathname 登记,离开时确认一次,被拒绝的移动不转发(URL 与挂载着的
草稿都在),页面自己处理的同 pathname 移动不会被追问两次。浏览器的前进/后退不在导航器覆盖范围内(POP 从 popstate 直接进
history listener),改用useBlocker+createBrowserRouter兜住。leavesDirtyZone原先拿目标 pathname 去比每一个登记的
脏区、却不知道操作员在哪一页,于是任何挂在后台的草稿都会拦住所有其它页面——「我什么都没改,为什么拦我」;现在只在离开当前
pathname 且该 pathname 上确实有编辑时才拦。 -
WebDAV 表单有了它需要的脏状态契约。 基线是服务端最后确认的载荷(首次 hydrate、一次干净的重新拉取、或一次成功保存),比较
用的正是保存会发送的东西,所以没动过的密钥字段不会被读成编辑;重新拉取不再覆盖草稿,保存失败保留草稿,成功保存只在 PUT 在途
期间没有输入时采用服务端归一化的值;Test 与同步仍然先落盘,现在共用一个 busy 旗标,进行中的写入不会被重复提交。 -
可用率读数跟着数据改变形状,也不再自相矛盾。 没有历史的对象原先每行都画 24 个空刻度(在路由成员上看像一条条码):现在密集
窗口保留 24 个刻度,稀疏或只有聚合的数据给一条按比例的长条,没有记录只给一个破折号。比例条、刻度与数字共用同一条色带规则
(≥95% 绿、80–95% 琥珀、其余红),修复前「没有失败就是绿」那条会让 40% 的聚合也涂成绿的、跟旁边的数字打架;行内的说明文字
(来源、计数、是否陈旧、口径歧义)移进悬停标题,并在弹窗里按来源保留;紧凑详情卡改为可滚动(max-height),动作行钉在卡片
底部且不透明,卡片边缘不再把渠道健康条切在半路。 -
采集到底做了什么,现在说清楚了。 「立即采集」与目录导入原先点了不作声:现在给一个带数量的 toast,失败时给上游自己的失败
消息与一个重试,导入前还有一个解释它会做什么的弹窗。站点没有采集到模型时,看板直接说出是哪一种状态——采集已关闭、上一轮失败、
从未采集过、目录为空,还是「站点确实存了 N 个模型,只是不在所选时间段内」(以前一律显示 0);筛选被读成一次失败的采集,现在
也直接说明。
Full Changelog: v4.5.1...v4.6.0