Skip to content

Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) - #178

Merged
Programcode merged 10 commits into
mainfrom
cost/weighted-quota-budget
Oct 22, 2026
Merged

Programcode merged 10 commits into
mainfrom
cost/weighted-quota-budget

Conversation

@Programcode

@Programcode Programcode commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

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).

Not for merge yet: Adam has to approve the migrations below first.

What changes for users

  • An Opus request counts as 5 of the day's requests, GPT-6.1 Sol as 3, Sonnet and DeepSeek V4 Pro as 2, and every other model as 1. Auto Pro counts as 5 because the quota is reserved before Auto picks a model. Auto Fast counts as 1.
  • If a request doesn't fit in what's left today, the dialog says "This model counts as 5 uses, and you have 4 left today. Pick a lighter model or wait until midnight UTC." A lighter model still runs.
  • If a model counts for more than the user's whole daily limit (only possible with a hand-set daily_limit_override below 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.
  • Once a user's AI spend for the current week reaches $2, every AI request is refused with a "Weekly AI Allowance Used" dialog. It has an OK button and no upgrade offer. The week starts Monday 00:00 UTC.
  • Refusal text now comes from the en/es/hu catalog everywhere, including pages that print error.message directly. 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:

  • OpenRouter calls count OpenRouter's usage.cost, which is what OpenRouter charged against our credits.
    • A BYOK Claude call (is_byok: true) adds only OpenRouter's fee, usually $0, because Anthropic bills our own key.
    • A Claude call that falls back to OpenRouter credits (is_byok: false) has its full price in usage.cost, so it counts in full.
  • Non-OpenRouter charges count in full: ElevenLabs voice now, and Exa research once Log the real cost of Anthropic (BYOK) calls and Exa research #179 lands (any usage record without a separate cap figure counts its full reported cost).
  • Partly priced requests: when part of a request's cost is unknown, the known part counts. The cap is a lower bound, not all-or-nothing.
  • Old rows don't count: a row with no known cap spend, or logged before this column existed, has NULL and is never counted. So Log the real cost of Anthropic (BYOK) calls and Exa research #179's backfill, which only rewrites cost_usd on old rows, can't push anyone toward the cap.
  • Side effect: spend from earlier in the deploy week doesn't count. Cost is logged after the provider answers, so a user can go past the cap by about one request.

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 model column 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 (null turns the cap off)
  • MODEL_QUOTA_UNITS
  • the trial and Pro daily defaults, 5 and 50

Changing a number needs an ai and ai-stream deploy, not a migration.

