# Resilience Guide (Tiếng Việt) 🌐 **Languages:** 🇺🇸 [English](../../../../architecture/RESILIENCE_GUIDE.md) · 🇪🇹 [am](../../../am/docs/architecture/RESILIENCE_GUIDE.md) · 🇸🇦 [ar](../../../ar/docs/architecture/RESILIENCE_GUIDE.md) · 🇦🇿 [az](../../../az/docs/architecture/RESILIENCE_GUIDE.md) · 🇧🇬 [bg](../../../bg/docs/architecture/RESILIENCE_GUIDE.md) · 🇧🇩 [bn](../../../bn/docs/architecture/RESILIENCE_GUIDE.md) · 🇧🇦 [bs](../../../bs/docs/architecture/RESILIENCE_GUIDE.md) · 🇨🇿 [cs](../../../cs/docs/architecture/RESILIENCE_GUIDE.md) · 🇩🇰 [da](../../../da/docs/architecture/RESILIENCE_GUIDE.md) · 🇩🇪 [de](../../../de/docs/architecture/RESILIENCE_GUIDE.md) · 🇬🇷 [el](../../../el/docs/architecture/RESILIENCE_GUIDE.md) · 🇪🇸 [es](../../../es/docs/architecture/RESILIENCE_GUIDE.md) · 🇪🇪 [et](../../../et/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇷 [fa](../../../fa/docs/architecture/RESILIENCE_GUIDE.md) · 🇫🇮 [fi](../../../fi/docs/architecture/RESILIENCE_GUIDE.md) · 🇫🇷 [fr](../../../fr/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇪 [ga](../../../ga/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [gu](../../../gu/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇬 [ha](../../../ha/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇱 [he](../../../he/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [hi](../../../hi/docs/architecture/RESILIENCE_GUIDE.md) · 🇭🇷 [hr](../../../hr/docs/architecture/RESILIENCE_GUIDE.md) · 🇭🇺 [hu](../../../hu/docs/architecture/RESILIENCE_GUIDE.md) · 🇦🇲 [hy](../../../hy/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇩 [id](../../../id/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇬 [ig](../../../ig/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇹 [it](../../../it/docs/architecture/RESILIENCE_GUIDE.md) · 🇯🇵 [ja](../../../ja/docs/architecture/RESILIENCE_GUIDE.md) · 🇬🇪 [ka](../../../ka/docs/architecture/RESILIENCE_GUIDE.md) · 🇰🇭 [km](../../../km/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [kn](../../../kn/docs/architecture/RESILIENCE_GUIDE.md) · 🇰🇷 [ko](../../../ko/docs/architecture/RESILIENCE_GUIDE.md) · 🇱🇹 [lt](../../../lt/docs/architecture/RESILIENCE_GUIDE.md) · 🇱🇻 [lv](../../../lv/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [ml](../../../ml/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [mr](../../../mr/docs/architecture/RESILIENCE_GUIDE.md) · 🇲🇾 [ms](../../../ms/docs/architecture/RESILIENCE_GUIDE.md) · 🇲🇹 [mt](../../../mt/docs/architecture/RESILIENCE_GUIDE.md) · 🇲🇲 [my](../../../my/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇵 [ne](../../../ne/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇱 [nl](../../../nl/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇴 [no](../../../no/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [or](../../../or/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [pa](../../../pa/docs/architecture/RESILIENCE_GUIDE.md) · 🇵🇭 [phi](../../../phi/docs/architecture/RESILIENCE_GUIDE.md) · 🇵🇱 [pl](../../../pl/docs/architecture/RESILIENCE_GUIDE.md) · 🇵🇹 [pt](../../../pt/docs/architecture/RESILIENCE_GUIDE.md) · 🇧🇷 [pt-BR](../../../pt-BR/docs/architecture/RESILIENCE_GUIDE.md) · 🇷🇴 [ro](../../../ro/docs/architecture/RESILIENCE_GUIDE.md) · 🇷🇺 [ru](../../../ru/docs/architecture/RESILIENCE_GUIDE.md) · 🇱🇰 [si](../../../si/docs/architecture/RESILIENCE_GUIDE.md) · 🇸🇰 [sk](../../../sk/docs/architecture/RESILIENCE_GUIDE.md) · 🇸🇮 [sl](../../../sl/docs/architecture/RESILIENCE_GUIDE.md) · 🇷🇸 [sr](../../../sr/docs/architecture/RESILIENCE_GUIDE.md) · 🇸🇪 [sv](../../../sv/docs/architecture/RESILIENCE_GUIDE.md) · 🇰🇪 [sw](../../../sw/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [ta](../../../ta/docs/architecture/RESILIENCE_GUIDE.md) · 🇮🇳 [te](../../../te/docs/architecture/RESILIENCE_GUIDE.md) · 🇹🇭 [th](../../../th/docs/architecture/RESILIENCE_GUIDE.md) · 🇹🇷 [tr](../../../tr/docs/architecture/RESILIENCE_GUIDE.md) · 🇺🇦 [uk-UA](../../../uk-UA/docs/architecture/RESILIENCE_GUIDE.md) · 🇵🇰 [ur](../../../ur/docs/architecture/RESILIENCE_GUIDE.md) · 🇺🇿 [uz](../../../uz/docs/architecture/RESILIENCE_GUIDE.md) · 🇳🇬 [yo](../../../yo/docs/architecture/RESILIENCE_GUIDE.md) · 🇨🇳 [zh-CN](../../../zh-CN/docs/architecture/RESILIENCE_GUIDE.md) · 🇹🇼 [zh-TW](../../../zh-TW/docs/architecture/RESILIENCE_GUIDE.md) --- OmniRoute có ba cơ chế phục hồi riêng biệt nhưng có liên quan với nhau. Mỗi cơ chế có phạm vi và mục đích khác nhau. Hãy phân biệt rõ chúng khi gỡ lỗi hành vi định tuyến. ![Mô hình phục hồi 3 lớp](../diagrams/exported/resilience-3layers.svg) > Nguồn: [diagrams/resilience-3layers.mmd](../diagrams/resilience-3layers.mmd) ## 1. Bộ ngắt mạch nhà cung cấp **Phạm vi:** toàn bộ nhà cung cấp (ví dụ: `glm`, `openai`, `anthropic`). **Mục đích:** ngừng gửi lưu lượng đến một nhà cung cấp liên tục gặp lỗi ở cấp dịch vụ/thượng nguồn. **Triển khai:** - Lớp cốt lõi: `src/shared/utils/circuitBreaker.ts` - Kết nối: `src/sse/handlers/chatHelpers.ts`, `src/sse/handlers/chat.ts` - API trạng thái: `GET /api/monitoring/health` - API đặt lại: `POST /api/resilience/reset` - Lớp bao: `open-sse/services/accountFallback.ts` - Bảng cơ sở dữ liệu: `domain_circuit_breakers` **Trạng thái:** - `CLOSED` — cho phép lưu lượng bình thường - `DEGRADED` — vẫn cho phép lưu lượng, nhưng các lỗi gia tăng của nhà cung cấp đang được theo dõi - `OPEN` — nhà cung cấp tạm thời bị chặn; định tuyến tổ hợp sẽ bỏ qua nhà cung cấp đó - `HALF_OPEN` — thời gian chờ đặt lại đã hết; cho phép yêu cầu thăm dò **Giá trị mặc định có thể cấu hình (`open-sse/config/constants.ts`, được hiển thị trong Bảng điều khiển → Cài đặt → Khả năng phục hồi):** | Loại | Suy giảm tại | Mở tại | Thời gian chờ đặt lại | | ------- | ------------ | ------ | --------------------- | | OAuth | 5 lỗi | 8 lỗi | 60s | | API-key | 7 lỗi | 12 lỗi | 30s | | Cục bộ | được suy ra | 2 lỗi | 15s | `degradationThreshold` kiểm soát thời điểm một nhà cung cấp chuyển sang `DEGRADED`; `failureThreshold` kiểm soát thời điểm mạch mở và nhà cung cấp bị bỏ qua. Các hồ sơ nhà cung cấp cục bộ hiện chưa được hiển thị trên trang cài đặt Khả năng phục hồi. **Mã kích hoạt:** chỉ các trạng thái cấp nhà cung cấp `[408, 500, 502, 503, 504]`. KHÔNG kích hoạt đối với lỗi cấp tài khoản (phần lớn lỗi 401/403/429 — các lỗi này thuộc cơ chế tạm ngưng hoặc khóa). **Phục hồi lười:** khi trạng thái `OPEN` hết hạn, `getStatus()`, `canExecute()`, `getRetryAfterMs()` sẽ làm mới trạng thái thành `HALF_OPEN`. Không cần bộ hẹn giờ chạy nền. --- ### Cơ chế tạm ngưng nhà cung cấp toàn cục tùy chọn (cổng cửa sổ) Lớp thứ tư, **tùy chọn bật** (`PROVIDER_COOLDOWN_ENABLED`, mặc định **tắt**), duy trì bộ nhớ xuyên yêu cầu về các nhà cung cấp gặp lỗi trong `open-sse/services/providerCooldownTracker.ts`; bộ nhớ này được tham chiếu khi phân giải mục tiêu tổ hợp để các yêu cầu tổ hợp liên tiếp ngừng thử lại một nhà cung cấp vừa gặp lỗi. Các mục cấp nhà cung cấp tuân theo cổng cửa sổ `PROVIDER_PROFILES`: | Hồ sơ | kích hoạt sau (`providerFailureThreshold`) | trong khoảng (`providerFailureWindowMs`) | tạm ngưng trong (`providerCooldownMs`) | | -------- | -----------------------------------------: | ---------------------------------------: | -------------------------------------: | | OAuth | `10` | `15min` | `5min` | | Khóa API | `15` | `30min` | `10min` | Khi chưa đạt ngưỡng, nhà cung cấp **không** được coi là đang tạm ngưng; một lần thành công sẽ xóa cửa sổ. Thay vào đó, các mục cấp kết nối (`provider:connectionId`) giữ nguyên cơ chế lùi theo cấp số nhân `minRetryCooldownMs → maxRetryCooldownMs`. Các giá trị ghi đè: `OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}`. Biện pháp bảo vệ chống hồi quy: `tests/unit/provider-cooldown-window-gate.test.ts`. ## 2. Thời gian chờ kết nối **Phạm vi:** một kết nối/tài khoản/khóa riêng lẻ của nhà cung cấp. **Mục đích:** bỏ qua một khóa lỗi trong khi các kết nối khác của cùng nhà cung cấp vẫn tiếp tục phục vụ. **Triển khai:** - Đánh dấu không khả dụng: `src/sse/services/auth.ts::markAccountUnavailable()` - Lựa chọn: `getProviderCredentials*` trong cùng tệp - Tính toán thời gian chờ: `open-sse/services/accountFallback.ts::checkFallbackError()` - Cài đặt: `src/lib/resilience/settings.ts` **Các trường cho mỗi kết nối:** - `rateLimitedUntil` — dấu thời gian cho đến khi hết thời gian chờ - `testStatus: "unavailable"` - `lastError`, `lastErrorType`, `errorCode` - `backoffLevel` — bộ đếm thời gian chờ tăng theo cấp số nhân **Thời gian chờ mặc định:** - Mức cơ sở OAuth: 5 giây - Mức cơ sở khóa API: 3 giây - Khóa API gặp lỗi 429: ưu tiên `Retry-After` từ thượng nguồn/các header đặt lại/văn bản đặt lại có thể phân tích - Thời gian chờ tăng dần: `baseCooldownMs * 2 ** failureIndex` **Cơ chế chống hiệu ứng bầy đàn:** ngăn các lỗi đồng thời kéo dài thời gian chờ quá mức hoặc tăng `backoffLevel` hai lần. **Tình trạng đình trệ nội dung luồng không khiến tài khoản bước vào thời gian chờ.** Khi cơ chế giám sát đình trệ nội dung (`open-sse/utils/streamHandler.ts`) từ bỏ một luồng không gửi đầu ra mô hình trong thời gian quy định, `markAccountUnavailable()` ghi lại lỗi trên kết nối nhưng không đặt thời gian chờ: tình trạng đình trệ thuộc về yêu cầu đó, thường là một lượt suy luận dài chưa có đầu ra. Người vận hành có thể bật lại bằng `resilienceSettings.streamStallCooldown.enabled` (mặc định là `false`). **Các khung suy luận khởi động lại ngân sách đình trệ nội dung.** Một mô hình suy luận có thể suy nghĩ trong nhiều phút trước token hiển thị đầu tiên: Claude truyền phát các khung `thinking_delta` có văn bản suy nghĩ có thể trống, còn Responses API truyền phát lần lượt từng mục suy luận. `isReasoningProgressFrame()` (`open-sse/utils/streamReadiness.ts`) nhận diện các khung này và cơ chế giám sát sẽ khởi động lại ngân sách của nó cho mỗi khung thay vì hủy lượt. Chúng vẫn không phải là đầu ra của mô hình, vì vậy một lượt kết thúc chỉ với nội dung suy luận vẫn được báo cáo là trống, còn một lượt ngừng suy luận và chỉ gửi tín hiệu duy trì kết nối vẫn kích hoạt cơ chế giám sát. Các khung nhị phân `reasoningContentEvent` của Kiro có chữ ký không trống sẽ duy trì hoạt động suy luận này xuyên suốt bộ thực thi dưới dạng một delta `reasoning_content` trống. Chữ ký không được chuyển tiếp. Siêu dữ liệu, các khung không hoàn chỉnh và chữ ký trống không khởi động lại ngân sách nội dung; thời gian chờ độc lập của luồng đang hoạt động và thao tác hủy từ máy khách vẫn được áp dụng (`open-sse/executors/kiro/reasoning.ts`). **Các trạng thái kết thúc (KHÔNG phải thời gian chờ):** - `banned` — được đặt khi phát hiện từ khóa bị cấm/tài khoản bị cấm (xem [BAN_DETECTION](../security/BAN_DETECTION.md)), và khi có ba lần từ chối liên tiếp theo từng yêu cầu từ thượng nguồn (`request_rejected`, ví dụ: OAuth Anthropic trả về 403 "Request not allowed" — `open-sse/services/requestRejectedStreak.ts`); một lần từ chối đơn lẻ chỉ đưa kết nối vào thời gian chờ - `expired` (chuyển sang trạng thái kết thúc sau số lần thử lại hữu hạn — `EXPIRED_RETRY_MAX = 3` với thời gian chờ tăng theo cấp số nhân — để các lỗi OAuth tạm thời có thể tự khắc phục trước khi tài khoản bị vô hiệu hóa vĩnh viễn) - `credits_exhausted` Các trạng thái này tồn tại cho đến khi thông tin xác thực thay đổi hoặc người vận hành đặt lại chúng. Không ghi đè các trạng thái kết thúc bằng trạng thái thời gian chờ tạm thời. **Khôi phục trì hoãn:** khi `rateLimitedUntil` đã qua, kết nối sẽ lại đủ điều kiện. Sau khi sử dụng thành công, `clearAccountError()` sẽ xóa tất cả các trường lỗi. ### Giới hạn mức sử dụng OAuth Claude: luồng ưu tiên thấp hơn + đặt lại giới hạn phiên **Phạm vi:** một kết nối gói đăng ký Claude (OAuth). Cả hai tính năng đều phải được **bật riêng cho từng kết nối** (Chỉnh sửa kết nối → phần Claude → `lowPriorityMode` / `autoLimitReset` trong `providerSpecificData`, cả hai mặc định đều tắt) và mô phỏng các lệnh `/low-priority` và `/limit-reset` của Claude Code (giao thức truyền được ghi nhận từ Claude Code 2.1.263). **Triển khai:** - Máy trạng thái + phân loại phản hồi: `open-sse/services/claudeLowPriority.ts` - Máy khách trạng thái/yêu cầu đặt lại: `open-sse/services/claudeLimitReset.ts` - Hook bộ thực thi (chèn header + thử lại với cùng tài khoản): `open-sse/executors/base.ts::execute()` - Lưu trạng thái bật tính năng: `src/lib/providers/requestDefaults.ts::normalizeProviderSpecificData()` **Điều kiện kích hoạt:** giới hạn sử dụng 5 giờ — một phản hồi `429` có các header chứa `anthropic-ratelimit-unified-status: rejected` và, khi tài khoản đủ điều kiện, `anthropic-ratelimit-unified-slow-offer: treatment`. Không có gì được gửi trước phản hồi 429 do chạm giới hạn đầu tiên đó; phản hồi 429 đột biến không có các header hợp nhất sẽ đi qua luồng xử lý thời gian chờ thông thường. **Luồng ưu tiên thấp hơn** (`lowPriorityMode`): - Khi gặp ngưỡng 429, executor chấp nhận đề nghị và ngay lập tức thử lại với **cùng** tài khoản bằng `anthropic-usage-limit: slow`; lane vẫn hoạt động cho đến thời điểm `anthropic-ratelimit-unified-reset` được thông báo (+60 giây gia hạn) và mọi yêu cầu trong khoảng thời gian đó đều mang header này. Phản hồi 429 bị chặn không bao giờ đến `handleChatCore`, vì vậy kết nối **không** bị đưa vào trạng thái cooldown và không bị xoay vòng sang kết nối khác. - `anthropic-ratelimit-unified-slow-status` trên các phản hồi sau đó: `active` / `not_needed` duy trì lane; `slot_busy` (429) hoặc `529` sẽ chờ theo `anthropic-ratelimit-unified-slow-retry-after` của máy chủ (mặc định 20 giây, giới hạn 5–600 giây, jitter ±30%) rồi thử lại, bị giới hạn bởi `anthropic-ratelimit-unified-slow-max-wait` (mặc định 20 phút, giới hạn 1 phút–6 giờ) — quá thời điểm đó, lane kết thúc và thời gian tạm ngưng 10 phút sẽ ngăn việc chấp nhận lại. Thời gian chờ còn được giới hạn thêm bởi thời gian còn lại trong timeout bắt đầu upstream của chính yêu cầu (`resolveFetchStartTimeout`, mặc định 10 phút) trừ đi khoảng đệm 5 giây: nếu không có giới hạn này, thời gian chờ tối đa mặc định 20 phút sẽ kéo dài quá vòng đời yêu cầu và thao tác sleep sẽ bị hủy giữa chừng, làm xuất hiện `TimeoutError` thay vì kết thúc `max_wait` một cách êm thấm kèm thời gian tạm ngưng. - `weekly_limit` / `budget_exhausted` / `off` / `ineligible`, việc chuyển sang cửa sổ 5 giờ mới, hoặc `ineligible` + `anthropic-ratelimit-unified-overage-in-use: true` (kết thúc lane dưới dạng `extra_usage` với bất kỳ trạng thái nào, vì phần sử dụng vượt mức có trả phí giờ đã bao phủ ngưỡng) sẽ kết thúc lane; phản hồi sau đó đi vào luồng cooldown thông thường. `budget_exhausted` được ghi nhớ cho đến thời điểm đặt lại ngân sách đã thông báo (≤ 8 ngày). - Việc kiểm tra ngưỡng diễn ra sau các lần thử lại nội bộ trong cùng một lần thực thi do lỗi 400 của chính executor (chỉnh sửa ngữ cảnh, giới hạn thinking/effort, tự động học tham số), vì vậy phản hồi 429 do chạm ngưỡng chỉ xuất hiện trong một trong các lần thử lại đó vẫn bị chặn thay vì đi vào luồng cooldown. - Trạng thái được lưu trong bộ nhớ theo từng kết nối (việc khởi động lại sẽ tốn thêm một phản hồi 429 do chạm ngưỡng để chấp nhận lại). **Đặt lại giới hạn phiên** (`autoLimitReset`, được thử trước lane khi cả hai đều bật): - `GET https://api.anthropic.com/api/oauth/usage?at_wall=1&skip_spend=1` → khối `juniper_tide`; khi `arm: "reset"` và `available: true`, `POST https://api.anthropic.com/api/organizations/{orgUUID}/reset_rate_limits` với `{ "program": "juniper_tide" }` (UUID tổ chức lấy từ `providerSpecificData.organizationUUID`, dự phòng từ bootstrap). - `result: reset|not_limited` → yêu cầu được thử lại ở tốc độ tối đa (không có header slow). `already_used` / `not_offered` ghi nhớ `next_available_at` (mặc định một tuần); mọi lỗi đều backoff 15 phút. Việc đặt lại chỉ diễn ra một lần mỗi tuần và vẫn được tính vào giới hạn hằng tuần. Các biện pháp bảo vệ khỏi hồi quy: `tests/unit/claude-low-priority-mode.test.ts`, `tests/unit/claude-limit-reset.test.ts`, `tests/unit/claude-low-priority-executor.test.ts`. ### Tính liên kết phiên (#7274) **Phạm vi:** một phiên client (header `X-Session-Id` / `x-codex-session-id` / `x-omniroute-session`) được ghim vào một kết nối, áp dụng cho **mọi** provider. **Mục đích:** giữ một agent nhiều lượt (Claude Code, aider, các agent tùy chỉnh) trên cùng một tài khoản xuyên suốt các yêu cầu, giảm mất ngữ cảnh giữa các tài khoản và các lỗi 429 do khởi động nguội lặp lại trên những provider có trạng thái phiên theo từng tài khoản. **Triển khai:** - Phân giải TTL: `src/sse/services/sessionAffinityPin.ts::resolveSessionAffinityTtlMs()` - Chọn/tạo ghim: `src/sse/services/sessionAffinityPin.ts::selectSessionAffinityConnection()` - Trích xuất header (chung, mọi provider): `src/sse/services/auth.ts::extractSessionAffinityKey()` - Bảng ghim được lưu bền vững: `sessionAccountAffinity` (`src/lib/db/sessionAccountAffinity.ts`) - Cài đặt: `sessionAffinityTtlMs` (TTL toàn cục tính bằng ms, `0` để tắt) — `src/lib/db/settings.ts`. Được đổi tên từ `codexSessionAffinityTtlMs` chỉ dành cho Codex bằng migration `124_generic_session_affinity_ttl.sql`, migration này chuyển mọi TTL Codex đã cấu hình trước đó thành giá trị mặc định mới. Trước #7274, `resolveSessionAffinityTtlMs()` luôn dừng sớm và trả về `0` cho mọi provider ngoại trừ `codex`, vì vậy cài đặt TTL (và các header phiên) không có hiệu lực ở bất kỳ nơi nào khác, dù cơ chế ghim và việc trích xuất header vốn đã không phụ thuộc vào provider. Bản sửa lỗi đã loại bỏ lệnh trả về sớm đó; giờ đây TTL được áp dụng đồng nhất cho mọi provider sau khi được đặt toàn cục lớn hơn `0`. Ba header liên kết phiên không bao giờ được chuyển tiếp lên upstream — các executor tự xây dựng header upstream từ đầu thay vì chuyển tiếp header của client, vì vậy chúng chỉ đóng vai trò là ID tương quan nội bộ. ### Lease kết nối phiên được quản lý độc quyền **Phạm vi:** một HTTP client/phiên được quản lý đang hoạt động sở hữu một kết nối OmniRoute đủ điều kiện. **Mục đích:** cung cấp quyền sở hữu kết nối độc quyền, bền vững cho những client cần một hàng rào định tuyến nghiêm ngặt xuyên suốt các yêu cầu. Cơ chế này khác với tính liên kết phiên, vốn là một ưu tiên mềm nhằm duy trì tính liên tục: một lease độc quyền lưu bền vững trạng thái vòng đời trong SQLite, thực thi tính duy nhất toàn cục của chủ sở hữu đang hoạt động và kết nối đang hoạt động, đồng thời từ chối một thế hệ lỗi thời trước khi gửi đến provider. Tính năng này phải được bật riêng cho từng API key. Một key được quản lý phải có scope `lease:exclusive` và một danh sách `allowedConnections` tường minh, không rỗng. Bất kỳ HTTP client nào cũng có thể sử dụng endpoint vòng đời; không yêu cầu tên client, user-agent, provider, phương thức OAuth hoặc model. Lease sở hữu một kết nối, không phải một model, vì vậy việc thay đổi model vẫn giữ nguyên liên kết miễn là kết nối vẫn đủ điều kiện theo cách thông thường. Các quy tắc thông thường về model, quota, tình trạng, cooldown và danh sách cho phép vẫn có tính quyết định và có thể chuyển cùng một thế hệ sang một kết nối khả dụng, đủ điều kiện khác. Vòng đời sử dụng `POST /api/v1/session-leases` với các hành động JSON `acquire`, `renew` và `release`. Các yêu cầu suy luận được quản lý cung cấp giá trị bất minh `X-OmniRoute-Lease-Owner` và giá trị chính xác `X-OmniRoute-Lease-Generation`. Chủ sở hữu sử dụng tiền tố `vlo_` theo sau bởi 43 ký tự base64url; chỉ hàm băm SHA-256 của giá trị này được lưu trữ. Mỗi hàng rào điều phối cuối cùng cũng liên kết ID khóa API đã xác thực và ID kết nối đang hoạt động. Các header điều khiển lease bị loại bỏ khỏi nhật ký, ảnh chụp nhanh yêu cầu được lưu giữ và các header của trình thực thi thượng nguồn. Nếu quá trình định tuyến thông thường có các ứng viên được quản lý đủ điều kiện nhưng mọi ứng viên đang rảnh đều bị chiếm dụng bởi một lease đang hoạt động của chủ sở hữu khác, OmniRoute trả về HTTP `429`, mã lease-capacity-unavailable, trạng thái waiting-for-capacity và giá trị `Retry-After` có giới hạn được suy ra từ thời điểm hết hạn liên quan sớm nhất. Trường hợp thông thường không có ứng viên đủ điều kiện không phải là tranh chấp lease và vẫn giữ nguyên ngữ nghĩa lỗi định tuyến hiện có. Các cơ chế liên quan vẫn tách biệt: - Mức chiếm dụng phiên OAuth là cơ chế phân phối mềm cục bộ trong tiến trình dành cho các tài khoản OAuth. - Semaphore tài khoản cấp giấy phép đồng thời cho yêu cầu và kết thúc khi yêu cầu hoàn tất. - Lease phiên được quản lý độc quyền là quyền sở hữu vòng đời bền vững với hàng rào thế hệ. --- ## 3. Khóa mô hình **Phạm vi:** bộ ba nhà cung cấp + kết nối + mô hình. **Phạm vi khóa theo trạng thái:** trạng thái lỗi quyết định khóa sẽ được ghi vào khóa nào (`resolveLockoutScope()` trong `open-sse/services/accountFallback/exactModelLock.ts`): - `429` / `403` / `402` — tín hiệu về hạn ngạch hoặc quyền sử dụng — khóa **nhóm hạn ngạch**: đối với codex là toàn bộ phạm vi `codex` / `spark` (mọi mô hình `gpt-5*` của kết nối), còn đối với các nhà cung cấp khác là `getQuotaScopedModelForProvider()`. - `404` khóa trực tiếp mô hình (`getModelLockKey()` thu hẹp `not_found`). - Bất kỳ trạng thái nào khác — lỗi truyền tải/máy chủ `5xx` và mã `502` do chính OmniRoute tổng hợp từ quá trình xác thực chất lượng — chỉ khóa **chính xác** bộ ba nhà cung cấp/kết nối/mô hình. Luồng lỗi trên một mô hình không phải là bằng chứng về hạn ngạch của tài khoản; trước khi có quy tắc này, một phản hồi rỗng trên `codex/gpt-5.6-luna` sẽ loại bỏ mọi mô hình `gpt-5*` của kết nối đó khỏi hoạt động định tuyến trong 2–30 phút (tăng dần), trong khi hạn ngạch của nó không bị ảnh hưởng. - Tùy chọn `scope` được bên gọi chỉ định rõ ràng luôn được ưu tiên (Antigravity truyền `"exact"`). **Mục đích:** tránh vô hiệu hóa toàn bộ kết nối khi chỉ một mô hình không khả dụng hoặc bị giới hạn hạn ngạch. **Ví dụ:** - Các nhà cung cấp có hạn ngạch theo từng mô hình trả về 429 - Các nhà cung cấp cục bộ trả về 404 khi thiếu một mô hình - Lỗi quyền dành riêng cho chế độ/mô hình của nhà cung cấp (ví dụ: các chế độ Grok) **Triển khai:** `open-sse/services/accountFallback.ts` — `lockModel()`, `clearModelLock()`, `getAllModelLockouts()`. ### Bảng điều khiển thời gian chờ của mô hình (v3.8.0) Giao diện: Cài đặt → Thời gian chờ của mô hình (`src/app/(dashboard)/dashboard/settings/components/ModelCooldownsCard.tsx`) Liệt kê các khóa đang hoạt động với: nhà cung cấp, kết nối, mô hình, lý do, expiresAt. Người vận hành có thể bật lại mô hình theo cách thủ công từ thẻ này. **API REST:** - `GET /api/resilience/model-cooldowns` — liệt kê các khóa đang hoạt động - `DELETE /api/resilience/model-cooldowns` — bật lại theo cách thủ công. Nội dung: `{provider, connection, model}`. Xác thực: quản lý. ### Trình quản lý thời gian chờ Giao diện: Giám sát → Trình quản lý thời gian chờ (`src/app/(dashboard)/dashboard/resilience/cooldowns/`). Một trang dành cho mọi kết nối bị loại khỏi hoạt động định tuyến vì lý do tạm thời, thay vì phải mở từng trang nhà cung cấp. Trang này liệt kê thời gian chờ của kết nối, các khóa mô hình và trạng thái kết thúc; xóa chúng theo từng kết nối, theo lựa chọn hoặc cho tất cả kết nối của một nhà cung cấp; đồng thời chỉnh sửa các quy tắc thời gian chờ được tinh chỉnh nhiều nhất: `streamStallCooldown.enabled` và thời gian chờ cơ sở `connectionCooldown` cùng số bước lùi tối đa của OAuth / khóa API (được lưu thông qua `PATCH /api/resilience`). Các trạng thái kết thúc (`banned`, `expired`, `credits_exhausted`) được liệt kê nhưng không bao giờ bị xóa tại đây. **API REST** (`src/lib/resilience/cooldownManager.ts`, xác thực: quản lý): - `GET /api/resilience/cooldowns[?provider=]` — các kết nối cùng trạng thái, thời gian chờ còn lại, cấp độ lùi, loại lỗi gần nhất và các khóa mô hình (không có thông tin xác thực) - `POST /api/resilience/cooldowns` — nội dung `{connectionIds: string[]}` hoặc `{all: true, provider?}`; trả về `{cleared, unchanged, skippedTerminal, lockoutsCleared}` ### Giao diện cài đặt khóa + phục hồi bằng cơ chế suy giảm khi thành công (v3.8.23) Khóa mô hình đã chuyển từ hành vi mã hóa cứng luôn bật thành một tính năng tùy chọn có thể cấu hình đầy đủ, với thẻ cài đặt riêng và cơ chế phục hồi tự sửa chữa. **Thẻ cài đặt:** Cài đặt → Khóa mô hình (`src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx`). Thẻ này **khác biệt** với `ModelCooldownsCard` chỉ đọc ở trên (chỉ _liệt kê_ các khóa đang hoạt động) — thẻ mới _cấu hình các tham số_. Các giá trị mặc định nằm trong `DEFAULT_MODEL_LOCKOUT_SETTINGS` (`src/lib/resilience/modelLockoutSettings.ts`): | Cài đặt | Mặc định | Ý nghĩa | | ----------------------- | -------------------------------- | ------------------------------------------------------------------ | | `enabled` | `false` | Công tắc chính — khóa mô hình **tắt theo mặc định**. | | `errorCodes` | `[403, 404, 429, 502, 503, 504]` | Các trạng thái thượng nguồn được tính là lỗi theo phạm vi mô hình. | | `baseCooldownMs` | `120_000` (120 giây) | Thời lượng khóa ban đầu cho lỗi đầu tiên. | | `maxCooldownMs` | `1_800_000` (30 phút) | Giới hạn trên của thời gian chờ tăng dần. | | `maxBackoffSteps` | `10` | Số bước tăng lùi theo cấp số nhân tối đa. | | `useExponentialBackoff` | `true` | Có tăng thời gian chờ theo cấp số nhân khi lỗi lặp lại hay không. | Các cài đặt được duy trì thông qua kho cài đặt thông thường và được xác thực bằng lược đồ cài đặt khả năng phục hồi; thẻ giới hạn `baseCooldownMs`/`maxCooldownMs` (với `maxCooldownMs ≥ baseCooldownMs`) và `maxBackoffSteps`. **Phục hồi bằng cơ chế suy giảm khi thành công:** quá trình phục hồi **không** chỉ dựa vào việc bộ hẹn giờ hết hạn. Một phản hồi hợp lệ sẽ giảm dần số lần lỗi của mô hình, để mô hình đã phục hồi giữa khoảng thời gian này ngừng tăng mức phạt (và được xóa khóa) trước khi bộ hẹn giờ kết thúc. Khi một đích kết hợp thành công, `open-sse/services/combo.ts` gọi `decayModelFailureCount()` (`open-sse/services/accountFallback.ts`), hàm này **giảm một nửa** giá trị `failureCount` đã lưu (`Math.floor(failureCount / 2)`); khi giá trị đạt `0`, mục khóa sẽ bị xóa hoàn toàn. Hàm đối ứng `recordModelLockoutFailure()` tăng số đếm (và tăng thời gian chờ) khi xảy ra lỗi trong khoảng thời gian tăng mức phạt. Cơ chế suy giảm khi thành công này bổ sung cho việc bộ hẹn giờ hết hạn thông thường — cả hai cách đều có thể bật lại một mô hình. **Trạng thái:** các khóa được lưu **trong bộ nhớ** (các `Map` theo từng tiến trình chứa `ModelLockoutEntry`, sử dụng khóa `provider:connectionId:model`; các khóa theo phạm vi chính xác sử dụng `provider:connectionId:exact:model`), không được lưu bền vững vào CSDL — chúng sẽ bị mất khi khởi động lại. _Các cài đặt_ được lưu bền vững; _trạng thái_ khóa đang hoạt động chỉ là tạm thời. --- ## 4. Kiểm soát đồng thời Quota-Share (v3.8.36) Các tài khoản thuê bao (GLM, MiniMax, v.v.) thường chỉ chấp nhận khoảng 1–3 yêu cầu đồng thời; vượt quá giới hạn này sẽ gây ra lỗi 429 và thời gian chờ phục hồi. Vấn đề này đặc biệt nghiêm trọng với các tổ hợp **quota-share** (`qtSd/…`), trong đó nhiều khóa API dùng chung một tài khoản thượng nguồn. Ba lớp bảo vệ giúp ngăn tài khoản dùng chung bị quá tải. ### Giới hạn đồng thời trên mỗi kết nối (`max_concurrent`) Mỗi kết nối nhà cung cấp có thể khai báo một mức trần `max_concurrent` (`provider_connections.max_concurrent`, được đặt trong hộp thoại kết nối / API / DB). Để trống nếu không muốn giới hạn. Đây là tham số duy nhất điều khiển lớp tuần tự hóa bên dưới — hãy đặt thành mức đồng thời thực tế của tài khoản (ví dụ: GLM ~1, MiniMax ~2). ### Giới hạn đồng thời trên mỗi mô hình (`modelConcurrency`) Một kết nối cũng có thể khai báo thêm các mức trần đồng thời chính xác cho từng mô hình bên trong ánh xạ `rateLimitOverrides`: ```json { "rateLimitOverrides": { "maxConcurrent": 4, "modelConcurrency": { "glm-5": 1, "glm-4.7": 3 } } } ``` Đặt giá trị này trong hộp thoại kết nối (**Ghi đè giới hạn tốc độ → Giới hạn đồng thời trên mỗi mô hình**, mỗi dòng một mục `model=cap`) hoặc thông qua `PATCH /api/providers/[id]` với cùng cấu trúc JSON. Ngữ nghĩa của khóa: - **Toàn kết nối so với riêng từng mô hình:** `maxConcurrent` vẫn là mức trần dùng chung trên toàn kết nối. Khi cả hai cùng áp dụng, cả hai cổng đều được chiếm một cách nguyên tử trong cùng một cổng tổng hợp (`global → provider → account → model`); hành vi thực tế tuân theo giới hạn nghiêm ngặt hơn đang áp dụng. - **Khớp chính xác khóa mô hình:** khóa là chuỗi mô hình được truyền cho trình thực thi sau khi phân giải định tuyến — thông thường là id mô hình thượng nguồn thuần túy (`glm-5`), không phải bí danh `provider/model` phía máy khách (`zai/glm-5` không khớp với `glm-5`). Các giá trị là mức trần số yêu cầu đồng thời dưới dạng số nguyên dương. - **Xếp hàng cục bộ, không tự khám phá:** các yêu cầu vượt mức sẽ được xếp hàng cục bộ theo ngữ nghĩa hàng đợi/thời gian chờ hiện có (lỗi tiếp nhận có kiểu `SEMAPHORE_TIMEOUT` / `SEMAPHORE_QUEUE_FULL`). OmniRoute không khám phá hoặc suy luận chính sách thượng nguồn — nó thực thi chính xác các mức trần do người vận hành cấu hình. Một cổng mô hình đã bão hòa sẽ không bao giờ vô hiệu hóa nhà cung cấp và không bao giờ tạo ra trạng thái khóa mô hình vĩnh viễn; hành vi 429/thời gian chờ phục hồi/chuyển đổi dự phòng của thượng nguồn vẫn là cơ chế xử lý lỗi cuối cùng. - **Phạm vi trên mỗi kết nối, mỗi tiến trình:** các giới hạn áp dụng cho từng kết nối cơ sở dữ liệu và được lưu trong bộ nhớ, vì vậy hai kết nối sử dụng lại cùng một khóa API thượng nguồn sẽ không phối hợp với nhau. - **Không cấu hình nghĩa là không thay đổi:** việc bỏ qua ánh xạ (hoặc để trống trường trên bảng điều khiển) sẽ không thêm cổng mô hình nào. Ví dụ cấu hình không khẳng định bất kỳ giới hạn chung nào của nhà cung cấp: ```text glm-5=1 glm-4.7=3 ``` ### Tuần tự hóa yêu cầu quota-share Khi một lượt điều phối quota-share nhắm đến kết nối khai báo `max_concurrent` dương, các yêu cầu đồng thời đến **tài khoản** đó được tuần tự hóa thông qua một semaphore trên mỗi kết nối (khóa `qsconn:`): các yêu cầu vượt mức sẽ **chờ trong hàng đợi** thay vì làm quá tải tài khoản. Cơ chế này **mở khi lỗi** — khi hàng đợi bão hòa hoặc hết thời gian chờ, yêu cầu vẫn tiếp tục mà không cần chiếm slot, thay vì từ chối một yêu cầu có thể được điều phối. Bật/tắt tại **Cài đặt → Khả năng phục hồi → Đồng thời trên mỗi kết nối quota-share** (`resilienceSettings.quotaShareConcurrencyLimit.enabled`, mặc định bật). Nếu không có giới hạn `max_concurrent`, hành vi không thay đổi. > Bản thân cổng định tuyến quota-share (`selectQuotaShareTarget`, DRR + P2C) > cũng mở khi lỗi và chỉ _hạ mức ưu tiên_ của một kết nối đã đạt giới hạn — với > một nhóm chỉ có một kết nối, nó không thể áp đặt giới hạn cứng, vì vậy semaphore > này mới là cơ chế thực sự kiểm soát lượng yêu cầu tràn vào. ### Thử lại có nhận biết thời gian chờ phục hồi của tổ hợp Đối với mọi chiến lược tổ hợp (khi được bật), một yêu cầu có thể dẫn đến lỗi 429 do thời gian chờ phục hồi tạm thời NGẮN sẽ chờ hết khoảng thời gian đó rồi điều phối lại thay vì trả về lỗi 429 — điều này bao phủ các cửa sổ TPM/RPM kiểu Gemini (khoảng 60 giây theo `retry-after`) trên các tổ hợp nhiều mô hình, ví dụ cả hai đích của một tổ hợp 2 mô hình đều chạm giới hạn tốc độ trên mỗi mô hình. Hành vi này được giới hạn bởi `comboCooldownWait` (`enabled`, `maxWaitMs`, `maxAttempts`, `budgetMs`) trong **Cài đặt → Khả năng phục hồi**. Hệ thống không bao giờ chờ đối với `quota_exhausted` (bị khóa đến nửa đêm) hoặc các nguyên nhân xác thực/không tìm thấy. Cùng cổng này cũng xử lý trường hợp trước khi điều phối (#16319): khi mọi đích của một tổ hợp đều bị bỏ qua trước khi có bất kỳ lần thử nào vì mô hình của chúng đang trong trạng thái khóa tạm thời ngắn, luồng không có lần thử nào sẽ chờ hết khóa theo cùng các mức trần rồi điều phối lại, thay vì trả về `503 ALL_TARGETS_SKIPPED` không kèm gợi ý. Khi không được phép chờ (đã chạm mức trần, hết số lần thử hoặc ngân sách, hay tính năng bị tắt), phản hồi cuối cùng được nâng thành `429 rate_limit_error`, mang theo `retry_after` / `reset_at` từ thời điểm hết hạn khóa tạm thời sớm nhất; các khóa do hạn ngạch, xác thực và không tìm thấy vẫn giữ phản hồi 503 không kèm gợi ý. ```text #16319 · comboCooldownWait · pre-dispatch (attempts = 0) all targets skipped + transient model lockout: enabled ∧ 0 < waitMs ≤ maxWaitMs ∧ attempt < maxAttempts ∧ budgetLeftMs ≥ waitMs → wait → re-dispatch wait = false → HTTP 429 rate_limit_error retry_after / reset_at ← earliest transient lock expiry quota_exhausted / auth_error / not_found / not_found_local: no transient hint → HTTP 503 ALL_TARGETS_SKIPPED ``` --- ## 5. Kiểm soát tiếp nhận hàng đợi yêu cầu (v3.8.49 · issue #6593) **Phạm vi**: hàng đợi giới hạn tốc độ cục bộ theo từng provider+connection (`open-sse/services/rateLimitManager.ts`, được hỗ trợ bởi Bottleneck), nằm dưới ba cơ chế ở trên một lớp. **`maxWaitMs` giới hạn thời gian chờ trong hàng đợi; `executionMaxWaitMs` giới hạn thời gian thực thi.** Hai giới hạn này được chủ ý tách biệt và không giới hạn nào tác động đến giới hạn còn lại. `resilienceSettings.requestQueue.maxWaitMs` là **ngân sách thời gian chờ trong hàng đợi**: bao gồm thời gian chờ một slot của provider rồi ở trạng thái QUEUED, và bộ hẹn giờ của nó được xóa ngay khi tác vụ rời khỏi QUEUED và bắt đầu thực thi (`rateLimitManager.ts`, `wrappedFn`). Yêu cầu vượt quá giới hạn này sẽ không bao giờ đến được upstream. Mặc định là 30000ms, được cung cấp bởi `DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS` trong `src/lib/resilience/settings.ts` và được cố định bằng `tests/unit/ratelimit-admission-control-6593.test.ts`, vì vậy thay đổi giá trị này sẽ khiến kiểm thử đó báo đỏ thay vì để đoạn văn này âm thầm trở nên lỗi thời. `resilienceSettings.requestQueue.executionMaxWaitMs` là giá trị Bottleneck nhận làm `expiration` của tác vụ, với bộ hẹn giờ chỉ bắt đầu sau khi tác vụ được điều phối. Đây là cơ chế dự phòng cho các executor không có timeout upstream riêng, và giá trị này được tăng lên bằng timeout khi bắt đầu fetch của chính executor nếu timeout đó dài hơn, để nó không thể ngắt một phản hồi đang truyền bình thường. Mặc định là 600000ms (10 phút). Việc đưa ngân sách hàng đợi vào `expiration` trước đây đã khiến các gateway không tăng dần bị ngắt giữa chừng — chúng có thể chạy hợp lệ trong nhiều phút trước khi nhận được byte đầu tiên — và đó là lý do lỗi hết hạn được biểu thị dưới dạng `code: "RATE_LIMIT_EXECUTION_TIMEOUT"` (HTTP 504), trong khi ngân sách hàng đợi mang mã timeout hàng đợi. Ghi đè một trong hai qua `RATE_LIMIT_MAX_WAIT_MS` / `RATE_LIMIT_EXECUTION_MAX_WAIT_MS` (env) hoặc dashboard (**Settings → Resilience**). Cả hai đều được giới hạn trong khoảng 1ms–24h khi chuẩn hóa. **Thứ tự ưu tiên, áp dụng cho cả hai:** biến env chỉ cung cấp giá trị _mặc định_. Giá trị được lưu bền vững trong `resilienceSettings.requestQueue` (dashboard / bản vá API, được lưu trong `key_value`) sẽ được ưu tiên hơn, và giá trị `rateLimitOverrides.maxWaitMs` / `.executionMaxWaitMs` theo từng connection sẽ được ưu tiên hơn nữa. Do đó, đặt biến env trên một deployment đã có giá trị được lưu bền vững sẽ không thay đổi gì — thay vào đó, hãy xóa hoặc cập nhật cài đặt được lưu bền vững. Thời gian nằm trong hàng đợi được giới hạn bởi `maxWaitMs`; `maxQueueDepth` bên dưới giới hạn số lượng caller có thể được xếp hàng cùng lúc. **`maxQueueDepth` — giới hạn tiếp nhận tùy chọn (mới).** `resilienceSettings.requestQueue.maxQueueDepth` giới hạn số lượng yêu cầu có thể nằm trong hàng đợi (chưa được điều phối) cho một provider+connection tại cùng một thời điểm. Khi hàng đợi đã chứa `maxQueueDepth` yêu cầu, một yêu cầu mới sẽ bị từ chối nhanh bằng lỗi có kiểu `code: "RATE_LIMIT_QUEUE_FULL"` **trước khi** nó đến được `limiter.schedule()` — vì vậy việc từ chối có chi phí thấp và xảy ra trước mọi công việc nén prompt / dịch thuật downstream dành cho yêu cầu đó. Mặc định `0` = tắt, duy trì hành vi hàng đợi không giới hạn hiện có; được giới hạn trong khoảng 0–100000. Ghi đè qua `RATE_LIMIT_MAX_QUEUE_DEPTH` (env) hoặc `resilienceSettings.requestQueue.maxQueueDepth` (dashboard/bản vá API). Bản thân bước kiểm tra tiếp nhận là một hàm thuần túy (`open-sse/services/rateLimitManager/admission.ts::checkQueueAdmission`) nên có thể được kiểm thử đơn vị mà không cần limiter Bottleneck thực. > RFC khởi tạo #6593 cũng đề xuất một cờ `bypassCompressionOnRateLimit`. > Pipeline `open-sse/services/compression/` của repo này thực hiện > nén prompt/ngữ cảnh trên yêu cầu LLM outbound (`chatCore.ts`, > quanh khối `resolveCompressionSettings`/`selectCompressionStrategy`), > chứ không phải nén phản hồi HTTP trên các phần thân 429 được tạo tổng hợp — không có > đường dẫn mã tương ứng cho một cờ bỏ qua theo nghĩa đen. Bước nén prompt đó > hiện cũng chạy _trước_ `withRateLimit()` trong pipeline yêu cầu, vì vậy > việc sắp xếp lại để bỏ qua bước này khi hàng đợi đầy là một thay đổi riêng biệt và lớn hơn > so với phạm vi của issue này; thay đổi đó đã được chủ ý **không** triển khai > ở đây và được để lại cho phần tiếp theo nếu lợi ích tiết kiệm CPU xứng đáng với > rủi ro từ việc sắp xếp lại. --- ## 6. Cơ chế giám sát thông lượng luồng chậm (#9709) Cơ chế bảo vệ tùy chọn `resilienceSettings.streamRecovery.throughputWatchdog` phát hiện một upstream vẫn đang gửi các chunk nhưng tạo ra đầu ra của trợ lý thấp hơn tốc độ đầu ra hữu ích đã cấu hình. Cơ chế này được chủ ý tách biệt với thời gian chờ khi không hoạt động: heartbeat và metadata không đặt lại bộ đếm thời gian nào và cũng không được tính là tiến triển. Cơ chế này cũng tách biệt với hạn chót cứng của lần thử (#9153), vốn vẫn là giới hạn an toàn tuyệt đối bất kể chất lượng đầu ra. Cơ chế giám sát yêu cầu một khoảng thời gian khởi động, sau đó là một cửa sổ cuộn hoàn chỉnh trước khi có thể hủy. Nó đếm các delta văn bản từ các sự kiện đầu ra của Chat Completions và Responses API (một phép xấp xỉ thận trọng theo byte UTF-8), bỏ qua các sự kiện chỉ chứa usage và các sự kiện rỗng, đồng thời tạm dừng đánh giá trong khi các sự kiện gọi công cụ hoặc suy luận đang diễn ra. Cơ chế này bị vô hiệu hóa theo mặc định và có thể được bật bằng `STREAM_THROUGHPUT_WATCHDOG_ENABLED=true`; cửa sổ, thời gian khởi động, tốc độ tối thiểu và đầu ra tối thiểu có thể đo được đều bị giới hạn bởi lớp chuẩn hóa thông thường của resilience settings. Khi được bật, thao tác hủy của cơ chế giám sát chỉ được áp dụng cho lần thử upstream đang hoạt động. Trước khi có bất kỳ byte nào hiển thị với client, đường dẫn phục hồi sớm hiện có trong cùng tài khoản có thể mở lại lần thử. Sau khi commit, luồng không bao giờ được phát lại một cách mù quáng; chỉ hợp đồng tiếp tục giữa luồng an toàn hiện có mới có thể ghép nối một hậu tố. Quá trình hoàn tất vẫn chỉ diễn ra một lần, vì vậy việc hạch toán usage và giải phóng semaphore không bị lặp lại. --- ## 7. Điều chỉnh lại trạng thái upstream (lỗi hạn ngạch bị báo sai) **Phạm vi:** một gateway upstream báo cáo tình trạng tạm thời cạn hạn ngạch bằng trạng thái HTTP không chính xác. **Mục đích:** sửa trạng thái gây hiểu nhầm TRƯỚC KHI phân loại, để các thành phần tiêu thụ phía downstream (fallback engine, quá trình tổng hợp combo, phản hồi gửi tới client) nhận biết đúng bản chất có thể thử lại của lỗi. Một số gateway báo hiệu tình trạng TẠM THỜI cạn hạn ngạch bằng một trạng thái HTTP không thể thử lại. `agentrouter.org` trả về `403` (đôi khi là `400`) với nội dung tiếng Trung (`用户额度不足` / `额度不足`) thay vì `429` tiêu chuẩn. Các client như Claude Code coi `403` là lỗi vĩnh viễn và hủy phiên; nếu không được sửa, fallback engine sẽ phân loại lỗi này là `AUTH_ERROR` thay vì một sự kiện hạn ngạch. **Triển khai:** - Registry + matcher: `open-sse/config/upstreamStatusRestatement.ts` — một danh sách quy tắc theo từng provider (`{id, fromStatuses, toStatus, textMarkers, excludeMarkers, defaultRetryAfterMs}`), được đối sánh thông qua `applyStatusRestatement()`. - Vị trí gọi: khối `providerFailure:` trong `open-sse/handlers/chatCore.ts` (khoảng dòng 3654), ngay sau khi `parseUpstreamError()` phân tích một phản hồi upstream có trạng thái HTTP lỗi (`!providerResponse.ok`) và trước khi bất kỳ quá trình phân loại nào chạy, để mọi thành phần tiêu thụ downstream đều nhận được trạng thái đã sửa. Các lỗi được nhúng bên trong luồng SSE `200` đi theo một đường dẫn phân tích luồng riêng biệt và muộn hơn, nên **không** được hook này xử lý ở thời điểm hiện tại — đây là một hạn chế đã biết nhưng chưa cần thiết đối với trạng thái sai của agentrouter (vốn xuất hiện dưới dạng trạng thái HTTP lỗi). - Điều kiện đủ để thử lại: `429` nằm trong `RETRY_AFTER_ELIGIBLE_STATUSES` (`open-sse/services/combo/unavailableRetryGate.ts`), vì vậy một lỗi đã được điều chỉnh trạng thái sẽ mang theo cửa sổ thử lại thực sự thay vì xuất hiện dưới dạng một lỗi `403` không thể tiếp tục. - Giá trị `60s` tổng hợp của `defaultRetryAfterMs` (`upstreamStatusRestatement.ts`) chỉ là thông tin mà phản hồi đã điều chỉnh trạng thái cho **client** biết; bản thân nó không phải thời lượng cooldown/lockout nội bộ của kết nối — thời lượng đó được chi phối riêng bởi cơ chế thực sự xử lý lỗi đã điều chỉnh trạng thái (backoff tăng dần của Connection Cooldown, §2, với giá trị cơ sở `3s` dành cho các provider dùng API key; hoặc Model Lockout, §3, dành cho các provider áp dụng hạn ngạch theo model như agentrouter). Router có thể đủ điều kiện thử lại nội bộ sớm hơn cửa sổ 60s mà nó thông báo cho client — đây là khoảng đệm có chủ đích, không phải lỗi. Các lỗi vĩnh viễn (`无权访问模型` của agentrouter — không có quyền truy cập model này) KHÔNG BAO GIỜ được điều chỉnh trạng thái: `excludeMarkers` phủ quyết quy tắc ngay cả khi `textMarkers` khớp, do đó lỗi vẫn giữ nguyên trạng thái ban đầu và không có cơ chế nào thử lại lỗi đó vô thời hạn. Quy tắc phân loại provider tương ứng (`agentrouter-model-access-denied` trong `open-sse/config/providerErrorRules.ts`: `reason: "auth_error"`, `scope: "model"`, cooldown cơ sở được khai báo là `6h`) được `checkFallbackError` (`open-sse/services/accountFallback.ts`) tham chiếu _trước_ nhánh trả về sớm `FORBIDDEN` chung cho danh mục apikey, được kiểm soát bởi `honorsRuleLockScope(provider)` (#10334 — hiện chỉ dành riêng cho agentrouter thông qua allowlist `HONORS_RULE_LOCK_SCOPE_PROVIDERS` trong `providerErrorRules.ts`). Cooldown 6h được khai báo trong quy tắc được truyền qua dưới dạng `fallbackResult.baseCooldownMs`, nhưng vẫn đi vào đường dẫn lockout hạn ngạch theo model có sẵn (`lockModelIfPerModelQuota()` / `recordModelLockoutFailure()`, không thay đổi bởi #10334 ngoại trừ nguồn cooldown): nó bị giới hạn xuống mức `mlSettings.maxCooldownMs` của operator (mặc định `1_800_000ms` / 30min), giống như mọi lockout model khác, và _lý do lockout được lưu bền vững_ vẫn là giá trị `"forbidden"` được hardcode từ trước, không phải `"auth_error"` của quy tắc — chỉ thời lượng cooldown được tôn trọng xuyên suốt toàn bộ quy trình, không phải chuỗi lý do. Bản thân kết nối vẫn hoạt động; các model ngang hàng trên cùng kết nối không bị ảnh hưởng. Các lỗi hạn mức được diễn giải lại (`额度不足`) khớp với một quy tắc nhà cung cấp trong môi trường production (`agentrouter-user-quota-exhausted`: `reason: "quota_exhausted"`, `scope: "connection"`, không tự khai báo thời gian chờ — áp dụng giá trị mặc định scaled backoff của tầng lưu trữ). Kể từ #10334, `scope` trên `ProviderErrorRuleMatch` ĐƯỢC sử dụng xuyên suốt toàn bộ luồng, nhưng **chỉ** đối với các nhà cung cấp nằm trong allowlist `HONORS_RULE_LOCK_SCOPE_PROVIDERS` (`providerErrorRules.ts` — hiện chỉ có `"agentrouter"`, được kiểm soát qua `honorsRuleLockScope()`). Đối với mọi nhà cung cấp khác, `scope` vẫn chỉ mang tính thông tin, hoàn toàn giống như trước #10334. `checkFallbackError` cung cấp scope của quy tắc khớp dưới dạng `fallbackResult.ruleScope`; `isAgentrouterConnectionQuotaScope()` (`src/sse/services/auth.ts`) là guard dùng chung để xác nhận rằng một `ruleScope` thực sự an toàn khi được coi là tín hiệu có phạm vi toàn bộ kết nối và có khả năng tự phục hồi (scope `"connection"`, reason `quota_exhausted`, không bao giờ là `permanent`, không bao giờ là `creditsExhausted` — một biện pháp phòng vệ trước khả năng trong tương lai có quy tắc ghép scope `"connection"` với trạng thái tài khoản vĩnh viễn). Hai consumer gọi guard này: - **Tầng lưu trữ** (`markAccountUnavailable()`, `src/sse/services/auth.ts`): thay vì rơi vào nhánh khóa **theo từng model** của nhà cung cấp passthrough (agentrouter có `passthroughModels: true` → `hasPerModelQuota()` trả về `true`), nó áp dụng **thời gian chờ kết nối tạm thời** — `testStatus: "unavailable"` + `rateLimitedUntil`, không bao giờ là trạng thái kết thúc (`credits_exhausted`/`banned`/`expired`) — để kết nối tự phục hồi sau khi thời gian chờ kết thúc, thay vì yêu cầu đặt lại thông tin xác thực theo cách thủ công. Bỏ qua đối với các kết nối có `disableCooling: true` (#2997): lựa chọn không tham gia này sẽ rơi xuống nhánh khóa theo từng model thay thế (một sự đánh đổi đã được ghi lại — xem chú thích mã phía trên nhánh). - **Định tuyến combo trong cùng yêu cầu** (`applyComboTargetExhaustion()`, `open-sse/services/combo/targetExhaustion.ts`): cùng guard đó đánh dấu kết nối vào tập hợp `exhaustedConnections` trong bộ nhớ, với khóa `${provider}:${connectionId}`. Cơ chế này chỉ bỏ qua một target CÙNG-YÊU-CẦU còn lại nếu _chính target đó đã mang đúng `connectionId` ấy_ trên đối tượng target của nó (`getExhaustedTargetSkipReason()`, `open-sse/services/combo/comboPredicates.ts`, `if (provider && connectionId)` trước khi tra cứu `exhaustedConnections`) — một combo danh sách model thông thường, trong đó các target ngang hàng không mang `connectionId` được ghim riêng và chỉ được phân giải theo từng lần điều phối từ header `X-OmniRoute-Selected-Connection-Id` của phản hồi, sẽ không bao giờ khớp khóa đó. Trong trường hợp phổ biến này, cơ chế bảo vệ thực sự ngăn một nhánh còn lại tái sử dụng tài khoản vừa cạn hạn mức KHÔNG phải là Set này — mà là tầng lưu trữ nêu trên (`rateLimitedUntil` của kết nối hiện nằm trong tương lai) kết hợp với việc cùng guard này ngăn thêm `transientRateLimitedProviders` cho lỗi đó (xem "Thiết kế hai giai đoạn" và chú thích mã trên nhánh `isAgentrouterConnectionQuotaScope` trong `targetExhaustion.ts`): do Set đó không được đánh dấu, cơ chế force-allow `allowRateLimitedConnection` của `combo.ts` (`open-sse/services/combo.ts:1005-1013`, `:2734-2738`) KHÔNG được kích hoạt cho các nhánh còn lại của nhà cung cấp, vì vậy bộ lọc `rateLimitedUntil` của quá trình chọn thông tin xác thực (`src/sse/services/auth.ts:1238`) vẫn được tuân thủ bình thường và một nhánh còn lại sẽ chọn một kết nối agentrouter khác vẫn đủ điều kiện hoặc thất bại do không có thông tin xác thực khả dụng — nó không tự ép quay lại kết nối mà nhánh này vừa đặt vào thời gian chờ. ### Thiết kế hai giai đoạn: diễn giải lại trạng thái, sau đó phân loại Cơ chế diễn giải lại trạng thái (`upstreamStatusRestatement.ts`) và các quy tắc phân loại nhà cung cấp (`open-sse/config/providerErrorRules.ts`, `providerRuleRegistry`) là hai registry riêng biệt, cả hai đều sử dụng id nhà cung cấp và các dấu hiệu văn bản làm khóa, nhưng chúng chạy ở những vị trí khác nhau và phục vụ các mục đích khác nhau: cơ chế diễn giải lại sẽ ghi lại trạng thái HTTP từ sớm trong `chatCore.ts`; các quy tắc phân loại chọn `reason` fallback và `scope` khóa (`model` / `provider` / `connection`) bên trong `checkFallbackError()` (`open-sse/services/accountFallback.ts`). Các quy tắc phân loại chỉ thấy toàn bộ **văn bản** lỗi (cần thiết để khớp các dấu hiệu trong body như `额度不足`) đối với các nhà cung cấp được liệt kê trong allowlist `FULL_TEXT_RULE_PROVIDERS` trong `providerErrorRules.ts` — hiện chỉ có `"agentrouter"`. Đối với mọi nhà cung cấp **trong catalog tích hợp sẵn** khác, `checkFallbackError` chỉ chuyển lỗi có cấu trúc (`{code, type}`) cho `getProviderErrorRuleMatch`; dữ liệu này đủ cho các quy tắc dựa trên header/trạng thái/mã lỗi nhưng không thể thấy các dấu hiệu trong văn bản body. Helper `resolveRuleMatchBody()` thực hiện việc lựa chọn này: toàn bộ văn bản lỗi cho các nhà cung cấp trong allowlist, còn lỗi có cấu trúc cho các nhà cung cấp khác. Việc thêm một nhà cung cấp **tích hợp sẵn** vào `FULL_TEXT_RULE_PROVIDERS` là lựa chọn tham gia rõ ràng theo từng nhà cung cấp — cơ chế này tồn tại để đường dẫn mặc định cho mọi nhà cung cấp không có trong danh sách vẫn giữ nguyên từng byte. `scope` của một quy tắc (`model` / `provider` / `connection`) là một lựa chọn tham gia riêng biệt với `FULL_TEXT_RULE_PROVIDERS`: `checkFallbackError` chỉ cung cấp nó dưới dạng `fallbackResult.ruleScope`, và các consumer phía sau chỉ tôn trọng nó như một giá trị không chỉ mang tính thông tin đối với các nhà cung cấp nằm trong allowlist `HONORS_RULE_LOCK_SCOPE_PROVIDERS` trong cùng file (`được kiểm soát qua honorsRuleLockScope()` — hiện chỉ có `"agentrouter"`). Xem phần "Các lỗi hạn mức được diễn giải lại" ở trên để biết một kết quả khớp `scope: "connection"` thực sự làm gì sau khi một nhà cung cấp được đưa vào allowlist đó. **#11104 — các quy tắc do operator khai báo bỏ qua cả hai allowlist.** Một operator có thể khai báo quy tắc riêng cho từng provider trong thời gian chạy thông qua `settings.providerErrorRules` (`open-sse/config/providerErrorRules.ts::setOperatorProviderErrorRules`) mà không cần chỉnh sửa tệp này. Việc đặt một quy tắc của operator phía sau `FULL_TEXT_RULE_PROVIDERS`/`HONORS_RULE_LOCK_SCOPE_PROVIDERS` — các allowlist nhằm bảo vệ hành vi **mặc định** của các quy tắc catalog tích hợp — sẽ khiến cơ chế settings không có tác dụng đối với mọi provider ngoại trừ những provider đã được liệt kê ở đó, vì việc khai báo quy tắc vốn đã là hành động opt-in rõ ràng của operator. `resolveRuleMatchBody()` và `honorsRuleLockScope()` đều kiểm tra `hasOperatorRuleForProvider()` trước: một provider có quy tắc của operator sẽ nhận văn bản lỗi thô và `scope` đã khai báo của quy tắc đó được tôn trọng, bất kể provider đó có xuất hiện trong một trong hai allowlist hay không. **Khoảng trống đã biết — `providerRuleRegistry` không bao giờ được tham vấn đối với HTTP 400.** Nhánh `BAD_REQUEST` của `checkFallbackError` phân loại hoàn toàn trạng thái 400 thông qua các mảng pattern riêng (`MODEL_ACCESS_DENIED_PATTERNS`, `CONTEXT_OVERFLOW_PATTERNS`, v.v. trong `accountFallback.ts`) và trả về trước khi đi tới nhánh `configuredRule`/`getProviderErrorRuleMatch` ở phía trên. Một quy tắc catalog tích hợp (hoặc quy tắc của operator) có `status: 400` hợp lệ về mặt cú pháp nhưng sẽ không bao giờ được kích hoạt. Hiện không có quy tắc nào nhắm tới 400, vì vậy không có gì trong môi trường production bị ảnh hưởng — nhưng một quy tắc 400 trong tương lai cần sửa nhánh này trước, đây là thay đổi lớn hơn việc thêm một quy tắc (nó phân loại lại 400 cho mọi provider đang phụ thuộc vào hành vi của mảng pattern) và nằm ngoài phạm vi của việc bổ sung quy tắc cho một provider duy nhất. ### Thêm một gateway khai báo sai quota mới 1. Đăng ký một mảng quy tắc trong `statusRestatementRegistry` (`open-sse/config/upstreamStatusRestatement.ts`). Giữ `textMarkers` dành riêng cho provider; tuyệt đối không tái sử dụng các cụm từ tiếng Anh chung có thể xung đột với `CREDITS_EXHAUSTED_SIGNALS` (`open-sse/services/accountFallback.ts`). 2. Có thể tùy chọn đăng ký các quy tắc phân loại trong `open-sse/config/providerErrorRules.ts` (`providerRuleRegistry`) để chọn đúng phạm vi khóa (`connection` cho quota trên toàn tài khoản, `model` cho lỗi theo từng model). Bước này chỉ có hiệu lực trong môi trường production đối với các provider có quy tắc cần toàn bộ văn bản lỗi (các marker trong body): hãy thêm id của provider vào `FULL_TEXT_RULE_PROVIDERS` trong cùng tệp — nếu không, `checkFallbackError` sẽ chỉ chuyển cho quy tắc lỗi có cấu trúc `{code, type}` và một quy tắc dựa trên văn bản body sẽ không bao giờ khớp với lưu lượng thực tế. Các quy tắc chỉ khớp dựa trên `status`/`headers` (như của Opencode hoặc Minimax) không cần opt-in này. Riêng biệt, nếu quy tắc khai báo `scope: "connection"` và mục đích là tạo cooldown thực sự trên toàn connection cùng với việc bỏ qua combo trong cùng request (không chỉ là một nhãn cung cấp thông tin), hãy thêm id của provider vào `HONORS_RULE_LOCK_SCOPE_PROVIDERS` trong cùng tệp — đây là cơ chế kiểm soát việc sử dụng theo kiểu `isAgentrouterConnectionQuotaScope()` trong `markAccountUnavailable()` (`src/sse/services/auth.ts`) và `applyComboTargetExhaustion()` (`open-sse/services/combo/targetExhaustion.ts`); nếu không có nó, `scope` vẫn được truyền qua `fallbackResult.ruleScope` nhưng không có gì xử lý giá trị đó. 3. Thêm các unit test tương tự `tests/unit/upstream-status-restatement.test.ts` và `tests/unit/agentrouter-error-rules.test.ts` (bao gồm các guard not-permanent / not-creditsExhausted và — nếu provider cần allowlist — một test xác nhận `resolveRuleMatchBody()` chỉ trả về toàn bộ văn bản cho provider đó). Không cần thay đổi `chatCore.ts`, `classifyError` hoặc combo. #### Khóa được phân nhóm theo egress (#10880) Các provider trong `EGRESS_BUCKETED_LOCK_PROVIDERS` (họ opencode) được xem là upstream được phân nhóm theo IP (gói miễn phí của opencode được phân nhóm theo IP, không phải theo tài khoản — xem #9611): trạng thái 429 được phân loại là `quota_exhausted` **hoặc** `rate_limit_exceeded` sẽ áp dụng cooldown cho mọi connection thuộc họ được allowlist có địa chỉ IP egress được biết gần nhất trùng với địa chỉ IP của connection gặp lỗi, trước khi quá trình xoay vòng có thể thử chúng — tránh N-1 lệnh gọi upstream chắc chắn thất bại (cùng mô hình với #10460/#10525). `rate_limit_exceeded` được đưa vào có chủ đích: trên đường dẫn `markAccountUnavailable`, các quy tắc dành riêng cho opencode không bao giờ khớp (không có headers/body nào được chuyển cho `checkFallbackError`, opencode không nằm trong `FULL_TEXT_RULE_PROVIDERS`), vì vậy một lỗi 429 có body chứa văn bản quota đăng ký ("monthly usage limit reached") được phân loại là `quota_exhausted` bởi fallback dựa trên văn bản quota (`buildSubscriptionQuotaFallback`, `accountFallback.ts`; cooldown 1h) trước khi quy tắc `status_429` có cơ hội được xử lý — trong khi lỗi 429 không có văn bản quota (chỉ là giới hạn tốc độ thông thường) được phân loại thông qua quy tắc `status_429` thành `rate_limit_exceeded` và vẫn áp dụng cooldown cho toàn bộ họ IP. Đối với một provider trong allowlist, giới hạn tốc độ được phân nhóm theo IP là tín hiệu tương đương với quota đã cạn. Các giới hạn thực tế: - **Nỗ lực tối đa**: khóa xác định `egress_ip` được biết gần nhất của kết nối từ `proxy_logs` (cửa sổ 24 giờ, đồng bộ, không có bộ nhớ đệm). Bộ nhớ đệm lạnh (IP egress chưa từng được thăm dò) hoặc không có hàng dữ liệu → kết nối bị lỗi vẫn được nhánh này đưa vào thời gian chờ (được ghi nhận như hiện tại), chỉ là không có kết nối cùng nhóm nào bị khóa. - **Không bao giờ là trạng thái cuối**: thời gian chờ là một cửa sổ hạn ngạch được gia hạn (`testStatus: "unavailable"`); trạng thái vĩnh viễn không bao giờ được suy ra từ một tín hiệu cấp IP. Các kết nối `disableCooling` hoàn toàn bỏ qua nhánh này. - **Độ chi tiết của khóa thay đổi đối với nhóm trong danh sách cho phép**: đây là thay đổi về phạm vi, không chỉ là một tối ưu hóa cho các kết nối cùng nhóm. opencode là một nhà cung cấp `passthroughModels`, vì vậy trước nhánh này, lỗi 429 tạo ra khóa theo từng MODEL; giờ đây nó tạo ra thời gian chờ cho kết nối — kể cả khi người vận hành chỉ chạy một kết nối duy nhất mà hoàn toàn không có kết nối cùng nhóm. Đây là độ chi tiết mà bảng quy tắc opencode đã xác định là chính xác (`scope: "connection"`, `providerErrorRules.ts`), nhưng cho đến nay chưa bao giờ được tuân thủ vì opencode không nằm trong `HONORS_RULE_LOCK_SCOPE_PROVIDERS`. Nhánh này tự ghi thời gian chờ + `backoffLevel` của kết nối bị lỗi, tương tự nhánh agentrouter có phạm vi kết nối, rồi trả về — khối theo từng model và luồng xử lý chung bên dưới sẽ không bao giờ được thực thi. - **Bao gồm combo**: giống như nhánh agentrouter, phạm vi này chủ ý bỏ qua việc hạ cấp `persistUnavailableState`/`isCombo` mà một bên gọi combo áp dụng cho lỗi 429. Khóa theo từng model không phải là một dạng yếu hơn của phạm vi này, mà là sai đơn vị: nó không cho biết gì về IP đã cạn hạn ngạch, vì vậy quá trình xoay vòng combo sẽ tiếp tục lãng phí một lệnh gọi chắc chắn thất bại cho mỗi kết nối cùng nhóm. - **An toàn cho kết nối cùng nhóm**: một kết nối cùng nhóm đã ở trạng thái cuối (banned/credits_exhausted) hoặc đã có thời gian chờ dài hơn sẽ không bao giờ bị ghi đè. - **Danh sách cho phép độc quyền**: việc mở rộng `EGRESS_BUCKETED_LOCK_PROVIDERS` là một quyết định rõ ràng của chủ sở hữu; không có cơ chế đấu nối chung (mẫu #10334/#10419). Truy vấn kết nối cùng nhóm liên kết với chính danh sách cho phép đó thay vì lặp lại dưới dạng một chuỗi SQL cố định, do đó việc mở rộng danh sách vẫn chỉ cần thay đổi một dòng. - **Xoay vòng IP egress theo cả hai hướng**: cửa sổ tra cứu (24 giờ) rộng hơn rất nhiều so với TTL của bộ nhớ đệm IP egress (5 phút), vì vậy "IP được biết gần nhất" là dữ liệu lịch sử, không phải trạng thái hiện tại. Nếu proxy của một kết nối đã xoay vòng trong cửa sổ này, khóa có thể **bỏ sót** một IP thực sự được dùng chung (IP được ghi nhận là IP mới, chưa cạn hạn ngạch) — và ngược lại, nó có thể **đưa một kết nối cùng nhóm đã chuyển khỏi IP đã cạn hạn ngạch vào thời gian chờ**. Trường hợp thứ hai khiến kết nối cùng nhóm đó chịu thêm một cửa sổ thời gian chờ; cả hai đều được chấp nhận như những giới hạn của cơ chế nỗ lực tối đa dựa trên dữ liệu lịch sử. - **Chi phí**: hai lượt quét có giới hạn trên `proxy_logs` (được lọc theo cửa sổ thông qua `idx_pl_timestamp`), chỉ xảy ra với tần suất của lỗi 429. Không có chỉ mục mới (migration 134 YAGNI). Đã đo trên bản sao DB có lưu lượng thực với quy mô vừa phải; một phiên bản có thông lượng cao sẽ chứa số lượng hàng lớn hơn tương ứng trong cùng cửa sổ. --- ## Các tính năng phục hồi khác - **19 chiến lược định tuyến** (ưu tiên, có trọng số, luân phiên, chuyển tiếp ngữ cảnh, lấp đầy trước, p2c, ngẫu nhiên, ít được sử dụng nhất, tối ưu hóa chi phí, nhận biết thời điểm đặt lại, cửa sổ đặt lại, dung lượng dự phòng, ngẫu nhiên nghiêm ngặt, tự động, lkgp, tối ưu hóa ngữ cảnh, tối ưu hóa bộ nhớ đệm, hợp nhất, quy trình) — xem [AUTO-COMBO.md](../routing/AUTO-COMBO.md). - **Định tuyến nhận biết thời điểm đặt lại** (v3.8.0) — ưu tiên các kết nối theo thời điểm đặt lại hạn ngạch. - **Chuyển cấp chế độ nền** — `background: true` của Responses API được chuyển xuống chế độ đồng bộ kèm cảnh báo. - **Phát hiện động giới hạn công cụ** — giảm tải cho các nhà cung cấp khi đạt đến giới hạn số lượng công cụ. - **Phương án dự phòng khẩn cấp** — được kiểm soát bởi `OMNIROUTE_EMERGENCY_FALLBACK`; người vận hành có thể ghi đè thiết lập này từ trang Feature Flags mà không cần khởi động lại. --- ## Gỡ lỗi - Combo có trọng số trả về `503 all_targets_cooling_down` (`Retry-After` được thiết lập, `diagnostics.excluded` liệt kê mọi mục tiêu với `model_lockout` / `circuit_open` / `provider_cooldown` / `unavailable`) → nhóm đã được cấu hình và kết nối, nhưng mọi mục tiêu đều đang bị loại trừ bởi bộ hẹn giờ phục hồi; cảnh báo `[COMBO] Weighted selection: every target excluded before dispatch — …` nêu rõ lý do và số giây còn lại. Phản hồi `404 no_executable_targets` từ cùng combo có nghĩa là không có bộ hẹn giờ phục hồi nào liên quan (không có gì để chạy hoặc mọi tài khoản đều không vượt qua được phép dò tìm tính khả dụng). Được triển khai trong `open-sse/services/combo/pinRecovery.ts` từ các mục loại trừ được thu thập trong `targetResolution.ts`. - Tất cả khóa của một nhà cung cấp đều bị bỏ qua → kiểm tra cả trạng thái bộ ngắt mạch VÀ `rateLimitedUntil`/`testStatus` của từng kết nối. - Nhà cung cấp bị loại trừ vĩnh viễn sau cửa sổ đặt lại → mã đang đọc trực tiếp `state` thay vì `getStatus()`/`canExecute()`. - Một khóa thất bại, các khóa khác vẫn nên hoạt động → ưu tiên thời gian chờ của kết nối hơn bộ ngắt mạch. - Chỉ một mô hình thất bại → ưu tiên khóa mô hình hơn thời gian chờ của kết nối. - Trạng thái lẽ ra phải tự phục hồi nhưng không xảy ra → kiểm tra dấu thời gian trong tương lai + đường dẫn đọc có làm mới trạng thái đã hết hạn hay không. Các trạng thái vĩnh viễn yêu cầu thay đổi thủ công. --- ## Dấu vân tay TLS & khả năng ẩn mình Khả năng ẩn mình dành riêng cho từng nhà cung cấp (JA3/JA4, CCH, làm rối mã) được ghi lại riêng — xem `docs/security/STEALTH_GUIDE.md` (git; không được biên dịch vào `/docs`). --- ## Kiểm thử khả năng phục hồi (Giai đoạn 8 · Khối C) Ngoài các kiểm thử đơn vị cho logic phục hồi, ba kiểm thử đánh giá môi trường chạy trong điều kiện áp lực/sự cố thực tế (tất cả đều là kiểm thử tích hợp/hằng đêm — không kiểm thử nào chặn PR): | Kiểm thử | Nội dung | Cách chạy | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | Hỗn loạn | Nút upstream giả lập tạo ra độ trễ/đặt lại/hết thời gian chờ/503 thực tế; xác thực rằng bộ ngắt mạch mở/phục hồi và `checkFallbackError` phân loại 503 là phương án dự phòng có thể phục hồi. | `RUN_CHAOS_INT=1 npm run test:chaos` | | Tăng trưởng heap | ~500 luồng cho mỗi `createSSEStream` khi dùng `--expose-gc`; thất bại nếu heap tăng vượt quá ngưỡng trần (cơ chế bảo vệ OOM #3069). | `npm run test:heap` | | Ngâm tải k6 | Tải duy trì liên tục lên `/api/monitoring/health`; các ngưỡng p95/lỗi. | `k6 run tests/load/k6-soak.js` (hằng đêm) | Được điều phối bởi `.github/workflows/nightly-resilience.yml` (cron + điều phối thủ công). Trong `test:integration` mặc định, các kiểm thử hỗn loạn và heap tự bỏ qua (khi không có `RUN_CHAOS_INT`/`--expose-gc`). --- ## Xem thêm - [Hướng dẫn kiến trúc](./ARCHITECTURE.md) — Kiến trúc hệ thống và cơ chế nội bộ - [Hướng dẫn người dùng](../guides/USER_GUIDE.md) — Nhà cung cấp, tổ hợp, tích hợp CLI - [Công cụ tổ hợp tự động](../routing/AUTO-COMBO.md) — Chấm điểm 16 yếu tố, gói chế độ