Skip to content

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

Open
AlinsRan wants to merge 4 commits into
apache:masterfrom
AlinsRan:fix/ai-proxy-error-latency-vars
Open

fix(ai-proxy): report error-path latency vars in milliseconds#13711
AlinsRan wants to merge 4 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 3 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.
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

1 participant