Skip to content

feat(usage): let chat callers see their own quota and conversation cost - #74

Merged
albanm merged 12 commits into
mainfrom
feat-self-limits-consumption
Oct 1, 2026
Merged

albanm merged 12 commits into
mainfrom
feat-self-limits-consumption

Conversation

@albanm

@albanm albanm commented Oct 1, 2026

Copy link
Copy Markdown
Member

Any chat caller (org member, external user, anonymous visitor, account owner) can now see their own AI quota and what the current conversation has cost.

  • GET /api/gateway/:type/:id/usage returns the caller's daily/weekly/monthly windows (used, limit, reset date), resolved with the same identity as a completion. The org credit cap and the anonymous+external pool are reported as a status (ok/exhausted + reset date); their numbers are only shown to admins of the account.
  • Gateway completions report usage.cost in credits (OpenRouter's convention), including the moderation check when its verdict settles before the gate opens.
  • The chat sums those costs into a conversation total, shown with the quota windows in a new Consumption tab of the chat settings dialog.
  • Quota 429s carry period and resets_at, drop the shared budgets' numbers for non-admins, are no longer retried client-side, and render as a localized message ("Your daily AI quota is used up. It resets on …").
  • Fix: recordUsage without a userId (owner of a user account) could increment an arbitrary usage document of that owner, e.g. an external user's record.
  • Dev fixtures: test1-user1/dev1-user1 had the non-existent org role user1; now user.

Why: a user should be able to see their own quotas and consumption, including the current conversation's.

Heads-up:

  • 429 body change: for account/untrusted scopes, usage/limit are now omitted unless the caller is an admin; period is added.
  • Org members can still read the credit cap through GET /api/limits/:type/:id (unchanged, intentional), so the redaction mainly protects shared budgets from external/anonymous callers.
  • The streamed gateway path now awaits the moderation gate before writing the finish/usage chunk (it already did before [DONE]).
  • The conversation total is informational: a moderation verdict landing after the gate failed open, or a blocked request, is recorded server-side but not reported to the client.

albanm and others added 12 commits September 30, 2026 11:05
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…dmins

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… usage

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rrors

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the raw message

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Without a userId the upsert filter matched an arbitrary usage document
of the owner (an external user's record or the untrusted pool).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The gate is awaited before the finish chunk, so it is never pending there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
'user1' is not a role: it mapped to no quota, so these members could
not exercise the simple-user profile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@albanm
albanm merged commit 39cc1e0 into main Oct 1, 2026
4 checks passed
@albanm
albanm deleted the feat-self-limits-consumption branch October 1, 2026 12:14
albanm added a commit that referenced this pull request Oct 6, 2026
All three PRs changed the in-browser loop and the gateway, which this branch deletes, so this
merge resolves conflicts mechanically and the behaviour is carried over onto the server loop in
the commits that follow - one per behaviour, each with its tests:

  - #73/#75 wait handover: `wait_for_user_action` requires a `message`, repairs a small-model
    slip, refuses a wait with no message. Merged here (it lives in shared/host-events.ts); the
    server's rendering of the message in the transcript follows.
  - #73/#75 interrupted turns: an aborted turn's open calls keep a result saying why, and the
    next turn carries a reminder of what the wait was for. Follows.
  - #74 self-service consumption, redacted quota refusals, conversation cost. Follows.

Resolved here:

  - Deleted on this branch, modified on main: kept deleted - api/src/gateway/router.ts,
    ui/src/composables/{use-agent-chat,agent-stream-parts}.ts and their two specs.
  - New on main and built on the deleted code, set aside to come back adapted:
    interrupted-turn.ts and its spec, gateway-cost.ts and its spec, the two self-usage specs, the
    "speaking during a wait" e2e (it reads the browser's /chat/completions request), and two
    moderation-cost api tests (gateway-shaped, and anonymous).
  - api/src/usage/enforce.ts: this branch's `checkAccountCreditCap` (re-checked between steps of
    a long run) and main's `accountViolation` (the cap plus the untrusted pool, also read by the
    self-usage view) compose - the cap check stays one definition.
  - shared/host-events.ts: main's `repairWaitInput` moves here from the deleted stream-part
    builder, beside the tool that uses it; verified byte-identical.
  - dev/resources/organizations.json: main's `user1` -> `user` role fix, plus this branch's NHI
    members. Takes effect when simple-directory reloads its fixtures.
  - The debug dialog shows its Consumption tab only when the chat can report usage, which the
    #74 port provides; until then its e2e keeps the two-tab expectation.
  - The wait-tool specs pass the now-required `message`.
  - ui/dts/auto-imports.d.ts: the running dev server wrote entries for main's set-aside files, and
    the committed copy already declared 11 modules this branch had deleted; pruned to what exists.

host-events.md and quotas-usage.md merged main's text, which describes the browser mechanisms;
each port commit rewrites its section for the server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
albanm added a commit that referenced this pull request Oct 6, 2026
- GET /api/usage/:type/:id/self replaces the gateway's /usage route.
- A refusal is stored as English text plus a data-refusal part (quota scope,
  period, reset; or moderation), redacted through quotaErrorBody like main's
  429 body; the chat renders it in its own language. The quota text named the
  shared budgets' numbers to every caller until now.
- The moderation call is spent onto the turn's run and recorded in its
  telemetry, so a blocked turn is no longer free in the conversation's total.
- The session sends a `cost` frame (conversationCost, summed over runs) on
  attach and after every turn; the Consumption tab shows it. Exact, unlike
  main's total summed from response usage chunks.
- Removes ui/src/utils/error.ts: both helpers read the gateway's errors and
  had no caller left.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant