Skip to content

feat(quota): persist per-account quota and skip out-of-quota Kiro accounts during routing#1854

Open
vuongtlt13 wants to merge 1 commit into
decolua:masterfrom
vuongtlt13:feat/skip_kiro_account
Open

feat(quota): persist per-account quota and skip out-of-quota Kiro accounts during routing#1854
vuongtlt13 wants to merge 1 commit into
decolua:masterfrom
vuongtlt13:feat/skip_kiro_account

Conversation

@vuongtlt13

Copy link
Copy Markdown

Summary

Two related quota improvements:

  1. Persist quota per account so the dashboard renders instantly, and
  2. Proactively skip Kiro accounts that are out of quota during account selection until their reset time — instead of repeatedly trying them and burning a request to discover they're depleted.

Motivation

Previously provider quota was live-fetched only and cached just in localStorage (and that cache was never read on mount). So:

  • The Quota page always rendered blank → N live provider calls → fill in, every visit.
  • Nothing on the backend knew an account was depleted, so the router would keep selecting an out-of-quota Kiro account, get a quota error, apply a short capped cooldown, retry, fail again… Kiro reports an exact resetAt via its usage API but never surfaces it on chat errors, so the precise reset time was being thrown away.

What changed

1. Persist quota onto each connection (all providers)

  • parseQuotaData moved to open-sse/services/usage/normalize.js and re-exported from the dashboard utils, so backend and frontend normalize identically.
  • GET /api/usage/[connectionId] now persists the normalized quotaInfos array (+ quotaPlan, quotaMessage, quotaUpdatedAt) onto the connection. Only overwrites quotaInfos when non-empty, so a transient auth/empty response can't wipe the last-good snapshot.
  • GET /api/providers/client bundles those fields in the connection list.
  • ProviderLimits hydrates quotaData (and localStorage) from the bundled snapshot immediately, so the table shows last-known values right away; the live refetch still runs and overwrites. The loading spinner now only shows when there is no data yet.

No DB migration required — connections already persist arbitrary fields in their JSON data blob.

2. Skip out-of-quota Kiro accounts during selection (Kiro-scoped)

  • New helpers in accountFallback.js: getQuotaResetUntil() / isQuotaDepleted(), gated by QUOTA_DEPLETION_PROVIDERS = new Set(["kiro"]).
  • An account is treated as depleted only when every known bucket (total > 0) is fully used and its resetAt is still in the future. If any bucket has room, or the reset has already passed, or there's no resetAt, the account stays usable.
  • getProviderCredentials filters these out and folds their resetAt into the retryAfter reported when all accounts are unavailable.
  • The reactive 429 path remains as a safety net for accounts whose snapshot isn't fresh.

Scope: strictly Kiro. For every other provider isQuotaDepleted() returns false, so routing is unchanged — extend later by adding a provider to QUOTA_DEPLETION_PROVIDERS (only when its resetAt is trustworthy).

3. Observability

Each quota-based skip is logged (throttled to once per 60s per account, since selection runs per request) so it appears in /dashboard/console-log:

ℹ️ [AUTH] kiro | skip "kiro-account-1" — out of quota, resets 2026-07-01T00:00:00.000Z (reset after 332h 4m 59s)

Testing

  • Unit-level smoke tests of parseQuotaData and getQuotaResetUntil/isQuotaDepleted across depleted / has-room / reset-passed / no-resetAt / multi-bucket / unlimited / non-Kiro cases.
  • Verified end-to-end with a real depleted Kiro account: it's skipped during selection, the request returns 503 with a retry-after, and the skip is logged to the console log.
  • eslint clean on all touched files.

Files changed

  • open-sse/services/usage/normalize.js (new) — shared parseQuotaData
  • open-sse/services/accountFallback.jsQUOTA_DEPLETION_PROVIDERS, getQuotaResetUntil, isQuotaDepleted
  • src/sse/services/auth.js — skip depleted accounts + throttled logging
  • src/app/api/usage/[connectionId]/route.js — persist quota snapshot
  • src/app/api/providers/client/route.js — bundle quota fields
  • src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js — instant hydration
  • src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js — re-export parseQuotaData

- Persist normalized quotaInfos (+plan/message/updatedAt) onto each connection
  in the usage route; bundle it in /api/providers/client so the dashboard
  hydrates instantly from the last-known snapshot before the live refetch.
- Share parseQuotaData via open-sse/services/usage/normalize.js (re-exported
  from the dashboard utils) so backend and frontend normalize identically.
- Skip Kiro accounts that are out of quota until their resetAt during account
  selection (Kiro-scoped via QUOTA_DEPLETION_PROVIDERS; other providers
  unchanged), and log each skip to the console log (throttled per account).
bloodf pushed a commit to bloodf/9router that referenced this pull request Jun 19, 2026
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