Repository navigation
Run Claude only on the Anthropic BYOK key, with no fallback provider - #185
Conversation
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>
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>
|
Deployment failed for project debate-navigator with the following error: Learn More: https://vercel.com/adam-gaborjanis-projects?upgradeToPro=build-rate-limit |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 10
ℹ️ Recent review info
📝 Walkthrough
Note 🎁 Summarized by CodeRabbit Free
Comment |
…mpt. 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
# Conflicts: # docs/architecture.md
|
Deployment failed for project debatelab-staging with the following error: Learn More: https://vercel.com/adam-gaborjanis-projects?upgradeToPro=build-rate-limit |
# Conflicts: # docs/architecture.md # supabase/functions/ai/provider.ts
# Conflicts: # src/pages/SparringPage.tsx
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>
#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>
# Conflicts: # supabase/functions/ai/provider-errors_test.ts
… 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>
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>
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>
…quest. 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>
Rename the refund migration to 20261022170000: production already has 20261022160000 (#179), so 20261022150000 would be out of order.
Every Claude (
anthropic/*) call now asks OpenRouter for theanthropicprovider only, with fallbacks off. Bedrock, Vertex, Azure, Claude Platform on AWS and every other provider are never tried. Other models keep balanced routing.Required manual step in OpenRouter
The request body can't stop OpenRouter from serving Anthropic on its own credits when the BYOK key fails; only the key's own setting does that. In OpenRouter → Settings → Integrations → Anthropic key, set Shared capacity fallback to "Never use shared capacity for any model on this provider" (
is_byok_only). Without it,only: ["anthropic"]still allows OpenRouter's shared Anthropic capacity when the key is out of credit.What changed
Routing.
supabase/functions/_shared/provider-routing.ts(new) givesanthropic/*the optionsprovider: { only: ["anthropic"], allow_fallbacks: false, require_parameters: true }. Everything else keeps{ require_parameters: true }with nosortororder. Every call path uses it:ai/provider.tscallOpenRouter: plain and streamed calls, research steps, the truncation and JSON repair retries, and Repair model JSON locally and enforce schemas before paying for a retry #176's strictjson_schemacalls. Research usesBACKGROUND_MODEL, which is Haiku, so a GPT request's research steps are pinned too.ai-stream/index.ts, streaming Ask.scripts/prompt-examples.ts.typesafe/jev-1.13,/alpha/decisions) isn't Claude and is unchanged.Failures. When a Claude call fails (non-2xx, or an SSE
errorchunk), nothing is retried on another provider. The endpoints answer 503{ code: "provider_unavailable" }. The client already shows that as "Anthropic is temporarily unavailable. Please try again later." in en, es and hu, on plain JSON, liveresultevents andai-streamSSE. Sparring now shows the translated message as well. Theai_requestsrow getserror_code = claude_unavailable. Ask no longer resends aprovider_unavailablestream failure through the plain endpoint, which would try Claude a second time on a failing key.Refund, fitted to Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) #178's weighted quota.
refundQuotaReservation(userId, rateLimit)gives back exactlyRateLimitResult.units. That's thep_unitscheckRateLimitpassed toconsume_ai_quotafor this request: Opus 5, GPT-6.1 Sol 3, Sonnet 2, DeepSeek V4 Pro 2, Auto Pro 5, others 1.refund_daily_quota(p_user_id, p_units, p_quota_key)lowers today's counter by those units, never below 0. It's SECURITY DEFINER and service-role only.Weekly $2 cap. A refunded request is logged with
cap_cost_usd = 0, so it doesn't count toward the weekly cap.When a refund happens. Only when nothing was paid for:
chargedAgainstCap).A failure that OpenRouter charged credits for keeps both its quota and its
cap_cost_usd, so no real spend is hidden from the cap. Jev routing and source-ranking rows are logged separately and keep their own cap cost.Guard. A Claude response whose usage shows
is_byok: falseorcost > 0logs aclaude_not_byokJSON warning with model, source,is_byok,costandupstream_inference_cost.Live proxy.
e2e/full-stack/fake-openrouter.mjsdropsonlyandallow_fallbackswhen the live proxy rewrites the model to Muse Spark, so the rewritten model can still route.Status
Merged as
17c00f91and live on production, per Adam: the migration was applied at 17:46 Budapest, andaiv91,ai-streamv28 andvoicev11 are deployed. I didn't do the merge, migration or deploy, and I haven't checked production myself.Deploy order (as applied)
20261022120000_opus_default_to_sonnet.sql(Move saved Opus 5.5 settings to Sonnet 5.5 (data migration, after #164) #180)20261022130000_index_ai_requests_user_created.sqland20261022130001_weighted_quota_weekly_budget.sql(Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) #178)20261022160000_backfill_byok_request_costs.sql(Log the real cost of Anthropic (BYOK) calls and Exa research #179)20261022170000_refund_daily_quota.sql(renamed from20261022150000at merge)aiandai-stream. Both callconsume_ai_quota(Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) #178) andrefund_daily_quota(this PR), so the migration has to be applied before they're deployed. If the functions go first, every refund call fails: it's logged and the student still gets the outage message, but the units aren't given back.Notes
cost > 0also fires for OpenRouter's 5% BYOK fee once past the monthly BYOK allowance. Locally the fake provider reportscost: 0.0001for every call, so the warning shows in local runs.CURRENT_DATErule asconsume_ai_quota.use-subscription.ts(Fix the 11 bugs from today's QA pass (H1, M1–M5, L1–L5) #184) countsai_requestsrows, including refundedclaude_unavailablerows, and ignores weights. The server's count decides, so this is only a display estimate.20261022160000. Its Exa charges go intoresearchUsagethroughonExaUsage, and the refund check runschargedAgainstCapover that merged usage. So a request whose Exa searches cost money is never refunded and keeps its cap cost.Verification
The full local run below is at
631d05c4(currentmainmerged in, through #176, #178, #181 and #184). The last commit,6fa3f2fc, came after it and has its own checks (see Reviews). GitHub Actions is off (billing), so these are local results, not CI.npm run verify, one step at a time, all exit 0: docs check, lint, typecheck, vitest 403/403, collaboration 30/30, Edge 388/388, build, sw, and Playwright smoke 43/43 on desktop Chromium.Database gate (
./.superset/db-gate.sh --db-only) at2415463d: 25 files and 470 tests pass, including Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) #178'sai_quota_weekly_budget_test.sqland this PR'sdaily_quota_refund_test.sql, plus the three concurrency scripts. The migrations and SQL tests haven't changed since.consume_ai_quota, so a second Opus is refused. It refunds 5, checks that a new Opus fits again, and checks the floor at 0, earlier days and privileges.claude_unavailablerow logged withcap_cost_usd 0leavesconsume_ai_quota's weekly check open.Function-layer tests in
provider-errors_test.tscover:ailive streaming andai-streamSSE, each with an HTTP 402 and an SSE error chunk;cap_cost_usd 0on refunded rows;cap_cost_usd 0.25), on success, or on a GPT failure;Each guard was mutation-checked: the test fails without it. That covers refunding 1 instead of the reserved units, dropping the cap exclusion in either function, dropping the charged-request check in either function, and counting a refused research step as work.
Two real Haiku calls to OpenRouter on the pinned route (made-up content):
Anthropic,is_byok: true,cost: 0.json_schema, reasoning, prompt caching): HTTP 200, served byAnthropic,is_byok: true,cost: 0, valid schema output.Both used the live-verify key, so they prove BYOK on that OpenRouter account, not necessarily on production's key.
Staging: skipped. Staging runs
main's Edge Functions and only gets new ones after a merge, so testing this there would need a staging deploy.Not verified: Opus and Sonnet on the pinned route, and a real out-of-credit response from OpenRouter. The exact status isn't documented, so any non-2xx or error chunk for Claude is treated as an outage.
Reviews:
ffd86044, and the midnight date was accepted.2415463d, against Weight the daily AI quota by model and cap AI spend at $2 a week (BYOK Claude excluded) #178: one medium finding, fixed in631d05c4. In an outage, refused Haiku research steps counted as paid work, so research tools never refunded. Of the four lows, the pinned route with Repair model JSON locally and enforce schemas before paying for a retry #176's schema is now verified by the second real call above. The midnight date, the Jev-before-failure case and the client estimate are listed under Notes.cap_cost_usdcould hide a real OpenRouter charge. Fixed in2415463d: a charged failure is never refunded.631d05c4(gpt-6.1-sol, xhigh): two medium findings, both confirmed in the code and fixed in6fa3f2fc.usage. A charge reported only on the error frame was dropped, and the failure could then be refunded with cap cost 0. Usage is now kept before the error is thrown. A new test case (error-frame-charged) fails against the old order in each reader.ai/index.tsrefunded a Claude refusal that raced a client abort;ai-streamalready refused that. The plain handler now checksworkSignal.aborted. A new test aborts after the 402 is read and before the refund decision, and fails without the check.6fa3f2fc: docs check, lint and typecheck exit 0, andprovider-errors_test.tspasses with each new guard mutation-checked. I didn't rerun the full unit, Edge, build and Playwright suites on that commit before it was merged.PR checks: the Vercel previews hit "Deployment rate limited — retry in 24 hours", and the GitHub Actions jobs are pending (billing). Neither reflects this code.
🤖 Generated with Claude Code