Migrations (additive: no DROP, no data rewrite)

  1. 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.
  2. 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 with search_path = public and raises unless auth.role() = 'service_role' and the arguments are sane (units ≥ 1, cap ≥ 0).
      • It sums cap_cost_usd since date_trunc('week', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' and refuses with denied_by = 'weekly' once the sum is ≥ the cap, without touching the counter.
      • It refuses with 'daily' when p_units > p_limit.
      • Otherwise it does one atomic INSERT … ON CONFLICT … DO UPDATE … WHERE request_count + p_units <= p_limit on daily_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_quota is 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 ai and ai-stream.

  • New functions on the old schema would fail closed with "Unable to verify your quota", and their cap_cost_usd insert would fail, so usage rows wouldn't be logged.
  • Old functions on the new schema keep working unchanged.

Rollback: redeploy the previous ai and ai-stream. The new column and function can stay; nothing else uses them.

Verification (local, VPS; GitHub Actions is blocked by billing)

  • Full npm run verify on 50498c12 passes: docs, lint, typecheck, 342 unit tests, 30 collaboration tests, 369 edge tests, build, test:sw, and 30/30 @smoke e2e.
  • pgTAP: the shared DB gate lock was held by another workspace's stack, so I applied all migrations to a throwaway supabase/postgres:17.11.0.004 container instead (with storage stubs). ai_quota_weekly_budget_test.sql passes 17/17 and daily_usage_quota_test.sql 14/14. The other failures in that container were environment-only (no GoTrue columns; pgTAP in public). Rerun ./.superset/db-gate.sh --db-only before merge.
  • New pgTAP file supabase/tests/ai_quota_weekly_budget_test.sql (17 tests). It covers:
    • grants and that consume_daily_quota still has one overload
    • weighted reservations, a request that exactly fills the limit, a request bigger than the limit, and zero units
    • that the old function shares the ai counter
    • the weekly cap ignoring BYOK rows (cost_usd only), NULL cap rows, last week's rows and other users' rows
    • a NULL cap turning the check off
    • that rows with only cost_usd, including backfilled ones, never count
    • that non-service callers are refused
  • supabase/tests/daily_usage_quota_test.sql and scripts/test-daily-quota.sh are back to main's versions.
  • Edge tests cover cap cost for BYOK (0), credit-billed Claude (full), merged calls, an Exa-style charge, a partly unknown request (known part only), voice, and the server's too-large message.
  • Vitest covers translated refusal messages.
  • The skill recipe recipes/usage-limits.spec.ts was updated to the weekly dialog and a cap_cost_usd: 2 row. I didn't run it in this round.

Reviews: Codex is rate-limited. The Opus review of 59d49c7 had 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

Programcode and others added 3 commits October 22, 2026 11:14
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>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 11:30
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Oct 22, 2026 1:27pm UTC
debatelab-staging Ready Ready Preview Oct 22, 2026 1:27pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 52 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 50498c1 and 85aa5c1.

📒 Files selected for processing: 8
  • docs/architecture.md
  • src/lib/ai/error-display.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • supabase/functions/ai-stream/index_test.ts
  • supabase/functions/ai/handler-research.test.ts
  • supabase/functions/ai/stream.test.ts
ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 34fb585 and 50498c1.

📒 Files selected for processing: 39
  • .claude/skills/verify-debatelab/features/README.md
  • .claude/skills/verify-debatelab/features/usage-limits.md
  • .claude/skills/verify-debatelab/recipes/usage-limits.spec.ts
  • docs/architecture.md
  • src/components/features/subscription/LimitReachedModal.tsx
  • src/hooks/use-subscription-error.ts
  • src/lib/ai/error-display.ts
  • src/lib/ai/errors.ts
  • src/lib/ai/limit-message.test.ts
  • src/lib/ai/limit-message.ts
  • src/lib/ai/request.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/AskPage.tsx
  • src/pages/AttackPage.tsx
  • src/pages/BrainstormPage.tsx
  • src/pages/FactoidsPage.tsx
  • src/pages/GeneratePage.tsx
  • src/pages/SparringPage.tsx
  • src/test/security-hardening.test.ts
  • supabase/functions/_shared/cost.test.ts
  • supabase/functions/_shared/cost.ts
  • supabase/functions/_shared/entitlements.test.ts
  • supabase/functions/_shared/entitlements.ts
  • supabase/functions/_shared/usage-limits.ts
  • supabase/functions/ai-stream/handler_test.ts
  • supabase/functions/ai-stream/index.ts
  • supabase/functions/ai-stream/index_test.ts
  • supabase/functions/ai/debate-context.test.ts
  • supabase/functions/ai/generate-honesty_test.ts
  • supabase/functions/ai/handler-research.test.ts
  • supabase/functions/ai/index.ts
  • supabase/functions/ai/provider-errors_test.ts
  • supabase/functions/ai/stream.test.ts
  • supabase/functions/ai/voice.test.ts
  • supabase/migrations/20261022130000_index_ai_requests_user_created.sql
  • supabase/migrations/20261022130001_weighted_quota_weekly_budget.sql
  • supabase/tests/ai_quota_weekly_budget_test.sql

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The PR adds model-weighted daily AI quotas and a weekly AI spend cap. It records cap-cost amounts, enforces both limits in quota reservations, and returns limit details for localized client messages. Tests and documentation cover the quota rules and refusal outcomes.

Changes: AI usage limits

Layer / File(s) Summary
Weekly spend accounting
supabase/functions/_shared/cost.ts, supabase/functions/_shared/cost.test.ts, supabase/migrations/20261022130000_index_ai_requests_user_created.sql, supabase/migrations/20261022130001_weighted_quota_weekly_budget.sql
Cost results now carry capCostUsd. Merged usage retains known cap costs when total cost is unknown. The migration adds the nullable cap_cost_usd column and an index for user-created request lookups.
Quota reservation and enforcement
supabase/functions/_shared/usage-limits.ts, supabase/functions/_shared/entitlements.ts, supabase/functions/_shared/entitlements.test.ts, supabase/migrations/20261022130001_weighted_quota_weekly_budget.sql, supabase/tests/ai_quota_weekly_budget_test.sql, supabase/functions/ai*/..., supabase/functions/ai-stream/..., src/test/security-hardening.test.ts
The consume_ai_quota function reserves weighted daily units and checks weekly spend. Entitlement checks pass model units and the weekly cap. AI endpoint responses include quota units, and tests cover denials, reservations, cost accounting, and updated quota RPC mocks.
Client limit messages
src/lib/ai/limit-message.ts, src/lib/ai/limit-message.test.ts, src/lib/ai/errors.ts, src/lib/ai/request.ts, src/lib/ai/error-display.ts, src/hooks/use-subscription-error.ts, src/components/features/subscription/LimitReachedModal.tsx, src/pages/{Ask,Attack,Brainstorm,Factoids,Generate,Sparring}Page.tsx, src/locales/{en,es,hu}.json
Subscription errors now carry quota units and use localized messages for weighted daily limits and weekly-budget denials. The modal adds a weekly-budget variant, and AI pages pass quota units or display the localized inline message.
Usage-limit verification and documentation
.claude/skills/verify-debatelab/recipes/usage-limits.spec.ts, .claude/skills/verify-debatelab/features/README.md, .claude/skills/verify-debatelab/features/usage-limits.md, docs/architecture.md
Playwright coverage checks weighted daily refusals, lighter-model requests, weekly-cap denials, provider calls, and counter values. The feature recipe and architecture documentation describe the limit rules and verification setup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AIEndpoint
  participant checkRateLimit
  participant consume_ai_quota
  participant ai_requests
  participant AIProvider
  AIEndpoint->>checkRateLimit: Check request user and model
  checkRateLimit->>consume_ai_quota: Reserve units with daily limit and weekly cap
  consume_ai_quota->>ai_requests: Read current-week cap_cost_usd
  consume_ai_quota-->>checkRateLimit: Return allowance and denial reason
  checkRateLimit-->>AIEndpoint: Return limit result with units
  alt Request allowed
    AIEndpoint->>AIProvider: Send AI request
    AIProvider-->>AIEndpoint: Return provider response
    AIEndpoint->>ai_requests: Record usage with cap_cost_usd
  else Request denied
    AIEndpoint-->>AIEndpoint: Return rate-limit response
  end
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

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.
@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

Deployment failed for project debatelab-staging with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/adam-gaborjanis-projects?upgradeToPro=build-rate-limit

@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

Deployment failed for project debate-navigator with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

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.
@Programcode Programcode changed the title Weight the daily AI quota by model cost and cap monthly AI spend Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) Oct 22, 2026
Programcode added a commit that referenced this pull request Oct 22, 2026
* 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>
Programcode added a commit that referenced this pull request Oct 22, 2026
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>
@Programcode
Programcode merged commit b2e92b6 into main Oct 22, 2026
1 of 14 checks passed
Programcode added a commit that referenced this pull request Oct 22, 2026
… 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>
Programcode added a commit that referenced this pull request Oct 22, 2026
Keep calculator v2 for the logged cost (BYOK fee plus the provider
charge) next to #178 cap figure, which still counts only OpenRouter fee.
Rename the backfill to 20261022160000: #180 took 20261022120000.
Programcode added a commit that referenced this pull request Oct 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants