Repository navigation
feat(usage): per-person AI allowances — server cap + workspace limits, enforced at the model scope - #567
Conversation
…del scope
Every model call is now recorded against the person it was for: the meter
row key (ai_usage_daily) gains user_id, and withWorkspaceChatModels takes
{ feature, userId, enforce } in place of a bare feature. Request handlers
pass the signed-in person; Inngest jobs, after() continuations, meetings
and the worker's indexing pass whoever started the work (event data, the
run row, the meeting's creator, the source's uploader) with enforce:false,
so running work is counted and never stopped midway.
Two limits, the tighter binds, by calendar month in UTC:
- AI_MONTHLY_TOKEN_LIMIT_PER_USER caps one person's tokens on the server's
own models across every workspace they belong to (calls to a workspace's
own endpoint never count; AI_TOKEN_LIMIT_EXEMPT_EMAILS are never capped).
- usage.memberMonthlyTokenLimit is a workspace default, and
member_ai_limits holds an admin's override for one person (none, or N).
A person at a limit gets a scope whose every model resolution throws
AiAllowanceExceededError (429), which describeChatResolutionFailure and
both shared route error handlers word for the screen. Request routes and
run-start routes (prospects, distribution, client prospector, trends,
repo explainer, predictive analysis, weekly reviews, proposals, scenarios)
also check first, so they answer 429 before doing or queueing any work.
Uploads are not refused; their indexing is counted.
New routes: GET /api/ai-allowance (your own), GET
/api/settings/usage/members (analytics.view) and PUT
/api/settings/usage/members/[userId] (settings.manage, audited).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…starter in its test The allowance readout ran through the few-second cache meant for checks that re-enter during one request, so the chat's notice lagged an answer behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lowance, the chat's notice Settings › Usage and costs gets "AI limits per person": the workspace default (usage.memberMonthlyTokenLimit), a read-only note for the server's cap, and a table of each member's use this month against the limit that binds them, with an Edit limit dialog (workspace default / no limit / custom) for settings.manage. Settings › Account gets "Your AI allowance this month": used of limit, the reset date, and who can change it. The Ask chat reads the allowance on mount and after each answer: from 80% an amber line above the composer, at the limit a red one and sending stops. A refused answer (code ai_allowance_reached, threaded through useAIChat into ThreadMessage.failureCode) offers no Retry. Copy in English and Simplified Chinese. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9K of a 2M cap read as 0%, as if unused. At the limit the composer's notice already says it, so the thread's error bar no longer repeats it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their fake database answers selects from a queue; the allowance read took the document lookup's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-limits-5e23fb # Conflicts: # apps/web/src/i18n/messages/en/errors.json # apps/web/src/i18n/messages/zh-CN/errors.json
Main moved workspace settings into the Workspace tab (#560); the refusal, the chat notice, the allowance card and the docs still sent people to Settings › Usage and costs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 87b8a155-13bf-476c-a21a-bbefdd1b8895) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be50f7cf38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| function workspaceLocalRuntime(companyId: bigint): AgentRuntime { | ||
| function workspaceLocalRuntime(companyId: bigint, createdBy: string | null): AgentRuntime { | ||
| const local = getLocalRuntime(); | ||
| const aiUse = { feature: "meetings", userId: createdBy, enforce: false } as const; |
There was a problem hiding this comment.
Check the allowance before starting a new meeting
A member who has already exhausted either allowance can still create new meetings and run model-backed turns indefinitely. In the inspected POST /api/collab/meetings flow, neither the route nor createMeetingForCompany checks the allowance, and this unconditional enforce: false also exempts the first turn of every newly created meeting. Consequently, the server cap does not protect the hosted model bill through this path. Check the creator's allowance before starting new meetings while preserving the exemption for meetings already underway.
Useful? React with 👍 / 👎.
| const aiUse = { | ||
| feature: "companyProfile", | ||
| userId: changedBy, | ||
| enforce: false, |
There was a problem hiding this comment.
Gate explicitly requested profile rebuilds before scheduling
When a member is already at their allowance and the workspace has sources requiring model extraction, POST /api/company/profile still schedules a new rebuild: the inspected route calls startRebuild without an allowance check, and this scope disables enforcement unconditionally. This allows new, explicitly requested AI work to consume server tokens after the cap is reached; it is not merely letting previously accepted work finish. Check the allowance in startRebuild before marking the profile as building and scheduling the continuation, retaining enforce: false inside the accepted background job.
Useful? React with 👍 / 👎.
…-limits-5e23fb # Conflicts: # apps/web/drizzle/meta/_journal.json # apps/web/src/i18n/messages/en/errors.json # apps/web/src/i18n/messages/zh-CN/errors.json
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: 7236aae3-bbdc-446d-9210-3c25c2221e61) |
What this does
Adds per-person AI allowances. Every AI model call is now recorded against the person it was for, and a person who reaches their monthly limit is refused new AI work with a sentence that says which limit, when it resets and who can raise it.
Two limits can apply. The tighter one binds. Both count AI tokens in + out (as Workspace › Usage and costs reports them) by calendar month, UTC.
AI_MONTHLY_TOKEN_LIMIT_PER_USER(env)AI_TOKEN_LIMIT_EXEMPT_EMAILSare never capped. Doesn't apply while a workspace runs on its own endpoint. Unset/0 = off.settings.manage, in Workspace › Usage and costs › AI limits per personusage.memberMonthlyTokenLimit, 0 = none) plus a per-person override (none, or N tokens)The server cap exists because anyone who creates a workspace owns it, so a workspace limit alone can't protect the hosted model bill.
With nothing configured, nothing changes. Both limits are off by default, in self-hosted and cloud mode.
How it works
ai_usage_dailygainsuser_id(in the row key, declared last for the CI column-order check).withWorkspaceChatModels(companyId, use, fn)now takes{ feature, userId, enforce }where it took a bare feature. A bare feature still means "work no one person started".after()continuations and the company-profile rebuild pass whoever started them: from event data (userIdadded to the predictive-analysis, repo-explainer and scenario events, optional for events already queued), from the run row, or fromcreatedByActorId.document_versions.uploaded_by).erroris anAiAllowanceExceededError(a 429ChatScopeUnavailableError), so every model resolution inside throws.describeChatResolutionFailureand both sharedhandleRouteErrors word it for the screen.refuseAtAiAllowancefirst, so they answer429 { code: "ai_allowance_reached" }before doing or queueing any work. Proposals and scenarios throw from their services.enforce: false: they're counted, not refused. The check runs when a request starts, so one request can carry someone slightly past the limit.member_ai_limitstable (row absent = workspace default, null = no limit), not in the settingsmemberscope, which is "your own preference" and unaudited. Changes are audited assettings.changed.GET /api/ai-allowancereturns your own allowance (any member).GET /api/settings/usage/members(analytics.view) returns each member's use and limits.PUT /api/settings/usage/members/[userId](settings.manage) sets one person's limit.docs/deployment.md(new section) and.env.example.Verification
tsc --noEmitis clean for web and worker. Lint is clean on every changed file; the four warnings left existed on main.permission-gates.test.ts;DATABASE_URLset, except five DB integration suites that time out under parallel workers against a shared database; they pass with--runInBand, which is how CI runs them.ai_usage_dailyunder their id;For the reviewer
20261027054725_member_ai_allowances(new table, new column + index, row-key index rebuilt) needs to run on deploy. Nothing is deployed. It was regenerated after main's20261026001358_feedback(feat(feedback): in-app bug reports and feature requests for the testing phase #568) so the snapshot chain stays linear; the SQL is byte-identical to the original, and both drift probes are clean.packages/llm:ChatScopeUnavailableError.statuswidened from the literal503tonumber, so a host can subclass it with its own status.🤖 Generated with Claude Code
Note
Medium Risk
Broad changes across AI entry points and metering schema affect billing-adjacent behavior, though limits default off, failed quota reads fail open, and coverage includes integration and UI tests.
Overview
Introduces per-person monthly AI token allowances with two independent caps (server-wide on hosted models vs workspace default + per-member overrides). Usage is attributed via
user_idon daily AI usage rows and enforced before work starts (refuseAtAiAllowance→ HTTP 429ai_allowance_reached) and insidewithWorkspaceChatModelsso model resolution fails at the limit.Data & config: migration adds
pdr_ai_v2_member_ai_limitsanduser_idonai_usage_daily;.env.exampledocumentsAI_MONTHLY_TOKEN_LIMIT_PER_USERand exempt emails.API surface:
GET /api/ai-allowance(self),GET/PUT /api/settings/usage/members(analytics.view / settings.manage). Many existing AI routes gain the upfront refusal gate and pass{ feature, userId, enforce? }into the model scope; background/scenario paths useenforce: falseso in-flight work still counts but is not blocked.Product UI: Workspace › Usage gets per-member limits table and edit dialog; Account shows personal allowance; Ask chat shows warn/block notices, disables send at limit, and suppresses useless retries on allowance errors.
Reviewed by Cursor Bugbot for commit d8d7315. Bugbot is set up for automated code reviews on this repo. Configure here.