feat(db): add a tenant column to ai_usage_events for centralized hosted billing#7183
Conversation
…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
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-18 07:13:35 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk 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.
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.
|
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>
What & why
ai_usage_eventsrecords AI usage/cost per event but had no tenant column — identity was only recoverable indirectly viaactor/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
0163adds a nullableinstallation_idcolumn + a(installation_id, created_at)index, with the matching Drizzle declaration inschema.ts.db:schema-driftanddb:migrationsboth pass (0163 contiguous; schema matches migrations).recordAiUsageEventgains an optionalinstallationIdfield. 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 BYOKbyok:...audit rows), with no special-casing, per the issue.sumAiCostForTenantSince(env, installationId, sinceIso)— the per-tenant billing aggregate, mirroringsumAiEstimatedNeuronsSince's shape but scoped byinstallation_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_idis 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 --noEmitclean.Closes #7176