Skip to content

fix(observability): report actual orb ai token usage on the dashboards - #3281

Merged
JSONbored merged 5 commits into
mainfrom
grafana-codex-usage-fix
Jul 5, 2026
Merged

fix(observability): report actual orb ai token usage on the dashboards#3281
JSONbored merged 5 commits into
mainfrom
grafana-codex-usage-fix

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • The "Gittensory - Codex usage (self-host)" dashboard's durable SQL panels (as flagged) filtered on model LIKE '%codex%' and surfaced only estimated_neurons, even though migration 0109_ai_usage_actual_tokens.sql added real provider/effort/input_tokens/output_tokens/total_tokens/cost_usd columns to ai_usage_events — and refactor(ai): remove legacy workers-ai framing from live review features #3270 wired all four live AI features to actually populate them.
  • Rewrote every durable panel in codex-usage.json to filter on provider = 'codex' instead of pattern-matching the model string.
  • Replaced the "Estimated neuron usage by day" panel with a real "Durable token usage by day" panel (input/output tokens) and added a new "Durable cost by day" panel; the recent-events table now includes effort/input_tokens/output_tokens/total_tokens/cost_usd alongside the existing estimated_neurons (kept for reference, not as the only metric).
  • Added grafana/dashboards/orb-ai-usage.json: a provider-neutral dashboard over ai_usage_events covering every configured provider (Codex, Claude Code, OpenAI-compatible, Ollama, Anthropic BYOK), broken down by provider/model/effort/repo/PR, plus an "events missing real usage" counter to track which features still haven't threaded real usage through recordAiUsageEvent.
  • Clarified claude-usage.json's description: it's Claude Code's own OTEL/session telemetry (a session-scoped cumulative counter via last_over_time), not the durable ai_usage_events DB attribution — cross-references the new orb-ai-usage dashboard for that.
  • All three dashboard JSON files are auto-provisioned from grafana/dashboards/ (directory-mounted in docker-compose.yml:424, no config wiring needed for the new file) and validated by scripts/validate-observability-configs.mjs. scripts/export-grafana-reporting-db.sh already exports every column these panels reference (verified — no changes needed there).

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • No issue linked — direct follow-up to refactor(ai): remove legacy workers-ai framing from live review features #3270 with clear scope, per the observability gap identified there.

