跳到主要内容
版本:Dev

指标与用量事件

AISIX 网关会公开聚合指标和可导出的用量事件。这些信号共同反映服务健康状况、流量趋势,以及每次请求尝试对应的模型、路由和策略结果。

选择遥测来源​

请先选择与运维问题匹配的来源;需要深入调查问题时,再按请求、模型或服务提供方关联各类信号。

运维问题首选来源提供的信息
各网关实例的流量是否健康?Prometheus 指标请求速率、延迟分布、Token 和成本计数、策略结果、路由健康、缓存行为和导出器投递健康。
单个请求发生了什么?访问日志结构化请求字段,包括状态、延迟、模型、模型服务提供方、请求 ID,以及可用时的路由结果。
调用应用可以观察到什么?响应头受支持路由上的请求关联、缓存结果、重试时机和选中目标提示。
请求记录可以存储到哪里、如何分析或用于核算?用量事件通过可观测性导出器投递的逐次尝试结果和消耗记录。

抓取 Prometheus 指标​

AISIX 默认通过专用指标监听器在 /metrics 提供 Prometheus 指标。你可以通过启动期可观测性设置修改路径或禁用该端点。

该端点设计上不做认证。请保持专用指标监听器私有。

在启动配置中配置 Prometheus 暴露:

config.yaml
observability:
metrics:
prometheus:
enabled: true
path: "/metrics"

专用监听器默认绑定到 0.0.0.0:9090。如果 Prometheus 应从其他接口或端口抓取指标,请设置不同的监听地址。

抓取默认指标端点:

curl -sS "http://127.0.0.1:9090/metrics"
产生流量后才会出现流量指标

AISIX 会在每次抓取时发布配置状态。其他指标族会在首次观测时注册,因此流量指标可能不会在启动后立即出现。请先发送一次模型请求,再检查 aisix_requests_total 和 aisix_tokens_consumed_total 等序列。

AISIX 使用 aisix_ 前缀输出原生指标名称。使用直方图序列计算跨网关实例的延迟百分位数,并使用请求计数器分析成功率和路由情况。精确指标名称、标签范围和 PromQL 示例请参见指标参考。

通过 observability.metrics.labels 可以选择各指标族输出的标签。配置示例、默认值和完整变量列表参见指标标签与变量。

区分请求计数与尝试计数​

请求是一次调用方交互。重试或故障转移可能让一个请求产生多次用量事件尝试。部分尝试会在目标级限流或请求组装阶段停止,尚未调用模型服务提供方,因此 AISIX 在不同信号中对尝试的计量方式也不同:

单位统计位置一个样本或一行代表什么
请求aisix_proxy_requests_total、aisix_llm_requests_total一次客户端请求,状态是请求最终结束时的状态。对流式响应而言,这是流结束时的状态,可能与流开始时调用方收到的 200 不同。
尝试aisix_deployment_requests_total 及部署指标族对某一个目标模型的一次上游调用。
发出尝试aisix_usage_events_emitted_total一次用量事件的发出尝试,在投递队列接受或拒绝它之前计数。
尝试用量事件记录,以及基于其构建的用量日志一次已记录的处理尝试,包括在上游调用前停止的尝试。同一请求的各次尝试共享 request_id,并按 attempt_index 排序。

如果某个目标返回 502,而后备目标成功,请求计数器只记录最终返回给调用方的 200。部署计数器和用量事件则会保留失败的尝试。因此,用量日志中的 5xx 尝试数可能远高于请求指标;两者计量的单位不同。

请按各查询真正回答的问题选用:

# 最终以服务端错误结束的请求——调用方实际经历的情况。
sum(increase(aisix_proxy_requests_total{status=~"5.."}[24h]))

# 发生过故障转移、但最终仍以服务端错误结束的请求。
sum(increase(aisix_proxy_requests_total{status=~"5..", is_fallback="true"}[24h]))

# 失败的上游尝试,按目标聚合——包含后续被故障转移救回的那些。
# 这是 5xx 用量日志行数在尝试层面的对应视图,范围限于通过模型组下发的端点。
sum(increase(aisix_deployment_failure_responses_total[24h])) by (model)

# 成功救回请求的故障转移,按模型组和实际到达的目标聚合。
sum(increase(aisix_routing_successful_fallbacks_total[24h])) by (model, fallback_model)

# 用量事件的发出尝试,以及被交接队列拒绝的事件。
sum(increase(aisix_usage_events_emitted_total{status_code="5xx"}[24h]))
sum(increase(aisix_usage_event_drops_total[24h]))

# 某个成员被限流拒绝的次数。`status` 携带原始 HTTP 状态码,
# 因此无需扫描整个状态族即可定位单一故障模式。
sum(increase(aisix_usage_events_emitted_total{user_id="<member-id>", status="429"}[24h]))

# 哪些队列交接被拒绝了。队列侧的 reason 与发出计数器携带相同的模型与
# Provider Key 标签,因此该差值在按模型的粒度上同样成立,而不只是总量。
sum(increase(aisix_usage_event_drops_total{reason=~"sink_.*"}[24h])) by (model, provider_key_name)

# 网关已记录但未能投递到控制面的用量。这些样本只携带成员标签,
# 因此按成员分组,而不是按模型分组。
sum(increase(aisix_usage_event_drops_total{reason=~"send_failed|retry_budget_exhausted"}[24h])) by (reason, user_name)

比较相同的数据范围​

在将两个总数视为不一致之前,请先检查它们的覆盖范围:

差异原因检查方法
抓取覆盖范围请求计数器不带环境标签。只有 Prometheus 抓取了服务该环境的每个网关,单环境用量总数才可与之比较。按 job 和 instance 对原始计数器分组,再与正在运行的网关实例核对。
时间覆盖范围increase(...[24h]) 只使用该区间内存在的样本。重启或更短的 Prometheus 保留期可能造成用量日志查询中没有的缺口。在相同时间范围内绘制原始计数器。
投递覆盖范围aisix_usage_event_drops_total 既统计因接收端被禁用、队列已满或关闭而无法进入投递队列的事件(sink_disabled、sink_full、sink_closed),也统计工作线程已接受、但未能投递到控制面的事件(send_failed、retry_budget_exhausted)。导出器侧的丢失不计入该指标。队列侧丢弃按 model 分组;投递侧丢弃按 user_name 分组——它们不带模型和 Provider Key 归因。监控控制面投递的 telemetry batch failed (events dropped) 和导出器投递的 sink delivery dropped。控制面在已接受的批次中拒绝的事件则由 aisix_usage_events_rejected_total 统计。
没有上游调用目标级限流和请求组装失败仍会产生用量事件,但没有部署计数器样本。组装失败包括凭证不可用、缺少 model_name,以及 api_base 缺失或格式错误。配置错误的目标会产生失败请求,但没有部署失败,因为模型服务提供方从未被调用。

部署指标族覆盖通过模型组分发的端点,而用量事件也覆盖直连模型。请逐项隔离差异,不要将全部差值都解释为网关侧拒绝。

导出用量事件​

