把 OpenRouter 的调用日志推到你的可观测平台:Broadcast 怎么接
线上跑了一段时间之后,大部分团队都会遇到同一个尴尬:模型调用的账单、延迟、报错分散在平台自己的用量页里,而应用侧的链路追踪在另一套系统里。想把两边对上,通常得在业务代码里再包一层 instrumentation,然后维护它。
OpenRouter 的 Broadcast 想解决的就是这一段。官方文档《Broadcast》页(openrouter.ai/docs/guides/features/broadcast)写明:它把 OpenRouter 请求的 trace 自动发送到外部可观测与分析平台,不需要在你的应用代码里加额外的 instrumentation。
这篇只写一件事:Broadcast 的配置到底分几层,以及 OTLP 目标端、Webhook、S3 这三类落地端在同一份 trace 上会长成什么样。它们的差异不是”换个地址”那么简单,元数据键的映射表在三页文档里就不一样。
一、前置条件:先确认这三件事
第一,这是账号侧的能力,不是 SDK 侧的能力。 文档给出的启用路径是进入 OpenRouter 控制台的 Settings > Observability,打开 “Enable Broadcast” 开关,再添加一个或多个 destination。也就是说,你在本机装什么、用哪个语言的 SDK,都不影响这一步。
第二,组织账号有权限门槛。 文档里的提示写得很明确:如果你用的是组织账号(organization account),必须是 organization admin 才能编辑 broadcast 设置。团队里普通成员打开设置页改不动,这不是 bug。
第三,目标端必须能从公网访问到。 Webhook 那一页把要求列成了三条:接受 application/json 内容类型、成功时返回 2xx 状态码、可从互联网公开访问。OTLP 自建 collector 那一页同样写了”配置 receiver 监听在一个可公开访问的端点”。内网里的 collector、只挂在 VPN 后面的接收服务,按文档这个口径是接不上的。
二、配置分两层:全局开关 + 每个 destination 独立配置
这是最容易看漏的结构。Broadcast 不是”一个开关配一个地址”,而是:
全局层:Enable Broadcast 总开关;文档还写明 Broadcast 可以在个人用户级别和组织级别分别配置,组织管理员可以设置对组织内所有 API key 生效的共享 destination。
destination 层:每个目标端除了自己的连接参数,还各自带三个通用配置。这三个是按 destination 分别设置的,不是全局的:
- API Key Filtering:一个 destination 可以只接收指定 API key 产生的 trace。文档写明,如果一个 API key 都没选,这个 destination 会收到你所有 API key 以及 chatroom 请求的 trace。
- Sampling Rate:控制这个 destination 接收多大比例的 trace。文档对取值的解释是,
1.0发送全部 trace,0.5则大约发送一半。 - Privacy Mode:勾上之后,发往这个 destination 的 trace 会剥掉 input messages(发给模型的提示) 和 output choices(模型返回的补全),而 token 计数、成本、耗时、模型信息与自定义元数据照常发送。文档明确说这是按 destination 配置的——你可以给一个目标端发完整 trace 用于排查,同时给另一个目标端发脱敏后的 trace 用于成本监控。
采样这里有一处值得单独记住的语义:文档写明采样是确定性的,当你传了 session_id 时,同一个 session 内的所有 trace 会被一致地整体包含或整体排除,避免看到半截会话。但紧接着还有一句限定——你会在每个 destination 里看到完整的 session,但不保证跨 destination 看到的是同一批 session。做多目标端对账的时候,这一句能省掉你半天的困惑。
三、请求侧能带哪些字段
destination 配好之后,trace 内容主要由请求体决定。《Broadcast》页写明每条 trace 默认就包含请求与响应数据(多模态内容会被剥除)、token 用量、成本、时间与延迟、模型 slug 与 provider 名称、以及是否带了工具、是否发生了 tool call。
在这之上,你可以额外带三类字段:
user:把 trace 关联到具体终端用户,文档标注了字符数上限(具体数值以官方文档为准)。session_id:把一组相关请求归到一起,文档同样标了字符数上限;这个值也可以通过x-session-idHTTP 头传。trace:任意 JSON 对象,会原样透传到你配置的所有 broadcast destination。
《Broadcast》页给出的 trace 字段示例如下(原样引用官方文档):
{
"model": "openai/gpt-4o",
"messages": [
{
"role": "user",
"content": "Summarize this document..."
}
],
"trace": {
"trace_id": "workflow_12345",
"trace_name": "Document Processing",
"span_name": "Summarization Step",
"generation_name": "Generate Summary",
"environment": "production",
"feature": "customer-support",
"version": "1.2.3"
}
}
代码块里的 openai/gpt-4o 只是官方文档当时用的示例值,平台上有哪些模型随时在变,不要把它当作模型清单来读。
《Broadcast》页的 Common Metadata Keys 表列了五个有特殊含义的键:trace_id、trace_name、span_name、generation_name、parent_span_id。其余键文档说是灵活的键值对,具体哪些键有特殊含义取决于目标端——这句话就是下一节的引子。
四、三类落地端的实际差异
连接参数不一样
| 落地端 | 官方文档列出的配置项 |
|---|---|
| OpenTelemetry Collector | Endpoint(OTLP traces 端点 URL)、Headers(可选,JSON 对象形式的自定义 HTTP 头) |
| Webhook | URL、Method(可选,POST 默认或 PUT)、Headers(可选,JSON 对象) |
| S3 / S3-Compatible | Bucket Name、Region(可选)、Custom Endpoint(可选)、Access Key Id、Secret Access Key、Session Token(可选)、Path Template(可选) |
OTLP 那一页写明:OpenRouter 使用 OTLP/HTTP 协议、JSON 编码发送 trace,要确保你的 collector 或后端能在 /v1/traces 路径上接受 OTLP over HTTP。Webhook 那一页则说明,端点收到的就是 OTLP 格式的 payload,因此它对任何 OTLP-aware 的系统都是兼容的。把两页放在一起看,这两个目标端在数据格式上写的都是 OTLP:OTLP 那一页的落点是「任何支持 OTLP over HTTP 的后端」,Webhook 那一页的落点是「任何能接收 JSON 的 HTTP 端点」,配置项上可见的差别是 Webhook 多一个 Method 可选(POST 默认或 PUT)。至于两者在投递行为上还有没有别的不同,官方文档没有说明这一点,不要替它补齐。
S3 侧多一个别处没有的东西:Path Template。文档写明默认值是 openrouter-traces/{date},可用变量是 {prefix}、{date}、{year}、{month}、{day}、{apiKeyName}。文档给的几种组织方式包括 traces/{year}/{month}/{day} 这种层级日期结构,以及 {apiKeyName}/{date} 这种按 API key 名先分目录的写法。落到对象上,文档说每条 trace 存成一个独立的 JSON 文件,命名格式是 {traceId}-{timestamp}.json。
元数据映射不一样(这一处最容易踩)
同样是 trace 里的那几个键,三页文档给出的映射表并不一致:
| 元数据键 | OTLP Collector | Webhook | S3 |
|---|---|---|---|
trace_id | Trace ID | traceId | id(trace 级) |
trace_name | Span Name | Span name | name(trace 级) |
span_name | Span Name | Span name | name(observation 级) |
generation_name | Span Name | Span name | name(observation 级) |
parent_span_id | Parent Span ID | parentSpanId | S3 那一页的表里没有这一行 |
最后一行是我第一次对着三页文档看时才注意到的:parent_span_id 在 OTLP 与 Webhook 两页都列了映射,S3 页的 Supported Metadata Keys 表只有四行,没有 parent_span_id。如果你的用法是”把 OpenRouter 的调用挂到自己已有的分布式 trace 下面”,那么按文档写明的内容,S3 这条路上没有对应的映射说明。它到底是不写入还是写入了但没写进文档?官方文档没有说明这一点,不要替它推断。
user 与 session_id 的落点也分两套写法:
- OTLP Collector 与 Webhook:
user→user.id,session_id→session.id(点号分隔的 span 属性) - S3:
user→userId,session_id→sessionId(trace JSON 里的驼峰字段)
自定义键的存放位置同理。OTLP 与 Webhook 两页都写明,自定义元数据键作为 span 属性放在 trace.metadata.* 命名空间下,例如 trace 里的 environment 会变成 trace.metadata.environment;模型、token 用量与成本则走标准的 GenAI 语义约定(gen_ai.*)。而 S3 那一页写的是,自定义元数据键存在 trace 中每个 observation 的 metadata 字段里,可以用 Athena、Presto 这类 JSON-aware 查询引擎去查。
这意味着一件很实际的事:你在 Grafana / Jaeger 里写的查询条件,直接搬到 Athena 上是查不出来的,字段路径根本不是同一套。多目标端并行的团队,仪表盘和查询语句得各写一份。
五、边界:文档明说的与文档没说的
- 目标端列表会变。《Broadcast》页除了当前可用的目标端,还单列了一个 Coming Soon 分区,列出若干”在开发中、即将可用”的目标端。文档自己也写了,最新的可用列表以控制台的 Broadcast 设置页为准。所以别把任何一份第三方整理的目标端清单(包括本文)当作现状。
- 多模态内容会被剥掉。 文档在描述 trace 数据时括注了 “with multimodal content stripped for efficiency”。指望在 trace 里回看用户发的图,按文档这个口径是拿不到的。
- Test Connection 不通过就存不下配置。 三页文档都写了同一句:配置只有在测试通过时才会保存。
- S3 是一条 trace 一个文件。 文档在提示框里说,如果你要的是按时间批量聚合(比如按小时或按天合并成一个文件),可以考虑改用 AWS Kinesis Firehose,由它缓冲记录后再批量写入 S3。这属于文档给出的替代路径,不是 Broadcast 自身的能力。
- 失败投递怎么办,文档没有给出机制说明。 Webhook 页只在提示框里建议”生产环境下确保端点高可用,并在你自己这一侧实现重试逻辑”。至于 OpenRouter 侧是否重试、重试几次、失败的 trace 在哪查——官方文档没有说明这一点。
- 延迟与安全的表述要照原文读。 文档写明凭据在存储前被加密、仅在发送 trace 时解密,并且 trace 是在请求完成后异步发送的,因此启用 Broadcast 不会给 API 响应增加延迟。这是官方对机制的陈述;实际安全性仍取决于你选的目标端与自身环境,本文不对此做任何担保性判断。
六、怎么验证接对了
第一步,用控制台的 Test Connection。 三页文档都要求先跑这一步。Webhook 页额外交代了测试请求的形状:OpenRouter 会向你的端点发送一个空的 OTLP payload,并带上 X-Test-Connection: true 头;端点返回 2xx 视为通过,400 也被接受(文档说明的理由是有些端点会拒绝空 payload)。如果你在自己的接收服务上做了严格的 schema 校验,记得给这个头开一条旁路,否则测试永远过不去。
第二步,发一条带 trace 字段的真实请求。 官方文档《Authentication》页给出的最小请求形如:
curl https://openrouter.ai/api/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d '{
"model": "openai/gpt-5.2",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
]
}'
把《Broadcast》页里的 user、session_id、trace 三个字段并进这个请求体,就是一条可验证的测试请求。以上为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
Windows 侧要多绕一步。 PowerShell 里 curl 默认是 Invoke-WebRequest 的别名,上面这段带单引号 JSON 的写法不能直接粘。通用做法是显式调用 curl.exe,并把请求体写进一个 UTF-8 文件后用 -d "@body.json" 引用,避开引号转义;用 WSL 或 Git Bash 则和 Linux/macOS 一致。这一段是通用 shell 做法,不是 OpenRouter 官方文档的内容,请以你本机 shell 的行为为准。密钥一律走环境变量(示例里的 $OPENROUTER_API_KEY),不要写进命令行历史。
第三步,按落地端分别核对。 OTLP 后端里看 span 是否带上了 trace.metadata.* 下的自定义键、user.id 与 session.id 是否有值;Webhook 端点直接打印收到的 JSON,确认 resourceSpans 结构与 trace.metadata.* 属性存在(文档提到开发调试阶段可以用 webhook.site 这类服务先看 payload 长什么样);S3 则去 bucket 里按 Path Template 展开的路径找 {traceId}-{timestamp}.json,打开确认 metadata 字段里有你传的键。
第四步,如果某个 destination 一条都没收到,先按上面第二节的三个 destination 级配置逐个排除:API Key Filtering 是不是只选了另一把 key、Sampling Rate 是不是调低了、Privacy Mode 是不是让你误以为”内容没了 = 没收到”。这三项是按 destination 独立生效的,很容易在多目标端场景下互相干扰。
最后提一句口径问题:Broadcast 这块的目标端和配置项迭代得比较快,本文只复述落盘当天官方文档写明的内容,该平台迭代频繁,以官方文档最新内容为准。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。