Skip to content

feat(routing): soonest-reset credential selector (MASTRA-765) - #3

Merged
ericlitman merged 14 commits into
mainfrom
claude/MASTRA-765-soonest-reset
Oct 3, 2026
Merged

ericlitman merged 14 commits into
mainfrom
claude/MASTRA-765-soonest-reset

Conversation

@ericlitman

@ericlitman ericlitman commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Replaces #2, which GitHub closed when its base branch (claude/MASTRA-768-e2e-hermetic) was deleted at the merge of #1. The CI run on the tests-only commit is recorded on #2: S1, S2, S3 and S5 fail on stock as intended (run 37140121133).

Linear: MASTRA-765 (tests from MASTRA-768, U4)

Adds routing.strategy: soonest-reset. Each new session goes to the credential whose weekly quota window resets soonest, among credentials that still have quota. Existing sessions follow upstream session affinity: they move only on cooldown or binding expiry. For one shared weekly limit, this order wastes the least quota. With per-model weekly limits it is a heuristic, and those limits stay out of scope here.

The hermetic e2e suite it extends merged in #1.

Commits

  1. 06eff342 tests only: hermetic selector scenarios S1, S2, S3 and S5, which must fail on stock, plus S7 and S8 as regression tests. The mock gains POST /_mock/script, which sets one credential's quota headers or 429s. Each scenario owns its own credentials. The hermetic config turns on soonest-reset, session affinity and save-cooldown-status.
  2. 143cc73c, 6c0fccaa, 7f35e809: zsprackett/CLIProxyAPI faa4ccff (soonest-reset strategy), 01ad0021 (persist observed quota with the cooldown state) and 67f5749d (Claude thread_not_found as a request fault). They are cherry-picked with -x, and Zac Sprackett stays the author. conductor_cooldown.go merged cleanly: upstream's v8.0.10..v8.0.13 change to it only adds unauthorized-credential handling. 4d7b2e84 (model-scoped weekly) was not taken, because it is per-model scope only.
  3. ee450e85: our selector rework.
    • Windows by length. A Codex window is weekly when its length is at least 10080 minutes, whichever position carries it, because Codex Pro reports its weekly window as primary. Claude windows are 5h and 7d.
    • Exhaustion gate. Skip a credential while an observed window is exhausted and its reset is ahead. If every candidate is gated, the ordering alone decides.
    • Unknown resets. An unknown Claude reset sorts first, and a passed one rolls forward by whole weeks. An unknown or passed Codex reset sorts last.
    • Deletions. The scheduler fast-path additions and the isBuiltInSelector entry are deleted. With session affinity on, picks always take the legacy path.
  4. fce6a86b: Go unit tests for S1, S2, S3, S5 and S10 (a real restart through the file store); a pick through session affinity; and a race test.
  5. Review fixes from Astra (gpt-6-astra, high), each a subtraction:
    • d595299a: save every observation, not once a minute.
    • 1d90a949: keep a Codex window with no Window-Minutes.
    • 1e43486d: no age cutoff on saved observations.
    • a714c907: the save-cooldown-status comment in config.example.yaml no longer mentions the removed throttle.

Size against base 1aa79f5: non-test Go +254/−7 (selector 152, persistence 84); Go tests +620; e2e +215/−12.

Acceptance criteria from MASTRA-765

  • "Go unit tests for S1, S2, S3, S5 and S10 … pass with go test -race."
    • Locally: go test -race -count=2 ./sdk/cliproxy/auth/ passes on 1e43486 (a714c90 changes only a YAML comment).
    • Race evidence covers this package only. Three upstream tests in sdk/cliproxy fail intermittently under -race -count=6 on stock v8.0.13 as well as on this branch: TestHandleAuthUpdates_SameRevisionWaitDoesNotWaitForOtherAuthInBatch and TestEndToEndAuthFile{Replacement,Patch}_RestoresModelsInV1ModelsWithoutRestart.
    • go test ./... passes, and CI runs it.
  • "The MASTRA-768 hermetic selector tests fail on the tests-only commit and pass at the PR head."
    • On stock (06eff34): S1, S2, S3 and S5 fail with ASSERTION_FAILED. For example, S1 expected e2e-s1-b and got e2e-s1-a. S7, S8 and the 9 protocol tests pass.
    • At the head (a714c90, clean tree): 15 of 15 pass.
    • The CI run on 06eff34 for this PR is the recorded failing run.
  • "A bound session follows upstream affinity; new sessions follow the ranking." Hermetic S7 and the Go affinity test both cover it.
  • "No token refresh, token-endpoint call or credential-file write." None was added. The persistence writes cooldown-state files, not credentials.
  • "The added non-test line count is in the PR." It is above.