用量事件是受支持代理路径输出的逐次尝试记录。发生重试或故障转移的请求会生成多条具有相同 request_id 的事件,并按 attempt_index 排序。因此统计这些记录得到的是尝试数而非请求数——在拿记录条数与请求指标作比较之前,请先阅读区分请求计数与尝试计数。每个请求恰好产生一条 attempt_index 为 0 的事件:它第一个尝试的事件,或者没有发起上游尝试时它唯一的事件。统计这些事件即可得到请求数,控制面也是这样计数的。/v1/messages/count_tokens 以及支持的端点中列出的单次调用端点遵循同样的规则:每次导致重试或故障转移的失败尝试都会产生一条自己的零 Token 事件。

每条事件都携带 occurred_at,即网关记录该事件的时间,格式为带恰好三位小数的 RFC 3339 UTC 时间戳,例如 2026-09-24T12:42:25.123Z。对象存储、阿里云 SLS 和 Datadog 按原样接收该字符串,OTLP 导出器则在其纳秒时间戳中携带同一时间。因此,同一请求的各次尝试,以及在同一秒内完成的事件,都可以精确到毫秒排序。RFC 3339 解析器都接受带小数的形式;只接受整秒(…:SSZ)的解析器需要更新。

用量事件不能从本地端点读取。请在可观测性导出器中配置导出器,将其投递到 OTLP/HTTP、对象存储、阿里云 SLS 或 Datadog。

每条事件都会包含请求结果、消耗详情、调用方请求的模型别名,以及网关能够观测到时处理该次尝试的解析模型。在 /v1/chat/completions 上,每条事件还会携带 cache_status(disabled、miss、hit 或 bypass);由响应缓存返回的响应会把命中层记录在 cache_hit_layer(exact 或 semantic),语义命中还会把匹配相似度记录在 cache_similarity。流式响应从不被缓存,因此即使环境中启用了缓存策略,流式请求的事件也会上报 cache_status="disabled"。

Responses 结束原因​

Responses API 的结果不声明结束原因,因此 AISIX 在 /v1/responses 上根据调用方收到的结果推导事件的 finish_reason,无论原生处理还是协议转换、流式还是非流式,取值沿用聊天补全的词汇:

结果 status记录的 finish_reason
completed输出中含有 function_call 或 custom_tool_call 条目时为 tool_calls,否则为 stop。
incompleteincomplete_details.reason 为 content_filter 时为 content_filter,否则为 length。
failed、cancelled 或其他状态无,该字段为空。

该值与其他结束原因一样送达控制面和每个导出器,包括 OTLP 的 gen_ai.response.finish_reasons。1.6.0 及更早版本的网关在 /v1/responses 上不记录结束原因。

读取实际服务档位​

service_tier 是上游响应报告其实际处理该次尝试时所用的服务档位,按服务提供方自己的词汇原样记录。它绝不是请求所要求的档位,网关也不会填入默认值。各服务提供方的档位名称不同,并且会随时间变化,因此该字段可以是任意字符串:

上游来源服务提供方报告的值(示例)
OpenAI 及兼容 OpenAI 的 Chat Completions响应顶层的 service_tierdefault、flex、scale 或 priority。OpenAI 的 fast mode 报告为 priority。
Responses APIResponse 对象的 service_tier。流式响应只读取终止事件中的 Response,因为更早的事件可能仍是请求所要求的档位,例如 auto。与 OpenAI Chat Completions 相同。
Anthropicusage.speed 不为 standard 时取该值,否则取 usage.service_tierfast mode 下为 fast,否则为 standard 或 priority 等值。
Amazon Bedrock ConverseserviceTier.typedefault、flex、priority 或 reserved。
Vertex AI 适配器上的 GeminiusageMetadata.serviceTier,否则取响应头 x-gemini-service-tier。以 unspecified 结尾的值视为未报告。standard、flex 或 priority。

承载可识别的 OpenAI、Anthropic 或 Responses 报文的透传路由也按同样方式读取档位,无论缓冲还是流式。

没有可报告的实际档位时,该字段省略:

  • 上游未报告档位。
  • 响应来自响应缓存,没有任何上游处理它。
  • 该次尝试没有收到响应,例如分发前被拒绝的请求或失败的尝试。
  • 来自 Responses API 上游的 /v1/responses 流在终止事件之前结束。
  • Batch 作业的用量行合并了多行,而这些行报告的档位并不全部相同。

模型定价按该字段匹配服务档位价格。控制面在展开的请求日志行中将其显示为 Service tier,并在请求导出中包含该字段。对象存储和阿里云 SLS 使用 service_tier,Datadog 和 OTLP 使用 aisix.service_tier。OTLP 属性 openai.request.service_tier 是另一个值:请求所要求的档位。1.6.x 及更早版本的网关不记录该字段。

读取延迟字段​

延迟字段会区分服务提供方耗时和调用方可见耗时:

字段范围
upstream_latency_ms一次上游尝试所花费的时间,不包括请求解析、安全护栏、路由、重试延迟和先前尝试。
upstream_ttft_ms从一次上游尝试开始到首个流式帧的时间,无论帧类型为何——包括 response.created、仅含角色的 chat 增量等元数据起始帧,与调用方侧代理的统计口径一致。非流式请求、错误和缓存命中时省略或为零。
downstream_latency_ms从收到请求到向下游投递的时间:非流式响应为完整响应,流式响应为首个 Token 或转发帧,A2A 和 /v1/audio/speech 则为整个流。它包括网关处理、重试及重试延迟和输出暂存,仅出现在最后一次尝试中。
gateway_overhead_msdownstream_latency_ms 中由 AISIX 自身花费的部分:同一时间窗口减去其中等待上游的时间。每次失败、重试或回退的尝试按其完整时长计为上游时间;胜出的尝试在流式时计到其首个流式帧,非流式时按其完整上游延迟计。重试退避、安全护栏调用(包括对外部安全护栏服务的调用)和输出暂存都计为网关时间。它只出现在携带 downstream_latency_ms 的事件上。合议模型请求不上报该字段,因为其成员在任何尝试之外并发运行。在 /v1/realtime 上,它是网关连接上游之前的时间。
upstream_tpot_ms一次上游尝试的流中,平均每个输出 Token 的时间,单位为毫秒,可带小数:(upstream_latency_ms − upstream_ttft_ms) / (输出 Token − 1)。输出 Token 包含推理 Token,与 gen_ai.usage.output_tokens 的计法一致。只出现在成功、且至少产生了两个输出 Token 的流式尝试上。

对象存储和阿里云 SLS 保留这些字段名,Datadog 和 OTLP 为其加上 aisix. 前缀,例如 aisix.gateway_overhead_ms 和 aisix.upstream_tpot_ms。1.6.0 及更早版本的网关既不记录 gateway_overhead_ms,也不记录 upstream_tpot_ms。

调用方断开时​

调用方断开后,AISIX 会停止处理该请求:丢弃正在进行的上游请求或上游流,并且不再发起新的重试或回退尝试。丢弃上游请求并不能撤销模型服务提供方已经开始的工作:对于断开前已经收到的请求,提供方仍可能完成处理并计费。

因此,被调用方放弃的请求仍会生成用量事件。这些事件的状态码为 499、error_class 为 client_disconnected 而不是 200,因此可以作为一个类别筛选。在响应头写出前被放弃的请求记录零 Token、零成本。流式传输中途断开时,记录断开前已到达的 Token;如果上游尚未上报用量,AISIX 会在能估算时在本地估算并设置 usage_estimated。这两种数字都不代表模型服务提供方实际收取的费用。统计成功请求时,请与控制台一致,把所有 2xx 的 status_code 都算作成功,以排除被放弃的请求。

合议模型请求的调用方中途离开时,已经返回的每个合议成员和评审模型调用仍会被记录。它们的事件排在前面,从该请求的下一个 attempt_index 起连续编号,之后才是 499 事件。它们的 Token 会计入完成的请求本应计入的 Token 限流,因此调用方放弃合议模型请求时,Key 的 Token 额度仍会为这些请求实际引起的上游工作而消耗。

用量日志记录了什么​

用量事件是请求的记录,而不是计费条目。只要 AISIX 能够将请求归因到某个调用方,就会为其写入用量事件,包括从未到达模型服务提供方的请求,以及没有任何可计费内容的请求(这类请求记录为零 Token)。可以用下表区分「没有记录」和「记录了一次拒绝」:

结果是否记录记录内容
成功是状态 200、解析出的目标,以及上报的消耗量。
Rerank 成功但没有 Token 数是状态 200,零 Token。例如 Cohere 的 rerank 模型上报的是搜索单元而不是 Token,AISIX 不对搜索单元定价。
模型服务提供方不支持该端点是状态 501,零 Token,没有上游调用。当解析出的模型服务提供方缺少对应能力时,AISIX 会在文本补全、Embeddings、图像生成和视频提交上自行返回该状态。
缓存命中是状态 200,回放所存响应的 Token 数量,成本为零,cache_status="hit",cache_hit_layer 为 exact 或 semantic,语义命中还带 cache_similarity。由于没有联系任何上游,它不携带 provider_request_id,也没有路由目标,但 provider_model_version 会标明产出所存响应的那个模型。只有非流式请求会被缓存,因此缓存命中与流式响应不会同时出现。
模型未找到是状态 404、error_class="model_not_found",error_message 形如 model "gpt-x" not found,model_id 为空。
被限流拒绝是状态 429、error_class="rate_limit_exceeded",error_message 给出触发的限额,例如 request limit exceeded (...);限流策略的拒绝还会附加 (policy '<name>')。调用方 Key 级、模型级和限流策略的拒绝都是这一形态,三者都在下发前完成检查。
预算耗尽是状态 429、error_class="billing_error",error_message 为返回给调用方的预算消息。
安全护栏拦截是状态 422、error_class="content_filter",guardrail_blocked 为 true,输入侧和输出侧拦截都是如此。error_message 只给出安全护栏名称或它无法评估内容的原因,从不包含命中的内容,例如 request blocked by content policy (guardrail 'pii-check') 或 response rejected: guardrail 'pii-check' could not evaluate it (<tag>)。
安全护栏在流式响应发出 200 响应头后拒绝回答是状态 200、guardrail_blocked=true、error_class="content_filter",error_message 为流结束时错误帧的文本。如果把 2xx 状态计为成功,请按 guardrail_blocked 或 error_class 排除这些记录。
上游错误或超时是每次失败的尝试各一条事件,携带该次尝试的 attempt_index、attempt_kind、attempt_model、error_class 和 error_message;服务提供方的响应带有请求 ID 响应头时,还携带该次尝试的 provider_request_id。如果后续某次尝试成功,则它的成功事件即最终事件;如果所有尝试都失败,则最后一次失败尝试自身的事件即最终事件,不会额外再写一条。
调用方在响应头写出前断开是状态 499、error_class="client_disconnected"、error_message="client closed the request before the response head was written",Token 数量和成本均为零。
响应正文在被读取前即被丢弃是状态和类别相同,error_message="client closed the request before the response body was streamed"。由于上游已经应答,事件会标明处理它的目标。
调用方在流式传输中途断开是状态和类别相同,error_message="client closed the request while the response was streaming",携带断开前已投递的 Token。
流的 200 响应头发出后上游失败是同一失败若发生在响应头之前会得到的状态,例如 502 或 504;error_class 和 error_message 取自该失败本身。参见响应头发出后失败的流。
认证失败否写一条访问日志,并计入 aisix_auth_decisions_total。该访问日志行携带状态码和 request_id,但由于尚未解析出密钥,不含 API Key ID。
请求正文解析错误或超过大小限制否只写访问日志和指标。
发往无需凭证的路由或未匹配任何路由的请求否/livez 和 /readyz 不记录任何内容。OAuth 发现路由和未匹配路径的 404 只写访问日志。
/livez、/readyz、/v1/models、/v1/videos/{id} 和 /v1/videos/{id}/content、OAuth 受保护资源发现路由,以及 /a2a/{agent}/.well-known/agent-card.json否这些路由在任何结果下都不产生用量事件。视频作业的工作量已由提交请求计量。

网关自己拒绝请求时,终态用量事件的 error_message(以及从中读取的 OTLP aisix.error_message 属性)与访问日志的 error 文本相同。这适用于所有会产生用量事件的端点,包括 /v1/realtime、/a2a 和 /mcp,覆盖文本完全由网关自己生成的拒绝:模型不存在或无权使用、IP 限制、限流或预算拒绝、安全护栏拦截、语义路由决策失败、没有可用的路由候选,以及缺少 WebSocket 升级。文本可能引用请求或上游内容的失败,其终态事件的 error_message 为空:invalid_request_error、未知的视频 ID,以及上游错误。上游错误的文本仍保留在对应的单次尝试事件上。

缓存命中是用所存响应作答的,因此它的事件描述的是调用方寻址的条目,而不是任何目标:

  • provider_model_version 标明产出所存响应的那个模型,它读自缓存条目而非本次请求,与当初那次调用自身事件上的值相同。对模型组而言,这是该条记录上唯一能指出产出方的字段。仅当所存响应本身没有携带模型名时它才为空。
  • provider_request_id 保持为空。它是客户用来与模型服务提供方控制台对账的标识,而本次请求并未到达任何模型服务提供方。
  • 在 routing 或 semantic 条目的命中上,由 Provider Key 派生的字段——provider_kind、provider_featured、branded_provider、pk_label 和 byo_label——为空,provider 上报 unknown。1.2.0 及更早版本的网关会用该请求路由策略排在首位的那个目标来填充它们,从而标注了一个从未运行过的目标,而且同一条缓存条目的两次命中还可能标注不同的目标。由此带来一个变化:按 Provider Key 标签筛选控制面的请求日志时不再返回模型组的缓存命中,而按模型服务提供方统计的请求面板会把它们计入 provider="unknown",旁边的 cache_status="hit" 说明了原因。
  • 直连模型的命中仍保留该模型自身的模型服务提供方、Provider Key 和上游模型,它们是该模型的静态属性,而不代表发生过下发。

该请求的访问日志遵循相同规则,参见解读缓存判定结果。

