可观测性
响应头与扩展字段、控制台日志与用量、调用告警与 webhook,以及 OpenTelemetry 链路导出。
更新于 2026-10-25·阅读约 23 分钟
SciRouter 在响应头与响应体中返回单次调用的实际情况,在控制台请求日志中提供完整的时间线与尝试明细。这些都是 OpenAI 格式之外的附加项,官方 SDK 忽略不认识的字段,调用方无需修改代码。
响应头
#非流式响应(对话补全、Messages 与向量)带下列响应头。流式响应只带 X-Request-Id、X-SciRouter-Cache 与限额响应头(表中注明流式也发出的除外);其余各项在调用结束后才能确定,放在流末尾的扩展分片中,见响应体的 scirouter 字段。
| 响应头 | 说明 |
|---|---|
X-Request-Id | 本次调用的编号,始终由服务端生成,不沿用请求中携带的值。报障时提供该值,平台据此定位单次调用 |
request-id | 与 X-Request-Id 取值相同(Anthropic SDK 读取该名称),/v1 的每个响应都带 |
X-SciRouter-Model | 实际提供服务的模型 id |
X-SciRouter-Provider | 实际提供服务的厂商 id。同一模型可能接入多个厂商的线路,失败时自动转移 |
X-SciRouter-Attempts | 上游尝试总数(含跳过的线路)。值为 2 及以上表示发生过自动转移;费用只按最终成功的一次结算 |
X-SciRouter-Upstream-Request-Id | 厂商返回的请求 id,厂商未返回时不带。向厂商提交工单时用于对账 |
X-SciRouter-Cost-Quota | 本次消耗的配额,命中缓存时为 0。流式响应头在正文之前发出,此时消耗未知;流式调用的消耗见末尾扩展分片中的 scirouter.cost_quota |
X-SciRouter-Cache | HIT、MISS、PARTIAL(仅向量)或 BYPASS;另有 X-SciRouter-Cache-Age、X-SciRouter-Cache-TTL,见缓存与幂等 |
X-SciRouter-Variant-Switch | 请求带了会改变计价档位的参数、被自动改用模型变体时返回;值为触发的参数,以逗号分隔。目前只出现在 /messages,流式响应同样在正文之前发出,见 Messages(Anthropic 格式) |
X-SciRouter-Max-Tokens-Clamped | Messages(Anthropic 格式)的 max_tokens 超过模型或套餐的单次上限、平台按上限发出时返回,格式为 requested=…; effective=…; limit=model(套餐的上限更小时为 limit=plan)。流式响应同样在正文之前发出 |
X-SciRouter-Param-Actions | 有参数被平台去掉、翻译、模拟、整形或自愈时返回,如 dropped=seed,top_k; translated=reasoning(xhigh→high); emulated=response_format。五组固定顺序,超过 512 字节截断。三种格式均返回,流式响应同样在正文之前发出。各参数的处理见参数如何被处理 |
限额响应头
#限额响应头与 OpenAI 同名,SDK 已有的退避逻辑可直接识别:
| 响应头 | 说明 |
|---|---|
x-ratelimit-limit-requests | 每分钟请求数上限 |
x-ratelimit-remaining-requests | 每分钟请求数余量 |
x-ratelimit-reset-requests | 距当前窗口结束的时长,如 42s |
x-ratelimit-limit-tokens | 每分钟 token 数上限 |
x-ratelimit-remaining-tokens | 每分钟 token 数余量 |
x-ratelimit-reset-tokens | 距当前窗口结束的时长 |
- 余量按 Key 与账号两级中较小的一级返回。没有限额时不返回这组响应头。
- 流式响应的限额响应头在第一个分片之前写入。例外:排队较久、心跳已先发出响应头时,这组响应头与
X-SciRouter-Cache均不带。 - 被限频的 429 响应仍带
Retry-After与error.details.retry_after_seconds。 - 浏览器跨源调用时,上述响应头均列在
Access-Control-Expose-Headers中,fetch可以读取。
响应体的 scirouter 字段
#非流式响应的顶层多一个 scirouter 对象:
json{
"id": "chatcmpl-…",
"choices": [],
"usage": {},
"scirouter": {
"request_id": "req_…",
"model": "YOUR_MODEL_ID",
"provider": "…",
"cost_quota": 18240,
"cache": "miss",
"attempts": 2,
"upstream_request_id": "…",
"timings": { "queue_wait_ms": 830, "ttfb_ms": 412, "total_ms": 5688 }
}
}数值为示例,choices 与 usage 省略了内容。
| 字段 | 说明 |
|---|---|
timings.queue_wait_ms | 在线路容量队列中等待的时长,未排队时为 0。该段耗时表示厂商分配的容量已满,与模型推理速度无关 |
timings.ttfb_ms | 成功的那次尝试从发出到收到上游首字节的时长;未到达上游时为 null |
timings.total_ms | 端到端总耗时 |
以下字段按需返回:
| 字段 | 说明 |
|---|---|
saved_quota、cache_age_s、cache_ttl_s、cache_items | 响应缓存相关,见缓存与幂等 |
tool_rounds、tool_calls | 使用平台托管工具时返回,见 MCP 与托管工具 |
variant_switch | {from, to, params},自动改用模型变体时返回 |
max_tokens_clamped | {requested, effective, limit},Messages 的输出上限被按上限发出时返回 |
param_actions | {dropped, translated, emulated, reshaped, healed},各项为参数名列表,与响应头 X-SciRouter-Param-Actions 内容相同 |
schema_valid | 结构化输出经平台模拟、且带 X-SciRouter-Validate-Schema: 1 时返回,表示正文是否符合 schema |
finish_detail | 值为 repetition 时表示模型陷入复读、平台中断了上游,此时 finish_reason 为 length |
prompt_cache | {read, write, write_1h, saved_quota}:命中上游提示词缓存、写入 5 分钟缓存、写入 1 小时缓存的输入 token 数,saved_quota 为命中节省的配额。未到达上游或四项均为 0 时不返回 |
endpoint_provider | provider 的旧名,取值相同。新代码读取 provider |
param_actions、schema_valid、finish_detail 的规则见参数如何被处理。
提示词缓存的命中部分按缓存命中价计费、写入部分按写入价计费,节省的配额不并入 cost_quota。一段时间内的命中率见控制台用量页的「上游缓存命中」一栏,统计口径见缓存与幂等。
流式响应的扩展分片
#流式响应在 data: [DONE] 之前多一个 choices 为空的分片,带同一个 scirouter 字段。该分片的形状与 OpenAI 最后一个 usage 分片相同,SDK 均可识别:
textdata: {"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]其中 cost_quota 为本次的准确消耗。上游未输出任何分片即结束的流不带该分片;流开始后出错时,最后一个分片为 {"error": {…}},没有扩展分片,也没有 [DONE]。
请求日志
#控制台 /console/logs 可按 request_id 查询单条记录,也可按结束原因、来源、慢请求、缓存命中、调用格式(OpenAI / Anthropic)、错误码与自定义时间区间(最长 90 天)筛选。同一个 Key 同时供 OpenAI SDK 与 Claude Code 使用时,可按调用格式分开查看。
每条记录可展开为调用详情:
| 部分 | 内容 |
|---|---|
| 时间线 | 准入 → 路由 → 排队 → 每次尝试 → 上游首字节 → 推流 → 结算,未发生的阶段不显示。两个首字节(平台发给调用方的 / 上游返回的)各有一条标记线,差值为 SciRouter 侧的耗时 |
| 路由 | 实际厂商、每次尝试的结果与耗时 |
| 诊断提示 | 只解释已有的事实:被 max_tokens 截断、主要耗时在排队、发生过转移、用量为估算、客户端中途断开 |
| 复制报障信息 | 将 request_id、时间、模型、错误码、厂商请求 id 汇总为一段文本,用于报障 |
归因标签
#一个 Key 下运行多个实验时,账单只能按 Key 区分。在请求体中带 OpenAI 原生的 metadata 字段(字符串键值对),每次调用即带上自定义的归因标签:
pythonimport os
from openai import OpenAI
client = OpenAI(api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1")
client.chat.completions.create(
model="YOUR_MODEL_ID",
messages=[{"role": "user", "content": "…"}],
metadata={"exp": "abc", "run": "12"},
user="researcher-42",
)typescriptimport OpenAI from 'openai'
const client = new OpenAI({ apiKey: process.env.SCIROUTER_API_KEY, baseURL: 'https://api.scirouter.cn/v1' })
await client.chat.completions.create({
model: 'YOUR_MODEL_ID',
messages: [{ role: 'user', content: '…' }],
metadata: { exp: 'abc', run: '12' },
user: 'researcher-42',
})| 项目 | 规则 |
|---|---|
| 键的数量 | 最多 8 个 |
| 键 | 1–32 个字符,只含小写字母、数字、_、.、- |
| 值 | 字符串,最长 64 个字符,不能含控制字符(换行、制表符等) |
| 超出限制 | 返回 400 |
user(/v1/messages 为 metadata.user_id) | 超过 512 个字符时截断保存,不报错 |
metadata与user不转发给模型厂商,只记录在控制台日志中。- 日志页可按标签筛选(
?tag=exp:abc,点击记录上的标签即可);用量页可按标签分组查看花费、错误率与截断率(窗口不超过 30 天)。 - 站内对话(Chat)的每次调用自动带
chat_session标签,值为会话 id:日志页可从一条消息跳转到对应调用,也可筛选出一场对话的全部调用。 session_id是保留键:取值相同的调用归入同一个会话,控制台按会话列出各条执行链路,导出时也带该值,见会话 id。
用量页
#控制台 /console/usage 可按模型、Key、项目、来源、状态、标签分组,查看请求数、Tokens、配额、错误率与截断率;按模型分组时另有 P50 / P95 延迟。开启「与上期对比」后,上一个等长窗口画在同一张图上。窗口可选近 24 小时、7 天、30 天、90 天,或自定义区间(最长 90 天)。
团队工作区还可按成员分组。统计口径:
- 请求数不含响应缓存命中。命中单独计数,这些调用未调用模型、不计费。
- 延迟分位只在按模型分组时提供。分位数不能跨模型合并,按其他维度分组时表中没有 P50 / P95 两列,图上也不能选择 P95,表格上方有说明。
实时限额
#控制台概览页的「实时」卡每 10 秒刷新一次,显示:
- 每分钟请求数与每分钟 token 数在当前窗口的用量(与
x-ratelimit-*响应头为同一组读数); - 同时进行的模型调用数;
- 同时运行的 agent 数;
- 正在进行的请求;
- 近 1 小时的错误。
agent 指 Chat 的一轮、课题的一步或研读的一次生成,一个 agent 可能同时调用多次模型。agent 数已满时新的 agent 排队,不报错。
在团队工作区中,卡片显示团队的限额:在团队工作区发起的调用(包括团队项目下的 Key)按团队限额计、成员共用,不占个人限额。进行中的请求与错误只列出当前用户自己的。
程序中读取同一组读数,使用每次响应的 x-ratelimit-* 响应头。
导出
#日志页的「导出当前筛选」将当前筛选的记录导出为 CSV(UTF-8 BOM,Excel 可直接打开)或 JSON Lines。单次上限 50 000 行;超过时先提示命中条数并要求缩小范围,不导出部分结果。
- 文件中不含提示词、返回片段与请求参数。
- 包含已打码的来源 IP、
user、标签、saved_quota、调用格式api_format等列;列顺序固定,新列只追加在末尾。 - 时间列按账号设置的显示时区输出,列名中注明时区(如
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> | 签名,验证方法见下 |
secret 为渠道创建时显示的完整字符串(含 whsec_ 前缀),直接作为 HMAC 的 key,不做 base64 解码。v1 为以 secret 为 key、对 {t}.{原始请求体} 计算的 HMAC-SHA256 十六进制值。验签示例:
pythonimport hashlib
import hmac
import time
def verify(secret: str, raw_body: bytes, signature: str) -> bool:
parts = dict(item.split("=", 1) for item in signature.split(","))
t, v1 = parts["t"], parts["v1"]
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) < 300- 接收端返回任意 2xx 即视为成功;5 秒未响应或返回非 2xx 时再重试 3 次(分别在 0.5、1、2 秒后)。
- 连续失败 10 次后渠道自动停用,原因记录在渠道的「上次失败」中;修复地址后重新启用即可。此处的「一次」指一条提醒的完整投递(含 3 次重试);投递成功即清零,24 小时内没有新的失败也重新计数。
data中的键与通知模板的变量同名,后台修改模板不影响data。
Prometheus 指标
#自部署时,网关的 /metrics 提供调用指标。对外经 nginx 访问时返回 404,只供内网抓取;生产环境须配置 Basic Auth,用户名默认为 collector。
| 指标 | 标签 |
|---|---|
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 面板(server/scirouter/scripts/grafana/scirouter.json)。按用户或按 Key 查看时,使用控制台的用量页与日志页。
链路导出
#日志中的一条记录对应一次调用。Chat 的一轮、课题与研读的一次运行、/v1 的托管工具循环通常包含多次模型调用以及其间的工具、检索与等待,平台将其记录为一条执行链路:以运行为根的树,每个节点有起止时刻、结局与用量。
添加导出目的地后,这些链路以 OTLP/HTTP JSON 发送到调用方自有的 Langfuse 或任意 OpenTelemetry 后端,与应用自身的链路一起查看。
添加目的地
#| 配置位置 | 配置者 | 导出的链路 | 可带正文 |
|---|---|---|---|
| 控制台侧栏「链路导出」 | 用户本人 | 个人工作区内的:个人 Key 的 /v1 调用,个人工作区内的 Chat、课题、研读 | 可以(默认关闭) |
| 团队详情中的「链路导出」 | 团队 owner / admin(成员不可见) | 该团队工作区内的,含团队项目下的 Key 发起的 /v1 调用 | 不可以 |
平台运维另在管理后台配置平台级目的地,导出全部链路的元数据,不带正文。每个目的地的配置项:
| 配置项 | 说明 |
|---|---|
| 地址 | OTLP/HTTP 的基地址,导出时请求 POST {地址}/v1/traces。只接受 https、端口 443、公网可达的地址;地址中不能含用户名与密码,也不能含 ? 或 # |
| 类型 | langfuse 或 otel。langfuse 另加请求头 x-langfuse-ingestion-version: 4 与 Langfuse 识别的若干属性,见导出内容 |
| 鉴权 | 不鉴权、Basic(用户名与凭据)、Bearer、自定义请求头(请求头名与凭据)。凭据加密保存,保存后无法读回;地址改到另一台主机时须重新填写凭据,已保存的凭据不会发往新地址 |
| 采样率 | 0–1,默认 1(全部导出),见采样、重试、停用与测试 |
| 带上内容 | 默认关闭,只有个人目的地可以开启,见导出内容 |
个人默认最多配置 3 个目的地,每个团队最多 5 个。
Langfuse
#- 在 Langfuse 项目的 Settings → API Keys 中新建一对 key:public key(
pk-lf-…)与 secret key(sk-lf-…)。 - 在「链路导出」中添加目的地,按下表填写:
| 配置项 | 取值 |
|---|---|
| 类型 | langfuse |
| 地址 | Langfuse Cloud 欧洲区 https://cloud.langfuse.com/api/public/otel,美国区 https://us.cloud.langfuse.com/api/public/otel;自托管填 https://<域名>/api/public/otel |
| 鉴权 | Basic(langfuse 类型只支持 Basic):用户名填 public key,凭据填 secret key |
- 点击「测试」。成功时 Langfuse 中出现一条名为
scirouter connectivity test的链路。
自托管的 Langfuse 同样须使用 https、端口 443 且公网可达;只在内网可达的实例无法连接。
其他 OpenTelemetry 后端
#类型选择 otel,地址填写后端 OTLP/HTTP 接收端的基地址(不含 /v1/traces,平台自动追加):
| 后端 | 地址 | 鉴权 |
|---|---|---|
| 自建 OpenTelemetry Collector(再转发给 Jaeger、Tempo 等) | 接收端对外的 https 地址,如 https://otel.example.org | 按 Collector 的配置:bearertokenauth 扩展对应 Bearer,basicauth 对应 Basic |
| Jaeger | OTLP/HTTP 接收端对外的 https 地址 | 按前置反向代理的要求填写 |
| Honeycomb | https://api.honeycomb.io(欧洲区 https://api.eu1.honeycomb.io) | 自定义请求头:请求头名 x-honeycomb-team,凭据填 Honeycomb 的 API Key |
- 平台只发送 OTLP/HTTP 的 JSON 编码,不使用 gRPC;接收端须接受
application/json(Collector 与 Jaeger 的 OTLP 接收端均支持)。 - Collector 与 Jaeger 的 OTLP/HTTP 接收端默认使用 4318 端口与明文 http,平台只连接 https 的 443 端口:须在前面加一层 https 反向代理,或为接收端配置证书并监听 443。
- 单次发送的请求体默认不超过 4 MiB,接收端的请求体上限须大于该值。
导出内容
#一条链路是 trace id 相同的一组 span,每个节点对应一个 span:
| 节点 | span 名 | gen_ai.operation.name | Langfuse 类型 |
|---|---|---|---|
运行:Chat 的一轮、课题 / 研读的一次运行、/v1 托管工具循环的根(v1_tool_loop) | invoke_agent {名字} | invoke_agent | agent |
| 步骤 | invoke_agent {步骤种类} | invoke_agent | agent(主 agent)/ chain(其余) |
| 模型调用 | chat {模型 id} | chat | generation;验收评审的调用为 evaluator |
| 检索 | execute_tool {名字} | execute_tool | retriever |
| 工具、沙箱、MCP、读技能、读回原文、留言 | execute_tool {名字} | execute_tool | tool |
| 等待:排队等名额、撞沙箱闸放回、等待用户回答 | wait {名字} | 无 | span |
| 截断标记:该链路记录的节点数已达上限,之后的节点不再记录 | marker truncated | 无 | event |
- 重试、换模型、续写、压缩、退回队列不单独成为 span,记为所在节点上的 span event(事件名为
retry、fallback、continue、compaction、requeue或other;能对应到某次调用时带该调用的scirouter.request_id)。 - 模型调用的 span kind 为 CLIENT,其余为 INTERNAL;出错节点的 status 为 ERROR,消息为错误码。
- resource 只有
service.name = scirouter;instrumentation scope 为scirouter.traceexport,版本为gen-ai-dev/1.42.0-dev。属性名遵循 OpenTelemetry GenAI 语义约定的该版本;该约定处于 Development 阶段,上游改名时平台同步修改,scope 的版本号标明所用版本。
每个节点都有的属性:
| 属性 | 说明 |
|---|---|
gen_ai.operation.name | 见上表;等待与截断标记没有该属性 |
gen_ai.conversation.id | 会话 id:Chat 会话、课题、论文,或 /v1 的 metadata.session_id;不属于任何会话时没有该属性 |
scirouter.trace.kind | chat / campaign / paper / api |
scirouter.node.type | run / step / generation / tool / retrieval / sandbox / mcp / skill / recall / post / wait / marker |
scirouter.status | ok / error / cancelled / skipped;运行收尾时仍未结束的节点为 running |
scirouter.error_code | 出错时的错误码 |
按节点类型另有的属性:
| 节点 | 属性 |
|---|---|
| 模型调用 | gen_ai.provider.name(厂商 id,没有时为 scirouter)、gen_ai.request.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、gen_ai.usage.cache_read.input_tokens(命中提示词缓存的部分,大于 0 时才有)、gen_ai.response.finish_reasons;scirouter.request_id(控制台日志可按它查到该次调用)、scirouter.cost_quota、scirouter.attempts、scirouter.provider、scirouter.purpose(turn / review 验收评审 / compaction 压缩摘要 / other)、scirouter.first_token_ms |
| 工具类 | gen_ai.tool.name;scirouter.cost_quota(工具自身按次的花费,有时才返回);scirouter.tool.<键>:只含白名单中的计数、布尔值与短枚举,即 argsBytes、resultChars、results、sources、papers、reused、prefetched、gated、empty、sandboxStatus、wallMs、timeout、exitCode、round、rounds、toolCalls、failure |
| 运行、步骤 | gen_ai.agent.name |
类型为 langfuse 时,每个 span 另加:
| 属性 | 说明 |
|---|---|
langfuse.observation.type | 上文节点表中的 Langfuse 类型 |
langfuse.trace.name | 链路标题,如「第 3 轮对话」 |
session.id | 与 gen_ai.conversation.id 相同,Langfuse 的 Sessions 据此将多条链路归为一个会话 |
langfuse.observation.completion_start_time | 模型调用的首字时刻 |
user.id | 仅团队目的地:节点所属成员的用户 id。个人目的地不带 |
trace id 与 span id 由平台的链路 id、节点 id 经 SHA-256 截取为 16 / 8 字节,同一节点每次导出的 id 相同;带 traceparent 的链路直接使用调用方的 trace id,见 traceparent。
正文默认不导出。节点中只有上述计数、长度与枚举,不含提示词、回答、检索词与网址。个人目的地开启「带上内容」后,模型调用另带 gen_ai.input.messages 与 gen_ai.output.messages 两个属性(按约定的消息结构序列化的字符串:输入整段记为一条 user 消息,输出记为一条 assistant 消息):
- 内容取自请求日志保存的正文片段,因此只有该次调用保存了正文时才有(账号设置「数据保留」中的「保存请求与响应正文」,见隐私);
- 平台内部的验收评审与压缩摘要调用一律不带正文;
- 单个节点带上正文后超过请求体上限时,该节点不带正文发送;
- 团队与平台级目的地不能开启该选项。
导出时机:
- 导出每分钟运行一轮。Chat、课题、研读的链路在整次运行结束后一次发送;
/v1的节点在各自结束后发送。 - 节点结束 2 分钟后才发送(等待全部落库),因此后端中看到的数据比调用晚两三分钟。
- 每个节点对每个目的地只发送一次。例外:对方已接收、平台尚未记录进度时实例恰好重启,该批会重发一次。
- 只发送归属该目的地的节点,同一条链路中其他用户的调用不会一并发出。
- 向量调用(
/embeddings)不在链路中。
traceparent
#/v1/chat/completions、/v1/messages、/v1/responses 识别 W3C Trace Context 的 traceparent 请求头。带该请求头时,本次调用记录在调用方的 trace 之下:
- 链路 id 为调用方的 trace id;本次调用(使用托管工具时为循环的根节点)的父节点为调用方的 span id。导出时两者原样使用,平台的节点挂在发起调用的 span 之下,在后端中属于同一棵树。
- 同一个 trace id 的多次请求,在控制台中也属于同一条链路。
- 只识别 version
00:00-<32 位小写十六进制 trace id>-<16 位小写十六进制 span id>-<2 位十六进制 flags>。格式错误、id 全为零或带多个traceparent时视为未带,不报错;不读取tracestate;flags 中的采样位不影响平台是否记录与导出。 - 未带时本次请求自成一条链路,链路 id 为本次调用的
request_id(托管工具循环的各轮沿用第一轮的值)。
traceparent 只负责关联两侧的链路。要在后端中看到完整的树,还须添加指向同一后端的导出目的地,见添加目的地。
bashcurl https://api.scirouter.cn/v1/chat/completions \
-H "Authorization: Bearer $SCIROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
-d '{
"model": "YOUR_MODEL_ID",
"messages": [{"role": "user", "content": "…"}],
"metadata": {"session_id": "notebook-42"}
}'Python(OpenAI SDK 与 OpenTelemetry SDK)在每次调用时用 extra_headers 带上当前 span:
pythonimport os
from openai import OpenAI
from opentelemetry import trace
from opentelemetry.propagate import inject
client = OpenAI(api_key=os.environ["SCIROUTER_API_KEY"], base_url="https://api.scirouter.cn/v1")
tracer = trace.get_tracer("my-agent")
with tracer.start_as_current_span("summarize-paper"):
headers = {}
inject(headers) # 把当前 span 写成 traceparent 头
response = client.chat.completions.create(
model="YOUR_MODEL_ID",
messages=[{"role": "user", "content": "…"}],
metadata={"session_id": "notebook-42"},
extra_headers=headers,
)- 整个脚本在同一个 span 下运行时,也可以在创建 client 时用
default_headers={"traceparent": …}一次带上。 - 安装 OpenTelemetry 的 httpx 自动埋点(
opentelemetry-instrumentation-httpx)后,OpenAI SDK 发出的请求自动带traceparent,无需手动设置。
会话 id
#请求体 metadata 中的 session_id 是保留键,取值为调用方自定义的会话 id。traceparent 决定一条链路中节点的父子关系,session_id 决定多条链路是否属于同一个会话,两者可同时使用。
- 控制台:会话链路逐行列出该会话中的链路(调用次数、Token、配额、耗时,表头为整个会话的合计)。
session_id同时是普通的归因标签,日志页按session_id:<值>可筛选出该会话的全部调用。 - 导出:写入
gen_ai.conversation.id,langfuse类型另写session.id。 - 规则与其他标签相同,见归因标签:值为字符串,最长 64 个字符,不能含控制字符,占 8 个键中的一个;格式错误时返回 400。
/v1/messages同样写在metadata中(user_id之外的字符串键均视为标签);/v1/responses写在其自身的metadata中。
采样、重试、停用与测试
#| 项目 | 规则 |
|---|---|
| 采样 | 按链路 id 的哈希判定,同一条链路整条导出或整条不导出 |
| 失败与重试 | 对方返回 2xx 视为已接收(含 OTLP 的部分成功);连接失败、10 秒未响应、返回非 2xx 均视为失败。失败的批次不丢弃,1 分钟后重试,之后每次等待时间翻倍,最长 1 小时;成功一次后失败计数清零 |
| 停用 | 连续失败 10 次后自动停用,目的地上注明原因(对方最近一次的响应,截取 300 字,凭据会被抹去)。修复后修改配置保存或重新启用:失败计数清零,从停止处继续发送;停用较久的只补发最近 24 小时的链路 |
| 测试 | 用已保存的配置发送一条只含一个 span 的合成链路(span 名 scirouter connectivity test,属性 scirouter.test = true),当场返回对方的状态码、错误原因与耗时。不计入导出条数,也不改变失败计数与状态 |
| 平台暂停导出 | 不能新建、不能测试;已有目的地可以查看、修改、删除,但不发送。恢复后从停止处继续发送(同样最多补发 24 小时) |
易误读的字段
#finish_reason: "length"表示回答被max_tokens截断,与模型故障无关。需要完整回答时调大max_tokens。attempts统计的是尝试次数,不等于失败次数:2可能是一次失败加一次成功,也可能是一条线路已满被跳过加一次成功。- 首字节有两种口径。SDK 测得的首字节包含网络与边缘节点的耗时;
timings.ttfb_ms只计网关到上游的一段。判断模型速度以后者为准。 - 命中响应缓存(
cache: "hit")的调用不经过上游、不计费,attempts为 0,timings.ttfb_ms为null。
隐私
#- 请求日志默认不保存提示词与返回正文。在账号设置「数据保留」中开启「保存请求与响应正文」后,之后的调用才会保存(脱敏后每段最多 16 000 字,更长的只保留开头,并在日志详情中标注)。
- 正文由平台保留 7 天后自动清空(天数由平台策略决定,页面上显示当前值)。关闭开关只影响之后的请求,已保存的正文按期过期。
- 调用记录本身(模型、耗时、token、配额、状态、标签等)保留 90 天,到期整行删除;按小时、按天汇总的用量不受影响。
- 日志不存储在浏览器中。响应中的厂商只返回 id(控制台中显示名称),不返回接口地址与线路信息。
- 执行链路只发送到已添加的导出目的地:个人工作区的链路发往用户本人添加的目的地,团队工作区的链路发往团队 owner / admin 添加的目的地,平台级目的地只接收元数据。只有个人目的地开启「带上内容」时才带正文,见链路导出。发出后的存储方式与保存时长由接收方决定。