Review

  • Plan: reviewed by Astra at xhigh. The disposition is on MASTRA-765.
  • Code: reviewed by Astra at high, which returned four findings. Three were fixed above.
  • Rejected: removing thread_not_found as a request fault. Upstream already classifies OpenAI's equivalent, previous_response_not_found, the same way. Without it, a 404 cools down a healthy account and pulls it out of the drain order.
  • Mine: I read every diff and re-ran the suites myself. Removing the picked == nil guard is safe, because both candidate paths return an error instead of an empty list.

Known limitation

A Codex window started outside this proxy stays invisible until the proxy sends that account a request. MASTRA-766 measures how often that happens before anyone adds usage polling.

🤖 Generated with Claude Code


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

ericlitman and others added 10 commits October 3, 2026 11:14
…RA-768)

Hermetic selector tests written before the selector. S1, S2, S3 and S5
fail on stock v8.0.13; S7 and S8 are regression tests and pass on stock.

- The mock takes POST /_mock/script to set per-credential quota headers
  or a 429 usage_limit_reached answer.
- The hermetic config sets routing.strategy soonest-reset (stock falls
  back to round-robin), session-affinity and save-cooldown-status.
- Each scenario owns its model and credentials through per-key models;
  `<model>-<name>` reaches one credential to seed its observation.
- Each assertion sends several new sessions that must all land on one
  credential, so round-robin cannot pass by ID order. Auth IDs hash the
  mock's random port, so ID order differs between runs.
- With session affinity on, the all-cooled 429 body has no provider
  field, and /v1/models lists the scenario models; two existing
  assertions follow that.

S10 (restart with persisted observations) is a Go unit test in a later
commit: a hermetic restart would kill requests of tests running in
parallel workers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a "soonest-reset" routing strategy that prefers the credential whose
weekly (7-day) quota window resets soonest, so quota is consumed before
it expires at the reset. Exhausted credentials fall into the normal
cooldown and selection moves to the next-soonest reset.

Reset times come from the passive quota snapshot captured from Claude
(Anthropic-Ratelimit-Unified-7d-Reset) and Codex (secondary window, or a
weekly primary window) response headers. Credentials without a known
future reset are picked first so one request can learn their window;
ties resolve by ID, so providers without quota signals behave like
fill-first.

The strategy is a built-in selector and runs on the scheduler fast path
for both single-provider and mixed-provider picks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit faa4ccff3b7920732e22752cac0eb62f817aafdf)
Quota observations (rate-limit headers from Claude/Codex responses) lived
only in memory, so after a restart soonest-reset had no reset times. It
then probed every credential with an unknown window, moving conversations
between accounts (breaking Claude threads and prompt cache), and the
management quota view showed nothing until traffic resumed.

When save-cooldown-status is enabled, the latest observation is saved in
the same per-auth/model record as cooldown state (stores key records by
auth and model, so one record carries both; records with no cooldown get
status "observed"). Saves are throttled to once a minute because every
response refreshes the observation. On restore the newest observation
wins, and observations older than eight days are neither saved nor
restored. Cooldown change detection is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 01ad00219f945d663ef8b3e26cf25890574e5928)
Claude Code continues a conversation with
thread: {"type":"continue","previous_message_id":...}, and Anthropic keeps
that thread state on the account that created it. When a request lands on
a different account, the upstream returns 404 with
error.details.error_code "thread_not_found".

That 404 went through the generic not-found path: the proxy retried every
other credential (none can have the thread) and put the model into a 12h
cooldown on each, so the client's own recovery request with
thread: {"type":"create"} then failed with auth_unavailable.

Recognize thread_not_found (read from error.details.error_code) as a
request fault, like OpenAI's previous_response_not_found: it returns to
the client without rotating or cooling down credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 67f5749d91980c93cf43144ca4fa6418ef460766)
…ovider (MASTRA-765)

Rework the soonest-reset selector per MASTRA-765 scope 2.2-2.6.

- Windows by length: a Codex window is weekly when window-minutes >=
  10080, whichever position carries it; Claude reads 5h and 7d.
- Exhaustion gate: skip a credential while an observed window is
  exhausted (Codex used-percent >= 100, or the most used window when
  limit-reached is set; Claude utilization >= 1 or status rejected) and
  its reset is ahead. When every candidate is gated, the ordering alone
  decides and upstream 429 handling takes over.
- Unknown resets: Claude unknown sorts first and a passed Claude reset
  rolls forward by whole weeks; Codex unknown or passed sorts last.
- Delete the scheduler fast-path additions and the isBuiltInSelector
  entry. Session affinity always takes the legacy pick path, so they
  were unreachable in the supported config; without affinity the legacy
  path now calls the selector too.
- Quota signals: v8.0.13 hands the selector deep clones taken under the
  manager lock (availableAuthsForSelector, cloneAuthSlice), so reads are
  already race-clean; a race test follows in the next commit.

Drop the zsprackett tests for the deleted fast path and the replaced
unknown-first rule; the persistence test now checks restored signals
directly instead of the dropped model-scoped reset helper (4d7b2e84 was
not taken).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…RA-765)

- S1: Codex order by weekly reset, weekly window chosen by length.
- S2: a small remainder resetting soon goes before a fresh late window.
- S3: Claude 5h utilization and rejected status gate until the reset;
  Codex used-percent and limit-reached gate; all gated keeps the order.
- S5: Claude unknown first, Codex unknown or passed last, passed Claude
  reset rolled forward by whole weeks.
- S10: a manager saves observations through MarkResult, a second
  manager on the same store restores them, and a new session goes to
  the soonest reset with no new headers.
- Routing: the config's soonest-reset + session-affinity selector, and
  a manager with that selector, reach the selector through affinity on
  the legacy pick path; a bound session stays put.
- Race: concurrent MarkResult observations and picks under -race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TRA-765)

MarkResult saved a passive quota observation only when a minute had passed
since the last save. A second credential observed inside that minute was
lost on restart, so the soonest-reset selector ranked it as unknown.
Save whenever MarkResult records an observation, through the existing
cooldown-state save path. Upstream already rewrites the auth file on every
result, so a save per observation is in keeping.

Remove quotaObservationPersistInterval, Manager.observationPersistedAt and
the throttle test. The S10 restart test now records two credentials,
restarts and picks the soonest reset with no new headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing (MASTRA-765)

The soonest-reset selector dropped a Codex window when Window-Minutes did not
parse, which also dropped its exhaustion signal: a credential with a used-up
primary window could still be picked. Keep every window with a known reset
and use the parsed length only to mark the window weekly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saves skipped, and restore rejected, observations older than eight days. A
Claude credential idle for nine days lost its known weekly schedule across a
restart and the soonest-reset selector sorted it first as unknown. The
selector already handles a passed reset: Claude rolls forward by whole weeks
and a passed Codex reset sorts last.

Remove quotaObservationRetention, both age checks, the now parameters that
existed only for them, and the retention test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(MASTRA-765)