被取消请求的 requested_model 是调用方寻址的条目——对于路由请求即模型组名称——model_id 则是它已经确定的目标。在任何尝试落定之前,model_id 遵循记录指向哪个模型中的规则:直连模型记录自己的标识符,模型组则不记录。两种情况下 requested_model_id 都携带调用方寻址条目的标识符。被取消的请求也不携带安全护栏归因,即使某次尝试已经应答也不携带 Token 数量,因为这两者都产生于网关来不及执行的响应处理阶段。

调用方不指定模型的路由——/mcp、/a2a/{agent}、透传命名空间、升级前的 /v1/realtime,以及文件、批处理和微调相关端点——同样会写入这条 499 记录。它们的模型字段为空,由各自的归因字段代替:passthrough_route_name,mcp_server_name 与 mcp_tool_name,或 a2a_agent_name 与 a2a_method、a2a_operation。该记录仍然携带 api_key_id、调用方身份、auth_type、operation 和 inbound_protocol。

记录指向哪个模型​

requested_model 是调用方发送的名称,requested_model_id 是该名称解析到的模型的 ID。model_id 的范围更窄:它只指向自身调用上游的模型,从不指向调用方寻址的路由模型、语义路由器或合议模型。它的取值取决于调用方请求的模型类型:

请求的模型model_id
直连模型、嵌入模型或通配符模型每条记录上都是该模型自身的 ID,包括分发前的失败和响应缓存命中。通配符匹配时为该通配符模型的 ID。
路由模型或语义路由器每次尝试的记录上是该次尝试所分发到的目标模型。在文本补全、rerank、图像、音频、视频等单请求端点上,胜出尝试之后发生的失败携带胜出目标的 ID。在选定目标之前写入的记录上为空:安全护栏拦截、预算耗尽、分发前的限流拒绝、模型被拒绝访问或 IP 限制、语义路由失败、在第一次尝试之前被取消的请求,以及响应缓存命中。
合议模型请求在合议成员运行前被拒绝时为空。每个合议成员和评审模型的记录携带该模型的 ID。合议模型的汇总记录以及合议成员运行之后的失败携带合议模型自身的 ID。
未解析到模型的名称,以及 MCP、A2A 和透传调用为空。

因此,在 model_id 中按路由模型或语义路由器的 ID 过滤,找不到它从未选定目标的记录,例如安全护栏拦截、限流拒绝和缓存命中。要找到寻址到模型组的请求的全部记录,请按 requested_model_id 或 requested_model 过滤。1.6.0 及更早版本的网关并非在所有端点上都遵循这条规则:视端点而定,模型组在分发前的失败可能携带模型组自身的 ID,直连模型在分发前的失败则可能 model_id 为空。

响应头发出后失败的流​

流式响应会在上游产出答案之前就发出 200 状态行,因此之后到达的失败无法再改变客户端收到的内容。用量记录在流结束时写入,它报告的是流实际如何结束,而不是客户端响应行上的 200:

流的结束方式记录的状态
上游断开连接,或发送了 AISIX 无法解码的内容502
流中途读取上游超时504
透传路由由 Redis 支持的并发租约在流仍打开时消失用量事件记录 429、error_class="rate_limit_lease_lost" 和 error_message="distributed concurrency lease was lost"。访问日志也记录相同状态,error_kind 记录该类别,error 记录该消息。
上游在流内发送了携带 4xx 状态的错误事件该状态。Anthropic 的流内错误按 Anthropic 为该错误类型记载的状态映射,因此 rate_limit_error 记录为 429。
上游在流内发送了其他错误事件,包括 5xx 状态,以及不携带 HTTP 状态的 Responses API error 或 response.failed 事件502
桥接的 /v1/responses 上游流不含任何内容、推理、工具调用或结束原因502,error_class="stream_aborted",消息说明上游返回了空流
调用方在流结束前离开499,error_class="client_disconnected"
流正常结束200

即使客户端收到的是 200,error_class 和 error_message 描述的也是失败本身。上游失败与调用方断开同时发生时,记录的是上游失败:转发传输错误的中继会中止连接,这与调用方离开看起来完全一样。

以上规则适用于 /v1/chat/completions(包括流式合议模型的评审模型,合议成员的记录仍为 200)、原生与转换路径上的 /v1/responses 和 /v1/messages、流式 /v1/audio/transcriptions、/v1/audio/speech,以及透传路由。携带可识别信封(OpenAI Chat Completions、Completions 或 Responses,以及 Anthropic Messages)的透传路由会像类型化端点一样读取流内错误事件;raw 路由没有可识别的错误信封,只记录传输失败和读取超时。

以下结果不变:安全护栏拦截保持其自身状态,已产出内容但未带结束原因就结束的流仍为 200,正常结束的流仍为 200。安全护栏在响应头之后拒绝回答时同样保持 200,并记录 guardrail_blocked=true 和 error_class="content_filter";同一个流上如果还记录了上游失败,以这次拒绝为准。

由于这些流现在被记录为失败,控制台的成功率会相应下降,它们也会退出只统计 2xx 请求的延迟分位数。aisix_usage_events_emitted_total 上的 status 和 status_code 标签同样随之变化,请求的访问日志状态与其用量事件保持一致。成本、预算和限流不依赖状态,因此不受影响。

1.4.0 及更早版本的网关会把大多数此类流记录为 200,error_class 和 error_message 为空。在其转换路径的 /v1/messages 上,上游失败被记录为 499;上游断开的流式转录也是如此。

请求指标也按同样的结束方式记录。aisix_requests_total、aisix_proxy_requests_total、aisix_proxy_failed_requests_total 和 aisix_llm_requests_total 在流结束时对流式请求计数一次,状态取上表中的状态,因此在响应头之后失败或被放弃的流在这些指标中同样算作失败。对应的时长指标 aisix_request_duration_seconds、aisix_proxy_request_duration_seconds 和 aisix_llm_request_duration_seconds 衡量整个流,而不是到响应开始为止的时间,因此从中读出的流式分位数包含生成时间。aisix_request_e2e_latency_seconds 和 aisix_request_ttft_seconds 携带流结束时状态对应的 status_class。被输出安全护栏扣留、以单个缓冲响应体转发的 /v1/responses 流也按同样方式记录:如果上游在流内报告失败,请求指标携带其用量事件记录的失败状态。上述规则覆盖流式的 /v1/chat/completions(直连模型、模型组和合议模型)、/v1/messages、/v1/responses、流式 /v1/audio/transcriptions 与 /v1/audio/translations 转发、/v1/audio/speech、/a2a,以及透传路由。

在响应头写出前就离开的调用方仍不会出现在这些请求指标中,只有 aisix_proxy_client_cancelled_requests_total 对其计数。

由于流式请求在结束时而不是开始时计数,在一批长流进行期间,基于这些计数器的 rate() 会滞后大约一个流的时长;稳态速率不变。按请求负载扩缩容时请考虑这一点。

1.5.0 及更早版本的网关在发送响应头时记录这些请求指标(透传路由除外):流式请求计为 200,时长截止到响应开始;对于被放弃或失败的 chat、messages、responses 流,两个延迟直方图可能报告 2xx。

备注

由于这些取消记录此前并不存在,环境的请求数现在会包含被放弃的请求,升级后成功率可能下降。控制面的这两个数值都由用量事件推导而来。延迟分位数不受影响,因为它们本来就只统计成功的请求。

