From 86a20762f80bace2c44709cc41c94aa0a5e350f6 Mon Sep 17 00:00:00 2001 From: x3M3x Date: Thu, 20 Aug 2026 01:21:59 +0400 Subject: [PATCH 1/2] feat(combos): add random, least-used, and reset-window routing strategies --- .../src/content/docs/fr/guides/combos.md | 6 +- .../fr/reference/configuration/routing.md | 4 +- docs-site/src/content/docs/guides/combos.md | 29 ++++- .../src/content/docs/ja/guides/combos.md | 6 +- .../ja/reference/configuration/routing.md | 4 +- .../src/content/docs/ko/guides/combos.md | 6 +- .../content/docs/ko/reference/cli/agents.md | 2 +- .../ko/reference/configuration/routing.md | 4 +- .../src/content/docs/reference/cli/agents.md | 2 +- .../docs/reference/configuration/routing.md | 13 ++- .../src/content/docs/ru/guides/combos.md | 6 +- .../content/docs/ru/reference/cli/agents.md | 2 +- .../ru/reference/configuration/routing.md | 4 +- .../src/content/docs/tr/guides/combos.md | 7 +- .../tr/reference/configuration/routing.md | 5 +- .../src/content/docs/zh-cn/guides/combos.md | 6 +- .../zh-cn/reference/configuration/routing.md | 4 +- .../src/content/docs/zh-tw/guides/combos.md | 6 +- .../docs/zh-tw/reference/cli/agents.md | 2 +- .../zh-tw/reference/configuration/routing.md | 4 +- gui/src/combo-workspace-data.ts | 27 ++++- gui/src/components/ComboWorkspace.tsx | 3 + .../components/combo-workspace-controls.tsx | 5 +- .../combo-workspace-detail-panel.tsx | 4 +- .../combo-workspace-overview-panel.tsx | 3 + gui/src/i18n/de.ts | 15 +++ gui/src/i18n/en.ts | 15 +++ gui/src/i18n/fr.ts | 15 +++ gui/src/i18n/ja.ts | 15 +++ gui/src/i18n/ko.ts | 15 +++ gui/src/i18n/ru.ts | 15 +++ gui/src/i18n/tr.ts | 15 +++ gui/src/i18n/zh-TW.ts | 15 +++ gui/src/i18n/zh.ts | 15 +++ src/cli/combo.ts | 6 +- src/cli/help.ts | 2 +- src/cli/registry.ts | 2 +- src/combos/index.ts | 1 + src/combos/reset-window.ts | 43 +++++++ src/combos/resolve.ts | 86 +++++++++++++- src/combos/types.ts | 7 +- src/providers/quota-routing-cache.ts | 32 ++++++ src/providers/quota.ts | 7 ++ src/router.ts | 4 +- src/types/config.ts | 4 +- tests/combos.test.ts | 107 +++++++++++++++++- 46 files changed, 528 insertions(+), 72 deletions(-) create mode 100644 src/combos/reset-window.ts create mode 100644 src/providers/quota-routing-cache.ts diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md index b8f76fc52f..fb8c5d3fea 100644 --- a/docs-site/src/content/docs/fr/guides/combos.md +++ b/docs-site/src/content/docs/fr/guides/combos.md @@ -309,9 +309,9 @@ Les combos sont stockés dans l'objet `combos` de niveau supérieur, saisi par l | Champ | Obligatoire | Par défaut | Règles | | --- | --- | --- | --- | | `targets` | Oui | — | Tableau ordonné non vide de `{ provider, model, weight? }` cibles configurées. Les paires provider/model en double sont rejetées. | -| `targets[].weight` | Non | `1` | Entier de 1 à 10 000. Utilisé en round-robin ; ignoré par le basculement. | -| `strategy` | Non | `"failover"` | `"failover"` ou `"round-robin"`. | -| `stickyLimit` | Non | `1` | Nombre entier de 1 à 100 requêtes réussies par sélection à tour de rôle. | +| `targets[].weight` | Non | `1` | Entier de 1 à 10 000. Utilisé en round-robin et random ; ignoré par les autres stratégies. | +| `strategy` | Non | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` ou `"reset-window"`. | +| `stickyLimit` | Non | `1` | Nombre entier de 1 à 100 requêtes réussies par sélection round-robin ; ignoré par les autres stratégies. | | `defaultEffort` | Non | `null` | `low`, `medium`, `high`, `xhigh`, `max` ou `ultra` ; appliqué uniquement lorsque l'appelant omet ses efforts et que la cible annonce son soutien. | | `imageInput` | Non | `"auto"` | `"auto"` ou `"disabled"`. `"auto"` publie les images uniquement si toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias` | Non | aucun | Identifiant de modèle public tronqué facultatif ; utilisez les règles d'alias ci-dessus. Une valeur vide est stockée sans alias. | diff --git a/docs-site/src/content/docs/fr/reference/configuration/routing.md b/docs-site/src/content/docs/fr/reference/configuration/routing.md index ab0b1718ca..1764898985 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/fr/reference/configuration/routing.md @@ -46,8 +46,8 @@ Chaque clé de combinaison est un identifiant conforme à `[A-Za-z0-9][A-Za-z0-9 | Clé | Type | Valeur par défaut | Signification | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | requis | Routes concrètes ordonnées. `weight` est compris entre 1 et 10000 et vaut `1` par défaut. | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Stratégie de sélection. L’ordre des cibles définit la priorité de repli ; les poids produisent une rotation pondérée régulière. | -| `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot de rotation. Plage de 1 à 100. | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Stratégie de sélection. L’ordre départage les égalités ; les poids s’appliquent à round-robin et random. | +| `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot round-robin. Plage de 1 à 100 ; ignoré par les autres stratégies. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | non défini | Appliqué uniquement lorsque l’appelant ne précise aucun effort et que la cible sélectionnée annonce le niveau demandé. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publie les images uniquement lorsque toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias?` | `string` | — | Identifiant public facultatif du modèle, à la place du slug canonique du sélecteur. | diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 6bc5f3b6ee..fb14a76ccd 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -1,6 +1,6 @@ --- -title: "Combos: failover and load balancing" -description: Route one virtual model to several providers for failover or weighted load balancing. +title: "Combos: failover and routing strategies" +description: Route one virtual model to several providers for failover, load balancing, or cached quota recovery. --- A **combo** is one virtual model that fronts an ordered list of real provider/model targets. Your @@ -170,6 +170,25 @@ Weights are relative, not percentages. Weights `2,1` and `200,100` express the s small values that communicate intent. ::: +### Random and least-used selection + +`random` chooses a target independently on each request, with `weight` controlling the chance of +selection. `least-used` chooses the eligible target with the fewest successful requests recorded by +this proxy process; configuration order breaks a tie. + +### Reset window: prefer the nearest known recovery + +`reset-window` selects the eligible target whose earliest cached quota-window reset is nearest. It +does not make a provider API request while routing: quota data is the most recently successful +dashboard/API probe. A target with no fresh reset timestamp ranks after a target with one, and +configured order breaks ties or supplies the fallback when no target has quota data. + +```bash +ocx combo set recovering \ + --targets zai/glm-5.3,anthropic/claude-opus-4-8 \ + --strategy reset-window +``` + ## What happens when a target fails Combo failures are divided into **hop** failures and **terminal** failures. @@ -308,9 +327,9 @@ Combos are stored in the top-level `combos` object, keyed by combo id: | Field | Required | Default | Rules | | --- | --- | --- | --- | | `targets` | Yes | — | Non-empty ordered array of configured `{ provider, model, weight? }` targets. Duplicate provider/model pairs are rejected. | -| `targets[].weight` | No | `1` | Integer from 1 to 10,000. Used by round-robin; ignored by failover. | -| `strategy` | No | `"failover"` | `"failover"` or `"round-robin"`. | -| `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection. | +| `targets[].weight` | No | `1` | Integer from 1 to 10,000. Used by round-robin and random; ignored by the other strategies. | +| `strategy` | No | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"`, or `"reset-window"`. | +| `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection; ignored by the other strategies. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; applied only when the caller omits effort and the target advertises support. | | `imageInput` | No | `"auto"` | `"auto"` or `"disabled"`. `"auto"` publishes image support only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. | diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md index 7cad2b0b6e..0846437753 100644 --- a/docs-site/src/content/docs/ja/guides/combos.md +++ b/docs-site/src/content/docs/ja/guides/combos.md @@ -211,9 +211,9 @@ ocx combo remove --yes |フィールド |必須 |デフォルト |ルール | | --- | --- | --- | --- | | `targets` |はい | — |構成された `{ provider, model, weight? }` ターゲットの空でない順序付けされた配列。重複するプロバイダーとモデルのペアは拒否されます。 | -| `targets[].weight` |いいえ | `1` | 1 ~ 10,000 の整数。ラウンドロビンで使用されます。フェイルオーバーによって無視されます。 | -| `strategy` |いいえ | `"failover"` | `"failover"` または `"round-robin"`。 | -| `stickyLimit` |いいえ | `1` |ラウンドロビン選択ごとの成功したリクエストの数は 1 ~ 100 の整数です。 | +| `targets[].weight` |いいえ | `1` | 1 ~ 10,000 の整数。round-robin と random で使用され、他の戦略では無視されます。 | +| `strategy` |いいえ | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"`、または `"reset-window"`。 | +| `stickyLimit` |いいえ | `1` |round-robin 選択ごとの成功リクエスト数。1 ~ 100 の整数で、他の戦略では無視されます。 | | `defaultEffort` |いいえ | `null` | `low`、`medium`、`high`、`xhigh`、`max`、または `ultra`;呼び出し元が努力を省略し、ターゲットがサポートをアドバタイズした場合にのみ適用されます。 | | `alias` |いいえ |なし |オプションのトリミングされたパブリック モデル ID。上記のエイリアス ルールを使用します。空の値はエイリアスなしで保存されます。 | | `nativeAlias` |いいえ | `false` | 現在サポートされている bare native alias に routing/catalog の優先権を明示的に与えます。 | diff --git a/docs-site/src/content/docs/ja/reference/configuration/routing.md b/docs-site/src/content/docs/ja/reference/configuration/routing.md index de0cb31ab6..ea9b4466d6 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ja/reference/configuration/routing.md @@ -57,8 +57,8 @@ picker catalog の convergence だけが保留中で routing change は失われ |キー |タイプ |デフォルト |意味 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` |必須 |具体的なルートを指示しました。 `weight` は 1 ~ 10000 で、デフォルトは `1` です。 | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` |選択戦略。ターゲットの順序はフェイルオーバーの優先順位です。重みはスムーズな重み付きラウンドロビンを形成します。 | -| `stickyLimit?` | `number` | `1` |成功したリクエストは 1 つのラウンドロビン バッチに保持されます。範囲は 1 ~ 100。 | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` |選択戦略。ターゲット順は同点を解決し、重みは round-robin と random に使用されます。 | +| `stickyLimit?` | `number` | `1` |1 つの round-robin バッチに保持する成功リクエスト数。範囲は 1 ~ 100。他の戦略では無視されます。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` |設定を解除する |呼び出し元が努力を省略し、選択されたターゲットが要求されたラングをアドバタイズする場合にのみ適用されます。 | | `alias?` | `string` | — |正規のピッカー スラグの代わりのオプションのパブリック モデル ID。 | | `nativeAlias?` | `boolean` | `false` | 現在サポートされている bare native id に限り、その未修飾 id で優先します。アカウント修飾およびプロバイダー修飾の OpenAI ルートは別のままです。 | diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index cec9ed6258..76d755320a 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -210,9 +210,9 @@ ocx combo remove --yes | 필드 | 필수 | 기본값 | 규칙 | | --- | --- | --- | --- | | `targets` | 예 | — | 설정된 `{ provider, model, weight? }` 대상의 비어 있지 않은 순서가 있는 배열이어야 합니다. 중복된 provider/model 쌍은 거부됩니다. | -| `targets[].weight` | 아니요 | `1` | 1에서 10,000 사이의 정수입니다. round-robin에서 사용되며 failover에서는 무시됩니다. | -| `strategy` | 아니요 | `"failover"` | `"failover"` 또는 `"round-robin"`입니다. | -| `stickyLimit` | 아니요 | `1` | round-robin 선택 한 번당 성공 요청 1에서 100회 사이의 정수입니다. | +| `targets[].weight` | 아니요 | `1` | 1에서 10,000 사이의 정수입니다. round-robin과 random에서 사용되며 다른 전략에서는 무시됩니다. | +| `strategy` | 아니요 | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"`, 또는 `"reset-window"`입니다. | +| `stickyLimit` | 아니요 | `1` | round-robin 선택 한 번당 성공 요청 1에서 100회 사이의 정수이며 다른 전략에서는 무시됩니다. | | `defaultEffort` | 아니요 | `null` | `low`, `medium`, `high`, `xhigh`, `max`, 또는 `ultra`입니다. 호출자가 effort를 생략하고 대상이 지원을 광고할 때만 적용됩니다. | | `alias` | 아니요 | 없음 | 선택적으로 앞뒤 공백을 제거한 공개 모델 ID입니다. 위의 alias 규칙을 따릅니다. 빈 값은 alias 없음으로 저장됩니다. | | `nativeAlias` | 아니요 | `false` | 현재 지원되는 bare native alias가 routing/catalog 우선권을 갖도록 명시적으로 허용합니다. | diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index e2b77cf713..2591e333e9 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -46,7 +46,7 @@ mode와 flag 전환은 현재 숫자 thread 한도를 유효한 v1/v2 Codex key ### `ocx combo ...` · `ocx route combo ...` -콤보 failover와 round-robin 가상 모델을 관리합니다. `ocx route combo`는 계층형 별칭이며, +여러 선택 전략을 사용하는 콤보 가상 모델을 관리합니다. `ocx route combo`는 계층형 별칭이며, 현재 지원되는 라우팅 리소스는 combo입니다. 대상은 `provider/model[:weight],provider/model[:weight]` 형식을 사용합니다. ```bash diff --git a/docs-site/src/content/docs/ko/reference/configuration/routing.md b/docs-site/src/content/docs/ko/reference/configuration/routing.md index c22a13d07d..ee872d6bf0 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ko/reference/configuration/routing.md @@ -56,8 +56,8 @@ Codex Auth 페이지에서 이 picker 동작을 opt-in할 수 있습니다. 비 | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 순서가 있는 concrete route입니다. `weight`는 1–10000이며 기본값은 `1`입니다. | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 선택 전략입니다. 대상 순서는 failover 우선순위이며, weight는 smooth weighted round-robin의 모양을 결정합니다. | -| `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100입니다. | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 선택 전략입니다. 대상 순서는 동률을 결정하며, weight는 round-robin과 random에 사용됩니다. | +| `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100이며 다른 전략에서는 무시됩니다. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 호출자가 effort를 생략했고 선택된 대상이 요청한 rung를 광고할 때만 적용됩니다. | | `alias?` | `string` | — | 정규화된 picker slug 대신 쓰는 선택적 공개 model id입니다. | | `nativeAlias?` | `boolean` | `false` | 현재 지원되는 bare native id가 해당 비수식 id에만 우선하도록 합니다. 계정 또는 프로바이더로 수식된 OpenAI route는 별도로 유지됩니다. | diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 931f22b3f5..da157d2843 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -80,7 +80,7 @@ gate: it requires the native feature to be enabled with an explicit v2 surface ### `ocx combo ...` · `ocx route combo ...` -Manage combo failover and round-robin virtual models. `ocx route combo` is the hierarchical alias; +Manage combo virtual models and routing strategies. `ocx route combo` is the hierarchical alias; combo is currently the supported routing resource. Targets use `provider/model[:weight],provider/model[:weight]`. diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 795d2210e4..ffa9f33f78 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -73,8 +73,8 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Ordered concrete routes. `weight` is 1–10000 and defaults to `1`. | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Selection strategy. Target order is failover priority; weights shape smooth weighted round-robin. | -| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Selection strategy. Target order is failover priority and breaks ties. Weights shape round-robin and random selection. | +| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100; ignored by other strategies. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Applied only when the caller omits effort and the selected target advertises the requested rung. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | @@ -98,6 +98,11 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, } ``` +`reset-window` prefers eligible targets whose earliest known quota reset is soonest. It reads only +the last successfully cached provider-quota snapshot, so request routing never waits for an upstream +quota probe. Missing or stale reset data ranks after known data; when every target is unknown, the +configured target order remains the fallback. + For strategy behavior, retryable failures, cooldowns, encrypted v2 task limits, and management commands, see [Combos](/guides/combos/). @@ -183,8 +188,8 @@ echoed as given. The CLI dry-run cannot supply these per-candidate account field ### Combos vs policy profiles -- A **combo** is explicit ordered/weighted target routing and failover: the configured order (or - smooth weighted round-robin) decides, and failures advance through the list. +- A **combo** is explicit target routing and failover: configured order, weighted selection, + successful-use history, or cached quota-reset timing decides, and failures advance through the list. - A **policy profile** is evidence-based selection among configured candidates: hard capability requirements filter first, then deterministic scoring ranks the survivors. diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index b2baf981cc..f656655b41 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -260,9 +260,9 @@ Combo хранятся в объекте верхнего уровня `combos`, | Поле | Обязательно | По умолчанию | Правила | | --- | --- | --- | --- | | `targets` | Yes | — | Непустой упорядоченный массив настроенных целей `{ provider, model, weight? }`. Дубли пар provider/model запрещены. | -| `targets[].weight` | No | `1` | Целое число от 1 до 10 000. Используется в round-robin; при failover игнорируется. | -| `strategy` | No | `"failover"` | `"failover"` или `"round-robin"`. | -| `stickyLimit` | No | `1` | Целое число от 1 до 100 успешных запросов на один выбор round-robin. | +| `targets[].weight` | No | `1` | Целое число от 1 до 10 000. Используется в round-robin и random; другие стратегии игнорируют его. | +| `strategy` | No | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` или `"reset-window"`. | +| `stickyLimit` | No | `1` | Целое число от 1 до 100 успешных запросов на один выбор round-robin; другие стратегии игнорируют поле. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max` или `ultra`; применяется только когда вызывающая сторона не указала effort, а цель объявляет поддержку. | | `alias` | No | none | Необязательный обрезанный публичный id модели; используйте правила alias выше. Пустое значение хранится как отсутствие alias. | | `nativeAlias` | No | `false` | Явно разрешает поддерживаемому сейчас bare native alias перехватить приоритет routing/catalog только для неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct; маршруты с квалификатором аккаунта сохраняют свою идентичность, а provider-qualified `openai-apikey/gpt-5.6-*` использует API-ключ и никогда не переходит на native alias. | diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 6f09a90d04..75e739244a 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -51,7 +51,7 @@ Codex. При переходах mode и feature flag текущий число ### `ocx combo ...` · `ocx route combo ...` -Управляйте virtual-моделями combo с failover и round-robin. `ocx route combo` — это иерархический +Управляйте virtual-моделями combo с несколькими стратегиями выбора. `ocx route combo` — это иерархический alias; на данный момент combo — единственный поддерживаемый routing-resource. Цели используют форму `provider/model[:weight],provider/model[:weight]`. diff --git a/docs-site/src/content/docs/ru/reference/configuration/routing.md b/docs-site/src/content/docs/ru/reference/configuration/routing.md index 7249bd8ecf..892f24d039 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ru/reference/configuration/routing.md @@ -71,8 +71,8 @@ selector-qualified строки и возвращает обычные GPT-ст | Ключ | Тип | По умолчанию | Значение | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Упорядоченные конкретные маршруты. `weight` находится в диапазоне 1–10000 и по умолчанию равен `1`. | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Стратегия выбора. Порядок целей задаёт приоритет failover; веса формируют плавный взвешенный round-robin. | -| `stickyLimit?` | `number` | `1` | Число успешных запросов, удерживаемых в одной партии round-robin. Диапазон 1–100. | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Стратегия выбора. Порядок целей разрешает ничьи; веса используются в round-robin и random. | +| `stickyLimit?` | `number` | `1` | Число успешных запросов в одной партии round-robin. Диапазон 1–100; остальные стратегии игнорируют это поле. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Применяется, только если вызывающая сторона не задала effort, а выбранная цель объявляет эту ступень. | | `alias?` | `string` | — | Необязательный публичный id модели вместо канонического slug в селекторе. | | `nativeAlias?` | `boolean` | `false` | Даёт поддерживаемому bare native id приоритет только для этого неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct. Маршруты с квалификатором аккаунта остаются отдельными. Провайдер-квалифицированные маршруты, например `openai-apikey/gpt-5.6-*`, используют настроенный API-ключ и никогда не переходят на native alias. | diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md index 6662b88c10..959cc4c724 100644 --- a/docs-site/src/content/docs/tr/guides/combos.md +++ b/docs-site/src/content/docs/tr/guides/combos.md @@ -341,9 +341,9 @@ saklanır: | Alan | Gerekli | Varsayılan | Kurallar | | --- | --- | --- | --- | | `targets` | Evet | — | Yapılandırılmış `{ provider, model, weight? }` hedeflerinin boş olmayan sıralı dizisi. Yinelenen sağlayıcı/model çiftleri reddedilir. | -| `targets[].weight` | Hayır | `1` | 1 ile 10.000 arasında tam sayı. Round-robin tarafından kullanılır; yük devretme tarafından yok sayılır. | -| `strategy` | Hayır | `"failover"` | `"failover"` veya `"round-robin"`. | -| `stickyLimit` | Hayır | `1` | Round-robin seçimi başına 1 ile 100 arasında başarılı istek tam sayısı. | +| `targets[].weight` | Hayır | `1` | 1 ile 10.000 arasında tam sayı. Round-robin ve random tarafından kullanılır; diğer stratejilerce yok sayılır. | +| `strategy` | Hayır | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` veya `"reset-window"`. | +| `stickyLimit` | Hayır | `1` | Round-robin seçimi başına 1 ile 100 arasında başarılı istek tam sayısı; diğer stratejilerce yok sayılır. | | `defaultEffort` | Hayır | `null` | `low`, `medium`, `high`, `xhigh`, `max` veya `ultra`; yalnızca arayan çabayı atladığında ve hedef desteği bildirdiğinde uygulanır. | | `alias` | Hayır | yok | İsteğe bağlı kırpılmış genel model kimliği; yukarıdaki takma ad kurallarını kullanın. Boş bir değer takma ad yok olarak saklanır. | | `nativeAlias` | Hayır | `false` | Şu anda desteklenen yalın bir yerel `alias`'ın yönlendirme ve katalog önceliği almasına açıkça izin verin. Asla takma addan çıkarılmaz. | @@ -381,4 +381,3 @@ Hata hedefe özgü olmaktan ziyade uç (terminal) bir hataydı. Geçersiz girdiy düzeltin, aşırı büyük bir bağlamı azaltın, bir politika reddini işleyin veya reddedilen istek kaynağını düzeltin. Kombolar bu durumlar için atlama yapmaz. - diff --git a/docs-site/src/content/docs/tr/reference/configuration/routing.md b/docs-site/src/content/docs/tr/reference/configuration/routing.md index cdb7e76572..98b7dbee3b 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/tr/reference/configuration/routing.md @@ -92,8 +92,8 @@ aileleri kullanamaz. | Anahtar | Tip | Varsayılan | Anlamı | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | gerekli | Sıralı somut rotalar. `weight` 1–10000 arasındadır ve varsayılan olarak `1`'dir. | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Seçim stratejisi. Hedef sırası yük devretme önceliğidir; ağırlıklar pürüzsüz ağırlıklı round-robin'i şekillendirir. | -| `stickyLimit?` | `number` | `1` | Tek bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100. | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Seçim stratejisi. Hedef sırası eşitlikleri bozar; ağırlıklar round-robin ve random için kullanılır. | +| `stickyLimit?` | `number` | `1` | Bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100; diğer stratejilerce yok sayılır. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | ayarlanmamış | Yalnızca arayan çabayı atladığında ve seçilen hedef istenen basamağı bildirdiğinde uygulanır. | | `alias?` | `string` | — | Kurallı seçici slug'ı yerine isteğe bağlı genel model kimliği. | | `nativeAlias?` | `boolean` | `false` | Şu anda desteklenen bir yalın yerel kimliğin yalnızca o niteliksiz kimlik için öncelikli olmasına izin verin. Yalın `gpt-5.6-*` kimlikleri Codex Havuz/Direct kimlik bilgilerini kullanır. Hesap nitelikli rotalar ayrı kalır. `openai-apikey/gpt-5.6-*` gibi sağlayıcı nitelikli rotalar yapılandırılmış API anahtarı rotalarını kullanır ve asla yerel takma ada düşmez. | @@ -294,4 +294,3 @@ otomatik bir yeniden oluşturmayı tetikler; `ocx logs rebuild-index` bunu zorla Bu sistemdeki hiçbir şey ağırlıkları, bütçeleri veya aday kümelerini otomatik olarak ayarlamaz. - diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md index fe89fc04c7..d99c7e709b 100644 --- a/docs-site/src/content/docs/zh-cn/guides/combos.md +++ b/docs-site/src/content/docs/zh-cn/guides/combos.md @@ -241,9 +241,9 @@ combo 会存储在顶层的 `combos` 对象中,并以 combo id 作为键: | 字段 | 必填 | 默认值 | 规则 | | --- | --- | --- | --- | | `targets` | 是 | — | 非空、有顺序的数组,元素为已配置的 `{ provider, model, weight? }` 目标。重复的 provider/model 对会被拒绝。 | -| `targets[].weight` | 否 | `1` | 1 到 10,000 的整数。轮询会使用它;故障切换会忽略它。 | -| `strategy` | 否 | `"failover"` | `"failover"` 或 `"round-robin"`。 | -| `stickyLimit` | 否 | `1` | 每次轮询选择可连续处理的成功请求数,范围为 1 到 100。 | +| `targets[].weight` | 否 | `1` | 1 到 10,000 的整数。round-robin 和 random 会使用它;其他策略会忽略它。 | +| `strategy` | 否 | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"` 或 `"reset-window"`。 | +| `stickyLimit` | 否 | `1` | 每次 round-robin 选择可连续处理的成功请求数,范围为 1 到 100;其他策略会忽略它。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;仅当调用方省略 effort 且目标声明支持时才会应用。 | | `imageInput` | 否 | `"auto"` | `"auto"` 或 `"disabled"`。`"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias` | 否 | 无 | 可选的、已修剪的公开模型 id;使用上面的别名规则。空值会以“无别名”形式存储。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md index 11a3ad97ce..1652cac5f4 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md @@ -61,8 +61,8 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏 | 键 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 有序的具体路由。`weight` 范围为 1–10000,默认值为 `1`。 | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 选择策略。目标顺序表示故障切换优先级;权重会影响平滑加权轮询。 | -| `stickyLimit?` | `number` | `1` | 在单个轮询批次中保留的成功请求数。范围 1–100。 | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 选择策略。目标顺序用于打破平局;权重用于 round-robin 和 random。 | +| `stickyLimit?` | `number` | `1` | 单个 round-robin 批次中保留的成功请求数。范围 1–100;其他策略会忽略它。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 仅在调用方省略 effort 且所选目标声明了请求的档位时应用。 | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias?` | `string` | — | 可选的公开 model id,用于替代规范化的选择器 slug。 | diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md index ac0efda056..320c46c26b 100644 --- a/docs-site/src/content/docs/zh-tw/guides/combos.md +++ b/docs-site/src/content/docs/zh-tw/guides/combos.md @@ -249,9 +249,9 @@ Combo 儲存於頂層 `combos` 物件中,以 combo id 為 key: | 欄位 | 必填 | 預設值 | 規則 | | --- | --- | --- | --- | | `targets` | 是 | — | 已設定 `{ provider, model, weight? }` 目標的非空有序陣列。重複的供應商/模型對會被拒絕。 | -| `targets[].weight` | 否 | `1` | 1 到 10,000 的整數。由 round-robin 使用;failover 忽略。 | -| `strategy` | 否 | `"failover"` | `"failover"` 或 `"round-robin"`。 | -| `stickyLimit` | 否 | `1` | 每次 round-robin 選擇的成功請求數,1 到 100 的整數。 | +| `targets[].weight` | 否 | `1` | 1 到 10,000 的整數。由 round-robin 與 random 使用;其他策略忽略。 | +| `strategy` | 否 | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"` 或 `"reset-window"`。 | +| `stickyLimit` | 否 | `1` | 每次 round-robin 選擇的成功請求數,1 到 100 的整數;其他策略忽略。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;僅在呼叫者省略 effort 且目標宣告支援時套用。 | | `alias` | 否 | 無 | 可選的修剪後公開模型 id;使用上述別名規則。空值儲存為無別名。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md index e8e7955d67..0a1f8f3d4c 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md @@ -43,7 +43,7 @@ ocx v2 threads 16 ### `ocx combo ...` · `ocx route combo ...` -管理組合 failover 與 round-robin 虛擬模型。`ocx route combo` 是階層式別名;組合是目前支援的路由資源。目標使用 +管理具有多種選擇策略的組合虛擬模型。`ocx route combo` 是階層式別名;組合是目前支援的路由資源。目標使用 `provider/model[:weight],provider/model[:weight]`。 ```bash diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md index e2ca4f3622..ae2708b136 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md @@ -44,8 +44,8 @@ Codex Auth 頁面將此 picker 行為作為選擇加入功能暴露。停用它 | Key | 型別 | 預設值 | 意義 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | 必填 | 有序的具體路由。`weight` 為 1–10000,預設 `1`。 | -| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 選擇策略。目標順序為 failover 優先序;權重塑造平滑的加權 round-robin。 | -| `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100。 | +| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 選擇策略。目標順序用於打破平手;權重用於 round-robin 與 random。 | +| `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100;其他策略會忽略它。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | 未設定 | 僅在呼叫者省略 effort 且所選目標廣告請求的階層時套用。 | | `alias?` | `string` | — | 可選的公開模型 id,取代標準 picker slug。 | | `nativeAlias?` | `boolean` | `false` | 讓目前支援的裸原生 id 僅對該未限定 id 取得優先。裸 `gpt-5.6-*` id 使用 Codex 池/Direct 憑證。帳號限定路由保持獨立。供應商限定路由(如 `openai-apikey/gpt-5.6-*`)使用其設定的 API-key 路由,且永不會落到原生別名。 | diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index 4f5e96e0b7..a6a15fdda6 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -7,7 +7,7 @@ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../../src/codex/catalog/native-mo export { SUPPORTED_NATIVE_OPENAI_SLUGS }; -export type ComboStrategy = "failover" | "round-robin"; +export type ComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window"; export type ComboEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra"; export const COMBO_EFFORTS: ComboEffort[] = ["low", "medium", "high", "xhigh", "max", "ultra"]; @@ -85,6 +85,9 @@ export interface ComboItem { export interface ComboSections { failover: ComboItem[]; roundRobin: ComboItem[]; + weighted: ComboItem[]; + leastUsed: ComboItem[]; + resetWindow: ComboItem[]; } export interface ComboAttentionItem { @@ -130,7 +133,15 @@ function normalizeAlias(raw: unknown): string | null { } export function normalizeStrategy(raw: unknown): ComboStrategy { - return raw === "round-robin" ? "round-robin" : "failover"; + return raw === "round-robin" + ? "round-robin" + : raw === "random" + ? "random" + : raw === "least-used" + ? "least-used" + : raw === "reset-window" + ? "reset-window" + : "failover"; } export function normalizeStickyLimit(raw: unknown): number { @@ -193,11 +204,17 @@ export function parseComboList(payload: unknown): ComboItem[] { export function groupCombos(items: ComboItem[]): ComboSections { const failover: ComboItem[] = []; const roundRobin: ComboItem[] = []; + const weighted: ComboItem[] = []; + const leastUsed: ComboItem[] = []; + const resetWindow: ComboItem[] = []; for (const item of items) { if (item.strategy === "round-robin") roundRobin.push(item); + else if (item.strategy === "random") weighted.push(item); + else if (item.strategy === "least-used") leastUsed.push(item); + else if (item.strategy === "reset-window") resetWindow.push(item); else failover.push(item); } - return { failover, roundRobin }; + return { failover, roundRobin, weighted, leastUsed, resetWindow }; } export function filterCombos(items: ComboItem[], query: string): ComboItem[] { @@ -270,7 +287,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} id: item.id.trim(), ...(options.renameFrom ? { renameFrom: options.renameFrom } : {}), combo: { - targets: item.targets.map((target) => item.strategy === "round-robin" + targets: item.targets.map((target) => (item.strategy === "round-robin" || item.strategy === "random") ? { provider: target.provider.trim(), model: target.model.trim(), weight: target.weight ?? 1 } : { provider: target.provider.trim(), model: target.model.trim() }), strategy: item.strategy, @@ -360,6 +377,8 @@ export function validateComboDraft( if (!Number.isInteger(item.stickyLimit) || item.stickyLimit < 1 || item.stickyLimit > 100) { return "invalidStickyLimit"; } + } + if (item.strategy === "round-robin" || item.strategy === "random") { for (const target of item.targets) { const weight = target.weight ?? 1; if (!Number.isInteger(weight) || weight < 1 || weight > 10000) return "invalidWeight"; diff --git a/gui/src/components/ComboWorkspace.tsx b/gui/src/components/ComboWorkspace.tsx index 66cd751f64..3e41a0f882 100644 --- a/gui/src/components/ComboWorkspace.tsx +++ b/gui/src/components/ComboWorkspace.tsx @@ -128,6 +128,9 @@ export default function ComboWorkspace({ {([ ["failover", sections.failover, "cws.group.failover"], ["round-robin", sections.roundRobin, "cws.group.roundRobin"], + ["weighted", sections.weighted, "cws.group.weighted"], + ["leastUsed", sections.leastUsed, "cws.group.leastUsed"], + ["resetWindow", sections.resetWindow, "cws.group.resetWindow"], ] as const).map(([key, items, labelKey]) => ( items.length > 0 ? (
diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 0a7ecaa65d..806a4bd759 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -24,6 +24,9 @@ export function StrategySeg({ {([ ["failover", "cws.strategy.failover"], ["round-robin", "cws.strategy.roundRobin"], + ["random", "cws.strategy.random"], + ["least-used", "cws.strategy.leastUsed"], + ["reset-window", "cws.strategy.resetWindow"], ] as const).map(([id, key]) => (
@@ -352,7 +352,7 @@ export function DetailPanel({ onChange={(targets) => updateDraft((d) => ({ ...d, targets }))} />

- {draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")} + {draft.strategy === "failover" ? t("cws.targets.failoverHint") : draft.strategy === "round-robin" ? t("cws.targets.roundRobinHint") : draft.strategy === "random" ? t("cws.targets.randomHint") : draft.strategy === "least-used" ? t("cws.targets.leastUsedHint") : t("cws.targets.resetWindowHint")}

{combos.length}{t("cws.count.total")}
{sections.failover.length}{t("cws.count.failover")}
{sections.roundRobin.length}{t("cws.count.roundRobin")}
+
{sections.weighted.length}{t("cws.count.weighted")}
+
{sections.leastUsed.length}{t("cws.count.leastUsed")}
+
{sections.resetWindow.length}{t("cws.count.resetWindow")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 41ae5d02e5..5438e7f144 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1850,6 +1850,9 @@ export const de: Record = { "cws.noSearchResults": "Keine Combos passen zur Suche.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-Robin", + "cws.group.weighted": "Gewichtet", + "cws.group.leastUsed": "Wenigste Nutzung", + "cws.group.resetWindow": "Zurücksetzfenster", "cws.targetCount": "{count} Ziele", "cws.targetCountOne": "1 Ziel", "cws.overviewTitle": "Combos", @@ -1857,6 +1860,9 @@ export const de: Record = { "cws.count.total": "Gesamt", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-Robin", + "cws.count.weighted": "Gewichtet", + "cws.count.leastUsed": "Wenigste Nutzung", + "cws.count.resetWindow": "Zurücksetzfenster", "cws.howTitle": "So funktioniert es", "cws.howBody": "Fordern Sie in Codex den öffentlichen Modellnamen der Combo an. Ohne eigenen Namen gilt combo/. OpenCodex wählt ein Ziel und springt nur bei wiederholbaren Upstream-Fehlern. Ist kein Ziel verfügbar, schlägt die Anfrage geschlossen fehl, statt den globalen Standardanbieter zu verwenden.", "cws.attentionTitle": "Aufmerksamkeit nötig", @@ -1875,7 +1881,13 @@ export const de: Record = { "cws.strategy": "Strategie", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-Robin", + "cws.strategy.random": "Zufall", + "cws.strategy.leastUsed": "Am wenigsten genutzt", + "cws.strategy.resetWindow": "Zurücksetzfenster", "cws.strategy.failoverHint": "Ziele der Reihe nach versuchen. Bei einem wiederholbaren Fehler (Limit, Ausfall, Abo-Sperre) zum nächsten springen.", + "cws.strategy.randomHint": "Gewichtete Zufallsauswahl. Ziele mit hoherer Gewichtung werden haufiger gewahlt.", + "cws.strategy.resetWindowHint": "Bevorzugt Ziele, deren bekanntes Kontingentfenster am frühesten zurückgesetzt wird. Ziele ohne bekannte Rücksetzzeit bleiben Ausweichziele.", + "cws.strategy.leastUsedHint": "Wahlt das Ziel mit den wenigsten aufgezeichneten Nutzungen.", "cws.strategy.roundRobinHint": "Datenverkehr deterministisch nach Gewicht verteilen. Das gewählte Ziel für einen Block erfolgreicher Anfragen behalten und dann weiterschalten.", "cws.field.id": "Combo-ID", "cws.field.idHintEdit": "Das Ändern der ID benennt die Combo um. Clients fordern {model} an.", @@ -1902,6 +1914,9 @@ export const de: Record = { "cws.targets": "Ziele", "cws.targets.failoverHint": "Reihenfolge zählt — das erste ist primär.", "cws.targets.roundRobinHint": "Gewichte steuern die deterministische relative Auswahl; die Reihenfolge löst Gleichstände im Rotationsring.", + "cws.targets.randomHint": "Gewichte steuern die Wahrscheinlichkeit, mit der ein Ziel pro Anfrage ausgewählt wird.", + "cws.targets.leastUsedHint": "Die Reihenfolge löst Gleichstände bei derselben Anzahl erfolgreicher Nutzungen.", + "cws.targets.resetWindowHint": "Die Reihenfolge löst Gleichstände und dient als Rückfall, wenn keine zwischengespeicherte Kontingent-Rücksetzzeit vorliegt.", "cws.target.provider": "Anbieter", "cws.target.model": "Modell", "cws.target.weight": "Gewicht", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 01d69f0fd1..4147222f5f 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1890,6 +1890,9 @@ export const en = { "cws.noSearchResults": "No combos match your search.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-robin", + "cws.group.weighted": "Weighted", + "cws.group.leastUsed": "Least used", + "cws.group.resetWindow": "Reset window", "cws.targetCount": "{count} targets", "cws.targetCountOne": "1 target", "cws.overviewTitle": "Combos", @@ -1897,6 +1900,9 @@ export const en = { "cws.count.total": "Total", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-robin", + "cws.count.weighted": "Weighted", + "cws.count.leastUsed": "Least used", + "cws.count.resetWindow": "Reset window", "cws.howTitle": "How it works", "cws.howBody": "Ask Codex for the combo's public model name. Without one, the default is combo/. OpenCodex selects a target and hops only on retryable upstream failures. If no target remains available, the request fails closed instead of using the global default provider.", "cws.attentionTitle": "Needs attention", @@ -1915,8 +1921,14 @@ export const en = { "cws.strategy": "Strategy", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-robin", + "cws.strategy.random": "Random", + "cws.strategy.leastUsed": "Least used", + "cws.strategy.resetWindow": "Reset window", "cws.strategy.failoverHint": "Try targets in order. If the first fails with a retryable error (rate limit, outage, subscription gate), hop to the next.", "cws.strategy.roundRobinHint": "Deterministically balance traffic by weight. Keep each selected target for a batch of successful requests, then advance.", + "cws.strategy.randomHint": "Weighted random selection. Higher-weight targets are more likely to be chosen on each request.", + "cws.strategy.leastUsedHint": "Pick the target with the fewest recorded uses, spreading load across all targets.", + "cws.strategy.resetWindowHint": "Prefer targets whose known quota window resets soonest. Targets without a known reset time remain fallback targets.", "cws.field.id": "Combo id", "cws.field.idHint": "Clients will request {model}", "cws.field.idInternalHint": "Internal combo id. You can change it after creation.", @@ -1942,6 +1954,9 @@ export const en = { "cws.targets": "Targets", "cws.targets.failoverHint": "Order matters — first is primary.", "cws.targets.roundRobinHint": "Weights control deterministic relative selection; order breaks ties in the rotation ring.", + "cws.targets.randomHint": "Weights control the chance of each target being selected on a request.", + "cws.targets.leastUsedHint": "Order breaks ties when targets have the same successful-use count.", + "cws.targets.resetWindowHint": "Order breaks ties and remains the fallback when no cached quota reset time is available.", "cws.target.provider": "Provider", "cws.target.model": "Model", "cws.target.weight": "Weight", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 3765f4d95f..bd555430e1 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1851,6 +1851,9 @@ export const fr: Record = { "cws.noSearchResults": "Aucune combinaison ne correspond à votre recherche.", "cws.group.failover": "Basculement", "cws.group.roundRobin": "Rotation", + "cws.group.weighted": "Pondere", + "cws.group.leastUsed": "Moins utilise", + "cws.group.resetWindow": "Fenêtre de réinitialisation", "cws.targetCount": "{count} cibles", "cws.targetCountOne": "1 cible", "cws.overviewTitle": "Combinaisons", @@ -1858,6 +1861,9 @@ export const fr: Record = { "cws.count.total": "Total", "cws.count.failover": "Basculement", "cws.count.roundRobin": "Rotation", + "cws.count.weighted": "Pondere", + "cws.count.leastUsed": "Moins utilise", + "cws.count.resetWindow": "Fenêtre de réinitialisation", "cws.howTitle": "Fonctionnement", "cws.howBody": "Demandez à Codex le nom de modèle public de la combinaison. Sans nom, combo/ est utilisé par défaut. OpenCodex sélectionne une cible et ne bascule qu’en cas d’échec réessayable en amont. Si aucune cible ne reste disponible, la requête est bloquée au lieu d’utiliser le fournisseur global par défaut.", "cws.attentionTitle": "Intervention requise", @@ -1880,7 +1886,13 @@ export const fr: Record = { "cws.strategy": "Stratégie", "cws.strategy.failover": "Basculement", "cws.strategy.roundRobin": "Rotation", + "cws.strategy.random": "Aleatoire", + "cws.strategy.leastUsed": "Moins utilise", + "cws.strategy.resetWindow": "Fenêtre de réinitialisation", "cws.strategy.failoverHint": "Essaie les cibles dans l’ordre. Si la première échoue avec une erreur réessayable (limite de débit, panne, restriction d’abonnement), passe à la suivante.", + "cws.strategy.randomHint": "Selection aleatoire ponderee. Les cibles avec un poids plus eleve sont plus susceptibles detre choisies.", + "cws.strategy.resetWindowHint": "Privilégie les cibles dont la fenêtre de quota connue se réinitialise le plus tôt. Celles sans heure connue restent des solutions de repli.", + "cws.strategy.leastUsedHint": "Choisit la cible avec le moins dutilisations enregistrees.", "cws.strategy.roundRobinHint": "Répartit le trafic de manière déterministe selon les pondérations. Conserve chaque cible sélectionnée pendant un lot de requêtes réussies, puis passe à la suivante.", "cws.field.id": "Identifiant de la combinaison", "cws.field.idHint": "Les clients demanderont {model}", @@ -1903,6 +1915,9 @@ export const fr: Record = { "cws.targets": "Cibles", "cws.targets.failoverHint": "L’ordre est important — la première est la cible principale.", "cws.targets.roundRobinHint": "Les pondérations contrôlent la sélection relative déterministe; l’ordre départage les égalités dans l’anneau de rotation.", + "cws.targets.randomHint": "Les pondérations contrôlent la probabilité de sélection de chaque cible pour une requête.", + "cws.targets.leastUsedHint": "L’ordre départage les cibles ayant le même nombre d’utilisations réussies.", + "cws.targets.resetWindowHint": "L’ordre départage les égalités et reste la solution de repli lorsqu’aucune heure de réinitialisation de quota n’est en cache.", "cws.target.provider": "Fournisseur", "cws.target.model": "Modèle", "cws.target.weight": "Pondération", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 63e20efd3b..8e66f5765f 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1907,6 +1907,9 @@ export const ja: Record = { "cws.searchPlaceholder": "コンボやターゲットを検索…", "cws.noSearchResults": "検索に一致するコンボがありません。", "cws.group.failover": "フェイルオーバー", + "cws.group.weighted": "重み付き", + "cws.group.leastUsed": "最少使用", + "cws.group.resetWindow": "リセットウィンドウ", "cws.group.roundRobin": "ラウンドロビン", "cws.targetCount": "{count} ターゲット", "cws.targetCountOne": "1 ターゲット", @@ -1914,6 +1917,9 @@ export const ja: Record = { "cws.overviewBlurb": "プロバイダー/モデルターゲット間でフェイルオーバーする、または決定論的で滑らかな重み付きラウンドロビンを使う仮想モデル。", "cws.count.total": "合計", "cws.count.failover": "フェイルオーバー", + "cws.count.weighted": "重み付き", + "cws.count.leastUsed": "最少使用", + "cws.count.resetWindow": "リセットウィンドウ", "cws.count.roundRobin": "ラウンドロビン", "cws.howTitle": "仕組み", "cws.howBody": "Codex に combo/ を要求します。OpenCodex はターゲットを選び、再試行可能な上流の失敗時のみホップします。利用可能なターゲットが残っていない場合、グローバルなデフォルトプロバイダーを使わずにフェイルクローズします。", @@ -1933,8 +1939,14 @@ export const ja: Record = { "cws.tab.about": "概要", "cws.strategy": "ストラテジー", "cws.strategy.failover": "フェイルオーバー", + "cws.strategy.random": "ランダム", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "リセットウィンドウ", "cws.strategy.roundRobin": "ラウンドロビン", "cws.strategy.failoverHint": "ターゲットを順に試します。最初が再試行可能なエラー(レート制限、障害、サブスクリプションゲート)で失敗した場合、次へホップします。", + "cws.strategy.randomHint": "重み付きランダム選択。重みが高いターゲットが選ばれやすくなります。", + "cws.strategy.resetWindowHint": "既知のクォータウィンドウが最も早くリセットされるターゲットを優先します。リセット時刻が不明なターゲットはフォールバックのままです。", + "cws.strategy.leastUsedHint": "記録された使用回数が最も少ないターゲットを選択します。", "cws.strategy.roundRobinHint": "重みで決定論的にトラフィックを分散します。選んだターゲットを成功リクエストのバッチ分保持し、次へ進みます。", "cws.field.id": "コンボ ID", "cws.field.idHint": "クライアントは {model} をリクエストします", @@ -1961,6 +1973,9 @@ export const ja: Record = { "cws.targets": "ターゲット", "cws.targets.failoverHint": "順序が重要 — 最初がプライマリです。", "cws.targets.roundRobinHint": "重みが決定論的な相対選択を制御し、順序がローテーションリングの同点を解消します。", + "cws.targets.randomHint": "重みは、各リクエストでターゲットが選ばれる確率を制御します。", + "cws.targets.leastUsedHint": "成功した使用回数が同じ場合は順序で同点を解消します。", + "cws.targets.resetWindowHint": "順序で同点を解消し、キャッシュ済みのクォータリセット時刻がない場合のフォールバックになります。", "cws.target.provider": "プロバイダー", "cws.target.model": "モデル", "cws.target.weight": "重み", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b13cd141ff..2388b8b9a3 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1876,6 +1876,9 @@ export const ko: Record = { "cws.searchPlaceholder": "콤보 또는 대상 검색…", "cws.noSearchResults": "검색과 일치하는 콤보가 없습니다.", "cws.group.failover": "장애 조치", + "cws.group.weighted": "가중치", + "cws.group.leastUsed": "최소 사용", + "cws.group.resetWindow": "재설정 기간", "cws.group.roundRobin": "라운드로빈", "cws.targetCount": "대상 {count}개", "cws.targetCountOne": "대상 1개", @@ -1883,6 +1886,9 @@ export const ko: Record = { "cws.overviewBlurb": "프로바이더/모델 대상 사이에서 장애 조치하거나 결정적 Smooth Weighted 라운드로빈을 사용하는 가상 모델입니다.", "cws.count.total": "전체", "cws.count.failover": "장애 조치", + "cws.count.weighted": "가중치", + "cws.count.leastUsed": "최소 사용", + "cws.count.resetWindow": "재설정 기간", "cws.count.roundRobin": "라운드로빈", "cws.howTitle": "동작 방식", "cws.howBody": "Codex에서 콤보의 공개 모델 이름을 요청하세요. 설정하지 않으면 combo/가 기본값입니다. OpenCodex는 재시도 가능한 업스트림 오류에서만 다음 대상으로 넘깁니다. 사용 가능한 대상이 없으면 전역 기본 프로바이더로 우회하지 않고 요청을 실패 처리합니다.", @@ -1901,8 +1907,14 @@ export const ko: Record = { "cws.tab.about": "정보", "cws.strategy": "전략", "cws.strategy.failover": "장애 조치", + "cws.strategy.random": "무작위", + "cws.strategy.leastUsed": "최소 사용", + "cws.strategy.resetWindow": "재설정 기간", "cws.strategy.roundRobin": "라운드로빈", "cws.strategy.failoverHint": "대상을 순서대로 시도합니다. 재시도 가능한 오류(한도, 장애, 구독 게이트)면 다음으로 넘어갑니다.", + "cws.strategy.randomHint": "가중 무작위 선택. 가중치가 높은 대상이 선택될 가능성이 높습니다.", + "cws.strategy.resetWindowHint": "알려진 할당량 기간이 가장 빨리 재설정되는 대상을 우선합니다. 재설정 시간이 알려지지 않은 대상은 대체 대상으로 남습니다.", + "cws.strategy.leastUsedHint": "기록된 사용 횟수가 가장 적은 대상을 선택합니다.", "cws.strategy.roundRobinHint": "가중치에 따라 트래픽을 결정적으로 분배합니다. 선택된 대상을 성공 요청 묶음 동안 유지한 뒤 다음 대상으로 진행합니다.", "cws.field.id": "콤보 ID", "cws.field.idHintEdit": "ID를 변경하면 콤보 이름이 바뀝니다. 클라이언트는 {model}을(를) 요청합니다.", @@ -1929,6 +1941,9 @@ export const ko: Record = { "cws.targets": "대상", "cws.targets.failoverHint": "순서가 중요합니다 — 첫 번째가 기본입니다.", "cws.targets.roundRobinHint": "가중치는 결정적 상대 선택을 제어하고, 순서는 회전 고리의 동률을 결정합니다.", + "cws.targets.randomHint": "가중치는 요청마다 각 대상이 선택될 확률을 제어합니다.", + "cws.targets.leastUsedHint": "성공 사용 횟수가 같은 대상은 순서로 동률을 해소합니다.", + "cws.targets.resetWindowHint": "순서로 동률을 해소하며, 캐시된 할당량 재설정 시간이 없을 때 대체 순서로 사용됩니다.", "cws.target.provider": "프로바이더", "cws.target.model": "모델", "cws.target.weight": "가중치", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 05cf793779..a27c1f4670 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1959,6 +1959,9 @@ export const ru: Record = { "cws.searchPlaceholder": "Поиск комбо или целей…", "cws.noSearchResults": "Нет комбо, соответствующих запросу.", "cws.group.failover": "Failover", + "cws.group.weighted": "Взвешенный", + "cws.group.leastUsed": "Наименее исп.", + "cws.group.resetWindow": "Окно сброса", "cws.group.roundRobin": "Round-robin", "cws.targetCount": "{count} целей", "cws.targetCountOne": "1 цель", @@ -1966,6 +1969,9 @@ export const ru: Record = { "cws.overviewBlurb": "Виртуальные модели, которые при сбоях переключаются между целями провайдер/модель (отказоустойчивое переключение, failover) или используют детерминированный плавный взвешенный циклический перебор (round-robin).", "cws.count.total": "Всего", "cws.count.failover": "Failover", + "cws.count.weighted": "Взвешенный", + "cws.count.leastUsed": "Наименее исп.", + "cws.count.resetWindow": "Окно сброса", "cws.count.roundRobin": "Round-robin", "cws.howTitle": "Как это работает", "cws.howBody": "Запросите у Codex публичное имя модели комбо. Если оно не задано, используется combo/. OpenCodex выбирает цель и переключается на следующую только при сбоях вышестоящего провайдера, допускающих повтор. Если доступных целей не осталось, запрос завершается ошибкой, а не переходит на глобальный провайдер по умолчанию.", @@ -1984,8 +1990,14 @@ export const ru: Record = { "cws.tab.about": "О комбо", "cws.strategy": "Стратегия", "cws.strategy.failover": "Failover", + "cws.strategy.random": "Случайный", + "cws.strategy.leastUsed": "Наименее используемый", + "cws.strategy.resetWindow": "Окно сброса", "cws.strategy.roundRobin": "Round-robin", "cws.strategy.failoverHint": "Цели перебираются по порядку. Если первая завершается ошибкой, допускающей повтор (лимит запросов, сбой, ограничение подписки), происходит переключение на следующую.", + "cws.strategy.randomHint": "Взвешенный случайный выбор. Цели с большим весом выбираются чаще.", + "cws.strategy.resetWindowHint": "Предпочитает цели, чьё известное окно квоты сбросится раньше. Цели без известного времени сброса остаются резервными.", + "cws.strategy.leastUsedHint": "Выбирает цель с наименьшим количеством записей использования.", "cws.strategy.roundRobinHint": "Детерминированное распределение трафика по весам. Выбранная цель удерживается на серию успешных запросов, затем селектор переходит к следующей.", "cws.field.id": "Id комбо", "cws.field.idHint": "Клиенты будут запрашивать {model}", @@ -2012,6 +2024,9 @@ export const ru: Record = { "cws.targets": "Цели", "cws.targets.failoverHint": "Порядок важен — первая цель основная.", "cws.targets.roundRobinHint": "Веса задают детерминированный относительный выбор; при равных весах порядок определяет очерёдность в кольце ротации.", + "cws.targets.randomHint": "Веса задают вероятность выбора каждой цели для запроса.", + "cws.targets.leastUsedHint": "Порядок разрешает равенство при одинаковом числе успешных использований.", + "cws.targets.resetWindowHint": "Порядок разрешает равенства и служит запасным вариантом, когда нет кэшированного времени сброса квоты.", "cws.target.provider": "Провайдер", "cws.target.model": "Модель", "cws.target.weight": "Вес", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index c98421183e..506f37a979 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1879,6 +1879,9 @@ export const tr: Record = { "cws.searchPlaceholder": "Kombolarda veya hedeflerde ara…", "cws.noSearchResults": "Aramanızla eşleşen kombo yok.", "cws.group.failover": "Yedekli (Failover)", + "cws.group.weighted": "Agirlikli", + "cws.group.leastUsed": "En az kullanilan", + "cws.group.resetWindow": "Sıfırlama penceresi", "cws.group.roundRobin": "Round-robin", "cws.targetCount": "{count} hedef", "cws.targetCountOne": "1 hedef", @@ -1886,6 +1889,9 @@ export const tr: Record = { "cws.overviewBlurb": "Sağlayıcı/model hedefleri arasında devreden sanal modeller.", "cws.count.total": "Toplam", "cws.count.failover": "Failover", + "cws.count.weighted": "Agirlikli", + "cws.count.leastUsed": "En az kullanilan", + "cws.count.resetWindow": "Sıfırlama penceresi", "cws.count.roundRobin": "Round-robin", "cws.howTitle": "Nasıl çalışır", "cws.howBody": "Codex'ten kombonun kamuya açık model adını isteyin. Bir ad olmadan varsayılan combo/ şeklindedir. OpenCodex bir hedef seçer ve yalnızca yeniden denenebilir yukarı akış hatalarında atlar. Hiçbir hedef kullanılabilir kalmazsa, istek küresel varsayılan sağlayıcıyı kullanmak yerine kapalı olarak başarısız olur (fail closed).", @@ -1904,8 +1910,14 @@ export const tr: Record = { "cws.tab.about": "Hakkında", "cws.strategy": "Strateji", "cws.strategy.failover": "Yedekli (Failover)", + "cws.strategy.random": "Rastgele", + "cws.strategy.leastUsed": "En az kullanilan", + "cws.strategy.resetWindow": "Sıfırlama penceresi", "cws.strategy.roundRobin": "Round-robin", "cws.strategy.failoverHint": "Hedefleri sırayla deneyin. İlk hedef yeniden denenebilir bir hatayla (oran sınırı, kesinti, abonelik engeli) başarısız olursa sonraki hedefe atlayın.", + "cws.strategy.randomHint": "Agirlikli rastgele secim. Daha yuksek agirlikli hedefler daha sik secilir.", + "cws.strategy.resetWindowHint": "Bilinen kota penceresi en erken sıfırlanacak hedefleri tercih eder. Sıfırlama zamanı bilinmeyen hedefler yedek olarak kalır.", + "cws.strategy.leastUsedHint": "En az kaydedilmis kullanima sahip hedefi secer.", "cws.strategy.roundRobinHint": "Trafiği ağırlığa göre kararlı bir şekilde dengeleyin. Seçilen her hedefi bir dizi başarılı istek boyunca tutun, ardından ilerleyin.", "cws.field.id": "Kombo ID", "cws.field.idHint": "İstemciler {model} isteyecek", @@ -1932,6 +1944,9 @@ export const tr: Record = { "cws.targets": "Hedefler", "cws.targets.failoverHint": "Sıralama önemlidir — birincil olan ilktir.", "cws.targets.roundRobinHint": "Ağırlıklar kararlı bağıntılı seçimi kontrol eder; sıralama rotasyon halkasındaki eşitlikleri bozar.", + "cws.targets.randomHint": "Ağırlıklar, her hedefin bir istekte seçilme olasılığını belirler.", + "cws.targets.leastUsedHint": "Başarılı kullanım sayısı eşit olan hedeflerde sıralama eşitliği bozar.", + "cws.targets.resetWindowHint": "Sıralama eşitlikleri bozar ve önbellekte kota sıfırlama zamanı yoksa yedek sıra olarak kalır.", "cws.target.provider": "Sağlayıcı", "cws.target.model": "Model", "cws.target.weight": "Ağırlık", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index d35a96bd34..3e80378dc8 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1408,6 +1408,9 @@ export const zhTW: Record = { "cws.searchPlaceholder": "搜尋組合或目標…", "cws.noSearchResults": "沒有符合的組合。", "cws.group.failover": "容錯移轉", + "cws.group.weighted": "加權", + "cws.group.leastUsed": "最少使用", + "cws.group.resetWindow": "重設視窗", "cws.group.roundRobin": "輪詢", "cws.targetCount": "{count} 個目標", "cws.targetCountOne": "1 個目標", @@ -1415,6 +1418,9 @@ export const zhTW: Record = { "cws.overviewBlurb": "可在供應商/模型目標間容錯移轉,或採用確定性平滑加權輪詢的虛擬模型。", "cws.count.total": "總計", "cws.count.failover": "容錯移轉", + "cws.count.weighted": "加權", + "cws.count.leastUsed": "最少使用", + "cws.count.resetWindow": "重設視窗", "cws.count.roundRobin": "輪詢", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中請求組合的公開模型名稱;未設定時預設使用 combo/。OpenCodex 僅在可重試的上游錯誤時切換目標。若沒有可用目標,請求會直接失敗,不會回退到全域性預設供應商。", @@ -1432,8 +1438,14 @@ export const zhTW: Record = { "cws.tab.about": "關於", "cws.strategy": "策略", "cws.strategy.failover": "容錯移轉", + "cws.strategy.random": "隨機", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "重設視窗", "cws.strategy.roundRobin": "輪詢", "cws.strategy.failoverHint": "按順序嘗試目標。若出現可重試錯誤(限流、故障、訂閱門控),則跳到下一個。", + "cws.strategy.randomHint": "加權隨機選擇。權重較高的目標更可能被選中。", + "cws.strategy.resetWindowHint": "優先選擇已知配額視窗最早重設的目標。沒有已知重設時間的目標仍會作為後備目標。", + "cws.strategy.leastUsedHint": "選擇記錄使用次數最少的目標。", "cws.strategy.roundRobinHint": "按權重確定性地分配流量。將所選目標保留一批成功請求後,再推進到下一個目標。", "cws.field.id": "組合 ID", "cws.field.idHint": "客戶端將請求 {model}", @@ -1456,6 +1468,9 @@ export const zhTW: Record = { "cws.targets": "目標", "cws.targets.failoverHint": "順序很重要 — 第一個為主。", "cws.targets.roundRobinHint": "權重控制確定性的相對選擇;順序用於打破輪換環中的平局。", + "cws.targets.randomHint": "權重控制每個目標在請求中被選中的機率。", + "cws.targets.leastUsedHint": "成功使用次數相同時,順序用於打破平局。", + "cws.targets.resetWindowHint": "順序用於打破平局,並在沒有快取的配額重設時間時作為後備順序。", "cws.target.provider": "供應商", "cws.target.model": "模型", "cws.target.weight": "權重", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index dc749ac530..6cb3f14276 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1869,6 +1869,9 @@ export const zh: Record = { "cws.searchPlaceholder": "搜索组合或目标…", "cws.noSearchResults": "没有匹配的组合。", "cws.group.failover": "故障转移", + "cws.group.weighted": "加权", + "cws.group.leastUsed": "最少使用", + "cws.group.resetWindow": "重设窗口", "cws.group.roundRobin": "轮询", "cws.targetCount": "{count} 个目标", "cws.targetCountOne": "1 个目标", @@ -1876,6 +1879,9 @@ export const zh: Record = { "cws.overviewBlurb": "可在提供方/模型目标间故障转移,或采用确定性平滑加权轮询的虚拟模型。", "cws.count.total": "总计", "cws.count.failover": "故障转移", + "cws.count.weighted": "加权", + "cws.count.leastUsed": "最少使用", + "cws.count.resetWindow": "重设窗口", "cws.count.roundRobin": "轮询", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中请求组合的公开模型名称;未设置时默认使用 combo/。OpenCodex 仅在可重试的上游错误时切换目标。若没有可用目标,请求会直接失败,不会回退到全局默认提供方。", @@ -1894,8 +1900,14 @@ export const zh: Record = { "cws.tab.about": "关于", "cws.strategy": "策略", "cws.strategy.failover": "故障转移", + "cws.strategy.random": "随机", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "重设窗口", "cws.strategy.roundRobin": "轮询", "cws.strategy.failoverHint": "按顺序尝试目标。若出现可重试错误(限流、故障、订阅门控),则跳到下一个。", + "cws.strategy.randomHint": "加权随机选择。权重较高的目标更可能被选中。", + "cws.strategy.resetWindowHint": "优先选择已知配额窗口最早重设的目标。没有已知重设时间的目标仍会作为后备目标。", + "cws.strategy.leastUsedHint": "选择记录使用次数最少的目标。", "cws.strategy.roundRobinHint": "按权重确定性地分配流量。将所选目标保留一批成功请求后,再推进到下一个目标。", "cws.field.id": "组合 ID", "cws.field.idHint": "客户端将请求 {model}", @@ -1922,6 +1934,9 @@ export const zh: Record = { "cws.targets": "目标", "cws.targets.failoverHint": "顺序很重要 — 第一个为主。", "cws.targets.roundRobinHint": "权重控制确定性的相对选择;顺序用于打破轮换环中的平局。", + "cws.targets.randomHint": "权重控制每个目标在请求中被选中的概率。", + "cws.targets.leastUsedHint": "成功使用次数相同时,顺序用于打破平局。", + "cws.targets.resetWindowHint": "顺序用于打破平局,并在没有缓存的配额重设时间时作为后备顺序。", "cws.target.provider": "提供方", "cws.target.model": "模型", "cws.target.weight": "权重", diff --git a/src/cli/combo.ts b/src/cli/combo.ts index de324eed19..73f72f643b 100644 --- a/src/cli/combo.ts +++ b/src/cli/combo.ts @@ -14,7 +14,7 @@ const USAGE = `Usage: ocx combo [list] [--json] ocx combo show [--json] ocx combo set --targets - [--strategy ] [--sticky <1-100>] + [--strategy ] [--sticky <1-100>] [--effort ] [--alias ] [--native-alias] [--display-name ] [--rename-from ] [--json] @@ -73,7 +73,7 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise { const targetsRaw = takeOption(args, "--targets"); if (!targetsRaw) throw new CliUsageError("--targets is required", USAGE); const strategy = takeOption(args, "--strategy") ?? "failover"; - if (strategy !== "failover" && strategy !== "round-robin") throw new CliUsageError("--strategy must be failover or round-robin", USAGE); + if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, or reset-window", USAGE); const stickyLimit = takeIntegerOption(args, "--sticky", { min: 1 }) ?? 1; if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE); const effort = takeOption(args, "--effort"); @@ -84,9 +84,9 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise { rejectArgs(args, USAGE); const combo: Record = { strategy, - stickyLimit, targets: parseTargets(targetsRaw), }; + if (strategy === "round-robin") combo.stickyLimit = stickyLimit; if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort; if (alias !== undefined) combo.alias = alias === "-" ? "" : alias; if (nativeAlias) combo.nativeAlias = true; diff --git a/src/cli/help.ts b/src/cli/help.ts index c0408e659a..75b7240844 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -52,7 +52,7 @@ Usage: ocx provider Providers, connectivity, quota, and selected models ocx account Accounts, login/reauth, key pools, and quota controls ocx models Live/custom models, visibility, context, and shadow calls - ocx combo Combo failover/round-robin routing + ocx combo Combo routing strategies and failover ocx agent Subagents, injection, effort caps, and sidecars ocx observe Logs, usage, storage, memory, and debug data ocx route Routing features (combo, policy) diff --git a/src/cli/registry.ts b/src/cli/registry.ts index 85bb3959e5..641b1b64f8 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -189,7 +189,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [ { name: "combo", usage: "ocx combo ...", - summary: "Manage combo failover and round-robin virtual models.", + summary: "Manage combo virtual models and routing strategies.", details: ["Alias hierarchy: ocx route combo ...", "Use --targets provider/model[:weight],provider/model[:weight]."], }, { diff --git a/src/combos/index.ts b/src/combos/index.ts index 571eb540d5..502e210dc6 100644 --- a/src/combos/index.ts +++ b/src/combos/index.ts @@ -43,3 +43,4 @@ export { concreteComboRequestBody, resetComboEffortWarningStateForTests, } from "./request"; +export { earliestQuotaResetAt, quotaResetRemainingMs } from "./reset-window"; diff --git a/src/combos/reset-window.ts b/src/combos/reset-window.ts new file mode 100644 index 0000000000..83e73dcb54 --- /dev/null +++ b/src/combos/reset-window.ts @@ -0,0 +1,43 @@ +import type { ProviderQuota } from "../providers/quota"; + +function collectResetCandidates(quota: ProviderQuota): number[] { + const candidates: number[] = []; + if (typeof quota.fiveHourResetAt === "number") candidates.push(quota.fiveHourResetAt); + if (typeof quota.weeklyResetAt === "number") candidates.push(quota.weeklyResetAt); + if (typeof quota.monthlyResetAt === "number") candidates.push(quota.monthlyResetAt); + if (quota.customWindows) { + for (const w of quota.customWindows) { + if (typeof w.resetAt === "number") candidates.push(w.resetAt); + } + } + return candidates; +} + +/** + * Earliest future reset timestamp from a cached provider quota snapshot, + * or null when no fresh quota data exists or all resets have elapsed. + */ +export function earliestQuotaResetAt( + quota: ProviderQuota | null, + now: number, +): number | null { + if (!quota) return null; + const future = collectResetCandidates(quota).filter(ts => ts > now); + if (future.length > 0) return Math.min(...future); + return null; +} + +/** + * Milliseconds until the soonest known quota-window reset. + * Returns Infinity when no quota data exists, quota is stale, or all known + * reset timestamps have elapsed. An elapsed reset is stale evidence — it + * does not prove the next request has fresh capacity. + */ +export function quotaResetRemainingMs( + quota: ProviderQuota | null, + now: number, +): number { + const nearest = earliestQuotaResetAt(quota, now); + if (nearest === null) return Number.POSITIVE_INFINITY; + return nearest - now; +} diff --git a/src/combos/resolve.ts b/src/combos/resolve.ts index e2e36601a1..4dc5cc0298 100644 --- a/src/combos/resolve.ts +++ b/src/combos/resolve.ts @@ -1,5 +1,7 @@ import type { OcxComboTarget, OcxConfig } from "../types"; +import { getCachedProviderQuota } from "../providers/quota-routing-cache"; import { coolComboTarget, isComboTargetInCooldown } from "./failover"; +import { quotaResetRemainingMs } from "./reset-window"; import { getCombo, resolveComboId, targetKey } from "./types"; import type { NormalizedComboConfig } from "./types"; import { @@ -19,6 +21,7 @@ interface SelectionState { activeKey?: string; successes: number; currentWeights: Map; + successfulUses: Map; } const selectionState = new Map(); @@ -82,6 +85,36 @@ function smoothWeightedIndex( return best; } +/** + * Select the eligible target whose earliest known quota reset is nearest. + * + * Only reads the last successfully cached provider-quota snapshot; it never + * triggers an upstream quota probe. When no target has fresh reset data, + * every remaining value is Infinity and configured order becomes the + * fallback. Targets with elapsed or stale reset timestamps are treated as + * unknown (Infinity). + */ +function resetWindowIndex( + targets: Required[], + eligible: (target: Required) => boolean, + now = Date.now(), +): number { + let selected = -1; + let smallestRemaining = Number.POSITIVE_INFINITY; + for (let index = 0; index < targets.length; index++) { + const target = targets[index]!; + if (!eligible(target)) continue; + const remaining = quotaResetRemainingMs(getCachedProviderQuota(target.provider, now), now); + // Strict comparison deliberately retains configured order for ties, + // including the no-snapshot fallback where every value is Infinity. + if (selected < 0 || remaining < smallestRemaining) { + selected = index; + smallestRemaining = remaining; + } + } + return selected; +} + export function pickComboTarget( config: OcxConfig, comboId: string, @@ -103,7 +136,7 @@ export function pickComboTarget( if (combo.strategy === "round-robin") { let state = selectionState.get(comboId); if (!state) { - state = { successes: 0, currentWeights: new Map() }; + state = { successes: 0, currentWeights: new Map(), successfulUses: new Map() }; selectionState.set(comboId, state); } if (state.activeKey) { @@ -120,6 +153,41 @@ export function pickComboTarget( state.successes = 0; } } + } else if (combo.strategy === "random") { + // Weighted random selection happens independently for every request. + const eligibleTargets = combo.targets + .map((target, index) => ({ target, index })) + .filter(({ target }) => eligible(target)); + if (eligibleTargets.length > 0) { + const totalWeight = eligibleTargets.reduce((sum, entry) => sum + entry.target.weight, 0); + let random = Math.random() * totalWeight; + for (const entry of eligibleTargets) { + random -= entry.target.weight; + if (random <= 0) { + targetIndex = entry.index; + break; + } + } + if (targetIndex < 0) targetIndex = eligibleTargets[eligibleTargets.length - 1]!.index; + } + } else if (combo.strategy === "least-used") { + let state = selectionState.get(comboId); + if (!state) { + state = { successes: 0, currentWeights: new Map(), successfulUses: new Map() }; + selectionState.set(comboId, state); + } + let fewestUses = Number.POSITIVE_INFINITY; + for (let index = 0; index < combo.targets.length; index++) { + const target = combo.targets[index]!; + if (!eligible(target)) continue; + const uses = state.successfulUses.get(targetKey(target)) ?? 0; + if (targetIndex < 0 || uses < fewestUses) { + targetIndex = index; + fewestUses = uses; + } + } + } else if (combo.strategy === "reset-window") { + targetIndex = resetWindowIndex(combo.targets, eligible); } else { targetIndex = combo.targets.findIndex(eligible); } @@ -141,9 +209,18 @@ export function noteComboSuccess( target: Required, writerGeneration = captureConfigGeneration(), ): void { - if (combo.strategy !== "round-robin") return; const key = targetKey(target); if (!mayCommitComboState(comboId, key, writerGeneration)) return; + if (combo.strategy === "least-used") { + let state = selectionState.get(comboId); + if (!state) { + state = { successes: 0, currentWeights: new Map(), successfulUses: new Map() }; + selectionState.set(comboId, state); + } + state.successfulUses.set(key, (state.successfulUses.get(key) ?? 0) + 1); + return; + } + if (combo.strategy !== "round-robin") return; const state = selectionState.get(comboId); if (!state || state.activeKey !== key) return; state.successes += 1; @@ -206,6 +283,11 @@ export function reconcileComboRotationState(context: GenerationContext): number state.currentWeights.delete(key); removed += 1; } + for (const key of state.successfulUses.keys()) { + if (context.comboTargets.has(comboTargetOwnerKey(comboId, key))) continue; + state.successfulUses.delete(key); + removed += 1; + } } liveComboTargets = new Set(context.comboTargets); lastReconciledGeneration = context.generation; diff --git a/src/combos/types.ts b/src/combos/types.ts index d1ac034096..6941e68358 100644 --- a/src/combos/types.ts +++ b/src/combos/types.ts @@ -205,8 +205,11 @@ export function comboConfigIssues( const body = raw as Record; if (body.strategy !== undefined && body.strategy !== "failover" - && body.strategy !== "round-robin") { - issues.push({ path: ["strategy"], message: 'strategy must be "failover" or "round-robin"' }); + && body.strategy !== "round-robin" + && body.strategy !== "random" + && body.strategy !== "least-used" + && body.strategy !== "reset-window") { + issues.push({ path: ["strategy"], message: 'strategy must be "failover", "round-robin", "random", "least-used", or "reset-window"' }); } if (body.stickyLimit !== undefined && (typeof body.stickyLimit !== "number" || !Number.isInteger(body.stickyLimit) diff --git a/src/providers/quota-routing-cache.ts b/src/providers/quota-routing-cache.ts new file mode 100644 index 0000000000..065d7338ca --- /dev/null +++ b/src/providers/quota-routing-cache.ts @@ -0,0 +1,32 @@ +import type { ProviderQuota, ProviderQuotaReport } from "./quota"; + +const quotaCache = new Map(); + +export function clearCachedProviderQuotas(): void { + quotaCache.clear(); +} + +export function replaceCachedProviderQuotas(reports: ProviderQuotaReport[]): void { + quotaCache.clear(); + for (const report of reports) { + quotaCache.set(report.provider, report.quota); + } +} + +export function getCachedProviderQuota( + provider: string, + now: number, + maxAgeMs = 30 * 60_000, +): ProviderQuota | null { + const quota = quotaCache.get(provider); + if (!quota) return null; + if (now - quota.updatedAt > maxAgeMs) return null; + return quota; +} + +export function setCachedProviderQuotaForTests( + provider: string, + quota: ProviderQuota, +): void { + quotaCache.set(provider, quota); +} diff --git a/src/providers/quota.ts b/src/providers/quota.ts index db0202161d..ea27cb737b 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -22,6 +22,10 @@ import { type GenerationContext, } from "../lib/state-store-sweeper"; import { readBoundedResponseBody } from "../lib/bounded-body"; +import { + clearCachedProviderQuotas, + replaceCachedProviderQuotas, +} from "./quota-routing-cache"; import { aggregateCodexPoolCapacity, CODEX_CAPACITY_MAX_QUOTA_AGE_MS, @@ -123,6 +127,7 @@ let invalidationEpoch = 0; /** Invalidate the report cache (e.g. after switching a provider's active account). */ export function clearProviderQuotaCache(): void { cache = null; + clearCachedProviderQuotas(); invalidationEpoch += 1; } @@ -1446,6 +1451,7 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n const reports = cache.response.reports.filter(report => context.providerNames.has(report.provider)); removed += cache.response.reports.length - reports.length; cache = { ...cache, response: { ...cache.response, reports } }; + replaceCachedProviderQuotas(reports); } liveAccountQuotaKeys = new Set(context.oauthAccountKeys); liveProviderQuotaKeys = new Set(context.providerNames); @@ -2283,6 +2289,7 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh ) { const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration)); cache = { key, ts: Date.now(), response: { ...response, reports } }; + replaceCachedProviderQuotas(reports); } return response; })(); diff --git a/src/router.ts b/src/router.ts index 47a604d77c..33c7ee7904 100644 --- a/src/router.ts +++ b/src/router.ts @@ -482,7 +482,7 @@ export function comboRouteDecisionTrace( reason: "combo-pick", candidateIndex: pick.targetIndex, ...(combo - ? { tieBreak: combo.strategy === "round-robin" ? "round-robin" : "failover" } + ? { tieBreak: combo.strategy } : {}), }, candidates: combo ? comboRouteCandidates(config, pick, combo) : undefined, @@ -725,7 +725,7 @@ export function routeModel( reason: route.routeReason, ...(route.combo ? { candidateIndex: route.combo.targetIndex } : {}), ...(combo - ? { tieBreak: combo.strategy === "round-robin" ? "round-robin" : "failover" } + ? { tieBreak: combo.strategy } : {}), }, candidates: route.routeKind === "combo" && route.combo && combo diff --git a/src/types/config.ts b/src/types/config.ts index 3e1801f08d..afc694076a 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -587,7 +587,7 @@ export interface OcxConfig { export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-first"; -export type OcxComboStrategy = "failover" | "round-robin"; +export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window"; export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra"; export interface OcxComboTarget { @@ -599,7 +599,7 @@ export interface OcxComboTarget { export interface OcxComboConfig { targets: OcxComboTarget[]; - /** Ordered failover (default) or deterministic smooth weighted round-robin. */ + /** Ordered failover (default), round-robin, weighted random, least-used, or quota reset-window selection. */ strategy?: OcxComboStrategy; /** Successful requests retained on one RR selection batch. Default 1; range 1..100. */ stickyLimit?: number; diff --git a/tests/combos.test.ts b/tests/combos.test.ts index 70bf3a657b..0168812794 100644 --- a/tests/combos.test.ts +++ b/tests/combos.test.ts @@ -45,6 +45,10 @@ import type { OcxConfig } from "../src/types"; import { syncCatalogModels } from "../src/codex/catalog"; import { injectClaudeAgentDefs } from "../src/claude/agents-inject"; import { reconcileComboRotationState } from "../src/combos/resolve"; +import { + clearCachedProviderQuotas, + setCachedProviderQuotaForTests, +} from "../src/providers/quota-routing-cache"; import { catalogConvergenceFactory } from "./helpers/catalog-convergence"; const VALID_COMBO = { targets: [{ provider: "a", model: "m1" }] }; @@ -156,6 +160,7 @@ async function responseJson(response: Response | null): Promise { clearComboSelectionState(); clearComboTargetCooldowns(); + clearCachedProviderQuotas(); }); describe("combo namespace primitives", () => { @@ -449,6 +454,104 @@ describe("deterministic combo selection", () => { expect(routeModel(config, "combo/free").providerName).toBe("a"); }); + test("random selection is weighted per request and does not inherit round-robin stickiness", () => { + const roundRobin = rrConfig(2, [1, 1]); + expect(pickComboTarget(roundRobin, "free")?.target.provider).toBe("a"); + + const random = baseConfig({ + combos: { + free: { + strategy: "random", + targets: [ + { provider: "a", model: "m1", weight: 1 }, + { provider: "b", model: "m2", weight: 3 }, + ], + }, + }, + }); + const entropy = spyOn(Math, "random"); + try { + entropy.mockReturnValueOnce(0).mockReturnValueOnce(0.5); + expect(pickComboTarget(random, "free")?.target.provider).toBe("a"); + expect(pickComboTarget(random, "free")?.target.provider).toBe("b"); + } finally { + entropy.mockRestore(); + } + }); + + test("least-used selection counts successful requests and preserves configured ties", () => { + const config = baseConfig({ + combos: { + free: { + strategy: "least-used", + targets: [ + { provider: "a", model: "m1" }, + { provider: "b", model: "m2" }, + ], + }, + }, + }); + + expect(successfulPicks(config, 4)).toEqual(["a/m1", "b/m2", "a/m1", "b/m2"]); + }); + + test("reset-window selects the eligible target whose cached quota resets soonest", () => { + const now = Date.now(); + const config = baseConfig({ + combos: { + free: { + strategy: "reset-window", + targets: [ + { provider: "a", model: "m1" }, + { provider: "b", model: "m2" }, + { provider: "c", model: "m3" }, + ], + }, + }, + }); + setCachedProviderQuotaForTests("a", { updatedAt: now, fiveHourResetAt: now + 24 * 60 * 60_000 }); + setCachedProviderQuotaForTests("b", { updatedAt: now, weeklyResetAt: now + 60 * 60_000 }); + setCachedProviderQuotaForTests("c", { updatedAt: now }); + + expect(pickComboTarget(config, "free")?.target.provider).toBe("b"); + expect(routeModel(config, "combo/free").routeDecision?.selected).toMatchObject({ + tieBreak: "reset-window", + }); + }); + + test("reset-window treats elapsed resets as unknown and falls back to configured order", () => { + const now = Date.now(); + const config = baseConfig({ + combos: { + free: { + strategy: "reset-window", + targets: [ + { provider: "a", model: "m1" }, + { provider: "b", model: "m2" }, + { provider: "c", model: "m3" }, + ], + }, + }, + }); + setCachedProviderQuotaForTests("a", { updatedAt: now, fiveHourResetAt: now - 1 }); + setCachedProviderQuotaForTests("b", { updatedAt: now, weeklyResetAt: now + 60 * 60_000 }); + setCachedProviderQuotaForTests("c", { updatedAt: now, monthlyResetAt: now + 60 * 60_000 }); + expect(pickComboTarget(config, "free")?.target.provider).toBe("b"); + + config.providers.a!.disabled = true; + expect(pickComboTarget(config, "free")?.target.provider).toBe("b"); + + clearCachedProviderQuotas(); + setCachedProviderQuotaForTests("b", { + updatedAt: now - 30 * 60_000 - 1, + weeklyResetAt: now + 1, + }); + expect(pickComboTarget(config, "free")?.target.provider).toBe("b"); + + config.providers.a!.disabled = false; + expect(pickComboTarget(config, "free")?.target.provider).toBe("a"); + }); + test("routes a concrete combo target without re-entering its shadowing alias", () => { const config = baseConfig({ combos: { @@ -588,7 +691,7 @@ describe("combo validation and normalization", () => { { raw: VALID_COMBO, providers: { combo: providers.a! }, path: [], message: 'reserved "combo/" namespace' }, { id: "a", raw: VALID_COMBO, path: [], message: 'combo id "a" collides' }, { raw: null, path: [], message: "combo must be an object" }, - { raw: { ...VALID_COMBO, strategy: "random" }, path: ["strategy"], message: "failover" }, + { raw: { ...VALID_COMBO, strategy: "unexpected" }, path: ["strategy"], message: "failover" }, { raw: { ...VALID_COMBO, stickyLimit: 1.5 }, path: ["stickyLimit"], message: "integer from 1 to 100" }, { raw: { ...VALID_COMBO, defaultEffort: "turbo" }, path: ["defaultEffort"], message: "low, medium, high" }, { raw: { targets: [] }, path: ["targets"], message: "non-empty array" }, @@ -710,7 +813,7 @@ describe("persisted combo config parity", () => { }); const rows: Array<{ id: string; combo: unknown; providers?: OcxConfig["providers"] }> = [ - { id: "free", combo: { ...VALID_COMBO, strategy: "random" } }, + { id: "free", combo: { ...VALID_COMBO, strategy: "unexpected" } }, { id: "free", combo: { ...VALID_COMBO, stickyLimit: 0 } }, { id: "free", combo: { ...VALID_COMBO, defaultEffort: "turbo" } }, { id: "free", combo: { targets: [] } }, From 82d60008e307c7d03cefd62a616e969c3962f662 Mon Sep 17 00:00:00 2001 From: x3M3x Date: Sat, 22 Aug 2026 19:56:18 +0400 Subject: [PATCH 2/2] test(combos): cover response routing state --- .../src/content/docs/fr/guides/combos.md | 6 +- .../fr/reference/configuration/routing.md | 4 +- docs-site/src/content/docs/guides/combos.md | 29 ++------ .../src/content/docs/ja/guides/combos.md | 6 +- .../ja/reference/configuration/routing.md | 4 +- .../src/content/docs/ko/guides/combos.md | 6 +- .../content/docs/ko/reference/cli/agents.md | 2 +- .../ko/reference/configuration/routing.md | 4 +- .../src/content/docs/reference/cli/agents.md | 2 +- .../docs/reference/configuration/routing.md | 13 ++-- .../src/content/docs/ru/guides/combos.md | 6 +- .../content/docs/ru/reference/cli/agents.md | 2 +- .../ru/reference/configuration/routing.md | 4 +- .../src/content/docs/tr/guides/combos.md | 7 +- .../tr/reference/configuration/routing.md | 5 +- .../src/content/docs/zh-cn/guides/combos.md | 6 +- .../zh-cn/reference/configuration/routing.md | 4 +- .../src/content/docs/zh-tw/guides/combos.md | 6 +- .../docs/zh-tw/reference/cli/agents.md | 2 +- .../zh-tw/reference/configuration/routing.md | 4 +- gui/src/combo-workspace-data.ts | 27 ++------ gui/src/components/ComboWorkspace.tsx | 3 - .../components/combo-workspace-controls.tsx | 5 +- .../combo-workspace-detail-panel.tsx | 4 +- .../combo-workspace-overview-panel.tsx | 3 - gui/src/i18n/de.ts | 15 ---- gui/src/i18n/en.ts | 15 ---- gui/src/i18n/fr.ts | 15 ---- gui/src/i18n/ja.ts | 15 ---- gui/src/i18n/ko.ts | 15 ---- gui/src/i18n/ru.ts | 15 ---- gui/src/i18n/tr.ts | 15 ---- gui/src/i18n/zh-TW.ts | 15 ---- gui/src/i18n/zh.ts | 15 ---- tests/combo-management-api.test.ts | 68 +++++++++++++++++++ tests/combos.test.ts | 15 ++++ 36 files changed, 140 insertions(+), 242 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md index fb8c5d3fea..b8f76fc52f 100644 --- a/docs-site/src/content/docs/fr/guides/combos.md +++ b/docs-site/src/content/docs/fr/guides/combos.md @@ -309,9 +309,9 @@ Les combos sont stockés dans l'objet `combos` de niveau supérieur, saisi par l | Champ | Obligatoire | Par défaut | Règles | | --- | --- | --- | --- | | `targets` | Oui | — | Tableau ordonné non vide de `{ provider, model, weight? }` cibles configurées. Les paires provider/model en double sont rejetées. | -| `targets[].weight` | Non | `1` | Entier de 1 à 10 000. Utilisé en round-robin et random ; ignoré par les autres stratégies. | -| `strategy` | Non | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` ou `"reset-window"`. | -| `stickyLimit` | Non | `1` | Nombre entier de 1 à 100 requêtes réussies par sélection round-robin ; ignoré par les autres stratégies. | +| `targets[].weight` | Non | `1` | Entier de 1 à 10 000. Utilisé en round-robin ; ignoré par le basculement. | +| `strategy` | Non | `"failover"` | `"failover"` ou `"round-robin"`. | +| `stickyLimit` | Non | `1` | Nombre entier de 1 à 100 requêtes réussies par sélection à tour de rôle. | | `defaultEffort` | Non | `null` | `low`, `medium`, `high`, `xhigh`, `max` ou `ultra` ; appliqué uniquement lorsque l'appelant omet ses efforts et que la cible annonce son soutien. | | `imageInput` | Non | `"auto"` | `"auto"` ou `"disabled"`. `"auto"` publie les images uniquement si toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias` | Non | aucun | Identifiant de modèle public tronqué facultatif ; utilisez les règles d'alias ci-dessus. Une valeur vide est stockée sans alias. | diff --git a/docs-site/src/content/docs/fr/reference/configuration/routing.md b/docs-site/src/content/docs/fr/reference/configuration/routing.md index 1764898985..ab0b1718ca 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/fr/reference/configuration/routing.md @@ -46,8 +46,8 @@ Chaque clé de combinaison est un identifiant conforme à `[A-Za-z0-9][A-Za-z0-9 | Clé | Type | Valeur par défaut | Signification | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | requis | Routes concrètes ordonnées. `weight` est compris entre 1 et 10000 et vaut `1` par défaut. | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Stratégie de sélection. L’ordre départage les égalités ; les poids s’appliquent à round-robin et random. | -| `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot round-robin. Plage de 1 à 100 ; ignoré par les autres stratégies. | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Stratégie de sélection. L’ordre des cibles définit la priorité de repli ; les poids produisent une rotation pondérée régulière. | +| `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot de rotation. Plage de 1 à 100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | non défini | Appliqué uniquement lorsque l’appelant ne précise aucun effort et que la cible sélectionnée annonce le niveau demandé. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publie les images uniquement lorsque toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias?` | `string` | — | Identifiant public facultatif du modèle, à la place du slug canonique du sélecteur. | diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index fb14a76ccd..6bc5f3b6ee 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -1,6 +1,6 @@ --- -title: "Combos: failover and routing strategies" -description: Route one virtual model to several providers for failover, load balancing, or cached quota recovery. +title: "Combos: failover and load balancing" +description: Route one virtual model to several providers for failover or weighted load balancing. --- A **combo** is one virtual model that fronts an ordered list of real provider/model targets. Your @@ -170,25 +170,6 @@ Weights are relative, not percentages. Weights `2,1` and `200,100` express the s small values that communicate intent. ::: -### Random and least-used selection - -`random` chooses a target independently on each request, with `weight` controlling the chance of -selection. `least-used` chooses the eligible target with the fewest successful requests recorded by -this proxy process; configuration order breaks a tie. - -### Reset window: prefer the nearest known recovery - -`reset-window` selects the eligible target whose earliest cached quota-window reset is nearest. It -does not make a provider API request while routing: quota data is the most recently successful -dashboard/API probe. A target with no fresh reset timestamp ranks after a target with one, and -configured order breaks ties or supplies the fallback when no target has quota data. - -```bash -ocx combo set recovering \ - --targets zai/glm-5.3,anthropic/claude-opus-4-8 \ - --strategy reset-window -``` - ## What happens when a target fails Combo failures are divided into **hop** failures and **terminal** failures. @@ -327,9 +308,9 @@ Combos are stored in the top-level `combos` object, keyed by combo id: | Field | Required | Default | Rules | | --- | --- | --- | --- | | `targets` | Yes | — | Non-empty ordered array of configured `{ provider, model, weight? }` targets. Duplicate provider/model pairs are rejected. | -| `targets[].weight` | No | `1` | Integer from 1 to 10,000. Used by round-robin and random; ignored by the other strategies. | -| `strategy` | No | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"`, or `"reset-window"`. | -| `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection; ignored by the other strategies. | +| `targets[].weight` | No | `1` | Integer from 1 to 10,000. Used by round-robin; ignored by failover. | +| `strategy` | No | `"failover"` | `"failover"` or `"round-robin"`. | +| `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; applied only when the caller omits effort and the target advertises support. | | `imageInput` | No | `"auto"` | `"auto"` or `"disabled"`. `"auto"` publishes image support only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. | diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md index 0846437753..7cad2b0b6e 100644 --- a/docs-site/src/content/docs/ja/guides/combos.md +++ b/docs-site/src/content/docs/ja/guides/combos.md @@ -211,9 +211,9 @@ ocx combo remove --yes |フィールド |必須 |デフォルト |ルール | | --- | --- | --- | --- | | `targets` |はい | — |構成された `{ provider, model, weight? }` ターゲットの空でない順序付けされた配列。重複するプロバイダーとモデルのペアは拒否されます。 | -| `targets[].weight` |いいえ | `1` | 1 ~ 10,000 の整数。round-robin と random で使用され、他の戦略では無視されます。 | -| `strategy` |いいえ | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"`、または `"reset-window"`。 | -| `stickyLimit` |いいえ | `1` |round-robin 選択ごとの成功リクエスト数。1 ~ 100 の整数で、他の戦略では無視されます。 | +| `targets[].weight` |いいえ | `1` | 1 ~ 10,000 の整数。ラウンドロビンで使用されます。フェイルオーバーによって無視されます。 | +| `strategy` |いいえ | `"failover"` | `"failover"` または `"round-robin"`。 | +| `stickyLimit` |いいえ | `1` |ラウンドロビン選択ごとの成功したリクエストの数は 1 ~ 100 の整数です。 | | `defaultEffort` |いいえ | `null` | `low`、`medium`、`high`、`xhigh`、`max`、または `ultra`;呼び出し元が努力を省略し、ターゲットがサポートをアドバタイズした場合にのみ適用されます。 | | `alias` |いいえ |なし |オプションのトリミングされたパブリック モデル ID。上記のエイリアス ルールを使用します。空の値はエイリアスなしで保存されます。 | | `nativeAlias` |いいえ | `false` | 現在サポートされている bare native alias に routing/catalog の優先権を明示的に与えます。 | diff --git a/docs-site/src/content/docs/ja/reference/configuration/routing.md b/docs-site/src/content/docs/ja/reference/configuration/routing.md index ea9b4466d6..de0cb31ab6 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ja/reference/configuration/routing.md @@ -57,8 +57,8 @@ picker catalog の convergence だけが保留中で routing change は失われ |キー |タイプ |デフォルト |意味 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` |必須 |具体的なルートを指示しました。 `weight` は 1 ~ 10000 で、デフォルトは `1` です。 | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` |選択戦略。ターゲット順は同点を解決し、重みは round-robin と random に使用されます。 | -| `stickyLimit?` | `number` | `1` |1 つの round-robin バッチに保持する成功リクエスト数。範囲は 1 ~ 100。他の戦略では無視されます。 | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` |選択戦略。ターゲットの順序はフェイルオーバーの優先順位です。重みはスムーズな重み付きラウンドロビンを形成します。 | +| `stickyLimit?` | `number` | `1` |成功したリクエストは 1 つのラウンドロビン バッチに保持されます。範囲は 1 ~ 100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` |設定を解除する |呼び出し元が努力を省略し、選択されたターゲットが要求されたラングをアドバタイズする場合にのみ適用されます。 | | `alias?` | `string` | — |正規のピッカー スラグの代わりのオプションのパブリック モデル ID。 | | `nativeAlias?` | `boolean` | `false` | 現在サポートされている bare native id に限り、その未修飾 id で優先します。アカウント修飾およびプロバイダー修飾の OpenAI ルートは別のままです。 | diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index 76d755320a..cec9ed6258 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -210,9 +210,9 @@ ocx combo remove --yes | 필드 | 필수 | 기본값 | 규칙 | | --- | --- | --- | --- | | `targets` | 예 | — | 설정된 `{ provider, model, weight? }` 대상의 비어 있지 않은 순서가 있는 배열이어야 합니다. 중복된 provider/model 쌍은 거부됩니다. | -| `targets[].weight` | 아니요 | `1` | 1에서 10,000 사이의 정수입니다. round-robin과 random에서 사용되며 다른 전략에서는 무시됩니다. | -| `strategy` | 아니요 | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"`, 또는 `"reset-window"`입니다. | -| `stickyLimit` | 아니요 | `1` | round-robin 선택 한 번당 성공 요청 1에서 100회 사이의 정수이며 다른 전략에서는 무시됩니다. | +| `targets[].weight` | 아니요 | `1` | 1에서 10,000 사이의 정수입니다. round-robin에서 사용되며 failover에서는 무시됩니다. | +| `strategy` | 아니요 | `"failover"` | `"failover"` 또는 `"round-robin"`입니다. | +| `stickyLimit` | 아니요 | `1` | round-robin 선택 한 번당 성공 요청 1에서 100회 사이의 정수입니다. | | `defaultEffort` | 아니요 | `null` | `low`, `medium`, `high`, `xhigh`, `max`, 또는 `ultra`입니다. 호출자가 effort를 생략하고 대상이 지원을 광고할 때만 적용됩니다. | | `alias` | 아니요 | 없음 | 선택적으로 앞뒤 공백을 제거한 공개 모델 ID입니다. 위의 alias 규칙을 따릅니다. 빈 값은 alias 없음으로 저장됩니다. | | `nativeAlias` | 아니요 | `false` | 현재 지원되는 bare native alias가 routing/catalog 우선권을 갖도록 명시적으로 허용합니다. | diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index 2591e333e9..e2b77cf713 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -46,7 +46,7 @@ mode와 flag 전환은 현재 숫자 thread 한도를 유효한 v1/v2 Codex key ### `ocx combo ...` · `ocx route combo ...` -여러 선택 전략을 사용하는 콤보 가상 모델을 관리합니다. `ocx route combo`는 계층형 별칭이며, +콤보 failover와 round-robin 가상 모델을 관리합니다. `ocx route combo`는 계층형 별칭이며, 현재 지원되는 라우팅 리소스는 combo입니다. 대상은 `provider/model[:weight],provider/model[:weight]` 형식을 사용합니다. ```bash diff --git a/docs-site/src/content/docs/ko/reference/configuration/routing.md b/docs-site/src/content/docs/ko/reference/configuration/routing.md index ee872d6bf0..c22a13d07d 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ko/reference/configuration/routing.md @@ -56,8 +56,8 @@ Codex Auth 페이지에서 이 picker 동작을 opt-in할 수 있습니다. 비 | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 순서가 있는 concrete route입니다. `weight`는 1–10000이며 기본값은 `1`입니다. | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 선택 전략입니다. 대상 순서는 동률을 결정하며, weight는 round-robin과 random에 사용됩니다. | -| `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100이며 다른 전략에서는 무시됩니다. | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 선택 전략입니다. 대상 순서는 failover 우선순위이며, weight는 smooth weighted round-robin의 모양을 결정합니다. | +| `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100입니다. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 호출자가 effort를 생략했고 선택된 대상이 요청한 rung를 광고할 때만 적용됩니다. | | `alias?` | `string` | — | 정규화된 picker slug 대신 쓰는 선택적 공개 model id입니다. | | `nativeAlias?` | `boolean` | `false` | 현재 지원되는 bare native id가 해당 비수식 id에만 우선하도록 합니다. 계정 또는 프로바이더로 수식된 OpenAI route는 별도로 유지됩니다. | diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index da157d2843..931f22b3f5 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -80,7 +80,7 @@ gate: it requires the native feature to be enabled with an explicit v2 surface ### `ocx combo ...` · `ocx route combo ...` -Manage combo virtual models and routing strategies. `ocx route combo` is the hierarchical alias; +Manage combo failover and round-robin virtual models. `ocx route combo` is the hierarchical alias; combo is currently the supported routing resource. Targets use `provider/model[:weight],provider/model[:weight]`. diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index ffa9f33f78..795d2210e4 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -73,8 +73,8 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Ordered concrete routes. `weight` is 1–10000 and defaults to `1`. | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Selection strategy. Target order is failover priority and breaks ties. Weights shape round-robin and random selection. | -| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100; ignored by other strategies. | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Selection strategy. Target order is failover priority; weights shape smooth weighted round-robin. | +| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Applied only when the caller omits effort and the selected target advertises the requested rung. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | @@ -98,11 +98,6 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, } ``` -`reset-window` prefers eligible targets whose earliest known quota reset is soonest. It reads only -the last successfully cached provider-quota snapshot, so request routing never waits for an upstream -quota probe. Missing or stale reset data ranks after known data; when every target is unknown, the -configured target order remains the fallback. - For strategy behavior, retryable failures, cooldowns, encrypted v2 task limits, and management commands, see [Combos](/guides/combos/). @@ -188,8 +183,8 @@ echoed as given. The CLI dry-run cannot supply these per-candidate account field ### Combos vs policy profiles -- A **combo** is explicit target routing and failover: configured order, weighted selection, - successful-use history, or cached quota-reset timing decides, and failures advance through the list. +- A **combo** is explicit ordered/weighted target routing and failover: the configured order (or + smooth weighted round-robin) decides, and failures advance through the list. - A **policy profile** is evidence-based selection among configured candidates: hard capability requirements filter first, then deterministic scoring ranks the survivors. diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index f656655b41..b2baf981cc 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -260,9 +260,9 @@ Combo хранятся в объекте верхнего уровня `combos`, | Поле | Обязательно | По умолчанию | Правила | | --- | --- | --- | --- | | `targets` | Yes | — | Непустой упорядоченный массив настроенных целей `{ provider, model, weight? }`. Дубли пар provider/model запрещены. | -| `targets[].weight` | No | `1` | Целое число от 1 до 10 000. Используется в round-robin и random; другие стратегии игнорируют его. | -| `strategy` | No | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` или `"reset-window"`. | -| `stickyLimit` | No | `1` | Целое число от 1 до 100 успешных запросов на один выбор round-robin; другие стратегии игнорируют поле. | +| `targets[].weight` | No | `1` | Целое число от 1 до 10 000. Используется в round-robin; при failover игнорируется. | +| `strategy` | No | `"failover"` | `"failover"` или `"round-robin"`. | +| `stickyLimit` | No | `1` | Целое число от 1 до 100 успешных запросов на один выбор round-robin. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max` или `ultra`; применяется только когда вызывающая сторона не указала effort, а цель объявляет поддержку. | | `alias` | No | none | Необязательный обрезанный публичный id модели; используйте правила alias выше. Пустое значение хранится как отсутствие alias. | | `nativeAlias` | No | `false` | Явно разрешает поддерживаемому сейчас bare native alias перехватить приоритет routing/catalog только для неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct; маршруты с квалификатором аккаунта сохраняют свою идентичность, а provider-qualified `openai-apikey/gpt-5.6-*` использует API-ключ и никогда не переходит на native alias. | diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 75e739244a..6f09a90d04 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -51,7 +51,7 @@ Codex. При переходах mode и feature flag текущий число ### `ocx combo ...` · `ocx route combo ...` -Управляйте virtual-моделями combo с несколькими стратегиями выбора. `ocx route combo` — это иерархический +Управляйте virtual-моделями combo с failover и round-robin. `ocx route combo` — это иерархический alias; на данный момент combo — единственный поддерживаемый routing-resource. Цели используют форму `provider/model[:weight],provider/model[:weight]`. diff --git a/docs-site/src/content/docs/ru/reference/configuration/routing.md b/docs-site/src/content/docs/ru/reference/configuration/routing.md index 892f24d039..7249bd8ecf 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ru/reference/configuration/routing.md @@ -71,8 +71,8 @@ selector-qualified строки и возвращает обычные GPT-ст | Ключ | Тип | По умолчанию | Значение | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Упорядоченные конкретные маршруты. `weight` находится в диапазоне 1–10000 и по умолчанию равен `1`. | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Стратегия выбора. Порядок целей разрешает ничьи; веса используются в round-robin и random. | -| `stickyLimit?` | `number` | `1` | Число успешных запросов в одной партии round-robin. Диапазон 1–100; остальные стратегии игнорируют это поле. | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Стратегия выбора. Порядок целей задаёт приоритет failover; веса формируют плавный взвешенный round-robin. | +| `stickyLimit?` | `number` | `1` | Число успешных запросов, удерживаемых в одной партии round-robin. Диапазон 1–100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Применяется, только если вызывающая сторона не задала effort, а выбранная цель объявляет эту ступень. | | `alias?` | `string` | — | Необязательный публичный id модели вместо канонического slug в селекторе. | | `nativeAlias?` | `boolean` | `false` | Даёт поддерживаемому bare native id приоритет только для этого неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct. Маршруты с квалификатором аккаунта остаются отдельными. Провайдер-квалифицированные маршруты, например `openai-apikey/gpt-5.6-*`, используют настроенный API-ключ и никогда не переходят на native alias. | diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md index 959cc4c724..6662b88c10 100644 --- a/docs-site/src/content/docs/tr/guides/combos.md +++ b/docs-site/src/content/docs/tr/guides/combos.md @@ -341,9 +341,9 @@ saklanır: | Alan | Gerekli | Varsayılan | Kurallar | | --- | --- | --- | --- | | `targets` | Evet | — | Yapılandırılmış `{ provider, model, weight? }` hedeflerinin boş olmayan sıralı dizisi. Yinelenen sağlayıcı/model çiftleri reddedilir. | -| `targets[].weight` | Hayır | `1` | 1 ile 10.000 arasında tam sayı. Round-robin ve random tarafından kullanılır; diğer stratejilerce yok sayılır. | -| `strategy` | Hayır | `"failover"` | `"failover"`, `"round-robin"`, `"random"`, `"least-used"` veya `"reset-window"`. | -| `stickyLimit` | Hayır | `1` | Round-robin seçimi başına 1 ile 100 arasında başarılı istek tam sayısı; diğer stratejilerce yok sayılır. | +| `targets[].weight` | Hayır | `1` | 1 ile 10.000 arasında tam sayı. Round-robin tarafından kullanılır; yük devretme tarafından yok sayılır. | +| `strategy` | Hayır | `"failover"` | `"failover"` veya `"round-robin"`. | +| `stickyLimit` | Hayır | `1` | Round-robin seçimi başına 1 ile 100 arasında başarılı istek tam sayısı. | | `defaultEffort` | Hayır | `null` | `low`, `medium`, `high`, `xhigh`, `max` veya `ultra`; yalnızca arayan çabayı atladığında ve hedef desteği bildirdiğinde uygulanır. | | `alias` | Hayır | yok | İsteğe bağlı kırpılmış genel model kimliği; yukarıdaki takma ad kurallarını kullanın. Boş bir değer takma ad yok olarak saklanır. | | `nativeAlias` | Hayır | `false` | Şu anda desteklenen yalın bir yerel `alias`'ın yönlendirme ve katalog önceliği almasına açıkça izin verin. Asla takma addan çıkarılmaz. | @@ -381,3 +381,4 @@ Hata hedefe özgü olmaktan ziyade uç (terminal) bir hataydı. Geçersiz girdiy düzeltin, aşırı büyük bir bağlamı azaltın, bir politika reddini işleyin veya reddedilen istek kaynağını düzeltin. Kombolar bu durumlar için atlama yapmaz. + diff --git a/docs-site/src/content/docs/tr/reference/configuration/routing.md b/docs-site/src/content/docs/tr/reference/configuration/routing.md index 98b7dbee3b..cdb7e76572 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/tr/reference/configuration/routing.md @@ -92,8 +92,8 @@ aileleri kullanamaz. | Anahtar | Tip | Varsayılan | Anlamı | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | gerekli | Sıralı somut rotalar. `weight` 1–10000 arasındadır ve varsayılan olarak `1`'dir. | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Seçim stratejisi. Hedef sırası eşitlikleri bozar; ağırlıklar round-robin ve random için kullanılır. | -| `stickyLimit?` | `number` | `1` | Bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100; diğer stratejilerce yok sayılır. | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Seçim stratejisi. Hedef sırası yük devretme önceliğidir; ağırlıklar pürüzsüz ağırlıklı round-robin'i şekillendirir. | +| `stickyLimit?` | `number` | `1` | Tek bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | ayarlanmamış | Yalnızca arayan çabayı atladığında ve seçilen hedef istenen basamağı bildirdiğinde uygulanır. | | `alias?` | `string` | — | Kurallı seçici slug'ı yerine isteğe bağlı genel model kimliği. | | `nativeAlias?` | `boolean` | `false` | Şu anda desteklenen bir yalın yerel kimliğin yalnızca o niteliksiz kimlik için öncelikli olmasına izin verin. Yalın `gpt-5.6-*` kimlikleri Codex Havuz/Direct kimlik bilgilerini kullanır. Hesap nitelikli rotalar ayrı kalır. `openai-apikey/gpt-5.6-*` gibi sağlayıcı nitelikli rotalar yapılandırılmış API anahtarı rotalarını kullanır ve asla yerel takma ada düşmez. | @@ -294,3 +294,4 @@ otomatik bir yeniden oluşturmayı tetikler; `ocx logs rebuild-index` bunu zorla Bu sistemdeki hiçbir şey ağırlıkları, bütçeleri veya aday kümelerini otomatik olarak ayarlamaz. + diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md index d99c7e709b..fe89fc04c7 100644 --- a/docs-site/src/content/docs/zh-cn/guides/combos.md +++ b/docs-site/src/content/docs/zh-cn/guides/combos.md @@ -241,9 +241,9 @@ combo 会存储在顶层的 `combos` 对象中,并以 combo id 作为键: | 字段 | 必填 | 默认值 | 规则 | | --- | --- | --- | --- | | `targets` | 是 | — | 非空、有顺序的数组,元素为已配置的 `{ provider, model, weight? }` 目标。重复的 provider/model 对会被拒绝。 | -| `targets[].weight` | 否 | `1` | 1 到 10,000 的整数。round-robin 和 random 会使用它;其他策略会忽略它。 | -| `strategy` | 否 | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"` 或 `"reset-window"`。 | -| `stickyLimit` | 否 | `1` | 每次 round-robin 选择可连续处理的成功请求数,范围为 1 到 100;其他策略会忽略它。 | +| `targets[].weight` | 否 | `1` | 1 到 10,000 的整数。轮询会使用它;故障切换会忽略它。 | +| `strategy` | 否 | `"failover"` | `"failover"` 或 `"round-robin"`。 | +| `stickyLimit` | 否 | `1` | 每次轮询选择可连续处理的成功请求数,范围为 1 到 100。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;仅当调用方省略 effort 且目标声明支持时才会应用。 | | `imageInput` | 否 | `"auto"` | `"auto"` 或 `"disabled"`。`"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias` | 否 | 无 | 可选的、已修剪的公开模型 id;使用上面的别名规则。空值会以“无别名”形式存储。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md index 1652cac5f4..11a3ad97ce 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md @@ -61,8 +61,8 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏 | 键 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 有序的具体路由。`weight` 范围为 1–10000,默认值为 `1`。 | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 选择策略。目标顺序用于打破平局;权重用于 round-robin 和 random。 | -| `stickyLimit?` | `number` | `1` | 单个 round-robin 批次中保留的成功请求数。范围 1–100;其他策略会忽略它。 | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 选择策略。目标顺序表示故障切换优先级;权重会影响平滑加权轮询。 | +| `stickyLimit?` | `number` | `1` | 在单个轮询批次中保留的成功请求数。范围 1–100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 仅在调用方省略 effort 且所选目标声明了请求的档位时应用。 | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias?` | `string` | — | 可选的公开 model id,用于替代规范化的选择器 slug。 | diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md index 320c46c26b..ac0efda056 100644 --- a/docs-site/src/content/docs/zh-tw/guides/combos.md +++ b/docs-site/src/content/docs/zh-tw/guides/combos.md @@ -249,9 +249,9 @@ Combo 儲存於頂層 `combos` 物件中,以 combo id 為 key: | 欄位 | 必填 | 預設值 | 規則 | | --- | --- | --- | --- | | `targets` | 是 | — | 已設定 `{ provider, model, weight? }` 目標的非空有序陣列。重複的供應商/模型對會被拒絕。 | -| `targets[].weight` | 否 | `1` | 1 到 10,000 的整數。由 round-robin 與 random 使用;其他策略忽略。 | -| `strategy` | 否 | `"failover"` | `"failover"`、`"round-robin"`、`"random"`、`"least-used"` 或 `"reset-window"`。 | -| `stickyLimit` | 否 | `1` | 每次 round-robin 選擇的成功請求數,1 到 100 的整數;其他策略忽略。 | +| `targets[].weight` | 否 | `1` | 1 到 10,000 的整數。由 round-robin 使用;failover 忽略。 | +| `strategy` | 否 | `"failover"` | `"failover"` 或 `"round-robin"`。 | +| `stickyLimit` | 否 | `1` | 每次 round-robin 選擇的成功請求數,1 到 100 的整數。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;僅在呼叫者省略 effort 且目標宣告支援時套用。 | | `alias` | 否 | 無 | 可選的修剪後公開模型 id;使用上述別名規則。空值儲存為無別名。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md index 0a1f8f3d4c..e8e7955d67 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md @@ -43,7 +43,7 @@ ocx v2 threads 16 ### `ocx combo ...` · `ocx route combo ...` -管理具有多種選擇策略的組合虛擬模型。`ocx route combo` 是階層式別名;組合是目前支援的路由資源。目標使用 +管理組合 failover 與 round-robin 虛擬模型。`ocx route combo` 是階層式別名;組合是目前支援的路由資源。目標使用 `provider/model[:weight],provider/model[:weight]`。 ```bash diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md index ae2708b136..e2ca4f3622 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md @@ -44,8 +44,8 @@ Codex Auth 頁面將此 picker 行為作為選擇加入功能暴露。停用它 | Key | 型別 | 預設值 | 意義 | | --- | --- | --- | --- | | `targets` | `{ provider: string; model: string; weight?: number }[]` | 必填 | 有序的具體路由。`weight` 為 1–10000,預設 `1`。 | -| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 選擇策略。目標順序用於打破平手;權重用於 round-robin 與 random。 | -| `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100;其他策略會忽略它。 | +| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 選擇策略。目標順序為 failover 優先序;權重塑造平滑的加權 round-robin。 | +| `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | 未設定 | 僅在呼叫者省略 effort 且所選目標廣告請求的階層時套用。 | | `alias?` | `string` | — | 可選的公開模型 id,取代標準 picker slug。 | | `nativeAlias?` | `boolean` | `false` | 讓目前支援的裸原生 id 僅對該未限定 id 取得優先。裸 `gpt-5.6-*` id 使用 Codex 池/Direct 憑證。帳號限定路由保持獨立。供應商限定路由(如 `openai-apikey/gpt-5.6-*`)使用其設定的 API-key 路由,且永不會落到原生別名。 | diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index a6a15fdda6..4f5e96e0b7 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -7,7 +7,7 @@ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../../src/codex/catalog/native-mo export { SUPPORTED_NATIVE_OPENAI_SLUGS }; -export type ComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window"; +export type ComboStrategy = "failover" | "round-robin"; export type ComboEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra"; export const COMBO_EFFORTS: ComboEffort[] = ["low", "medium", "high", "xhigh", "max", "ultra"]; @@ -85,9 +85,6 @@ export interface ComboItem { export interface ComboSections { failover: ComboItem[]; roundRobin: ComboItem[]; - weighted: ComboItem[]; - leastUsed: ComboItem[]; - resetWindow: ComboItem[]; } export interface ComboAttentionItem { @@ -133,15 +130,7 @@ function normalizeAlias(raw: unknown): string | null { } export function normalizeStrategy(raw: unknown): ComboStrategy { - return raw === "round-robin" - ? "round-robin" - : raw === "random" - ? "random" - : raw === "least-used" - ? "least-used" - : raw === "reset-window" - ? "reset-window" - : "failover"; + return raw === "round-robin" ? "round-robin" : "failover"; } export function normalizeStickyLimit(raw: unknown): number { @@ -204,17 +193,11 @@ export function parseComboList(payload: unknown): ComboItem[] { export function groupCombos(items: ComboItem[]): ComboSections { const failover: ComboItem[] = []; const roundRobin: ComboItem[] = []; - const weighted: ComboItem[] = []; - const leastUsed: ComboItem[] = []; - const resetWindow: ComboItem[] = []; for (const item of items) { if (item.strategy === "round-robin") roundRobin.push(item); - else if (item.strategy === "random") weighted.push(item); - else if (item.strategy === "least-used") leastUsed.push(item); - else if (item.strategy === "reset-window") resetWindow.push(item); else failover.push(item); } - return { failover, roundRobin, weighted, leastUsed, resetWindow }; + return { failover, roundRobin }; } export function filterCombos(items: ComboItem[], query: string): ComboItem[] { @@ -287,7 +270,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {} id: item.id.trim(), ...(options.renameFrom ? { renameFrom: options.renameFrom } : {}), combo: { - targets: item.targets.map((target) => (item.strategy === "round-robin" || item.strategy === "random") + targets: item.targets.map((target) => item.strategy === "round-robin" ? { provider: target.provider.trim(), model: target.model.trim(), weight: target.weight ?? 1 } : { provider: target.provider.trim(), model: target.model.trim() }), strategy: item.strategy, @@ -377,8 +360,6 @@ export function validateComboDraft( if (!Number.isInteger(item.stickyLimit) || item.stickyLimit < 1 || item.stickyLimit > 100) { return "invalidStickyLimit"; } - } - if (item.strategy === "round-robin" || item.strategy === "random") { for (const target of item.targets) { const weight = target.weight ?? 1; if (!Number.isInteger(weight) || weight < 1 || weight > 10000) return "invalidWeight"; diff --git a/gui/src/components/ComboWorkspace.tsx b/gui/src/components/ComboWorkspace.tsx index 3e41a0f882..66cd751f64 100644 --- a/gui/src/components/ComboWorkspace.tsx +++ b/gui/src/components/ComboWorkspace.tsx @@ -128,9 +128,6 @@ export default function ComboWorkspace({ {([ ["failover", sections.failover, "cws.group.failover"], ["round-robin", sections.roundRobin, "cws.group.roundRobin"], - ["weighted", sections.weighted, "cws.group.weighted"], - ["leastUsed", sections.leastUsed, "cws.group.leastUsed"], - ["resetWindow", sections.resetWindow, "cws.group.resetWindow"], ] as const).map(([key, items, labelKey]) => ( items.length > 0 ? (
diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx index 806a4bd759..0a7ecaa65d 100644 --- a/gui/src/components/combo-workspace-controls.tsx +++ b/gui/src/components/combo-workspace-controls.tsx @@ -24,9 +24,6 @@ export function StrategySeg({ {([ ["failover", "cws.strategy.failover"], ["round-robin", "cws.strategy.roundRobin"], - ["random", "cws.strategy.random"], - ["least-used", "cws.strategy.leastUsed"], - ["reset-window", "cws.strategy.resetWindow"], ] as const).map(([id, key]) => (
@@ -352,7 +352,7 @@ export function DetailPanel({ onChange={(targets) => updateDraft((d) => ({ ...d, targets }))} />

- {draft.strategy === "failover" ? t("cws.targets.failoverHint") : draft.strategy === "round-robin" ? t("cws.targets.roundRobinHint") : draft.strategy === "random" ? t("cws.targets.randomHint") : draft.strategy === "least-used" ? t("cws.targets.leastUsedHint") : t("cws.targets.resetWindowHint")} + {draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}

{combos.length}{t("cws.count.total")}
{sections.failover.length}{t("cws.count.failover")}
{sections.roundRobin.length}{t("cws.count.roundRobin")}
-
{sections.weighted.length}{t("cws.count.weighted")}
-
{sections.leastUsed.length}{t("cws.count.leastUsed")}
-
{sections.resetWindow.length}{t("cws.count.resetWindow")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 5438e7f144..41ae5d02e5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1850,9 +1850,6 @@ export const de: Record = { "cws.noSearchResults": "Keine Combos passen zur Suche.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-Robin", - "cws.group.weighted": "Gewichtet", - "cws.group.leastUsed": "Wenigste Nutzung", - "cws.group.resetWindow": "Zurücksetzfenster", "cws.targetCount": "{count} Ziele", "cws.targetCountOne": "1 Ziel", "cws.overviewTitle": "Combos", @@ -1860,9 +1857,6 @@ export const de: Record = { "cws.count.total": "Gesamt", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-Robin", - "cws.count.weighted": "Gewichtet", - "cws.count.leastUsed": "Wenigste Nutzung", - "cws.count.resetWindow": "Zurücksetzfenster", "cws.howTitle": "So funktioniert es", "cws.howBody": "Fordern Sie in Codex den öffentlichen Modellnamen der Combo an. Ohne eigenen Namen gilt combo/. OpenCodex wählt ein Ziel und springt nur bei wiederholbaren Upstream-Fehlern. Ist kein Ziel verfügbar, schlägt die Anfrage geschlossen fehl, statt den globalen Standardanbieter zu verwenden.", "cws.attentionTitle": "Aufmerksamkeit nötig", @@ -1881,13 +1875,7 @@ export const de: Record = { "cws.strategy": "Strategie", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-Robin", - "cws.strategy.random": "Zufall", - "cws.strategy.leastUsed": "Am wenigsten genutzt", - "cws.strategy.resetWindow": "Zurücksetzfenster", "cws.strategy.failoverHint": "Ziele der Reihe nach versuchen. Bei einem wiederholbaren Fehler (Limit, Ausfall, Abo-Sperre) zum nächsten springen.", - "cws.strategy.randomHint": "Gewichtete Zufallsauswahl. Ziele mit hoherer Gewichtung werden haufiger gewahlt.", - "cws.strategy.resetWindowHint": "Bevorzugt Ziele, deren bekanntes Kontingentfenster am frühesten zurückgesetzt wird. Ziele ohne bekannte Rücksetzzeit bleiben Ausweichziele.", - "cws.strategy.leastUsedHint": "Wahlt das Ziel mit den wenigsten aufgezeichneten Nutzungen.", "cws.strategy.roundRobinHint": "Datenverkehr deterministisch nach Gewicht verteilen. Das gewählte Ziel für einen Block erfolgreicher Anfragen behalten und dann weiterschalten.", "cws.field.id": "Combo-ID", "cws.field.idHintEdit": "Das Ändern der ID benennt die Combo um. Clients fordern {model} an.", @@ -1914,9 +1902,6 @@ export const de: Record = { "cws.targets": "Ziele", "cws.targets.failoverHint": "Reihenfolge zählt — das erste ist primär.", "cws.targets.roundRobinHint": "Gewichte steuern die deterministische relative Auswahl; die Reihenfolge löst Gleichstände im Rotationsring.", - "cws.targets.randomHint": "Gewichte steuern die Wahrscheinlichkeit, mit der ein Ziel pro Anfrage ausgewählt wird.", - "cws.targets.leastUsedHint": "Die Reihenfolge löst Gleichstände bei derselben Anzahl erfolgreicher Nutzungen.", - "cws.targets.resetWindowHint": "Die Reihenfolge löst Gleichstände und dient als Rückfall, wenn keine zwischengespeicherte Kontingent-Rücksetzzeit vorliegt.", "cws.target.provider": "Anbieter", "cws.target.model": "Modell", "cws.target.weight": "Gewicht", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 4147222f5f..01d69f0fd1 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1890,9 +1890,6 @@ export const en = { "cws.noSearchResults": "No combos match your search.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-robin", - "cws.group.weighted": "Weighted", - "cws.group.leastUsed": "Least used", - "cws.group.resetWindow": "Reset window", "cws.targetCount": "{count} targets", "cws.targetCountOne": "1 target", "cws.overviewTitle": "Combos", @@ -1900,9 +1897,6 @@ export const en = { "cws.count.total": "Total", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-robin", - "cws.count.weighted": "Weighted", - "cws.count.leastUsed": "Least used", - "cws.count.resetWindow": "Reset window", "cws.howTitle": "How it works", "cws.howBody": "Ask Codex for the combo's public model name. Without one, the default is combo/. OpenCodex selects a target and hops only on retryable upstream failures. If no target remains available, the request fails closed instead of using the global default provider.", "cws.attentionTitle": "Needs attention", @@ -1921,14 +1915,8 @@ export const en = { "cws.strategy": "Strategy", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-robin", - "cws.strategy.random": "Random", - "cws.strategy.leastUsed": "Least used", - "cws.strategy.resetWindow": "Reset window", "cws.strategy.failoverHint": "Try targets in order. If the first fails with a retryable error (rate limit, outage, subscription gate), hop to the next.", "cws.strategy.roundRobinHint": "Deterministically balance traffic by weight. Keep each selected target for a batch of successful requests, then advance.", - "cws.strategy.randomHint": "Weighted random selection. Higher-weight targets are more likely to be chosen on each request.", - "cws.strategy.leastUsedHint": "Pick the target with the fewest recorded uses, spreading load across all targets.", - "cws.strategy.resetWindowHint": "Prefer targets whose known quota window resets soonest. Targets without a known reset time remain fallback targets.", "cws.field.id": "Combo id", "cws.field.idHint": "Clients will request {model}", "cws.field.idInternalHint": "Internal combo id. You can change it after creation.", @@ -1954,9 +1942,6 @@ export const en = { "cws.targets": "Targets", "cws.targets.failoverHint": "Order matters — first is primary.", "cws.targets.roundRobinHint": "Weights control deterministic relative selection; order breaks ties in the rotation ring.", - "cws.targets.randomHint": "Weights control the chance of each target being selected on a request.", - "cws.targets.leastUsedHint": "Order breaks ties when targets have the same successful-use count.", - "cws.targets.resetWindowHint": "Order breaks ties and remains the fallback when no cached quota reset time is available.", "cws.target.provider": "Provider", "cws.target.model": "Model", "cws.target.weight": "Weight", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index bd555430e1..3765f4d95f 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1851,9 +1851,6 @@ export const fr: Record = { "cws.noSearchResults": "Aucune combinaison ne correspond à votre recherche.", "cws.group.failover": "Basculement", "cws.group.roundRobin": "Rotation", - "cws.group.weighted": "Pondere", - "cws.group.leastUsed": "Moins utilise", - "cws.group.resetWindow": "Fenêtre de réinitialisation", "cws.targetCount": "{count} cibles", "cws.targetCountOne": "1 cible", "cws.overviewTitle": "Combinaisons", @@ -1861,9 +1858,6 @@ export const fr: Record = { "cws.count.total": "Total", "cws.count.failover": "Basculement", "cws.count.roundRobin": "Rotation", - "cws.count.weighted": "Pondere", - "cws.count.leastUsed": "Moins utilise", - "cws.count.resetWindow": "Fenêtre de réinitialisation", "cws.howTitle": "Fonctionnement", "cws.howBody": "Demandez à Codex le nom de modèle public de la combinaison. Sans nom, combo/ est utilisé par défaut. OpenCodex sélectionne une cible et ne bascule qu’en cas d’échec réessayable en amont. Si aucune cible ne reste disponible, la requête est bloquée au lieu d’utiliser le fournisseur global par défaut.", "cws.attentionTitle": "Intervention requise", @@ -1886,13 +1880,7 @@ export const fr: Record = { "cws.strategy": "Stratégie", "cws.strategy.failover": "Basculement", "cws.strategy.roundRobin": "Rotation", - "cws.strategy.random": "Aleatoire", - "cws.strategy.leastUsed": "Moins utilise", - "cws.strategy.resetWindow": "Fenêtre de réinitialisation", "cws.strategy.failoverHint": "Essaie les cibles dans l’ordre. Si la première échoue avec une erreur réessayable (limite de débit, panne, restriction d’abonnement), passe à la suivante.", - "cws.strategy.randomHint": "Selection aleatoire ponderee. Les cibles avec un poids plus eleve sont plus susceptibles detre choisies.", - "cws.strategy.resetWindowHint": "Privilégie les cibles dont la fenêtre de quota connue se réinitialise le plus tôt. Celles sans heure connue restent des solutions de repli.", - "cws.strategy.leastUsedHint": "Choisit la cible avec le moins dutilisations enregistrees.", "cws.strategy.roundRobinHint": "Répartit le trafic de manière déterministe selon les pondérations. Conserve chaque cible sélectionnée pendant un lot de requêtes réussies, puis passe à la suivante.", "cws.field.id": "Identifiant de la combinaison", "cws.field.idHint": "Les clients demanderont {model}", @@ -1915,9 +1903,6 @@ export const fr: Record = { "cws.targets": "Cibles", "cws.targets.failoverHint": "L’ordre est important — la première est la cible principale.", "cws.targets.roundRobinHint": "Les pondérations contrôlent la sélection relative déterministe; l’ordre départage les égalités dans l’anneau de rotation.", - "cws.targets.randomHint": "Les pondérations contrôlent la probabilité de sélection de chaque cible pour une requête.", - "cws.targets.leastUsedHint": "L’ordre départage les cibles ayant le même nombre d’utilisations réussies.", - "cws.targets.resetWindowHint": "L’ordre départage les égalités et reste la solution de repli lorsqu’aucune heure de réinitialisation de quota n’est en cache.", "cws.target.provider": "Fournisseur", "cws.target.model": "Modèle", "cws.target.weight": "Pondération", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 8e66f5765f..63e20efd3b 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1907,9 +1907,6 @@ export const ja: Record = { "cws.searchPlaceholder": "コンボやターゲットを検索…", "cws.noSearchResults": "検索に一致するコンボがありません。", "cws.group.failover": "フェイルオーバー", - "cws.group.weighted": "重み付き", - "cws.group.leastUsed": "最少使用", - "cws.group.resetWindow": "リセットウィンドウ", "cws.group.roundRobin": "ラウンドロビン", "cws.targetCount": "{count} ターゲット", "cws.targetCountOne": "1 ターゲット", @@ -1917,9 +1914,6 @@ export const ja: Record = { "cws.overviewBlurb": "プロバイダー/モデルターゲット間でフェイルオーバーする、または決定論的で滑らかな重み付きラウンドロビンを使う仮想モデル。", "cws.count.total": "合計", "cws.count.failover": "フェイルオーバー", - "cws.count.weighted": "重み付き", - "cws.count.leastUsed": "最少使用", - "cws.count.resetWindow": "リセットウィンドウ", "cws.count.roundRobin": "ラウンドロビン", "cws.howTitle": "仕組み", "cws.howBody": "Codex に combo/ を要求します。OpenCodex はターゲットを選び、再試行可能な上流の失敗時のみホップします。利用可能なターゲットが残っていない場合、グローバルなデフォルトプロバイダーを使わずにフェイルクローズします。", @@ -1939,14 +1933,8 @@ export const ja: Record = { "cws.tab.about": "概要", "cws.strategy": "ストラテジー", "cws.strategy.failover": "フェイルオーバー", - "cws.strategy.random": "ランダム", - "cws.strategy.leastUsed": "最少使用", - "cws.strategy.resetWindow": "リセットウィンドウ", "cws.strategy.roundRobin": "ラウンドロビン", "cws.strategy.failoverHint": "ターゲットを順に試します。最初が再試行可能なエラー(レート制限、障害、サブスクリプションゲート)で失敗した場合、次へホップします。", - "cws.strategy.randomHint": "重み付きランダム選択。重みが高いターゲットが選ばれやすくなります。", - "cws.strategy.resetWindowHint": "既知のクォータウィンドウが最も早くリセットされるターゲットを優先します。リセット時刻が不明なターゲットはフォールバックのままです。", - "cws.strategy.leastUsedHint": "記録された使用回数が最も少ないターゲットを選択します。", "cws.strategy.roundRobinHint": "重みで決定論的にトラフィックを分散します。選んだターゲットを成功リクエストのバッチ分保持し、次へ進みます。", "cws.field.id": "コンボ ID", "cws.field.idHint": "クライアントは {model} をリクエストします", @@ -1973,9 +1961,6 @@ export const ja: Record = { "cws.targets": "ターゲット", "cws.targets.failoverHint": "順序が重要 — 最初がプライマリです。", "cws.targets.roundRobinHint": "重みが決定論的な相対選択を制御し、順序がローテーションリングの同点を解消します。", - "cws.targets.randomHint": "重みは、各リクエストでターゲットが選ばれる確率を制御します。", - "cws.targets.leastUsedHint": "成功した使用回数が同じ場合は順序で同点を解消します。", - "cws.targets.resetWindowHint": "順序で同点を解消し、キャッシュ済みのクォータリセット時刻がない場合のフォールバックになります。", "cws.target.provider": "プロバイダー", "cws.target.model": "モデル", "cws.target.weight": "重み", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 2388b8b9a3..b13cd141ff 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1876,9 +1876,6 @@ export const ko: Record = { "cws.searchPlaceholder": "콤보 또는 대상 검색…", "cws.noSearchResults": "검색과 일치하는 콤보가 없습니다.", "cws.group.failover": "장애 조치", - "cws.group.weighted": "가중치", - "cws.group.leastUsed": "최소 사용", - "cws.group.resetWindow": "재설정 기간", "cws.group.roundRobin": "라운드로빈", "cws.targetCount": "대상 {count}개", "cws.targetCountOne": "대상 1개", @@ -1886,9 +1883,6 @@ export const ko: Record = { "cws.overviewBlurb": "프로바이더/모델 대상 사이에서 장애 조치하거나 결정적 Smooth Weighted 라운드로빈을 사용하는 가상 모델입니다.", "cws.count.total": "전체", "cws.count.failover": "장애 조치", - "cws.count.weighted": "가중치", - "cws.count.leastUsed": "최소 사용", - "cws.count.resetWindow": "재설정 기간", "cws.count.roundRobin": "라운드로빈", "cws.howTitle": "동작 방식", "cws.howBody": "Codex에서 콤보의 공개 모델 이름을 요청하세요. 설정하지 않으면 combo/가 기본값입니다. OpenCodex는 재시도 가능한 업스트림 오류에서만 다음 대상으로 넘깁니다. 사용 가능한 대상이 없으면 전역 기본 프로바이더로 우회하지 않고 요청을 실패 처리합니다.", @@ -1907,14 +1901,8 @@ export const ko: Record = { "cws.tab.about": "정보", "cws.strategy": "전략", "cws.strategy.failover": "장애 조치", - "cws.strategy.random": "무작위", - "cws.strategy.leastUsed": "최소 사용", - "cws.strategy.resetWindow": "재설정 기간", "cws.strategy.roundRobin": "라운드로빈", "cws.strategy.failoverHint": "대상을 순서대로 시도합니다. 재시도 가능한 오류(한도, 장애, 구독 게이트)면 다음으로 넘어갑니다.", - "cws.strategy.randomHint": "가중 무작위 선택. 가중치가 높은 대상이 선택될 가능성이 높습니다.", - "cws.strategy.resetWindowHint": "알려진 할당량 기간이 가장 빨리 재설정되는 대상을 우선합니다. 재설정 시간이 알려지지 않은 대상은 대체 대상으로 남습니다.", - "cws.strategy.leastUsedHint": "기록된 사용 횟수가 가장 적은 대상을 선택합니다.", "cws.strategy.roundRobinHint": "가중치에 따라 트래픽을 결정적으로 분배합니다. 선택된 대상을 성공 요청 묶음 동안 유지한 뒤 다음 대상으로 진행합니다.", "cws.field.id": "콤보 ID", "cws.field.idHintEdit": "ID를 변경하면 콤보 이름이 바뀝니다. 클라이언트는 {model}을(를) 요청합니다.", @@ -1941,9 +1929,6 @@ export const ko: Record = { "cws.targets": "대상", "cws.targets.failoverHint": "순서가 중요합니다 — 첫 번째가 기본입니다.", "cws.targets.roundRobinHint": "가중치는 결정적 상대 선택을 제어하고, 순서는 회전 고리의 동률을 결정합니다.", - "cws.targets.randomHint": "가중치는 요청마다 각 대상이 선택될 확률을 제어합니다.", - "cws.targets.leastUsedHint": "성공 사용 횟수가 같은 대상은 순서로 동률을 해소합니다.", - "cws.targets.resetWindowHint": "순서로 동률을 해소하며, 캐시된 할당량 재설정 시간이 없을 때 대체 순서로 사용됩니다.", "cws.target.provider": "프로바이더", "cws.target.model": "모델", "cws.target.weight": "가중치", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index a27c1f4670..05cf793779 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1959,9 +1959,6 @@ export const ru: Record = { "cws.searchPlaceholder": "Поиск комбо или целей…", "cws.noSearchResults": "Нет комбо, соответствующих запросу.", "cws.group.failover": "Failover", - "cws.group.weighted": "Взвешенный", - "cws.group.leastUsed": "Наименее исп.", - "cws.group.resetWindow": "Окно сброса", "cws.group.roundRobin": "Round-robin", "cws.targetCount": "{count} целей", "cws.targetCountOne": "1 цель", @@ -1969,9 +1966,6 @@ export const ru: Record = { "cws.overviewBlurb": "Виртуальные модели, которые при сбоях переключаются между целями провайдер/модель (отказоустойчивое переключение, failover) или используют детерминированный плавный взвешенный циклический перебор (round-robin).", "cws.count.total": "Всего", "cws.count.failover": "Failover", - "cws.count.weighted": "Взвешенный", - "cws.count.leastUsed": "Наименее исп.", - "cws.count.resetWindow": "Окно сброса", "cws.count.roundRobin": "Round-robin", "cws.howTitle": "Как это работает", "cws.howBody": "Запросите у Codex публичное имя модели комбо. Если оно не задано, используется combo/. OpenCodex выбирает цель и переключается на следующую только при сбоях вышестоящего провайдера, допускающих повтор. Если доступных целей не осталось, запрос завершается ошибкой, а не переходит на глобальный провайдер по умолчанию.", @@ -1990,14 +1984,8 @@ export const ru: Record = { "cws.tab.about": "О комбо", "cws.strategy": "Стратегия", "cws.strategy.failover": "Failover", - "cws.strategy.random": "Случайный", - "cws.strategy.leastUsed": "Наименее используемый", - "cws.strategy.resetWindow": "Окно сброса", "cws.strategy.roundRobin": "Round-robin", "cws.strategy.failoverHint": "Цели перебираются по порядку. Если первая завершается ошибкой, допускающей повтор (лимит запросов, сбой, ограничение подписки), происходит переключение на следующую.", - "cws.strategy.randomHint": "Взвешенный случайный выбор. Цели с большим весом выбираются чаще.", - "cws.strategy.resetWindowHint": "Предпочитает цели, чьё известное окно квоты сбросится раньше. Цели без известного времени сброса остаются резервными.", - "cws.strategy.leastUsedHint": "Выбирает цель с наименьшим количеством записей использования.", "cws.strategy.roundRobinHint": "Детерминированное распределение трафика по весам. Выбранная цель удерживается на серию успешных запросов, затем селектор переходит к следующей.", "cws.field.id": "Id комбо", "cws.field.idHint": "Клиенты будут запрашивать {model}", @@ -2024,9 +2012,6 @@ export const ru: Record = { "cws.targets": "Цели", "cws.targets.failoverHint": "Порядок важен — первая цель основная.", "cws.targets.roundRobinHint": "Веса задают детерминированный относительный выбор; при равных весах порядок определяет очерёдность в кольце ротации.", - "cws.targets.randomHint": "Веса задают вероятность выбора каждой цели для запроса.", - "cws.targets.leastUsedHint": "Порядок разрешает равенство при одинаковом числе успешных использований.", - "cws.targets.resetWindowHint": "Порядок разрешает равенства и служит запасным вариантом, когда нет кэшированного времени сброса квоты.", "cws.target.provider": "Провайдер", "cws.target.model": "Модель", "cws.target.weight": "Вес", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 506f37a979..c98421183e 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1879,9 +1879,6 @@ export const tr: Record = { "cws.searchPlaceholder": "Kombolarda veya hedeflerde ara…", "cws.noSearchResults": "Aramanızla eşleşen kombo yok.", "cws.group.failover": "Yedekli (Failover)", - "cws.group.weighted": "Agirlikli", - "cws.group.leastUsed": "En az kullanilan", - "cws.group.resetWindow": "Sıfırlama penceresi", "cws.group.roundRobin": "Round-robin", "cws.targetCount": "{count} hedef", "cws.targetCountOne": "1 hedef", @@ -1889,9 +1886,6 @@ export const tr: Record = { "cws.overviewBlurb": "Sağlayıcı/model hedefleri arasında devreden sanal modeller.", "cws.count.total": "Toplam", "cws.count.failover": "Failover", - "cws.count.weighted": "Agirlikli", - "cws.count.leastUsed": "En az kullanilan", - "cws.count.resetWindow": "Sıfırlama penceresi", "cws.count.roundRobin": "Round-robin", "cws.howTitle": "Nasıl çalışır", "cws.howBody": "Codex'ten kombonun kamuya açık model adını isteyin. Bir ad olmadan varsayılan combo/ şeklindedir. OpenCodex bir hedef seçer ve yalnızca yeniden denenebilir yukarı akış hatalarında atlar. Hiçbir hedef kullanılabilir kalmazsa, istek küresel varsayılan sağlayıcıyı kullanmak yerine kapalı olarak başarısız olur (fail closed).", @@ -1910,14 +1904,8 @@ export const tr: Record = { "cws.tab.about": "Hakkında", "cws.strategy": "Strateji", "cws.strategy.failover": "Yedekli (Failover)", - "cws.strategy.random": "Rastgele", - "cws.strategy.leastUsed": "En az kullanilan", - "cws.strategy.resetWindow": "Sıfırlama penceresi", "cws.strategy.roundRobin": "Round-robin", "cws.strategy.failoverHint": "Hedefleri sırayla deneyin. İlk hedef yeniden denenebilir bir hatayla (oran sınırı, kesinti, abonelik engeli) başarısız olursa sonraki hedefe atlayın.", - "cws.strategy.randomHint": "Agirlikli rastgele secim. Daha yuksek agirlikli hedefler daha sik secilir.", - "cws.strategy.resetWindowHint": "Bilinen kota penceresi en erken sıfırlanacak hedefleri tercih eder. Sıfırlama zamanı bilinmeyen hedefler yedek olarak kalır.", - "cws.strategy.leastUsedHint": "En az kaydedilmis kullanima sahip hedefi secer.", "cws.strategy.roundRobinHint": "Trafiği ağırlığa göre kararlı bir şekilde dengeleyin. Seçilen her hedefi bir dizi başarılı istek boyunca tutun, ardından ilerleyin.", "cws.field.id": "Kombo ID", "cws.field.idHint": "İstemciler {model} isteyecek", @@ -1944,9 +1932,6 @@ export const tr: Record = { "cws.targets": "Hedefler", "cws.targets.failoverHint": "Sıralama önemlidir — birincil olan ilktir.", "cws.targets.roundRobinHint": "Ağırlıklar kararlı bağıntılı seçimi kontrol eder; sıralama rotasyon halkasındaki eşitlikleri bozar.", - "cws.targets.randomHint": "Ağırlıklar, her hedefin bir istekte seçilme olasılığını belirler.", - "cws.targets.leastUsedHint": "Başarılı kullanım sayısı eşit olan hedeflerde sıralama eşitliği bozar.", - "cws.targets.resetWindowHint": "Sıralama eşitlikleri bozar ve önbellekte kota sıfırlama zamanı yoksa yedek sıra olarak kalır.", "cws.target.provider": "Sağlayıcı", "cws.target.model": "Model", "cws.target.weight": "Ağırlık", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 3e80378dc8..d35a96bd34 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1408,9 +1408,6 @@ export const zhTW: Record = { "cws.searchPlaceholder": "搜尋組合或目標…", "cws.noSearchResults": "沒有符合的組合。", "cws.group.failover": "容錯移轉", - "cws.group.weighted": "加權", - "cws.group.leastUsed": "最少使用", - "cws.group.resetWindow": "重設視窗", "cws.group.roundRobin": "輪詢", "cws.targetCount": "{count} 個目標", "cws.targetCountOne": "1 個目標", @@ -1418,9 +1415,6 @@ export const zhTW: Record = { "cws.overviewBlurb": "可在供應商/模型目標間容錯移轉,或採用確定性平滑加權輪詢的虛擬模型。", "cws.count.total": "總計", "cws.count.failover": "容錯移轉", - "cws.count.weighted": "加權", - "cws.count.leastUsed": "最少使用", - "cws.count.resetWindow": "重設視窗", "cws.count.roundRobin": "輪詢", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中請求組合的公開模型名稱;未設定時預設使用 combo/。OpenCodex 僅在可重試的上游錯誤時切換目標。若沒有可用目標,請求會直接失敗,不會回退到全域性預設供應商。", @@ -1438,14 +1432,8 @@ export const zhTW: Record = { "cws.tab.about": "關於", "cws.strategy": "策略", "cws.strategy.failover": "容錯移轉", - "cws.strategy.random": "隨機", - "cws.strategy.leastUsed": "最少使用", - "cws.strategy.resetWindow": "重設視窗", "cws.strategy.roundRobin": "輪詢", "cws.strategy.failoverHint": "按順序嘗試目標。若出現可重試錯誤(限流、故障、訂閱門控),則跳到下一個。", - "cws.strategy.randomHint": "加權隨機選擇。權重較高的目標更可能被選中。", - "cws.strategy.resetWindowHint": "優先選擇已知配額視窗最早重設的目標。沒有已知重設時間的目標仍會作為後備目標。", - "cws.strategy.leastUsedHint": "選擇記錄使用次數最少的目標。", "cws.strategy.roundRobinHint": "按權重確定性地分配流量。將所選目標保留一批成功請求後,再推進到下一個目標。", "cws.field.id": "組合 ID", "cws.field.idHint": "客戶端將請求 {model}", @@ -1468,9 +1456,6 @@ export const zhTW: Record = { "cws.targets": "目標", "cws.targets.failoverHint": "順序很重要 — 第一個為主。", "cws.targets.roundRobinHint": "權重控制確定性的相對選擇;順序用於打破輪換環中的平局。", - "cws.targets.randomHint": "權重控制每個目標在請求中被選中的機率。", - "cws.targets.leastUsedHint": "成功使用次數相同時,順序用於打破平局。", - "cws.targets.resetWindowHint": "順序用於打破平局,並在沒有快取的配額重設時間時作為後備順序。", "cws.target.provider": "供應商", "cws.target.model": "模型", "cws.target.weight": "權重", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 6cb3f14276..dc749ac530 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1869,9 +1869,6 @@ export const zh: Record = { "cws.searchPlaceholder": "搜索组合或目标…", "cws.noSearchResults": "没有匹配的组合。", "cws.group.failover": "故障转移", - "cws.group.weighted": "加权", - "cws.group.leastUsed": "最少使用", - "cws.group.resetWindow": "重设窗口", "cws.group.roundRobin": "轮询", "cws.targetCount": "{count} 个目标", "cws.targetCountOne": "1 个目标", @@ -1879,9 +1876,6 @@ export const zh: Record = { "cws.overviewBlurb": "可在提供方/模型目标间故障转移,或采用确定性平滑加权轮询的虚拟模型。", "cws.count.total": "总计", "cws.count.failover": "故障转移", - "cws.count.weighted": "加权", - "cws.count.leastUsed": "最少使用", - "cws.count.resetWindow": "重设窗口", "cws.count.roundRobin": "轮询", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中请求组合的公开模型名称;未设置时默认使用 combo/。OpenCodex 仅在可重试的上游错误时切换目标。若没有可用目标,请求会直接失败,不会回退到全局默认提供方。", @@ -1900,14 +1894,8 @@ export const zh: Record = { "cws.tab.about": "关于", "cws.strategy": "策略", "cws.strategy.failover": "故障转移", - "cws.strategy.random": "随机", - "cws.strategy.leastUsed": "最少使用", - "cws.strategy.resetWindow": "重设窗口", "cws.strategy.roundRobin": "轮询", "cws.strategy.failoverHint": "按顺序尝试目标。若出现可重试错误(限流、故障、订阅门控),则跳到下一个。", - "cws.strategy.randomHint": "加权随机选择。权重较高的目标更可能被选中。", - "cws.strategy.resetWindowHint": "优先选择已知配额窗口最早重设的目标。没有已知重设时间的目标仍会作为后备目标。", - "cws.strategy.leastUsedHint": "选择记录使用次数最少的目标。", "cws.strategy.roundRobinHint": "按权重确定性地分配流量。将所选目标保留一批成功请求后,再推进到下一个目标。", "cws.field.id": "组合 ID", "cws.field.idHint": "客户端将请求 {model}", @@ -1934,9 +1922,6 @@ export const zh: Record = { "cws.targets": "目标", "cws.targets.failoverHint": "顺序很重要 — 第一个为主。", "cws.targets.roundRobinHint": "权重控制确定性的相对选择;顺序用于打破轮换环中的平局。", - "cws.targets.randomHint": "权重控制每个目标在请求中被选中的概率。", - "cws.targets.leastUsedHint": "成功使用次数相同时,顺序用于打破平局。", - "cws.targets.resetWindowHint": "顺序用于打破平局,并在没有缓存的配额重设时间时作为后备顺序。", "cws.target.provider": "提供方", "cws.target.model": "模型", "cws.target.weight": "权重", diff --git a/tests/combo-management-api.test.ts b/tests/combo-management-api.test.ts index 49ec7b53b6..d627e22186 100644 --- a/tests/combo-management-api.test.ts +++ b/tests/combo-management-api.test.ts @@ -1036,4 +1036,72 @@ describe("supported disabled-provider activation", () => { }); }, 10_000); }); + +describe("combo response-path strategy accounting", () => { + function responseRequest(): Request { + return new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "combo/free", input: "hello", stream: false }), + }); + } + + function completion(label: string): Response { + return Response.json({ + id: `chatcmpl-${label}`, + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: label }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }); + } + + test("least-used counts successful response-path attempts", async () => { + let aHits = 0; + let bHits = 0; + const upstreamA = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch() { aHits += 1; return completion("a"); } }); + const upstreamB = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch() { bHits += 1; return completion("b"); } }); + try { + const config = baseConfig({ + providers: { + a: { adapter: "openai-chat", baseUrl: `${upstreamA.url}v1`, allowPrivateNetwork: true, apiKey: "ka", models: ["m1"] }, + b: { adapter: "openai-chat", baseUrl: `${upstreamB.url}v1`, allowPrivateNetwork: true, apiKey: "kb", models: ["m2"] }, + }, + combos: { free: { strategy: "least-used", targets: [{ provider: "a", model: "m1" }, { provider: "b", model: "m2" }] } }, + }); + expect((await handleResponses(responseRequest(), config, { model: "", provider: "" })).status).toBe(200); + expect((await handleResponses(responseRequest(), config, { model: "", provider: "" })).status).toBe(200); + expect({ aHits, bHits }).toEqual({ aHits: 1, bHits: 1 }); + } finally { + await upstreamA.stop(true); + await upstreamB.stop(true); + } + }, 10_000); + + test("reset-window retries the next target and cools the failed target", async () => { + let aHits = 0; + let bHits = 0; + const upstreamA = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch() { aHits += 1; return Response.json({ error: { message: "busy" } }, { status: 429, headers: { "retry-after": "60" } }); }, + }); + const upstreamB = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch() { bHits += 1; return completion("b"); } }); + try { + const config = baseConfig({ + providers: { + a: { adapter: "openai-chat", baseUrl: `${upstreamA.url}v1`, allowPrivateNetwork: true, apiKey: "ka", models: ["m1"] }, + b: { adapter: "openai-chat", baseUrl: `${upstreamB.url}v1`, allowPrivateNetwork: true, apiKey: "kb", models: ["m2"] }, + }, + combos: { free: { strategy: "reset-window", targets: [{ provider: "a", model: "m1" }, { provider: "b", model: "m2" }] } }, + }); + const response = await handleResponses(responseRequest(), config, { model: "", provider: "" }); + expect(response.status).toBe(200); + expect({ aHits, bHits }).toEqual({ aHits: 1, bHits: 1 }); + expect(isComboTargetInCooldown("free", { provider: "a", model: "m1" })).toBe(true); + } finally { + await upstreamA.stop(true); + await upstreamB.stop(true); + } + }, 10_000); +}); import { ManagementRequest as Request } from "./helpers/management-auth"; diff --git a/tests/combos.test.ts b/tests/combos.test.ts index 0168812794..acd83d27bb 100644 --- a/tests/combos.test.ts +++ b/tests/combos.test.ts @@ -47,6 +47,8 @@ import { injectClaudeAgentDefs } from "../src/claude/agents-inject"; import { reconcileComboRotationState } from "../src/combos/resolve"; import { clearCachedProviderQuotas, + getCachedProviderQuota, + replaceCachedProviderQuotas, setCachedProviderQuotaForTests, } from "../src/providers/quota-routing-cache"; import { catalogConvergenceFactory } from "./helpers/catalog-convergence"; @@ -431,6 +433,19 @@ describe("combo failure policy and advancement", () => { }); describe("deterministic combo selection", () => { + test("replacing quota snapshots removes providers omitted from the refresh", () => { + const now = Date.now(); + replaceCachedProviderQuotas([ + { provider: "a", label: "a", source: "test", quota: { updatedAt: now } }, + { provider: "b", label: "b", source: "test", quota: { updatedAt: now } }, + ]); + replaceCachedProviderQuotas([ + { provider: "a", label: "a", source: "test", quota: { updatedAt: now } }, + ]); + expect(getCachedProviderQuota("a", now)).not.toBeNull(); + expect(getCachedProviderQuota("b", now)).toBeNull(); + }); + test("equal-weight RR rotates exactly", () => { const config = rrConfig(1, [1, 1, 1]); expect(successfulPicks(config, 6)).toEqual([