Validation

  • git diff --check
  • npm run actionlint (via npm run test:ci)
  • node scripts/validate-observability-configs.mjs — dashboards and alert rules valid
  • npm run typecheck
  • npm run test:coverage — no src/** changes in this PR (JSON config only); full suite green (8807 passed), including test/unit/selfhost-grafana-dashboard.test.ts and test/unit/selfhost-grafana-reporting.test.ts
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate — 0 vulnerabilities
  • Ran the full npm run test:ci gate locally — green

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A, no API/OpenAPI/MCP changes; Grafana dashboard JSON only.
  • UI changes use live API data or real empty/error/loading states. — N/A, not a gittensory-ui change (Grafana operator dashboards, not the app UI).
  • Visible UI changes include a UI Evidence section. — N/A per the template's own scope (operator-only Grafana dashboards, not apps/gittensory-ui); did not stand up a Grafana instance to screenshot since no live datasource is available in this environment, but every panel query was validated against the actual ai_usage_events/reporting-DB schema and the repo's own dashboard/alert-rule validator.
  • Public docs/changelogs are updated where needed. — N/A, no changelog edit.

The Codex usage dashboard's durable SQL panels filtered on
model LIKE '%codex%' and only surfaced estimated_neurons, even though
migration 0109 added real provider/effort/input_tokens/output_tokens/
total_tokens/cost_usd columns to ai_usage_events months ago:

- Rewrite every durable panel in codex-usage.json to filter on
  provider = 'codex' instead of pattern-matching the model string.
- Replace the "Estimated neuron usage by day" panel with a real
  "Durable token usage by day" panel (input/output tokens), add a new
  "Durable cost by day" panel, and add effort/token/cost columns to the
  recent-events table (keeping estimated_neurons alongside for
  reference, not as the only metric).
- Add grafana/dashboards/orb-ai-usage.json: a provider-neutral dashboard
  over ai_usage_events showing every configured provider (Codex, Claude
  Code, OpenAI-compatible, Ollama, Anthropic BYOK) broken down by
  provider/model/effort/repo/PR, plus an "events missing real usage"
  counter to track which features still haven't threaded real usage
  through (see #3270).
- Clarify claude-usage.json's description: it's Claude Code's own
  OTEL/session telemetry, not the durable ai_usage_events DB
  attribution — cross-references the new orb-ai-usage dashboard for that.

All three dashboard JSON files are auto-provisioned from
grafana/dashboards/ (no config wiring needed for the new file) and
validated by scripts/validate-observability-configs.mjs.
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 5, 2026
@loopover-orb

loopover-orb Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-05 04:41:05 UTC

3 files · 1 AI reviewer · 1 blocker · readiness 93/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Maintainer test expectations unmet — Add or update tests, or attach passing validation output that satisfies the maintainer's test expectations.

Review summary
The dashboard changes correctly move the Codex durable token/cost panels onto the real ai_usage_events token and cost columns while preserving model-label matching for status-oriented Codex rows that can lack provider attribution. The new ORB dashboard is coherent and queryable against the migration 0109 columns, and the Claude dashboard change is only descriptive. I do not see a reachable correctness defect in the visible diff, but a few observability/readability choices are worth tightening.

Nits — 6 non-blocking
  • nit: grafana/dashboards/orb-ai-usage.json:10 and grafana/dashboards/orb-ai-usage.json:61 put operational invariants and implementation details into very long dashboard descriptions, which makes the dashboard harder to scan and maintain even though the queries are valid.
  • nit: grafana/dashboards/orb-ai-usage.json:36 counts only provider-attributed events as "Total AI events", while grafana/dashboards/orb-ai-usage.json:61 separately counts provider-null completed calls, so the summary row can look internally inconsistent unless the title makes that attribution boundary explicit.
  • nit: grafana/dashboards/codex-usage.json:10 describes the mixed provider/model-label filter accurately, but the repeated inline SQL predicate at grafana/dashboards/codex-usage.json:70, grafana/dashboards/codex-usage.json:146, and grafana/dashboards/codex-usage.json:183 is easy to drift if another panel is added later.
  • grafana/dashboards/orb-ai-usage.json:36: rename the stat to "Attributed AI events" or change the query to count all durable AI rows and add a separate attributed count, so the summary title matches the provider IS NOT NULL predicate.
  • grafana/dashboards/orb-ai-usage.json:61: move the long explanation of zero-default usage fields into repo docs or a shorter panel description, leaving the panel to state the operational meaning of a nonzero value.
  • Diff looks like trivial or whitespace-only churn — Reduce whitespace-only or formatting-only churn and keep the diff focused on substantive changes.

Concerns raised — review before merging

  • Maintainer test expectations unmet — Add or update tests, or attach passing validation output that satisfies the maintainer's test expectations.
Signal Result Evidence
Code review ❌ 1 blocker 1 reviewer
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 56 registered-repo PR(s), 46 merged, 416 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 56 PR(s), 416 issue(s).
Gate result ❌ Blocking Repo-configured hard blocker found.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: Python, TypeScript, JavaScript, Ruby, Go, Kotlin, MDX, Shell
  • Official Gittensor activity: 56 PR(s), 416 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

…-usage filter

- Wrap every sum(input_tokens|output_tokens|total_tokens|cost_usd) in
  coalesce(..., 0) across codex-usage.json's and orb-ai-usage.json's SQL
  panels, matching the existing coalesce(sum(estimated_neurons), 0)
  convention already used in sumAiEstimatedNeuronsSince — an empty
  result set now renders 0, not a blank/missing data point.
- Tighten orb-ai-usage.json's "Events missing real usage" panel to
  filter status = 'ok' instead of a bare feature exclusion. Verified
  against the actual status literals ever written to ai_usage_events
  (quota_exceeded/disabled/unavailable never reach a DB write at all;
  ai_key_change audit rows use "set"/"replace"/"delete") that the old
  filter was overcounting: quota_exceeded and BYOK key-lifecycle rows
  never carry provider/usage data by design, not because a feature is
  missing usage tracking. Reworded the panel + dashboard descriptions
  to match what the tightened query actually measures.
- Verified all four rewritten SQL queries against a real sqlite3
  instance seeded with rows matching each status/provider combination.
… counter explicitly

The "Events missing real usage" panel's query relied on an implicit
invariant in a different file (recordAiKeyChange's status param is
typed "set"|"replace"|"delete", never "ok") to keep ai_key_change audit
rows out of the count, even though the panel's own description already
promised that exclusion explicitly. Add the feature <> 'ai_key_change'
filter directly to the query so the panel is self-contained and doesn't
silently over-count if that invariant ever changes elsewhere.

Verified behavior-neutral against a seeded sqlite3 table: identical
result (1) for realistic data with and without the new filter, and the
new filter correctly excludes a row that violates the current type
invariant (an ai_key_change row with status='ok'), which the old query
would have wrongly counted.
@JSONbored

Copy link
Copy Markdown
Owner Author

Addressed the flagged blocker and the test-expectations note in 842e728:

Blocker (orb-ai-usage.json:62): added the feature <> 'ai_key_change' filter directly to the "Events missing real usage" query, rather than relying on recordAiKeyChange's status: "set"|"replace"|"delete" type constraint in src/db/repositories.ts to keep those rows out implicitly. Verified behavior-neutral against a seeded sqlite3 table matching the ai_usage_events schema: identical result for realistic data with/without the filter, and the new filter correctly excludes a hypothetical ai_key_change row with status='ok' that the old query would have wrongly counted (the current type system rules this out today, but the panel no longer depends on that holding).

Test expectations: these are Grafana dashboard JSON files with no corresponding unit test in the repo (confirmed no existing test references codex-usage.json/orb-ai-usage.json/claude-usage.json). In lieu of a unit test, validation evidence for every query change in this PR:

  • node scripts/validate-observability-configs.mjs — pass (dashboards and alert rules valid)
  • All 4 rewritten/added SQL queries verified against a real sqlite3 instance seeded with rows for every status/provider combination (quota_exceeded, ai_key_change set/replace/delete, a genuine status='ok'-with-null-provider gap row), confirming each panel counts exactly the rows its description claims
  • npm run typecheck — pass
  • Full npm run test:ci — green (ran on the earlier commit in this PR; this follow-up is JSON-only with no src/** changes)

…ge too

The "Events missing real usage" panel only checked provider IS NULL,
missing a second reachable gap: coerceAiUsage (ai-review.ts) extracts
provider independently of input_tokens/output_tokens/total_tokens/
cost_usd, so a raw provider response like {"usage": {"provider":
"codex"}} with no parseable token counts yields a stored row with
provider='codex' but all four numeric columns at their default.

The AI reviewer's literal suggested fix (checking those four columns
IS NULL) is dead code against this schema — migration 0109 declares
them NOT NULL DEFAULT 0, and recordAiUsageEvent's finiteNumber()
coercion means they can never be SQL NULL, only 0. Verified this with
an independent two-agent analysis (both traced the same schema/
coercion/extraction code) plus two adversarial agents that built a
scratch sqlite3 table from the actual migration SQL and ran the fixed
predicate against 16 seeded rows total across both verification
passes, confirming: the original provider-IS-NULL case still counts,
the new provider-set-all-zero case now counts, a provider-set-with-
real-nonzero-usage case does NOT count (no false positive), and ai_key_
change/quota_exceeded rows stay excluded. Also empirically confirmed a
literal NULL insert into these columns is rejected by the schema itself.

Query is now: status = 'ok' AND feature <> 'ai_key_change' AND
(provider IS NULL OR (input_tokens = 0 AND output_tokens = 0 AND
total_tokens = 0 AND cost_usd = 0)). Description updated to explain why
a zero-value check, not an IS NULL check, is the correct "no data"
signal for this schema.
@JSONbored

Copy link
Copy Markdown
Owner Author

Fixed in c743eed, and this one needed a real code change, not just a defensive no-op — the underlying concern was genuine.

What was actually wrong: the panel only checked provider IS NULL, missing a second reachable gap: coerceAiUsage (ai-review.ts) extracts provider independently of input_tokens/output_tokens/total_tokens/cost_usd from a provider's raw usage object. A response shaped like {"usage": {"provider": "codex"}} with no parseable token counts produces a stored row with provider='codex' but all four numeric columns at their zero default — invisible to the old query.

Why I didn't copy the reviewer's literal suggested SQL: (input_tokens IS NULL AND output_tokens IS NULL AND total_tokens IS NULL AND cost_usd IS NULL) is dead code against this schema — migration 0109 declares all four columns NOT NULL DEFAULT 0, and recordAiUsageEvent's finiteNumber() coercion means they can never be SQL NULL, only 0. Ran two independent agents to trace the schema/coercion/extraction code separately, then two adversarial agents that built a scratch sqlite3 table from the actual migration SQL and ran the corrected predicate against 16 seeded rows total (across both verification passes) covering: the original provider-null case (still counted), the new provider-set-all-zero case (now counted), a provider-set-with-real-nonzero-usage case (correctly NOT counted — no false positive), and ai_key_change/quota_exceeded rows (still excluded). Also empirically confirmed a literal NULL insert into those columns is rejected by the schema itself, ruling out the reviewer's literal predicate for good.

Final query:

SELECT count(*) AS unattributed FROM ai_usage_events
WHERE status = 'ok' AND feature <> 'ai_key_change'
  AND (provider IS NULL OR (input_tokens = 0 AND output_tokens = 0 AND total_tokens = 0 AND cost_usd = 0))

Validation: node scripts/validate-observability-configs.mjs (pass), npm run typecheck (pass), plus the empirical sqlite3 verification above in lieu of a unit test (no test file covers these dashboard JSONs).

…/recent-events panels

The status-breakdown and recent-events panels filtered strictly on
provider = 'codex', but a quota-blocked ai_review_pr call never gets
provider attribution — record() short-circuits before any provider
ever runs, so `actualUsage` (and therefore `provider`) is never passed.
That call's `model` field, however, is computed unconditionally via
reviewerModelLabel(env, input) inside record() regardless of status, so
it always reflects the configured/intended reviewer (e.g. "codex",
"codex:o4-mini", "codex+claude-code") even when the call never executed.
Filtering by provider alone therefore made every quota-blocked codex
review invisible to these panels — a real regression, not the
"disabled"/"unavailable" rows the review comment also named (those two
statuses never reach a DB write at all, confirmed by tracing every
return path in runGittensoryAiReview, so no filter change could have
dropped them).

Fixed the "Successful review records" (for consistency with the panel
below), "Review record status", and "Recent Codex-attributed review
events" panels to match 'codex' as a reviewer-name token in EITHER
provider OR the "+"-joined model label — reusing the exact matching
pattern already established (and covered by
test/unit/selfhost-grafana-reporting.test.ts) for the pre-existing
model-string-based filter this PR replaced. Left the token/cost sum
panels (10, 13) on the strict provider filter, since an unattributed
row contributes 0 to a sum regardless of which filter is used, and
documented why in both the dashboard and panel descriptions to
preempt an "inconsistent filters" flag on a future pass.

Verified via two independent agents tracing the exact code paths
(early-return statements, record()'s model/provider computation) plus
two adversarial agents running the corrected predicate against 9-10
seeded sqlite3 rows each, covering: the flagged quota_exceeded/codex
case, cross-provider exclusion, dual-reviewer-join and provider-model
override label formats, substring false-positive safety, cross-feature
scoping, and no double-counting. Final end-to-end check against all
three fixed panels' exact query text confirms the expected row/count
results.
@JSONbored

Copy link
Copy Markdown
Owner Author

Fixed in 910267b. This one was correct in substance but the reviewer's framing conflated two different statuses:

  • quota_exceeded rows ARE real and DO get dropped by a plain provider = 'codex' filter — traced why: record() in ai-review.ts computes model unconditionally via reviewerModelLabel(env, input) on every call regardless of status, but provider comes from the actualUsage parameter, which is never passed at either quota_exceeded call site (the call short-circuits before any provider ever runs). So a quota-blocked review has a real, meaningful model (e.g. codex:o4-mini) but provider = NULL — genuinely invisible to the old filter.
  • disabled/unavailable rows, also named in the review comment, are not actually affected — traced every return path in runGittensoryAiReview and confirmed those two statuses never reach a record()/DB-write call at all. There was nothing there for any filter to drop.

Fix: match 'codex' as a reviewer-name token in either provider OR the +-joined model label (reusing the exact pattern this PR replaced, which is already covered by test/unit/selfhost-grafana-reporting.test.ts), applied to the "Review record status" panel (flagged), "Recent Codex-attributed review events" (same root cause — confirmed empirically), and "Successful review records" (so it doesn't quietly disagree with the now-fixed status panel on how many successful reviews there were). Left the two token/cost sum panels on the strict provider filter and documented why in both dashboard and panel descriptions, since an unattributed row contributes 0 to a sum regardless of filter — no need to complicate those.

Verified with two independent agents tracing the code paths separately, then two adversarial agents running the corrected predicate against seeded sqlite3 data (9-10 rows each pass) covering the flagged case, cross-provider exclusion, dual-reviewer/model-override label formats, substring false-positive safety, cross-feature scoping, and no double-counting — plus a final end-to-end check against the exact shipped query text. node scripts/validate-observability-configs.mjs and npm run typecheck both pass.

@JSONbored
JSONbored merged commit 3f8e024 into main Jul 5, 2026
8 checks passed
@JSONbored
JSONbored deleted the grafana-codex-usage-fix branch July 5, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Development

Successfully merging this pull request may close these issues.

1 participant