Skip to content

feat: subscription-aware routing (discount covered models by observed quota headroom)#495

Merged
steventohme merged 6 commits into
mainfrom
steven/sub-aware-routing
Jun 23, 2026
Merged

feat: subscription-aware routing (discount covered models by observed quota headroom)#495
steventohme merged 6 commits into
mainfrom
steven/sub-aware-routing

Conversation

@steventohme

Copy link
Copy Markdown
Collaborator

What

When a caller presents a subscription (ChatGPT/Codex or Claude Pro/Max), bias routing toward the models that subscription already pays for — priced by the actual remaining rate-limit headroom, not full catalog cost. Today the scorer prices every model at full catalog cost and the sub only changes billing after a model wins, so a pre-paid GPT turn still loses to cheap OSS on the cost axis (e.g. a real opencode turn scored deepseek-v4-pro 1.67 vs gpt-5.5 0.89 and routed to deepseek).

These quotas are perishable (reset every window), so unused headroom has zero salvage value: the marginal cost of a covered turn is ~0 while the window has slack and rises to full price only as it binds — use-it-or-lose-it / bid-price control. (Design doc: router-internal/docs/plans/SUBSCRIPTION_AWARE_ROUTING.md.)

How

  • internal/proxy/usage (new, pure/inner-ring): parses the quota headroom both backends already report on every response — Codex x-codex-{primary,secondary}-* and Anthropic anthropic-ratelimit-unified-{5h,weekly}-* — into per-credential window state. CostFactor maps utilization uε + (1−ε)·u^γ: ~ε when the window has slack (covered model ~free), →1 (full catalog price, no subsidy) as it binds. The tighter of the two windows governs. In-memory, TTL-evicted, salted credential keys.
  • providers: a context-carried UpstreamHeaderObserver (no Client-interface change); the anthropic + openai clients surface resp.Header after every upstream call (including 429s, where headroom matters most).
  • proxy: records headroom keyed by the resolved subscription credential; pre-routing computes per-covered-model cost factors (Codex → conservative headline GPT set the backend bills; Claude → catalog Anthropic models) and sets them on router.Request.
  • scorer: blendScoresV2 multiplies a covered model's cost term by its factor — so it rides the same per-cluster α/λ blend as the base cost.

Safety / rollout

  • Off by default, behind ROUTER_SUBSCRIPTION_AWARE_ROUTING (+ _COST_EPSILON default 0.05, _COST_GAMMA default 2, _OBSERVATION_TTL default 10m). Feature-off path is byte-identical (no observer installed, no factors computed).
  • Cold start = full price: no subsidy until the first response populates the observer for that credential — never a blind discount.
  • ε > 0, not 0: a covered model never reads as strictly free (avoids dominating quality ties / burning quota early then throttling on hard later turns).

Testing

  • usage: header parsing (both families), shadow-price monotonicity (slack→ε, binding→full price, tighter window governs), observer record/TTL/keying.
  • scorer: no-op guard (factor 1.0 ≡ baseline decision + score).
  • go build ./... + full go test ./... green.
  • Validation gate before enabling the flag: offline screen on cost-weighted production traffic (share shift toward sub-covered models + quality delta), per ROUTER_MODEL_LIFECYCLE.

Follow-ups (documented, out of scope)

  • Pace-aware shadow price using window reset time (this v1 uses utilization u^γ, which already encodes the perishability bias).
  • Claude Code inbound-bearer observation (v1 keys on the dedicated X-Weave-*-Subscription headers — the opencode dual-sub path).

🤖 Generated with Claude Code

…observed quota headroom

When a caller presents a subscription (ChatGPT/Codex or Claude Pro/Max), bias
routing toward the models that subscription already pays for, priced by the
actual remaining rate-limit headroom rather than full catalog cost. These quotas
are perishable (reset every window), so unused headroom has zero salvage value —
the marginal cost of a covered turn is ~0 while the window has slack and rises to
full price only as it binds (use-it-or-lose-it / bid-price control).

- internal/proxy/usage: new pure observer. Parses Codex x-codex-{primary,secondary}-*
  and Anthropic anthropic-ratelimit-unified-{5h,weekly}-* headers into per-credential
  window state; CostFactor maps utilization u to epsilon+(1-epsilon)*u^gamma — ~epsilon
  when slack, →1 (full price) as the window binds. In-memory, TTL-evicted, salted keys.
- providers: context-carried UpstreamHeaderObserver (no Client-interface change);
  anthropic + openai clients surface resp.Header after every upstream call.
