# 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) · 🇬🇷 [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) · 🇰🇭 [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`). **დანიშნულება:** შეწყდეს ტრაფიკის გაგზავნა პროვაიდერთან, რომელიც ზედა დონის სისტემის/სერვისის დონეზე განმეორებით განიცდის შეფერხებას. **იმპლემენტაცია:** - ძირითადი კლასი: `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` - DB ცხრილი: `domain_circuit_breakers` **მდგომარეობები:** - `CLOSED` — ნორმალური ტრაფიკი დაშვებულია - `DEGRADED` — ტრაფიკი კვლავ დაშვებულია, თუმცა პროვაიდერის გახშირებული შეფერხებები აღირიცხება - `OPEN` — პროვაიდერი დროებით დაბლოკილია; კომბინირებული მარშრუტიზაცია მას გამოტოვებს - `HALF_OPEN` — ჩამოყრის მოლოდინის დრო ამოიწურა; საცდელი მოთხოვნა დაშვებულია **კონფიგურირებადი ნაგულისხმევი მნიშვნელობები (`open-sse/config/constants.ts`, ხელმისაწვდომია Dashboard → Settings → Resilience-ში):** | კლასი | დეგრადაცია | გახსნა | ჩამოყრის მოლოდინის დრო | | ------- | ----------- | ------------ | ---------------------- | | OAuth | 5 შეფერხება | 8 შეფერხება | 60s | | API-key | 7 შეფერხება | 12 შეფერხება | 30s | | Local | გამოთვლილი | 2 შეფერხება | 15s | `degradationThreshold` განსაზღვრავს, როდის გადავა პროვაიდერი `DEGRADED` მდგომარეობაში; `failureThreshold` განსაზღვრავს, როდის გაიხსნება ის და როდის იქნება გამოტოვებული. ლოკალური პროვაიდერის პროფილები Resilience-ის პარამეტრების გვერდზე ჯერ არ არის ხელმისაწვდომი. **ამოქმედების კოდები:** მხოლოდ პროვაიდერის დონის სტატუსები `[408, 500, 502, 503, 504]`. არ აამოქმედოთ ანგარიშის დონის შეცდომებისთვის (401/403/429-ის უმეტესობა — ისინი გაგრილების პერიოდს ან ბლოკირებას მიეკუთვნება). **ზარმაცი აღდგენა:** როდესაც `OPEN` მდგომარეობას ვადა გაუვა, `getStatus()`, `canExecute()`, `getRetryAfterMs()` მდგომარეობას `HALF_OPEN`-ზე განაახლებს. ფონური ტაიმერი საჭირო არ არის. --- ### არჩევითად ჩასართავი გლობალური პროვაიდერის გაგრილების პერიოდი (დროის ფანჯრის ბარიერი) მეოთხე, **არჩევითად ჩასართავი** ფენა (`PROVIDER_COOLDOWN_ENABLED`, ნაგულისხმევად **გამორთულია**) შეფერხებული პროვაიდერების შესახებ მოთხოვნებს შორის საერთო მეხსიერებას ინარჩუნებს `open-sse/services/providerCooldownTracker.ts`-ში, რომელსაც კომბინირებული სამიზნეების განსაზღვრისას მიმართავენ, რათა მომდევნო კომბინირებულმა მოთხოვნებმა თავიდან აღარ გაიაროს პროვაიდერი, რომელმაც ახლახან შეფერხება განიცადა. პროვაიდერის დონის ჩანაწერები `PROVIDER_PROFILES` დროის ფანჯრის ბარიერს ითვალისწინებს: | პროფილი | ამოქმედდება (`providerFailureThreshold`) | პერიოდის განმავლობაში (`providerFailureWindowMs`) | გაგრილების ხანგრძლივობა (`providerCooldownMs`) | | ------- | ---------------------------------------: | ------------------------------------------------: | ---------------------------------------------: | | OAuth | `10` | `15min` | `5min` | | API key | `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-ის საბაზისო მნიშვნელობა: 5 წმ - API-გასაღების საბაზისო მნიშვნელობა: 3 წმ - API-გასაღების 429: უპირატესობას ანიჭებს ზედა დონის `Retry-After`/განულების სათაურებს/გაანალიზებად განულების ტექსტს - უკანდახევა: `baseCooldownMs * 2 ** failureIndex` **ერთდროული მოთხოვნების მოზღვავებისგან დაცვა:** ხელს უშლის პარალელურ შეცდომებს, ზედმეტად გაახანგრძლივონ დაყოვნების პერიოდი ან ორჯერ გაზარდონ `backoffLevel`. **ტერმინალური მდგომარეობები (არა დაყოვნების პერიოდები):** - `banned` — დგინდება აკრძალული საკვანძო სიტყვის / ანგარიშის აკრძალვის გამოვლენისას (იხილეთ [BAN_DETECTION](../security/BAN_DETECTION.md)), აგრეთვე ზედა დონის სერვერის მიერ ზედიზედ სამი ცალკეული მოთხოვნის უარყოფისას (`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) კავშირი. ორივე ფუნქცია **თითოეული კავშირისთვის ცალ-ცალკე ჩასართავია** (Edit connection → Claude section → `lowPriorityMode` / `autoLimitReset` `providerSpecificData`-ში, ორივე ნაგულისხმევად გამორთულია) და იმეორებს Claude Code-ის `/low-priority` და `/limit-reset` ბრძანებების ქცევას (ქსელური კონტრაქტი აღებულია Claude Code 2.1.263-დან). **იმპლემენტაცია:** - მდგომარეობათა ავტომატი + პასუხების კლასიფიკაცია: `open-sse/services/claudeLowPriority.ts` - განულების სტატუსის/მოთხოვნის კლიენტი: `open-sse/services/claudeLimitReset.ts` - შემსრულებლის ჰუკი (სათაურის ჩასმა + იმავე ანგარიშით განმეორებითი ცდა): `open-sse/executors/base.ts::execute()` - ჩართვის არჩევანის შენახვა: `src/lib/providers/requestDefaults.ts::normalizeProviderSpecificData()` **ტრიგერი:** გამოყენების 5-საათიანი ზღვარი — `429`, რომლის სათაურებიც შეიცავს `anthropic-ratelimit-unified-status: rejected`-ს და, როდესაც ანგარიში შესაბამის მოთხოვნებს აკმაყოფილებს, `anthropic-ratelimit-unified-slow-offer: treatment`-ს. პირველ ასეთ ზღვრულ 429-მდე არაფერი იგზავნება; ერთბაშად მიღებული 429, გაერთიანებული სათაურების გარეშე, დაყოვნების პერიოდის ჩვეულებრივი გზით მუშავდება. **დაბალი პრიორიტეტის არხი** (`lowPriorityMode`): - ზღვრული 429-ის მიღებისას შემსრულებელი იღებს შეთავაზებას და დაუყოვნებლივ იმეორებს მოთხოვნას **იმავე** ანგარიშითა და `anthropic-usage-limit: slow`-ით; არხი აქტიური რჩება გამოცხადებულ `anthropic-ratelimit-unified-reset`-მდე (+60 წმ საშეღავათო პერიოდი) და ამ შუალედში თითოეული მოთხოვნა ამ სათაურს შეიცავს. ჩაჭერილი 429 არასდროს აღწევს `handleChatCore`-მდე, ამიტომ კავშირი **არ** გადადის დაყოვნების პერიოდში და სხვა კავშირით არ ჩანაცვლდება. - `anthropic-ratelimit-unified-slow-status` შემდგომ პასუხებში: `active` / `not_needed` ინარჩუნებს არხს; `slot_busy` (429) ან `529` იცდის სერვერის მიერ მითითებული `anthropic-ratelimit-unified-slow-retry-after`-ის შესაბამისად (ნაგულისხმევად 20 წმ, შეზღუდვა 5–600 წმ, ±30% შემთხვევითი გადახრა) და მოთხოვნას იმეორებს, `anthropic-ratelimit-unified-slow-max-wait`-ით განსაზღვრულ ფარგლებში (ნაგულისხმევად 20 წთ, შეზღუდვა 1 წთ–6 სთ) — ამ დროის გასვლის შემდეგ არხი იხურება და 10-წუთიანი შესვენება ხელახლა მიღებას ბლოკავს. ლოდინი დამატებით იზღუდება მოთხოვნის ზედა დონის სერვერთან დაწყების ტაიმაუტის დარჩენილი დროით (`resolveFetchStartTimeout`, ნაგულისხმევად 10 წთ), გამოკლებული 5 წმ მარაგი: ამ შეზღუდვის გარეშე 20-წუთიანი ნაგულისხმევი მაქსიმალური ლოდინი მოთხოვნის სიცოცხლის ხანგრძლივობას გადააჭარბებდა და ძილი შუა ლოდინში შეწყდებოდა, რის შედეგადაც კორექტული `max_wait` დასრულებისა და შესვენების ნაცვლად `TimeoutError` გამოჩნდებოდა. - `weekly_limit` / `budget_exhausted` / `off` / `ineligible`, 5-საათიანი ფანჯრის განახლება, ან `ineligible` + `anthropic-ratelimit-unified-overage-in-use: true` (რაც ნებისმიერ სტატუსზე მას `extra_usage`-ით ასრულებს, რადგან ფასიანი გადაჭარბებული გამოყენება ახლა ზღვარს ფარავს) არხს ხურავს; შემდეგ პასუხი დაყოვნების პერიოდის ჩვეულებრივი გზით მუშავდება. `budget_exhausted` დამახსოვრებულია გამოცხადებულ ბიუჯეტის განულებამდე (≤ 8 დღე). - ზღვრის შემოწმება სრულდება შემსრულებლის მიერ 400-ით გამოწვეული შიდა განმეორებითი ცდების შემდეგ (კონტექსტის რედაქტირება, აზროვნების/ძალისხმევის შეზღუდვები, პარამეტრების ავტომატური შესწავლა), ამიტომ ზღვრული 429, რომელიც მხოლოდ ერთ-ერთი ასეთი განმეორებითი ცდისას ჩნდება, მაინც ჩაიჭრება და დაყოვნების პერიოდის გზამდე არ მივა. - მდგომარეობა მეხსიერებაში ინახება თითოეული კავშირისთვის (გადატვირთვის შემდეგ ხელახლა მისაღებად ერთი დამატებითი ზღვრული 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`-დან, საწყისი ჩატვირთვის სათადარიგო ვარიანტით). - `result: reset|not_limited` → მოთხოვნა სრული სიჩქარით მეორდება (ნელი რეჟიმის სათაურის გარეშე). `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` სათაური), მიბმული ერთ კავშირზე, **ნებისმიერი** პროვაიდერისთვის. **დანიშნულება:** მრავალსვლიანი აგენტის (Claude Code, aider, მორგებული აგენტები) მოთხოვნებს შორის იმავე ანგარიშზე შენარჩუნება, რაც ამცირებს სხვადასხვა ანგარიშს შორის კონტექსტის დაკარგვას და თითოეული ანგარიშისთვის სესიის მდგომარეობის მქონე პროვაიდერებთან განმეორებით ცივი გაშვებისას წარმოქმნილ 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 მილიწამებში, `0` გამორთავს) — `src/lib/db/settings.ts`. მხოლოდ Codex-ისთვის განკუთვნილ `codexSessionAffinityTtlMs`-ს სახელი შეეცვალა მიგრაციით `124_generic_session_affinity_ttl.sql`, რომელსაც ადრე კონფიგურირებული Codex-ის ნებისმიერი TTL ახალ ნაგულისხმევ მნიშვნელობად გადააქვს. #7274-მდე `resolveSessionAffinityTtlMs()` ყველა პროვაიდერისთვის, გარდა `codex`-ისა, დაუყოვნებლივ აბრუნებდა `0`-ს, ამიტომ TTL-ის პარამეტრი (და სესიის სათაურები) სხვაგან არ მოქმედებდა, მიუხედავად იმისა, რომ მიმაგრების მექანიზმი და სათაურების ამოღება უკვე პროვაიდერისგან დამოუკიდებელი იყო. შესწორებამ ეს ნაადრევი დაბრუნება წაშალა; ახლა, როდესაც გლობალურად დაყენებულია `0`-ზე მეტი მნიშვნელობა, TTL ყველა პროვაიდერზე ერთგვაროვნად ვრცელდება. სესიის კუთვნილების სამი სათაური არასდროს გადაიგზავნება ზემდგომ სერვერზე — შემსრულებლები თავიანთ ზემდგომ სათაურებს ნულიდან აგებენ და კლიენტის სათაურებს პირდაპირ არ გადასცემენ, ამიტომ ეს მხოლოდ შიდა კორელაციის იდენტიფიკატორად რჩება. ### მართული სესიის კავშირების ექსკლუზიური იჯარები **მოქმედების არეალი:** ერთი აქტიური მართული HTTP კლიენტი/სესია ფლობს ერთ შესაბამის OmniRoute კავშირს. **დანიშნულება:** უზრუნველყოს კავშირის გამძლე, ექსკლუზიური მფლობელობა კლიენტებისთვის, რომლებსაც მოთხოვნებს შორის მკაცრი მარშრუტიზაციის საზღვარი სჭირდებათ. ეს განსხვავდება სესიის კუთვნილებისგან, რომელიც უწყვეტობის რბილი პრეფერენციაა: ექსკლუზიური იჯარა სასიცოცხლო ციკლის მდგომარეობას SQLite-ში ინახავს, გლობალურად უზრუნველყოფს აქტიური მფლობელისა და აქტიური კავშირის უნიკალურობას და პროვაიდერთან გადაგზავნამდე უარყოფს მოძველებულ თაობას. ფუნქცია თითოეული API გასაღებისთვის ცალკე, სურვილისამებრ აქტიურდება. მართულ გასაღებს უნდა ჰქონდეს `lease:exclusive` მოქმედების არეალი და ცალსახად მითითებული არაცარიელი `allowedConnections` სია. სასიცოცხლო ციკლის ბოლო წერტილის გამოყენება ნებისმიერ HTTP კლიენტს შეუძლია; არ არის საჭირო კლიენტის სახელი, user-agent, პროვაიდერი, OAuth მეთოდი ან მოდელი. იჯარა ფლობს კავშირს და არა მოდელს, ამიტომ მოდელის შეცვლისას მიბმა შენარჩუნდება, სანამ კავშირი ჩვეულებრივად შესაბამისი რჩება. მოდელის, კვოტის, ჯანმრთელობის მდგომარეობის, დაყოვნების პერიოდისა და ნებადართული კავშირების სიის ჩვეულებრივი წესები კვლავ გადამწყვეტია და იმავე თაობის სხვა თავისუფალ შესაბამის კავშირზე გადაყვანა შეუძლია. სასიცოცხლო ციკლი არის `POST /api/v1/session-leases`, JSON მოქმედებებით `acquire`, `renew` და `release`. მართული ინფერენციის მოთხოვნები გადასცემენ გაუმჭვირვალე `X-OmniRoute-Lease-Owner` მნიშვნელობას და ზუსტ `X-OmniRoute-Lease-Generation`-ს. მფლობელის მნიშვნელობა იწყება `vlo_`-ით, რომელსაც მოსდევს 43 base64url სიმბოლო; ინახება მხოლოდ მისი SHA-256 ჰეში. ყოველი საბოლოო გადაგზავნის შემზღუდველი შემოწმება ასევე აკავშირებს ავთენტიფიცირებული API გასაღების ID-სა და აქტიური კავშირის ID-ს. იჯარის მართვის სათაურები იშლება ჟურნალებიდან, შენახული მოთხოვნის მომენტალური ასლებიდან და ზემდგომი შემსრულებლის სათაურებიდან. თუ ჩვეულებრივ მარშრუტიზაციას შესაბამისი მართული კანდიდატები აქვს, მაგრამ ყველა თავისუფალი კანდიდატი უცხო აქტიური იჯარითაა დაკავებული, 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` ტრანსპორტის/სერვერის შეცდომები და ხარისხის ვალიდაციის შედეგად თავად OmniRoute-ის მიერ გენერირებული `502` — ბლოკავს მხოლოდ პროვაიდერის/კავშირის/მოდელის **ზუსტ** კომბინაციას. ერთ მოდელზე გაუმართავი ნაკადი ანგარიშის კვოტის შესახებ მტკიცებულება არ არის; ამ წესის შემოღებამდე `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}`. ავტორიზაცია: მართვის უფლებები. ### დაბლოკვის პარამეტრების ინტერფეისი + წარმატებაზე დაფუძნებული კლებადი აღდგენა (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`) და DB-ში არ ინახება — გადატვირთვისას ისინი იკარგება. _პარამეტრები_ ინახება მუდმივად; აქტიური დაბლოკვის _მდგომარეობა_ დროებითია. --- ## 4. კვოტის გაზიარების პარალელურობის კონტროლი (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). ### Quota-share მოთხოვნების სერიალიზაცია როდესაც quota-share დისპეტჩერიზაცია მიმართულია კავშირზე, რომელსაც დადებითი `max_concurrent` აქვს მითითებული, ამ **ანგარიშზე** მიმართული პარალელური მოთხოვნები სერიალიზდება თითოეული კავშირის სემაფორის მეშვეობით (გასაღები `qsconn:`): ჭარბი მოთხოვნები ანგარიშის გადატვირთვის ნაცვლად **რიგში ელოდებიან**. მექანიზმი **fail-open** პრინციპით მუშაობს — გადავსებული რიგის ან დროის ამოწურვის შემთხვევაში მოთხოვნა სლოტის გარეშე გაგრძელდება და დისპეტჩერიზაციისთვის ვარგისი მოთხოვნა არასოდეს იქნება უარყოფილი. გადართვა შესაძლებელია **Settings → Resilience → Quota-share per-connection concurrency**-ში (`resilienceSettings.quotaShareConcurrencyLimit.enabled`, ნაგულისხმევად ჩართულია). `max_concurrent` ზღვრის გარეშე ქცევა უცვლელი რჩება. > Quota-share მარშრუტიზაციის გამშვები (`selectQuotaShareTarget`, DRR + P2C) თავადაც > fail-open პრინციპით მუშაობს და ზღვარზე მყოფ კავშირს მხოლოდ _დაბალ პრიორიტეტს ანიჭებს_ — > ერთკავშირიან პულში მას მკაცრი შეზღუდვის დაწესება არ შეუძლია, ამიტომ სწორედ ეს სემაფორი > ახდენს მოთხოვნების ნაკადის რეალურ შეკავებას. ### კომბინაციის დაყოვნების პერიოდის გათვალისწინებით განმეორებითი ცდა თითოეული კომბინაციის სტრატეგიისთვის (როდესაც ჩართულია), მოთხოვნა, რომელიც მოკლე დროებითი დაყოვნების გამო საბოლოო 429 პასუხს გამოიწვევდა, ელოდება მის დასრულებას და 429-ის დაბრუნების ნაცვლად ხელახლა იგზავნება — ეს მოიცავს Gemini-ის კლასის TPM/RPM ფანჯრებს (~60წმ retry-after) მრავალმოდელიან კომბინაციებში, მაგალითად, როდესაც 2-მოდელიანი კომბინაციის ორივე სამიზნე თითოეული მოდელის სიხშირის ლიმიტს აღწევს. იზღუდება `comboCooldownWait`-ის (`enabled`, `maxWaitMs`, `maxAttempts`, `budgetMs`) საშუალებით **Settings → Resilience**-ში. ის არასოდეს ელოდება `quota_exhausted`-ის (შუაღამემდე დაბლოკილი) ან ავტორიზაციის/ვერუპოვნელობის მიზეზების შემთხვევაში. --- ## 5. მოთხოვნების რიგში დაშვების კონტროლი (v3.8.49 · საკითხი #6593) **მოქმედების არეალი**: ლოკალური, თითოეული პროვაიდერი+კავშირისთვის განკუთვნილი სიხშირის შეზღუდვის რიგი (`open-sse/services/rateLimitManager.ts`, რომელიც Bottleneck-ზეა დაფუძნებული), ზემოთ აღწერილი სამი მექანიზმის ქვემოთ ერთი დონით. **`maxWaitMs` ზღუდავს რიგში ლოდინს; `executionMaxWaitMs` ზღუდავს შესრულებას.** ეს ორი განზრახაა ერთმანეთისგან განცალკევებული და არცერთი არ აწვდის მნიშვნელობას მეორეს. `resilienceSettings.requestQueue.maxWaitMs` არის **რიგში ლოდინის ბიუჯეტი**: ის მოიცავს პროვაიდერის სლოტის მოლოდინს და შემდეგ QUEUED მდგომარეობაში ყოფნას, ხოლო მისი ტაიმერი იწმინდება ზუსტად იმ მომენტში, როდესაც დავალება ტოვებს QUEUED მდგომარეობას და შესრულებას იწყებს (`rateLimitManager.ts`, `wrappedFn`). მოთხოვნა, რომელიც ამ ზღვარს გადააჭარბებს, ზედა დონის სერვისამდე ვერასოდეს მიაღწევს. ნაგულისხმევი მნიშვნელობაა 30000ms, რომელსაც `src/lib/resilience/settings.ts`-ში განსაზღვრული `DEFAULT_REQUEST_QUEUE_MAX_WAIT_MS` აწვდის და `tests/unit/ratelimit-admission-control-6593.test.ts` აფიქსირებს, ამიტომ მისი შეცვლის შემთხვევაში ეს ტესტი გაწითლდება და ეს აბზაცი შეუმჩნევლად მოძველებული არ დარჩება. `resilienceSettings.requestQueue.executionMaxWaitMs` არის მნიშვნელობა, რომელსაც Bottleneck დავალების `expiration`-ად იღებს და რომლის ტაიმერიც მხოლოდ გაგზავნის შემდეგ იწყება. ის დამცავი მექანიზმია იმ შემსრულებლებისთვის, რომლებსაც ზედა დონის სერვისთან ურთიერთობის საკუთარი ტაიმაუტი არ აქვთ, და თუ შემსრულებლის მიერ fetch-ის დაწყებისთვის განსაზღვრული ტაიმაუტი უფრო ხანგრძლივია, მნიშვნელობაც მასამდე იზრდება, რათა დამუშავების პროცესში მყოფი ჯანსაღი პასუხი ნაადრევად არ შეწყდეს. ნაგულისხმევი მნიშვნელობაა 600000ms (10 წთ). რიგის ბიუჯეტის `expiration`-ისთვის მიწოდება ადრე არაინკრემენტულ გეითვეებს შესრულების შუაში წყვეტდა — მათ პირველი ბაიტების დაბრუნებამდე კანონიერად შეიძლება რამდენიმე წუთი დასჭირდეთ — და სწორედ ამიტომ `expiration` წარმოდგენილია როგორც `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` კი ზღუდავს, ერთდროულად რამდენი გამომძახებელი შეიძლება იდგეს რიგში. **`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/` კონვეიერი > გამავალ LLM მოთხოვნაზე პრომპტის/კონტექსტის შეკუმშვას ასრულებს (`chatCore.ts`, > `resolveCompressionSettings`/`selectCompressionStrategy` ბლოკის მიდამოებში) > და არა სინთეზირებულ 429 პასუხების სხეულების HTTP შეკუმშვას — პირდაპირი შემოვლის > ალმის შესაბამისი კოდის გზა არ არსებობს. პრომპტის შეკუმშვის ეს ეტაპი მოთხოვნების > კონვეიერში ამჟამად `withRateLimit()`-მდე სრულდება, ამიტომ რიგის შევსების გამო > უარყოფისას მის გამოსატოვებლად ეტაპების გადალაგება ამ საკითხის მოქმედების არეალზე > განცალკევებული და უფრო მასშტაბური ცვლილებაა; ის აქ განზრახ **არ** განხორციელებულა > და შემდგომ სამუშაოდ დარჩა, თუ CPU-ის რესურსების დაზოგვით მიღებული სარგებელი > გადალაგების რისკს გაამართლებს. --- ## 6. ნელი ნაკადის გამტარუნარიანობის მეთვალყურე (#9709) არასავალდებულო `resilienceSettings.streamRecovery.throughputWatchdog` დამცავი მექანიზმი აღმოაჩენს upstream-ს, რომელიც კვლავ აგზავნის ფრაგმენტებს, მაგრამ ასისტენტის გამომავალს კონფიგურირებულ სასარგებლო გამომავალის სიჩქარეზე ნაკლები სიჩქარით წარმოქმნის. იგი განზრახ განსხვავდება უმოქმედობის ტაიმაუტისგან: heartbeat-ები და მეტამონაცემები არცერთ ტაიმერს არ ანულებს და პროგრესად არ ითვლება. იგი ასევე განსხვავდება მცდელობის მკაცრი ვადისგან (#9153), რომელიც გამომავალის ხარისხის მიუხედავად უსაფრთხოების აბსოლუტურ ზედა ზღვარად რჩება. შეწყვეტამდე მეთვალყურეს სჭირდება გახურების პერიოდი, რომელსაც სრული მოძრავი ფანჯარა მოჰყვება. იგი ითვლის ტექსტურ დელტებს Chat Completions-ისა და Responses API-ის გამომავალ მოვლენებში (კონსერვატიული UTF-8 ბაიტური მიახლოება), უგულებელყოფს მხოლოდ გამოყენების მონაცემების შემცველ და ცარიელ მოვლენებს და შეფასებას აჩერებს, სანამ ხელსაწყოს გამოძახების ან მსჯელობის მოვლენები მიმდინარეობს. ნაგულისხმევად იგი გამორთულია და მისი ჩართვა შესაძლებელია `STREAM_THROUGHPUT_WATCHDOG_ENABLED=true`-ით; ფანჯარა, გახურების პერიოდი, მინიმალური სიჩქარე და მინიმალური გაზომვადი გამომავალი შეზღუდულია მდგრადობის პარამეტრების ნორმალიზაციის ჩვეულებრივი შრით. ჩართვისას მეთვალყურის მიერ შეწყვეტა ვრცელდება მხოლოდ აქტიურ upstream-მცდელობაზე. კლიენტისთვის ხილული ნებისმიერი ბაიტის გაგზავნამდე იმავე ანგარიშის ადრეული აღდგენის არსებულმა გზამ შეიძლება მცდელობა თავიდან გახსნას. commit-ის შემდეგ ნაკადი არასოდეს გაეშვება თავიდან ბრმად; სუფიქსის მიმაგრება მხოლოდ შუა ნაკადში უსაფრთხო გაგრძელების არსებულ კონტრაქტს შეუძლია. ფინალიზაცია კვლავ ერთჯერადია, ამიტომ გამოყენების აღრიცხვა და semaphore-ის გათავისუფლება არ დუბლირდება. --- ## 7. Upstream სტატუსის ხელახალი განსაზღვრა (არასწორად მითითებული კვოტის შეცდომები) **მოქმედების სფერო:** ერთი upstream-კარიბჭე, რომელიც კვოტის დროებით ამოწურვას არასწორი HTTP სტატუსით ატყობინებს. **დანიშნულება:** კლასიფიკაციამდე შეცდომაში შემყვანი სტატუსის გასწორება, რათა ქვედა დონის მომხმარებლებმა (fallback-ძრავამ, კომბინირებულმა აგრეგაციამ, კლიენტისთვის განკუთვნილმა პასუხმა) მარცხის რეალური, ხელახლა ცდადი ბუნება დაინახონ. ზოგიერთი კარიბჭე კვოტის დროებით ამოწურვაზე არახელახლა ცდადი HTTP სტატუსით მიუთითებს. `agentrouter.org` სტანდარტული `429`-ის ნაცვლად აბრუნებს `403`-ს (ზოგჯერ `400`-ს) ჩინურ სხეულთან ერთად (`用户额度不足` / `额度不足`). ისეთი კლიენტები, როგორიცაა 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()` შეცდომის HTTP სტატუსის მქონე upstream-პასუხს (`!providerResponse.ok`) დაამუშავებს და ნებისმიერი კლასიფიკაციის შესრულებამდე, რათა ქვედა დონის ყველა მომხმარებელმა გასწორებული სტატუსი დაინახოს. `200` SSE ნაკადში ჩაშენებული შეცდომები გადის ცალკე, მოგვიანებით შესრულებულ ნაკადის დამუშავების გზას და დღეს ამ hook-ით **არ** იფარება — ეს ცნობილი შეზღუდვაა, რომელიც agentrouter-ის არასწორი სტატუსისთვის ჯერ საჭირო არ არის (რადგან ის შეცდომის HTTP სტატუსად ვლინდება). - ხელახალი ცდის დასაშვებობა: `429` შედის `RETRY_AFTER_ELIGIBLE_STATUSES`-ში (`open-sse/services/combo/unavailableRetryGate.ts`), ამიტომ ხელახლა განსაზღვრულ შეცდომას რეალური ხელახალი ცდის ფანჯარა ახლავს, ნაცვლად იმისა, რომ უმოქმედო `403`-ად გამოჩნდეს. - სინთეზური `60s` `defaultRetryAfterMs` (`upstreamStatusRestatement.ts`) მხოლოდ ისაა, რასაც ხელახლა განსაზღვრული პასუხი **კლიენტს** ატყობინებს; ეს თავისთავად კავშირის შიდა cooldown/lockout-ის ხანგრძლივობა არ არის — მას ცალკე მართავს მექანიზმი, რომელიც ხელახლა განსაზღვრულ შეცდომას რეალურად ამუშავებს (Connection Cooldown-ის მზარდი backoff, §2, საბაზისო `3s` API-გასაღების პროვაიდერებისთვის; ან Model Lockout, §3, თითოეული მოდელის კვოტის მქონე ისეთი პროვაიდერებისთვის, როგორიცაა agentrouter). router-ს შიდა ხელახალი ცდის უფლება შეიძლება კლიენტისთვის გამოცხადებულ 60s-იან ფანჯარაზე ადრე მიეცეს — ეს განზრახ დატოვებული მარაგია და არა ხარვეზი. მუდმივი შეცდომები (agentrouter-ის `无权访问模型` — ამ მოდელზე წვდომა არ არის) არასოდეს განისაზღვრება ხელახლა: `excludeMarkers` წესს ვეტოს ადებს მაშინაც კი, როდესაც `textMarkers` ემთხვევა, ამიტომ შეცდომა თავდაპირველ სტატუსს ინარჩუნებს და არაფერი ცდილობს მის ხელახლა შესრულებას უსასრულოდ. პროვაიდერის შესაბამისი კლასიფიკაციის წესი (`agentrouter-model-access-denied` `open-sse/config/providerErrorRules.ts`-ში: `reason: "auth_error"`, `scope: "model"`, გამოცხადებული `6h` საბაზისო cooldown) `checkFallbackError`-ის მიერ (`open-sse/services/accountFallback.ts`) მოწმდება ზოგადი apikey-კატეგორიის `FORBIDDEN` ადრეულ დაბრუნებამდე, `honorsRuleLockScope(provider)`-ის პირობით (#10334 — ამჟამად ექსკლუზიურად agentrouter-ისთვის, `providerErrorRules.ts`-ში არსებული `HONORS_RULE_LOCK_SCOPE_PROVIDERS` ნებადართული სიის მეშვეობით). წესის გამოცხადებული 6h cooldown გადაეცემა როგორც `fallbackResult.baseCooldownMs`, თუმცა ის კვლავ მიეწოდება თითოეული მოდელის კვოტის დაბლოკვის მანამდე არსებულ გზას (`lockModelIfPerModelQuota()` / `recordModelLockoutFailure()`, რომელიც #10334-ს არ შეუცვლია, გარდა cooldown-ის წყაროსი): მისი მნიშვნელობა მცირდება ოპერატორის `mlSettings.maxCooldownMs`-მდე (ნაგულისხმევად `1_800_000ms` / 30min), როგორც ყველა სხვა მოდელის დაბლოკვის შემთხვევაში, ხოლო _შენახული დაბლოკვის მიზეზი_ კვლავ მანამდე არსებული, მყარად გაწერილი `"forbidden"` რჩება და არა წესის `"auth_error"` — თავიდან ბოლომდე გათვალისწინებულია მხოლოდ cooldown-ის ხანგრძლივობა და არა მიზეზის სტრიქონი. თავად კავშირი აქტიური რჩება; იმავე კავშირის სხვა მოდელებზე ეს გავლენას არ ახდენს. ხელახლა წარმოდგენილი კვოტის შეცდომები (`额度不足`) production გარემოში პროვაიდერის წესს ემთხვევა (`agentrouter-user-quota-exhausted`: `reason: "quota_exhausted"`, `scope: "connection"`, საკუთარი გამოცხადებული cooldown-ის გარეშე — გამოიყენება persistence ფენის scaled backoff-ის ნაგულისხმევი მნიშვნელობა). #10334-იდან მოყოლებული, `ProviderErrorRuleMatch`-ზე არსებული `scope` სრულად, end-to-end გამოიყენება, მაგრამ **მხოლოდ** `HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist-ში მყოფი პროვაიდერებისთვის (`providerErrorRules.ts` — დღეს მხოლოდ `"agentrouter"`, `honorsRuleLockScope()`-ის მეშვეობით კონტროლდება). ყველა სხვა პროვაიდერისთვის `scope` კვლავ მხოლოდ საინფორმაციოა, ზუსტად ისე, როგორც #10334-მდე. `checkFallbackError` დამთხვევილი წესის scope-ს `fallbackResult.ruleScope`-ის სახით აბრუნებს; `isAgentrouterConnectionQuotaScope()` (`src/sse/services/auth.ts`) არის საერთო guard, რომელიც ადასტურებს, რომ `ruleScope` მართლაც უსაფრთხოდ შეიძლება ჩაითვალოს მთელ connection-ზე მოქმედ, თვითაღდგენად სიგნალად (scope `"connection"`, reason `quota_exhausted`, არასოდეს `permanent`, არასოდეს `creditsExhausted` — დაცვა მომავალი წესისგან, რომელმაც შეიძლება scope `"connection"` ანგარიშის მუდმივ მდგომარეობას დაუკავშიროს). მას ორი მომხმარებელი იძახებს: - **Persistence** (`markAccountUnavailable()`, `src/sse/services/auth.ts`): passthrough-პროვაიდერის **თითო მოდელზე** lockout-ის branch-ში გადასვლის ნაცვლად (agentrouter არის `passthroughModels: true` → `hasPerModelQuota()` აბრუნებს `true`-ს), ის იყენებს **connection-ის დროებით cooldown-ს** — `testStatus: "unavailable"` + `rateLimitedUntil`, და არასოდეს terminal status-ს (`credits_exhausted`/`banned`/`expired`) — ამიტომ cooldown-ის გასვლის შემდეგ connection თვითონ აღდგება და credential-ის ხელით reset არ გახდება საჭირო. ეს გამოტოვებულია იმ connection-ებისთვის, რომლებზეც `disableCooling: true` (#2997): ეს opt-out სანაცვლოდ თითო მოდელზე lockout-ის branch-ში გადადის (დოკუმენტირებული კომპრომისი — იხილეთ branch-ის ზემოთ არსებული code comment). - **იმავე request-ის combo routing** (`applyComboTargetExhaustion()`, `open-sse/services/combo/targetExhaustion.ts`): იგივე guard connection-ს in-memory `exhaustedConnections` set-ში მონიშნავს, key-ით `${provider}:${connectionId}`. ეს მხოლოდ იმავე request-ში დარჩენილ target-ს გამოტოვებს, რომელსაც _თავისივე target object-ზე უკვე აქვს ზუსტად ის `connectionId`_ (`getExhaustedTargetSkipReason()`, `open-sse/services/combo/comboPredicates.ts`, `if (provider && connectionId)` `exhaustedConnections` lookup-მდე) — ჩვეულებრივი model-list combo, სადაც sibling target-ებს საკუთარი pinned `connectionId` არ აქვთ და ის თითო dispatch-ზე მხოლოდ response-ის `X-OmniRoute-Selected-Connection-Id` header-იდან განისაზღვრება, ამ key-ს არასოდეს დაემთხვევა. ამ გავრცელებულ შემთხვევაში დარჩენილი leg-ის მიერ ახლახან ამოწურული ანგარიშის ხელახლა გამოყენებისგან რეალური დაცვა ეს Set **არ არის** — დაცვას უზრუნველყოფს ზემოთ აღწერილი persistence ფენა (connection-ის `rateLimitedUntil` ახლა მომავალშია), ამავე guard-ის მიერ ამ failure-ისთვის `transientRateLimitedProviders`-ის ჩახშობასთან ერთად (იხილეთ „ორსაფეხურიანი დიზაინი“ და `targetExhaustion.ts`-ში `isAgentrouterConnectionQuotaScope` branch-ზე არსებული code comment): თუ ეს Set მოუნიშნავი რჩება, `combo.ts`-ის `allowRateLimitedConnection` force-allow (`open-sse/services/combo.ts:1005-1013`, `:2734-2738`) პროვაიდერის დარჩენილი leg-ებისთვის **არ** ამოქმედდება, ამიტომ credential-ის შერჩევა ჩვეულებრივ ითვალისწინებს `rateLimitedUntil` filter-ს (`src/sse/services/auth.ts:1238`) და დარჩენილი leg ან სხვა, ჯერ კიდევ eligible agentrouter connection-ს აირჩევს, ან ხელმისაწვდომი credential-ების არარსებობის გამო failure-ით დასრულდება — ის იძულებით აღარ დაბრუნდება იმ connection-ზე, რომელსაც ამ branch-მა ახლახან cooldown დაუწესა. ### ორსაფეხურიანი დიზაინი: status-ის ხელახლა წარმოდგენა, შემდეგ კლასიფიკაცია Status-ის ხელახლა წარმოდგენა (`upstreamStatusRestatement.ts`) და პროვაიდერის კლასიფიკაციის წესები (`open-sse/config/providerErrorRules.ts`, `providerRuleRegistry`) განცალკევებული registry-ებია, რომლებიც ორივე provider id-სა და text marker-ებზეა დაფუძნებული, თუმცა სხვადასხვა ადგილას სრულდება და განსხვავებულ მიზნებს ემსახურება: ხელახლა წარმოდგენა HTTP status-ს `chatCore.ts`-ში ადრეულ ეტაპზე გადაწერს; კლასიფიკაციის წესები კი `checkFallbackError()`-ში fallback-ის `reason`-სა და lock-ის `scope`-ს (`model` / `provider` / `connection`) ირჩევს (`open-sse/services/accountFallback.ts`). კლასიფიკაციის წესები error-ის სრულ **ტექსტს** (რომელიც საჭიროა body marker-ების, მაგალითად `额度不足`, დასამთხვევად) მხოლოდ `providerErrorRules.ts`-ის `FULL_TEXT_RULE_PROVIDERS` allowlist-ში ჩამოთვლილი პროვაიდერებისთვის ხედავს — ამჟამად მხოლოდ `"agentrouter"`-ისთვის. ყველა სხვა **ჩაშენებული catalog** პროვაიდერის შემთხვევაში `checkFallbackError` ფუნქციას `getProviderErrorRuleMatch` მხოლოდ structured error-ს (`{code, type}`) გადასცემს, რაც საკმარისია header/status/code-ზე დაფუძნებული წესებისთვის, მაგრამ body-ის text marker-ებს ვერ ხედავს. ამ არჩევანს helper `resolveRuleMatchBody()` ასრულებს: allowlist-ში მყოფი პროვაიდერებისთვის error-ის სრული ტექსტი, სხვა შემთხვევებში კი structured error. **ჩაშენებული** პროვაიდერის `FULL_TEXT_RULE_PROVIDERS`-ში დამატება აშკარა, თითო პროვაიდერზე ცალკე opt-in-ია — ის იმისთვის არსებობს, რომ სიაში არმყოფი ყველა პროვაიდერის ნაგულისხმევი path byte-for-byte უცვლელი დარჩეს. წესის `scope` (`model` / `provider` / `connection`) `FULL_TEXT_RULE_PROVIDERS`-ისგან დამოუკიდებელი opt-in-ია: `checkFallbackError` მას მხოლოდ `fallbackResult.ruleScope`-ის სახით აბრუნებს, ხოლო downstream მომხმარებლები მას საინფორმაციო label-ისგან განსხვავებული მნიშვნელობით მხოლოდ იმ პროვაიდერებისთვის ითვალისწინებენ, რომლებიც იმავე ფაილში არსებულ `HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist-ში არიან (`honorsRuleLockScope()`-ის მეშვეობით კონტროლდება — დღეს მხოლოდ `"agentrouter"`). იმის სანახავად, თუ რას აკეთებს რეალურად `scope: "connection"`-ის დამთხვევა მას შემდეგ, რაც პროვაიდერი ამ allowlist-ში მოხვდება, იხილეთ ზემოთ „ხელახლა წარმოდგენილი კვოტის შეცდომები“. **#11104 — ოპერატორის მიერ გამოცხადებული წესები ორივე დაშვების სიას გვერდს უვლის.** ოპერატორს შეუძლია გაშვების დროს თითოეული პროვაიდერისთვის წესი გამოაცხადოს `settings.providerErrorRules`-ის (`open-sse/config/providerErrorRules.ts::setOperatorProviderErrorRules`) მეშვეობით, ამ ფაილის რედაქტირების გარეშე. ოპერატორის წესის `FULL_TEXT_RULE_PROVIDERS`/`HONORS_RULE_LOCK_SCOPE_PROVIDERS`-ის მიღმა მოქცევა — ეს დაშვების სიები ჩაშენებული კატალოგის წესების **ნაგულისხმევი** ქცევის დასაცავადაა განკუთვნილი — პარამეტრების მექანიზმს გამოუსადეგარს გახდიდა ყველა პროვაიდერისთვის, გარდა უკვე ჩამოთვლილებისა, რადგან წესის გამოცხადება თავისთავად უკვე ოპერატორის აშკარა თანხმობაა. `resolveRuleMatchBody()` და `honorsRuleLockScope()` ორივე ჯერ `hasOperatorRuleForProvider()`-ს ამოწმებს: ოპერატორის წესის მქონე პროვაიდერი იღებს შეცდომის დაუმუშავებელ ტექსტს და მის მიერ გამოცხადებული `scope` დაცულია, მიუხედავად იმისა, შედის თუ არა ის რომელიმე დაშვების სიაში. **ცნობილი ხარვეზი — HTTP 400-ისთვის `providerRuleRegistry` არასოდეს მოწმდება.** `checkFallbackError`-ის `BAD_REQUEST` განშტოება 400 სტატუსს მთლიანად საკუთარი შაბლონების მასივებით (`MODEL_ACCESS_DENIED_PATTERNS`, `CONTEXT_OVERFLOW_PATTERNS` და სხვ. `accountFallback.ts`-ში) ახდენს კლასიფიკაციას და შედეგს აბრუნებს მანამდე, სანამ მის ზემოთ მდებარე `configuredRule`/`getProviderErrorRuleMatch` განშტოებამდე მიაღწევს. ჩაშენებული კატალოგის წესი (ან ოპერატორის წესი) `status: 400`-ით სინტაქსურად მართებულია, მაგრამ არასოდეს ამოქმედდება. ამჟამად არცერთი არსებული წესი 400-ს არ ეხება, ამიტომ საწარმოო გარემოში არაფერზე მოქმედებს — თუმცა მომავალში 400-ის წესის დამატებამდე ჯერ ამ განშტოების შეცვლა იქნება საჭირო, რაც უფრო მასშტაბური ცვლილებაა, ვიდრე წესის დამატება (ეს ხელახლა ახდენს 400-ის კლასიფიკაციას ყველა იმ პროვაიდერისთვის, რომელიც უკვე ეყრდნობა შაბლონების მასივების ქცევას) და ერთი პროვაიდერის წესის დამატების ფარგლებს სცდება. ### კვოტის შესახებ არასწორი ინფორმაციის გამცემი ახალი კარიბჭის დამატება 1. დაარეგისტრირეთ ერთი წესების მასივი `statusRestatementRegistry`-ში (`open-sse/config/upstreamStatusRestatement.ts`). `textMarkers` პროვაიდერისთვის სპეციფიკური უნდა დარჩეს; არასოდეს გამოიყენოთ ხელახლა ზოგადი ინგლისური ფრაზები, რომლებიც `CREDITS_EXHAUSTED_SIGNALS`-თან (`open-sse/services/accountFallback.ts`) იკვეთება. 2. სურვილისამებრ, დაარეგისტრირეთ კლასიფიკაციის წესები `open-sse/config/providerErrorRules.ts`-ში (`providerRuleRegistry`), რათა დაბლოკვის სწორი მოქმედების არე შეირჩეს (`connection` ანგარიშის მასშტაბის კვოტისთვის, `model` კი ცალკეული მოდელის შეცდომებისთვის). საწარმოო გარემოში ეს ნაბიჯი მხოლოდ იმ პროვაიდერებისთვის ამოქმედდება, რომელთა წესებსაც შეცდომის სრული ტექსტი (სხეულის მარკერები) სჭირდება: იმავე ფაილში დაამატეთ პროვაიდერის id `FULL_TEXT_RULE_PROVIDERS`-ში — წინააღმდეგ შემთხვევაში `checkFallbackError` წესს მხოლოდ სტრუქტურირებულ `{code, type}` შეცდომას გადასცემს და სხეულის ტექსტზე დაფუძნებული წესი რეალურ ტრაფიკს ვერასოდეს დაემთხვევა. წესებს, რომლებიც მხოლოდ `status`/`headers`-ის მიხედვით ემთხვევა (მაგალითად, Opencode-ის ან Minimax-ის), ეს თანხმობა არ სჭირდება. ცალკე, თუ წესი აცხადებს `scope: "connection"`-ს და მიზანი კავშირის მასშტაბის რეალური დაყოვნებაა, იმავე მოთხოვნის ფარგლებში კომბინაციის გამოტოვებასთან ერთად (და არა უბრალოდ საინფორმაციო ჭდე), იმავე ფაილში დაამატეთ პროვაიდერის id `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-ის ოჯახი) განიხილება, როგორც IP-ის მიხედვით დაჯგუფებული ზემდგომი სერვისები (opencode-ის უფასო დონე IP-ის მიხედვითაა დაჯგუფებული და არა ანგარიშის მიხედვით — იხილეთ #9611): სტატუსი 429, რომელიც კლასიფიცირებულია როგორც `quota_exhausted` **ან** `rate_limit_exceeded`, დროებით აჩერებს დაშვების სიაში მყოფი ოჯახის ყველა კავშირს, რომლის ბოლო ცნობილი გამავალი IP ემთხვევა წარუმატებელი კავშირის IP-ს, სანამ როტაცია მათ გამოყენებას შეძლებს — რითაც თავიდან იცილებს N-1 გარანტირებულად წარუმატებელ ზემდგომ გამოძახებას (იგივე ფორმა, რაც #10460/#10525-ში). `rate_limit_exceeded` განზრახ არის შეტანილი: `markAccountUnavailable`-ის გზაზე opencode-ის სპეციფიკური წესები არასოდეს ემთხვევა (არც სათაურები/სხეული გადაეცემა `checkFallbackError`-ს და არც opencode შედის `FULL_TEXT_RULE_PROVIDERS`-ში), ამიტომ 429, რომლის სხეულიც გამოწერის კვოტის ტექსტს შეიცავს ("monthly usage limit reached"), კლასიფიცირდება როგორც `quota_exhausted` კვოტის ტექსტზე დაფუძნებული სათადარიგო მექანიზმით (`buildSubscriptionQuotaFallback`, `accountFallback.ts`; 1-საათიანი დაყოვნება), სანამ `status_429` წესი საერთოდ მიიღებს დამუშავების შანსს — ხოლო კვოტის ტექსტის არმქონე 429 (უბრალო სიხშირის შეზღუდვა) `status_429` წესის მეშვეობით კლასიფიცირდება როგორც `rate_limit_exceeded` და მაინც დროებით აჩერებს IP ოჯახს. დაშვების სიაში მყოფი პროვაიდერისთვის IP-ის მიხედვით დაჯგუფებული სიხშირის შეზღუდვა ამოწურული კვოტის ეკვივალენტური სიგნალია. რეალური შეზღუდვები: - **მაქსიმალური მცდელობა**: ბლოკირება `proxy_logs`-იდან იღებს კავშირის ბოლო ცნობილ `egress_ip`-ს (24-საათიანი ფანჯარა, სინქრონული, კეშის გარეშე). ცივი კეშის შემთხვევაში (გამავალი IP არასოდეს შემოწმებულა) ან ჩანაწერის არარსებობისას → წარუმატებელ კავშირს ეს განშტოება მაინც ანიჭებს cooldown-ს (იწერება ისევე, როგორც დღეს), უბრალოდ არცერთი მონათესავე კავშირი არ იბლოკება. - **არასოდეს არის ტერმინალური**: cooldown არის განახლებადი კვოტის ფანჯარა (`testStatus: "unavailable"`); IP-ის დონის სიგნალიდან მუდმივი მდგომარეობა არასოდეს განისაზღვრება. `disableCooling` კავშირები ამ განშტოებას მთლიანად გამოტოვებენ. - **allowlist-ში შეტანილი ოჯახისთვის ბლოკირების გრანულარობა იცვლება**: ეს არის მოქმედების არეალის ცვლილება და არა მხოლოდ მონათესავე კავშირების ოპტიმიზაცია. 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 არასოდეს გადაიწერება. - **ექსკლუზიური allowlist**: `EGRESS_BUCKETED_LOCK_PROVIDERS`-ის გაფართოება მფლობელის აშკარა გადაწყვეტილებაა; ზოგადი ინტეგრაცია არ არსებობს (ნიმუში #10334/#10419). მონათესავე კავშირების მოთხოვნა SQL-ის ლიტერალად გამეორების ნაცვლად იმავე allowlist-ს აკავშირებს, ამიტომ მისი გაფართოება ერთსტრიქონიან ცვლილებად რჩება. - **გამავალი IP-ის როტაცია, ორივე მიმართულებით**: ძიების ფანჯარა (24h) ბევრად აღემატება გამავალი IP-ის კეშის TTL-ს (5 min), ამიტომ „ბოლო ცნობილი IP“ ისტორიაა და არა მიმდინარე მდგომარეობა. თუ კავშირის proxy ფანჯრის ფარგლებში შეიცვალა, ბლოკირებამ შეიძლება **გამოტოვოს** ნამდვილად საზიარო IP (ჩაწერილი IP ახალი, ამოუწურავია) — და სიმეტრიულად, მან შეიძლება **cooldown მიანიჭოს მონათესავე კავშირს, რომელიც ამასობაში გადავიდა** ამოწურული IP-დან. მეორე შემთხვევა ამ მონათესავე კავშირს ერთ cooldown-ის ფანჯარას უჯდება; ორივე მიღებულია, როგორც ისტორიაზე დაფუძნებული ძიების მაქსიმალური მცდელობის შეზღუდვები. - **ღირებულება**: `proxy_logs`-ის ორი შეზღუდული სკანირება (ფანჯრით გაფილტრული `idx_pl_timestamp`-ის მეშვეობით), მხოლოდ 429-ის სიხშირით. ახალი ინდექსი არ არის საჭირო (მიგრაცია 134 YAGNI). გაზომილია ზომიერი მოცულობის რეალური ტრაფიკის DB-ის ასლზე; მაღალი გამტარუნარიანობის ინსტანციაში იმავე ფანჯარაში პროპორციულად მეტი ჩანაწერი ინახება. --- ## მდგრადობის სხვა ფუნქციები - **მარშრუტიზაციის 19 სტრატეგია** (პრიორიტეტული, შეწონილი, ციკლური, კონტექსტის გადაცემით, ჯერ შევსებით, p2c, შემთხვევითი, ყველაზე ნაკლებად გამოყენებული, ხარჯზე ოპტიმიზებული, განულების გათვალისწინებით, განულების ფანჯრით, თავისუფალი რესურსის მიხედვით, მკაცრად შემთხვევითი, ავტომატური, lkgp, კონტექსტზე ოპტიმიზებული, კეშზე ოპტიმიზებული, შერწყმული, კონვეიერული) — იხილეთ [AUTO-COMBO.md](../routing/AUTO-COMBO.md). - **განულების გათვალისწინებით მარშრუტიზაცია** (v3.8.0) — კავშირებს პრიორიტეტს კვოტის განულების დროის მიხედვით ანიჭებს. - **ფონური რეჟიმის დეგრადაცია** — Responses API-ის `background: true` სინქრონულ რეჟიმზე გადადის გაფრთხილებით. - **ინსტრუმენტების ლიმიტის დინამიკური გამოვლენა** — ინსტრუმენტების რაოდენობის ლიმიტის მიღწევისას პროვაიდერების გამოყენებას ამცირებს. - **ავარიული სარეზერვო მექანიზმი** — იმართება `OMNIROUTE_EMERGENCY_FALLBACK`-ით; ოპერატორებს შეუძლიათ მისი გადაფარვა ფუნქციური ალმების გვერდიდან გადატვირთვის გარეშე. --- ## გამართვა - შეწონილი combo აბრუნებს `503 all_targets_cooling_down`-ს (`Retry-After` დაყენებულია, ხოლო `diagnostics.excluded` ჩამოთვლის ყველა სამიზნეს `model_lockout` / `circuit_open` / `provider_cooldown` / `unavailable` მიზეზებით) → პული კონფიგურირებული და დაკავშირებულია, უბრალოდ ყველა სამიზნე გამორიცხულია მდგრადობის ტაიმერის მიერ; გაფრთხილება `[COMBO] Weighted selection: every target excluded before dispatch — …` ასახელებს მიზეზებსა და დარჩენილ წამებს. იმავე combo-დან მიღებული `404 no_executable_targets` ნიშნავს, რომ მდგრადობის ტაიმერი არ ყოფილა ჩართული (გასაშვები არაფერია, ან ყველა ანგარიშმა ხელმისაწვდომობის შემოწმება ვერ გაიარა). ჩაშენებულია `open-sse/services/combo/pinRecovery.ts`-ში, `targetResolution.ts`-ში შეგროვებული გამორიცხვების საფუძველზე. - პროვაიდერის ყველა გასაღები გამოტოვებულია → შეამოწმეთ როგორც circuit breaker-ის მდგომარეობა, ისე თითოეული კავშირის `rateLimitedUntil`/`testStatus`. - პროვაიდერი გადატვირთვის ფანჯრის შემდეგაც მუდმივად გამორიცხულია → კოდი `getStatus()`/`canExecute()`-ის ნაცვლად პირდაპირ `state`-ს კითხულობს. - ერთი გასაღები ვერ მუშაობს, დანარჩენები კი უნდა მუშაობდეს → circuit breaker-ის ნაცვლად უპირატესობა მიანიჭეთ კავშირის cooldown-ს. - მხოლოდ ერთი მოდელი ვერ მუშაობს → კავშირის cooldown-ის ნაცვლად უპირატესობა მიანიჭეთ მოდელის lockout-ს. - მდგომარეობა ავტომატურად უნდა აღდგეს, მაგრამ არ აღდგა → შეამოწმეთ მომავლის დროის ნიშნული და წაკითხვის გზა, რომელიც ვადაგასულ მდგომარეობას განაახლებს. მუდმივი სტატუსები ხელით შეცვლას მოითხოვს. --- ## TLS-ანაბეჭდები და შეუმჩნევლობა პროვაიდერისთვის სპეციფიკური შეუმჩნევლობის მექანიზმები (JA3/JA4, CCH, ობფუსკაცია) ცალკეა დოკუმენტირებული — იხილეთ `docs/security/STEALTH_GUIDE.md` (git; `/docs`-ში კომპილირებული არ არის). --- ## მდგრადობის ტესტირება (ფაზა 8 · ბლოკი C) მდგრადობის ლოგიკის მოდულური ტესტების გარდა, სამი ტესტი ამოწმებს შესრულების გარემოს რეალური დატვირთვის/მარცხის პირობებში (ყველა ინტეგრაციული/ღამის ტესტია — არცერთი არ ბლოკავს PR-ებს): | ტესტი | რას ამოწმებს | გაშვება | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | | ქაოსი | ყალბი ზემდგომი კვანძი ახდენს რეალური დაყოვნების/განულების/ტაიმაუტის/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-ფაქტორიანი შეფასება, რეჟიმების პაკეტები