You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Failover theo phân loại lỗi (retry trước headers, capture mẫu lỗi thật) #5
Request lỗi account-level → tự failover sang acc khác. Bước đầu tiên là capture mẫu lỗi thật — KHÔNG đoán matcher:
Capture mẫu lỗi thật (prereq của matcher): acc hiện tại đang rỗng balances → gọi request thật để lấy mẫu quota-exhausted (status + biz code + body). Ghi mẫu vào issue này. Đừng spam upstream để kích 429 — nhánh unknown error phải log đầy đủ status + biz code + headers (redact credential) để mẫu tự tích lũy khi dùng thật.
Bảng phân loại lỗi (đã chốt):
429 + Retry-After → cooling acc đó đúng thời hạn, failover
429 không Retry-After → cooling 30s, failover
Quota exhausted (match theo mẫu captured) → exhausted tới quotaExpiresAt, failover
401 trước khi có response → single-flight refresh 1 lần; refresh fail vì network/5xx ≠ expired; fail xác nhận → expired, failover
Timeout / connection reset sau khi đã gửi body → KHÔNG retry, không rotate (không biết upstream xử lý chưa)
5xx / network → retry policy hiện có của proxy, không rotate acc
Global 429 (không phải account-level) → cooldown cấp pool ngắn, không rotate
Lỗi giữa stream → không retry, chỉ đánh dấu acc theo bằng chứng
Retry boundary: chỉ retry khi downstream chưa nhận response headers (upstream đã trả 429 nhưng proxy chưa forward là vẫn được). Body request phải replay được.
Single-flight refresh per account: N request cùng gặp JWT hết hạn → 1 promise refresh duy nhất, cả N chờ cùng kết quả; credential mới persist vào store; refresh fail network/5xx không đánh expired.
Acceptance criteria
Mẫu quota-exhausted thật được capture và comment vào issue; matcher viết theo mẫu đó
Mock upstream: acc A 429 → acc B 200 → client nhận 200, A cooling có Retry-After đúng
Acc A exhausted → request chuyển sang B, A không được pick tới hạn
401 → refresh 1 lần (verify chỉ 1 lần qua mock counter); refresh fail → expired; network fail → KHÔNG expired
Retry chỉ xảy ra khi chưa forward headers (test: upstream 429 trước stream → failover; lỗi giữa stream → client nhận lỗi, không failover)
N concurrent requests khi JWT hết hạn → đúng 1 refresh call
Timeout sau khi gửi body → không có request thứ 2 lên bất kỳ acc nào
Unknown error → log đầy đủ redacted, không rotate, không crash
Bun 1.3.x không phát server-side close khi client destroy đột ngột → release khi client abort có thể chỉ dựa vào pipeTo.finally (đợi upstream kết thúc). Stream treo/dài giữ slot mãi.
Mỗi reservation có max lifetime (config accounts.reservationTimeoutMs, default 15 phút). Quá hạn → force release + log cảnh báo.
Test: fake stream treo → watchdog giải phóng slot, request kế pick được acc.
Ghim version Bun trong Dockerfile; ghi issue upstream tham chiếu (bun issue về missing close event) — ghi link vào đây khi có.
3. Routes còn đọc primary (routes-mcp, async handler):
Đây vẫn là đường request của user. Primary exhausted mà acc B còn quota → hai route này vẫn đi primary và fail. Nối qua pool trong ticket này (cùng pattern handler.ts), hoặc tối thiểu ghi rõ decyzję: các route này dùng pool pick.\n
claim/runtime.ts là background job, không phải request path — để sau.
4. Small verifications (nếu chưa có test):
Acc bị xóa khỏi store khi đang có reservation → release() sau đó no-op an toàn, không throw.
Pass-through 1 acc: 429 trên acc duy nhất KHÔNG chuyển nó sang cooling rồi tự chặn request kế — hành vi phải y như cũ (no-pool). Test để pin behavior này.
Update về Bun socket-close bug — ĐÃ ĐƯỢC FIX trong Bun 1.4.0:
Local Bun 1.3.11: bug CÓ (repro: client destroy → không close event)
Docker oven/bun:1.4.0-alpine (image production đang dùng): ĐÃ FIX (repro: close event phát đúng)
→ Production (docker) KHÔNG bị ảnh hưởng. Chỉ môi trường dev chạy bun 1.3.11 local mới thấy.
Vẫn làm watchdog (accounts.reservationTimeoutMs, default 15 phút) vì: (1) phòng hồi quy Bun sau này, (2) stream treo upstream thật vẫn có thể giữ slot — không phụ thuộc Bun bug. Dockerfile đã pin 1.4.0-alpine từ trước — giữ nguyên.
Kết quả khảo sát 'refresh JWT' — KHÔNG có refresh grant programmatic trong codebase:
Facts từ code:
start-plan JWT: lấy từ OAuth login (token exchange cần authCode từ browser callback/server poll). JWT chỉ có iat, KHÔNG có exp (jwt-age.ts, verified live 2026-10: 8-day-old JWT vẫn dùng được). Upstream chỉ từ chối khi trả 401/3012 — không có expiry theo tuổi.
coding-plan: apiKey.secret long-lived, 401 nghĩa là key bị revoke → phải re-login interactive.
bigmodel: oauthAccessToken — cũng không có refresh_token grant trong code.
→ Không thể 'refresh' tự động: mọi đường đều cần interactive re-login (browser flow).
Thiết kế điều chỉnh cho 401 handling trong #5 (thay cho auto-refresh):
401 account-level → đánh dấu acc expired (không retry refresh — không có gì để refresh), failover sang acc khác.
Single-flight áp dụng cho việc kiểm tra/tránh stampede đánh dấu expired, không phải refresh call.
Mục 5 review (rotation) không áp dụng vì không có refresh response; mục 6 (lockfile) áp dụng khi user re-login bằng CLI trong khi server chạy — đã có lockfile protocol + hot-reload.
Mục 3 review (pool cập nhật credential ngay): khi user re-login qua CLI, store ghi qua lockfile → pool syncWithStore() (5s timer + trước mỗi reserve nếu stale) nạp credential mới — sẽ thêm 'sync-on-stale' trước reserve để không đợi tối đa 5s. Test kèm.
Mẫu lỗi thật #1 — quota-exhausted (captured live từ acc rỗng balances):
POST /v1/messages (anthropic passthrough, start-plan, acc rỗng balances)
HTTP/1.1 200 OK ← LƯU Ý: HTTP 200, KHÔNG PHÁI 4xx/5xx!
{"code":1005,"msg":"exceed quota limit","logid":"20261109054957b119e7a7a8bc0fc942ed"}
Và ở openai format (/v1/chat/completions), cùng lỗi hiện tại surfacing là:
HTTP/1.1 502
{"error":{"type":"translation_failed","message":"upstream returned invalid Anthropic message: {\"code\":1005,\"msg\":\"exceed quota limit\",\"logid\":...}"}}
Điểm mấu chốt: matcher KHÔNG được dựa vào status — phải quét body cho code:1005 / exceed quota limit dù HTTP status là 200. Đây chính là lý do review yêu cầu capture mẫu thật: đoán theo status sẽ miss hoàn toàn nhánh này.
Việc cần làm (phần còn lại của ticket):
Classifier: parse body JSON của response 200 → code === 1005 → mark acc exhausted (tới claim-expiry/quota reset khi biết) + failover nếu chưa stream.
Fixture test với body mẫu (đã sạch dữ liệu nhạy cảm — chỉ có code/msg/logid).
Chưa kích 429 — để mẫu tự tích lũy qua unknown-error logging.
Activity
longphamdev commentedon Nov 9, 2026
Bước đầu bắt buộc trước capture mẫu lỗi (từ review #4 — failover dựa vào pool giải phóng slot đúng):
1. Watchdog reservation (Bun socket-close regression mitigation):
pipeTo.finally(đợi upstream kết thúc). Stream treo/dài giữ slot mãi.accounts.reservationTimeoutMs, default 15 phút). Quá hạn → force release + log cảnh báo.2. Refresh JWT của acc non-primary:
saveAccount(match theo userId — facade đã fix Phase 1 hardening: golden KDF test, facade saveCredential match theo userId, .bak/lock polish #1). Nếu chưa: acc B hết hạn JWT → 401 liên tục, Failover theo phân loại lỗi (retry trước headers, capture mẫu lỗi thật) #5 failover sẽ CHE lỗi thay vì sửa. Test: refresh acc B không đè primary A (integration-level).3. Routes còn đọc primary (
routes-mcp,async handler):claim/runtime.tslà background job, không phải request path — để sau.4. Small verifications (nếu chưa có test):
release()sau đó no-op an toàn, không throw.exhaustedrevive theoquotaExpiresAt(đã có test ở AccountPool module: state machine, round-robin, inflight cap, queue timeout #3 pool), xác nhận KHÔNG nhầm vớitokenExpiresAt— thêm test nếu thiếu.longphamdev commentedon Nov 9, 2026
Update về Bun socket-close bug — ĐÃ ĐƯỢC FIX trong Bun 1.4.0:
oven/bun:1.4.0-alpine(image production đang dùng): ĐÃ FIX (repro: close event phát đúng)→ Production (docker) KHÔNG bị ảnh hưởng. Chỉ môi trường dev chạy bun 1.3.11 local mới thấy.
Vẫn làm watchdog (
accounts.reservationTimeoutMs, default 15 phút) vì: (1) phòng hồi quy Bun sau này, (2) stream treo upstream thật vẫn có thể giữ slot — không phụ thuộc Bun bug. Dockerfile đã pin1.4.0-alpinetừ trước — giữ nguyên.longphamdev commentedon Nov 9, 2026
Kết quả khảo sát 'refresh JWT' — KHÔNG có refresh grant programmatic trong codebase:
Facts từ code:
authCodetừ browser callback/server poll). JWT chỉ cóiat, KHÔNG cóexp(jwt-age.ts, verified live 2026-10: 8-day-old JWT vẫn dùng được). Upstream chỉ từ chối khi trả 401/3012 — không có expiry theo tuổi.apiKey.secretlong-lived, 401 nghĩa là key bị revoke → phải re-login interactive.oauthAccessToken— cũng không córefresh_tokengrant trong code.→ Không thể 'refresh' tự động: mọi đường đều cần interactive re-login (browser flow).
Thiết kế điều chỉnh cho 401 handling trong #5 (thay cho auto-refresh):
expired(không retry refresh — không có gì để refresh), failover sang acc khác.account <id> credential rejected upstream — run: zcode-proxy auth login zai --add(re-login cùng acc sẽ upsert đúng entry theo userId — nhờ Phase 1 hardening: golden KDF test, facade saveCredential match theo userId, .bak/lock polish #1).Mục 3 review (pool cập nhật credential ngay): khi user re-login qua CLI, store ghi qua lockfile → pool
syncWithStore()(5s timer + trước mỗi reserve nếu stale) nạp credential mới — sẽ thêm 'sync-on-stale' trước reserve để không đợi tối đa 5s. Test kèm.longphamdev commentedon Nov 9, 2026
Mẫu lỗi thật #1 — quota-exhausted (captured live từ acc rỗng balances):
Và ở openai format (
/v1/chat/completions), cùng lỗi hiện tại surfacing là:Điểm mấu chốt: matcher KHÔNG được dựa vào status — phải quét body cho
code:1005/exceed quota limitdù HTTP status là 200. Đây chính là lý do review yêu cầu capture mẫu thật: đoán theo status sẽ miss hoàn toàn nhánh này.Việc cần làm (phần còn lại của ticket):
code === 1005→ mark acc exhausted (tới claim-expiry/quota reset khi biết) + failover nếu chưa stream.longphamdev commentedon Nov 9, 2026
#5 hoàn thành — tổng kết các commit:
c35c2d787c28d23b26b2b2dae80294fd841dfd725f1b14f1eĐiểm chính:
{code:1005}— classifier quét body, không dựa status (đã lưu fixture, không có dữ liệu nhạy cảm)Suite: local 1209 pass / 0 fail; container 1210 pass / 0 fail. Worktree #2 đã sync tới HEAD.
Frontier còn lại: #6 (poller + /quota schema). CLI (#2) làm khi cần demo.
longphamdev commentedon Nov 9, 2026
Hoàn thành: failover 401/1005 + global guards (401/429) + half-open + routes-mcp/async wiring + capture mẫu lỗi thật. Suite 1209/0 local, 1210/0 container. Chi tiết ở comment tổng kết. Frontier: #6.
feat(proxy): integrate AccountPool into request path (#4)
feat(auth): reservation watchdog + single-account 429 parity (#5 prep)
fix(proxy): watchdog aborts upstream + stream-wrapper release (#5 pre…
test(proxy): wrapper data-path integrity + env-independent update che…
feat(proxy): 401 account failover + credential freshness + state-rese…
feat(proxy): 401 hardening — half-open, global-outage guard, tried-set (
feat(proxy): biz-1005 quota exhaustion + route pool wiring (#5 step 3)
feat(proxy): global-429 pool guard (#5 step 4)
fix(proxy): 1005 peek policy + exhausted half-open + guard escapes (#5 …
test(proxy): 1005 scan safety + global-401 escape hatch verified (#5 …