访问日志、用量事件与 Logs 页面​

这三种信号计量的对象不同,因此发生重试的请求产生的数量也不同:

信号每个请求的数量在哪里查看
访问日志无论结果如何,恰好一条网关的标准错误流
用量事件每次尝试一条,外加该请求的最终事件可观测性导出器以及控制面
Logs 页面记录每条用量事件一行控制面的请求日志

请通过 request_id 关联它们,调用方也会以 x-aisix-request-id 收到该值。在同一个请求内部,按 attempt_index 对各次尝试排序,并通过 attempt_kind 区分重试与故障转移。不代表任何一次尝试的最终事件,例如调用方在两次尝试之间离开时的 499 事件,或失败的合议模型请求的最终事件,取该请求已发出的最后一个 attempt_index 的下一个值,attempt_kind 为空。空的 attempt_kind 只在 attempt_index 为 0 的事件上表示 initial,initial 也从不标在之后的事件上。由于三者数量不同,拿记录条数与请求指标作比较就是在比较两种不同的单位——请先阅读区分请求计数与尝试计数。

在高于 1.2.0 的网关上,流式请求的访问日志会在流结束时与最终用量事件一同写出。两条记录会报告相同的状态,访问日志的 error_kind 和 error 分别携带事件中的 error_class 和 error_message。1.5.0 及更早版本的网关会在透传流的访问日志中将 error_kind 和 error 留空。

读取错误类别​

访问日志的 error_kind 与用量事件的 error_class 使用同一套词汇描述失败,适用于流式与非流式请求,也适用于单次尝试事件与最终事件。来自上游调用或其准备阶段的失败,以上游类别命名:

类别原因
upstream_status上游返回了非 2xx 的 HTTP 状态。
upstream_in_band上游在 HTTP 状态已为成功的流式响应中报告了错误。
upstream_decodeAISIX 无法解析上游响应。
stream_aborted上游在流中途结束了响应,或者桥接的 /v1/responses 流为空。
timeout上游调用超出了时间预算,包括连接超时。
transport与上游的连接失败。
config模型服务提供方不支持所请求的操作,或 AISIX 无法构造上游请求。
invalid_config目标的配置无法工作,例如 Provider Key 缺少 api_base,或模型缺少 model_name。
invalid_credentialsProvider Key 的凭证缺失、为空或格式不正确。

其他失败使用 OpenAI 风格错误信封中 error.type 报告的名称,例如 model_not_found、rate_limit_exceeded、billing_error、content_filter、invalid_api_key 或 permission_denied。另有两个类别描述的是请求如何结束,而不是发送给调用方的错误:client_disconnected 和 rate_limit_lease_lost。有一种失败的名称与其 error.type 不同:语义路由器在 fail 策略下无法完成路由决策时,记录为 semantic_decision_failed,而调用方收到的是 error.type api_error 和 error.code semantic_routing_failed。用量事件的 error_message 与访问日志的 error 相同,即 semantic router '<model>' could not pick a route (<reason>)。参见语义路由失败。

对于上游失败,类别与面向客户端的 error.type 不同。错误信封保留自己的拼写,因此调用方可能收到 error.type 为 upstream_error 或 transport_error,而访问日志和用量事件记录的是 upstream_status 或 transport。参见 OpenAI 风格代理错误。

1.5.0 及更早版本的网关在非流式请求的访问日志行,以及不携带尝试记录的最终用量事件(例如 embeddings、rerank 或图像请求的错误事件)上,使用面向客户端的拼写。这些记录报告 upstream_error、transport_error、upstream_decode_error、upstream_in_band_error、config_error、invalid_request_error 和 authentication_error,对应更新版本网关报告的 upstream_status、transport、upstream_decode、upstream_in_band、config、invalid_config 和 invalid_credentials。流式日志行和单次尝试事件此前已使用类别名称。在两个版本的网关同时上报期间,匹配旧值的查询或告警需要同时包含两种拼写。

用量事件如何送达控制面​

这条链路只存在于托管模式。网关在内存中对用量事件排队,由一个工作线程分批投递到控制面。队列容量为 16,384 条;队列满时,网关会丢弃当前正在发出的事件而不是让它等待,因为遥测不能拖慢请求路径。此类丢弃会增加 aisix_usage_event_drops_total{reason},reason 取值 sink_full、sink_closed 或 sink_disabled 说明原因,同时写入 usage event dropped 日志。

工作线程在累积 100 条事件或每隔 5 秒时(以先到者为准)刷新一次,这也是刚完成的请求需要几秒才会出现的原因。同一时刻只有一个批次在途,批次按顺序发出。

投递失败的批次会被重发——前提是控制面已表明它能识别自己已经存储过的批次,不会重复计数。首次重发等待 1 秒,此后每次尝试等待时间翻倍,上限 30 秒。因此,控制面的一次中断不再意味着丢失该时间窗内记录的用量。首次失败会写入 telemetry batch failed; re-sending (the control plane de-duplicates by batch id) 日志,之后成功投递的批次会写入 telemetry batch delivered after re-sending 日志。

重发是有上限的,因为到达过晚的用量记录已经进不了它所属的计费窗口:

  • 批次在其最早一条事件发生 30 分钟后被放弃——不是在首次尝试 30 分钟后。轮到某个批次时它已经超过该时限的,会被直接放弃,连一次尝试都不做。
  • 批次在控制面作出应答的 8 次连续失败之后也会被放弃。完全没有应答的失败——连接错误或超时,也就是中断所产生的情况——不计入该上限;这类失败由上面的时限来约束。
  • 应答表明该批次永远不可接受时,批次立即结束,不再重发。
  • 未表明具备去重能力的控制面,每个批次仍然只尝试一次,与重发能力出现之前完全一致。
  • 关闭时,手上的批次只做最后一次尝试,不会等待退避间隔。

被放弃的批次中,每条事件都会计入 aisix_usage_event_drops_total{reason}:send_failed(从未重发就被放弃)或 retry_budget_exhausted(超过时限,或 8 次被应答的失败),并写入带 reason、attempts 和 batch_id 的 telemetry batch failed (events dropped) 日志。这些样本只携带成员标签(user_id、user_name);model 和 Provider Key 标签为 unknown,因为工作线程持有的是事件本身,而不是发出该事件的处理器所用的标签集。

批次重发期间,新事件仍会不断进入队列,而队列是唯一承载它们的地方。因此,用量仍会在两种情况下丢失:队列被填满,此时新发出的事件按 sink_full 丢弃;以及中断时长超过上述上限。请对 aisix_usage_event_drops_total 的任何增长告警,并按 reason 拆分以区分这两种情况。

控制面也可能接受一个批次,却拒绝其中个别未通过逐字段校验的事件,例如 ID 格式错误或状态码超出 HTTP 范围。这些事件会被丢弃而不会重发,因为控制面会再次拒绝它们。1.4.0 之后版本的网关会把被拒绝的数量累加到 aisix_usage_events_rejected_total,并记录带 batch_id、count 和 rejected 的 control plane rejected usage events in an accepted telemetry batch (events dropped) 日志。该计数器没有标签,因为控制面只返回一个数量;这些事件也不计入 aisix_usage_event_drops_total。1.4.0 及更早版本的网关会把任何成功应答都视为全部送达。

因此,缺失的记录可能属于本来就不产生记录的请求(见上表),可能是在上述环节之一丢失的事件(丢弃计数器的 reason 可以区分),也可能是被控制面拒绝的事件(由 aisix_usage_events_rejected_total 统计)。

区分请求类型​

每条事件都携带 operation,用于标识匹配端点所执行的工作类型。inbound_protocol 和模型名称都无法可靠地区分对话、图像、视频或其他操作,因此按请求类型聚合流量时请使用该字段。

其取值是一个适合索引、分组和绘图的固定集合:

取值端点
chat/v1/chat/completions
messages/v1/messages
count_tokens/v1/messages/count_tokens
responses/v1/responses
completions/v1/completions
embeddings/v1/embeddings
rerank/v1/rerank
systemone/v1/systemone
decisions/v1/decisions
image_generation/v1/images/generations
image_edit/v1/images/edits
transcription/v1/audio/transcriptions
translation/v1/audio/translations
speech/v1/audio/speech
video_generationPOST /v1/videos
realtime/v1/realtime
files、batches、fine_tuning文件、批处理和微调管理端点
batch_completion网关自己对已完成批处理作业的计量,在作业完成时记录,而非在提交时记录
mcp、a2aMCP 网关和 A2A 网关
passthrough透传路由

查询该字段时,请注意以下行为:

  • 它描述的是请求,不是结果。 失败的请求、被护栏拒绝的请求,携带的取值与成功请求相同。它是这类记录上唯一能说明端点的字段。
  • 它以请求为作用域。 发生重试或故障转移的请求会为每次尝试生成一条事件,且每次尝试携带相同取值,因此按 operation 统计事件数得到的是尝试数。参见区分请求计数与尝试计数。
  • 轮询视频作业不是视频生成。 只有提交(POST /v1/videos)会产生用量事件,查询作业状态或下载结果都不会。因此 video_generation 的计数是被请求生成的视频数量,而不是围绕这些视频发出的请求数量。

由于 operation 属于元数据,导出器即使在不接收提示词内容的 metadata_only 模式下也会保留它。在阿里云 SLS 中,该字段会作为独立列写入。请为查询使用的字段启用分析,然后按操作类型聚合流量:

* | SELECT operation, COUNT(*) AS calls, SUM(prompt_tokens + completion_tokens) AS tokens GROUP BY operation ORDER BY calls DESC

要筛选某一类流量,请直接按操作过滤,例如 operation: video_generation。不要从模型推断操作类型:同一个模型可以服务多个端点,而 requested_model 标识的是配置的别名或模型组,不是请求类型。

将事件归因到成员​

每条事件使用 user_id 标识请求发生时拥有调用方 API Key 的组织成员:

情况记录行为
调用方 API Key 没有关联成员省略 user_id。所有权必须显式分配,不会从 Key 的创建者推断。
一个成员使用多个凭证事件可以有不同的 api_key_id,但共享同一个 user_id。这包括同一成员通过 API Key 和 OIDC 调用、且两者解析到不同 Key 的情况。
Key 被重新分配或删除既有事件保留原始 user_id。重新分配后,后续请求归属于新所有者;删除 Key 不会清除早期归属。
事件产生时的网关尚不支持该字段不记录成员,因此成员筛选只覆盖升级后的流量。

按 user_id 筛选可包含成员拥有的所有凭证;按 api_key_id 筛选则只查看一个凭证。

每条事件还携带 api_key_name,即请求发生时调用方 API Key 的显示名称,因此后来的重命名不会改变早期事件记录的名称。Key 没有名称时省略。对象存储和阿里云 SLS 保留 api_key_name 这个名称,Datadog 和 OTLP 以 aisix.api_key_name 携带,与 content_mode 无关。1.6.0 及更早版本的网关不记录该字段。

在 AISIX Enterprise 和 AISIX Cloud 的控制台 Logs 页面中,Member 筛选器提供该能力,旁边的 Status 筛选器可接受状态族(4xx)、具体状态码(429)或区间(500-599)。二者组合即可用一次查询回答「该成员最近 24 小时内哪些请求被限流了」这类问题。CSV 导出同时包含 user_id 和成员名称。

将事件归因到团队和标签​

每条事件还携带两个由组织控制的归属字段:

  • team_id:调用方 API Key 绑定的团队 ID。Key 没有团队绑定时省略。
  • tags:请求的成本归属标签,形式为标签键到标签值的对象。每个标签取调用方 API Key 自己的值,否则取 Key 所属团队上设置的值,再否则取 Key 所属成员上设置的值。请求没有解析出任何标签时省略。

两者都在请求运行时确定,并且在同一请求的每条事件上都相同,包括重试、故障转移和网关自己的拒绝。它们从不从请求中读取。各导出器使用以下名称携带它们,与 content_mode 无关:

导出器团队标签
OTLPaisix.team_id每个标签一个 aisix.tags.<key> 字符串属性,位于操作跨度上
Datadogaisix.team_id嵌套的 aisix.tags 对象,Datadog 将其作为 aisix.tags.<key> facet
阿里云 SLSteam_id每个标签一个 tags.<key> 字段,因此每个标签都是查询可以直接筛选的列
对象存储(NDJSON)team_idtags 对象

例如,SLS 查询可以用 * | SELECT "tags.department" AS department, SUM(prompt_tokens + completion_tokens) AS tokens GROUP BY department 按部门汇总 Token。标签和团队不会成为 Prometheus 指标标签。1.6.x 及更早版本的网关不记录这两个字段。

按请求头归因事件​

多个 Agent 或终端用户共用一个调用方 API Key 时,api_key_id 和 user_id 只能说明是哪个 Key、归属哪个成员,无法说明每个请求是为谁发起的。调用方可以在自定义请求头中携带这类归因信息,例如子用户 ID、部门或项目,可观测性导出器则可以把这些请求头的值记录到它导出的每条用量事件上。接收事件的外部系统即可据此进行成本归因。

网关对这些值只做记录。它们不影响路由、限流、预算或计费,也永远不会发送到控制面,因此控制台不会在请求日志中展示它们,也不提供按它们筛选。这些值只通过导出器送达。记录请求头也不会改变网关向上游转发的内容。

配置要记录的请求头​

在导出器的 request_headers 字段中列出请求头名称。所有导出器类型都接受该字段,每个导出器只记录自己列表中的请求头,因此两个导出器可以记录不同的请求头。省略或为空列表时不记录任何请求头。同一导出器还接受 upstream_response_headers,参见记录上游响应头。

在 AISIX Enterprise 或 AISIX Cloud 的控制台中,在导出器表单的 Request headers to record 和 Upstream response headers to record 输入框中填写以逗号分隔的名称。表单会在保存前把名称转为小写并去重。通过 AISIX Cloud Admin API,可以在创建导出器时设置这两个列表,也可以使用可观测性导出器中的变量更新已有导出器:

curl -sS -X PATCH "$AISIX_CP/environments/$ENV_ID/observability_exporters/$EXPORTER_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"request_headers": ["x-sub-user", "x-department"],
"upstream_response_headers": ["x-request-id"]
}'

PATCH 会整体替换它指定的每个列表,空列表则清空该列表。读取接口会返回这两个列表。

对于开源网关,在资源文件的导出器条目中添加这两个列表,然后校验并重新加载该文件:

resources.yaml
observability_exporters:
- name: request-events-s3
kind: object_store
provider: s3
bucket: acme-aisix-events
prefix: ai-gateway
region: us-east-1
credential_ref: acme_s3
request_headers: ["x-sub-user", "x-department"]
upstream_response_headers: ["x-request-id"]

网关加载更新后的导出器时,列表即生效,无需重启。

列表包含以下内容时,AISIX Cloud Admin API 和 aisix validate 会拒绝:

  • 名称包含小写字母、数字和 ! # $ % & ' * + . ^ _ ` | ~ - 以外的字符。请用小写书写名称;与请求头的匹配不区分大小写。
  • 携带凭证的请求头:api-key、authorization、cookie、proxy-authorization、x-amz-content-sha256、x-amz-date、x-amz-security-token、x-api-key 或 x-goog-api-key。
  • set-cookie,仅限 upstream_response_headers。
  • 同一名称出现两次。

列表长度没有限制。使用 auth_mode: header_key 的透传路由从其 auth_header_name 读取调用方的网关 Key,即使导出器列出了该请求头,该路由的请求上也永远不会记录它。

注意

不要列出携带凭证或其他机密的请求头。上述检查只拒绝列出的固定名称,其他被列出的请求头会像普通请求头一样被记录,其值会以明文导出到该导出器的目的地。

1.6.0 及更早版本的网关不支持这两个列表。连接 AISIX Enterprise 或 AISIX Cloud 时,这类网关会忽略它们:控制面仍会保存导出器,并针对这类网关返回 field_ignored 告警,控制台会在保存后提示。1.6.0 及更早版本的开源网关会拒绝设置了它们的资源文件。

从启动配置升级​

1.6.0 版本的网关在启动配置的 observability.usage_event.request_headers(AISIX_OBSERVABILITY__USAGE_EVENT__REQUEST_HEADERS)中选择要记录的请求头,并为所有导出器记录。从下一个版本起,该设置退役:设置了它的配置仍能加载,但该设置不再生效,网关会在启动时记录一条告警,说明 observability.usage_event 不起作用。

升级设置了该项的网关之前,请把它的列表复制到每个需要继续接收这些请求头的导出器的 request_headers 中。在导出器列出这些请求头之前,其记录不带任何请求头。然后从启动配置、环境变量或 Helm values 中删除该设置。

记录的值​

导出器投递的每条用量事件携带一个 request_headers 对象,键为小写请求头名称,包含该导出器列表中的请求头:

"request_headers": {"x-sub-user": "alice", "x-department": "eng, ops"}

取值遵循以下规则:

  • 请求未发送的已列出请求头没有对应的键。请求一个已列出的请求头都没有发送时,记录不带 request_headers 字段。
  • 发送了多次的请求头记录为一个值,各次出现按到达顺序以 ", " 连接。
  • 控制字符会被移除,值被截断为 256 个字符。处理后为空的值没有对应的键。
  • 同一请求的每条用量事件都携带相同的对象,与请求类型无关:模型端点、透传路由、MCP、A2A 等均如此,也包括每次导致重试或故障转移的失败尝试所产生的事件。
  • 记录与导出器的 content_mode 无关,在默认的仅元数据模式下这些请求头同样会被导出。

这些值由调用方自行声明:任何持有该 API Key 的人都可以发送任意值。它们适合用于归因,不能作为身份认证或授权边界。

记录上游响应头​

导出器的 upstream_response_headers 列出要从上游响应中记录的响应头,例如服务提供方的请求 ID。它们按每次尝试记录:每次尝试的用量事件携带该次尝试自己的上游响应头,因此发生重试或故障转移的请求会保留每次尝试的响应头。未收到响应头的尝试不携带任何响应头。

取值规则与请求头相同,放在 upstream_response_headers 对象中。/mcp 会记录请求头,但不记录上游响应头。

Batch 任务的用量​

记录 Batch 任务 Token 用量的用量事件,在某个 GET /v1/batches/{id} 请求观察到该 Batch 完成时产生。它携带的是这个请求的请求头,而不是创建该 Batch 的请求的请求头——该事件的 API Key 和成员归因也取自同一个请求。因此,如果后台轮询程序不发送归因请求头,该 Batch 的用量就不带这些值。

各导出器中的请求头与响应头​

各目的地以自己的格式呈现这些对象:

导出器字段
对象存储每条 NDJSON 记录中的嵌套 request_headers 和 upstream_response_headers 对象。
阿里云 SLSrequest_headers 和 upstream_response_headers 字段,其值为相应对象的紧凑 JSON 字符串。
Datadog嵌套属性 aisix.request_headers 和 aisix.upstream_response_headers,单个请求头以 aisix.request_headers.<name> 和 aisix.upstream_response_headers.<name> 访问。
OTLP每个请求头一个属性,遵循 OpenTelemetry HTTP 语义约定:请求的 SERVER 跨度上为 http.request.header.<name>,该次尝试的生成式 AI 操作跨度上为 http.response.header.<name>。值均为只含一个元素的字符串数组。

记录的 Token 计数​

用量事件按上游上报的原样记录 Token 计数。调用方读到的 usage 块会按其使用的协议适配,而事件保留的是上游自己的口径,因此两者可能不同。

在 /v1/chat/completions 上,调用方收到的 usage.prompt_tokens_details 和 usage.completion_tokens_details 会带上上游上报的每个计数及其值,上报为 0 的也包括在内:cached_tokens、reasoning_tokens、audio_tokens、image_tokens、text_tokens、accepted_prediction_tokens 和 rejected_prediction_tokens。上游没有上报的计数会省略,什么都没上报的明细对象也会省略。由响应缓存返回的响应只报告非零的 cached_tokens 和 reasoning_tokens。1.6.x 及更早版本的网关会丢弃每个为 0 的计数,且从不转发 text_tokens、image_tokens 和预测计数。

事件携带 total_tokens,即上游自己上报的总数,原样记录:

上游total_tokens 的来源
Vertex AI 适配器上的 GeminitotalTokenCount
OpenAI 兼容上游和 Azure OpenAIusage.total_tokens
Responses APIusage.total_tokens
Amazon Bedrock ConversetotalTokens

上游没有上报总数时省略该字段,例如 Anthropic 从不上报总数。网关从不用自己计算的和来填充它。当网关把多份用量报告合并为一个数值时(例如同一个流的多个用量帧),只有每份报告都带有总数,事件才会保留总数。

所有上报 Token 用量的端点族都会记录该字段:Chat Completions、Completions、/v1/messages、/v1/responses、决策端点、Embeddings、Rerank、音频转录与翻译、图像生成与编辑、Realtime、已完成批次的归因,以及按其中某一族计量的透传路由。Realtime 会话事件,以及已完成批次中某个模型的事件(operation 为 batch_completion),记录其各上游响应上报总数之和;只要有一个计入 Token 的响应没有上报总数,就省略该字段。Amazon Bedrock 和 Vertex AI 的 Embeddings 不上报总数,因此其事件省略该字段。不计 Token 的事件,例如 Count Tokens、视频提交和 /v1/audio/speech,不带该字段。

