Appearance
Budget Hierarchy and Owner Taxonomy Interview
See also: Budgets and Spending, Data Relationships, ADR: Spend Control Plane Reporting and Team Hard-Limit Enforcement
Date: 2026-05-11
Related issue: #106
Scope
This interview aligned design decisions for GitHub issue #106. The original direction considered a hierarchy of personal user budgets, team aggregate budgets, team-owned/service-style traffic, and per-user model budgets. A follow-up interview revised that direction: team budgets and team-owned API keys should be removed entirely, and spend control should be based on user and service-account principals.
Superseded Direction
The first pass considered keeping team budgets as aggregate guardrails and treating service-account-style traffic as a team-owned-key convention. That direction is superseded by the decisions below.
Revised Core Direction
Drop team budgets entirely.
Teams remain useful for membership, service-account grouping, and model-access inheritance, but they are not spend-bearing principals and are not budget owners.
Spend-bearing principals are:
- human users,
- service accounts.
The implementation goal becomes:
Replace team-owned spend control with user/service-account principal budgeting, remove team budgets and team-owned API keys, and add human user model budgets.
Decisions
Team Budgets
- Remove team budgets from runtime, persistence, API, UI, docs, and tests.
- Teams are not budget scopes.
- Delete existing
team_budgetsand associated team budget alert data in migration. - Because there are no production instances, destructive cleanup is acceptable.
Team-Owned API Keys
- Remove team-owned API keys in the same change.
- Delete existing team-owned API keys during migration.
- Remove
ApiKeyOwnerKind::Teamfrom the runtime/domain owner enum after migration cleanup. - If migration-only decoding is needed, keep it local to migration/store code rather than runtime domain behavior.
Historical Team-Owned Ledger Rows
- Delete historical team-owned
usage_cost_eventsrows. - This is intentionally destructive and acceptable because there are no production instances or accounting history to preserve.
- New ledger data should only be attributable to human users or service accounts.
Service Accounts
- Service accounts are a separate principal kind, not users.
- Service accounts use the same aggregate budget mechanics as users.
- Every service account must belong to exactly one team.
- Service accounts inherit model access from their required team association.
- Budgets attach directly to the service-account principal, not to the team.
Service Account Budget Requirement
- A service account must have an active budget before any service-account API key can be active.
- The budget may be hard or soft.
- Human user budgets remain optional.
Human User Budgets
- Human user aggregate budgets remain supported.
- Human user budgets are optional.
- Personal user budgets remain the primary fairness mechanism for human user-owned traffic.
Model Budgets
- Model budgets apply only to human users.
- Do not add service-account model budgets in #106.
- Model budgets are keyed primarily by gateway model ID.
- Fallback to exact stored
upstream_modelonly whenmodel_idis absent. - Upstream model normalization is trim-only; no lowercasing or aliasing.
Enforcement Order
- Human user-owned traffic checks budgets in narrow-to-broad order:
- user model budget, when one applies,
- overall user budget, when one exists.
- Service-account-owned traffic checks the service-account budget.
- Narrower budget failures should surface first.
Generic Budget Table
- Keep the generic
budgetstable direction. - Supported scope kinds are:
user,service_account,user_model.
- Do not include a
teambudget scope kind. - Store typed nullable columns plus a canonical
scope_key. - Enforce one active budget per
scope_key.
Budget Scope Keys
Use versioned budget scope keys:
text
budget:v1:user:<user_id>
budget:v1:service_account:<service_account_id>
budget:v1:user:<user_id>:model:<model_id>
budget:v1:user:<user_id>:upstream_model:<trimmed_upstream_model>- Keep
usage_cost_events.ownership_scope_keyseparate from budgetscope_key. - The budget evaluator maps request context to one or more budget scope keys.
API Contract
- Replace old user/team budget admin endpoints with a generic budget API.
- Requests use typed
scopeobjects. - Responses include typed scope plus computed
scope_key. - This is a breaking admin-control-plane change.
Rollout
Implement as one breaking migration PR:
- backend generic budget API,
- removal of team budget APIs,
- removal of team-owned API key creation/authentication paths,
- service-account budget enforcement,
- admin UI update,
- docs,
- ADR,
- migration guide.
Existing Budget Data Migration
- Backfill existing
user_budgetsrows into the genericbudgetstable. - Preserve IDs and fields where possible.
- Delete
team_budgetsrows rather than migrating them. - Delete associated team budget alert data.
- Leave old tables only if needed as migration artifacts; runtime code should use generic budgets only.
Soft Budgets
hard_limit = falseremains alerting/reporting only.- Soft budgets never reject requests.
- A service account can satisfy the active-budget requirement with either a soft or hard budget.
Unpriced and Usage-Missing Rows
- Never count
unpricedorusage_missingrows toward any budget. - Continue recording and reporting them.
Pre-Provider Checks
- Evaluate all applicable scopes pre-provider and post-provider.
- Pre-provider rejects only when an applicable hard budget is already at or over limit.
- Post-provider uses actual projected cost.
Reporting
- Global spend totals remain single-source from
usage_cost_events. - Owner/principal filters become:
all,user,service_account.
- Optional team filtering may narrow service-account reporting by the service account's required team association.
- Team is grouping metadata only, not an owner kind or budget scope.
- Budget-scope spend is independent and may overlap.
- Reports must not sum user and user-model budget-scope spend as additive totals.
Timezones
- Keep current UTC window behavior in #106.
- Preserve the timezone field.
- Isolate window calculation so #101 can introduce timezone-aware windows later.
Alerts
- Migrate budget alerts/history to generic budget scopes.
- Delivery behavior remains unchanged.
- Remove team budget alert records during migration.
Admin UI
Spend Controls should show:
- User Budgets,
- Service Account Budgets,
- User Model Budgets.
There should be no Team Budgets section.
Config-Seeded API Keys
- Seeded keys must declare or create a service-account owner.
- Seeded service accounts must include:
- required team association,
- active budget.
- Remove the
system-legacyteam fallback for spend-bearing seeded keys.
Concurrency
- Preserve current best-effort hard-limit semantics.
- Document possible concurrent overshoot.
- Keep atomic/idempotent insert guarantees and tests.
Undocumented Assumptions to Capture
- Teams are grouping/access-control constructs, not spend principals.
- Team-level budgets are harmful as a fairness mechanism because they are too coarse and can allow one actor/workload to consume the shared pool.
- Human user fairness should be handled by human user budgets and human user model budgets.
- Service-account spend should be controlled by service-account budgets.
- Service accounts must belong to exactly one team for model-access inheritance.
- Budget scope keys and ledger ownership keys are separate concepts.
- User-model budgets overlap user budgets and must not create a second accounting source.
- Hard limits are not strictly race-safe under concurrent requests.
- Destructive migration of team budget/key/ledger data is acceptable because there are no production instances.
Potential Follow-Up Enhancements
- Service-account model budgets, if workload-specific expensive-model controls become necessary.
- Race-safe budget reservations or transactional counters.
- Timezone-aware windows from #101.
- Provider-qualified upstream model fallback if exact upstream matching proves ambiguous.
- Generic scope hierarchy/rollup reporting.