- proxy: records headroom keyed by the resolved subscription credential; pre-routing,
  computes per-covered-model cost factors (Codex→headline GPT set; Claude→catalog
  anthropic models) and sets them on router.Request.
- scorer: blendScoresV2 multiplies a covered model's cost term by its factor.
- Off by default behind ROUTER_SUBSCRIPTION_AWARE_ROUTING (+ _COST_EPSILON/_GAMMA,
  _OBSERVATION_TTL). Cold start = full price until the first response is observed.

Tested: usage parsing + shadow-price monotonicity (slack→epsilon, binding→full,
tighter window governs) + observer TTL; scorer no-op guard; full suite green.
Validation gate before enabling: offline screen on cost-weighted prod traffic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Comments Outside Diff (1)

  1. internal/proxy/subsidy.go, line 211-219 (link)

    P2 claudeCoveredModels() iterates over catalog.Models and allocates a fresh []string on every call to subsidyFactors. When the Anthropic subscription path is active, this runs on every routed request in the hot path. The catalog is static after startup; this could be a package-level var (initialized once via an init or a sync.Once) to avoid the per-request allocation and loop.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "feat(proxy): subscription-aware routing ..." | Re-trigger Greptile

Comment thread internal/proxy/usage/usage.go Outdated
Comment thread internal/proxy/subsidy.go
… symmetry

- parsePercent always divides 0-100 percent headers by 100 (+ clamps), instead
  of a magnitude heuristic that misread the boundary: "1" (1% used, max headroom)
  was left as 1.0 (=100%, full price), silently wiping the subsidy at the freshest
  moment. Both backends document these as 0-100; the remaining/limit path already
  computes a fraction directly and bypasses this. (greptile P1)
- withUsageObserver now gates the Codex recording on codexSubscriptionFromContext
  (token + account-id), mirroring subsidyFactors exactly — no more recording
  observations under a key the read side never consumes. (greptile P2)
- Add regression tests for the 1% case + >100 clamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/router/cluster/scorer.go
costs (and the cMin/cMax cost normalization) range over the full deployed
model set, but only eligibleModels are scored. Discounting an EXCLUDED covered
model (context overflow, pin max-out, operator filter) to ~epsilon could make it
the artificial cost-normalization floor, weakening the cost axis for models that
can actually win. Gate the discount on membership in eligibleModels — an
ineligible model can't be chosen, so subsidizing it serves no purpose. (cursor)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/proxy/turnloop.go
Comment thread internal/router/cluster/scorer.go
The session planner compared pin-vs-fresh on full catalog pricing, so a session
pinned to a cheap model would never switch to a now-near-free subscription
model — the discount only applied to the fresh decision and was inert on sticky
sessions after turn 1 (cursor). Thread the per-model subsidy factor into
planner.Inputs and scale both sides' effective price in the EV math (Input/Output
scaled uniformly; CacheReadMultiplier is a ratio and stays correct). nil factor
map = today's behavior. Added a deterministic flip test (cheap pin → subsidized
covered model switches).

Also document that the legacy v1 scoring path has no cost axis, so the subsidy
is V2-only by construction (all deployed bundles run V2). (cursor, low)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/proxy/subsidy.go
Comment thread internal/proxy/turnloop.go
…on headroom

- Header observer is now context-aware and records only responses served on the
  caller's subscription, keyed by the call's RESOLVED credential. The handover
  summarizer (runs on the same request after clearCredentials, on a deployment
  key) no longer poisons the user's headroom snapshot. (cursor)
- Subsidized requests are cache-ineligible: the semantic-cache key doesn't
  capture headroom-dependent model choice, so a hit could return a body from a
  model chosen under different headroom. subsidyFactors early-returns nil when
  the feature is off, so OFF deployments are unaffected. (cursor)
- gofmt the planner test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread internal/proxy/usage/usage.go Outdated
Comment thread internal/router/planner/policy.go
… epsilon=0

- usage.Observer.Record now merges per-window with the prior (non-stale)
  observation: a response reporting only one rate-limit window no longer erases
  the other window's last-known utilization (which would make CostFactor look
  slack and over-discount until TTL). A genuinely reset window reports used≈0
  (present) and still overwrites; only an omitted window is preserved. (cursor)
- planner applySubsidy keys on map MEMBERSHIP, not factor sign, so a legitimate
  0.0 factor (ROUTER_SUBSCRIPTION_COST_EPSILON=0) discounts in the EV math just
  as the scorer multiplies the cost term by it — no stay/switch vs fresh
  disagreement. (cursor)
- Tests for both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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 and found 1 potential issue.

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 f4d5f7a. Configure here.

Comment thread internal/proxy/service.go
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