# Resilience Guide (Ελληνικά) 🌐 **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) · 🇪🇸 [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) · 🇻🇳 [vi](../../../vi/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 διαθέτει τρεις διακριτούς αλλά συναφείς μηχανισμούς ανθεκτικότητας. Καθένας έχει διαφορετική εμβέλεια και σκοπό. Διατηρείτε τους ξεχωριστούς κατά την αποσφαλμάτωση της συμπεριφοράς δρομολόγησης. ![Μοντέλο ανθεκτικότητας 3 επιπέδων](../diagrams/exported/resilience-3layers.svg) > Πηγή: [diagrams/resilience-3layers.mmd](../diagrams/resilience-3layers.mmd) ## 1. Αυτόματος διακόπτης παρόχου **Πεδίο εφαρμογής:** ολόκληρος ο πάροχος (π.χ. `glm`, `openai`, `anthropic`). **Σκοπός:** διακοπή της αποστολής κίνησης σε έναν πάροχο που αποτυγχάνει επανειλημμένα σε επίπεδο upstream/υπηρεσίας. **Υλοποίηση:** - Βασική κλάση: `src/shared/utils/circuitBreaker.ts` - Διασύνδεση: `src/sse/handlers/chatHelpers.ts`, `src/sse/handlers/chat.ts` - API κατάστασης: `GET /api/monitoring/health` - API επαναφοράς: `POST /api/resilience/reset` - Περιτυλίγματα: `open-sse/services/accountFallback.ts` - Πίνακας ΒΔ: `domain_circuit_breakers` **Καταστάσεις:** - `CLOSED` — επιτρέπεται η κανονική κίνηση - `DEGRADED` — η κίνηση εξακολουθεί να επιτρέπεται, αλλά παρακολουθείται ο αυξημένος αριθμός αποτυχιών του παρόχου· επιστρέφει σε `CLOSED` μετά από αρκετές επιτυχίες, έπειτα από μια έγκυρη δοκιμή σύνδεσης ή αφού παρέλθει η μεγαλύτερη περίοδος παραμονής σε `OPEN` για το τελευταίο είδος αποτυχίας (η τιμή του `cooldownByKind`, διαφορετικά το `resetTimeout`, × `maxBackoffMultiplier`, τουλάχιστον μία περίοδος αναμονής), χωρίς νέα αποτυχία - `OPEN` — ο πάροχος αποκλείεται προσωρινά· η συνδυαστική δρομολόγηση τον παραλείπει - `HALF_OPEN` — έληξε το χρονικό όριο επαναφοράς· επιτρέπεται ένα δοκιμαστικό αίτημα **Παραμετροποιήσιμες προεπιλογές (`open-sse/config/constants.ts`, διαθέσιμες στον Πίνακα ελέγχου → Ρυθμίσεις → Ανθεκτικότητα):** | Κατηγορία | Υποβάθμιση στις | Άνοιγμα στις | Χρονικό όριο επαναφοράς | | ---------- | --------------- | ------------ | ----------------------- | | OAuth | 5 αποτυχίες | 8 αποτυχίες | 60s | | Κλειδί API | 7 αποτυχίες | 12 αποτυχίες | 30s | | Τοπικός | παράγωγο | 2 αποτυχίες | 15s | Το `degradationThreshold` ελέγχει πότε ένας πάροχος εισέρχεται σε κατάσταση `DEGRADED`· το `failureThreshold` ελέγχει πότε ανοίγει ο διακόπτης και ο πάροχος παραλείπεται. Τα προφίλ τοπικών παρόχων δεν εμφανίζονται ακόμη στη σελίδα ρυθμίσεων Ανθεκτικότητας. **Κωδικοί ενεργοποίησης:** μόνο καταστάσεις επιπέδου παρόχου `[408, 500, 502, 503, 504]`. ΜΗΝ ενεργοποιείτε τον διακόπτη για σφάλματα επιπέδου λογαριασμού (τα περισσότερα 401/403/429 — αυτά υπάγονται στην περίοδο αναμονής ή στο κλείδωμα). **Οκνηρή ανάκαμψη:** όταν λήξει η κατάσταση `OPEN`, τα `getStatus()`, `canExecute()`, `getRetryAfterMs()` ανανεώνουν την κατάσταση σε `HALF_OPEN`. Οι ίδιες αναγνώσεις (και η επόμενη αποτυχία) επαναφέρουν μια παρωχημένη κατάσταση `DEGRADED` σε `CLOSED`. Δεν απαιτείται χρονοδιακόπτης παρασκηνίου. --- ### Προαιρετική καθολική περίοδος αναμονής παρόχου (πύλη χρονικού παραθύρου) Ένα τέταρτο, **προαιρετικό** επίπεδο (`PROVIDER_COOLDOWN_ENABLED`, προεπιλογή: **απενεργοποιημένο**) διατηρεί στο `open-sse/services/providerCooldownTracker.ts` μια μνήμη των παρόχων που αποτυγχάνουν μεταξύ διαφορετικών αιτημάτων. Η μνήμη αυτή χρησιμοποιείται κατά την επίλυση συνδυαστικών προορισμών, ώστε τα διαδοχικά συνδυαστικά αιτήματα να μην επανεξετάζουν έναν πάροχο που μόλις απέτυχε. Οι καταχωρίσεις επιπέδου παρόχου τηρούν την πύλη χρονικού παραθύρου `PROVIDER_PROFILES`: | Προφίλ | ενεργοποίηση μετά από (`providerFailureThreshold`) | εντός (`providerFailureWindowMs`) | αναμονή για (`providerCooldownMs`) | | ---------- | -------------------------------------------------: | --------------------------------: | ---------------------------------: | | OAuth | `10` | `15min` | `5min` | | Κλειδί API | `15` | `30min` | `10min` | Κάτω από το όριο, ο πάροχος **δεν** θεωρείται ότι βρίσκεται σε περίοδο αναμονής· μια επιτυχία εκκαθαρίζει το παράθυρο. Αντίθετα, οι καταχωρίσεις επιπέδου σύνδεσης (`provider:connectionId`) διατηρούν την εκθετική υποχώρηση `minRetryCooldownMs → maxRetryCooldownMs`. Παρακάμψεις: `OMNIROUTE_PROVIDER_BREAKER_{OAUTH,API_KEY}_{FAILURE_THRESHOLD,FAILURE_WINDOW_MS,COOLDOWN_MS}`. Προστασία από παλινδρόμηση: `tests/unit/provider-cooldown-window-gate.test.ts`. ## 2. Περίοδος αναμονής σύνδεσης **Πεδίο εφαρμογής:** μεμονωμένη σύνδεση/λογαριασμός/κλειδί παρόχου. **Σκοπός:** παράλειψη ενός προβληματικού κλειδιού, ενώ οι άλλες συνδέσεις για τον ίδιο πάροχο συνεχίζουν να εξυπηρετούν αιτήματα. **Υλοποίηση:** - Επισήμανση ως μη διαθέσιμου: `src/sse/services/auth.ts::markAccountUnavailable()` - Επιλογή: `getProviderCredentials*` στο ίδιο αρχείο - Υπολογισμός περιόδου αναμονής: `open-sse/services/accountFallback.ts::checkFallbackError()` - Ρυθμίσεις: `src/lib/resilience/settings.ts` **Πεδία ανά σύνδεση:** - `rateLimitedUntil` — χρονική σήμανση μέχρι τη λήξη της περιόδου αναμονής - `testStatus: "unavailable"` - `lastError`, `lastErrorType`, `errorCode` - `backoffLevel` — μετρητής εκθετικής οπισθοχώρησης **Προεπιλεγμένες περίοδοι αναμονής:** - Βάση OAuth: 5s - Βάση κλειδιού API: 3s - 429 κλειδιού API: προτιμώνται τα upstream `Retry-After`/headers επαναφοράς/κείμενο επαναφοράς που μπορεί να αναλυθεί - Οπισθοχώρηση: `baseCooldownMs * 2 ** failureIndex` **Προστασία από το φαινόμενο thundering herd:** αποτρέπει την υπερβολική παράταση της περιόδου αναμονής ή τη διπλή αύξηση του `backoffLevel` από ταυτόχρονες αποτυχίες. **Τα 429 ορίου ταυτόχρονων αιτημάτων δεν ανεβαίνουν στην κλίμακα οπισθοχώρησης.** Ένα `429`, του οποίου το σώμα αναφέρει όριο ταυτόχρονων αιτημάτων ("too many concurrent requests", "concurrency limit", "parallel request limit", GLM `并发数过高`), σημαίνει ότι ο λογαριασμός λειτουργεί κανονικά αλλά είναι απασχολημένος. Το `checkFallbackError()` επιστρέφει την αιτία `concurrency_limited`: πραγματοποιεί άμεσα μετάπτωση στην επόμενη σύνδεση, θέτει αυτήν σε αναμονή για ένα σταθερό διάστημα `CONCURRENCY_LIMIT_COOLDOWN_MS` (1s) ή για το διάστημα που υποδεικνύει το upstream, διατηρεί αμετάβλητο το `backoffLevel` και ορίζει το `skipProviderBreaker`. Ένα ρητό σώμα που δηλώνει εξάντληση ορίου χρήσης εξακολουθεί να υπερισχύει, επομένως ένα σώμα που αντιστοιχεί επίσης σε μοτίβο ορίου χρήσης εξακολουθεί να χαρακτηρίζεται ως εξάντληση ορίου χρήσης. Ανίχνευση: `looksLikeConcurrencyLimit()` στο `src/shared/utils/classify429.ts`· ετυμηγορία: `open-sse/services/accountFallback/concurrencyLimit429.ts`. Το `classify429()` (το `FailureKind` του breaker) παραμένει αμετάβλητο και εξακολουθεί να επιστρέφει `rate_limit` για αυτά τα σώματα. **Οι στασιμότητες περιεχομένου ροής δεν θέτουν τον λογαριασμό σε αναμονή.** Όταν ο επόπτης στασιμότητας περιεχομένου (`open-sse/utils/streamHandler.ts`) εγκαταλείψει μια ροή που δεν έστειλε εγκαίρως έξοδο μοντέλου, το `markAccountUnavailable()` καταγράφει το σφάλμα στη σύνδεση, αλλά δεν ορίζει περίοδο αναμονής: η στασιμότητα αφορά το συγκεκριμένο αίτημα και, στις περισσότερες περιπτώσεις, πρόκειται για έναν παρατεταμένο κύκλο συλλογισμού χωρίς ακόμη να υπάρχει έξοδος. Οι διαχειριστές μπορούν να ενεργοποιήσουν ξανά αυτήν τη συμπεριφορά μέσω του `resilienceSettings.streamStallCooldown.enabled` (προεπιλογή `false`). **Τα πλαίσια συλλογισμού επανεκκινούν το χρονικό περιθώριο στασιμότητας περιεχομένου.** Ένα μοντέλο συλλογισμού μπορεί να σκέφτεται για λεπτά πριν από το πρώτο ορατό token του: το Claude μεταδίδει πλαίσια `thinking_delta`, των οποίων το κείμενο συλλογισμού μπορεί να είναι κενό, ενώ το Responses API μεταδίδει διαδοχικά στοιχεία συλλογισμού. Το `isReasoningProgressFrame()` (`open-sse/utils/streamReadiness.ts`) αναγνωρίζει αυτά τα πλαίσια και ο επόπτης επανεκκινεί το χρονικό του περιθώριο σε καθένα από αυτά, αντί να ακυρώνει τον κύκλο. Εξακολουθούν να μην αποτελούν έξοδο μοντέλου, επομένως ένας κύκλος που ολοκληρώνεται μόνο με συλλογισμό εξακολουθεί να αναφέρεται ως κενός, ενώ ένας κύκλος που σταματά να συλλογίζεται και στέλνει μόνο σήματα διατήρησης σύνδεσης εξακολουθεί να ενεργοποιεί τον επόπτη. Τα δυαδικά πλαίσια `reasoningContentEvent` του Kiro με μη κενή υπογραφή διατηρούν αυτήν τη δραστηριότητα συλλογισμού μέσω του executor ως ένα κενό delta `reasoning_content`. Η υπογραφή δεν προωθείται. Τα μεταδεδομένα, τα ημιτελή πλαίσια και οι κενές υπογραφές δεν επανεκκινούν το χρονικό περιθώριο περιεχομένου· το ανεξάρτητο χρονικό όριο ενεργής ροής και η ακύρωση από τον client εξακολουθούν να ισχύουν (`open-sse/executors/kiro/reasoning.ts`). **Τερματικές καταστάσεις (ΟΧΙ περίοδοι αναμονής):** - `banned` — ορίζεται από την ανίχνευση απαγορευμένης λέξης-κλειδιού / αποκλεισμού λογαριασμού (βλ. [BAN_DETECTION](../security/BAN_DETECTION.md)) και από τρεις διαδοχικές upstream απορρίψεις ανά αίτημα (`request_rejected`, π.χ. Anthropic OAuth 403 "Request not allowed" — `open-sse/services/requestRejectedStreak.ts`)· μία μεμονωμένη απόρριψη θέτει μόνο τη σύνδεση σε αναμονή - `expired` (μεταβαίνει σε τερματική κατάσταση μετά από περιορισμένο αριθμό επαναλήψεων — `EXPIRED_RETRY_MAX = 3` με εκθετική οπισθοχώρηση — ώστε τα παροδικά σφάλματα OAuth να μπορούν να αποκατασταθούν αυτόματα πριν από την οριστική απενεργοποίηση του λογαριασμού) - `credits_exhausted` Αυτές διατηρούνται μέχρι να αλλάξουν τα διαπιστευτήρια ή να τις επαναφέρει ένας διαχειριστής. Μην αντικαθιστάτε τις τερματικές καταστάσεις με παροδική κατάσταση αναμονής. **Οκνηρή ανάκτηση:** όταν παρέλθει το `rateLimitedUntil`, η σύνδεση καθίσταται ξανά διαθέσιμη για επιλογή. Μετά από επιτυχή χρήση, το `clearAccountError()` διαγράφει όλα τα πεδία σφάλματος. ### Όριο χρήσης Claude OAuth: λωρίδα χαμηλότερης προτεραιότητας + επαναφορά ορίου συνεδρίας **Πεδίο εφαρμογής:** μία σύνδεση συνδρομής Claude (OAuth). Και οι δύο λειτουργίες είναι **προαιρετικές ανά σύνδεση** (Επεξεργασία σύνδεσης → ενότητα Claude → `lowPriorityMode` / `autoLimitReset` στο `providerSpecificData`, και οι δύο απενεργοποιημένες από προεπιλογή) και αντικατοπτρίζουν τις εντολές `/low-priority` και `/limit-reset` του Claude Code (σύμβαση πρωτοκόλλου που καταγράφηκε από το Claude Code 2.1.263). **Υλοποίηση:** - Μηχανή καταστάσεων + ταξινόμηση απόκρισης: `open-sse/services/claudeLowPriority.ts` - Client κατάστασης/δέσμευσης επαναφοράς: `open-sse/services/claudeLimitReset.ts` - Hook executor (εισαγωγή header + επανάληψη στον ίδιο λογαριασμό): `open-sse/executors/base.ts::execute()` - Διατήρηση προαιρετικής ενεργοποίησης: `src/lib/providers/requestDefaults.ts::normalizeProviderSpecificData()` **Ενεργοποίηση:** το όριο χρήσης των 5 ωρών — ένα `429`, του οποίου τα headers περιέχουν `anthropic-ratelimit-unified-status: rejected` και, όταν ο λογαριασμός είναι κατάλληλος, `anthropic-ratelimit-unified-slow-offer: treatment`. Δεν αποστέλλεται τίποτα πριν από αυτό το πρώτο 429 ορίου· ένα αιφνίδιο 429 χωρίς ενοποιημένα headers ακολουθεί την κανονική διαδρομή περιόδου αναμονής. **Λωρίδα χαμηλότερης προτεραιότητας** (`lowPriorityMode`): - Στο 429 του ορίου, ο executor αποδέχεται την προσφορά και επαναλαμβάνει αμέσως την προσπάθεια με τον **ίδιο** λογαριασμό και `anthropic-usage-limit: slow`· η λωρίδα παραμένει ενεργή μέχρι το ανακοινωθέν `anthropic-ratelimit-unified-reset` (+60s περίοδος χάριτος) και κάθε αίτημα εντός αυτού του χρονικού παραθύρου φέρει την κεφαλίδα. Το 429 που αναχαιτίστηκε δεν φτάνει ποτέ στο `handleChatCore`, επομένως η σύνδεση **δεν** τίθεται σε cooldown και δεν αντικαθίσταται μέσω εναλλαγής. - Το `anthropic-ratelimit-unified-slow-status` σε μεταγενέστερες αποκρίσεις: τα `active` / `not_needed` διατηρούν τη λωρίδα· το `slot_busy` (429) ή ένα `529` περιμένουν για το διάστημα που ορίζει το `anthropic-ratelimit-unified-slow-retry-after` του διακομιστή (προεπιλογή 20s, περιορισμός 5–600s, ±30% jitter) και επαναλαμβάνουν την προσπάθεια, με όριο το `anthropic-ratelimit-unified-slow-max-wait` (προεπιλογή 20 min, περιορισμός 1 min–6 h) — μετά από αυτό, η λωρίδα τερματίζεται και μια περίοδος αναμονής 10 λεπτών εμποδίζει την εκ νέου αποδοχή. Η αναμονή περιορίζεται επιπλέον από τον χρόνο που απομένει μέχρι το timeout έναρξης upstream του ίδιου του αιτήματος (`resolveFetchStartTimeout`, 10 min από προεπιλογή), μείον ένα περιθώριο 5 s: χωρίς αυτό το όριο, ο προεπιλεγμένος μέγιστος χρόνος αναμονής των 20 λεπτών θα ξεπερνούσε τη διάρκεια ζωής του αιτήματος και η αναμονή θα διακοπτόταν στη μέση, εμφανίζοντας ένα `TimeoutError` αντί για τον ομαλό τερματισμό `max_wait` + περίοδο αναμονής. - Τα `weekly_limit` / `budget_exhausted` / `off` / `ineligible`, η ανανέωση ενός παραθύρου 5h ή το `ineligible` + `anthropic-ratelimit-unified-overage-in-use: true` (που τερματίζει τη λωρίδα ως `extra_usage` με οποιαδήποτε κατάσταση, καθώς η πληρωμένη υπέρβαση καλύπτει πλέον το όριο) τερματίζουν τη λωρίδα· η απόκριση ακολουθεί κατόπιν την κανονική διαδρομή cooldown. Το `budget_exhausted` διατηρείται στη μνήμη μέχρι την ανακοινωθείσα επαναφορά του προϋπολογισμού (≤ 8 ημέρες). - Ο έλεγχος του ορίου εκτελείται μετά τις επαναλήψεις εντός της ίδιας προσπάθειας που προκαλούνται από 400 στον ίδιο τον executor (επεξεργασία context, περιορισμοί thinking/effort, αυτόματη εκμάθηση παραμέτρων), επομένως ένα 429 ορίου που εμφανίζεται μόνο σε μία από αυτές τις επαναλήψεις εξακολουθεί να αναχαιτίζεται αντί να φτάσει στη διαδρομή cooldown. - Η κατάσταση διατηρείται στη μνήμη ανά σύνδεση (μια επανεκκίνηση κοστίζει ένα επιπλέον 429 ορίου για εκ νέου αποδοχή). **Επαναφορά ορίου συνεδρίας** (`autoLimitReset`, δοκιμάζεται πριν από τη λωρίδα όταν είναι ενεργοποιημένα και τα δύο): - `GET https://api.anthropic.com/api/oauth/usage?at_wall=1&skip_spend=1` → μπλοκ `juniper_tide`· όταν `arm: "reset"` και `available: true`, `POST https://api.anthropic.com/api/organizations/{orgUUID}/reset_rate_limits` με `{ "program": "juniper_tide" }` (UUID οργανισμού από το `providerSpecificData.organizationUUID`, με εναλλακτική το bootstrap). - `result: reset|not_limited` → το αίτημα επαναλαμβάνεται με πλήρη ταχύτητα (χωρίς την κεφαλίδα slow). Τα `already_used` / `not_offered` απομνημονεύουν το `next_available_at` (προεπιλογή μία εβδομάδα)· οποιαδήποτε αποτυχία ενεργοποιεί αναμονή 15 λεπτών. Η επαναφορά γίνεται μία φορά την εβδομάδα και εξακολουθεί να προσμετράται στο εβδομαδιαίο όριο. Δικλείδες προστασίας από παλινδρομήσεις: `tests/unit/claude-low-priority-mode.test.ts`, `tests/unit/claude-limit-reset.test.ts`, `tests/unit/claude-low-priority-executor.test.ts`. ### Συνάφεια συνεδρίας (#7274) **Εμβέλεια:** μία συνεδρία πελάτη (κεφαλίδα `X-Session-Id` / `x-codex-session-id` / `x-omniroute-session`) καρφιτσωμένη σε μία σύνδεση, για **οποιονδήποτε** πάροχο. **Σκοπός:** διατήρηση ενός agent πολλαπλών αλληλεπιδράσεων (Claude Code, aider, προσαρμοσμένοι agents) στον ίδιο λογαριασμό μεταξύ αιτημάτων, μειώνοντας την απώλεια context μεταξύ λογαριασμών και τα επαναλαμβανόμενα 429 ψυχρής εκκίνησης σε παρόχους με κατάσταση συνεδρίας ανά λογαριασμό. **Υλοποίηση:** - Επίλυση TTL: `src/sse/services/sessionAffinityPin.ts::resolveSessionAffinityTtlMs()` - Επιλογή/δημιουργία καρφιτσώματος: `src/sse/services/sessionAffinityPin.ts::selectSessionAffinityConnection()` - Εξαγωγή κεφαλίδας (γενική, για οποιονδήποτε πάροχο): `src/sse/services/auth.ts::extractSessionAffinityKey()` - Μόνιμος πίνακας καρφιτσωμάτων: `sessionAccountAffinity` (`src/lib/db/sessionAccountAffinity.ts`) - Ρύθμιση: `sessionAffinityTtlMs` (καθολικό TTL σε ms, το `0` το απενεργοποιεί) — `src/lib/db/settings.ts`. Μετονομάστηκε από το `codexSessionAffinityTtlMs`, που αφορούσε μόνο το Codex, μέσω της μετεγκατάστασης `124_generic_session_affinity_ttl.sql`, η οποία μεταφέρει οποιοδήποτε προηγουμένως ρυθμισμένο TTL του Codex ως τη νέα προεπιλογή. Πριν από το #7274, το `resolveSessionAffinityTtlMs()` επέστρεφε αμέσως `0` για κάθε πάροχο εκτός από το `codex`, επομένως η ρύθμιση TTL (και οι κεφαλίδες συνεδρίας) δεν είχαν αποτέλεσμα πουθενά αλλού, παρόλο που ο μηχανισμός καρφιτσώματος και η εξαγωγή κεφαλίδων ήταν ήδη ανεξάρτητα από τον πάροχο. Η διόρθωση αφαίρεσε αυτήν την πρόωρη επιστροφή· το TTL εφαρμόζεται πλέον ομοιόμορφα σε κάθε πάροχο μόλις οριστεί καθολικά πάνω από `0`. Οι τρεις κεφαλίδες συνάφειας συνεδρίας δεν προωθούνται ποτέ upstream — οι executors δημιουργούν τις δικές τους upstream κεφαλίδες από την αρχή αντί να διαβιβάζουν τις κεφαλίδες του πελάτη, επομένως αυτό παραμένει μόνο ένα εσωτερικό αναγνωριστικό συσχέτισης. ### Αποκλειστικές μισθώσεις σύνδεσης διαχειριζόμενης συνεδρίας **Εμβέλεια:** ένας ενεργός διαχειριζόμενος πελάτης/μία ενεργή διαχειριζόμενη συνεδρία HTTP κατέχει μία επιλέξιμη σύνδεση OmniRoute. **Σκοπός:** παροχή διαρκούς αποκλειστικής κυριότητας σύνδεσης για πελάτες που χρειάζονται ένα αυστηρό όριο δρομολόγησης μεταξύ αιτημάτων. Αυτό διαφέρει από τη συνάφεια συνεδρίας, η οποία αποτελεί μια ήπια προτίμηση συνέχειας: μια αποκλειστική μίσθωση διατηρεί την κατάσταση κύκλου ζωής στο SQLite, επιβάλλει καθολική μοναδικότητα ενεργού κατόχου και ενεργής σύνδεσης και απορρίπτει μια παρωχημένη γενιά πριν από την αποστολή στον πάροχο. Η λειτουργία είναι προαιρετική ανά κλειδί API. Ένα διαχειριζόμενο κλειδί πρέπει να έχει το scope `lease:exclusive` και μια ρητή, μη κενή λίστα `allowedConnections`. Οποιοσδήποτε πελάτης HTTP μπορεί να χρησιμοποιήσει το endpoint κύκλου ζωής· δεν απαιτείται όνομα πελάτη, user-agent, πάροχος, μέθοδος OAuth ή μοντέλο. Η μίσθωση κατέχει μια σύνδεση και όχι ένα μοντέλο, επομένως μια αλλαγή μοντέλου διατηρεί τη δέσμευση όσο η σύνδεση παραμένει κανονικά επιλέξιμη. Οι κανονικοί κανόνες μοντέλου, quota, εύρυθμης λειτουργίας, cooldown και λίστας επιτρεπόμενων παραμένουν δεσμευτικοί και μπορούν να μεταφέρουν την ίδια γενιά σε άλλη ελεύθερη επιλέξιμη σύνδεση. Ο κύκλος ζωής είναι `POST /api/v1/session-leases` με ενέργειες JSON `acquire`, `renew` και `release`. Τα διαχειριζόμενα αιτήματα εξαγωγής συμπερασμάτων περιλαμβάνουν την αδιαφανή τιμή `X-OmniRoute-Lease-Owner` και την ακριβή `X-OmniRoute-Lease-Generation`. Ο κάτοχος χρησιμοποιεί το πρόθεμα `vlo_` ακολουθούμενο από 43 χαρακτήρες base64url· αποθηκεύεται μόνο ο κατακερματισμός SHA-256 του. Κάθε τελικό όριο αποστολής δεσμεύει επίσης το αναγνωριστικό του αυθεντικοποιημένου κλειδιού API και το αναγνωριστικό της ενεργής σύνδεσης. Οι κεφαλίδες ελέγχου μίσθωσης αφαιρούνται από τα αρχεία καταγραφής, τα διατηρούμενα στιγμιότυπα αιτημάτων και τις κεφαλίδες του ανάντη εκτελεστή. Εάν η συνήθης δρομολόγηση διαθέτει επιλέξιμους διαχειριζόμενους υποψηφίους, αλλά κάθε ελεύθερος υποψήφιος καταλαμβάνεται από μια ενεργή μίσθωση τρίτου, το OmniRoute επιστρέφει HTTP `429`, τον κωδικό lease-capacity-unavailable, μια κατάσταση αναμονής για χωρητικότητα και μια φραγμένη τιμή `Retry-After` που προκύπτει από την ενωρίτερη σχετική λήξη. Η συνήθης απουσία επιλέξιμων υποψηφίων δεν αποτελεί διένεξη μίσθωσης και διατηρεί την υπάρχουσα σημασιολογία σφαλμάτων δρομολόγησης. Οι σχετικοί μηχανισμοί παραμένουν διακριτοί: - Η κατάληψη συνεδρίας OAuth αποτελεί ήπια κατανομή σε τοπικό επίπεδο διεργασίας για λογαριασμούς OAuth. - Οι σημαφόροι λογαριασμών παραχωρούν άδειες ταυτόχρονης εκτέλεσης αιτημάτων και τερματίζονται όταν ολοκληρώνεται ένα αίτημα. - Οι αποκλειστικές μισθώσεις διαχειριζόμενων συνεδριών παρέχουν ανθεκτική ιδιοκτησία κύκλου ζωής με όριο γενιάς. --- ## 3. Αποκλεισμός μοντέλου **Εμβέλεια:** τριάδα παρόχου + σύνδεσης + μοντέλου. **Εμβέλεια κλειδιού ανά κατάσταση:** η κατάσταση αποτυχίας καθορίζει σε ποιο κλειδί εγγράφεται ένας αποκλεισμός (`resolveLockoutScope()` στο `open-sse/services/accountFallback/exactModelLock.ts`): - `429` / `403` / `402` — ένδειξη ορίου χρήσης ή δικαιώματος — κλειδώνουν την **οικογένεια ορίου χρήσης**: για το codex, ολόκληρη την εμβέλεια `codex` / `spark` (κάθε μοντέλο `gpt-5*` της σύνδεσης), ενώ για άλλους παρόχους το `getQuotaScopedModelForProvider()`. - Το `404` κλειδώνει το βασικό μοντέλο (το `getModelLockKey()` περιορίζει το `not_found`). - Οποιαδήποτε άλλη κατάσταση — αποτυχίες μεταφοράς/διακομιστή `5xx` και το συνθετικό `502` του ίδιου του OmniRoute από την επικύρωση ποιότητας — κλειδώνει μόνο την **ακριβή** πλειάδα παρόχου/σύνδεσης/μοντέλου. Μια προβληματική ροή σε ένα μοντέλο δεν αποτελεί ένδειξη για το όριο χρήσης του λογαριασμού· πριν από αυτόν τον κανόνα, μία κενή απόκριση στο `codex/gpt-5.6-luna` αφαιρούσε κάθε μοντέλο `gpt-5*` αυτής της σύνδεσης από τη δρομολόγηση για 2–30 λεπτά (με κλιμάκωση), ενώ το όριο χρήσης της παρέμενε ανεπηρέαστο. - Η ρητή επιλογή `scope` του καλούντος υπερισχύει πάντα (το Antigravity περνά `"exact"`). **Σκοπός:** αποφυγή απενεργοποίησης ολόκληρης σύνδεσης όταν μόνο ένα μοντέλο δεν είναι διαθέσιμο ή έχει περιοριστεί λόγω ορίου χρήσης. **Παραδείγματα:** - Πάροχοι με όριο χρήσης ανά μοντέλο που επιστρέφουν 429 - Τοπικοί πάροχοι που επιστρέφουν 404 για ένα μοντέλο που λείπει - Αποτυχίες δικαιωμάτων λειτουργίας/μοντέλου ειδικές για τον πάροχο (π.χ. λειτουργίες Grok) **Υλοποίηση:** `open-sse/services/accountFallback.ts` — `lockModel()`, `clearModelLock()`, `getAllModelLockouts()`. ### Πίνακας εργαλείων χρόνων αναμονής μοντέλων (v3.8.0) Διεπαφή χρήστη: Ρυθμίσεις → Χρόνοι αναμονής μοντέλων (`src/app/(dashboard)/dashboard/settings/components/ModelCooldownsCard.tsx`) Παραθέτει τους ενεργούς αποκλεισμούς με: πάροχο, σύνδεση, μοντέλο, αιτία, expiresAt. Οι διαχειριστές μπορούν να επανενεργοποιήσουν χειροκίνητα ένα μοντέλο από την κάρτα. **REST API:** - `GET /api/resilience/model-cooldowns` — παράθεση ενεργών αποκλεισμών - `DELETE /api/resilience/model-cooldowns` — χειροκίνητη επανενεργοποίηση. Σώμα: `{provider, connection, model}`. Έλεγχος ταυτότητας: διαχείριση. ### Διαχείριση χρόνων αναμονής Διεπαφή χρήστη: Παρακολούθηση → Διαχείριση χρόνων αναμονής (`src/app/(dashboard)/dashboard/resilience/cooldowns/`). Μία σελίδα για κάθε σύνδεση που έχει αφαιρεθεί από τη δρομολόγηση για παροδικό λόγο, αντί για το άνοιγμα της σελίδας κάθε παρόχου. Παραθέτει τους χρόνους αναμονής συνδέσεων, τους αποκλεισμούς μοντέλων και τις τερματικές καταστάσεις, τα εκκαθαρίζει ανά σύνδεση, για μια επιλογή ή για όλες τις συνδέσεις ενός παρόχου, και επεξεργάζεται τους κανόνες χρόνου αναμονής που ρυθμίζονται συχνότερα: το `streamStallCooldown.enabled` και τον βασικό χρόνο αναμονής και τα μέγιστα βήματα οπισθοχώρησης του `connectionCooldown` για OAuth / κλειδί API (αποθηκεύονται μέσω `PATCH /api/resilience`). Οι τερματικές καταστάσεις (`banned`, `expired`, `credits_exhausted`) παρατίθενται αλλά δεν εκκαθαρίζονται ποτέ εδώ. **REST API** (`src/lib/resilience/cooldownManager.ts`, έλεγχος ταυτότητας: διαχείριση): - `GET /api/resilience/cooldowns[?provider=]` — συνδέσεις με κατάσταση, εναπομένοντα χρόνο αναμονής, επίπεδο οπισθοχώρησης, τελευταίο τύπο σφάλματος και αποκλεισμούς μοντέλων (χωρίς διαπιστευτήρια) - `POST /api/resilience/cooldowns` — σώμα `{connectionIds: string[]}` ή `{all: true, provider?}`· επιστρέφει `{cleared, unchanged, skippedTerminal, lockoutsCleared}` ### Διεπαφή ρυθμίσεων αποκλεισμού + ανάκτηση με απομείωση επιτυχιών (v3.8.23) Ο αποκλεισμός μοντέλου μετατράπηκε από μονίμως ενεργή, ενσωματωμένη συμπεριφορά σε μια πλήρως παραμετροποιήσιμη, προαιρετική δυνατότητα με τη δική της κάρτα ρυθμίσεων και μια αυτοϊώμενη διαδρομή ανάκτησης. **Κάρτα ρυθμίσεων:** Ρυθμίσεις → Αποκλεισμός μοντέλου (`src/app/(dashboard)/dashboard/settings/components/ModelLockoutCard.tsx`). Αυτή είναι **διακριτή** από την παραπάνω `ModelCooldownsCard` μόνο για ανάγνωση (η οποία απλώς _παραθέτει_ τους ενεργούς αποκλεισμούς) — η νέα κάρτα _διαμορφώνει τις παραμέτρους_. Οι προεπιλογές βρίσκονται στο `DEFAULT_MODEL_LOCKOUT_SETTINGS` (`src/lib/resilience/modelLockoutSettings.ts`): | Ρύθμιση | Προεπιλογή | Σημασία | | ----------------------- | -------------------------------- | ------------------------------------------------------------------------------------- | | `enabled` | `false` | Κύριος διακόπτης — ο αποκλεισμός μοντέλου είναι **απενεργοποιημένος από προεπιλογή**. | | `errorCodes` | `[403, 404, 429, 502, 503, 504]` | Καταστάσεις ανάντη υπηρεσίας που υπολογίζονται ως αποτυχία με εμβέλεια μοντέλου. | | `baseCooldownMs` | `120_000` (120 δευτ.) | Αρχική διάρκεια αποκλεισμού για την πρώτη αποτυχία. | | `maxCooldownMs` | `1_800_000` (30 λεπτά) | Ανώτατο όριο του κλιμακούμενου χρόνου αναμονής. | | `maxBackoffSteps` | `10` | Μέγιστα βήματα εκθετικής κλιμάκωσης οπισθοχώρησης. | | `useExponentialBackoff` | `true` | Αν οι επαναλαμβανόμενες αποτυχίες κλιμακώνουν εκθετικά τον χρόνο αναμονής. | Οι ρυθμίσεις διατηρούνται μέσω του κανονικού χώρου αποθήκευσης ρυθμίσεων και επικυρώνονται μέσω του σχήματος ρυθμίσεων ανθεκτικότητας· η κάρτα περιορίζει τα `baseCooldownMs`/`maxCooldownMs` (με `maxCooldownMs ≥ baseCooldownMs`) και `maxBackoffSteps`. **Ανάκτηση με απομείωση επιτυχιών:** η ανάκτηση **δεν** βασίζεται αποκλειστικά στη λήξη του χρονομέτρου. Μια υγιής απόκριση μειώνει σταδιακά τον αριθμό αποτυχιών του μοντέλου, ώστε ένα μοντέλο που ανέκαμψε εντός του χρονικού παραθύρου να σταματά να κλιμακώνεται (και να εκκαθαρίζεται) προτού λήξει το χρονόμετρό του. Σε έναν επιτυχή συνδυαστικό στόχο, το `open-sse/services/combo.ts` καλεί το `decayModelFailureCount()` (`open-sse/services/accountFallback.ts`), το οποίο **υποδιπλασιάζει** το αποθηκευμένο `failureCount` (`Math.floor(failureCount / 2)`)· όταν φτάσει στο `0`, η καταχώριση αποκλεισμού διαγράφεται πλήρως. Το αντίστοιχο `recordModelLockoutFailure()` αυξάνει τον αριθμό (και κλιμακώνει τον χρόνο αναμονής) για αποτυχίες εντός του παραθύρου κλιμάκωσης. Αυτή η απομείωση λόγω επιτυχίας λειτουργεί επιπλέον της απλής λήξης του χρονομέτρου — οποιαδήποτε από τις δύο διαδρομές μπορεί να επανενεργοποιήσει ένα μοντέλο. **Κατάσταση:** οι αποκλεισμοί διατηρούνται **στη μνήμη** (`Map` ανά διεργασία με `ModelLockoutEntry` και κλειδί `provider:connectionId:model`, ενώ οι κλειδαριές ακριβούς εμβέλειας έχουν κλειδί `provider:connectionId:exact:model`), δεν διατηρούνται στη ΒΔ — χάνονται κατά την επανεκκίνηση. Οι _ρυθμίσεις_ διατηρούνται· η ενεργή _κατάσταση_ αποκλεισμού είναι εφήμερη. --- ## 4. Έλεγχος ταυτόχρονων αιτημάτων Quota-Share (v3.8.36) Οι λογαριασμοί συνδρομής (GLM, MiniMax κ.λπ.) συχνά δέχονται μόνο ~1–3 ταυτόχρονα αιτήματα· η υπέρβαση αυτού του ορίου ενεργοποιεί σφάλματα 429 και περιόδους αναμονής. Αυτό είναι ιδιαίτερα έντονο στους συνδυασμούς **quota-share** (`qtSd/…`), όπου πολλά κλειδιά API μοιράζονται έναν ανάντη λογαριασμό. Τρία επίπεδα εμποδίζουν την υπερφόρτωση ενός κοινόχρηστου λογαριασμού. ### Όριο ταυτόχρονων αιτημάτων ανά σύνδεση (`max_concurrent`) Κάθε σύνδεση παρόχου μπορεί να δηλώνει ένα ανώτατο όριο `max_concurrent` (`provider_connections.max_concurrent`, το οποίο ορίζεται στο παράθυρο σύνδεσης / API / DB). Αφήστε το κενό για να μην υπάρχει όριο. Αυτή είναι η μοναδική ρύθμιση που ελέγχει το επίπεδο σειριοποίησης παρακάτω — ορίστε την στην πραγματική δυνατότητα ταυτόχρονων αιτημάτων του λογαριασμού (π.χ. GLM ~1, MiniMax ~2). ### Όρια ταυτόχρονων αιτημάτων ανά μοντέλο (`modelConcurrency`) Μια σύνδεση μπορεί επιπλέον να δηλώνει ακριβή ανώτατα όρια ταυτόχρονων αιτημάτων ανά μοντέλο μέσα στον χάρτη `rateLimitOverrides`: ```json { "rateLimitOverrides": { "maxConcurrent": 4, "modelConcurrency": { "glm-5": 1, "glm-4.7": 3 } } } ``` Ορίστε το στο παράθυρο σύνδεσης (**Παρακάμψεις ορίων ρυθμού → Όρια ταυτόχρονων αιτημάτων ανά μοντέλο**, ένα `model=cap` ανά γραμμή) ή μέσω `PATCH /api/providers/[id]` με την ίδια δομή JSON. Σημασιολογία κλειδιών: - **Σε επίπεδο σύνδεσης έναντι συγκεκριμένου μοντέλου:** Το `maxConcurrent` παραμένει το κοινόχρηστο ανώτατο όριο σε επίπεδο σύνδεσης. Όταν ισχύουν και τα δύο, αποκτώνται και οι δύο πύλες ατομικά στην ίδια σύνθετη πύλη (`global → provider → account → model`)· η πραγματική συμπεριφορά καθορίζεται από το αυστηρότερο εφαρμοζόμενο όριο. - **Ακριβής αντιστοίχιση κλειδιού μοντέλου:** το κλειδί είναι η συμβολοσειρά μοντέλου που μεταβιβάζεται στον εκτελεστή μετά την επίλυση της δρομολόγησης — συνήθως το απλό αναγνωριστικό ανάντη μοντέλου (`glm-5`), όχι ένα ψευδώνυμο `provider/model` στην πλευρά του πελάτη (το `zai/glm-5` δεν αντιστοιχεί στο `glm-5`). Οι τιμές είναι θετικοί ακέραιοι που ορίζουν ανώτατα όρια ταυτόχρονων αιτημάτων. - **Τοπική ουρά, χωρίς αυτόματο εντοπισμό:** τα επιπλέον αιτήματα μπαίνουν σε τοπική ουρά σύμφωνα με την υπάρχουσα σημασιολογία ουράς/λήξης χρονικού ορίου (τυποποιημένα σφάλματα αποδοχής `SEMAPHORE_TIMEOUT` / `SEMAPHORE_QUEUE_FULL`). Το OmniRoute δεν εντοπίζει ούτε συμπεραίνει την πολιτική του ανάντη συστήματος — επιβάλλει τα ακριβή όρια που διαμόρφωσε ο διαχειριστής. Μια κορεσμένη πύλη μοντέλου δεν απενεργοποιεί ποτέ τον πάροχο και δεν δημιουργεί ποτέ μόνιμο αποκλεισμό μοντέλου· η συμπεριφορά ανάντη 429/περιόδου αναμονής/εφεδρικής μετάβασης παραμένει ο τελικός μηχανισμός αντιμετώπισης σφαλμάτων. - **Εμβέλεια ανά σύνδεση και ανά διεργασία:** τα όρια ισχύουν ανά σύνδεση βάσης δεδομένων και διατηρούνται στη μνήμη, επομένως δύο συνδέσεις που επαναχρησιμοποιούν το ίδιο ανάντη κλειδί API δεν συντονίζονται μεταξύ τους. - **Η απουσία διαμόρφωσης δεν επιφέρει αλλαγές:** η παράλειψη του χάρτη (ή το κενό πεδίο στον πίνακα ελέγχου) δεν προσθέτει πύλη μοντέλου. Παράδειγμα διαμόρφωσης χωρίς να επιβάλλεται κάποιο καθολικό όριο παρόχου: ```text glm-5=1 glm-4.7=3 ``` ### Σειριοποίηση αιτημάτων quota-share Όταν μια αποστολή quota-share στοχεύει μια σύνδεση που δηλώνει θετικό `max_concurrent`, τα ταυτόχρονα αιτήματα προς αυτόν τον **λογαριασμό** σειριοποιούνται μέσω ενός σηματοφορέα ανά σύνδεση (κλειδί `qsconn:`): τα επιπλέον αιτήματα **περιμένουν στην ουρά** αντί να υπερφορτώνουν τον λογαριασμό. Λειτουργεί με **fail-open** — ένας κορεσμένος σηματοφορέας ή η λήξη χρονικού ορίου επιτρέπει τη συνέχιση χωρίς θέση, αντί να απορρίψει ποτέ ένα αίτημα που μπορεί να αποσταλεί. Ενεργοποιήστε ή απενεργοποιήστε το από **Ρυθμίσεις → Ανθεκτικότητα → Ταυτόχρονα αιτήματα ανά σύνδεση quota-share** (`resilienceSettings.quotaShareConcurrencyLimit.enabled`, ενεργοποιημένο από προεπιλογή). Χωρίς όριο `max_concurrent`, η συμπεριφορά παραμένει αμετάβλητη. > Η πύλη δρομολόγησης quota-share (`selectQuotaShareTarget`, DRR + P2C) λειτουργεί και η ίδια > με fail-open και απλώς _υποβαθμίζει την προτεραιότητα_ μιας σύνδεσης που έχει φτάσει το όριο — με μια > ομάδα μίας μόνο σύνδεσης δεν μπορεί να επιβάλει αυστηρό όριο, επομένως αυτός ο σηματοφορέας είναι εκείνος που στην πράξη > περιορίζει την πλημμυρίδα αιτημάτων. ### Επανάληψη με επίγνωση της περιόδου αναμονής συνδυασμού Για κάθε στρατηγική συνδυασμού (όταν είναι ενεργοποιημένη), ένα αίτημα που θα οριστικοποιούσε ένα 429 λόγω ΣΥΝΤΟΜΗΣ παροδικής περιόδου αναμονής περιμένει μέχρι τη λήξη της και αποστέλλεται ξανά, αντί να επιστρέψει το 429 — αυτό καλύπτει παράθυρα TPM/RPM τύπου Gemini (~60s retry-after) σε συνδυασμούς πολλών μοντέλων, π.χ. όταν και οι δύο στόχοι ενός συνδυασμού 2 μοντέλων φτάνουν ένα όριο ρυθμού ανά μοντέλο. Περιορίζεται από το `comboCooldownWait` (`enabled`, `maxWaitMs`, `maxAttempts`, `budgetMs`) στις **Ρυθμίσεις → Ανθεκτικότητα**. Δεν περιμένει ποτέ για `quota_exhausted` (κλειδωμένο μέχρι τα μεσάνυχτα) ή για αιτίες που σχετίζονται με έλεγχο ταυτότητας/μη εύρεση. Η ίδια πύλη καλύπτει την περίπτωση πριν από την αποστολή (#16319): όταν κάθε στόχος ενός συνδυασμού παραλείπεται πριν πραγματοποιηθεί οποιαδήποτε προσπάθεια, επειδή το μοντέλο του βρίσκεται σε σύντομο παροδικό αποκλεισμό, η διαδρομή μηδενικών προσπαθειών περιμένει τη λήξη του αποκλεισμού, σύμφωνα με τα ίδια ανώτατα όρια, και πραγματοποιεί εκ νέου αποστολή, αντί να απαντήσει με ένα `503 ALL_TARGETS_SKIPPED` χωρίς υπόδειξη. Όταν δεν επιτρέπεται αναμονή (εξάντληση ορίου, προσπαθειών ή προϋπολογισμού, ή απενεργοποιημένη λειτουργία), η τελική απόκριση αναβαθμίζεται σε `429 rate_limit_error` και περιλαμβάνει τα `retry_after` / `reset_at` από την ενωρίτερη λήξη παροδικού αποκλεισμού· οι αποκλεισμοί λόγω ορίου, ελέγχου ταυτότητας και μη εύρεσης διατηρούν το 503 χωρίς υπόδειξη. ```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. Έλεγχος Εισαγωγής στην Ουρά Αιτημάτων (v3.8.49 · ζήτημα #6593) **Πεδίο εφαρμογής**: η τοπική ουρά περιορισμού ρυθμού ανά πάροχο+σύνδεση (`open-sse/services/rateLimitManager.ts`, η οποία υποστηρίζεται από το Bottleneck), ένα επίπεδο κάτω από τους τρεις παραπάνω μηχανισμούς. **Το `maxWaitMs` οριοθετεί την αναμονή στην ουρά· το `executionMaxWaitMs` οριοθετεί την εκτέλεση.** Τα δύο είναι σκόπιμα ξεχωριστά και κανένα δεν επηρεάζει το άλλο. Το `resilienceSettings.requestQueue.maxWaitMs` είναι το **χρονικό περιθώριο αναμονής στην ουρά**: καλύπτει την αναμονή για μια θέση στον πάροχο και, στη συνέχεια, την παραμονή σε κατάσταση QUEUED, ενώ ο χρονοδιακόπτης του μηδενίζεται τη στιγμή που η εργασία εξέρχεται από την κατάσταση QUEUED και αρχίζει να εκτελείται (`rateLimitManager.ts`, `wrappedFn`). Ένα αίτημα που υπερβαίνει αυτό το όριο δεν φτάνει ποτέ στο upstream. Η προεπιλογή είναι 30000ms, παρέχεται από το `DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS` στο `src/lib/resilience/settings.ts` και κατοχυρώνεται από το `tests/unit/ratelimit-admission-control-6593.test.ts`, ώστε μια αλλαγή της να προκαλεί αποτυχία αυτής της δοκιμής, αντί να αφήνει αυτήν την παράγραφο σιωπηρά παρωχημένη. Το `resilienceSettings.requestQueue.executionMaxWaitMs` είναι αυτό που λαμβάνει το Bottleneck ως `expiration` της εργασίας, του οποίου ο χρονοδιακόπτης ξεκινά μόνο μετά την αποστολή. Αποτελεί δικλείδα ασφαλείας για εκτελεστές που δεν διαθέτουν δικό τους upstream χρονικό όριο και αυξάνεται στο χρονικό όριο έναρξης fetch του ίδιου του εκτελεστή όταν εκείνο είναι μεγαλύτερο, ώστε να μην μπορεί να διακόψει μια υγιή απόκριση που βρίσκεται σε εξέλιξη. Η προεπιλογή είναι 600000ms (10 λεπτά). Η διοχέτευση του χρονικού περιθωρίου ουράς στο `expiration` ήταν αυτό που προηγουμένως τερμάτιζε τις μη επαυξητικές πύλες κατά τη διάρκεια της εκτέλεσης — δικαιολογημένα εκτελούνται για αρκετά λεπτά πριν από τα πρώτα byte — και γι’ αυτό μια λήξη εμφανίζεται ως `code: "RATE_LIMIT_EXECUTION_TIMEOUT"` (HTTP 504), ενώ το χρονικό περιθώριο ουράς φέρει τον κωδικό χρονικού ορίου ουράς. Παρακάμψτε οποιοδήποτε από τα δύο μέσω των `RATE_LIMIT_MAX_WAIT_MS` / `RATE_LIMIT_EXECUTION_MAX_WAIT_MS` (περιβάλλον) ή από τον πίνακα ελέγχου (**Ρυθμίσεις → Ανθεκτικότητα**). Και τα δύο περιορίζονται σε 1ms–24h κατά την κανονικοποίηση. **Προτεραιότητα, και για τα δύο:** η μεταβλητή περιβάλλοντος παρέχει μόνο την _προεπιλογή_. Μια τιμή που έχει αποθηκευτεί μόνιμα στο `resilienceSettings.requestQueue` (πίνακας ελέγχου / ενημέρωση API, αποθηκευμένη στο `key_value`) υπερισχύει αυτής, ενώ ένα ανά σύνδεση `rateLimitOverrides.maxWaitMs` / `.executionMaxWaitMs` υπερισχύει και εκείνης. Επομένως, ο ορισμός της μεταβλητής περιβάλλοντος σε μια εγκατάσταση που διαθέτει ήδη μια μόνιμα αποθηκευμένη τιμή δεν αλλάζει τίποτα — αντ’ αυτού, διαγράψτε ή ενημερώστε την αποθηκευμένη ρύθμιση. Η παραμονή στην ουρά οριοθετείται από το `maxWaitMs`· το παρακάτω `maxQueueDepth` οριοθετεί πόσοι καλούντες μπορούν να βρίσκονται ταυτόχρονα στην ουρά. **Επαναλήψεις που δηλώνονται στο σώμα: ρύθμιση ρυθμού έναντι αποκλεισμού.** Ένα σώμα 429 μπορεί να ορίζει τον δικό του χρόνο αναμονής (Gemini `RetryInfo.retryDelay`, κείμενο "retry after Ns"), τον οποίο διαβάζει το `updateFromResponseBody`. Μια αναμονή που χωρά εντός του `maxWaitMs` της σύνδεσης αποτελεί ρύθμιση ρυθμού: ο περιοριστής αδειάζει το απόθεμά του για αυτό το χρονικό διάστημα και οι κλήσεις στην ουρά αποστέλλονται όταν αυτό αναπληρωθεί. Μια αναμονή μεγαλύτερη από το `maxWaitMs` δεν είναι κάτι που μπορεί απλώς να περιμένει η ουρά — το Gemini αποκρίνεται σε ένα εξαντλημένο ημερήσιο όριο με `retryDelay: "86399s"`. Το αναμεταδιδόμενο `Retry-After` στην ανακατασκευασμένη απόκριση 429 (#16528) ήδη κάνει τον εφεδρικό μηχανισμό να κλειδώνει το μοντέλο για αυτό το χρονικό διάστημα, αλλά η ταυτόχρονη παύση του περιοριστή θα εξακολουθούσε να καθυστερεί οποιαδήποτε κλήση φτάνει νωρίς στο κλειδί (για παράδειγμα, μετά από επαναφορά ανθεκτικότητας) για ολόκληρο το `maxWaitMs`. Όταν μια τέτοια αναμονή προέρχεται από μια τυποποιημένη λεπτομέρεια Google `RetryInfo` (την προέλευση `google_rpc_retry_info`) και ο περιοριστής του κλειδιού έχει ήδη πεδίο εφαρμογής το μοντέλο (δείτε `getLimiterKey`), η απόκριση 429 μετατρέπεται σε αποκλεισμό του μοντέλου σε εκείνη τη σύνδεση (`lockModelForLongBodyRetry` στο `open-sse/services/rateLimitManager/bodyRetryPause.ts`, με ανώτατο όριο τις 24h), ενώ ο περιοριστής δεν τίθεται σε παύση. Η επιλογή λογαριασμού παρακάμπτει το μοντέλο κατά τη διάρκεια αυτού του κλειδώματος μέσω του `isModelLocked()`, όπως και η πύλη προαποστολής συνδυασμού για στόχους που είναι σταθερά αντιστοιχισμένοι στη σύνδεση. Στο antigravity, όπου ο περιοριστής εφαρμόζεται ανά οικογένεια ορίων, το κλείδωμα αφορά το ακριβές μοντέλο, όπως και τα υπόλοιπα κλειδώματα ορίων του. Το όνομα που ζήτησε ο πελάτης κλειδώνεται επίσης όταν ένα ψευδώνυμο επιλύθηκε σε εκείνο το μοντέλο, επειδή αυτοί οι έλεγχοι χρησιμοποιούν το ζητούμενο όνομα· κλειδώνεται με ακρίβεια (`lockExactModel`), επομένως ένα ψευδώνυμο που διασχίζει μια οικογένεια ορίων του antigravity ή ένα πεδίο εφαρμογής codex δεν κλειδώνει ποτέ το ίδιο. Μια υπόδειξη σε φυσική γλώσσα, ένα μη τυποποιημένο `retryDelay` ή ένας περιοριστής κοινός για ολόκληρη τη σύνδεση διατηρεί την παύση. Όταν ο περιοριστής που μόλις ανακατασκεύασε η διαδρομή 429 τίθεται πράγματι σε παύση, ένας καλών του οποίου ο υπολειπόμενος χρόνος στην ουρά λήγει πριν τελειώσει η παύση απορρίπτεται αμέσως με τον κωδικό χρονικού ορίου ουράς (`checkLimiterPause`), αντί να περιμένει να παρέλθει αυτός ο χρόνος· η καταγεγραμμένη λήξη αγκυρώνεται πριν δημιουργηθεί ο περιοριστής, στο σημείο όπου το Bottleneck ξεκινά το ρολόι ανανέωσής του, και οποιαδήποτε μεταγενέστερη αλλαγή στο απόθεμα την ακυρώνει. Καλύπτεται από τα `tests/unit/rate-limit-long-body-retry-lockout.test.ts` και `tests/unit/rate-limit-body-retry-pause.test.ts`. **`maxQueueDepth` — προαιρετικό όριο εισαγωγής (νέο).** Το `resilienceSettings.requestQueue.maxQueueDepth` οριοθετεί πόσα αιτήματα μπορούν να βρίσκονται στην ουρά (χωρίς να έχουν αποσταλεί ακόμη) ταυτόχρονα για έναν πάροχο+σύνδεση. Όταν η ουρά περιέχει ήδη `maxQueueDepth` αιτήματα, ένα νέο αίτημα απορρίπτεται άμεσα με ένα τυποποιημένο σφάλμα `code: "RATE_LIMIT_QUEUE_FULL"` **πριν** φτάσει ποτέ στο `limiter.schedule()` — επομένως, η απόρριψη έχει χαμηλό κόστος και πραγματοποιείται πριν από οποιαδήποτε μεταγενέστερη εργασία συμπίεσης / μετάφρασης προτροπής για αυτό το αίτημα. Προεπιλογή `0` = απενεργοποιημένο, διατηρώντας την υπάρχουσα συμπεριφορά ουράς χωρίς όριο· περιορίζεται σε 0–100000. Παράκαμψη μέσω του `RATE_LIMIT_MAX_QUEUE_DEPTH` (περιβάλλον) ή του `resilienceSettings.requestQueue.maxQueueDepth` (πίνακας ελέγχου/ενημέρωση API). Ο ίδιος ο έλεγχος εισαγωγής είναι αμιγής συνάρτηση (`open-sse/services/rateLimitManager/admission.ts::checkQueueAdmission`), ώστε να μπορεί να υποβληθεί σε δοκιμές μονάδας χωρίς πραγματικό περιοριστή Bottleneck. > Το RFC που άνοιξε το #6593 πρότεινε επίσης μια σημαία `bypassCompressionOnRateLimit`. > Η διοχέτευση `open-sse/services/compression/` αυτού του repo αφορά τη συμπίεση > prompt/context στο εξερχόμενο αίτημα προς το LLM (`chatCore.ts`, περίπου στο > μπλοκ `resolveCompressionSettings`/`selectCompressionStrategy`), όχι τη συμπίεση > απόκρισης HTTP σε συντεθειμένα σώματα 429 — δεν υπάρχει αντίστοιχη διαδρομή > κώδικα για μια κυριολεκτική σημαία παράκαμψης. Αυτό το βήμα συμπίεσης prompt > εκτελείται επίσης επί του παρόντος _πριν_ από το `withRateLimit()` στη διοχέτευση > αιτημάτων, επομένως η αναδιάταξη ώστε να παραλείπεται σε περίπτωση απόρριψης > λόγω πλήρους ουράς αποτελεί ξεχωριστή, μεγαλύτερη αλλαγή από το πεδίο εφαρμογής > αυτού του ζητήματος· **δεν** υλοποιήθηκε σκόπιμα εδώ και παραμένει ως επόμενο > βήμα, εφόσον η εξοικονόμηση CPU αξίζει τον κίνδυνο της αναδιάταξης. ### Δίχτυ ασφαλείας αυτόματης ενεργοποίησης ορίου ρυθμού & προστασία σύνδεσης (`rateLimitProtection`) **Δίχτυ ασφαλείας αυτόματης ενεργοποίησης** (`resilienceSettings.requestQueue.autoEnableApiKeyProviders`, προεπιλογή `true`): Από προεπιλογή, το OmniRoute ενεργοποιεί αυτόματα την προστασία ουράς ορίου ρυθμού για όλες τις ενεργές συνδέσεις παρόχων με κλειδί API (`getProviderCategory(provider) === "apikey"`). Αυτό διασφαλίζει ότι τα εισερχόμενα αιτήματα προς κλειδιά API ανάντη υπηρεσιών που είναι ευαίσθητα στον ρυθμό εισαγωγής τίθενται σε ουρά και περιορίζονται μέσω του Bottleneck, αντί να αποστέλλονται μαζικά και να προκαλούν σφάλματα 429 στις ανάντη υπηρεσίες. Το δίχτυ ασφαλείας μπορεί να παρακαμφθεί μέσω της μεταβλητής περιβάλλοντος `RATE_LIMIT_AUTO_ENABLE` (`false`/`0`/`off` το απενεργοποιεί, `true`/`1`/`on` επιβάλλει την ενεργοποίησή του), η οποία υπερισχύει της αποθηκευμένης ρύθμισης του πίνακα ελέγχου (**Ρυθμίσεις → Ανθεκτικότητα** / `PATCH /api/resilience`). **Σημασιολογία του `rateLimitProtection` ανά σύνδεση:** Η προστασία ορίου ρυθμού ανά σύνδεση παρακολουθείται μέσω του πεδίου boolean `rateLimitProtection` στις συνδέσεις παρόχων (αποθηκεύεται ως `rate_limit_protection` στο SQLite): - **Ρητή ενεργοποίηση (`rateLimitProtection === true`)**: Η προστασία ορίου ρυθμού ενεργοποιείται ρητά για αυτήν τη σύνδεση, ανεξάρτητα από την κατηγορία παρόχου ή από το αν είναι ενεργοποιημένο το δίχτυ ασφαλείας αυτόματης ενεργοποίησης. - **Ρητή απενεργοποίηση (`rateLimitProtection === false`)**: Απενεργοποιεί τη ρητή προστασία, αλλά **δεν** εξαιρεί μια ενεργή σύνδεση με κλειδί API από το καθολικό δίχτυ ασφαλείας αυτόματης ενεργοποίησης. Στο `reconcileEnabledConnections` (`open-sse/services/rateLimitManager.ts`), το `false` και το `undefined` είναι ισοδύναμα: μόνο το `rateLimitProtection === true` αντιμετωπίζεται ως ρητή παράκαμψη. Αν το `rateLimitProtection` είναι `false` (ή δεν έχει οριστεί), μια ενεργή σύνδεση με κλειδί API εξακολουθεί να πληροί τη συνθήκη αυτόματης ενεργοποίησης και παραμένει προστατευμένη με όριο ρυθμού. - **Δεν υπάρχει εξαίρεση ανά σύνδεση**: Προς το παρόν, ένας διαχειριστής δεν μπορεί να ορίσει ότι «αυτή η συγκεκριμένη ενεργή σύνδεση με κλειδί API πρέπει να λειτουργεί χωρίς προστασία ουράς ορίου ρυθμού», διατηρώντας παράλληλα ενεργή την αυτόματη ενεργοποίηση για τις υπόλοιπες. Για να λειτουργούν πλήρως οι συνδέσεις με κλειδί API χωρίς ουρές ορίου ρυθμού, το δίχτυ ασφαλείας πρέπει να απενεργοποιηθεί καθολικά (`RATE_LIMIT_AUTO_ENABLE=false` ή `resilienceSettings.requestQueue.autoEnableApiKeyProviders = false`). **Υποστηριζόμενες διαδρομές εγγραφής:** - **`POST /api/rate-limits`**: Η υποστηριζόμενη διαδρομή εγγραφής διαχείρισης για την εναλλαγή του `rateLimitProtection` μιας σύνδεσης (`{ connectionId: string, enabled: boolean }`). Η κλήση της με `enabled: true` ενεργοποιεί τη ρητή προστασία· η κλήση της με `enabled: false` καταργεί τη ρητή προστασία (αν και οι ενεργές συνδέσεις με κλειδί API εξακολουθούν να καλύπτονται από το καθολικό δίχτυ ασφαλείας, εφόσον είναι ενεργή η αυτόματη ενεργοποίηση). - **`PUT /api/providers/[id]`**: Σκοπίμως **δεν** αποδέχεται ούτε αλλάζει το `rateLimitProtection` (#11278). Η ενημέρωση των μεταδεδομένων σύνδεσης παρόχου μέσω του `PUT /api/providers/[id]` δεν θα αλλάξει ποτέ την αποθηκευμένη κατάσταση προστασίας ορίου ρυθμού. --- ## 6. Μηχανισμός επιτήρησης ρυθμαπόδοσης αργής ροής (#9709) Ο προαιρετικός μηχανισμός προστασίας `resilienceSettings.streamRecovery.throughputWatchdog` εντοπίζει μια upstream υπηρεσία που εξακολουθεί να στέλνει τμήματα δεδομένων, αλλά παράγει έξοδο βοηθού κάτω από τον διαμορφωμένο ρυθμό χρήσιμης εξόδου. Είναι σκόπιμα διακριτός από το χρονικό όριο αδράνειας: τα heartbeat και τα μεταδεδομένα δεν επαναφέρουν κανέναν από τους δύο χρονομετρητές και δεν υπολογίζονται ως πρόοδος. Είναι επίσης διακριτός από την ανελαστική προθεσμία προσπάθειας (#9153), η οποία παραμένει ένα απόλυτο ανώτατο όριο ασφαλείας ανεξάρτητα από την ποιότητα της εξόδου. Ο μηχανισμός επιτήρησης απαιτεί μια περίοδο προθέρμανσης, ακολουθούμενη από ένα πλήρες κυλιόμενο παράθυρο, προτού μπορέσει να διακόψει. Μετρά τις διαφορές κειμένου από συμβάντα εξόδου των Chat Completions και Responses API (μια συντηρητική προσέγγιση βάσει byte UTF-8), αγνοεί συμβάντα που περιέχουν μόνο στοιχεία χρήσης ή είναι κενά και αναστέλλει την αξιολόγηση όσο βρίσκονται σε εξέλιξη συμβάντα κλήσης εργαλείων ή συλλογισμού. Είναι απενεργοποιημένος από προεπιλογή και μπορεί να ενεργοποιηθεί με `STREAM_THROUGHPUT_WATCHDOG_ENABLED=true`· το παράθυρο, η προθέρμανση, ο ελάχιστος ρυθμός και η ελάχιστη μετρήσιμη έξοδος οριοθετούνται από το κανονικό επίπεδο κανονικοποίησης των ρυθμίσεων ανθεκτικότητας. Όταν είναι ενεργοποιημένη, μια διακοπή από τον μηχανισμό επιτήρησης εφαρμόζεται μόνο στην ενεργή upstream προσπάθεια. Πριν από οποιαδήποτε byte ορατά στον πελάτη, η υπάρχουσα διαδρομή έγκαιρης ανάκτησης στον ίδιο λογαριασμό μπορεί να ανοίξει ξανά την προσπάθεια. Μετά την οριστικοποίηση, η ροή δεν αναπαράγεται ποτέ στα τυφλά· μόνο η υπάρχουσα σύμβαση ασφαλούς συνέχισης στο μέσο της ροής μπορεί να συρράψει ένα επίθημα. Η ολοκλήρωση παραμένει μίας εκτέλεσης, ώστε η λογιστική χρήσης και η αποδέσμευση του semaphore να μην επαναλαμβάνονται. --- ## 7. Επαναδιατύπωση Upstream Κατάστασης (εσφαλμένα διατυπωμένα σφάλματα ορίου χρήσης) **Πεδίο εφαρμογής:** μία upstream πύλη που αναφέρει προσωρινή εξάντληση ορίου χρήσης με εσφαλμένη HTTP κατάσταση. **Σκοπός:** διόρθωση μιας παραπλανητικής κατάστασης ΠΡΙΝ από την ταξινόμηση, ώστε οι downstream καταναλωτές (μηχανισμός fallback, συγκέντρωση combo, απόκριση προς τον πελάτη) να βλέπουν την πραγματική, επαναλήψιμη φύση της αποτυχίας. Ορισμένες πύλες σηματοδοτούν την ΠΡΟΣΩΡΙΝΗ εξάντληση ορίου χρήσης με μια μη επαναλήψιμη HTTP κατάσταση. Το `agentrouter.org` επιστρέφει `403` (μερικές φορές `400`) με κινεζικό σώμα (`用户额度不足` / `额度不足`) αντί για το τυπικό `429`. Πελάτες όπως το Claude Code αντιμετωπίζουν το `403` ως μόνιμο και διακόπτουν τη συνεδρία, ενώ χωρίς διόρθωση ο μηχανισμός fallback θα το ταξινομούσε ως `AUTH_ERROR` αντί για συμβάν ορίου χρήσης. **Υλοποίηση:** - Μητρώο + αντιστοιχιστής: `open-sse/config/upstreamStatusRestatement.ts` — μια λίστα κανόνων ανά πάροχο (`{id, fromStatuses, toStatus, textMarkers, excludeMarkers, defaultRetryAfterMs}`), οι οποίοι αντιστοιχίζονται μέσω του `applyStatusRestatement()`. - Σημείο κλήσης: το μπλοκ `providerFailure:` στο `open-sse/handlers/chatCore.ts` (περίπου στη γραμμή 3654), αμέσως αφού το `parseUpstreamError()` αναλύσει μια upstream απόκριση με κατάσταση σφάλματος HTTP (`!providerResponse.ok`) και πριν εκτελεστεί οποιαδήποτε ταξινόμηση, ώστε κάθε downstream καταναλωτής να βλέπει τη διορθωμένη κατάσταση. Τα σφάλματα που είναι ενσωματωμένα σε μια ροή SSE με `200` ακολουθούν μια ξεχωριστή, μεταγενέστερη διαδρομή ανάλυσης ροής και **δεν** καλύπτονται επί του παρόντος από αυτό το hook — ένας γνωστός περιορισμός, ο οποίος δεν είναι ακόμη απαραίτητος για την εσφαλμένη κατάσταση του agentrouter (η οποία εμφανίζεται ως κατάσταση σφάλματος HTTP). - Καταλληλότητα για επανάληψη: το `429` περιλαμβάνεται στο `RETRY_AFTER_ELIGIBLE_STATUSES` (`open-sse/services/combo/unavailableRetryGate.ts`), επομένως ένα επαναδιατυπωμένο σφάλμα φέρει ένα πραγματικό παράθυρο επανάληψης αντί να εμφανίζεται ως ανενεργό `403`. - Το συνθετικό `60s` του `defaultRetryAfterMs` (`upstreamStatusRestatement.ts`) είναι μόνο αυτό που γνωστοποιεί η επαναδιατυπωμένη απόκριση στον **πελάτη**· δεν αποτελεί το ίδιο την εσωτερική διάρκεια cooldown/αποκλεισμού της σύνδεσης — αυτή διέπεται ξεχωριστά από τον εκάστοτε μηχανισμό που χειρίζεται πραγματικά το επαναδιατυπωμένο σφάλμα (το κλιμακούμενο backoff του Connection Cooldown, §2, με βάση `3s` για παρόχους API key· ή το Model Lockout, §3, για παρόχους ορίου χρήσης ανά μοντέλο όπως ο agentrouter). Ο δρομολογητής μπορεί να καταστεί κατάλληλος για εσωτερική επανάληψη νωρίτερα από το παράθυρο των 60s που γνωστοποιεί στον πελάτη — πρόκειται για σκόπιμο περιθώριο, όχι για σφάλμα. Τα μόνιμα σφάλματα (το `无权访问模型` του agentrouter — απουσία πρόσβασης σε αυτό το μοντέλο) ΔΕΝ επαναδιατυπώνονται ΠΟΤΕ: το `excludeMarkers` ακυρώνει τον κανόνα ακόμη και όταν υπάρχει αντιστοιχία με το `textMarkers`, ώστε το σφάλμα να διατηρεί την αρχική του κατάσταση και τίποτα να μην το επαναλαμβάνει επ' αόριστον. Ο αντίστοιχος κανόνας ταξινόμησης παρόχου (`agentrouter-model-access-denied` στο `open-sse/config/providerErrorRules.ts`: `reason: "auth_error"`, `scope: "model"`, δηλωμένο βασικό cooldown `6h`) εξετάζεται από το `checkFallbackError` (`open-sse/services/accountFallback.ts`) _πριν_ από τη γενική πρόωρη επιστροφή `FORBIDDEN` της κατηγορίας apikey, με έλεγχο μέσω του `honorsRuleLockScope(provider)` (#10334 — προς το παρόν αποκλειστικά για τον agentrouter μέσω της λίστας επιτρεπόμενων `HONORS_RULE_LOCK_SCOPE_PROVIDERS` στο `providerErrorRules.ts`). Το δηλωμένο cooldown 6h του κανόνα μεταβιβάζεται ως `fallbackResult.baseCooldownMs`, αλλά εξακολουθεί να τροφοδοτεί την προϋπάρχουσα διαδρομή αποκλεισμού ορίου χρήσης ανά μοντέλο (`lockModelIfPerModelQuota()` / `recordModelLockoutFailure()`, αμετάβλητη από το #10334 εκτός από την πηγή του cooldown): περιορίζεται προς τα κάτω στο `mlSettings.maxCooldownMs` του διαχειριστή (προεπιλογή `1_800_000ms` / 30min), όπως κάθε άλλος αποκλεισμός μοντέλου, και η _αποθηκευμένη αιτία αποκλεισμού_ παραμένει η προϋπάρχουσα, ενσωματωμένη τιμή `"forbidden"`, όχι η τιμή `"auth_error"` του κανόνα — μόνο η διάρκεια του cooldown τηρείται από άκρο σε άκρο, όχι η συμβολοσειρά της αιτίας. Η ίδια η σύνδεση παραμένει ενεργή· τα συγγενικά μοντέλα στην ίδια σύνδεση δεν επηρεάζονται. Τα σφάλματα ορίου χρήσης που επαναδιατυπώνονται (`额度不足`) αντιστοιχούν σε έναν κανόνα παρόχου στην παραγωγή (`agentrouter-user-quota-exhausted`: `reason: "quota_exhausted"`, `scope: "connection"`, χωρίς δικό του δηλωμένο χρόνο αναμονής — εφαρμόζεται η προεπιλεγμένη κλιμακούμενη καθυστέρηση επανάληψης του επιπέδου διατήρησης). Από το #10334 και μετά, το `scope` στο `ProviderErrorRuleMatch` χρησιμοποιείται από άκρο σε άκρο, αλλά **μόνο** για παρόχους στη λίστα επιτρεπόμενων `HONORS_RULE_LOCK_SCOPE_PROVIDERS` (`providerErrorRules.ts` — προς το παρόν μόνο ο `"agentrouter"`, με έλεγχο μέσω `honorsRuleLockScope()`). Για κάθε άλλο πάροχο, το `scope` παραμένει πληροφοριακό, ακριβώς όπως πριν από το #10334. Το `checkFallbackError` εκθέτει το scope του κανόνα που αντιστοιχίστηκε ως `fallbackResult.ruleScope`· το `isAgentrouterConnectionQuotaScope()` (`src/sse/services/auth.ts`) είναι ο κοινόχρηστος έλεγχος που επιβεβαιώνει ότι ένα `ruleScope` είναι πράγματι ασφαλές να αντιμετωπιστεί ως σήμα που αφορά ολόκληρη τη σύνδεση και αποκαθίσταται αυτόματα (scope `"connection"`, reason `quota_exhausted`, ποτέ `permanent`, ποτέ `creditsExhausted` — μια άμυνα απέναντι σε έναν μελλοντικό κανόνα που θα συνδυάζει το scope `"connection"` με μια μόνιμη κατάσταση λογαριασμού). Δύο καταναλωτές τον καλούν: - **Διατήρηση** (`markAccountUnavailable()`, `src/sse/services/auth.ts`): αντί να καταλήξει στον κλάδο αποκλεισμού **ανά μοντέλο** του παρόχου διέλευσης (το agentrouter έχει `passthroughModels: true` → το `hasPerModelQuota()` επιστρέφει `true`), εφαρμόζει έναν **προσωρινό χρόνο αναμονής σύνδεσης** — `testStatus: "unavailable"` + `rateLimitedUntil`, ποτέ μια τελική κατάσταση (`credits_exhausted`/`banned`/`expired`) — ώστε η σύνδεση να αποκαθίσταται αυτόματα μόλις λήξει ο χρόνος αναμονής, αντί να απαιτεί μη αυτόματη επαναφορά των διαπιστευτηρίων. Παραλείπεται για συνδέσεις με `disableCooling: true` (#2997): αυτή η εξαίρεση καταλήγει στον αποκλεισμό ανά μοντέλο (ένας τεκμηριωμένος συμβιβασμός — δείτε το σχόλιο κώδικα πάνω από τον κλάδο). - **Συνδυαστική δρομολόγηση στο ίδιο αίτημα** (`applyComboTargetExhaustion()`, `open-sse/services/combo/targetExhaustion.ts`): ο ίδιος έλεγχος επισημαίνει τη σύνδεση στο σύνολο `exhaustedConnections` στη μνήμη, με κλειδί `${provider}:${connectionId}`. Αυτό παραλείπει μόνο έναν εναπομείναντα στόχο ΤΟΥ ΙΔΙΟΥ ΑΙΤΗΜΑΤΟΣ που _φέρει ήδη ο ίδιος ακριβώς αυτό το `connectionId`_ στο δικό του αντικείμενο στόχου (`getExhaustedTargetSkipReason()`, `open-sse/services/combo/comboPredicates.ts`, `if (provider && connectionId)` πριν από την αναζήτηση στο `exhaustedConnections`) — ένας απλός συνδυασμός λίστας μοντέλων, όπου οι συγγενικοί στόχοι δεν φέρουν δικό τους καρφιτσωμένο `connectionId` και ένα τέτοιο αναγνωριστικό επιλύεται μόνο ανά αποστολή από την κεφαλίδα `X-OmniRoute-Selected-Connection-Id` της απόκρισης, δεν αντιστοιχεί ποτέ σε αυτό το κλειδί. Για αυτήν τη συνηθισμένη περίπτωση, η πραγματική προστασία από την επαναχρησιμοποίηση του λογαριασμού που μόλις εξάντλησε το όριό του από ένα εναπομείναν σκέλος ΔΕΝ είναι αυτό το Set — είναι το παραπάνω επίπεδο διατήρησης (το `rateLimitedUntil` της σύνδεσης βρίσκεται πλέον στο μέλλον) σε συνδυασμό με τον ίδιο αυτό έλεγχο που καταστέλλει το `transientRateLimitedProviders` για την αποτυχία (δείτε «Σχεδιασμός δύο σταδίων» και το σχόλιο κώδικα στον κλάδο `isAgentrouterConnectionQuotaScope` στο `targetExhaustion.ts`): καθώς αυτό το Set παραμένει χωρίς επισήμανση, η εξαναγκασμένη αποδοχή του `allowRateLimitedConnection` στο `combo.ts` (`open-sse/services/combo.ts:1005-1013`, `:2734-2738`) ΔΕΝ ενεργοποιείται για τα εναπομείναντα σκέλη του παρόχου, επομένως το φίλτρο `rateLimitedUntil` της επιλογής διαπιστευτηρίων (`src/sse/services/auth.ts:1238`) τηρείται κανονικά και ένα εναπομείναν σκέλος είτε επιλέγει μια διαφορετική, ακόμη κατάλληλη σύνδεση agentrouter είτε αποτυγχάνει επειδή δεν υπάρχουν διαθέσιμα διαπιστευτήρια — δεν επιστρέφει εξαναγκαστικά στη σύνδεση για την οποία αυτός ο κλάδος μόλις εφάρμοσε χρόνο αναμονής. ### Σχεδιασμός δύο σταδίων: επαναδιατύπωση κατάστασης και, στη συνέχεια, ταξινόμηση Η επαναδιατύπωση κατάστασης (`upstreamStatusRestatement.ts`) και οι κανόνες ταξινόμησης παρόχων (`open-sse/config/providerErrorRules.ts`, `providerRuleRegistry`) είναι ξεχωριστά μητρώα που χρησιμοποιούν και τα δύο ως κλειδιά το αναγνωριστικό παρόχου και δείκτες κειμένου, αλλά εκτελούνται σε διαφορετικά σημεία και εξυπηρετούν διαφορετικούς σκοπούς: η επαναδιατύπωση ξαναγράφει την κατάσταση HTTP νωρίς στο `chatCore.ts`· οι κανόνες ταξινόμησης επιλέγουν το εναλλακτικό `reason` και το `scope` κλειδώματος (`model` / `provider` / `connection`) μέσα στο `checkFallbackError()` (`open-sse/services/accountFallback.ts`). Οι κανόνες ταξινόμησης βλέπουν το πλήρες **κείμενο** του σφάλματος (απαραίτητο για την αντιστοίχιση δεικτών σώματος όπως το `额度不足`) μόνο για παρόχους που περιλαμβάνονται στη λίστα επιτρεπόμενων `FULL_TEXT_RULE_PROVIDERS` στο `providerErrorRules.ts` — προς το παρόν μόνο ο `"agentrouter"`. Για κάθε άλλο πάροχο του **ενσωματωμένου καταλόγου**, το `checkFallbackError` παραδίδει στο `getProviderErrorRuleMatch` μόνο το δομημένο σφάλμα (`{code, type}`), το οποίο επαρκεί για κανόνες που βασίζονται σε κεφαλίδες/κατάσταση/κωδικό, αλλά δεν μπορεί να εντοπίσει δείκτες στο κείμενο του σώματος. Η βοηθητική συνάρτηση `resolveRuleMatchBody()` εκτελεί αυτήν την επιλογή: πλήρες κείμενο σφάλματος για τους παρόχους της λίστας επιτρεπόμενων και το δομημένο σφάλμα για τους υπόλοιπους. Η προσθήκη ενός **ενσωματωμένου** παρόχου στο `FULL_TEXT_RULE_PROVIDERS` αποτελεί ρητή ενεργοποίηση ανά πάροχο — υπάρχει ώστε η προεπιλεγμένη διαδρομή για κάθε πάροχο που δεν περιλαμβάνεται στη λίστα να παραμένει αμετάβλητη, byte προς byte. Το `scope` ενός κανόνα (`model` / `provider` / `connection`) αποτελεί ξεχωριστή ρητή ενεργοποίηση από το `FULL_TEXT_RULE_PROVIDERS`: το `checkFallbackError` το εκθέτει μόνο ως `fallbackResult.ruleScope`, και οι μεταγενέστεροι καταναλωτές το αντιμετωπίζουν ως οτιδήποτε άλλο πέρα από μια πληροφοριακή ετικέτα μόνο για παρόχους στη λίστα επιτρεπόμενων `HONORS_RULE_LOCK_SCOPE_PROVIDERS` στο ίδιο αρχείο (`με έλεγχο μέσω honorsRuleLockScope()` — προς το παρόν μόνο ο `"agentrouter"`). Δείτε την ενότητα «Επαναδιατυπωμένα σφάλματα ορίου χρήσης» παραπάνω για το τι κάνει πραγματικά μια αντιστοίχιση `scope: "connection"` όταν ένας πάροχος περιλαμβάνεται σε αυτήν τη λίστα επιτρεπόμενων. **#11104 — οι κανόνες που δηλώνονται από τον διαχειριστή παρακάμπτουν και τις δύο λίστες επιτρεπόμενων.** Ένας διαχειριστής μπορεί να δηλώσει κατά την εκτέλεση έναν κανόνα ανά πάροχο μέσω του `settings.providerErrorRules` (`open-sse/config/providerErrorRules.ts::setOperatorProviderErrorRules`) χωρίς να επεξεργαστεί αυτό το αρχείο. Η εξάρτηση ενός κανόνα διαχειριστή από τα `FULL_TEXT_RULE_PROVIDERS`/`HONORS_RULE_LOCK_SCOPE_PROVIDERS` — λίστες επιτρεπόμενων που προορίζονται για την προστασία της **προεπιλεγμένης** συμπεριφοράς των ενσωματωμένων κανόνων καταλόγου — θα καθιστούσε τον μηχανισμό ρυθμίσεων ανενεργό για κάθε πάροχο εκτός από όσους είναι ήδη καταχωρισμένοι εκεί, αφού η δήλωση του κανόνα αποτελεί ήδη ρητή επιλογή συμμετοχής από τον διαχειριστή. Τα `resolveRuleMatchBody()` και `honorsRuleLockScope()` ελέγχουν πρώτα το `hasOperatorRuleForProvider()`: ένας πάροχος με κανόνα διαχειριστή λαμβάνει το ανεπεξέργαστο κείμενο σφάλματος και το δηλωμένο `scope` του εφαρμόζεται, ανεξάρτητα από το αν εμφανίζεται επίσης σε κάποια από τις δύο λίστες επιτρεπόμενων. **Γνωστό κενό — δεν γίνεται ποτέ αναζήτηση στο `providerRuleRegistry` για HTTP 400.** Ο κλάδος `BAD_REQUEST` του `checkFallbackError` ταξινομεί την κατάσταση 400 εξ ολοκλήρου μέσω των δικών του πινάκων μοτίβων (`MODEL_ACCESS_DENIED_PATTERNS`, `CONTEXT_OVERFLOW_PATTERNS` κ.λπ. στο `accountFallback.ts`) και επιστρέφει πριν προσεγγιστεί ο κλάδος `configuredRule`/`getProviderErrorRuleMatch` που βρίσκεται παραπάνω. Ένας ενσωματωμένος κανόνας καταλόγου (ή ένας κανόνας διαχειριστή) με `status: 400` είναι συντακτικά έγκυρος, αλλά δεν θα ενεργοποιηθεί ποτέ. Κανένας υπάρχων κανόνας δεν στοχεύει σήμερα το 400, επομένως τίποτα στην παραγωγή δεν επηρεάζεται — αλλά ένας μελλοντικός κανόνας για το 400 απαιτεί πρώτα τροποποίηση αυτού του κλάδου, κάτι που αποτελεί μεγαλύτερη αλλαγή από την προσθήκη ενός κανόνα (επαναταξινομεί το 400 για κάθε πάροχο που βασίζεται ήδη στη συμπεριφορά των πινάκων μοτίβων) και βρίσκεται εκτός του πεδίου εφαρμογής μιας προσθήκης κανόνα για έναν μόνο πάροχο. ### Προσθήκη μιας νέας πύλης που δηλώνει εσφαλμένα το quota 1. Καταχωρίστε έναν πίνακα κανόνων στο `statusRestatementRegistry` (`open-sse/config/upstreamStatusRestatement.ts`). Διατηρήστε τα `textMarkers` ειδικά για τον πάροχο· μην επαναχρησιμοποιείτε γενικές αγγλικές φράσεις που συγκρούονται με τα `CREDITS_EXHAUSTED_SIGNALS` (`open-sse/services/accountFallback.ts`). 2. Προαιρετικά, καταχωρίστε κανόνες ταξινόμησης στο `open-sse/config/providerErrorRules.ts` (`providerRuleRegistry`) για να επιλέξετε το σωστό εύρος κλειδώματος (`connection` για quota σε επίπεδο λογαριασμού, `model` για σφάλματα ανά μοντέλο). Αυτό το βήμα τίθεται σε ισχύ στην παραγωγή μόνο για παρόχους των οποίων οι κανόνες χρειάζονται το πλήρες κείμενο σφάλματος (δείκτες σώματος): προσθέστε το αναγνωριστικό παρόχου στο `FULL_TEXT_RULE_PROVIDERS` στο ίδιο αρχείο — διαφορετικά, το `checkFallbackError` παρέχει στον κανόνα μόνο το δομημένο σφάλμα `{code, type}` και ένας κανόνας κειμένου σώματος δεν θα αντιστοιχιστεί ποτέ σε πραγματική κίνηση. Οι κανόνες που αντιστοιχίζονται αποκλειστικά βάσει `status`/`headers` (όπως εκείνοι του Opencode ή του Minimax) δεν χρειάζονται αυτήν τη ρητή ενεργοποίηση. Ξεχωριστά, εάν ο κανόνας δηλώνει `scope: "connection"` και η πρόθεση είναι μια πραγματική περίοδος αναμονής σε επίπεδο σύνδεσης μαζί με παράλειψη του συνδυασμού στο ίδιο αίτημα (και όχι απλώς μια ενημερωτική ετικέτα), προσθέστε το αναγνωριστικό παρόχου στο `HONORS_RULE_LOCK_SCOPE_PROVIDERS` στο ίδιο αρχείο — αυτό ελέγχει τη χρήση τύπου `isAgentrouterConnectionQuotaScope()` στο `markAccountUnavailable()` (`src/sse/services/auth.ts`) και στο `applyComboTargetExhaustion()` (`open-sse/services/combo/targetExhaustion.ts`)· χωρίς αυτό, το `scope` εξακολουθεί να μεταφέρεται μέσω του `fallbackResult.ruleScope`, αλλά τίποτα δεν ενεργεί βάσει αυτού. 3. Προσθέστε δοκιμές μονάδας κατά τα πρότυπα των `tests/unit/upstream-status-restatement.test.ts` και `tests/unit/agentrouter-error-rules.test.ts` (συμπεριλαμβανομένων των ελέγχων not-permanent / not-creditsExhausted και — εάν ο πάροχος χρειάζεται τη λίστα επιτρεπόμενων — μιας δοκιμής που επιβεβαιώνει ότι το `resolveRuleMatchBody()` επιστρέφει το πλήρες κείμενο μόνο για τον συγκεκριμένο πάροχο). Δεν απαιτούνται αλλαγές στα `chatCore.ts`, `classifyError` ή combo. #### Κλείδωμα ανά ομάδα εξόδου (#10880) Οι πάροχοι στο `EGRESS_BUCKETED_LOCK_PROVIDERS` (οικογένεια opencode) αντιμετωπίζονται ως upstream ομαδοποιημένοι ανά IP (το δωρεάν επίπεδο του opencode ομαδοποιείται ανά IP, όχι ανά λογαριασμό — βλ. #9611): μια κατάσταση 429 ταξινομημένη ως `quota_exhausted` **ή** `rate_limit_exceeded` θέτει σε περίοδο αναμονής κάθε σύνδεση της οικογένειας της λίστας επιτρεπόμενων της οποίας η τελευταία γνωστή IP εξόδου αντιστοιχεί σε εκείνη της σύνδεσης που απέτυχε, πριν η εναλλαγή προλάβει να τις δοκιμάσει — αποφεύγοντας N-1 upstream κλήσεις με εγγυημένη αποτυχία (ίδιο μοτίβο με τα #10460/#10525). Το `rate_limit_exceeded` συμπεριλαμβάνεται σκόπιμα: στη διαδρομή `markAccountUnavailable` οι ειδικοί κανόνες του opencode δεν αντιστοιχίζονται ποτέ (δεν παραδίδονται headers/σώμα στο `checkFallbackError`, το opencode δεν βρίσκεται στο `FULL_TEXT_RULE_PROVIDERS`), επομένως ένα 429 του οποίου το σώμα περιέχει το κείμενο quota συνδρομής ("επιτεύχθηκε το μηνιαίο όριο χρήσης") ταξινομείται ως `quota_exhausted` από τον εφεδρικό μηχανισμό κειμένου quota (`buildSubscriptionQuotaFallback`, `accountFallback.ts`· περίοδος αναμονής 1 ώρας) πριν προσεγγιστεί ποτέ ο κανόνας `status_429` — ενώ ένα 429 χωρίς κείμενο quota (απλός περιορισμός ρυθμού) ταξινομείται μέσω του κανόνα `status_429` ως `rate_limit_exceeded` και εξακολουθεί να θέτει την οικογένεια IP σε περίοδο αναμονής. Για έναν πάροχο της λίστας επιτρεπόμενων, ένας περιορισμός ρυθμού ομαδοποιημένος ανά IP αποτελεί το ίδιο σήμα με ένα εξαντλημένο quota. Πραγματικοί περιορισμοί: - **Βέλτιστη δυνατή προσπάθεια**: το κλείδωμα επιλύει την τελευταία γνωστή `egress_ip` της σύνδεσης από το `proxy_logs` (παράθυρο 24 ωρών, σύγχρονα, χωρίς cache). Ψυχρό cache (το egress IP δεν έχει ανιχνευθεί ποτέ) ή απουσία γραμμής → η σύνδεση που αποτυγχάνει εξακολουθεί να τίθεται σε cooldown από τον κλάδο (καταγράφεται όπως σήμερα), απλώς δεν κλειδώνεται καμία συγγενική σύνδεση. - **Ποτέ οριστικό**: το cooldown είναι ένα ανανεούμενο παράθυρο ορίου (`testStatus: "unavailable"`)· μια μόνιμη κατάσταση δεν συνάγεται ποτέ από ένα σήμα σε επίπεδο IP. Οι συνδέσεις `disableCooling` παρακάμπτουν πλήρως τον κλάδο. - **Αλλάζει η λεπτομέρεια κλειδώματος για την οικογένεια της λίστας επιτρεπόμενων**: πρόκειται για αλλαγή εμβέλειας και όχι μόνο για βελτιστοποίηση συγγενικών συνδέσεων. Το opencode είναι πάροχος `passthroughModels`, επομένως πριν από αυτόν τον κλάδο ένα 429 προκαλούσε κλείδωμα ανά MODEL· τώρα προκαλεί cooldown σύνδεσης — ακόμη και για διαχειριστή που εκτελεί μία μόνο σύνδεση χωρίς καμία συγγενική σύνδεση. Αυτή είναι η λεπτομέρεια που ο πίνακας κανόνων του opencode δηλώνει ήδη ως σωστή (`scope: "connection"`, `providerErrorRules.ts`), αλλά μέχρι τώρα δεν τηρούνταν ποτέ, επειδή το opencode δεν βρίσκεται στο `HONORS_RULE_LOCK_SCOPE_PROVIDERS`. Ο κλάδος εγγράφει ο ίδιος το cooldown - `backoffLevel` της σύνδεσης που αποτυγχάνει, αντικατοπτρίζοντας τον κλάδο agentrouter με εμβέλεια σύνδεσης, και επιστρέφει — το μπλοκάρισμα ανά μοντέλο και η γενική διαδρομή παρακάτω δεν εκτελούνται ποτέ. - **Περιλαμβάνεται το combo**: όπως και ο κλάδος agentrouter, η εμβέλεια αγνοεί σκόπιμα την υποβάθμιση `persistUnavailableState`/`isCombo` που εφαρμόζει ένας καλών combo σε ένα 429. Ένα κλείδωμα ανά μοντέλο δεν αποτελεί ασθενέστερη μορφή αυτής της εμβέλειας· είναι η λανθασμένη μονάδα: δεν λέει τίποτα για το εξαντλημένο IP, οπότε η εναλλαγή combo θα συνέχιζε να σπαταλά μία εγγυημένα αποτυχημένη κλήση ανά συγγενική σύνδεση. - **Ασφάλεια συγγενικών συνδέσεων**: μια συγγενική σύνδεση που βρίσκεται ήδη σε οριστική κατάσταση (banned/credits_exhausted) ή ήδη σε μεγαλύτερο cooldown δεν αντικαθίσταται ποτέ. - **Αποκλειστική λίστα επιτρεπόμενων**: η διεύρυνση του `EGRESS_BUCKETED_LOCK_PROVIDERS` αποτελεί ρητή απόφαση του κατόχου· δεν υπάρχει γενική καλωδίωση (μοτίβο #10334/#10419). Το ερώτημα συγγενικών συνδέσεων δεσμεύει την ίδια λίστα επιτρεπόμενων αντί να την επαναλαμβάνει ως SQL literal, ώστε η διεύρυνσή της να παραμένει αλλαγή μίας γραμμής. - **Εναλλαγή egress IP, και προς τις δύο κατευθύνσεις**: το παράθυρο αναζήτησης (24h) είναι πολύ μεγαλύτερο από το TTL του cache egress-IP (5 min), επομένως το «τελευταίο γνωστό IP» είναι ιστορικό, όχι τρέχουσα κατάσταση. Αν ο proxy μιας σύνδεσης άλλαξε μέσα στο παράθυρο, το κλείδωμα ενδέχεται να **μην εντοπίσει** ένα πραγματικά κοινόχρηστο IP (το καταγεγραμμένο IP είναι το νέο, μη εξαντλημένο) — και, συμμετρικά, ενδέχεται να **θέσει σε cooldown μια συγγενική σύνδεση που έκτοτε έχει μετακινηθεί** από το εξαντλημένο IP. Η δεύτερη περίπτωση κοστίζει σε εκείνη τη συγγενική σύνδεση ένα παράθυρο cooldown· και οι δύο γίνονται αποδεκτές ως περιορισμοί βέλτιστης δυνατής προσπάθειας μιας αναζήτησης βάσει ιστορικού. - **Κόστος**: δύο οριοθετημένες σαρώσεις του `proxy_logs` (με φιλτράρισμα παραθύρου μέσω `idx_pl_timestamp`), μόνο με τη συχνότητα εμφάνισης 429. Κανένα νέο index (migration 134 YAGNI). Μετρήθηκε σε αντίγραφο DB πραγματικής κίνησης μεσαίου μεγέθους· ένα instance υψηλού throughput διατηρεί αναλογικά περισσότερες γραμμές στο ίδιο παράθυρο. --- ## Άλλες δυνατότητες ανθεκτικότητας - **20 στρατηγικές δρομολόγησης** (προτεραιότητα, σταθμισμένη, round-robin, αναμετάδοση περιβάλλοντος, πρώτα-η-πλήρωση, p2c, τυχαία, λιγότερο-χρησιμοποιημένη, βελτιστοποιημένη ως προς το κόστος, με επίγνωση επαναφοράς, παράθυρο επαναφοράς, διαθέσιμη χωρητικότητα, σταθμισμένη βάσει ποσόστωσης, αυστηρά τυχαία, αυτόματη, lkgp, βελτιστοποιημένη ως προς το περιβάλλον, βελτιστοποιημένη ως προς την κρυφή μνήμη, συγχώνευση, διοχέτευση) — δείτε το [AUTO-COMBO.md](../routing/AUTO-COMBO.md). - **Δρομολόγηση με επίγνωση επαναφοράς** (v3.8.0) — δίνει προτεραιότητα στις συνδέσεις βάσει του χρόνου επαναφοράς της ποσόστωσης. - **Υποβάθμιση λειτουργίας παρασκηνίου** — η επιλογή `background: true` του Responses API υποβαθμίζεται σε σύγχρονη λειτουργία με προειδοποίηση. - **Δυναμική ανίχνευση ορίου εργαλείων** — υποχωρεί από παρόχους όταν εξαντλούνται τα όρια πλήθους εργαλείων. - **Εφεδρική λύση έκτακτης ανάγκης** — ελέγχεται από το `OMNIROUTE_EMERGENCY_FALLBACK`· οι διαχειριστές μπορούν να την παρακάμψουν από τη σελίδα Feature Flags χωρίς επανεκκίνηση. --- ## Εντοπισμός σφαλμάτων - Το σταθμισμένο combo απαντά με `503 all_targets_cooling_down` (έχει οριστεί το `Retry-After`, ενώ το `diagnostics.excluded` παραθέτει κάθε προορισμό με `model_lockout` / `circuit_open` / `provider_cooldown` / `unavailable`) → το pool έχει ρυθμιστεί και συνδεθεί, αλλά κάθε προορισμός αποκλείεται απλώς από έναν χρονοδιακόπτη ανθεκτικότητας· η προειδοποίηση `[COMBO] Weighted selection: every target excluded before dispatch — …` αναφέρει τους λόγους και τα δευτερόλεπτα που απομένουν. Ένα `404 no_executable_targets` από το ίδιο combo σημαίνει ότι δεν εμπλέκεται κανένας χρονοδιακόπτης ανθεκτικότητας (δεν υπάρχει τίποτα προς εκτέλεση ή κάθε λογαριασμός απέτυχε στον έλεγχο διαθεσιμότητας). Υλοποιείται στο `open-sse/services/combo/pinRecovery.ts` από τους αποκλεισμούς που συλλέγονται στο `targetResolution.ts`. - Παραλείπονται όλα τα κλειδιά για έναν πάροχο → ελέγξτε τόσο την κατάσταση του circuit breaker ΟΣΟ ΚΑΙ τα `rateLimitedUntil`/`testStatus` κάθε σύνδεσης. - Ο πάροχος αποκλείεται μόνιμα μετά το παράθυρο επαναφοράς → ο κώδικας διαβάζει απευθείας το `state` αντί για τα `getStatus()`/`canExecute()`. - Ένα κλειδί αποτυγχάνει, ενώ τα υπόλοιπα θα πρέπει να λειτουργούν → προτιμήστε το cooldown σύνδεσης αντί για το circuit breaker. - Αποτυγχάνει μόνο ένα μοντέλο → προτιμήστε το κλείδωμα μοντέλου αντί για το cooldown σύνδεσης. - Η κατάσταση θα έπρεπε να ανακάμπτει αυτόματα, αλλά δεν το κάνει → ελέγξτε για μελλοντική χρονική σήμανση και για διαδρομή ανάγνωσης που ανανεώνει την κατάσταση που έχει λήξει. Οι μόνιμες καταστάσεις απαιτούν μη αυτόματες αλλαγές. --- ## Αποτύπωση TLS & απόκρυψη Η απόκρυψη ανά πάροχο (JA3/JA4, CCH, συσκότιση) τεκμηριώνεται ξεχωριστά — δείτε το `docs/security/STEALTH_GUIDE.md` (git· δεν μεταγλωττίζεται στο `/docs`). --- ## Δοκιμές ανθεκτικότητας (Φάση 8 · Μπλοκ C) Πέρα από τις δοκιμές μονάδας για τη λογική ανθεκτικότητας, τρεις δοκιμές ελέγχουν το περιβάλλον εκτέλεσης υπό πραγματικές συνθήκες έντονου φόρτου/αστοχίας (όλες είναι δοκιμές ενσωμάτωσης/νυχτερινές — καμία δεν εμποδίζει τα PR): | Δοκιμή | Τι ελέγχει | Εκτέλεση | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Χάος | Ένας ψευδής κόμβος upstream εισάγει πραγματική καθυστέρηση/επαναφορά/λήξη χρονικού ορίου/503· επαληθεύει ότι ο διακόπτης κυκλώματος ανοίγει/ανακάμπτει και ότι το `checkFallbackError` ταξινομεί το 503 ως ανακτήσιμη εφεδρική περίπτωση. | `RUN_CHAOS_INT=1 npm run test:chaos` | | Αύξηση σωρού | ~500 ροές ανά `createSSEStream` με `--expose-gc`· αποτυγχάνει αν ο σωρός αυξηθεί πέρα από το ανώτατο όριο (προστασία OOM #3069). | `npm run test:heap` | | Παρατεταμένο φορτίο k6 | Διαρκές φορτίο στο `/api/monitoring/health`· κατώφλια p95/σφαλμάτων. | `k6 run tests/load/k6-soak.js` (νυχτερινά) | Ενορχηστρώνονται από το `.github/workflows/nightly-resilience.yml` (cron + χειροκίνητη ενεργοποίηση). Στο προεπιλεγμένο `test:integration`, οι δοκιμές χάους και σωρού παραλείπονται αυτόματα (χωρίς `RUN_CHAOS_INT`/`--expose-gc`). --- ## Δείτε επίσης - [Οδηγός αρχιτεκτονικής](./ARCHITECTURE.md) — Αρχιτεκτονική συστήματος και εσωτερική λειτουργία - [Οδηγός χρήστη](../guides/USER_GUIDE.md) — Πάροχοι, συνδυασμοί, ενσωμάτωση CLI - [Μηχανή αυτόματων συνδυασμών](../routing/AUTO-COMBO.md) — Βαθμολόγηση 16 παραγόντων, πακέτα λειτουργιών