feat(ai-proxy): ingest external usage events for API-equivalent cost tracking
メンテナーはふだん 2 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 35/100
- issue の種類
- 機能追加
- 明瞭さ
- おおむね明確
- 活発さ
- 静か
- 技術スタック
- lua, prometheus
- 領域
- ai, api, observability-sre
調査の方向性
まず apisix/plugins/ai-providers/base.lua、特に extract_usage と merge_usage を読み、次に既存の apisix/plugins/opentelemetry.lua exporter パスを public-api および logger プラグインのメカニズムと比較します。llm_* ctx.var フィールドが Prometheus と logger シンクに到達するまでを追跡します。認証済みの OTLP メトリクスレシーバーがメタデータのみの外部イベントをこのパイプラインに正規化し、インポートされたイベントをレート制限および予算パスから除外できれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Drafted by Claude through back-and-forth with @arthuursantos. The direction and prioritization are his; the prose is Claude's.
Description
As an operator running APISIX as an AI gateway, I want APISIX to ingest the OpenTelemetry usage metrics emitted by agents that do not flow through the proxy (e.g. Claude Pro/Max/Team subscription, which authenticates directly against Anthropic), so that subscription-backed token/cost usage lands in the same metrics and logging pipeline as the traffic ai-proxy already meters — giving me one gateway-owned view of total LLM consumption instead of two disconnected ones.
Scope note: this is about unifying accounting, not about gaining visibility. Visibility already exists today (see Alternatives considered). The gap this closes is having subscription usage and proxied usage in one pipeline, under one set of labels.
Problem
Today ai-proxy meters token usage only for requests it proxies: usage is extracted from the upstream response on the wire (extract_usage / merge_usage in apisix/plugins/ai-providers/base.lua) and surfaced via the normalized set of ctx.var fields — llm_prompt_tokens, llm_completion_tokens, llm_total_tokens, llm_cache_read_input_tokens, llm_cache_creation_input_tokens, llm_reasoning_tokens — which then feed the Prometheus LLM metrics and the logger plugins.
Subscription-backed agents (Claude Code with a Pro/Max/Team plan, IDE assistants, etc.) call the provider directly and never traverse the gateway, so their consumption is invisible to APISIX. Operators running a mixed fleet (some traffic via ai-proxy API keys, some via subscriptions) have no single gateway-owned place that reflects total LLM usage or lets them normalize subscription usage against API pricing under the same labels.
Source of the data
Per the Claude observability docs, the Claude Code CLI emits three independent OTLP signals, each with its own exporter:
- Metrics (
OTEL_METRICS_EXPORTER) — "Counters for tokens, cost, sessions, lines of code, and tool decisions." - Log events (
OTEL_LOGS_EXPORTER) — structured records per prompt / API request / API error / tool result. - Traces (
OTEL_TRACES_EXPORTER, beta) — spans;claude_code.llm_requestcarries token counts as attributes.
It exports over OTLP HTTP (OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf, default port 4318), enabled by CLAUDE_CODE_ENABLE_TELEMETRY=1. By default only structural/numeric data is exported; prompt/response content is opt-in (OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_*).
The relevant signal for usage accounting is therefore metrics, not traces.
Prior art
Mozilla AI's Otari implemented this in mozilla-ai/otari#378: a master-key-protected POST /v1/usage/external-events endpoint that ingests normalized usage metadata from local agents/collectors (mapping Claude Code's OTel export — request_id → source_event_id, cache-token accounting, etc.). Key constraints there, which align with Claude Code's default export posture:
- accepts only metadata and numeric usage fields — never prompts, completions, tool input, or tool output;
- events are idempotent via a unique
(source, source_event_id); - imported events do not trigger reservation/reconciliation/refund/spend-mutation and do not affect budget limits.
Proposed solution (APISIX-idiomatic)
Otari persists events into a usage_logs table because it is a DB-backed application. APISIX is a stateless streaming gateway with no usage store, so the natural adaptation is: receive the usage event, normalize it, and inject it into the telemetry path ai-proxy already owns — no new storage layer.
- Ingestion endpoint. Expose an authenticated receiver (likely via the
public-apiplugin mechanism) that accepts external usage events. Auth on par with other sensitive endpoints (dedicated key / consumer). - Wire format = OTLP metrics (
http/protobuf). This is exactly what Claude Code emits, so an operator can pointOTEL_EXPORTER_OTLP_ENDPOINTat APISIX with no intermediate transform. (An optional compact JSON schema could be a secondary input — see open questions.) - Map to the existing normalized model. Translate the token/cost counters onto the same
llm_*ctx.varfields listed above. - Emit into existing sinks. Re-emit through the current Prometheus LLM metrics and logger plugins (
http-logger,loki-logger, etc.), tagged with asourcelabel (e.g.claude-code) so proxied vs. imported usage is distinguishable in the same dashboards. - Safety constraints (mirroring Otari): imported events are accounting-only and must be inert with respect to
ai-rate-limitingreservation/budget paths; content must never be accepted or persisted.
Scope / non-goals
- Not routing subscription traffic through the gateway or handling subscription OAuth tokens — separate concern, and constrained by provider ToS.
- Not a persistent usage database inside APISIX — events flow to the existing observability sinks.
- Not ingesting request/response content — usage metadata/counters only.
- Not consuming the
tracesor genericlogssignals — themetricssignal is the target.
Alternatives considered
- Existing
opentelemetryplugin (apisix/plugins/opentelemetry.lua). Does not address this. It is an OTLP exporter of the traces signal (opentelemetry.trace.exporter.otlp,batch_span_processor,tracer_provider), producing spans for requests that traverse an APISIX route and shipping them outbound to a collector. Wrong direction (export, not ingest), wrong signal (traces, not the metrics counters that carry usage/cost), and no data source for agents that bypass the gateway. - Standalone OpenTelemetry Collector. Because Claude Code speaks standard OTLP, an operator can point its metrics exporter at a standalone collector that forwards to Prometheus/Datadog/Grafana today — no APISIX change needed. This fully solves visibility. What it does not do is unify that usage with
ai-proxy's ownllm_*metrics under one gateway-owned pipeline and label set. This feature exists specifically to close that unification gap; operators who only need visibility should use a standalone collector instead.
References
- Prior art: mozilla-ai/otari#378
- Source telemetry: Claude — Observability with OpenTelemetry
- 主要言語
- Lua
- スター
- 17.3k
- フォーク
- 3k
- 平均マージ
- 4日 14時間
- マージ済み PR(30日)
- 73
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
apache/apisix のほかの issue
-
bug: an admin key injected with ${{VAR}} that contains only digits becomes a number, so the correct key gets 401対応中かも @Ymmcc が 1 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
bug(body-transformer): runtime crash when request or response body is a JSON primitive対応中かも @bhuvan-somisetty が 6 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
bug: openapi-to-mcp appends a second ? to a query-suffixed path template対応中かも @lordcheng10 が 5 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 83/100
メンテナーはふだん 2 日以内に返信
-
ai-proxy forwards client `Transfer-Encoding: chunked` but sends an unchunked body (upstream 400)対応中かも @locker95 が 4 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 2 日以内に返信
-
As a user, I want the opa plugin to propagate trace context (traceparent) to OPA, so that OPA spans are linked to the APISIX request trace対応中かも @GitNimay が 21 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
apache/apisix#14051 · コメント 3 件 ·
メンテナーはふだん 2 日以内に返信
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
-
severity: low
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
luainkernel/lunatik#2460 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 82/100
nvim-neo-tree/neo-tree.nvim#2121 ·
メンテナーはふだん 5 日以内に返信
-
bug discord guide:04-dustwallow-marsh locale:enUS lua-error triage v0.3.6
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
メンテナーはふだん 1 日以内に返信