fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(telemetry): emit a UsageEvent for failed non-chat requests - #650

Merged
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events
Jun 25, 2026
Merged

fix(telemetry): emit a UsageEvent for failed non-chat requests#650
jarvis9443 merged 2 commits into
mainfrom
fix/non-chat-error-usage-events

Conversation

@jarvis9443

@jarvis9443jarvis9443 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Problem

chat / messages / responses emit a zero-token UsageEvent on a failed attempt (#655), so failures appear in the dashboard Logs and the budget ledger. The single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank, /v1/audio/*, /v1/images/generations — dropped the event entirely on the error path. A failed request to those endpoints was invisible in Logs and the ledger (only the access log + a metrics counter recorded it).

Fix

Add a shared usage_attr::emit_error_usage_event helper and call it from each handler's error arm: one zero-token event carrying status_code, a bounded error_class (ProxyError::kind), the requested model name, api_key, and client IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch on the error path) — requested_model + status_code + error_class are enough to surface the row. The 501 NotImplemented path still emits nothing (no upstream call), unchanged.

Tests

Two existing tests pinned the old "no event on 5xx" behavior (completions, rerank) — updated to assert the new zero-token error event (status, zero tokens, non-empty error_class, exactly one event). New equivalent tests added for embeddings, images, and audio/speech. Full aisix-proxy suite green (487), clippy + fmt clean.

Origin: cross-API consistency audit after AISIX-Cloud#867.

Summary by CodeRabbit

  • New Features
    • Failed requests across audio, completions, embeddings, images, and rerank now generate zero-token usage records for better visibility in logs and analytics.
  • Bug Fixes
    • Error responses are now consistently attributed with status codes, error classification, API key, and request context.
    • Upstream 5xx failures now surface as tracked events instead of being silently omitted.

chat / messages / responses emit a zero-token UsageEvent on a failed attempt
(#655), so failures show up in the dashboard Logs and the budget ledger. The
single-attempt non-chat handlers — /v1/completions, /v1/embeddings, /v1/rerank,
/v1/audio/* and /v1/images/generations — dropped the event entirely on the error
path, so a failed request was invisible: it appeared in neither Logs nor the
ledger, only in metrics + the access log.
Add a shared `usage_attr::emit_error_usage_event` helper and call it from each
handler's error arm: one zero-token event carrying status_code, a bounded
error_class (ProxyError::kind), the requested model name, api_key, and client
IP/UA. model_id is left empty (the resolved id isn't threaded out of dispatch
on the error path) — requested_model + status + error_class are enough to
surface the row. The 501 NotImplemented path still emits nothing (no upstream
call), unchanged.
Two existing tests pinned the old "no event on 5xx" behavior (completions,
rerank) — updated to assert the new zero-token error event. New equivalent
tests added for embeddings, images and audio. Full aisix-proxy suite green
(487), clippy + fmt clean.
Origin: cross-API consistency audit after AISIX-Cloud#867.
@coderabbitai

coderabbitaiBot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The proxy now emits zero-token error UsageEvents for failed /v1/completions, /v1/embeddings, /v1/images/generations, /v1/rerank, and /v1/audio/* requests. Shared telemetry helpers in usage_attr.rs build and dispatch these events, and tests now assert the new emissions.

Changes

Error UsageEvent emission

Layer / File(s)Summary
Shared telemetry helpers
crates/aisix-proxy/src/usage_attr.rs
usage_attr.rs adds provider-key telemetry tag resolution, tag application, and emit_error_usage_event, which builds zero-token error UsageEvents and sends them to the usage sink and OTLP exporters.
Non-audio handler wiring
crates/aisix-proxy/src/completions.rs, crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/images.rs, crates/aisix-proxy/src/rerank.rs
These handlers now emit error UsageEvents on upstream failures, and their 5xx tests assert one zero-token event with mapped status, request attribution, and a non-empty error class.
Audio handler wiring
crates/aisix-proxy/src/audio.rs
The transcription and translation failure paths pass an empty requested_model, speech uses the resolved model_name, and the speech 5xx test asserts the single zero-token event and its fields.

Sequence Diagram(s)

sequenceDiagram
participant completions
participant emit_error_usage_event
participant ProxyState
participant usage_sink
participant exporters
completions->>emit_error_usage_event: upstream failure metadata
emit_error_usage_event->>usage_sink: emit zero-token UsageEvent
emit_error_usage_event->>ProxyState: load current snapshot
ProxyState-->>emit_error_usage_event: exporters
emit_error_usage_event->>exporters: fan out UsageEvent
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • api7/aisix#646: Updates /v1/responses telemetry attribution with provider-key tags, which uses the same usage_attr plumbing touched here.
  • api7/aisix#647: Modifies the shared non-chat usage telemetry flow in usage_attr.rs, closely matching this PR’s new error-path UsageEvent emission.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
E2e Test Quality Review⚠️ WarningOnly in-process tokio/MockServer tests were added; no API→service→DB/external end-to-end coverage, so the E2E requirement is unmet.Add at least one true E2E test that exercises the public API through the full stack and verifies the emitted usage row/event in the real sink or ledger.
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: emitting UsageEvents for failed non-chat requests.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
Security Check✅ PassedNo security issues found: the new error telemetry uses sanitized client context and bounded error_class, with no raw secrets/headers/body logged.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/non-chat-error-usage-events

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

…age-events
# Conflicts:
#	crates/aisix-proxy/src/audio.rs

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aisix-proxy/src/usage_attr.rs (1)

66-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider recording latency_ms on the error event.

Every caller already computes elapsed before invoking this helper (e.g. completions.rs Line 130, audio.rs Line 287), but the emitted error event leaves latency_ms at its Default of 0. For a feature whose purpose is surfacing failed requests in Logs, time-to-failure is useful signal (distinguishes a fast 4xx reject from a slow upstream timeout that mapped to 502). Threading an elapsed: Duration param through here and the six call sites is mechanical.

♻️ Sketch
 pub(crate) fn emit_error_usage_event(
state: &ProxyState,
label: &'static str,
request_id: &str,
requested_model: &str,
api_key_id: &str,
status_code: u16,
error_class: &str,
+ elapsed: std::time::Duration,
client: &ClientContext,
) {
let event = UsageEvent {
request_id: request_id.to_string(),
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
api_key_id: api_key_id.to_string(),
requested_model: requested_model.to_string(),
status_code,
+ latency_ms: elapsed.as_millis().min(u32::MAX as u128) as u32,
inbound_protocol: "openai".to_string(),
error_class: error_class.to_string(),
client_source_ip: client.source_ip.clone(),
client_user_agent: client.user_agent.clone(),
..Default::default()
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/aisix-proxy/src/usage_attr.rs` around lines 66 - 95, The error usage
event currently omits `latency_ms`, leaving failed requests recorded with the
default value even though callers already compute `elapsed`. Update
`emit_error_usage_event` in `usage_attr.rs` to accept the elapsed duration and
populate `latency_ms` on `UsageEvent`, then thread that new argument through
each caller that invokes this helper (such as the error paths in
`completions.rs` and `audio.rs`) so the emitted error event carries the actual
time-to-failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/aisix-proxy/src/usage_attr.rs`:
- Around line 66-95: The error usage event currently omits `latency_ms`, leaving
failed requests recorded with the default value even though callers already
compute `elapsed`. Update `emit_error_usage_event` in `usage_attr.rs` to accept
the elapsed duration and populate `latency_ms` on `UsageEvent`, then thread that
new argument through each caller that invokes this helper (such as the error
paths in `completions.rs` and `audio.rs`) so the emitted error event carries the
actual time-to-failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa240605-4d41-4fb4-bc6f-70ff49a44d05

📥 Commits

Reviewing files that changed from the base of the PR and between 09e52c4 and b439038.

📒 Files selected for processing (6)
  • crates/aisix-proxy/src/audio.rs
  • crates/aisix-proxy/src/completions.rs
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/images.rs
  • crates/aisix-proxy/src/rerank.rs
  • crates/aisix-proxy/src/usage_attr.rs

Sign up for freeto 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

@jarvis9443