-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(providers): pick a warm API key before the first attempt #4277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a3759e
a11da68
e395a87
2e6841b
e1cc655
fe2b763
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,42 @@ Extract the rotation primitives into a credential-neutral kernel, then make the | |
| generic OAuth kind actually consume the `strategy` and `autoSwitchThreshold` it | ||
| already persists. | ||
|
|
||
| ## Availability and the slice this cycle can actually take | ||
|
|
||
| Re-verified at the wp2 P entry against `origin/dev`. The lane partition for the | ||
| round in flight does not list `src/oauth/generic-account-failover.ts`, | ||
| `src/oauth/pool-settings-capability.ts` or `src/codex/pool-rotation.ts`, so the | ||
| kernel extraction and the generic-kind strategy work are available now. Two things | ||
| are not: | ||
|
|
||
| - `src/codex/routing.ts` is owned by lane L3, so the Codex-side import swap waits. | ||
| - `src/server/responses/core.ts` is owned by lane L1 and is the most contended | ||
| file in the round with four open PRs, which is also why the wp4b call-site | ||
| wiring could not follow #4277 immediately. | ||
|
|
||
| This cycle takes the kernel, the Anthropic import swap and the generic consumer. | ||
| Only the CODEX import swap is deferred, and it is deferred for free: once | ||
| `pool-rotation.ts` re-exports the kernel, `src/codex/` keeps its existing import | ||
| path and needs no edit at all. So the contended files stay out of this PR without | ||
| the kernel being an orphan. | ||
|
|
||
| Two kinds of change are moving here and they carry different risk, which is why | ||
| only one of them is behind the flag: | ||
|
|
||
| - **Relocation** is behaviour-preserving. Moving the state and primitives into | ||
| `pool-kernel.ts` and re-exporting them changes no selection outcome, so it is | ||
| not flagged. `git` history and a green existing suite are its proof. | ||
| - **Behaviour** is flagged. The generic kind consuming `strategy` and | ||
| `autoSwitchThreshold`, and the DTO reporting `inert: false`, only happen when | ||
| `pool.kernel` is on. Flag off restores today's outcomes exactly, because the | ||
| pre-kernel path is the same code reached through the shim. | ||
|
Comment on lines
+37
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Describe flag-off as behavioral parity, not restoration of the pre-kernel implementation path. Relocation into 🤖 Prompt for AI Agents |
||
|
|
||
| Anchors confirmed present on `origin/dev`: `selectPriorityTier` :86, | ||
| `pickRoundRobinAccount` :189, `notePoolRotationSuccess` :213, | ||
| `seedPoolRotationAccount` :245, `reconcilePoolRotationState` :260 in | ||
| `pool-rotation.ts`; `preferredInitialAccount` :246 and the | ||
| `rankAccountsByHeadroom` import :19 in `generic-account-failover.ts`. | ||
|
|
||
| ## Current behaviour (verified on dd9a2906b) | ||
|
|
||
| The primitives already take an opaque `poolKey`, so a third key is addable: | ||
|
|
@@ -45,29 +81,73 @@ and stamps `inert: true` on the generic DTO (`:40-54`, `:57-67`). | |
| ## Change surface | ||
|
|
||
| NEW `src/oauth/pool-kernel.ts` | ||
| - move `SelectionState`, `pickRoundRobinAccount`, `peekRoundRobinAccount`, | ||
| `seedPoolRotationAccount`, `notePoolRotationSuccess`, `notePoolRotationFailure`, | ||
| `selectPriorityTier`, and the strategy/sticky normalizers | ||
| - move the WHOLE private `selectionState` map together with | ||
| `pickRoundRobinAccount`, `peekRoundRobinAccount`, `seedPoolRotationAccount`, | ||
| `notePoolRotationSuccess`, `notePoolRotationFailure`, `clearPoolRotationState`, | ||
| `selectPriorityTier`, the priority parsers, `POOL_KEY_*` and the strategy and | ||
| sticky normalizers. Moving a function subset while leaving the map behind would | ||
| split one piece of state across two modules. | ||
| - the move is safe: `pool-rotation.ts` imports only two TYPES, | ||
| `OcxAccountPoolRotationStrategy` from `../types` and `GenerationContext` from | ||
| `../lib/state-store-sweeper`. Neither creates a cycle into `src/oauth`. | ||
| - add `genericPoolKey(provider) => \`generic:\${provider}\`` | ||
| - lift fill-first to `pickFillFirst(ids, afterId, hasHeadroom)` so both existing | ||
| copies call one implementation | ||
| - extend the reconcile sweep to `generic:*` keys, which `:270-276` currently skips | ||
| - add a fill-first helper with the signature | ||
| `pickFillFirst(ids, afterId, hasHeadroom, stableAll)`. The earlier three-argument | ||
| shape was rejected by the audit: both existing copies walk a STABLE FULL roster | ||
| and not the eligible subset, so dropping `stableAll` changes the wrap order | ||
| whenever an ineligible id sits between two eligible ones. | ||
| - extend the reconcile sweep to `generic:*`. `buildGenerationContext` already fills | ||
| `oauthAccountKeys` from `listLiveOAuthAccountKeys` as `provider\0id` for every | ||
| live OAuth provider, so the sweep needs no new field and no Codex dependency; | ||
| today those keys are simply skipped as `valid === null`. | ||
|
Comment on lines
+99
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 12 \
'reconcilePoolRotationState|oauthAccountKeys|genericPoolKey|generic:' \
src/oauth src/lib src/codexRepository: lidge-jun/opencodex Length of output: 13477 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- pool rotation implementation ---'
sed -n '1,340p' src/codex/pool-rotation.ts
printf '%s\n' '--- OAuth account enumeration ---'
rg -n -C 8 'function listLiveOAuthAccountKeys|export function listLiveOAuthAccountKeys|listLiveOAuthAccountKeys' src
printf '%s\n' '--- plan context ---'
sed -n '80,115p' devlog/_plan/260911_account_pool_unification/020_phase2_shared_kernel.mdRepository: lidge-jun/opencodex Length of output: 16335 🤖 get_repo_knowledge executed:
Length of output: 12500 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- generic failover pool-key and account flow ---'
rg -n -C 12 \
'genericPoolKey|POOL_KEY|pickRoundRobinAccount|seedPoolRotationAccount|notePoolRotation|preferredInitialAccount|accountId' \
src/oauth/generic-account-failover.ts src/oauth/anthropic-routing.ts src/codex/routing.ts
printf '%s\n' '--- pool rotation tests and plan references ---'
rg -n -C 8 \
'reconcilePoolRotationState|generic:|genericPoolKey|seedPoolRotationAccount|stale|rotation state' \
tests src devlog/_plan/260911_account_pool_unificationRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 10 \
'genericPoolKey|pickRoundRobinAccount|seedPoolRotationAccount|preferredInitialAccount|notePoolRotation' \
src/oauth/generic-account-failover.ts src/oauth/anthropic-routing.ts src/codex/routing.ts
rg -n -C 6 \
'reconcilePoolRotationState|generic:|genericPoolKey|seedPoolRotationAccount|pool rotation' \
tests/codex-integration tests/oauth tests -g '*pool*' -g '*failover*' -g '*rotation*' 2>/dev/null || true
sed -n '115,180p' devlog/_plan/260911_account_pool_unification/020_phase2_shared_kernel.mdRepository: lidge-jun/opencodex Length of output: 29361 Define the generic pool reconciliation contract.
🤖 Prompt for AI Agents |
||
|
|
||
| NOT moved, deliberately: the Codex fill-first copy in `src/codex/routing.ts` stays | ||
| where it is. Deleting it is the only thing that would force an edit to a file lane | ||
| L3 owns, and the audit flagged that as a blocker against this unit's own freeze. | ||
| Only `anthropic-routing.ts` and the generic kind switch to the kernel helper, and | ||
| the Anthropic caller keeps its weekly `exhausted5h` pre-filter rather than pushing | ||
| that rule into the shared helper. | ||
|
|
||
| MODIFY `src/codex/pool-rotation.ts` — re-export the kernel so existing importers | ||
| and `tests/codex-integration/codex-pool-rotation.test.ts` keep working unchanged. | ||
|
|
||
| MODIFY `src/oauth/generic-account-failover.ts` — route selection through the kernel | ||
| by strategy: `quota` keeps `rankAccountsByHeadroom`, `round-robin` calls | ||
| `pickRoundRobinAccount(genericPoolKey(name), ...)`, `fill-first` calls the lifted | ||
| helper; seed on manual selection; note success and failure. Keep the presence | ||
| MODIFY `src/oauth/generic-account-failover.ts` — branch BOTH paths on strategy, not | ||
| just the proactive one. `preferredInitialAccount` currently no-ops when the active | ||
| account is healthy and requires `hasHeadroomEvidence`, and the 429 path always ends | ||
| in `rankAccountsByHeadroom`; leaving either unbranched keeps the strategy inert in | ||
| practice even after the DTO says otherwise. `quota` keeps | ||
| `rankAccountsByHeadroom`, `round-robin` calls | ||
| `pickRoundRobinAccount(genericPoolKey(name), ...)`, and `fill-first` uses the | ||
| kernel helper with `autoSwitchThreshold` as its headroom test. Keep the presence | ||
|
Comment on lines
+114
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 20 \
'rotateGenericOAuthAccountOn429|pickRoundRobinAccount|notePoolRotationSuccess|failedAccountId|parseRetryAfter' \
src/oauth testsRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- plan excerpt ---'
cat -n devlog/_plan/260911_account_pool_unification/020_phase2_shared_kernel.md | sed -n '90,135p'
printf '%s\n' '--- generic failover implementation ---'
cat -n src/oauth/generic-account-failover.ts | sed -n '1,225p'
printf '%s\n' '--- rotation helper definitions and callers ---'
rg -n -C 12 \
'export (function|const) (pickRoundRobinAccount|notePoolRotationSuccess|notePoolRotationFailure|seedPoolRotationAccount)|function (pickRoundRobinAccount|notePoolRotationSuccess|notePoolRotationFailure|seedPoolRotationAccount)|pickRoundRobinAccount\(|notePoolRotationSuccess\(' \
src testsRepository: lidge-jun/opencodex Length of output: 32854 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- rotation state algorithm ---'
cat -n src/codex/pool-rotation.ts | sed -n '90,255p'
printf '%s\n' '--- phase-2 test requirements ---'
rg -n -C 8 \
'TEST|test|429|round-robin|fill-first|cursor|rotation|generic-account-failover' \
devlog/_plan/260911_account_pool_unification/020_phase2_shared_kernel.md
printf '%s\n' '--- generic failover tests around reactive rotation ---'
cat -n tests/oauth/generic-oauth-failover.test.ts | sed -n '80,190p'Repository: lidge-jun/opencodex Length of output: 24389 Preserve the 429 ring and rotation state for each strategy. At 🤖 Prompt for AI Agents |
||
| quorum, the `EXCLUDED_PROVIDERS` guard and the per-provider `health` cooldown. | ||
|
|
||
| MODIFY `src/oauth/pool-settings-capability.ts` — drop `inert: true`, add | ||
| `stickyLimit`. MODIFY `src/types/provider.ts:512-518` comments and | ||
| `oauth-account-routes.ts:395` to accept `stickyLimit`. | ||
|
|
||
| MODIFY `src/codex/routing.ts` and `src/oauth/anthropic-routing.ts` — import from | ||
| the kernel instead of holding their own copies. | ||
| MODIFY `src/server/management/oauth-account-routes.ts` — a manual account selection | ||
| must seed the cursor, or the operator's pick immediately loses to sticky | ||
| round-robin. Today that PUT calls only `forgetGenericFailoverRoster`, which clears | ||
| the presence cache and not the rotation state. Add | ||
| `seedPoolRotationAccount(genericPoolKey(provider), accountId)` beside it, mirroring | ||
| what `resetAnthropicRoutingForManualSelection` already does for Anthropic. | ||
| `clearGenericFailoverHealth` is the wrong map and `clearPoolRotationState` wipes | ||
| where seeding is wanted. | ||
|
Comment on lines
+124
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add a focused regression test for generic manual-selection seeding. The Phase 2 test list covers generic strategy selection and capability assertions. 🤖 Prompt for AI Agents |
||
|
|
||
| MODIFY `src/oauth/pool-settings-capability.ts` — report `inert` from the flag rather | ||
| than as a type literal. While `pool.kernel` is off the generic DTO must keep saying | ||
| `inert: true`, because nothing consumes the strategy yet and the reversibility rule | ||
| below requires the old behaviour to be exactly restorable. The literal becomes a | ||
| computed field and only turns false once the kernel is on. | ||
|
|
||
| Known readers of that field, all of which move in the same PR: | ||
| `src/cli/account-extended.ts` (forces generic auto-switch inactive), | ||
| `tests/server/account-pool-management-api.test.ts` and | ||
| `tests/cli/cli-account-pool-verbs.test.ts`. The GUI does not read it. | ||
| Also lift the `stickyLimit` rejection at `oauth-account-routes.ts:395` and update | ||
| `src/types/provider.ts:512-518` comments. | ||
|
|
||
| MODIFY `src/oauth/anthropic-routing.ts` — import from the kernel. `src/codex/` | ||
| keeps importing `./pool-rotation`, which is now a re-export, so this layer needs | ||
| no edit inside lane L3's files at all. The audit confirmed the shim is sufficient: | ||
| `routing.ts`, `auth-api.ts`, `account-priority.ts` and | ||
| `state-store-registrations.ts` all keep their existing import path. | ||
|
Comment on lines
+146
to
+150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add the missing kernel bindings to the generic and management consumers. The change surface calls 🤖 Prompt for AI Agents |
||
|
|
||
| ## Reversibility (audit blocker, mandatory) | ||
|
|
||
|
|
@@ -85,6 +165,13 @@ the kernel instead of holding their own copies. | |
|
|
||
| ## Tests | ||
|
|
||
| Audit record: the A-phase reviewer returned PASS-WITH-FINDINGS with two blockers, | ||
| both folded above. The first was that lifting fill-first out of its Codex copy | ||
| would have forced an edit inside lane L3's freeze. The second was that dropping | ||
| `inert: true` unconditionally contradicts this document's own reversibility rule, | ||
| which requires `pool.kernel` to default off and the old behaviour to be exactly | ||
| restorable. | ||
|
|
||
| - `tests/codex-integration/codex-pool-rotation.test.ts` — unchanged behaviour | ||
| through the re-export (`pickRoundRobinAccount` `:270`, `selectPriorityTier` `:111`) | ||
| - `tests/oauth/generic-oauth-failover.test.ts` — a configured strategy changes the | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50375
🤖 get_repo_knowledge executed:
get_repo_knowledge lidge-jun/opencodex /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/learnings /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventionsLength of output: 12149
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 42712
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 11760
Make every effective-active read scope-aware.
The plan keys
manualPreferencebycodexPoolKeyForScopebut also requiresgetEffectiveActiveCodexAccountIdto return the live preference. The current API atsrc/codex/routing.ts:1625accepts noquotaScope. Scoped paths at:1379,:2029, and:2218can therefore read the sharedcodexpreference forsparkorreserve; the resolved request can then consume that shared one-shot. PassquotaScopethrough these reads, or bypass the preference for independent scopes. Add a regression test proving that an independent scope neither applies nor consumes the shared preference.🤖 Prompt for AI Agents