feat: emit AI credits as OTEL span attributes#4707
Conversation
Add setBudgetAttributes() to otel.js that emits per-request AI credits and effective token budget data as numeric span attributes: awf.ai_credits - AI credits consumed by this request awf.ai_credits_total - Running total AI credits for this session awf.effective_tokens - Effective tokens this request awf.effective_tokens_total - Running total effective tokens awf.model_multiplier - Model cost multiplier applied These are emitted as numeric values so Sentry and Grafana can aggregate them (sum/avg/p95) without the string-type blocking issue. Closes github/gh-aw#38424 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Adds OpenTelemetry span attributes for per-request AI-credits and effective-token budget data from the api-proxy, aiming to make these values queryable/aggregatable in observability backends (Sentry/Grafana/Tempo).
Changes:
- Emit new
awf.*budget span attributes via a newotel.setBudgetAttributes()helper. - Call
setBudgetAttributes()from the HTTP upstreamonUsagecallback alongside existingsetTokenAttributes(). - Add unit tests for
setBudgetAttributes()and extend the OTEL-disabled noop surface.
Show a summary per file
| File | Description |
|---|---|
| containers/api-proxy/upstream-response.js | Calls otel.setBudgetAttributes() with the computed budget fields during usage tracking. |
| containers/api-proxy/proxy-request.js | Adds setBudgetAttributes to the OTEL-disabled noop shim to avoid missing-method calls. |
| containers/api-proxy/otel.test.js | Adds unit tests asserting new budget attributes are attached to spans. |
| containers/api-proxy/otel.js | Implements setBudgetAttributes() and exports it from the OTEL module. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 3
| if (budgetResult.ai_credits_this_response != null) { | ||
| attrs['awf.ai_credits'] = budgetResult.ai_credits_this_response; | ||
| } | ||
| if (budgetResult.ai_credits_total != null) { | ||
| attrs['awf.ai_credits_total'] = budgetResult.ai_credits_total; | ||
| } |
| if (budgetResult.effective_tokens_this_response != null) { | ||
| attrs['awf.effective_tokens'] = budgetResult.effective_tokens_this_response; | ||
| } | ||
| if (budgetResult.effective_tokens_total != null) { | ||
| attrs['awf.effective_tokens_total'] = budgetResult.effective_tokens_total; | ||
| } |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@copilot address the review feedback |
Addressed all three review items in commit
|
🔥 Smoke Test: PAT Auth — PASS
PR: feat: emit AI credits as OTEL span attributes Overall: PASS Note 🔒 Integrity filter blocked 1 itemThe following item was blocked because it doesn't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
🔥 Smoke Test: API Proxy OpenTelemetry Tracing
All scenarios pass ✅
|
|
Smoke Test Results:
Overall: PASS
|
|
✅ Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "registry.npmjs.org"See Network Configuration for more information.
|
🔍 Chroot Version Comparison Results
Overall: ❌ FAILED — Python and Node.js versions differ between host and chroot environments.
|
|
GitHub API: ✅ PASS Total: PASS
|
🔬 Smoke Test Results — PR #4707
Overall: FAIL — file test unverifiable due to workflow variable injection failure. PR by @lpcox · No assignees. Note 🔒 Integrity filter blocked 1 itemThe following item was blocked because it doesn't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
|
@lpcox @${{ github.event.pull_request.assignees }} Smoke Test Results:
Running in direct BYOK mode (AWF_AUTH_TYPE=github-oidc + AWF_AUTH_AZURE_* + COPILOT_PROVIDER_BASE_URL) via api-proxy → Azure OpenAI (Foundry, o4-mini-aw) authenticated via Microsoft Entra Overall: PASS Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "api.openai.com"See Network Configuration for more information.
|
Smoke Test Results — FAIL
Overall: FAIL — service containers are not accessible from this runner environment.
|
Smoke Test: Copilot BYOK (Direct) Mode ✅ PASSTest Results:
Mode: Direct BYOK (COPILOT_PROVIDER_API_KEY) with api-proxy sidecar
|
|
@copilot address review feedback |
Both open review threads were already addressed in commit
The review threads reference the original commit |
Problem
AI Credits (AIC) are not queryable in Sentry or Grafana. The api-proxy writes
ai_credits_this_responseandai_credits_totaltotoken-usage.jsonl, but never emits them as OTEL span attributes. This means:has:gh-aw.aicreturns 0 results in Sentryspan."gh-aw.aic"is absent from the Grafana/Tempo attribute indexSee github/gh-aw#38424 for the full observability gap analysis.
Solution
Add
setBudgetAttributes()to the OTEL module that emits per-request budget data as span attributes:awf.ai_creditsawf.ai_credits_totalawf.model_unitsawf.model_units_totalawf.model_multiplierCalled from the
onUsagecallback inupstream-response.jsaftercomputeTokenBudgetUsage()returns, so budget attributes land on the same span asgen_ai.usage.*token attributes.Key Design Decisions
awf.cached_read,awf.cached_write,awf.reasoning) because Sentry silently drops unknown numeric custom attributes; Grafana/Tempo can still filter and display string valuesawf.model_units/awf.model_units_totalto avoid Sentry's PII scrubbing rule that redacts values for keys containing "token"awf.prefix (consistent with existingawf.cached_read,awf.reasoning)Testing
otel.test.jscovering: full budget, partial budget, undefined budget, null span