Skip to content

feat(routing): same-provider earliest-expiry-first account scheduling - #403

Merged
caigee-cmd merged 3 commits into
caigee-cmd:mainfrom
yoruuuchan:feat/credit-expiry-fefo
Oct 26, 2026
Merged

caigee-cmd merged 3 commits into
caigee-cmd:mainfrom
yoruuuchan:feat/credit-expiry-fefo

Conversation

@yoruuuchan

Copy link
Copy Markdown
Contributor

Summary

  • Add an opt-in fefo routing strategy that ranks accounts only inside the same provider family by the nearest spendable credit-package expiry. Same UTC+8 expiry day rotates fairly; a provider with no package dates keeps round-robin behavior, and unknown-date accounts are fallback candidates inside a dated provider. Existing round-robin, weighted round-robin, fill-first, affinity, pinning, provider/region gates, cooldowns, and failover are unchanged.
  • Refresh a FEFO account's quota after a completed response/stream (coalesced per account), and re-probe accounts whose trusted packages all expired, so newly granted or spent credits affect the next selection without a manual refresh.
  • Expose WorkBuddy package expiry as FEFO-eligible only for personal identities with a complete result page; enterprise/shared identities, 100-row pages, undated packages, and other providers' reset windows/token lifetimes never create a priority. Also skip expired/depleted packages in the existing expires_at summary and use the billing UTC+8 clock for the package query window.
  • Add the strategy to System -> Account routing strategy and document the provider data boundary.

Provider data boundary

  • WorkBuddy personal accounts: get-user-resource returns per-package CycleEndTime, so these accounts can be ranked by real package expiry.
  • Qoder: /api/v2/quota/usage exposes only the subscription cycle boundary (expiresAt) plus aggregate user/add-on/org buckets. It does not expose per-package credit expiry. A Qoder five-account example therefore cannot be ranked by actual credit expiry with the current upstream API; Qoder accounts stay in the unknown-date cohort and keep the existing rotation. No subscription reset or token lifetime is reused as a credit expiry.
  • Trae / Devin / Command Code / Codex: only reset windows or totals are available, so these accounts also stay in the unknown-date cohort.
  • WorkBuddy enterprise/shared or 100-row result page: scope is cleared, so the account gets no FEFO priority even though the dates remain visible in the console.
  • WorkBuddy package balances remain the adapter's existing integer credit values; sub-1-credit precision is not changed by this PR.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./internal/accounts ./internal/gateway ./internal/control ./internal/providers/workbuddy
  • go test ./internal/executor (excluding two pre-existing Windows/time-sensitive failures also present on main: TestRepeatedFailuresBackOff, TestBackoffResetsOnKindChange)
  • go test ./internal/runtime (excluding pre-existing Windows path failure TestQoderRuntimeSpecSelectsCNCLIAndConfigDir)
  • go test ./internal/api -run 'TestSystemSettingsRoutePersists.*RoutingStrategy'
  • cd frontend && npm run build && npm run lint && npm run sync
  • Added changelog/unreleased/fefo-credit-expiry-routing.md
  • No tokens, auth blobs, raw captures, or host details in the diff
  • cd worker && npm test - no worker change

Live paid-account A/B acceptance was not run in this environment; routing, quota refresh, cooldown/saturation escape, same-day rotation, cross-provider isolation, and legacy-strategy compatibility are covered by synthetic Go tests.

@caigee-cmd caigee-cmd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The scheduling boundary is the part that matters, and it is drawn correctly:

  • opt-in fefo only; round-robin / WRR / fill-first stay unchanged
  • rank inside one provider family, never across Qoder vs WorkBuddy
  • only WorkBuddy personal packs with an explicit account-fefo scope and a real CycleEndTime
  • enterprise / shared / 100-row pages clear the scope
  • Qoder subscription expiresAt, Trae/Devin/Codex/Command Code reset windows, and token lifetimes never become a credit expiry
  • same UTC+8 day still rotates; unknown-date accounts are fallbacks
  • after-use quota refresh is coalesced and off the request path

AfterAccountUse on finishRequestLog does cover Chat Completions, Anthropic Messages, and Responses (finishCompatibility → finishRequestLog). Skipping RequestStatusStreaming so the hook waits for the body is correct.

Independently useful even without FEFO: soonestExpiry no longer advertises a depleted or already-expired pack.

Do not merge as-is: the PR conflicts with main after #400 (mostly hashed internal/webui/static). Rebase, re-run cd frontend && npm run sync, and this is ready to approve.

@caigee-cmd caigee-cmd left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Architecture check against AGENTS.md / docs/ARCHITECTURE_SUMMARY.md — the layering is correct, still blocked on rebase.

  • Pick stays in executor. fefo.go / PickRoute is Pool scheduling. Runtime does not choose accounts. Matches “Pool / 选号 live in executor; runtime does not own pick strategy”.
  • Refresh stays in runtime. RefreshFEFOQuotaAfterUse, the quota fetch gate, and the stale-package probe are account lifecycle, not a public-protocol concern.
  • Gateway does not import runtime. AfterAccountUse is an injected callback from app. That is required: executor ChatStream returns at response headers, the body is relayed in gateway, so after-use cannot live in executor without firing too early. finishRequestLog already covers Chat Completions, Messages, and Responses.
  • accounts stays a data type. RoutingStrategyFEFO and QuotaPackage.Scope are entity fields. No accounts → executor/runtime import.
  • Provider maps upstream facts. WorkBuddy sets/clears account-fefo from get-user-resource; executor never invents Qoder expiresAt or token lifetimes as credit expiry. Personal-account routing, not billing.

Still do not merge until rebased onto current main with cd frontend && npm run sync.

@caigee-cmd
caigee-cmd force-pushed the feat/credit-expiry-fefo branch from 6440f1d to 0e5b4ff Compare October 26, 2026 23:35
@caigee-cmd
caigee-cmd merged commit fabe198 into caigee-cmd:main Oct 26, 2026
6 checks passed
caigee-cmd added a commit that referenced this pull request Oct 26, 2026
Rebased onto main after #403; regenerated webui static assets.
caigee-cmd added a commit that referenced this pull request Oct 26, 2026
Rebased onto main after #403; regenerated webui static assets.

Co-authored-by: caigee-cmd <caigee-cmd@users.noreply.github.com>
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