Skip to content

router: serve subscription-only below the overdraft floor instead of 402#747

Merged
steventohme merged 4 commits into
mainfrom
devin/1784216303-subscription-only-below-floor
Jul 16, 2026
Merged

router: serve subscription-only below the overdraft floor instead of 402#747
steventohme merged 4 commits into
mainfrom
devin/1784216303-subscription-only-below-floor

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

A usage-bypass org whose prepaid balance falls past the -$5 subscription overdraft floor was getting a hard 402 on every request — even though those turns serve for free on the caller's own Anthropic subscription. The balance middleware aborted before the proxy's usage-bypass path could run, so the org was permanently locked out of its own-account traffic (only a top-up above the floor could recover it). Prod confirmed this for org_ytbWhf0XxuuKLR3XAjtviSl0: subscription_exempt=true, balance_usd_micros=-6988315, threshold=-5000000 → 402, 167/167 requests over 14 days.

This changes the below-floor behavior for subscription-covered requests from "402" to "serve subscription-only": the turn flows on the caller's own subscription with paid failover disabled and no debit, so the customer's traffic never stops while our unbilled spend stays bounded at the floor. If the turn can't stay on the subscription (sub exhausted, or requested model isn't subscription-covered), it's refused with a clear 402 rather than billed. The customer also gets a visible warning prepended to the reply explaining credits are depleted and how to top up.

Non-subscription requests are unaffected — they still 402 at the normal balance gate.

How it works

New billing.SubscriptionOnlyContextKey (mirrors the existing HasOverrideContextKey marker), set by the middleware and consumed by the proxy:

// internal/server/middleware/balance_check.go — below-floor branch
if result.BalanceMicros <= threshold {
    if subscriptionExempt {                     // was: unconditional 402
        c.Request = c.Request.WithContext(billing.WithSubscriptionOnly(ctx))
        c.Next(); return                        // pass through, flagged
    }
    c.AbortWithStatusJSON(402, ...)             // non-subscription: unchanged
}

In subscription-only mode the proxy:

  • keeps the usage-bypass gate engaged up to exhaustion (the util < threshold gate exists only to conserve quota by routing to a cheaper paid model — pointless when paid failover is off):
    // internal/proxy/usage_bypass.go — usageBypassEngaged
    if snap.Exhausted() { return false }
    if billing.SubscriptionOnlyFromContext(ctx) { return true }   // new
    util := max(snap.Primary.UsedPercent, snap.Secondary.UsedPercent)
    return util < threshold
  • refuses instead of paying in ProxyMessages, both when the turn never resolved to a bypass passthrough and when a bypass attempt hits a retryable error (which normally reroutes to a paid model):
    if billing.SubscriptionOnlyFromContext(ctx) && !routeRes.UsageBypass {
        return ErrCreditsExhaustedSubscriptionUnavailable   // -> 402
    }
    // ...and in the errBypassRetryable branch, before the paid reroute:
    if billing.SubscriptionOnlyFromContext(ctx) { return ErrCreditsExhaustedSubscriptionUnavailable }
  • prepends a customer-facing warning by wrapping the bypass writer with the existing translate.NewAnthropicRoutingMarkerWriter (streaming responses only), always emitted (not gated by the routing-marker opt-out):

    Weave Router → your Weave router credits are depleted, so this turn is running on your own Anthropic subscription and paid model fallback is disabled. Add credits to restore full routing: https://app.workweave.ai/settings/billing/router-credits

New sentinel proxy.ErrCreditsExhaustedSubscriptionUnavailable classifies to HTTP 402 via ClassifyDispatchError, so the API handlers surface a clear top-up message.

Tests

  • middleware: below-floor subscription request passes through flagged subscription-only; non-subscription request below zero still 402s.
  • proxy: subscription-only keeps the bypass engaged even above the util threshold and emits the warning; exhausted sub → 402 (no paid dispatch); retryable bypass failure → 402 (no paid reroute, scorer not run).
  • dispatch_error: sentinel → 402 with top-up CTA.

make precommit (fmt/vet/build/test) green.

Caveats / follow-up

  • Immediate remediation still needed for the reported org. This fix only helps once deployed; the currently-deployed router keeps 402ing org_ytbWhf0XxuuKLR3XAjtviSl0 until someone with write access tops up its credits above the floor (or adds a billing override). I have read-only prod access.
  • The warning marker injects on streaming responses only (same limitation as the existing routing marker); non-streaming clients still get served subscription-only but without the inline warning.
  • A compaction summary (if triggered) runs before the routing decision, so an exhausted-sub turn that we ultimately 402 may incur one small paid summary debit. Rare (only at ~85% context) and bounded.

Link to Devin session: https://app.devin.ai/sessions/3d96bf143dbb41d3b11dca19995f2ba7
Requested by: @steventohme

Co-Authored-By: Steven Tohme <steven@workweave.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

Comment thread internal/server/middleware/balance_check.go
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

End-to-end local test — PASS (3/3 scenarios)

Ran the router locally (Docker Compose, managed mode + billing enabled) with a deterministic mock Anthropic upstream, driving real /v1/messages requests on the production router-key path. Balance for the test org set to −$6.99 (below the −$5.00 overdraft floor), matching prod for org_ytbWhf0XxuuKLR3XAjtviSl0. Shell-only test → evidence is command output (no GUI/recording).

Scenario Expected Result
A — below floor, subscription-covered, upstream 200 200, served on subscription, warning shown, no debit PASS
B — below floor, subscription-covered, upstream 429 (retryable) 402 refusal, no paid reroute, no debit PASS
C — below floor, no subscription header 402 insufficient_credits, never reaches proxy PASS
Scenario A — the fix in action (subscription-only serve + warning)

Response headers:

HTTP/1.1 200 OK
X-Router-Decision: usage_bypass
X-Router-Model: claude-sonnet-4-5
X-Router-Provider: anthropic

Warning injected as content-block index 0, real content re-indexed to 1 (SSE framing preserved):

content_block_delta index=0: "✦ **Weave Router** → your Weave router credits are depleted, so this turn is
running on your own Anthropic subscription and paid model fallback is disabled. Add credits to restore
full routing: https://app.workweave.ai/settings/billing/router-credits"
content_block_delta index=1: "Hello from the mock Anthropic upstream."

Server log:

WARNING Balance past subscription overdraft floor: serving subscription-only, paid failover disabled
        balance_usd_micros=-6988315 threshold_usd_micros=-5000000
INFO    turnloop classified turn_type=main_loop

Mock hit once with the requested model (no scorer substitution); ledger count 0 (no debit), balance unchanged.

Scenario B — retryable upstream → refuse, don't reroute to paid
HTTP/1.1 402 Payment Required
{"error":{"message":"Your Weave router credits are exhausted and your Anthropic subscription can't serve
this turn (rate-limited, or the requested model isn't subscription-covered). Add credits to re-enable paid
routing: https://app.workweave.ai/settings/billing/router-credits","type":"api_error"},"type":"error"}
INFO    Subscription-only bypass hit retryable error; refusing instead of paid reroute
WARNING Subscription-only request refused: credits exhausted and subscription unavailable

Mock: exactly one attempt → 429. Ledger count still 0.

Scenario C — regression: non-subscription still 402s at the normal threshold
HTTP/1.1 402 Payment Required
{"error":"insufficient_credits","top_up_url":"https://app.workweave.ai/settings/billing/router-credits",...}
INFO Balance check rejected: balance at or below threshold threshold_usd_micros=0 subscription_exempt=false

No mock hit — request never reached the proxy.

Harness notes: required a temporary ANTHROPIC_BASE_URL override in cmd/router/main.go to point at the mock (reverted, not in this PR). Also: trivial prompts classify as hard-pinned Classifier turns that skip the usage-bypass path — MainLoop-shaped requests (tools + max_tokens>=4096) are required to exercise the fix.

Devin session: https://app.devin.ai/sessions/3d96bf143dbb41d3b11dca19995f2ba7

Below the overdraft floor the balance middleware also flags Codex-covered
/v1/chat/completions and /v1/responses requests subscription-only, but only
ProxyMessages honored the flag. A Codex turn that routed to a paid model
would dispatch and debit with no bound.

ProxyOpenAIChatCompletion now restricts routing to the caller's own
subscription providers, pins dispatch to that binding (no paid failover),
refuses with ErrCreditsExhaustedSubscriptionUnavailable when the turn can't
serve on the subscription (route resolved to a paid model, or a pre-commit
sub failure), and surfaces the depleted-credits warning where the stream
allows it.

Co-Authored-By: Steven Tohme <steven@workweave.ai>
Comment thread internal/proxy/service.go Outdated
Comment thread internal/proxy/service.go Outdated

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7de3608. Configure here.

Comment thread internal/proxy/service.go
Comment thread internal/proxy/service.go
@steventohme
steventohme merged commit f1e5e93 into main Jul 16, 2026
10 of 11 checks passed
@steventohme
steventohme deleted the devin/1784216303-subscription-only-below-floor branch July 16, 2026 17:57
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.

1 participant