Skip to content

fix(obs): emit the detailed request metrics on every endpoint, not just chat and messages - #888

Merged
jarvis9443 merged 6 commits into
mainfrom
fix/llm-request-metrics-endpoint-coverage
Aug 4, 2026
Merged

fix(obs): emit the detailed request metrics on every endpoint, not just chat and messages#888
jarvis9443 merged 6 commits into
mainfrom
fix/llm-request-metrics-endpoint-coverage

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

Fixes api7/AISIX-Cloud#1234

Problem

aisix_llm_requests_total / aisix_proxy_requests_total / aisix_proxy_failed_requests_total and their duration histograms were emitted by the chat and messages handlers only. Every other endpoint recorded just the legacy aisix_requests_total.

So /v1/responses traffic (Codex and friends) never appeared in any request-count or success-rate query built on the detailed families — while still showing up in the legacy series, which made the gap read as a bad query rather than missing instrumentation.

The reported endpoint was /v1/responses, but it is the whole handler family: /v1/completions, /v1/embeddings, /v1/rerank, /v1/messages/count_tokens, the three /v1/audio/* routes, /v1/images/generations and /v1/videos* had the identical gap, as did the non-inference surfaces (/mcp, /a2a, /v1/realtime, passthrough, files/batches/fine-tuning) and the pre-dispatch rejections.

Metrics::record_proxy_request — the proxy-only variant, no LLM series — had no call sites at all, which is the tell that the two-tier design was intended from the start and simply never wired past chat and messages.

Implementation

Every handler now emits through one chokepoint, request_metrics::record, which writes the legacy series and the detailed families from a single call. Calling Metrics::record_request directly is what produced a request present in one family and absent from the others, so nothing does that any more (outside the admin API, which is not proxy traffic).

The LLM-vs-proxy split is a property of the route (LLM_ENDPOINTS), not of the call site. That matters for correctness, not just tidiness: a 413 refused before dispatch now lands in the same denominator as the model-not-found the handler itself records, instead of the two disagreeing about whether the endpoint had a failure.

Non-inference surfaces get the proxy families only — counting an MCP tool call, a batch-file upload or a 413 as an LLM request would corrupt every per-request token and cost average. /v1/realtime stays out for the same reason: it does reach a model but feeds none of the aisix_llm_*_tokens_total families, so counting it would inflate the denominator without contributing tokens.

chat and messages move onto the same helper so the family cannot drift apart again. Their emitted labels are unchanged.

Fixed alongside

All in the code paths this change touches:

  • normalize_endpoint_label was missing /v1/videos*, so all video traffic reported endpoint="other" on the in-flight gauge.
  • The passthrough error path used the caller-supplied :provider path segment verbatim as the provider label, so /passthrough/<random>/x minted one series per random value — security: prevent unauthenticated metric-label cardinality DoS #451's unbounded-cardinality hole on the provider axis. It now collapses to unresolved unless a configured model uses that provider. (The AisixPath rejection path passes the raw parts.uri.path(), so normalizing the endpoint label in reject is load-bearing too, not just defensive.)
  • /v1/embeddings and /v1/images/generations hardcoded status = 200 on a success arm that also carries the 501 NotImplemented response — mislabelling it in the access log and booking it as outcome="success". Same fix feat(completions): emit UsageEvent on /v1/completions 200 (#403) #426 already made for completions/responses/rerank.
  • /v1/responses and the rest of the family now report a real upstream_model label instead of leaving it unresolved, matching chat and messages.

Behavior change

New series appear for the previously-missing endpoints. Existing chat/messages series are byte-identical. Dashboards that group aisix_proxy_requests_total by endpoint will now see the tool/management/tunnel surfaces too.

Baseline

LiteLLM increments litellm_proxy_total_requests_metric / litellm_proxy_failed_requests_metric from a single central callback (async_log_success_event + async_post_call_failure_hook) carrying a route label, so all routes are covered uniformly by construction. This change converges on that shape; no divergence.

Tests

tests/e2e/src/cases/request-metrics-endpoint-coverage-e2e.test.ts — verified failing before the fix and passing after:

  • /v1/responses reaches aisix_llm_requests_total (empty before), with the detailed labels actually populated (upstream_model="gpt-4o-mini", not unknown).
  • a failed /v1/responses lands in the same denominator, with model="unresolved" rather than the caller's text.
  • passthrough is counted as a proxy request but never an LLM one, and the bogus provider name does not reach a label.

Rust unit tests in request_metrics pin the classification: no route falls through to "other", no LLM_ENDPOINTS entry is unreachable (a typo there fails silently — the endpoint just stops appearing), and the tier split holds for both sides.

Full DP E2E suite passes (483 tests).

…st chat and messages

`aisix_proxy_requests_total`, `aisix_proxy_failed_requests_total`,
`aisix_llm_requests_total` and their duration histograms were emitted by
the chat and messages handlers only. Every other endpoint recorded just
the legacy `aisix_requests_total`, so `/v1/responses` traffic (Codex and
friends) was absent from every request-count and success-rate query built
on the detailed families while still appearing in the legacy one — which
made the gap read as a bad query rather than missing instrumentation.

Ten endpoints were affected: /v1/responses, /v1/completions,
/v1/embeddings, /v1/rerank, /v1/messages/count_tokens, the three audio
routes, /v1/images/generations and /v1/videos, plus the non-inference
surfaces (/mcp, /a2a, /v1/realtime, passthrough, files/batches/
fine-tuning) and the pre-dispatch rejections.

All of them now emit through one chokepoint, `request_metrics::record`,
which writes the legacy series and the detailed families together. The
LLM-vs-proxy split is a property of the route (`LLM_ENDPOINTS`), not of
the call site, so a request lands in the same families however it ended:
a 413 refused before dispatch now sits in the same denominator as the
model-not-found the handler itself records. Tool calls, the opaque
passthrough tunnel and the management routes stay out of the LLM
families — counting them there would corrupt every per-request token and
cost average. `/v1/realtime` stays out for the same reason: it feeds none
of the `aisix_llm_*_tokens_total` families.

chat and messages move onto the same helper so the family cannot drift
apart again; their emitted labels are unchanged.

Fixed alongside, all in the paths this touches:

- `normalize_endpoint_label` was missing `/v1/videos*`, so video traffic
  reported `endpoint="other"`.
- The passthrough error path used the caller-supplied `:provider` path
  segment verbatim as the `provider` label, letting
  `/passthrough/<random>/x` mint unbounded series (#451 on the provider
  axis). It now collapses to `unresolved` unless a configured model uses
  that provider.
- `/v1/embeddings` and `/v1/images/generations` hardcoded `status = 200`
  on a success arm that also carries the 501 NotImplemented response,
  mislabelling it in the access log and booking it as
  `outcome="success"`. Same fix #426 made for completions/responses/rerank.
- `/v1/responses` and the rest of the family now report a real
  `upstream_model` label instead of leaving it unresolved.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2802818-8729-47d2-bebe-6bc57f548889

📥 Commits

Reviewing files that changed from the base of the PR and between 17141a6 and 2d93bae.

📒 Files selected for processing (20)
  • crates/aisix-proxy/AGENTS.md
  • crates/aisix-proxy/src/a2a.rs
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/jobs.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/passthrough.rs
  • crates/aisix-proxy/src/realtime.rs
  • crates/aisix-proxy/src/reject.rs
  • crates/aisix-proxy/src/request_metrics.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/responses.rs
  • crates/aisix-proxy/src/videos.rs
  • tests/e2e/src/cases/request-metrics-endpoint-coverage-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

…ared emit

#887 landed a new `record_request` call for non-WebSocket requests to
/v1/realtime while this branch was open — exactly the drift the shared
chokepoint exists to prevent. Route it through `request_metrics::record`
so the refusal reaches the detailed proxy families like every other
pre-dispatch rejection.
The struct now carries the `AuthenticatedKey` for the caller labels, which
already owns the key id.
@jarvis9443 jarvis9443 closed this Aug 4, 2026
@jarvis9443 jarvis9443 reopened this Aug 4, 2026
GitHub did not dispatch a workflow run for 513b1ea (no check-runs on the
commit, and close/reopen did not re-fire it).
Conflict in chat.rs' error tail, resolved by deleting both sides:

- #886 moved rate-limit rejection counting to `quota::reject`, which every
  endpoint funnels through and which knows the offending layer, and removed
  it from chat's `record_error` to avoid double-booking. Keeping this
  branch's `note_ratelimit_rejection` would have reintroduced exactly that
  double count.
- `record_error`'s remaining job — the legacy `record_request` — is now done
  by `request_metrics::record` further down the same arm.

So neither helper has anything left to do.
@jarvis9443

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jarvis9443
jarvis9443 merged commit 2f0ae68 into main Aug 4, 2026
12 checks passed
@jarvis9443
jarvis9443 deleted the fix/llm-request-metrics-endpoint-coverage branch August 4, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant