Skip to content

Route on the credit budget, not just the rate-limit windows - #162

Merged
ualtinok merged 4 commits into
cortexkit:mainfrom
iceteaSA:feat/spend-control-routing
Sep 18, 2026
Merged

ualtinok merged 4 commits into
cortexkit:mainfrom
iceteaSA:feat/spend-control-routing

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

The follow-up deferred from #160. That PR made the credit budget visible; nothing acted on it.

The blind spot

spend_control.individual_limit is a third exhaustion axis on its own clock. The account I probed sat at 0% on its 5-hour window and 51% on its 7-day window while 20% through a credit budget resetting a month out. Admission and placement both weighed rate-limit windows only, so:

  • admission would not skip an account whose budget was spent
  • sticky-balanced placement would happily send a cold session to an account with 1% of its budget left

Both present as unexplained failures on an account the sidebar shows as healthy.

What this wires

AdmissionexhaustedQuotaResetAt now treats a reached credit budget with a future reset as exhausted, on the same fail-open contract as windows: a missing or lapsed reset never blocks.

Cold placementcandidateWeight folds the credit reading into the existing min-over-axes via sustainableWindowWeight, using the credit's own resetsAt rather than assuming a 5h/7d duration. That matters: this window resets on a month clock, so borrowing a window duration would compute pressure against the wrong denominator.

Warm-pin migrationdecideStickyBreak treats a reached credit budget as confirmed exhaustion.

That third piece was not in the original scope, and the implementer flagged it as a known gap rather than leaving it silent. It belongs here rather than in a follow-up: wiring admission and placement but not migration produces an inconsistent contract, where a cold session refuses to start on a spend-exhausted account while a warm session stays pinned to it and keeps being refused. A half-wired axis is worse than an unwired one, because the halves disagree and the disagreement reads as a routing bug. decideStickyBreak already migrates on 401/403, but a spent budget presents as neither, so the pin would have survived until the rate-limit window independently exhausted.

One definition of "spent"

Admission and migration both call a single exported spendControlExhaustedResetAt. Two definitions of exhaustion would be the same defect one layer down, so a test asserts they agree on the same quota object: isQuotaExhausted(spent) is true and decideStickyBreak(spent) migrates.

spend_control.reached is authoritative rather than inferring from percentages — a test pins that the boolean wins over a spent-looking percentage.

Invariants held

  • An account with no spend control routes exactly as today. Proven behaviourally — the test pins healthy→retain and exhausted-window→migrate, and passes identically with the change reverted, rather than asserting a field is undefined. Most accounts have no spend control, so this is the guard that matters.
  • Filtering never removes the last candidate. Untouched: applyAdmissionQuotaSafety still returns the current selection when nothing is retained. A test reddens if that branch is disabled, so every-account-spent degrades to today's behaviour rather than to zero candidates.
  • Stale, malformed and missing readings retain the pin, matching how windows behave — only confirmed exhaustion migrates.
  • Check ordering in decideStickyBreak is unchanged: unknown guard first, stale before killswitch (the comment there explains why a stale snapshot must not judge an account the killswitch would consider below floor), windows, then credit.

Verification

(fail) a reached credit budget with a future reset exhausts the account          [admission reverted]
(fail) admission quota skips a fallback whose credit budget is spent             [admission reverted]
(fail) deprioritises a nearly-spent credit budget in cold placement              [weighting reverted]
(fail) ignores a malformed credit reading instead of excluding the account       [guard removed]
(fail) admission quota preserves the last fallback when every budget is spent    [preservation reverted]
(fail) migrates a pin on an account with a reached credit budget                 [migration reverted]
(fail) admission and migration agree on a spent credit budget                    [migration reverted]
(fail) trusts the reached boolean over a spent-looking percentage                [inference substituted]

I re-ran the migration mutation by hand against the merged branch to confirm it is not self-satisfying.

Not modelled