该字段的覆盖范围取决于网关版本:

  • 1.7.0 及更高版本的网关在上述所有端点族上记录。
  • 1.5.0 至 1.6.x 版本的网关只在 Chat Completions、Completions、/v1/messages、/v1/responses 和透传路由上记录,其 Embeddings、Rerank、音频、图像、Realtime 和批次事件省略该字段。
  • 1.4.0 及更早版本的网关从不记录该字段。

各计数之间不做校正,因此 reasoning_tokens 可能大于 completion_tokens,cached_prompt_tokens 也可能大于 prompt_tokens。控制面如何依据 total_tokens 区分两种推理计数口径,请参阅推理 Token 如何计费。

对于 Vertex AI 适配器上的 Gemini 思考模型,Gemini 会把思考 Token(thoughtsTokenCount)报告在回答 Token(candidatesTokenCount)之外。只要提示词 + 补全 + 推理 Token 之和等于 totalTokenCount,事件就把 completion_tokens 记为回答 Token,reasoning_tokens 记为思考 Token,total_tokens 记为 totalTokenCount。如果 Gemini 没有上报总数,或上报的总数与这些计数对不上,事件会像 1.4.0 及更早版本的网关那样,把思考 Token 计入 completion_tokens。无论哪种情况,调用方的响应都不变:OpenAI 形态的响应仍把思考 Token 计入 completion_tokens,Token 限流和 Prometheus Token 指标也是如此。详见 Google Vertex AI。

在 1.4.0 之后版本的网关上,/v1/messages 和 /v1/completions 会与 /v1/chat/completions 完全一样地记录 reasoning_tokens 和 total_tokens,因此同一次上游调用无论经由这些端点中的哪一个发起,记录的计数都相同。1.4.0 及更早版本的网关在这两个端点上不记录推理计数。

导出器收到的就是这些记录下来的计数。对象存储和阿里云 SLS 原样携带事件,包括 total_tokens。OTLP 和 Datadog 则按 OpenTelemetry 约定报告 gen_ai.usage.input_tokens 和 gen_ai.usage.output_tokens:输入包含缓存 Token,输出包含推理。详见导出遥测中的 Token 计数。

按有效期拆分的缓存创建​

1 小时提示词缓存写入比 5 分钟写入更贵,因此事件可以按缓存有效期拆分 cache_creation_tokens:

字段含义
cache_creation_5m_tokenscache_creation_tokens 中以 5 分钟有效期写入的部分。
cache_creation_1h_tokenscache_creation_tokens 中以 1 小时有效期写入的部分。

拆分数据来自 Anthropic 的 usage.cache_creation.ephemeral_5m_input_tokens 和 ephemeral_1h_input_tokens,覆盖 Anthropic API、Amazon Bedrock 和 Vertex AI 上 Claude 的原生 Messages API、到 Claude 的 Chat 和 Responses 转换,以及按 Anthropic 计量的透传路由;也来自 Bedrock Converse 的 usage.cacheDetails(按 ttl)。

这两个字段同时出现,且只在二者之和等于非零的 cache_creation_tokens 时出现。上游未报告拆分、拆分没有覆盖全部写入,或未缓存请求的全零拆分,都不会带这两个字段;网关从不推算拆分。无论哪种情况,cache_creation_tokens 仍是总数,缓存命中不携带拆分。

对象存储、阿里云 SLS 和控制面沿用这两个字段名。Datadog 和 OTLP 将其报告为 aisix.cache_creation_5m_tokens 和 aisix.cache_creation_1h_tokens,gen_ai.usage.cache_creation.input_tokens 仍是总数。AISIX Enterprise 和 AISIX Cloud 会保存拆分,并单独为 1 小时部分计费:价格设置了 1 小时缓存写入费率时按该费率,否则对 Anthropic、Amazon Bedrock 或 Google Vertex AI 上的 Claude 模型按当前生效输入费率的 2 倍,其他模型按缓存写入费率。参见模型定价。1.6.x 及更早版本的网关不记录拆分,因此控制面将其整笔写入按缓存写入费率计费。

音频与图像 Token​

事件可以携带上游上报的音频和图像 Token 计数:prompt_audio_tokens、prompt_image_tokens、completion_audio_tokens 和 completion_image_tokens。每个计数都是 prompt_tokens 或 completion_tokens 的一部分(包括缓存部分),不会再加到它们上面。上游未上报的计数会被省略;上报为 0 的保持为零。

上游用量来源
OpenAI 格式的聊天补全,包括旧版 completions 和 Batch 输出prompt_tokens_details 和 completion_tokens_details
Responses API,包括 /v1/decisionsinput_tokens_details 和 output_tokens_details
Realtime,以及按 Token 计费的音频转录input_token_details 和 output_token_details
图片生成和图片编辑input_tokens_details 和 output_tokens_details
Vertex AI 适配器上的 Gemini按模态拆分的 promptTokensDetails 和 candidatesTokensDetails

带有可识别信封的透传路由读取同样的字段。Anthropic 和 Amazon Bedrock Converse 不上报这些计数。缓存命中不携带这些计数,因为它们不随缓存的响应一起存储。1.6.0 及更早版本的网关不记录这些字段。

对象存储和阿里云 SLS 保留这些字段名,Datadog 为其加上 aisix. 前缀,OTLP 则以 gen_ai.usage.audio.input_tokens、gen_ai.usage.image.input_tokens、gen_ai.usage.audio.output_tokens 和 gen_ai.usage.image.output_tokens 报告。

OpenAI 缓存写入 Token​

AISIX 可以识别 Chat Completions usage.prompt_tokens_details 或 Responses usage.input_tokens_details 中的 cache_write_tokens。它会将该原始值保留为用量事件的可选 cache_write_tokens 字段,包括通过 /v1/messages 或 /v1/responses 桥接的请求,以及支持协议感知的透传路由。此功能要求网关版本高于 1.1.0。

用量事件字段
{
"prompt_tokens": 101,
"completion_tokens": 11,
"cached_prompt_tokens": 19,
"cache_write_tokens": 37
}

上游未提供时省略此字段;明确返回 0 时保留零值。控制面的请求日志详情、用量事件 API 和 JSON/CSV 导出均保留这一区别,CSV 用空单元格表示缺失值。对于使用字段前缀的日志后端,沿用其原有前缀,例如 Datadog 中为 aisix.cache_write_tokens。

此字段与 Anthropic 的可累加字段 cache_creation_tokens 相互独立,不增加输入/输出 Token 总量,也不改变现有计费计算。上例的输入加输出仍为 112。现有缓存计数器不会因此重命名或合并。

下一步​

配置可观测性导出器,把用量事件发送到外部收集器、日志目标、对象存储或数据仓库工作流。使用访问日志与请求关联调查单个请求;构建 Prometheus 仪表板或告警时,请使用指标参考。