Skip to content

feat(db): add a tenant column to ai_usage_events for centralized hosted billing#7183

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
davion-knight:feat-ai-usage-tenant
Jul 18, 2026
Merged

feat(db): add a tenant column to ai_usage_events for centralized hosted billing#7183
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
davion-knight:feat-ai-usage-tenant

Conversation

@davion-knight

Copy link
Copy Markdown
Contributor

What & why

ai_usage_events records AI usage/cost per event but had no tenant column — identity was only recoverable indirectly via actor/route/metadata_json. A central hosted-billing aggregator (shared by ORB + AMS once AMS's storage migration lands, #7173) needs a real column to attribute usage without parsing free-form metadata.

Changes

  • Migration 0163 adds a nullable installation_id column + a (installation_id, created_at) index, with the matching Drizzle declaration in schema.ts. db:schema-drift and db:migrations both pass (0163 contiguous; schema matches migrations).
  • recordAiUsageEvent gains an optional installationId field. It's optional and defaults to null, so every existing self-host call site is byte-identical to today — only hosted containers populate it. Applies to both this table's purposes (AI usage and the double-purposed BYOK byok:... audit rows), with no special-casing, per the issue.
  • sumAiCostForTenantSince(env, installationId, sinceIso) — the per-tenant billing aggregate, mirroring sumAiEstimatedNeuronsSince's shape but scoped by installation_id (covered by the new index). Because it filters by equality, self-host rows (null tenant) are never matched — it's inherently hosted-only.

Tests

installation_id is written when supplied and stays null when omitted (self-host unaffected), and the aggregate is correctly scoped by tenant + time window — excluding other tenants, rows before the window, and null-tenant self-host rows, and summing to 0 (not erroring) for a tenant with no rows.

100% patch coverage on the changed lines; tsc --noEmit clean.

Closes #7176

…ed billing

ai_usage_events records AI usage/cost per event but had no tenant column -- identity was only
recoverable indirectly via actor/route/metadata_json. A central hosted-billing aggregator (shared by
ORB and AMS once AMS's storage migration lands, JSONbored#7173) needs a real column to attribute usage without
parsing free-form metadata.

Adds a nullable `installation_id` column (migration 0163 + the matching Drizzle declaration + a
(installation_id, created_at) index for the aggregate below). Nullable and backfilled-null: self-host
has no installation concept and keeps working byte-identically -- recordAiUsageEvent's new
installationId field is optional, so every existing self-host call site writes null and behaves exactly
as before; only hosted containers populate it at insert time. The column applies to BOTH of this
table's purposes -- AI usage AND the double-purposed BYOK `byok:...` key-lifecycle audit rows -- with no
special-casing.

Adds sumAiCostForTenantSince(env, installationId, sinceIso), the per-tenant billing aggregate mirroring
sumAiEstimatedNeuronsSince's shape but scoped by installation_id (covered by the new index). Because it
filters installation_id by equality, self-host rows (null tenant) are never matched -- it is inherently
hosted-only.

Tests: installation_id is written when supplied and stays null when omitted (self-host unchanged), and
the aggregate is correctly scoped by tenant and time window (excluding other tenants, older rows, and
null-tenant rows). db:schema-drift and db:migrations checks pass (0163 contiguous, schema matches
migrations). 100% patch coverage on the changed lines.

Closes JSONbored#7176
@davion-knight
davion-knight requested a review from JSONbored as a code owner July 18, 2026 07:05
@superagent-security

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.76%. Comparing base (a3affda) to head (260874c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7183   +/-   ##
=======================================
  Coverage   93.76%   93.76%           
=======================================
  Files         692      692           
  Lines       68921    68924    +3     
  Branches    18790    18791    +1     
=======================================
+ Hits        64625    64628    +3     
  Misses       3302     3302           
  Partials      994      994           
Flag Coverage Δ
shard-1 43.90% <33.33%> (-0.01%) ⬇️
shard-2 36.86% <33.33%> (-0.01%) ⬇️
shard-3 33.10% <100.00%> (-0.13%) ⬇️
shard-4 34.58% <33.33%> (+0.57%) ⬆️
shard-5 32.23% <33.33%> (+0.03%) ⬆️
shard-6 45.83% <33.33%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/db/repositories.ts 96.70% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 72.97% <ø> (ø)

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 18, 2026
@loopover-orb

loopover-orb Bot commented Jul 18, 2026

Copy link
Copy Markdown

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-18 07:13:35 UTC

4 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
Adds a nullable installation_id tenant column to ai_usage_events with a matching contiguous migration (0163), Drizzle schema declaration, a supporting composite index, and a new sumAiCostForTenantSince aggregate mirroring the existing sumAiEstimatedNeuronsSince pattern. The change is additive and backward-compatible — installationId is optional and defaults to null, so existing self-host call sites are unaffected, and the equality filter naturally excludes null-tenant rows. Migration is D1-safe (plain ALTER TABLE ADD COLUMN + CREATE INDEX IF NOT EXISTS, no temp objects/pragmas/transactions), and tests cover write-then-null, explicit-null, and tenant/time-window scoping including the zero-rows case.

Nits — 5 non-blocking
  • repositories.ts: sumAiCostForTenantSince's `row?.total ?? 0` fallback is marked v8-ignored as unreachable defensive code, which is fine but worth double-checking the coalesce(sum(...),0) truly guarantees a row is always returned across the D1 driver version in use.
  • The new index ai_usage_events_installation_created_idx duplicates the CREATE INDEX IF NOT EXISTS guard in the migration but not in the Drizzle schema.ts declaration — verify drizzle-kit's schema-drift check tolerates this asymmetry (PR claims it passes, but flag for reviewer awareness).
  • test/unit/ai-usage-tenant.test.ts's seed() helper backdates rows via a raw UPDATE keyed on `max(created_at)`, which is fragile if two seeds could tie on the same timestamp — not an issue today but worth a comment noting the ordering assumption.
  • Consider adding a one-line JSDoc note on sumAiCostForTenantSince clarifying that unlike sumAiEstimatedNeuronsSince it is NOT filtered by status='ok', so failed/error rows with a costUsd are still included in the tenant total — worth confirming that's intentional for billing accuracy.
  • If AMS's storage migration (Design: shared hosting control-plane for ORB + AMS SaaS (provisioning, secrets, billing, health) #7173) hasn't landed yet, confirm installationId's expected format/source is documented somewhere so hosted callers populate it consistently.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #7176
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 (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 196 registered-repo PR(s), 125 merged, 2 issue(s).
Contributor context ✅ Confirmed Gittensor contributor davion-knight; Gittensor profile; 196 PR(s), 2 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The PR adds a nullable installation_id column via migration 0163 and matching schema change, extends recordAiUsageEvent with an optional installationId that defaults to null (preserving existing self-host call sites), and adds sumAiCostForTenantSince mirroring the existing aggregate pattern, with tests covering write/null-default/aggregation behavior — covering all listed deliverables.

Review context
  • Author: davion-knight
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Rust
  • Official Gittensor activity: 196 PR(s), 2 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
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.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 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 LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot 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.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit bc49b20 into JSONbored:main Jul 18, 2026
16 checks passed
loopover-orb Bot pushed a commit that referenced this pull request Jul 18, 2026
Adds listAiCostByTenantSince (one GROUP BY query, highest-cost-first)
and wires it into /v1/app/operator-dashboard. The ledger and
per-tenant column already existed (#7176/#7183); nothing consumed
them yet. Always empty for self-host, unchanged behavior there.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tenant column to ai_usage_events for centralized hosted billing across ORB + AMS

1 participant