feat: budgets with warn, downgrade, stop and admin overrides (P-d) #26

Merged
KofTwentyTwo merged 2 commits from feature/p4-budgets into develop 2026-10-13 23:46:20 +00:00
KofTwentyTwo commented 2026-10-13 23:44:07 +00:00 (Migrated from github.com)

P-d: budgets with warn, downgrade and stop, admin overrides, usage over time

Spec: docs/DESIGN.md part P, item 4. This is based on develop (after #24).

Tables

All are assistant_* tables, gated by assistant.admin, with AI exposure NONE.

  • assistant_budget (in the nav as Budgets):
    • scope: INSTANCE, USER, ROLE (a permission), PROVIDER or MODEL;
    • period DAY or MONTH (UTC), and metric COST_USD or TOKENS;
    • warn % (default 80), and what happens at 100%: DOWNGRADE (optionally to a named model) or STOP;
    • a hard ceiling (default 120%), at which the turn always stops.
  • assistant_budget_override (child list on the budget): an extra allowance or "unlimited until", with a required reason and expiry. The grantor is taken from the session. Only assistant.admin can create one.
  • assistant_budget_event (child list): WARNED, DOWNGRADED or STOPPED, deduplicated per budget, scope and period by a unique key, so it holds across replicas.
  • assistant_budget_usage:
    • Usage rows are owner-locked, so a user's session cannot sum other users' usage. This table holds only counters, time, user, provider and model; no conversation or request data.
    • It lets shared scopes (instance, provider, model) be summed under the caller's own session. No session is elevated.
    • It is read-only and visible only to admins. UPGRADE.md has the backfill.

Behaviour

  • Budgets are checked at turn start (the existing 429 BUDGET path) and before every model call. The strictest outcome wins.
  • At the warn %:
    • a panel notice to the user;
    • an optional Alerts signal assistant.budget.warning, through the reflective Alerts producer SPI pattern from qbit-webhooks. The Alerts qbit turns it into the bell.
  • At 100% with DOWNGRADE: the turn switches to the named model, or the cheapest enabled model (LOCAL first), and the user gets a notice. The PHI rule still applies: if no eligible target exists, the turn stops.
  • STOP, or the hard ceiling: the turn ends with BUDGET and the budget's name.
  • The existing config budgets and provider monthly caps are unchanged, and overrides never waive them.

App home

  • A Budgets card: used / limit, a percent bar, the state, and the active overrides.
  • 30-day cost and token bars, broken down by user, model, provider and tool.
  • Flow layout only, inherited colors, and empty states.

Tests

205 in total, all green in ./mvnw verify with the JaCoCo gates (core: 133).

  • Warnings: none at 79%; one warning at 80%, and only once per period.
  • Downgrade: to the named model; to the cheapest LOCAL model; PHI with no eligible target stops.
  • Stops: the hard ceiling stops; the turn-start 429 and the mid-turn BUDGET.
  • Overrides: extra allowance, unlimited until, expired, and admin-only.
  • Scopes: ROLE, PROVIDER, MODEL and INSTANCE, the TOKENS metric, and the UTC month boundary with a fixed clock.
  • Precedence and home: the strictest outcome wins; the home renders with data and empty.

🤖 Generated with Claude Code

## P-d: budgets with warn, downgrade and stop, admin overrides, usage over time Spec: `docs/DESIGN.md` part P, item 4. This is based on `develop` (after #24). ### Tables All are `assistant_*` tables, gated by `assistant.admin`, with AI exposure NONE. - **`assistant_budget`** (in the nav as Budgets): - scope: INSTANCE, USER, ROLE (a permission), PROVIDER or MODEL; - period DAY or MONTH (UTC), and metric COST_USD or TOKENS; - warn % (default 80), and what happens at 100%: DOWNGRADE (optionally to a named model) or STOP; - a hard ceiling (default 120%), at which the turn always stops. - **`assistant_budget_override`** (child list on the budget): an extra allowance or "unlimited until", with a required reason and expiry. The grantor is taken from the session. Only `assistant.admin` can create one. - **`assistant_budget_event`** (child list): WARNED, DOWNGRADED or STOPPED, deduplicated per budget, scope and period by a unique key, so it holds across replicas. - **`assistant_budget_usage`:** - Usage rows are owner-locked, so a user's session cannot sum other users' usage. This table holds only counters, time, user, provider and model; no conversation or request data. - It lets shared scopes (instance, provider, model) be summed under the caller's own session. No session is elevated. - It is read-only and visible only to admins. UPGRADE.md has the backfill. ### Behaviour - Budgets are checked at turn start (the existing 429 `BUDGET` path) and before **every** model call. The strictest outcome wins. - **At the warn %:** - a panel notice to the user; - an optional Alerts signal `assistant.budget.warning`, through the reflective Alerts producer SPI pattern from qbit-webhooks. The Alerts qbit turns it into the bell. - **At 100% with DOWNGRADE:** the turn switches to the named model, or the cheapest enabled model (LOCAL first), and the user gets a notice. The PHI rule still applies: if no eligible target exists, the turn stops. - **STOP, or the hard ceiling:** the turn ends with `BUDGET` and the budget's name. - The existing config budgets and provider monthly caps are unchanged, and overrides never waive them. ### App home - A Budgets card: used / limit, a percent bar, the state, and the active overrides. - 30-day cost and token bars, broken down by user, model, provider and tool. - Flow layout only, inherited colors, and empty states. ### Tests 205 in total, all green in `./mvnw verify` with the JaCoCo gates (core: 133). - **Warnings:** none at 79%; one warning at 80%, and only once per period. - **Downgrade:** to the named model; to the cheapest LOCAL model; PHI with no eligible target stops. - **Stops:** the hard ceiling stops; the turn-start 429 and the mid-turn `BUDGET`. - **Overrides:** extra allowance, unlimited until, expired, and admin-only. - **Scopes:** ROLE, PROVIDER, MODEL and INSTANCE, the TOKENS metric, and the UTC month boundary with a fixed clock. - **Precedence and home:** the strictest outcome wins; the home renders with data and empty. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.