d595299 removed the one-minute save throttle; the save-cooldown-status
comment still described it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
MASTRA-765 Finish the soonest-weekly-reset credential selector on CLIProxyAPI v8.0.13 (from zsprackett's fork)

Context

Eric chose option 1 on MASTRA-763 (2026-10-03): drain subscription windows inside CLIProxyAPI with a soonest-weekly-reset selector, starting from zsprackett/CLIProxyAPI. Upstream will not take quota-aware routing (router-for-me/CLIProxyAPI#4715). We carry the patch in a private repo, mobilyze-llc/CLIProxyAPI, made by pushing upstream history and tags into a new private repository. GitHub allows no private fork of a public repo.

Policy: send each new session to the account whose weekly window resets soonest, among accounts that still have quota. For one shared weekly limit, that order never wastes more quota than any other. With per-model weekly limits it is a heuristic, not an optimum.

The plan was reviewed by Astra (gpt-6-astra, xhigh) on 2026-10-03, and I verified every finding I accepted against the source. This body reflects that review.

Starting point: zsprackett commits from 2026-10-02, MIT:

  • faa4ccff: soonest-reset strategy (sdk/cliproxy/auth/selector_soonest_reset.go, 118 lines).
  • 4d7b2e84: model-scoped weekly windows.
  • 01ad0021: persist observed quota with cooldown state.
  • 67f5749d: Claude thread_not_found as a request fault.

Upstream selector.go, service_config.go and config_basic.go are unchanged from v8.0.10 to v8.0.13. Only conductor_cooldown.go changed (+55/-20).

Scope

  1. U1. Create the private repo at tag v8.0.13 (d7914af), with an upstream remote for later rebases. CI runs go build ./... and go test ./... and must be green on unmodified upstream.
  2. U5, in one PR. The first commit carries the failing selector tests from MASTRA-768 (U4); its CI run is the evidence that they fail on stock. Later commits:
    1. Cherry-pick the four commits with authorship, and hand-merge conductor_cooldown.go.
    2. Windows by length. Codex: weekly when window-minutes >= 10080. Claude: 5h and 7d.
    3. Exhaustion gate. Skip a credential while an observed window is exhausted (Codex used-percent >= 100 or limit reached; Claude utilization >= 1 or status rejected) and that window's reset is in the future. If every credential is gated, ignore the gate and keep the ordering: upstream 429 handling and cooldown decide.
    4. Unknown resets.
      • Claude: an unknown weekly reset sorts first. Its weekly window runs on a fixed schedule, so one real request learns it and the ordering corrects itself. Roll a passed Claude reset forward by whole weeks.
      • Codex: an unknown or passed reset sorts last. That window is usually unstarted, and starting it early can waste it.
    5. Deletions. Remove zsprackett's unreachable scheduler.go additions and the isBuiltInSelector entry. Add a test proving routing reaches the selector through session affinity.
    6. Read quota signals in a way go test -race accepts.

Not in scope

  • Per-model weekly limits (Claude seven_day_opus and similar). No observed header establishes them. They stay with upstream's per-model cooldown after a 429.
  • Active usage polling. It stays out until measured.

Known limitation. A Codex window started outside the proxy is invisible until the proxy sends that account a request. Outside starts come from Eric's interactive CLIs and the factory's native routes, which use the same accounts. MASTRA-766 measures how often selection sees an unknown reset before anything is added.

Acceptance criteria

  • Go unit tests for S1 (order), S2 (soonest small remainder before fresh-but-late), S3 (5-hour gate), S5 (unknown handling per provider) and S10 (persisted observations). Each passes with go test -race ./sdk/cliproxy/..., and upstream's tests pass too.
  • The MASTRA-768 hermetic selector tests fail on the tests-only commit and pass at the PR head.
  • Stickiness. With routing.strategy: soonest-reset and session-affinity: true, a bound session follows upstream affinity: it moves only on cooldown or binding expiry. New sessions follow the ranking.
  • No token handling added. No token refresh, token-endpoint call or credential-file write.
  • Size recorded. The added non-test line count is in the PR.
  • Contribution back. Once the tests above pass, open one PR against zsprackett/CLIProxyAPI's main carrying only the selector changes and their Go tests: no e2e suite, no CI or MOBILYZE.md changes, nothing about studio2. Eric approved this on 2026-10-03. Do not wait on a merge.

Source refs

  • Parent: MASTRA-763 (findings, audit and decision comments).
  • Blocks MASTRA-766. Related: MASTRA-768 (tests), MASTRA-767 (factory routes).
  • Not in mastra-pilot: CODING_STANDARDS.md:21.

Review in Linear

ericlitman and others added 4 commits October 3, 2026 14:22
…ASTRA-765)

A token refresh rewrites the auth file, the watcher reloads it and
Manager.Update replaced the auth with one that carries no quota
observation. soonest-reset then ranked the credential as unknown until
its next response. Update now keeps the existing Signals and ObservedAt
when the incoming auth has no observation; an incoming observation still
wins and cooldown fields keep their upstream reset behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TRA-768)

S3 seeded a 5-hour reset four seconds ahead and slept five, so a CI pause
before the first assertions could flip the result. The exhausted window
now resets an hour out and only the skip is asserted. The Go case
"claude 5h reset passed" covers the transition after the reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est (MASTRA-768)

The test sent none of the four signals DetectClaudeCodeRequest needs
(X-App: cli, a claude-cli User-Agent, the claude-code beta and a valid
metadata.user_id), so it exercised the cloaking path. It now sends them,
claude-main cloaks every unconfirmed client, and the test asserts the
upstream request has no injected system prompt. Without the signals the
assertion fails on the billing-header system block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…affinity (MASTRA-765)

MOBILYZE.md listed 4d7b2e84 as taken; it was left out. config.example.yaml
now says soonest-reset is meant to run with session affinity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericlitman

Copy link
Copy Markdown
Contributor Author

Review gate: pstack:interrogate, run because Unfret is not installed here. Reviewers: Opus at max and Sol at max. Grok was a dropout. The verdict is recorded on MASTRA-765. Fix-round commits:

  • 91d1e912: keep quota observations across a credential replacement. An OAuth token refresh reloads the auth file through the watcher, and Manager.Update dropped Quota.Signals. The Go test fails without the fix, as does the service-path test.
  • 83abd29f: delete S3's wall-clock reset transition. Go's "claude 5h reset passed" case covers it.
  • 2db0cd3b: the hermetic Claude Code test now sends the four Claude Code detection signals and asserts the passthrough path, with no cloaking system injected. That assertion fails without the signals.
  • 83a9afb9: MOBILYZE.md lists only the taken commits, and config.example.yaml pairs soonest-reset with session affinity.

Correction to commit d595299's message: upstream does not rewrite the auth file on every result. FileTokenStore.Save skips unchanged JSON (sdk/auth/filestore.go:131). Saving every observation rewrites the .cds files on each observed response, about 1.2 ms for 6 credentials. That cost is accepted at our volume.

@ericlitman
ericlitman merged commit 3b2b025 into main Oct 3, 2026
4 of 5 checks passed
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