You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for a Usage view in Studio, to be built as a tab/section
on the existing Billing page (src/ui/pages/Billing.jsx) — not a
separate page. Direction agreed; build deferred to a follow-up.
Related: #79 (Billing tile permission gating — same decision applies to
this tab). Depends on the org-context work already merged in e2fd715
(currentUser.orgId).
Investigation findings
omnibioai-usage-client is emit-only
Source: omnibioai-usage-client/usage_client/. Public surface is emit_usage_event() → one XADD onto the Redis stream usage:events.
No query/read/aggregate methods. Fails open (Redis outage returns False, never raises; no retry/queue).
Event shape (models.py): event_id, timestamp, organization_id, service, resource, action, quantity (float ≥ 0), unit,
optional user_id, trace_id, metadata. resource is dot-namespaced
free text, not an enum.
Wired producers today (cross-repo state, per its README):
Service
Events
Workbench (plugins/workflow_runner/usage_emit.py)
workflow.execute (count), gpu.hour (hours)
RAG (ragbio/usage_emit.py)
rag.query.executed (count); dataset.uploaded defined but unwired
model-registry (usage_emit.py)
model.register (count)
tes / billing-service / billing-worker receive USAGE_REDIS_URL
in compose but are consumers/infra, not producers. No token or raw-compute
metering is live today (the README's llm.tokens example is aspirational).
The read API already exists — this is a frontend-only task
billing-worker (running) drains usage:events into MySQL:
usage:events → billing consumer
→ usage_events (raw persisted, every event)
→ usage_daily_rollups (per org / day / service+resource+action+unit)
→ billable_usage_records (billable subset)
→ rated_usage_records (priced) → invoice line items
billing-service (app/routers/billing.py) exposes the read side, and nginx-router.conflocation ^~ /billing already proxies all of it
behind the same auth_request gate the Billing page uses (verified:
every endpoint returns a clean 401 through nginx — routed and gated, no
nginx change needed):
Endpoint
Returns
Source
GET /billing/organizations/{id}/usage?start_date&end_date
raw quantity per (service, action, resource, unit), arbitrary range
usage_daily_rollups
GET /billing/organizations/{id}/costs?start_date&end_date
total rated $ + breakdowns by service/action/resource
rated_usage_records
GET /billing/organizations/{id}/cost-history?start_date&end_date
daily $ points (gaps = no usage)
rated_usage_records
GET /billing/organizations/{id}/cost-breakdown?…&group_by=service|action|resource|month
$ + quantity grouped one dimension at a time
rated_usage_records
GET /billing/organizations/{id}/usage-events?…&resource&user_id&limit&offset
raw per-event log, paginated (built for the HIPAA RAG-query-log report)
usage_events
GET /billing/organizations/{id}/allowances?as_of
included/used/remaining vs the rating-engine plan
billing_plan_allowances + billable_usage_records
Work required: a usageApi.js (~identical to billingApi.js, or extend
it) + the tab UI + date-range UX. No backend work for the aggregated views.
Data is sparse in this deployment: usage_daily_rollups has real
test data for ~19 orgs (rag.query, model.register, workflow.execute, gpu.hour), but billing_plan_allowances is empty (so /allowances and /subscription/usage-limits return nothing for everyone) and there is
exactly 1 rated_usage_records row (so /costs* is near-empty apart from
org 1's $12.50 August period). Pipeline works; it hasn't been fed.
Per-org only
Every endpoint takes organization_id in the path; every query is
filtered by it. Org member → own org only; manage_all_orgs → any one
org, still one at a time. No platform-wide aggregate endpoint exists —
a platform-wide view would need N calls or a new billing-service endpoint.
No redundancy with /subscription/usage-limits
Same source events, three different projections:
/subscription/usage-limits (already rendered on the Billing page):
plan quota consumption (used / included, % bar) — needs an organization_subscriptions row + billing_plan_allowances.
/usage: raw operational quantity, no quota concept, arbitrary
range — this is the new thing a Usage tab adds.
/costs*: dollar cost — the other new thing.
"What you're entitled to" ≠ "what you ran" ≠ "what it cost".
Auth — identical to everything else
billing-service/app/core/iam.py: independent JWT verification via omnibioai-iam-client (same package api-gateway / control-center / rag
use), token org_id claim must match the URL or caller holds manage_all_orgs; else 403; bad/missing token → 401. Frontend needs currentUser.orgId (merged in e2fd715) + Authorization: Bearer
(already in billingApi.js).
/usage-events per-user data — this endpoint returns a raw
per-user activity log (it exists for the HIPAA RAG-query-log report).
Aggregated /usage and /costs do not expose individual users; /usage-events does. Decide deliberately whether to surface it at
all (and if so, behind what gate) — do NOT include it by default. Same
posture as the cron-log finding.
Tracking issue for a Usage view in Studio, to be built as a tab/section
on the existing Billing page (
src/ui/pages/Billing.jsx) — not aseparate page. Direction agreed; build deferred to a follow-up.
Related: #79 (Billing tile permission gating — same decision applies to
this tab). Depends on the org-context work already merged in
e2fd715(
currentUser.orgId).Investigation findings
omnibioai-usage-clientis emit-onlySource:
omnibioai-usage-client/usage_client/. Public surface isemit_usage_event()→ oneXADDonto the Redis streamusage:events.No query/read/aggregate methods. Fails open (Redis outage returns
False, never raises; no retry/queue).Event shape (
models.py):event_id,timestamp,organization_id,service,resource,action,quantity(float ≥ 0),unit,optional
user_id,trace_id,metadata.resourceis dot-namespacedfree text, not an enum.
Wired producers today (cross-repo state, per its README):
plugins/workflow_runner/usage_emit.py)workflow.execute(count),gpu.hour(hours)ragbio/usage_emit.py)rag.query.executed(count);dataset.uploadeddefined but unwiredusage_emit.py)model.register(count)tes/billing-service/billing-workerreceiveUSAGE_REDIS_URLin compose but are consumers/infra, not producers. No token or raw-compute
metering is live today (the README's
llm.tokensexample is aspirational).The read API already exists — this is a frontend-only task
billing-worker(running) drainsusage:eventsinto MySQL:billing-service(app/routers/billing.py) exposes the read side, andnginx-router.conflocation ^~ /billingalready proxies all of itbehind the same
auth_requestgate the Billing page uses (verified:every endpoint returns a clean 401 through nginx — routed and gated, no
nginx change needed):
GET /billing/organizations/{id}/usage?start_date&end_dateusage_daily_rollupsGET /billing/organizations/{id}/costs?start_date&end_daterated_usage_recordsGET /billing/organizations/{id}/cost-history?start_date&end_daterated_usage_recordsGET /billing/organizations/{id}/cost-breakdown?…&group_by=service|action|resource|monthrated_usage_recordsGET /billing/organizations/{id}/usage-events?…&resource&user_id&limit&offsetusage_eventsGET /billing/organizations/{id}/allowances?as_ofbilling_plan_allowances+billable_usage_recordsWork required: a
usageApi.js(~identical tobillingApi.js, or extendit) + the tab UI + date-range UX. No backend work for the aggregated views.
Data is sparse in this deployment:
usage_daily_rollupshas realtest data for ~19 orgs (
rag.query,model.register,workflow.execute,gpu.hour), butbilling_plan_allowancesis empty (so/allowancesand/subscription/usage-limitsreturn nothing for everyone) and there isexactly 1
rated_usage_recordsrow (so/costs*is near-empty apart fromorg 1's $12.50 August period). Pipeline works; it hasn't been fed.
Per-org only
Every endpoint takes
organization_idin the path; every query isfiltered by it. Org member → own org only;
manage_all_orgs→ any oneorg, still one at a time. No platform-wide aggregate endpoint exists —
a platform-wide view would need N calls or a new billing-service endpoint.
No redundancy with
/subscription/usage-limitsSame source events, three different projections:
/subscription/usage-limits(already rendered on the Billing page):plan quota consumption (
used / included, % bar) — needs anorganization_subscriptionsrow +billing_plan_allowances./usage: raw operational quantity, no quota concept, arbitraryrange — this is the new thing a Usage tab adds.
/costs*: dollar cost — the other new thing."What you're entitled to" ≠ "what you ran" ≠ "what it cost".
Auth — identical to everything else
billing-service/app/core/iam.py: independent JWT verification viaomnibioai-iam-client(same package api-gateway / control-center / raguse), token
org_idclaim must match the URL or caller holdsmanage_all_orgs; else 403; bad/missing token → 401. Frontend needscurrentUser.orgId(merged ine2fd715) +Authorization: Bearer(already in
billingApi.js).Open decisions before building
plausibly org-member-visible, not admin-only. Decide alongside Workbench: Billing tile is ungated — decide the right permission level before wider rollout #79.
/usage-eventsper-user data — this endpoint returns a rawper-user activity log (it exists for the HIPAA RAG-query-log report).
Aggregated
/usageand/costsdo not expose individual users;/usage-eventsdoes. Decide deliberately whether to surface it atall (and if so, behind what gate) — do NOT include it by default. Same
posture as the cron-log finding.
/usage,/costs,/cost-historyall requireexplicit
start_date/end_date; pick sensible defaults (currentbilling period? trailing 30 days?).
/costs*at all whilerated_usage_recordsiseffectively empty, or gate that section on there being data.
🤖 Generated with Claude Code