可观测性

每次调用打给了谁、试了几次、慢在哪、离限额还有多远、这个实验花了多少——从响应头、响应体到控制台的日志、用量与导出。

每一次调用,SciRouter 都会把「实际发生了什么」交还给你:响应头与响应体里给当下这一次的事实, 控制台的请求日志里给完整的时间线与尝试明细。全部是 OpenAI 形状之外的附加项,官方 SDK 会忽略它不认识的字段,你什么都不用改就能拿到。

响应头

非流式响应带这一组:

含义
X-Request-Id这次调用的编号。报障时带上它,它是我们定位到那一次具体调用的唯一线索
X-SciRouter-Model实际服务的模型 id
X-SciRouter-Provider实际服务这次调用的供应商 id。同一模型可能挂在多家供应商的线路上,失败时会自动转移
X-SciRouter-Attempts上游尝试总数(含跳过的)。2 及以上说明发生过自动转移;费用只按最终成功的那次结算
X-SciRouter-Upstream-Request-Id供应商返回的请求 id(有才给)。拿供应商工单时用它对账
X-SciRouter-Cost-Quota本次消耗的配额。流式响应给不出准确值,所以只有非流式有
X-SciRouter-CacheHIT / MISS / BYPASS,见「缓存与幂等」

限额余量与 OpenAI 同名,你的 SDK 里已有的退避逻辑直接认:

含义
x-ratelimit-limit-requests / x-ratelimit-remaining-requests / x-ratelimit-reset-requests每分钟请求数的上限 / 余量 / 到当前窗口结束还有多久(如 42s
x-ratelimit-limit-tokens / x-ratelimit-remaining-tokens / x-ratelimit-reset-tokens每分钟 token 数,同上

余量按你的 Key 与账号两级里更小的那一级给。没有限额时这组头不出现(不会给你一个 0 让你误以为该退避了)。 流式响应的限额头在第一个分片之前就写好了;被限频的 429 响应仍带 Retry-Aftererror.details.retry_after_seconds

浏览器里跨源调用时这些头都在 Access-Control-Expose-Headers 里,fetch 读得到。

响应体里的 scirouter 字段

非流式响应的顶层多一个 scirouter 对象:

json
{
  "id": "chatcmpl-…",
  "choices": [ … ],
  "usage": { … },
  "scirouter": {
    "request_id": "req_…",
    "model": "deepseek/deepseek-r1",
    "provider": "p_…",
    "cost_quota": 18240,
    "cache": "miss",
    "attempts": 2,
    "upstream_request_id": "…",
    "timings": { "queue_wait_ms": 830, "ttfb_ms": 412, "total_ms": 5688 }
  }
}
  • timings.queue_wait_ms:在供应商渠道的容量队列里等了多久(没排队为 0)——这一段慢不是模型慢,是厂商给的容量满了;
  • timings.ttfb_ms:成功那次尝试从发出到收到上游首字节;没走到上游时为 null;
  • timings.total_ms:端到端总耗时。

流式响应data: [DONE] 之前多一个 choices 为空的分片,带同一个 scirouter 字段(形状和 OpenAI 自己的最后一个 usage 分片一样,SDK 都认):

text
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"…","choices":[{"delta":{"content":"…"},…}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":…,"model":"…","choices":[],"scirouter":{"request_id":"req_…","attempts":1,…}}
data: [DONE]

上游一个分片都没吐就结束的流不会有这个分片。

控制台里的请求日志

/console/logs 能按 request_id 直接查一条,也能按结束原因、来源、慢请求、缓存命中、错误码、自定义时间区间(最长 90 天)筛。 每条记录点开是调用详情

  • 时间线:准入 → 排队 → 路由 → 每次尝试 → 上游首字节 → 推流 → 结算,各段多久一目了然; 两个首字节(你看到的 / 上游给出的)各一根标记线,差值就是花在 SciRouter 侧的时间;
  • 路由:实际供应商、每次尝试的结果与耗时;
  • 诊断提示:只解释已有的事实——被 max_tokens 截断、主要耗时在排队、发生过转移、用量为估算、客户端中途断开;
  • 复制报障信息:一键把 request_id、时间、模型、错误码、供应商请求 id 拼成一段,贴给我们即可。

归因:给调用贴标签

一个 Key 下跑十个实验,账单只能按 Key 分。在请求体里带 OpenAI 原生的 metadata 字段(字符串键值对), 每次调用就带上了你自己的归因标签:

python
client.chat.completions.create(
    model="deepseek/deepseek-r1",
    messages=[{"role": "user", "content": "…"}],
    metadata={"exp": "abc", "run": "12"},
    user="researcher-42",
)
ts
await client.chat.completions.create({
  model: 'deepseek/deepseek-r1',
  messages: [{ role: 'user', content: '…' }],
  metadata: { exp: 'abc', run: '12' },
  user: 'researcher-42',
})
  • 上限:最多 8 个键,键只能是 1–32 位的小写字母、数字、_.-,值最长 64 个字符、不能含换行;超限 400。
  • metadatauser 都不会转发给模型供应商——它们是给你自己看的,落在控制台日志里。
  • 日志页可以按标签筛(?tag=exp:abc,点一下记录上的标签即可),用量页可以按标签分组看花费、错误率、截断率(窗口 ≤ 30 天)。
  • 站内对话(Chat)的每次调用自动带 chat_session 标签,值是会话 id:日志页能从一条消息跳到那次调用,也能筛出一场对话的全部调用。

一段时间:用量页的维度

/console/usage 可以按模型 / Key / 项目 / 来源 / 状态 / 标签分组,看请求数、Tokens、配额、错误率、截断率, 按模型时还有 P50 / P95 延迟;开「与上期对比」会把上一个等长窗口画在同一张图上。窗口有近 24 小时 / 7 天 / 30 天 / 90 天,或自定义区间(最长 90 天)。

两条口径:请求数不含响应缓存命中(命中单独计数,它们没有调用模型、不计费);延迟分位只在按模型分组时给—— 分位数不能跨模型合并,合并出来的数字没有定义,所以其它分组那一列写的是「按模型查看」,不是 0。

此刻:离限额还有多远

控制台概览页的「实时」卡每 10 秒刷新一次:每分钟请求 / 每分钟 Token 当前窗口用了多少(与 x-ratelimit-* 头同一组读数)、 并发在途几个、正在跑的是哪几个请求、近 1 小时错了些什么。接口是 GET /console/limits/live,可选 ?keyId= 多看一级某个 Key 的读数。

导出

日志页的「导出当前筛选」按当前筛选把记录导成 CSV(UTF-8 BOM,Excel 直接开)或 JSON Lines,单次上限 50 000 行, 超过会先告诉你命中了多少条、让你缩小范围,不会导出一半。文件里没有提示词与返回片段,也没有请求参数; 时间列按你账号设置的显示时区,列名里写着时区(如 created_at (Asia/Shanghai))。

出事主动说:调用告警

账号页「通知」里有五个调用告警,默认开着、选了渠道就会发(每 5 分钟扫一次,冷却见括号):

通知项触发冷却
错误率飙升某把 Key 近 15 分钟错误(不含限频)超过 20% 且样本 ≥ 20每把 Key 1 小时
延迟劣化某模型近 15 分钟 P95 比近 7 天基线高出一倍以上且样本 ≥ 20每个模型 1 小时
频繁被限频近 15 分钟 429 ≥ 30 次1 小时
配额即将耗尽按近 24 小时的消耗速度,剩余配额撑不过 3 天24 小时
回答频繁被截断某模型近 1 小时被 max_tokens 截断的回答超过 30% 且样本 ≥ 20每个模型 24 小时

正文只有数字与名字,没有链接(提醒里的链接是钓鱼的样子);每条都写明该去控制台哪一页看。

接进你自己的系统:通用 webhook

通知渠道除了企业微信 / 飞书 / Bark,还可以是你自己的 https 地址(公网;本机与内网地址会被拒绝)。建好渠道时会显示一次签名密钥whsec_…),之后读不回,换密钥要删掉重建。

每条提醒是一次 JSON POST:

json
{
  "id": "whd_01HZX…",
  "event": "error_rate_spike",
  "scope": "user",
  "subject": "错误率飙升",
  "text": "…纯文本…",
  "markdown": "…Markdown…",
  "data": { "key": "生产", "percent": "35", "requests": "40", "errors": "14", "topCode": "UPSTREAM_ERROR", "window": "15 分钟", "site": "SciRouter", "time": "2026-09-22 10:30" },
  "site": "SciRouter",
  "time": "2026-09-22T02:30:00Z"
}

头:X-SciRouter-Event(通知项 id;试发是 test)、X-SciRouter-Delivery(投递 id,重试时不变,据此去重)、 X-SciRouter-Signature: t=<unix 秒>,v1=<hex>。验签三行:

python
t, v1 = parse("X-SciRouter-Signature")            # "t=1726990000,v1=…"
expected = hmac_sha256(secret, f"{t}.{raw_body}").hexdigest()
ok = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) < 300

