Repository navigation
fix(routing): earliest-reset-first 4-fix bundle (post-deploy hotfix) - #23
Conversation
PR #21 (apex-v2.0.0) 머지 후 production 에서 다음 4가지 회귀 관찰됨: (1) openai-compatible llama.cpp → "all candidates excluded" 503 (2) opus/haiku 가 같은 시점 다른 계정으로 분기 (modelWindowMapping) (3) 100% fresh Anthropic 계정이 missing 처리되어 자동 제외 (4) APEX 17%/2h 가 GNUMAX 87%/4h 보다 우선 (burn-down 의도 위반) Fix: - F1: self-hosted/openai-compatible (no cache) → score=0 fallback 429-marked empty cache 는 excluded 유지 (부활 X) - F2: drop modelWindowMapping (Omelette/Sonnet hard guard 제거) - F3: resetAt=null AND Q=100% → fresh max-urgency (nothing else) - F4: additive S=0.85T+0.15Q → multiplicative S=T×Q penalty 100x rescale, Q_SATURATION_CAP 삭제 Verification: - 48/48 strategy unit tests PASS (기존 + F1×4, F2×2, F3×6, F4×6 신규) - 2791/2791 full unit suite PASS - prettier / eslint / typecheck:core / typecheck:noimplicit:core / docs-sync clean - APEX 17%/2h vs GNUMAX 87%/4h scenario test asserts GNUMAX selection - 429-marked cache excluded test (Oracle B1 patch verified) Plan: .sisyphus/plans/routing-strategy-v6.md (Oracle 4-blocker + Momus 9-issue resolved) Independent of PR #22 (mojibake hotfix).
There was a problem hiding this comment.
Pull request overview
Hotfix update to the earliest-reset-first routing strategy after production regressions in v3.7.0, shifting the strategy to a v6 model that removes per-model weekly window logic and changes scoring to a multiplicative urgency×remaining formulation.
Changes:
- Rewrite
earliest-reset-firstscoring toT_pts × Q_remain, add fresh-quota handling (Q===100 && resetAt===null), and add ascore=0fallback for no-cache self-hosted providers. - Remove model-specific weekly-window mapping and drop the
modelHintparameter from strategy APIs and theauth.tscall site. - Add/adjust unit tests for the four production regressions and bump version/docs/changelog to
3.7.1.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/sse/services/strategies/earliestResetFirst.ts |
v6 rewrite: multiplicative scoring, fresh quota handling, self-hosted fallback, API signature changes. |
src/sse/services/strategies/modelWindowMapping.ts |
Deleted; per-model weekly window mapping removed. |
src/sse/services/auth.ts |
Updates caller to new selectByEarliestResetFirst(candidates, sessionId) signature and updates strategy comment. |
tests/unit/auth-strategy-earliest-reset-first.test.mjs |
Updates fixtures and adds regression coverage for F1–F4 behaviors. |
package.json |
Patch version bump to 3.7.1. |
docs/openapi.yaml |
Updates OpenAPI info.version to 3.7.1. |
CHANGELOG.md |
Adds 3.7.1 release notes describing the hotfix bundle. |
| const s = scoreSessionTrack(conn.id); | ||
| if (s.kind === "excluded") return { valid: false, reason: s.reason }; | ||
| const w = scoreWeeklyTrack(conn.id, modelHint); | ||
| const w = scoreWeeklyTrack(conn.id); | ||
| if (w.kind === "excluded") return { valid: false, reason: w.reason }; |
There was a problem hiding this comment.
isAffinityValid can incorrectly keep affinity pinned to a connection that was marked quota-exhausted via markAccountExhaustedFrom429(). In that case both tracks are missing (empty quotas) so this function returns valid: true, while scoreAccount() would exclude it via isAccountQuotaExhausted(). Consider checking isAccountQuotaExhausted(conn.id) here (and returning invalid) so affinity breaks and selection can fall back to another account; add a regression test for this scenario.
Copilot review on PR #23 caught an inconsistency between scoreAccount and isAffinityValid: - scoreAccount's F1 branch correctly distinguishes self-hosted (no cache entry → score=0 fallback) from 429-marked exhausted (cache exists with empty quotas + exhausted=true → excluded). - isAffinityValid had no equivalent guard. A connection marked exhausted via markAccountExhaustedFrom429() has empty quotas, so scoreSessionTrack and scoreWeeklyTrack both return kind:"missing" (not "excluded"). With no excluded gate, isAffinityValid returned valid:true and pinned the next request to the same 429-burning account. Add the same isAccountQuotaExhausted guard in isAffinityValid (after the static rate-limit/terminal checks, before quota-track scoring) so affinity breaks and selection falls back to another candidate via the normal scoring path. Regression test added: 429-marked + bound session must produce valid:false with reason "quota_exhausted_unknown_reset". Strategy tests: 49/49 PASS. Full unit: 2792/2792 PASS.
Summary
apex-v2.0.0) shippedearliest-reset-firstwith an additiveS = 0.85·T + 0.15·min(Q, 30). Production observation revealed four regressions, all addressed in this single hotfix.prettier/eslint/typecheck:core/typecheck:noimplicit:core/check:docs-syncclean..sisyphus/plans/routing-strategy-v6.md(Oracle 4-blocker review + Momus 9-issue review resolved).Regressions fixed
trackScores=0branches:isAccountQuotaExhausted→ excluded; otherwisescore=0fallback (paid still wins, self-hosted usable)modelWindowMapping.tsentirely. Routing reads only overallweeklywindow. 429s on per-model quota retry viaaccountFallbackresetAtuntil first request) auto-excludedQ === 100 && resetAt === null→ max-urgencyT=100, score=10000; other nullresetAtcases staymissing17%/2houtranked GNUMAX87%/4h(additive formula privileges urgency over abundance)S = T_pts × Q_remain, score range[0, 10000]. Penalties rescaled 100×.Q_SATURATION_CAPdeletedHand verification (user's actual scenario)
→ GNUMAX wins by 770 points. Asserted in test
F4-1.Behaviour matrix (post-fix)
score=0, eligible; lex tie-break across self-hosted onlyexcluded: "quota_exhausted_unknown_reset"score>0) + self-hosted (score=0) mixedbackoffLevel=4(finalScore=-8400) + self-hostedQ=100, resetAt=null(fresh)score=10000(max urgency)Q=80, resetAt=null(ambiguous)kind: "missing"Q < 5%either trackexcludedT × QproductAPI change
selectByEarliestResetFirst(candidates, sessionId)—modelHintparameter removed.scoreAccount(conn),scoreWeeklyTrack(connId),isAffinityValid(conn, sessionId)—modelHintremoved.auth.ts:706-714updated; only one caller site, no external API surface change.Files
src/sse/services/strategies/earliestResetFirst.ts— rewritten (4-fix bundle)src/sse/services/strategies/modelWindowMapping.ts— deletedsrc/sse/services/auth.ts— caller sitemodelHintremoved + comment updatedtests/unit/auth-strategy-earliest-reset-first.test.mjs— fixture simplified, 18 new tests addedpackage.json3.7.0 → 3.7.1(patch bump for hotfix)docs/openapi.yamlinfo.version: 3.7.1CHANGELOG.md— new[3.7.1]section.sisyphus/plans/routing-strategy-v5.md— marked superseded.sisyphus/plans/routing-strategy-v6.md— canonical SoT (separate workspace dir)Out of scope
cacheTelemetryfollow-up fields (still v4 §6 follow-up).auth.ts.origcleanup.Tag plan
Merge → tag
apex-v2.0.1(patch bump on top ofapex-v2.0.0).