quotaSnapshotPassesPolicy — the threshold-based minimum-remaining filter behind getUsableFallbackAccounts — still judges windows only. Credit has no configured threshold, so that is a separate policy axis rather than exhaustion, and wiring it would mean inventing a default the operator never set.

Gates from the repo root: build, format:check (157 files), lint (157), types, test — 147 core + 1391 OpenCode (+19) + 14 Pi.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Makes routing act on the credit budget (spend_control), which was previously surfaced but never enforced. Admission, cold placement, and warm-pin migration now treat a reached budget as a third exhaustion axis on its own reset clock.

  • All three share a single spendControlExhaustedResetAt signal, so admission and migration can never disagree on what "spent" means.
  • The provider's reached boolean is authoritative; a spent-looking percentage alone never exhausts.
  • Missing, malformed, or lapsed resets fail open, and accounts with no spend control route exactly as before.
  • The last candidate survives when every budget is spent, so all-spent degrades to today's behavior rather than to zero candidates.

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

Review in cubic

The credit budget (spend_control) is a third exhaustion axis on its own
reset clock, independent of the 5h/7d rate-limit windows. Admission now
skips an account whose budget is reached, and cold-session placement
weights the budget's remaining capacity alongside the windows so a
nearly-spent budget deprioritises the account. Both fail open on a
missing or lapsed reset, and an account with no spend control is
unaffected.
Covers: a spent budget is skipped by admission; a nearly-spent budget is
deprioritised in cold placement; a no-spend-control account routes by its
rate-limit windows alone; and the last-candidate preservation still holds
when every budget is spent.
decideStickyBreak judged only rate-limit windows, so a session pinned to
an account whose credit budget was spent stayed pinned and kept being
refused — the same blind spot admission and cold placement now close.
Extract spendControlExhaustedResetAt as the single definition of a spent
budget and use it in both exhaustedQuotaResetAt and decideStickyBreak, so
admission and migration can never disagree. Ordering is unchanged: the
unknown and stale guards still run first, and a stale, malformed, or
missing reading retains the pin.
Covers: a reached budget migrates the pin with the exhausted reason
shape; a stale, malformed, missing, or lapsed reading retains it; the
reached boolean is trusted over a spent-looking percentage; a
no-spend-control account decides exactly as today; and admission and
migration agree on the same spent state.
@ualtinok
ualtinok merged commit 77c2be9 into cortexkit:main Sep 18, 2026
5 checks passed
@ualtinok

Copy link
Copy Markdown
Contributor

Merged as 77c2be9. Gate on the merge: core 147, opencode 1397, pi 14, typecheck and biome clean.

Including migration was the right call and the argument for it is the one I would have made: half a wired axis is worse than none, because a cold session refusing to start on an account a warm session stays pinned to reads as a routing bug rather than as a budget. A follow-up would have shipped that disagreement for however long the follow-up took.

I verified the two properties that could cause an outage rather than the ones that prove the feature, since those are where the cost is asymmetric.

Filtering never removes the last path. Disabling the guard reddens two, including admission quota preserves the last fallback when every credit budget is spent. That is the case I most wanted covered: every account spent at once is exactly when a new exhaustion axis takes a working setup to zero candidates, and it degrades to today's probe order instead.

Fail-open on a lapsed reset. Making a past reset count as exhausted reddens both the admission and the migration test. Worth noting those are two independent tests over one definition — which is the payoff of spendControlExhaustedResetAt being exported and shared rather than the check being written twice. Two definitions would have been the same defect one layer down, and you would have needed four tests to catch it.

Using the credit's own resetsAt rather than borrowing a window duration is the detail I would have got wrong. Pressure against a 5h denominator on a budget that resets monthly is not a small error — it is off by two orders of magnitude, and it would have looked like a plausible weighting rather than a bug.

One thing I would still like eventually, though not here: model_usage.credits_would_enable. A spent budget that only gates gpt-6-astra is different from one that gates everything, and right now we would migrate a session off an account that could still serve every model it is actually being asked for.

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