Repository navigation
Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) - #178
Conversation
Expensive models use several daily quota units per request (Opus 5, Sonnet 2, cheap models 1), and a user whose logged AI spend this UTC month reaches the budget ($3) is refused until next month. Weights, budget and tier defaults live in _shared/usage-limits.ts. The limit dialog explains a weighted refusal and has its own monthly variant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 52 minutes. View limit details
ℹ️ Recent review info
📝 Walkthrough
Note 🎁 Summarized by CodeRabbit Free
Comment |
Codex review: those pages printed the server's English reason. The limit dialog's text now comes from one helper that getUserFacingAIError and Sparring also use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/components/features/subscription/LimitReachedModal.tsx
… spend Adds ai_requests.cap_cost_usd (OpenRouter credit charges plus Exa and ElevenLabs) and a new consume_ai_quota function for the weighted daily quota and the weekly cap. consume_daily_quota is left unchanged, so the migration is additive. Old rows have NULL cap spend and never count.
|
Deployment failed for project debatelab-staging with the following error: Learn More: https://vercel.com/adam-gaborjanis-projects?upgradeToPro=build-rate-limit |
|
Deployment failed for project debate-navigator with the following error: Learn More: https://vercel.com/adam-gaborjanis-projects?upgradeToPro=build-rate-limit |
AISubscriptionError now carries the catalog text for coded refusals, so pages that print error.message no longer show the server English reason on Spanish and Hungarian screens. A model heavier than the whole daily limit gets its own message (server and client) instead of wait until midnight.
* Sign out a revoked session and fix the email code check (H1, M4) H1: when Supabase refuses a signed-in request (401 from REST or Functions, 401/403 from Auth's user lookup), force a token refresh outside the request. If Auth refuses that too, sign this browser out locally and show "Your session expired. Sign in again." on /login. Outages, 5xx and rate limits keep the session. M4: the code field keeps digits only, Verify waits for six digits, and a refused code says it may be wrong or expired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Handle the daily AI limit the same way in Crossfire, Sparring and Settings (M1, M2, L2) M1: when Crossfire's first AI turn is refused for the daily limit, the round goes back to setup (no clock) and the daily-limit dialog Ask uses opens. A round with exchanges keeps its state and can still be ended. Ending a round with no exchange by hand no longer shows an error. M2: Sparring setup warns when today's remaining requests are fewer than the round needs, and an opponent or judge turn refused for the limit opens the same dialog. L2: one shared dialog component for every 402 in these tools, a localized inline message instead of the server's English text, and Settings shows requests used today and when they refresh. The client count now leaves out refused calls and jev_* helper rows, reads the account's own rate_limits override, and is null (never blocking) when it cannot be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save each researched page once per debate (M3) Auto-saved sources are matched per debate by a normalized URL (host lower-cased without www, no scheme, fragment, tracking parameters or trailing slash). A page already in the debate's Library gets its research metadata refreshed instead of a second row; the student's title, folder and on/off choice stay. Batches from tools that finish together run one after another so each sees the rows the last one saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let Library titles wrap on phones and enlarge row controls (M5) Below the sm breakpoint a source title takes two lines instead of one truncated line, and the type badge (already shown by the row icon) is hidden to give it room. On touch screens the delete button is 44px and the whole 44px square around the on/off switch toggles it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop telling every account it signed in with Google (L1) Settings no longer hard-codes "Signed in with Google". Under the email it shows how the account signs in, from what Auth recorded: Google, email code, both, or demo code. The description keeps only the API key sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name flowchart edges by their nodes and translate the keyboard help (L3) Edges are announced as "From Thesis to Argument 1" (computed for the canvas only, never saved) instead of "Edge from thesis to c1". React Flow 11 has no prop for its keyboard help text, so a small component puts the EN/ES/HU help into the two description elements it renders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the Attack switches and the Ask message box accessible names (L4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Title each app page and keep private pages out of search (L4, L5) /login, /app/* and /share/* (including a share link that no longer works) get a page title from the existing route labels, such as "Generate · DebateLab", and robots "noindex, nofollow" while on screen, matching the X-Robots-Tag vercel.json already sends. /expocit/sources is left as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document session expiry, source dedupe and the usage estimate; follow the Ask label in replays architecture.md describes the revoked-session check (H1), one Library row per page per debate (M3), and what the remaining-requests estimate can and cannot know (M1, M2, L2). The Ask message box's accessible name is now its label (L4), so the two recorded agentic replays target that name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add verify recipes for a revoked session and for running out of daily requests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix two things the browser checks caught (M5, L4) M5: on touch screens the Library on/off switch gets its 44px tap area from a pseudo-element, the same way the Checkbox does. Wrapping it in a label did not forward taps reliably. L4: page titles end in "DebateLab" (the shell's title) rather than the longer brand name from site.json, as the QA report asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show daily-limit errors inline in the dialog's words (L2) getUserFacingAIError now turns a daily-limit or trial refusal into the limit dialog's own text, so Crossfire and Sparring stop printing the server's English message. Sparring's change matches open PR #178 line for line, so that file merges cleanly; #178's fuller helper in error-display.ts should replace this one when it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # src/lib/ai/error-display.ts
refund_daily_quota takes p_units (default 1, the reservation every AI request makes today) and never goes below zero. Once the weighted quota in #178 lands, the refund can return what consume_ai_quota reserved for the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… out of the weekly cap. #178 reserves each request's weight (Opus 5, Sonnet 2, others 1; Auto Pro 5) through consume_ai_quota. A refunded Claude failure now gives back the same units checkRateLimit reserved, taken from its RateLimitResult, and logs cap_cost_usd 0 so it does not count toward the weekly $2 either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…185) * Run Claude only on the Anthropic BYOK key, with no fallback provider. Every anthropic/* call now asks OpenRouter for the anthropic provider only, with fallbacks off, so Bedrock, Vertex and other providers are never tried. Other models keep balanced routing. A refused Claude call returns a 503 outage message the client already translates, logs claude_unavailable, and gives the reserved daily request back through a new refund_daily_quota RPC. A Claude response whose usage shows is_byok false or a cost above zero logs a claude_not_byok warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refund a failed Claude request only when nothing was written yet. A stream that fails after the model wrote text, or a repair retry after a completed attempt, already spent tokens on the key, so the request stays counted. The student still sees the outage message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep paid research out of Claude refunds and stop the Ask double attempt. From the Opus review of #185: - A request whose research steps already ran on Claude keeps its quota. - Ask no longer resends a provider_unavailable stream failure through the plain endpoint, which tried Claude a second time on a failing key. - ai-stream refunds before it sends the error, so an immediate retry sees it. - Sparring shows the translated provider message instead of the raw one. - Tests cover no refund on success, on a non-Claude failure, after a completed repair attempt, and after research. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let a quota refund give back more than one unit. refund_daily_quota takes p_units (default 1, the reservation every AI request makes today) and never goes below zero. Once the weighted quota in #178 lands, the refund can return what consume_ai_quota reserved for the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the refund migration its own version. #180 added 20261022120000_opus_default_to_sonnet.sql, so the refund migration's identical version failed to apply after the merge from main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refund the weighted units consume_ai_quota reserved, and keep refunds out of the weekly cap. #178 reserves each request's weight (Opus 5, Sonnet 2, others 1; Auto Pro 5) through consume_ai_quota. A refunded Claude failure now gives back the same units checkRateLimit reserved, taken from its RateLimitResult, and logs cap_cost_usd 0 so it does not count toward the weekly $2 either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never refund a Claude failure OpenRouter already charged credits for. Zeroing cap_cost_usd on such a row would hide real spend from the weekly cap, so a request with a charge against the cap now keeps both its quota and its cap cost. Only free failures are refunded and logged with cap cost 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refund research tools in a Claude outage too. Research steps run on Haiku, so in an outage they are refused along with the main call. Such a refusal did no paid work and no longer blocks the refund; a research step that finished or was charged still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bill usage on a stream's error frame, and never refund a cancelled request. From Codex review round 2 of #185: - Both stream readers threw on an error frame before reading its usage, so a charge reported only there was dropped, and the failure could then be refunded with cap cost 0. Usage is now kept before the error is thrown. - ai/index.ts refunded a Claude refusal that raced a client abort. ai-stream already refused that; the plain handler now does too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Cost fix 4 of 7. Expensive models use up the daily AI allowance faster, and each user has a $2 weekly AI spend cap that leaves out Claude billed to our own Anthropic key (BYOK).
What changes for users
daily_limit_overridebelow 5), the message says "This model counts as 5 uses, more than your daily limit of 3. Pick a lighter model." It no longer tells them to wait.error.messagedirectly. Spanish and Hungarian users no longer see the server's English reason.What counts toward the $2
There's a new column,
ai_requests.cap_cost_usd, holding the part of a request's cost that counts:usage.cost, which is what OpenRouter charged against our credits.is_byok: true) adds only OpenRouter's fee, usually $0, because Anthropic bills our own key.is_byok: false) has its full price inusage.cost, so it counts in full.cost_usdon old rows, can't push anyone toward the cap.I chose this over matching the model name (as Adam asked). Research helper calls and Exa are merged into the parent request's row, so the row's
modelcolumn doesn't say where each charge came from, but OpenRouter's own credit charge does.Where to change the numbers
Everything lives in
supabase/functions/_shared/usage-limits.ts:WEEKLY_AI_BUDGET_USD = 2(nullturns the cap off)MODEL_QUOTA_UNITSChanging a number needs an
aiandai-streamdeploy, not a migration.Migrations (additive: no DROP, no data rewrite)
20261022130000_index_ai_requests_user_created.sql:CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ai_requests_user_created ON public.ai_requests (user_id, created_at);It's in its own file because of CONCURRENTLY.20261022130001_weighted_quota_weekly_budget.sql:ALTER TABLE public.ai_requests ADD COLUMN IF NOT EXISTS cap_cost_usd numeric;The column is nullable with no default, so existing rows aren't rewritten. It also adds a column comment.CREATE FUNCTION public.consume_ai_quota(p_user_id uuid, p_limit integer, p_units integer DEFAULT 1, p_weekly_cap_usd numeric DEFAULT NULL, p_quota_key text DEFAULT 'ai') RETURNS TABLE (allowed boolean, current_count integer, denied_by text). It's SECURITY DEFINER withsearch_path = publicand raises unlessauth.role() = 'service_role'and the arguments are sane (units ≥ 1, cap ≥ 0).cap_cost_usdsincedate_trunc('week', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'and refuses withdenied_by = 'weekly'once the sum is ≥ the cap, without touching the counter.'daily'whenp_units > p_limit.INSERT … ON CONFLICT … DO UPDATE … WHERE request_count + p_units <= p_limitondaily_usage_counters, the same counter row and key (ai) the old function uses.REVOKE ALL … FROM PUBLIC, anon, authenticated; GRANT EXECUTE … TO service_role.consume_daily_quotais not touched. Import, share, voice and Crossfire signals keep calling it. Changing its arguments or result columns in place would have needed a DROP, so the weighted path got a new function instead.Deploy order: apply the migrations first, then deploy
aiandai-stream.cap_cost_usdinsert would fail, so usage rows wouldn't be logged.Rollback: redeploy the previous
aiandai-stream. The new column and function can stay; nothing else uses them.Verification (local, VPS; GitHub Actions is blocked by billing)
npm run verifyon50498c12passes: docs, lint, typecheck, 342 unit tests, 30 collaboration tests, 369 edge tests, build, test:sw, and 30/30 @smoke e2e.supabase/postgres:17.11.0.004container instead (with storage stubs).ai_quota_weekly_budget_test.sqlpasses 17/17 anddaily_usage_quota_test.sql14/14. The other failures in that container were environment-only (no GoTrue columns; pgTAP inpublic). Rerun./.superset/db-gate.sh --db-onlybefore merge.supabase/tests/ai_quota_weekly_budget_test.sql(17 tests). It covers:consume_daily_quotastill has one overloadaicountercost_usd, including backfilled ones, never countsupabase/tests/daily_usage_quota_test.sqlandscripts/test-daily-quota.share back to main's versions.recipes/usage-limits.spec.tswas updated to the weekly dialog and acap_cost_usd: 2row. I didn't run it in this round.Reviews: Codex is rate-limited. The Opus review of
59d49c7had one low finding, the wrong advice when a model can never fit, which is fixed here. Codex round 1 had one low finding, English refusals on es/hu screens, also fixed here.Overlap
normalizeOpenRouterUsagesoreportedCostUsd= fee + upstream for BYOK. Whichever lands second must keepcapCostUsd: usage.cost(the credit charge only) in that function. Exa usage needs no change: it counts through thereportedCostUsdfallback.