站点观测
站点探针是唯一不花上游 token 就能判断「上游是不是挂了」的手段:它读站点自己公开的页面(Uptime Kuma 状态页、New-API 价格表),把观察到的可用率与价格用于展示,并在站点开启自动应用时按策略禁用或恢复路由成员。
它不替代主动探测:主动探测发的是真实生成请求,代表「我们自己的 key 能不能用」;站点探针读的是「站点自己怎么说」。两者口径不同,所以外部源只进探针工具与成员状态,不进渠道健康语义。
一、数据源
数据源配置挂在站点上(不动渠道):
probe_source_kind | 读什么 |
|---|---|
uptime_kuma | 公开状态页:/api/status-page/{slug} + /api/status-page/heartbeat/{slug} |
newapi | New-API 公开价格表:/api/pricing(+ /api/status) |
sub2api_transit | /.well-known/ai-transit.json |
配置列:probe_source_url / probe_source_config / probe_source_enabled / probe_auto / probe_last_run_at / probe_last_error。
自动推导源
站点上的 probe_auto(默认 1)开启时,探针源从平台列推导,不需要手填 URL:
| 平台 | 推导出的源 |
|---|---|
new-api / one-api 族 | /api/pricing |
sub2api | /.well-known/ai-transit.json |
| 其他 | 推导不出 → 静默跳过 |
自定义 URL 优先于推导。 从第三方目录批量导入时,已有手工源的站点永不被覆盖。
二、采集与存储
| 表 | 内容 |
|---|---|
site_probe_runs | 一轮采集(站点、时间、错误) |
site_probe_samples | 一轮 × 一个被监控模型的样本 |
site_probe_external_history | 第三方目录每一次拉取的读数(每站点 × 模型 × 小时一行,当小时最新的一份胜出) |
绝不写 model_health / probe_results——那两张表的含义是「真的发过一次生成请求」。混进去会让「这个渠道上次成功是什么时候」这个问题失去意义。
站点自报那条时间线就是 site_probe_samples 按窗口折叠出来的(一次采集 = 一格),所以样本的保留期同时是这条时间线能回看多久。 第三方参考的格来自 site_probe_external_history:目录公布的是滚动窗口汇总(例如「97.1%,57677/59391」),把多次拉取相加会把这个窗口乘以拉取次数,所以每对每小时只留一份、以当小时最新的一次为准;行上那个百分比永远是最新一份读数自己的数,不是历史之和。
site_probe_samples是全库写入量最大的历史表:一轮 = 站点 × 模型 一行。所以它的保留期(默认 7 天)要刻意给值。读侧按同一个量级设计:报告必须按站点取用(
GET /admin/site-probe/report?site_id=N), 而每个样本的分区探测由idx_site_probe_samples_partition覆盖索引回答。2026-10-14 在 21.4 万样本的生产库上实测:全舰队读一次 9.9s CPU(控制台每开一次面板、改一次阈值就触发一次, 直接把 2 核机器打满);按站点取用 + 覆盖索引后 0.30s。观测页面板的时间格同样在 SQL 里折叠(
SiteProbeSampleHistory,24 格),而不是把保留期内的样本 全读进内存再算:observed_at上的时间索引专为这条读服务。
三、采集节奏
由调度器驱动,节奏是热加载的运行设置:
| 设置 | 默认 | 说明 |
|---|---|---|
site_probe_interval_seconds | 900(最小 60) | 采集间隔 |
site_probe_jitter_seconds | 120 | 每轮随机加上,需 ≤ interval |
节奏必须可配,因为站点公布探针数据的频率差异极大:每 60 秒轮询一次的状态页在 15 分钟默认值下毫无意义;而每 15 分钟才更新的站点,更快的轮询不会带来任何新信息。
四、名字匹配
站点的模型名与网关的模型名不一定一致,所以匹配分三级,并且必须先预览:
| 级别 | 说明 |
|---|---|
| 精确 | 名字完全相同 |
| 规范化 | 去前缀 / 大小写 / 分隔符差异后相同 |
| 关键字 | 包含关系 |
预览会显示每一条命中靠哪一级匹配,以及它会影响哪些成员。宽松匹配的结果是可核对、可取消勾选的。
五、判定
判定是轮次化的,避免抖动:
| 参数 | 默认 | 含义 |
|---|---|---|
ratio_threshold | 0.9 | 可用率阈值 |
min_samples | 5 | 每轮至少要有的样本数 |
low_rounds | 2 | 连续 N 轮低于阈值才禁用 |
high_rounds | 2 | 连续 N 轮达标才恢复 |
min_samples 那一项很关键:样本不足时不出判定,而不是把「没数据」当成「挂了」。
六、动作
动作只走既有的两个函数(AutoDisableByPair / AutoRecoverByPair),所以两条既有保护自动生效:
| 保护 | 行为 |
|---|---|
| 单成员路由 | 永不被禁用(禁掉它等于把这个模型下线) |
| 人工关闭的成员 | 永不被自动恢复(操作员关它是有理由的) |
七、自动应用是站点级开关
probe_auto(自动应用) | 行为 |
|---|---|
| 勾选 | 每轮采集结束后自行按该站阈值禁用 / 恢复成员(无人值守) |
| 不勾选 | 只采集与展示,等你点「应用」 |
配置写入前会校验(未知键 / 越界值 → 400),读取时解析失败一律当作不自动。
八、可用率的证据链
判定(自动禁用/恢复)按优先级从高到低:
- 站点自报(探针源);
- 本站实测:用我们自己的
proxy_logs近 24h 现算; - 第三方目录(watchbot.cfd 等)。
站点没自报时用第 2 级,因为「我们自己的 key 能不能打通」比任何第三方说法都直接。
展示(清单上的读数与详情弹窗)不做这个取舍:四类读数分开列,谁也不冒充谁——
| 读数 | 数据来源 | 时间格 |
|---|---|---|
| 实际调用 | proxy_logs(按上游模型名折叠,同一模型的多个渠道合为一条) | 每一次请求 |
| 主动探测 | probe_results | 每一次探测 |
| 站点自报 | 探针源 | 每一次采集 |
| 第三方参考 | 目录快照 + 拉取历史 | 每一次拉取 |
行上那个百分比取的是第一条没过期的读数(顺序同上),因为它是操作员唯一能自己复现的那条;站点自报过期(而我们自己的调用是新的)时,清单会直接引用实际调用。判定仍然走上面那条链,两者是两件事。
一个源什么都没公布时不出格、不显示 0%:目录只给价格表的行、站点没报健康数据的模型,都是「没测量」而不是「失败」。
实测过 26 个站点,均没有免登录的逐模型健康接口(
/api/models/status401、/api/ratio_config403、/api/uptime404、/healthz只说「我活着」),而 New-API 价格源本身只公布价格。所以三级证据链不是设计偏好,是现实约束。
第三级证据按目录自己的口径读(2026-10-14 核对):目录每一行都带 acquisitionState(它自己有没有在采这个 pair)、collectedAt(它上次看这个 pair 的时间)、observedAt(最后一个有样本的桶起点)以及窗口内的请求数/成功数/失败数。可用度只看前两个:目录说 fresh 且这一对有过测量,这条读数就是证据;no_samples(当前桶空)不是拒绝理由——目录自己的面板仍以窗口聚合值作标题(实测 96.9% / 58,466 次请求),旁边只标一个「暂无样本」。网关过去用「observedAt 超过 2 小时算过期」+「no_samples 整条丢掉」两把尺子,结果把操作员在目录页面上看得见的读数在控制台里藏起来或标成「已过期」。
http 型监控在 Uptime Kuma 里标记为弱证据——它只说明 HTTP 端口活着。
九、上游报价与网关计费
站点公开价与登录账户分组价分别保存到报价快照,保留原生额度、显示币种、采集时间和适用范围。采集不写生效计费规则。
在模型报价详情打开关联成员的「计费规则」,通过「比较并复制」查看差异后一次确认。非 USD 的原生额度需要明确换算声明;未知单位、过期、无法计算或模型/站点/账户/分组不匹配的报价不能复制。
复制携带报价 ID 与目标规则版本,之后观察值刷新不会自动跟随改价。LiteLLM/models.dev 参考目录与监测目录不同:前者提供参考报价,后者只导入监测地址或健康数据。详情见定价配置。
十、管理接口
| 接口 | 作用 |
|---|---|
GET /admin/site-probe/report | 读数表(站点自报可用率、匹配方式、连续轮数、判定、受影响成员)。控制台按站点分页取用:带 site_id=N 时只读该站点的样本与状态,routes 字段始终是全部启用路由名(「能不能挂载」这个问题是全局的) |
PUT /admin/site-probe/source | 保存站点源 |
DELETE /admin/site-probe/source/{siteId} | 清除站点源 |
POST /admin/site-probe/detect | 识别站点平台 |
POST /admin/site-probe/collect | 立即采集(指定站点时只自动应用本次选择的站点) |
POST /admin/site-probe/apply | 应用判定(预览 → 应用) |
POST /admin/site-probe/adopt-price | 携带报价 ID 与目标版本复制报价(兼容批量入口) |
POST /admin/site-probe/catalog/import | 从目录批量导入站点源(必须显式带目录 url,绝不回退默认) |
POST /admin/site-probe/catalog/prune | 清理目录导入的源 |
前端入口:连接 → 站点观测。
十一、网络契约
探针采集器、detect 与 catalog 的 HTTP 客户端只读控制台配置的全局代理(并走 SSRF 策略),绝不回退到环境变量。
与 OAuth 客户端同一条契约:容器里的 HTTP_PROXY 常指向宿主机,用它出网会 connection refused。
第三方目录快照(第八节的第三级证据)由调度器自己按 15 分钟抓一次,与站点采集频率无关。地址取 SITE_PROBE_CATALOG_URL, 留空用内置默认目录(siteprobe.DefaultCatalogURL);目录不可达时保留上一份快照(fail-open),下一轮再试。
「立即采集」会强制刷新这份快照(不看 15 分钟节流):面板上显示的第三方读数就是这个站点的证据,点一下却只刷新站点自己、不动眼前的读数是说不通的。抓取失败只记一行日志并在响应里带回状态,同一轮的站点采集照常进行。
控制台的站点观测页会写一行第三方快照的状态:条目数 + 上次同步时间,失败时写失败时间与原因(GET /admin/site-probe/observations 的 third_party,POST /admin/site-probe/collect 也返回同一份)。 因此「网关能不能拉到目录」是个可看见的事实,而不是让读数默默变旧、让每条读它的规则去怪数据。 可用性详情里,「已过期」只描述我们这份快照有多久没刷新成功(超过 2 小时),不再拿目录的 observedAt 去给数据本身判过期。
每次成功拉取都会写一行 site_probe_external_history,所以第三方参考和站点自报一样有逐时时间格(默认 15 分钟拉一次 → 每小时一格)。这一列是我们自己的记录:目录公布的是滚动窗口,只有我们自己记录才能回答「它在什么时候说了什么」。
这个抓取有自己的超时预算(15s),比单站点采集的 30s 短:目录只是回退证据,一个不响应的目录不该把一轮采集 (以及控制台的「立即采集」)压在它后面。抓取失败只记一行日志,同一轮的站点采集照常进行。
十二、失败处理
采集失败只记 run / 站点错误,不产生判定(fail-open)。
所以一次网络故障不会导致「所有成员被禁用」——没有新数据就是没有新判定,而不是「可用率为 0」。
十三、保留期
| 变量 | 默认 |
|---|---|
SITE_PROBE_RETENTION_DAYS | 7 |
每日维护按它删旧轮次连带其样本(以及第三方拉取历史);POST /admin/db/gc 的孤立行清扫会回收站点已删除后的残留。