Skip to content

feat: per-route "skip on quota exhaustion" toggle for subscription providers - #2739

Open
tunwinlat wants to merge 8 commits into
mnfst:mainfrom
tunwinlat:feat/quota-aware-routing-skip
Open

feat: per-route "skip on quota exhaustion" toggle for subscription providers#2739
tunwinlat wants to merge 8 commits into
mnfst:mainfrom
tunwinlat:feat/quota-aware-routing-skip

Conversation

@tunwinlat

@tunwinlat tunwinlat commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Subscription providers with usage windows (Anthropic, Kimi, OpenAI, MiniMax, xAI) currently keep getting attempted on every request even when their quota is exhausted — each attempt fails with a quota/rate-limit error before the fallback chain engages, adding avoidable latency to every routed request.

This PR adds an opt-in per-route "Skip on quota exhaustion" flag: a new backend service polls each subscription connection's usage endpoint on an interval, and the router skips flagged routes while their connection is known to be exhausted.

What changed

  • ModelRoute gains skipWhenQuotaExhausted?: boolean (shared type; stored in the existing jsonb route columns — no migration). A quotaCheck capability on the shared subscription configs marks which providers expose a usable usage endpoint.
  • New SubscriptionQuotaService (routing/subscription-quota.service.ts): polls every 60s (SUBSCRIPTION_QUOTA_POLL_INTERVAL_MS, clamped to ≥30s), in-memory state only, fail-open everywhere — fetch errors, unwrap failures, or missing data never skip a route, and a failed refresh keeps the previous state. Per provider:
    • Anthropic: GET api.anthropic.com/api/oauth/usage (5h/weekly/per-model windows, utilization >= 100)
    • Kimi: GET api.kimi.com/coding/v1/usages (5h/weekly/monthly, used >= limit)
    • OpenAI: GET chatgpt.com/backend-api/wham/usage (used_percent >= 100)
    • MiniMax: GET <resource-origin>/v1/token_plan/remains (remaining-percent <= 0)
    • xAI: POST grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig (gRPC-web + dependency-free protobuf scan)
    • OAuth tokens reuse the same unwrap/refresh path the proxy uses at request time.
  • ResolveService skips flagged routes backed by an exhausted connection in both promotion paths (tier chain and header tiers), for primaries and fallbacks alike. A never-empty safeguard keeps the original chain when every candidate is exhausted, so a harness is never left unroutable.
  • Routing UI: a compact gauge-icon toggle (matching the existing Parameters affordance) on quota-capable subscription routes — tier overrides, fallback rows, specificity and header-tier cards. Default off.

For users

New "Skip on quota exhaustion" toggle on subscription routes in a harness's routing. When enabled, an exhausted connection's models are bypassed until the quota window resets, so requests reach a working fallback immediately.

For operators

  • No migration, no new dependencies, in-memory state only (re-polls from scratch at boot).
  • Optional env knob SUBSCRIPTION_QUOTA_POLL_INTERVAL_MS (default 60000, min 30000).

Notes

  • Kimi's /coding/v1/usages is undocumented (reverse-engineered, used by several community tools); the parser is deliberately defensive (string numbers, window fallbacks).
  • The xAI parser relies on protobuf field heuristics over gRPC-web (grok.com has no public usage API). It fails open if the schema changes. A regression fixture captured from a real live response is included in the spec.
  • Relation to Show subscription usage limits #2378: that PR adds a display-only usage endpoint; this one feeds routing decisions. Intentionally a standalone service (subscription-quota.*, not subscription-usage.*) to avoid conflicting with the unmerged PR — the fetchers could be unified later.
  • supportsQuotaCheck (shared configs, drives UI visibility) and the backend service's provider list are two sources of truth to keep in sync when adding providers.

Validation

  • npm test --workspace=packages/shared — 429/429
  • npm test --workspace=packages/backend — full suite 458/458 suites, 8413 tests
  • Frontend: all touched/neighboring suites green (Routing, FallbackList, tier cards, header tiers)
  • npm run build — clean; eslint clean on all changed files
  • Live-verified all five providers' usage endpoints against real subscription connections on a self-hosted install, including the skip behavior end to end

Summary by cubic