你的接收端返回任意 2xx 即成功;5 秒没回或非 2xx 会再试 3 次(0.5 / 1 / 2 秒后)。连续失败 10 次渠道会自动停用, 原因写在渠道的「上次失败」里,修好地址后重新启用即可。data 里的键与通知模板的变量同名,后台改模板不影响它。

接进你的监控:Prometheus

自部署时网关的 /metrics 给出调用指标(只给内网抓,对外 404): scirouter_request_duration_seconds{model,source,status}scirouter_request_ttfb_seconds{model,source}scirouter_tokens_total{model,direction}scirouter_attempts_total{provider,outcome}scirouter_ratelimited_total{scope}scirouter_finish_reason_total{model,reason}。 标签只到模型 / 供应商 / 来源 / 状态,不含用户与 Key;仓库里带一份 Grafana 面板(scripts/grafana/scirouter.json)。 按用户、按 Key 看请用控制台的用量页与日志页。

几个容易读错的地方

  • finish_reason: "length"被 max_tokens 截断,不是模型故障。需要完整回答就调大 max_tokens
  • attempts 数的是尝试,不是失败:2 可能是一次失败 + 一次成功,也可能是一条渠道满了被跳过 + 一次成功。
  • 首字节有两个口径。SDK 里量到的首字节含网络与边缘那一段;timings.ttfb_ms 是网关到上游的那一段。判断「模型快不快」看后者。
  • 响应缓存命中(cache: "hit")的调用没有上游、不计费,attempts 为 0,timings.ttfb_ms 为 null。

隐私

请求日志里的提示词与返回片段按你在账号设置里的保留期保存,超期只剩统计列;日志不会存到浏览器里。 供应商只给名称,不给接口地址与线路信息。

请验证你的身份

为了保护你的账户,这个操作需要再验证一次。

验证码已发送到你绑定的手机号。5 分钟内的其他敏感操作不会再问。

Cookie 偏好

您可以随时回到这里修改。页脚有常驻入口。

必要

维持登录状态、记住主题与语言偏好。没有它们本站无法正常工作,因此无法关闭。

统计

了解哪些功能有人用、用户在哪一步流失。只收集维度(页面、操作类型、计数),不收集您输入的任何内容。

本站不加载任何第三方统计脚本,也不做会话录屏。完整说明