feat(chat): per-user daily token budget for non-admin users (#62) - #63
Conversation
The k8s manifests (#56/#57) predate the change that moved all tpk streams under a dedicated `tpk` database (#59). For app-only — which connects to an externally-managed Timeplus Enterprise — the app now runs `CREATE DATABASE IF NOT EXISTS tpk` on startup, so the pre-existing DB user needs CREATE DATABASE (first run) plus read/write on that database. A restricted user would otherwise fail at startup/ingest. - app-only.yaml: header now states the app creates its own database on the existing server; add a `TIMEPLUS_DATABASE` env (default `tpk`) with the privilege note and the pre-create fallback for restricted users. - deploy/k8s/README.md: add a "tpk database" note to the App-only section (CREATE DATABASE grant + pre-create SQL), and a TIMEPLUS_DATABASE row to the Configuration table. Notes that enterprise/allinone are unaffected (they provision a full-privilege tpk user). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
Operators can cap daily LLM token consumption per non-admin user so a single user can't run up unbounded cost against the shared agent. admin (the reserved super-role) is exempt. (Token budget only; the optional question cap from #62 is deferred.) - usage.py: append-only `chat_usage` stream (ts, username, tokens) written per turn independent of TPK_CHAT_AUDIT, plus DbUsage (used_today/record). Reads fail OPEN (unreachable store never blocks a user); writes are best-effort. - db: create chat_usage in ensure_schema (+ drop_schema); add kg_roles.daily_token_limit (uint32) with the same _add_column_if_missing lazy migration as capabilities (existing roles -> 0 = unlimited). - auth.Role + kg_roles carry daily_token_limit; api /roles create/update/list carry it (validated >= 0). - config.daily_token_limit(): global fallback (env TPK_DAILY_TOKEN_LIMIT > [server].daily_token_limit > 0) applied when a role sets no limit of its own. - server /chat: sum usage_metadata tokens across the turn's model calls; before running the agent, block a non-admin who is at/over budget with HTTP 429 + reset time (next UTC midnight). Enforcement is next-turn (a turn's cost is only known once it runs); the crossing turn completes. Metering + enforcement only active when a usage store is present (production), so unit tests that inject a fake agent are untouched. - web: role editor gains a "Daily token budget per user" field (add + edit); chat surfaces the 429 with the server's friendly reset message. - tests: usage helpers + fail-open, config precedence, enforcement (over/under/ admin-bypass/no-store), and backend round-trips (role limit, daily count). Verified end-to-end on live timeplusd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
Surface the budget proactively, not only on a 429. Adds GET /chat/usage
(chat-cap gated) returning {limited, used, limit, remaining, reset} for the
current user — limited:false for admins, unlimited roles, or when no budget is
enforced, so the UI shows no indicator. Extracts the effective-limit rule into
usage.effective_daily_limit (role's own limit, else the global fallback), reused
by /chat enforcement and the new endpoint.
Chat UI fetches /chat/usage on load and after each turn (a turn spends tokens;
the append-only read lags slightly so it refreshes again shortly after), and
shows "used / limit", tokens left, and the reset time in the header and empty
state for users who have a budget.
Tests: /chat/usage for a limited user, and unlimited for admin / no-store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
|
Added user-facing budget display (commit c05ca94): a new |
The global daily-token-budget fallback was documented in .env.example but not wired into the compose env, so setting it in .env had no effect. Pass it through in both the DB+App and all-in-one files (a role's own daily_token_limit still overrides it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
Adds a per-user daily token limit that overrides the role limit, so admins can tune budgets for individual users, not just per role. Effective limit precedence is now: user override > role limit > global fallback (0 = unlimited at each level). - db/auth: kg_users.daily_token_limit (uint32) with _add_column_if_missing migration (existing users -> 0 = inherit); User field + parse/upsert. - usage.effective_daily_limit(user_limit, role, global) implements the precedence; /chat enforcement and /chat/usage both pass the user's override. - api: AddUser/UpdateUser carry daily_token_limit (validated >= 0); /users list returns it; update leaves it unchanged when omitted. - web Users tab: a "Daily tokens" column with a per-user number input (admins show "unlimited"), and a budget field in the Add-user modal. - tests: precedence unit test, user-override enforcement + /chat/usage reflect it. Verified E2E on live timeplusd incl. the kg_users column migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
Change the built-in fallback from 0 (unlimited) to 500000 tokens/user/day, so non-admin users are capped by default. A per-user or role daily_token_limit still overrides it, and setting TPK_DAILY_TOKEN_LIMIT / [server].daily_token_limit to 0 restores unlimited-by-default. Updates the README matrix, repos.toml / repos.container.toml comments, .env.example, and the config precedence test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
Code reviewFound 2 issues:
timeplus-knowledge/src/tpk/usage.py Lines 24 to 33 in 802adfa timeplus-knowledge/src/tpk/auth.py Lines 91 to 94 in 802adfa
timeplus-knowledge/src/tpk/api.py Lines 429 to 436 in 802adfa 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
#63) Code review of #63 found two issues: 1. Role/user daily_token_limit of 0 means "inherit the next level down", not "unlimited" (only a 0 that reaches the global fallback is unlimited). The role-level comments, API error, effective_daily_limit docstring, and role UI labels wrongly said "0 = unlimited" — which, now that the global default is 500000, would mislead an admin into thinking a role is uncapped when it's capped at 500000. Relabel role level to "0 = inherit global" everywhere; the /chat/usage docstring's "degrades to unlimited" corrected to "global default". (The per-user field was already labeled correctly.) 2. daily_token_limit bypassed the bounded-delegation guard: a non-admin users:manage delegate could set any manageable user's/role's budget to an arbitrary value, handing out unlimited LLM spend. Add _guard_token_limit — only an admin may change a budget; a delegate may still manage users/roles as long as it leaves the budget unchanged (403 otherwise). Tests: role-0-inherits-global precedence; manager-cannot-set-budget (verified against live timeplusd). Full suite green; web build + sanitize pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPzYZXxyTdj5G25KoujpHb
|
Both issues fixed in b3d45ef:
Full suite green; web build + sanitize pass. |
Implements the token-budget half of #62 (question cap deferred, per your call).
What
Operators can cap daily LLM token consumption per non-admin user.
adminis exempt. The limit is per-role (daily_token_limit), with a global fallback via config.How
/chathandler sumsusage_metadatatokens across every model call in a turn (the tool loop makes several), with aresponse_metadatafallback for gateways that only pass the raw shape.chat_usagestream (ts, username, tokens), written independent ofTPK_CHAT_AUDITso the budget can't be silently disabled by turning auditing off. Reads fail open (an unreachable store never wrongly blocks a user); writes are best-effort.kg_roles.daily_token_limit(uint32,_add_column_if_missingmigration; existing roles → 0 = unlimited). Effective limit = the role's own, else the globalTPK_DAILY_TOKEN_LIMIT/[server].daily_token_limit(0 = unlimited).Enforcement/metering are only active when a usage store is present (built in the production path), so existing unit tests that inject a fake agent are untouched and never hit a DB.
Caveat (documented in the issue)
Some gateways omit
usage_metadata— those turns count as 0 tokens (free). A token budget is only as good as the meter; verify against your gateway.Tests / verification
webbuild +check:sanitizegreen.🤖 Generated with Claude Code