feat: auto region cho API key Kiro + viet hoa giao dien (vi) - #5
Conversation
A Kiro API key (ksk_) is bound to the data-plane region it was minted in. A request sent to the wrong region is rejected as an invalid bearer token (HTTP 403) even though the key is valid elsewhere. The stored region can be wrong when an account was imported without a region probe or the key was re-issued in a different region. On an auth failure for an api_key account, reprobe the key against the candidate regions and, if another region accepts it, persist that region and retry the call once. Auth errors are raised before any streaming callback fires, so the single retry cannot double-emit output to the client. - config: add UpdateAccountRegion to persist a re-detected region - kiro: split callKiroEndpointsOnce out of CallKiroAPI and wrap it with the api_key region-failover retry - kiro_api: add reprobeApiKeyRegion using the same probe as the add-key flow - tests: cover redetect+persist, dead-key, and non-api_key skip paths
Adds a full Vietnamese locale alongside the existing Chinese (default) and English translations, and makes the language switching logic language-agnostic instead of a hardcoded zh/en binary. - web/locales/vi.json: complete 512-key Vietnamese translation - en.json / zh.json: add lang.vi plus uptime.d/h/m/s and models.familyOther keys (previously hardcoded in app.js) - index.html: add a VI button to both language switchers - app.js: language toggle now cycles zh -> en -> vi; formatUptime and the model-family "Other" label use translation keys instead of an inline zh/en check All three locales verified to share an identical 512-key set.
zsecducna
left a comment
There was a problem hiding this comment.
Cảm ơn PR — hướng giải quyết đúng và bổ sung tốt cho resolveApiKeyRegion trên main (probe lúc add; PR này probe lúc request). Hai đầu ghép lại là đủ. go build, go vet, và toàn bộ 293 test đều pass.
Nhưng có 2 vấn đề chặn merge. Repo chưa có CI (mergeStateStatus: UNSTABLE chỉ nghĩa là không có check nào), nên mình đã verify tay.
🔴 1. Data race trên account.Region (đã reproduce bằng -race)
proxy/kiro_api.go:183 — account.Region = region ghi không có lock.
Pointer là shared, không phải copy: pool.GetNextForModelWithApiKey trả &p.accounts[i]. Session affinity chủ động ghim 1 customer key vào 1 account, nên request đồng thời trên cùng account là trường hợp thường gặp, không phải corner case.
Reader chạy song song: kiroRegionForProfile (proxy/kiro_api.go:49) — mọi request đều gọi để build endpoint URL. Go string là giá trị multi-word → torn read là undefined behavior.
Test reproduce (bỏ vào proxy/, chạy go test ./proxy/ -run TestRaceRepro -race):
func TestRaceRepro(t *testing.T) {
mustInitConfig(t)
orig := probeKiroApiKey
t.Cleanup(func() { probeKiroApiKey = orig })
probeKiroApiKey = func(key, region string) (*config.AccountInfo, error) {
return &config.AccountInfo{Email: "x@example.com", UserId: "u1"}, nil
}
acct := &config.Account{
ID: "race-1", AuthMethod: "api_key",
KiroApiKey: "ksk_race", AccessToken: "ksk_race", Region: "us-east-1",
}
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); reprobeApiKeyRegion(acct) }()
go func() { defer wg.Done(); _ = kiroRegionForProfile(acct, "") }()
wg.Wait()
}Kết quả: WARNING: DATA RACE — write tại kiro_api.go:183 vs read tại kiro_api.go:49.
Ngoài ra không có singleflight: N request cùng 403 trên 1 account → N lượt probe, N lần ghi không đồng bộ, N lần Save().
🔴 2. Key multi-region bị nhân đôi slot → quota bị đếm 2 lần
handleAdminAddKiroApiKey coi multi-region là 1 slot cho mỗi region key phục vụ, dedup theo (UserId, region) qua findAccountForKiroIdentity. reprobeApiKeyRegion giả định "chỉ có đúng 1 region đúng" và dời slot, không dedup.
Kịch bản: key phục vụ cả 2 region → có slot A(us-east-1) và B(eu-central-1). B gặp bất kỳ lỗi nào IsAuthFailure phân loại là auth → reprobe bỏ qua eu-central-1, probe us-east-1 (luôn đứng đầu candidate list), thành công → flip B.Region = us-east-1.
Kết quả: 2 account trùng UserId + region → routing weight gấp đôi, quota bị đếm 2 lần trong /admin/pool. Đây đúng là invariant mà dedup lúc add sinh ra để bảo vệ, và không có gì reconcile lại.
🟡 Các điểm nên sửa luôn trong pass này (không chặn merge)
3. Latency trong serving path. Mỗi probe dùng REST client timeout 30s, không cache/backoff/circuit-breaker. Ban xảy ra sau khi probe xong (account_failover.go), và handler loop thử tới maxAccountRetryAttempts=3 account → 1 request của khách có thể tốn tới 3 lượt sweep (~90s mặc định; với KIRO_PROFILE_REGIONS nhiều region thì còn lâu hơn).
4. Claim "cannot double-emit" đúng nhưng không được enforce. Với path HTTP status thì đúng: 401/403/402 return trước parseAndStream. Nhưng cổng retry là pool.IsAuthFailure(err) — một string classifier áp lên mọi error, kể cả error trả về sau khi callback đã bắn (mid-stream read error, errEventStreamFrameTooLarge). Hiện tại an toàn chỉ vì các chuỗi đó tình cờ không chứa token 401/403. Không có gì cấu trúc (vd. flag emitted) giữ invariant này — nếu sau này có error path nào bubble chuỗi trông giống auth sau khi stream, khách sẽ nhận output nhân đôi và bị tính dư output token.
5. ProfileArn làm feature thành no-op. kiroRegionForProfile ưu tiên payload.ProfileArn → account.ProfileArn → account.Region. Account api_key có ProfileArn khác rỗng (config sửa tay / import path tương lai) → reprobe flip & persist Region, nhưng retry vẫn regionalize theo ARN → vẫn 403 → account bị ban, và config còn lại Region mâu thuẫn với ARN thực dùng. reprobeApiKeyRegion không check/clear ProfileArn.
6. Skip check phân biệt hoa thường. kiro_api.go:173 dùng normalizeRegion(region) == current, trong khi chỗ dedup dùng strings.EqualFold. Account có Region: "US-EAST-1" sẽ probe lại chính region vừa 403 (phí 1 lượt 30s).
✅ Phần locale vi — OK, không có vấn đề
web/locales/vi.json: JSON hợp lệ, đúng 512 key, parity đầy đủ với en.json/zh.json (đã verify bằng script), không có value rỗng. en/zh đều đã nhận 6 key mới. Toggle xử lý đúng khi stored lang lạ (indexOf −1 → wrap về zh). Cả 2 switcher trong index.html đều có nút VI.
Nếu muốn ship sớm, tách phần vi ra PR riêng thì merge được ngay.
Đề xuất: thêm lock/singleflight quanh chỗ ghi region (mục 1) + check dedup trước khi flip region (mục 2), kèm regression test cho cả hai. Mục 3–5 nên gộp luôn: singleflight + guard "chưa emit byte nào" + dedup check giải quyết được phần lớn.
Tóm tắt / Summary
PR này bổ sung 2 tính năng độc lập:
feat(kiro))feat(web))1. Auto re-detect region cho API key (
ksk_)Vấn đề
Một Kiro API key (
ksk_...) bị ràng buộc theo data-plane region nơi nó được cấp. Nếu request gửi tới sai region, upstream trả về:dù key vẫn hợp lệ ở region khác. Region lưu trong tài khoản có thể sai khi:
Trước đây
CallKiroAPIgặp 401/403 là trả lỗi ngay, không hề dò lại region cho tài khoảnapi_key(chỉ dò lúc thêm key mới), nên tài khoản luôn fail — và ở luồng phục vụ thật còn bị ban nhầm.Giải pháp
Khi tài khoản
api_keygặp lỗi xác thực, hệ thống probe key qua các region ứng viên (kiroApiKeyCandidateRegions, bỏ qua region đang lỗi). Region nào chấp nhận key thì lưu lại vào config + cập nhật account in-memory và retry đúng 1 lần ở region đó. Nếu không region nào nhận → giữ nguyên lỗi gốc (key chết thật). Lỗi auth luôn xảy ra trước khi stream bắt đầu nên retry không gây double-emit output.Thay đổi
config/config.go: thêmUpdateAccountRegionđể lưu region mới dò được.proxy/kiro_api.go: thêmreprobeApiKeyRegion— dò lại region + persist.proxy/kiro.go: tách vòng lặp endpoint thànhcallKiroEndpointsOnce;CallKiroAPIbọc nó bằng logic region-failover choapi_key.Kiểm thử
TestReprobeApiKeyRegion*: dò lại + persist, key chết trảfalse, bỏ qua tài khoản non-api_key.ok kiro-go/{auth,config,pool,proxy}.2. Việt hóa giao diện (locale
vi)Thêm locale Tiếng Việt đầy đủ bên cạnh Chinese (mặc định) và English, và làm logic chuyển ngôn ngữ language-agnostic thay vì chỉ zh/en cứng.
web/locales/vi.json: bản dịch Tiếng Việt hoàn chỉnh (512 keys).en.json/zh.json: thêmlang.vi, các keyuptime.d/h/m/svàmodels.familyOther(trước đây hardcode trongapp.js).index.html: thêm nút VI vào cả 2 language switcher.app.js: toggle ngôn ngữ giờ xoay vòngzh -> en -> vi;formatUptimevà nhãn model-family "Other" dùng translation key thay vì check zh/en inline.Cả 3 locale đã được xác minh chia sẻ cùng bộ 512 keys.
Test plan
go build ./...go test ./...— tất cả package passvi.jsonhợp lệ JSON, đủ 512 keys khớp vớien.json/zh.json