Skip to content

fix(proxy): suppress exhausted Claude subscription on OpenAI path#762

Open
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/openai-subscription-suppress
Open

fix(proxy): suppress exhausted Claude subscription on OpenAI path#762
rohith500 wants to merge 2 commits into
workweave:mainfrom
rohith500:fix/openai-subscription-suppress

Conversation

@rohith500

Copy link
Copy Markdown
Contributor

Summary

ProxyOpenAIChatCompletion never checked whether the caller's Claude
subscription was exhausted before injecting credentials, unlike
ProxyMessages (#519). On an OpenAI-wire turn routed to Anthropic, this meant
a spent OAuth token was dispatched instead of falling through to the
deployment/BYOK key. The subscription-only 402 guard had the same gap.

Full root-cause analysis and original live evidence: #761

Fix

Two changes in ProxyOpenAIChatCompletion, both mirroring ProxyMessages
exactly:

  1. Suppress an exhausted Claude subscription before credential resolution:
    if s.claudeSubscriptionExhausted(ctx, r.Header) {
        ctx = withSuppressedClaudeSubscription(ctx)
    }
  2. Extend the subscription-only refusal guard to also check observed
    exhaustion, matching service.go:2373's exact composition:
    if !servedOnSubscription(ctx) || s.anthropicSubscriptionObservedExhausted(ctx, r.Header) {

Known composition gap (documented, not fixed here)

Messages' subscription-only guard is additionally gated by
&& !routeRes.UsageBypass; this PR doesn't add that exclusion on the
OpenAI side. Verified this is inert today: usageBypassEngaged and
anthropicSubscriptionObservedExhausted read the identical usage-observer
snapshot (usage_bypass.go:93 and :136), so UsageBypass=true and
observed-exhausted=true cannot coexist — structurally, not just
empirically. Revisit when bypassToAnthropic/UsageBypass consumption is
wired on this surface (separate, not-yet-filed issue).

Tests

  • TestOpenAI_SubscriptionExhausted_ServesOnDeploymentKey
  • TestSubscriptionOnly_OpenAI_ExhaustedSubscription_Refuses402

Both fail on main, pass on this branch. Landed in usage_bypass_test.go,
reusing existing fixtures (bypassScorerPickMdl, bypassSubToken, fakeRouter,
fakeProvider) rather than new mocks.

Live verification (re-run against the actual committed diff, not a

local patch — git diff HEAD was empty throughout)

Check Pre-fix Post-fix
Credential at dispatch (OpenAI) spent OAuth token none — deploy key
Mock-Anthropic round trip 3 attempts, 429 to client 1 dispatch, 200 to client
Subscription-only + exhausted 1 dispatch, spent OAuth, no refusal 0 dispatches, ErrCreditsExhaustedSubscriptionUnavailable

Test plan

  • Both new tests fail on main, pass on this branch
  • make check green
  • go test ./internal/proxy/... -race clean except pre-existing TestFireTelemetryRecoversFromPanic flake
  • Existing Codex subscription-only coverage (subscription_only_openai_test.go) unaffected
  • Live credential / 429 / subscription-only repros re-run against committed 59966cc

Fixes #761

Made with Cursor

rohith500 and others added 2 commits July 16, 2026 18:47
…ption

ProxyOpenAIChatCompletion must drop a spent Claude OAuth token when a
deployment key exists, and refuse subscription-only turns when the
observer reports exhaustion — locking both guards before the fix.

Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror ProxyMessages so ProxyOpenAIChatCompletion drops a spent Claude
OAuth token before credential resolution and refuses subscription-only
turns when the observer reports exhaustion.

Fixes workweave#761

Signed-off-by: N Rohith Reddy <rohithreddy2202@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Thanks for this, @rohith500 — really nice catch. 🙏

The OpenAI path was silently missing the exhausted-Claude-subscription handling that ProxyMessages already had, and this closes that gap cleanly. I gave it a careful read against the repo conventions (AGENTS.md) and it conforms — no changes needed:

  • Layering / where code lives — the logic lands as behavior on proxy.Service (internal/proxy), exactly where turn dispatch belongs.
  • No magic provider strings — the new tests use providers.ProviderAnthropic throughout, matching the surrounding usage_bypass_test.go convention.
  • Sentinel errors — reuses the existing typed ErrCreditsExhaustedSubscriptionUnavailable and lets the HTTP layer map it, rather than leaking status codes.
  • Comments — the added comments explain why (the doomed-429 subscription-window behavior), not the diff — which is exactly the bar AGENTS.md sets.
  • Tests — non-tautological; both new cases assert real behavior (deployment-key fallthrough drops the OAuth token; subscription-only refuses with zero paid dispatch).

Approving from a conventions standpoint — great contribution.

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.

ProxyOpenAIChatCompletion never suppresses an exhausted Claude subscription — spent OAuth token dispatched, no failover to deployment key

1 participant