Skips exhausted subscription routes during routing so requests hit working fallbacks faster. Previously every candidate was attempted even when its subscription was exhausted; now quota-capable routes can opt in to be skipped, with a safeguard that keeps chains non-empty.

  • Adds skipWhenQuotaExhausted?: boolean to ModelRoute (stored in existing jsonb; no migration). PUT omitting the field preserves the stored flag; explicit false clears it; specificity swaps and fallback promotion retain it. DTOs whitelist the flag; tier, specificity, and header-tier controllers forward it.
  • Introduces a quota poller service (SubscriptionQuotaService via SubscriptionQuotaModule) that polls Anthropic, Moonshot (Kimi), OpenAI, MiniMax, and xAI every 60s (SUBSCRIPTION_QUOTA_POLL_INTERVAL_MS, min 30s; digits-only). In-memory, fail-open; failed refresh keeps prior state; prunes deleted/disabled connections; reuses existing OAuth refresh.
  • Router consults quota state for primaries and fallbacks across default tiers, specificity overrides, and header tiers; if all candidates are exhausted it keeps the original chain.
  • UI adds a compact gauge-icon toggle on quota-capable subscription routes (primaries and fallbacks, including header tiers). Default off. Guards double-click races and in-flight saves.
  • manifest-shared exposes supportsQuotaCheck and marks provider coverage. Docs and tests updated; no new runtime dependencies.

Written for commit ec6f4ef. Summary will update on new commits.

Review in cubic

…n providers

Subscription providers that expose a usage/quota endpoint (Anthropic,
Kimi, OpenAI, MiniMax, xAI) can now be polled on an interval, and any
route in a harness's routing (tier overrides, fallbacks, specificity
and header-tier routes) can opt into being skipped while its
connection's quota is exhausted. This stops the router from wasting
attempts on models that are guaranteed to fail and gets to a working
fallback faster.

- SubscriptionQuotaService polls each active subscription connection
  every 60s (SUBSCRIPTION_QUOTA_POLL_INTERVAL_MS, min 30s), fail-open
  on any error, in-memory state only
- ModelRoute gains an optional skipWhenQuotaExhausted flag (jsonb, no
  migration); honored for primaries and fallbacks alike, with a
  never-empty safeguard that keeps the original chain when every
  candidate is exhausted
- Routing UI exposes the toggle as a gauge icon on quota-capable
  subscription routes
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.94794% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.50%. Comparing base (6b04574) to head (ec6f4ef).
⚠️ Report is 18 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2739      +/-   ##
==========================================
+ Coverage   96.48%   96.50%   +0.02%     
==========================================
  Files         600      605       +5     
  Lines       35708    36385     +677     
  Branches     9977    10184     +207     
