Skip to content

fix(ai-proxy): report error-path latency vars in milliseconds#13711

Merged
AlinsRan merged 6 commits into
apache:masterfrom
AlinsRan:fix/ai-proxy-error-latency-vars
Jul 22, 2026
Merged

fix(ai-proxy): report error-path latency vars in milliseconds#13711
AlinsRan merged 6 commits into
apache:masterfrom
AlinsRan:fix/ai-proxy-error-latency-vars

Conversation

@AlinsRan

Copy link
Copy Markdown
Contributor

Description

Fixes #13698

$apisix_upstream_response_time and $llm_time_to_first_token changed unit depending on the upstream status code.

The AI latency vars are only assigned on the success path (ai-providers/base.lua), in milliseconds. The 429/5xx early exit in ai-proxy/base.lua returns without setting them, so the log-phase fallback in init.lua fills $apisix_upstream_response_time from nginx $upstream_response_time — which is in seconds. For the same 0.2s upstream latency:

status=200 apisix_upstream_response_time=201    llm_time_to_first_token=201
status=500 apisix_upstream_response_time=0.200  llm_time_to_first_token=0

A 1000x difference in the same log field, which breaks latency aggregation and alert rules.

This assigns both vars on the early-exit paths using the same clock source the success path uses (ctx.llm_request_start_time), so an error response is comparable with a served one:

Path apisix_upstream_response_time llm_time_to_first_token
429 / 5xx from upstream ms ms (time until the upstream answered)
upstream responded with no body ms ms
transport error (connect/timeout) ms stays 0 — no first token ever arrived
request never sent (400) unset unset
internal error in pcall(do_request) unset unset

init.lua's fallback is deliberately left alone: it also serves non-AI routes, where mirroring nginx $upstream_response_time in seconds is the existing documented behaviour. After this change the fallback is simply unreachable for AI routes that reached the upstream.

Because llm_time_to_first_token now carries a real value on errors, the prometheus llm_latency observation needed a status < 400 guard — that histogram has no status label, and letting fast 429 rejections in would skew the percentiles used for capacity planning. One small side effect of the guard: a request where the upstream returned 200 but the body could not be parsed (plugin ends up returning 500) is now excluded from llm_latency; that sample was anomalous to begin with.

Behaviour change to note in the changelog: on 429/5xx, $apisix_upstream_response_time goes from a seconds decimal to a milliseconds integer, and $llm_time_to_first_token from 0 to a real value. That is the unification the issue asks for, but anyone who wrote a dashboard specifically around the old error-path unit will need to adjust.

Tests

New t/plugin/ai-proxy-latency-vars.t covers 200 / 500 / 429 / transport error, plus a case where a 200 and a 500 happen in one nginx lifecycle. Verified it is discriminating: on master 4 of its subtests fail (the 500, 429, mixed-lifecycle and transport-error cases) and pass with this change, while the 200 case passes either way, pinning the success path against regression.

Existing t/plugin/ai-proxy*.t, t/plugin/ai-proxy-multi*.t and t/plugin/prometheus-ai-proxy.t were run against this branch with no new failures.

The AI latency vars were only assigned on the success path, so a 429/5xx
early exit left them empty and the log phase filled
$apisix_upstream_response_time from nginx $upstream_response_time, which
is in seconds. The same 0.2s upstream latency was therefore logged as
`201` for a 200 and `0.200` for a 500, and $llm_time_to_first_token was
stuck at 0 for every non-200.

Assign both vars on the early-exit paths from the same clock the success
path uses, so error responses are comparable with served ones. Requests
that never reached the upstream stay unset, and $llm_time_to_first_token
stays 0 on transport errors, where no first token ever arrived.

Since llm_time_to_first_token now carries a real value on errors, guard
the prometheus llm_latency observation with a status check: that
histogram has no status label and must keep measuring served responses.

Fixes apache#13698
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Jul 20, 2026
AlinsRan added 5 commits July 20, 2026 14:05
Assert that a 200, a 500 and a 429 through the same route produce exactly
one llm_latency observation. Without the guard the count is 3.
The counters are buffered per worker and only reach the shared dict on
the sync interval, so a 0.5s sleep raced with it and the metric came back
missing in CI.
The poll for the prometheus counter can outlast the default 5s block
timeout, so the block died on a client socket timeout rather than on its
assertion.
The metric read depends on the prometheus counter reaching the shared
dict, which needs the HUP + fixture harness that prometheus-ai-proxy.t
uses; a bare mock does not flush reliably on the upstream CI, so the
assertion failed there while passing locally. The latency-var behavior
this file is about is fully covered by the log-phase cases; the exporter
status guard was verified separately by fail-on-old.
The status < 400 guard added to the llm_latency observation had no CI
coverage. Add a case that sends a served 200 and a 429 through the same
route and asserts the observation count stays 1, not 2. Verified it is
discriminating: it fails when the guard is removed.

@membphis membphis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@AlinsRan
AlinsRan merged commit f9f4a70 into apache:master Jul 22, 2026
20 of 21 checks passed
@AlinsRan
AlinsRan deleted the fix/ai-proxy-error-latency-vars branch July 22, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: apisix_upstream_response_time and llm_time_to_first_token have inconsistent time units based on HTTP status code

4 participants