==========================================
+ Hits        34452    35114     +662     
- Misses        695      706      +11     
- Partials      561      565       +4     
Flag Coverage Δ
backend 95.70% <96.25%> (+0.04%) ⬆️
frontend 97.36% <98.61%> (+0.01%) ⬆️
shared 99.46% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 18.82kB (1.2%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
manifest-frontend-esm 1.59MB 18.82kB (1.2%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: manifest-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/index-*.css 2.21kB 151.88kB 1.48%
assets/index-*.js 2.42kB 149.34kB 1.65%
assets/routing-*.css 1.56kB 65.64kB 2.43%
assets/Routing-*.js 1.55kB 61.86kB 2.57%
assets/Welcome-*.css 1.56kB 59.49kB 2.69%
assets/RoutingActions-*.js 2.78kB 52.21kB 5.63% ⚠️
assets/GlobalOverview-*.js 3.78kB 30.4kB 14.21% ⚠️
assets/ProviderConnectionsPage-*.js 1.07kB 20.67kB 5.45% ⚠️
assets/providers-*.js 86 bytes 11.88kB 0.73%
assets/header-*.js 97 bytes 3.25kB 3.08%
assets/overview-*.css 1.52kB 3.24kB 88.04% ⚠️
assets/provider-*.js 91 bytes 2.24kB 4.23%
assets/Local-*.js 34 bytes 865 bytes 4.09%
assets/Subscriptions-*.js 34 bytes 641 bytes 5.6% ⚠️
assets/Byok-*.js 34 bytes 632 bytes 5.69% ⚠️

Files in assets/Routing-*.js:

  • ./src/pages/RoutingHeaderTiersSection.tsx → Total Size: 13.79kB

  • ./src/pages/Routing.tsx → Total Size: 38.39kB

  • ./src/components/HeaderTierCard.tsx → Total Size: 22.52kB

Files in assets/RoutingActions-*.js:

  • ./src/pages/RoutingActions.tsx → Total Size: 6.48kB

  • ./src/components/QuotaSkipToggle.tsx → Total Size: 1.53kB

  • ./src/components/FallbackList.tsx → Total Size: 22.04kB

  • ./src/pages/RoutingDefaultTierSection.tsx → Total Size: 7.41kB

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 42 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread packages/backend/src/routing/subscription-quota.service.ts Outdated
Comment thread packages/shared/src/model-route.ts
Comment thread packages/frontend/src/pages/RoutingHeaderTiersSection.tsx
Comment thread packages/frontend/src/components/FallbackList.tsx Outdated
Comment thread packages/backend/src/routing/subscription-quota.service.ts
Comment thread packages/backend/src/routing/subscription-quota.service.ts Outdated
Comment thread packages/backend/src/routing/routing-core/tier.service.ts Outdated
Comment thread packages/frontend/src/pages/RoutingTierCard.tsx Outdated
Cover the quota-skip toggle handlers and prop threading across
Routing, RoutingActions, the tier/specificity/header-tier sections,
HeaderTierCard, and the FallbackList revert path.
- Kimi parser: NaN sentinel for missing/corrupt quota numbers so
  absent data can never read as exhausted (fail-open)
- setOverride (tier/specificity/header-tier): preserve a persisted
  skipWhenQuotaExhausted when the field is omitted from the PUT;
  explicit false still clears
- legacy route conversion helpers round-trip the flag
- quota toggles: in-flight guards against double-click races on
  fallback rows and header-tier primary chips
- prune quota state for deleted/deactivated connections each poll
- validate SUBSCRIPTION_QUOTA_POLL_INTERVAL_MS as digits-only
- drop a dead isManual() guard on the primary chip toggle

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 17 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/backend/src/routing/subscription-quota.service.ts Outdated
Comment thread packages/frontend/src/components/FallbackList.tsx Outdated
Comment thread packages/shared/src/model-route.ts Outdated
Comment thread packages/backend/src/routing/subscription-quota.service.ts Outdated
Comment thread packages/frontend/src/pages/RoutingHeaderTiersSection.tsx Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/frontend/src/components/FallbackList.tsx
Comment thread packages/frontend/src/components/FallbackList.tsx
@guillaumegay13

Copy link
Copy Markdown
Collaborator

hi @tunwinlat, thanks for your contribution!

before moving forward, Manifest already has a cooldown system where it'll skip the route : 15s default for rate limits and 5 min max based on the Retry-After value in the error response.

it's not enough for you?

@tunwinlat

Copy link
Copy Markdown
Contributor Author

hi @tunwinlat, thanks for your contribution!

before moving forward, Manifest already has a cooldown system where it'll skip the route : 15s default for rate limits and 5 min max based on the Retry-After value in the error response.

it's not enough for you?

Hi @guillaumegay13, thanks for the attention. Sorry, I missed some contexts in the summary.

I am trying to fix 2 things that I find a bit bothersome with retrying when quota is full on top of just failed attempts.

  1. when the provider has extra usage enabled with credits, I am not able to restrict some harnesses that are token hungry to stop using the provider. this allows me to skip the model with exhausted quota for the harnesses I want.
  2. fallback caused by quota exhaustion contributes to the error rate. It's misleading because while it's an error in a way, that's an expected behaviour when quota is exhausted. But a global behaviour change is challenging because some providers don't support that. So I wanted to shoot all 3 things in one go.

Please let me know if you have any questions or suggestions.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/frontend/src/pages/RoutingTierCard.tsx">

<violation number="1" location="packages/frontend/src/pages/RoutingTierCard.tsx:272">
P2: When a fallback is promoted in a specificity tier, `skipWhenQuotaExhausted` is passed here but the specificity `onOverride` chain ignores it, so the new primary route loses its quota-skip flag. Thread the optional flag through `RoutingSpecificitySection` and `handleSpecificityOverride` into `overrideSpecificity` (the same applies to the other swap branch).</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

provId ?? '',
newPrimaryRoute?.authType,
newPrimaryRoute?.keyLabel ?? undefined,
skipWhenQuotaExhausted,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a fallback is promoted in a specificity tier, skipWhenQuotaExhausted is passed here but the specificity onOverride chain ignores it, so the new primary route loses its quota-skip flag. Thread the optional flag through RoutingSpecificitySection and handleSpecificityOverride into overrideSpecificity (the same applies to the other swap branch).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/src/pages/RoutingTierCard.tsx, line 272:

<comment>When a fallback is promoted in a specificity tier, `skipWhenQuotaExhausted` is passed here but the specificity `onOverride` chain ignores it, so the new primary route loses its quota-skip flag. Thread the optional flag through `RoutingSpecificitySection` and `handleSpecificityOverride` into `overrideSpecificity` (the same applies to the other swap branch).</comment>

<file context>
@@ -252,13 +253,25 @@ const RoutingTierCard: Component<RoutingTierCardProps> = (props) => {
+          provId ?? '',
+          newPrimaryRoute?.authType,
+          newPrimaryRoute?.keyLabel ?? undefined,
+          skipWhenQuotaExhausted,
+        );
+      }
</file context>

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