From 69c2153ddb4b1a2acad1ce4fbfd1b6f9ef1b0fbb Mon Sep 17 00:00:00 2001 From: x3M3x Date: Sat, 29 Aug 2026 20:30:16 +0400 Subject: [PATCH] fix(gui): group and translate the newer combo strategies in the workspace --- gui/src/combo-workspace-data.ts | 34 +++++++++++++++++-- gui/src/components/ComboWorkspace.tsx | 1 + .../components/combo-workspace-add-modal.tsx | 13 ++----- .../components/combo-workspace-controls.tsx | 4 +-- .../combo-workspace-detail-panel.tsx | 13 ++----- .../combo-workspace-overview-panel.tsx | 1 + 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 ++++++-- gui/tests/combo-strategy-roundtrip.test.ts | 14 +++++++- 16 files changed, 171 insertions(+), 44 deletions(-) diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts index 07943dc7f00..d6d9ea204f5 100644 --- a/gui/src/combo-workspace-data.ts +++ b/gui/src/combo-workspace-data.ts @@ -4,6 +4,7 @@ */ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../../src/codex/catalog/native-models"; +import type { TKey } from "./i18n/shared"; export { SUPPORTED_NATIVE_OPENAI_SLUGS }; @@ -20,6 +21,30 @@ export const COMBO_STRATEGIES: readonly ComboStrategy[] = [ "reset-window", ] as const; +export const COMBO_STRATEGY_LABEL_KEYS: Record = { + failover: "cws.strategy.failover", + "round-robin": "cws.strategy.roundRobin", + random: "cws.strategy.random", + "least-used": "cws.strategy.leastUsed", + "reset-window": "cws.strategy.resetWindow", +}; + +export const COMBO_STRATEGY_HINT_KEYS: Record = { + failover: "cws.strategy.failoverHint", + "round-robin": "cws.strategy.roundRobinHint", + random: "cws.strategy.randomHint", + "least-used": "cws.strategy.leastUsedHint", + "reset-window": "cws.strategy.resetWindowHint", +}; + +export const COMBO_TARGETS_HINT_KEYS: Record = { + failover: "cws.targets.failoverHint", + "round-robin": "cws.targets.roundRobinHint", + random: "cws.targets.randomHint", + "least-used": "cws.targets.leastUsedHint", + "reset-window": "cws.targets.resetWindowHint", +}; + const COMBO_STRATEGY_SET = new Set(COMBO_STRATEGIES); /** @@ -101,6 +126,7 @@ export interface ComboItem { export interface ComboSections { failover: ComboItem[]; roundRobin: ComboItem[]; + other: ComboItem[]; } export interface ComboAttentionItem { @@ -211,11 +237,13 @@ export function parseComboList(payload: unknown): ComboItem[] { export function groupCombos(items: ComboItem[]): ComboSections { const failover: ComboItem[] = []; const roundRobin: ComboItem[] = []; + const other: ComboItem[] = []; for (const item of items) { - if (item.strategy === "round-robin") roundRobin.push(item); - else failover.push(item); + if (item.strategy === "failover") failover.push(item); + else if (item.strategy === "round-robin") roundRobin.push(item); + else other.push(item); } - return { failover, roundRobin }; + return { failover, roundRobin, other }; } export function filterCombos(items: ComboItem[], query: string): ComboItem[] { diff --git a/gui/src/components/ComboWorkspace.tsx b/gui/src/components/ComboWorkspace.tsx index 28b31d67cd4..d66ca8a2421 100644 --- a/gui/src/components/ComboWorkspace.tsx +++ b/gui/src/components/ComboWorkspace.tsx @@ -129,6 +129,7 @@ export default function ComboWorkspace({ {([ ["failover", sections.failover, "cws.group.failover"], ["round-robin", sections.roundRobin, "cws.group.roundRobin"], + ["other", sections.other, "cws.group.other"], ] as const).map(([key, items, labelKey]) => ( items.length > 0 ? (
diff --git a/gui/src/components/combo-workspace-add-modal.tsx b/gui/src/components/combo-workspace-add-modal.tsx index ed18aaacfe2..57f6253509d 100644 --- a/gui/src/components/combo-workspace-add-modal.tsx +++ b/gui/src/components/combo-workspace-add-modal.tsx @@ -13,6 +13,7 @@ import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; import { ComboCapabilities, EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { COMBO_STRATEGY_HINT_KEYS, COMBO_TARGETS_HINT_KEYS } from "../combo-workspace-data"; import { clampedNumberInput } from "./combo-workspace-utils"; export function AddComboModal({ @@ -163,11 +164,7 @@ export function AddComboModal({ onChange={(strategy) => setDraft((d) => ({ ...d, strategy }))} />

- {draft.strategy === "failover" - ? t("cws.strategy.failoverHint") - : draft.strategy === "round-robin" - ? t("cws.strategy.roundRobinHint") - : null} + {t(COMBO_STRATEGY_HINT_KEYS[draft.strategy])}

@@ -216,11 +213,7 @@ export function AddComboModal({ onChange={(targets) => setDraft((d) => ({ ...d, targets }))} />

- {draft.strategy === "failover" - ? t("cws.targets.failoverHint") - : draft.strategy === "round-robin" - ? t("cws.targets.roundRobinHint") - : null} + {t(COMBO_TARGETS_HINT_KEYS[draft.strategy])}

- {value} + {t(COMBO_STRATEGY_LABEL_KEYS[value])} ) : null} diff --git a/gui/src/components/combo-workspace-detail-panel.tsx b/gui/src/components/combo-workspace-detail-panel.tsx index 409da0b2655..32a6625fde9 100644 --- a/gui/src/components/combo-workspace-detail-panel.tsx +++ b/gui/src/components/combo-workspace-detail-panel.tsx @@ -15,6 +15,7 @@ import { useT } from "../i18n/shared"; import { Notice } from "../ui"; import type { ModelOption, ProviderOption } from "./combo-workspace-types"; import { ComboCapabilities, EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls"; +import { COMBO_STRATEGY_HINT_KEYS, COMBO_TARGETS_HINT_KEYS } from "../combo-workspace-data"; import { clampedNumberInput } from "./combo-workspace-utils"; type DetailTab = "config" | "about"; @@ -317,11 +318,7 @@ export function DetailPanel({ onChange={(strategy) => updateDraft((d) => ({ ...d, strategy }))} />

- {draft.strategy === "failover" - ? t("cws.strategy.failoverHint") - : draft.strategy === "round-robin" - ? t("cws.strategy.roundRobinHint") - : null} + {t(COMBO_STRATEGY_HINT_KEYS[draft.strategy])}

@@ -367,11 +364,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") - : null} + {t(COMBO_TARGETS_HINT_KEYS[draft.strategy])}

{combos.length}{t("cws.count.total")}
{sections.failover.length}{t("cws.count.failover")}
{sections.roundRobin.length}{t("cws.count.roundRobin")}
+
{sections.other.length}{t("cws.count.other")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 0913f2ad47c..d2f1830a1e5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -480,7 +480,7 @@ export const de: Record = { "models.tab.compatibility": "Kompatibilität", "models.tab.routing": "Routing (beta)", "models.tabsLabel": "Modell-Oberflächen", - "models.subtitle.combos": "Geordnete Modellgruppen, die unter einer id antworten. Failover probiert Ziele der Reihe nach, Round-Robin verteilt die Last.", + "models.subtitle.combos": "Geordnete Modellgruppen, die unter einer id antworten. Ziele mit Failover verketten oder die Last mit einer Balancing-Strategie verteilen.", "models.subtitle.compatibility": "Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.", "models.subtitle.routing": "Policy-Profile, Dry-Run-Auswertung und quellenbasierte Routing-Analysen.", "models.subtitle": "Steuere, welche Modelle Codex sieht — natives GPT-Passthrough und geroutete Anbieter, nach Anbieter gruppiert (Kopfzeile zum Einklappen anklicken). Ausgeblendete Modelle fehlen in Katalog und Auswahl, bleiben aber per genauer ID aufrufbar. Änderungen gelten bei der nächsten Codex-Runde — opencodex invalidiert Codex 5-Minuten-Modell-Cache, kein Neustart nötig.", @@ -2018,13 +2018,15 @@ export const de: Record = { "cws.noSearchResults": "Keine Combos passen zur Suche.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-Robin", + "cws.group.other": "Weitere Strategien", "cws.targetCount": "{count} Ziele", "cws.targetCountOne": "1 Ziel", "cws.overviewTitle": "Combos", - "cws.overviewBlurb": "Virtuelle Modelle mit Failover über Anbieter/Modell-Ziele oder deterministischem Smooth Weighted Round-Robin.", + "cws.overviewBlurb": "Virtuelle Modelle, die über Anbieter/Modell-Ziele mit Failover, Round-Robin, gewichtetem Zufall, seltenst genutztem Ziel oder frühestem Quota-Reset weiterleiten.", "cws.count.total": "Gesamt", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-Robin", + "cws.count.other": "Weitere", "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", @@ -2044,8 +2046,14 @@ export const de: Record = { "cws.strategy": "Strategie", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-Robin", + "cws.strategy.random": "Zufall", + "cws.strategy.leastUsed": "Seltenst genutzt", + "cws.strategy.resetWindow": "Reset-Fenster", "cws.strategy.failoverHint": "Ziele der Reihe nach versuchen. Bei einem wiederholbaren Fehler (Limit, Ausfall, Abo-Sperre) zum nächsten springen.", "cws.strategy.roundRobinHint": "Datenverkehr deterministisch nach Gewicht verteilen. Das gewählte Ziel für einen Block erfolgreicher Anfragen behalten und dann weiterschalten.", + "cws.strategy.randomHint": "Pro Anfrage ein geeignetes Ziel ziehen, mit Wahrscheinlichkeiten proportional zum Gewicht. Keine Bindung zwischen Anfragen.", + "cws.strategy.leastUsedHint": "Jede Anfrage an das geeignete Ziel mit den wenigsten erfassten Erfolgen weiterleiten. Zählungen starten mit dem Proxy neu.", + "cws.strategy.resetWindowHint": "Bevorzugt das geeignete Ziel, dessen Quota-Fenster am frühesten zurückgesetzt wird. Ohne Quota-Daten gilt die Konfigurationsreihenfolge.", "cws.field.id": "Combo-ID", "cws.field.idHintEdit": "Das Ändern der ID benennt die Combo um. Clients fordern {model} an.", "cws.field.alias": "Öffentlicher Modellname", @@ -2071,6 +2079,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 jeder Ziehung; die Reihenfolge spielt keine Rolle.", + "cws.targets.leastUsedHint": "Die Reihenfolge löst nur Gleichstände zwischen gleich oft genutzten Zielen.", + "cws.targets.resetWindowHint": "Die Reihenfolge gilt, wenn Quota-Daten fehlen oder gleich ausfallen.", "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 8f1f1333d31..2d4c55b1398 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -505,7 +505,7 @@ export const en = { "models.tab.compatibility": "Compatibility", "models.tab.routing": "Routing (beta)", "models.tabsLabel": "Model surfaces", - "models.subtitle.combos": "Ordered groups of models that answer as one id. Failover tries targets in order; round-robin spreads the load.", + "models.subtitle.combos": "Ordered groups of models that answer as one id. Chain targets with failover or spread the load with a balancing strategy.", "models.subtitle.compatibility": "Read-only compatibility verdict matrix from lab projection evidence.", "models.subtitle.routing": "Policy profiles, dry-run evaluation, and source-backed routing analytics.", "models.subtitle": "Toggle which models Codex sees — native GPT passthrough and routed providers, grouped by provider (click a header to collapse). Hidden models stay off the catalog + model picker but remain directly callable by exact id. Changes apply on the next Codex turn — opencodex invalidates Codex's 5-min model cache so no restart is needed.", @@ -2058,13 +2058,15 @@ export const en = { "cws.noSearchResults": "No combos match your search.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-robin", + "cws.group.other": "Other strategies", "cws.targetCount": "{count} targets", "cws.targetCountOne": "1 target", "cws.overviewTitle": "Combos", - "cws.overviewBlurb": "Virtual models that fail over across provider/model targets or use deterministic smooth weighted round-robin.", + "cws.overviewBlurb": "Virtual models that route across provider/model targets with failover, round-robin, weighted random, least-used, or soonest quota reset.", "cws.count.total": "Total", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-robin", + "cws.count.other": "Other", "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", @@ -2084,8 +2086,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": "Draw one eligible target per request, with odds proportional to weight. No stickiness between requests.", + "cws.strategy.leastUsedHint": "Route each request to the eligible target with the fewest recorded successes. Counts restart with the proxy.", + "cws.strategy.resetWindowHint": "Prefer the eligible target whose quota window resets soonest. Falls back to configuration order when quota data is missing.", "cws.field.id": "Combo id", "cws.field.idHint": "Clients will request {model}", "cws.field.idInternalHint": "Internal combo id. You can change it after creation.", @@ -2111,6 +2119,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 each draw's odds; order does not matter.", + "cws.targets.leastUsedHint": "Order only breaks ties between equally used targets.", + "cws.targets.resetWindowHint": "Order applies when quota data is missing or tied.", "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 fedc4698be3..99d64ee4e3a 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -490,7 +490,7 @@ export const fr: Record = { "models.tab.compatibility": "Compatibilité", "models.tab.routing": "Routage (bêta)", "models.tabsLabel": "Espaces des modèles", - "models.subtitle.combos": "Groupes ordonnés de modèles qui répondent sous un même identifiant. Le repli essaie les cibles dans l’ordre ; la rotation distribue la charge.", + "models.subtitle.combos": "Groupes ordonnés de modèles qui répondent sous un même identifiant. Enchaînez les cibles avec le repli ou répartissez la charge avec une stratégie d’équilibrage.", "models.subtitle.compatibility": "Matrice en lecture seule des verdicts de compatibilité issus des preuves de projection du laboratoire.", "models.subtitle.routing": "Profils de stratégie, évaluation à blanc et analyses de routage fondées sur les sources.", "models.subtitle": "Choisissez les modèles visibles par Codex — accès direct aux GPT natifs et fournisseurs routés, regroupés par fournisseur (cliquez sur un en-tête pour le réduire). Les modèles masqués sont retirés du catalogue et du sélecteur, mais restent directement accessibles par leur identifiant exact. Les modifications s’appliquent au prochain tour Codex — opencodex invalide le cache de modèles de Codex de 5 min, sans nécessiter de redémarrage.", @@ -2019,13 +2019,15 @@ export const fr: Record = { "cws.noSearchResults": "Aucune combinaison ne correspond à votre recherche.", "cws.group.failover": "Basculement", "cws.group.roundRobin": "Rotation", + "cws.group.other": "Autres stratégies", "cws.targetCount": "{count} cibles", "cws.targetCountOne": "1 cible", "cws.overviewTitle": "Combinaisons", - "cws.overviewBlurb": "Modèles virtuels qui basculent entre des cibles fournisseur/modèle ou utilisent une rotation pondérée fluide et déterministe.", + "cws.overviewBlurb": "Modèles virtuels qui routent entre des cibles fournisseur/modèle par repli, rotation, aléatoire pondéré, moins utilisé ou réinitialisation de quota la plus proche.", "cws.count.total": "Total", "cws.count.failover": "Basculement", "cws.count.roundRobin": "Rotation", + "cws.count.other": "Autres", "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", @@ -2049,8 +2051,14 @@ export const fr: Record = { "cws.strategy": "Stratégie", "cws.strategy.failover": "Basculement", "cws.strategy.roundRobin": "Rotation", + "cws.strategy.random": "Aléatoire", + "cws.strategy.leastUsed": "Moins utilisé", + "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.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.strategy.randomHint": "Tire une cible éligible par requête, avec des probabilités proportionnelles au poids. Aucune adhérence entre requêtes.", + "cws.strategy.leastUsedHint": "Dirige chaque requête vers la cible éligible ayant le moins de succès enregistrés. Les compteurs redémarrent avec le proxy.", + "cws.strategy.resetWindowHint": "Préfère la cible éligible dont la fenêtre de quota se réinitialise le plus tôt. Sans données de quota, l’ordre de configuration s’applique.", "cws.field.id": "Identifiant de la combinaison", "cws.field.idHint": "Les clients demanderont {model}", "cws.field.idInternalHint": "Identifiant interne de la combinaison. Vous pouvez le modifier après la création.", @@ -2072,6 +2080,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 les chances de chaque tirage ; l’ordre n’a pas d’importance.", + "cws.targets.leastUsedHint": "L’ordre ne départage que les cibles également utilisées.", + "cws.targets.resetWindowHint": "L’ordre s’applique quand les données de quota manquent ou sont égales.", "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 646faf05606..4788f0ef800 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -488,7 +488,7 @@ export const ja: Record = { "models.tab.compatibility": "互換性", "models.tab.routing": "ルーティング (beta)", "models.tabsLabel": "モデルサーフェス", - "models.subtitle.combos": "複数のモデルを 1 つの id にまとめ、順に応答させます。failover は順番に試し、round-robin は負荷を分散します。", + "models.subtitle.combos": "複数のモデルを 1 つの id にまとめ、順に応答させます。failover でターゲットを連鎖させるか、分散戦略で負荷を分散します。", "models.subtitle.compatibility": "ラボ投影証拠の読み取り専用互換性判定マトリクス。", "models.subtitle.routing": "ポリシープロファイル、dry-run 評価、そして根拠の残るルーティング分析です。", "models.subtitle": "Codex に表示するモデルを切り替えます — ネイティブ GPT パススルーとルーティングプロバイダー、プロバイダー別(ヘッダーをクリックで折りたたみ)。非表示モデルはカタログとピッカーから外れますが、正確な id での直接呼び出しは可能です。変更は次回の Codex ターンで適用 — opencodex は Codex の 5 分間モデルキャッシュを無効化するので再起動は不要です。", @@ -2076,13 +2076,15 @@ export const ja: Record = { "cws.noSearchResults": "検索に一致するコンボがありません。", "cws.group.failover": "フェイルオーバー", "cws.group.roundRobin": "ラウンドロビン", + "cws.group.other": "その他の戦略", "cws.targetCount": "{count} ターゲット", "cws.targetCountOne": "1 ターゲット", "cws.overviewTitle": "コンボ", - "cws.overviewBlurb": "プロバイダー/モデルターゲット間でフェイルオーバーする、または決定論的で滑らかな重み付きラウンドロビンを使う仮想モデル。", + "cws.overviewBlurb": "プロバイダー/モデルターゲット間を、フェイルオーバー、ラウンドロビン、重み付きランダム、最少使用、最短クォータリセットで振り分ける仮想モデル。", "cws.count.total": "合計", "cws.count.failover": "フェイルオーバー", "cws.count.roundRobin": "ラウンドロビン", + "cws.count.other": "その他", "cws.howTitle": "仕組み", "cws.howBody": "Codex に combo/ を要求します。OpenCodex はターゲットを選び、再試行可能な上流の失敗時のみホップします。利用可能なターゲットが残っていない場合、グローバルなデフォルトプロバイダーを使わずにフェイルクローズします。", "cws.attentionTitle": "要対応", @@ -2103,8 +2105,14 @@ export const ja: Record = { "cws.strategy": "ストラテジー", "cws.strategy.failover": "フェイルオーバー", "cws.strategy.roundRobin": "ラウンドロビン", + "cws.strategy.random": "ランダム", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "リセットウィンドウ", "cws.strategy.failoverHint": "ターゲットを順に試します。最初が再試行可能なエラー(レート制限、障害、サブスクリプションゲート)で失敗した場合、次へホップします。", "cws.strategy.roundRobinHint": "重みで決定論的にトラフィックを分散します。選んだターゲットを成功リクエストのバッチ分保持し、次へ進みます。", + "cws.strategy.randomHint": "リクエストごとに適格なターゲットを 1 つ抽選します。確率は重みに比例し、リクエスト間でスティッキネスはありません。", + "cws.strategy.leastUsedHint": "各リクエストを、成功回数が最も少ない適格なターゲットへ振ります。カウントはプロキシの再起動でリセットされます。", + "cws.strategy.resetWindowHint": "クォータのウィンドウが最も早くリセットされる適格なターゲットを優先します。クォータデータがない場合は設定順に従います。", "cws.field.id": "コンボ ID", "cws.field.idHint": "クライアントは {model} をリクエストします", "cws.field.idInternalHint": "コンボの内部 ID。作成後も変更できます。", @@ -2130,6 +2138,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 00edec94d72..33c4452c146 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -491,7 +491,7 @@ export const ko: Record = { "models.tab.compatibility": "호환성", "models.tab.routing": "라우팅 (beta)", "models.tabsLabel": "모델 표면", - "models.subtitle.combos": "여러 모델을 하나의 id로 묶어 순서대로 응답하게 합니다. failover는 순서대로 시도하고, round-robin은 부하를 나눕니다.", + "models.subtitle.combos": "여러 모델을 하나의 id로 묶어 순서대로 응답하게 합니다. failover로 대상을 연결하거나 분산 전략으로 부하를 나눕니다.", "models.subtitle.compatibility": "랩 프로젝션 증거의 읽기 전용 호환성 판정 행렬.", "models.subtitle.routing": "정책 프로필, dry-run 평가, 그리고 근거가 남는 라우팅 분석입니다.", "models.subtitle": "Codex가 보는 모델을 켜고 끕니다 — 네이티브 GPT passthrough와 라우팅된 모델을 프로바이더별로 묶어 보여줍니다(헤더를 클릭하면 접힘). 숨긴 모델은 카탈로그와 선택기에서 빠지지만 정확한 id로 직접 호출할 수 있습니다. 변경 사항은 다음 Codex 턴에 적용됩니다 — opencodex가 Codex의 5분 모델 캐시를 무효화하므로 재시작이 필요 없습니다.", @@ -2045,13 +2045,15 @@ export const ko: Record = { "cws.noSearchResults": "검색과 일치하는 콤보가 없습니다.", "cws.group.failover": "장애 조치", "cws.group.roundRobin": "라운드로빈", + "cws.group.other": "기타 전략", "cws.targetCount": "대상 {count}개", "cws.targetCountOne": "대상 1개", "cws.overviewTitle": "콤보", - "cws.overviewBlurb": "프로바이더/모델 대상 사이에서 장애 조치하거나 결정적 Smooth Weighted 라운드로빈을 사용하는 가상 모델입니다.", + "cws.overviewBlurb": "프로바이더/모델 대상 사이를 장애 조치, 라운드로빈, 가중 랜덤, 최소 사용, 최단 쿼터 리셋으로 라우팅하는 가상 모델입니다.", "cws.count.total": "전체", "cws.count.failover": "장애 조치", "cws.count.roundRobin": "라운드로빈", + "cws.count.other": "기타", "cws.howTitle": "동작 방식", "cws.howBody": "Codex에서 콤보의 공개 모델 이름을 요청하세요. 설정하지 않으면 combo/가 기본값입니다. OpenCodex는 재시도 가능한 업스트림 오류에서만 다음 대상으로 넘깁니다. 사용 가능한 대상이 없으면 전역 기본 프로바이더로 우회하지 않고 요청을 실패 처리합니다.", "cws.attentionTitle": "확인 필요", @@ -2071,8 +2073,14 @@ export const ko: Record = { "cws.strategy": "전략", "cws.strategy.failover": "장애 조치", "cws.strategy.roundRobin": "라운드로빈", + "cws.strategy.random": "랜덤", + "cws.strategy.leastUsed": "최소 사용", + "cws.strategy.resetWindow": "리셋 윈도우", "cws.strategy.failoverHint": "대상을 순서대로 시도합니다. 재시도 가능한 오류(한도, 장애, 구독 게이트)면 다음으로 넘어갑니다.", "cws.strategy.roundRobinHint": "가중치에 따라 트래픽을 결정적으로 분배합니다. 선택된 대상을 성공 요청 묶음 동안 유지한 뒤 다음 대상으로 진행합니다.", + "cws.strategy.randomHint": "요청마다 가중치에 비례한 확률로 적합한 대상을 하나 뽑습니다. 요청 간 고정이 없습니다.", + "cws.strategy.leastUsedHint": "각 요청을 성공 횟수가 가장 적은 적합한 대상으로 보냅니다. 횟수는 프록시 재시작 시 초기화됩니다.", + "cws.strategy.resetWindowHint": "쿼터 윈도우가 가장 빨리 리셋되는 적합한 대상을 우선합니다. 쿼터 데이터가 없으면 설정 순서를 따릅니다.", "cws.field.id": "콤보 ID", "cws.field.idHintEdit": "ID를 변경하면 콤보 이름이 바뀝니다. 클라이언트는 {model}을(를) 요청합니다.", "cws.field.alias": "공개 모델 이름", @@ -2098,6 +2106,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 d4a6e3e142c..08bf01b65b2 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -493,7 +493,7 @@ export const ru: Record = { "models.tab.compatibility": "Совместимость", "models.tab.routing": "Маршрутизация (beta)", "models.tabsLabel": "Поверхности моделей", - "models.subtitle.combos": "Упорядоченные группы моделей, отвечающие под одним id. Failover пробует цели по порядку, round-robin распределяет нагрузку.", + "models.subtitle.combos": "Упорядоченные группы моделей, отвечающие под одним id. Связывайте цели через failover или распределяйте нагрузку стратегией балансировки.", "models.subtitle.compatibility": "Матрица совместимости только для чтения из проекции лаборатории.", "models.subtitle.routing": "Профили политик, оценка в режиме dry-run и аналитика маршрутизации с подтверждением источников.", "models.subtitle": "Управляйте тем, какие модели видит Codex — нативные GPT (сквозной проброс) и модели маршрутизируемых провайдеров, сгруппированные по провайдеру (нажмите на заголовок, чтобы свернуть группу). Скрытые модели исчезают из каталога и селектора, но остаются вызываемыми по точному id. Изменения применяются на следующем ходе Codex — opencodex сбрасывает 5-минутный кэш моделей Codex, поэтому перезапуск не требуется.", @@ -2128,13 +2128,15 @@ export const ru: Record = { "cws.noSearchResults": "Нет комбо, соответствующих запросу.", "cws.group.failover": "Failover", "cws.group.roundRobin": "Round-robin", + "cws.group.other": "Другие стратегии", "cws.targetCount": "{count} целей", "cws.targetCountOne": "1 цель", "cws.overviewTitle": "Комбо", - "cws.overviewBlurb": "Виртуальные модели, которые при сбоях переключаются между целями провайдер/модель (отказоустойчивое переключение, failover) или используют детерминированный плавный взвешенный циклический перебор (round-robin).", + "cws.overviewBlurb": "Виртуальные модели, маршрутизирующие между целями провайдер/модель через failover, round-robin, взвешенный случайный выбор, наименее используемую цель или ближайший сброс квоты.", "cws.count.total": "Всего", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-robin", + "cws.count.other": "Другие", "cws.howTitle": "Как это работает", "cws.howBody": "Запросите у Codex публичное имя модели комбо. Если оно не задано, используется combo/. OpenCodex выбирает цель и переключается на следующую только при сбоях вышестоящего провайдера, допускающих повтор. Если доступных целей не осталось, запрос завершается ошибкой, а не переходит на глобальный провайдер по умолчанию.", "cws.attentionTitle": "Требует внимания", @@ -2154,8 +2156,14 @@ export const ru: Record = { "cws.strategy": "Стратегия", "cws.strategy.failover": "Failover", "cws.strategy.roundRobin": "Round-robin", + "cws.strategy.random": "Случайный", + "cws.strategy.leastUsed": "Наименее используемый", + "cws.strategy.resetWindow": "Окно сброса", "cws.strategy.failoverHint": "Цели перебираются по порядку. Если первая завершается ошибкой, допускающей повтор (лимит запросов, сбой, ограничение подписки), происходит переключение на следующую.", "cws.strategy.roundRobinHint": "Детерминированное распределение трафика по весам. Выбранная цель удерживается на серию успешных запросов, затем селектор переходит к следующей.", + "cws.strategy.randomHint": "Для каждого запроса выбирается одна подходящая цель с вероятностью, пропорциональной весу. Между запросами привязки нет.", + "cws.strategy.leastUsedHint": "Каждый запрос направляется к подходящей цели с наименьшим числом успешных запросов. Счётчики обнуляются при перезапуске прокси.", + "cws.strategy.resetWindowHint": "Предпочитается подходящая цель, чьё окно квот сбрасывается раньше всех. Без данных о квотах действует порядок из конфигурации.", "cws.field.id": "Id комбо", "cws.field.idHint": "Клиенты будут запрашивать {model}", "cws.field.idInternalHint": "Внутренний id комбо. Его можно изменить после создания.", @@ -2181,6 +2189,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 13914010555..a7e001271bb 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -496,7 +496,7 @@ export const tr: Record = { "models.tab.compatibility": "Uyumluluk", "models.tab.routing": "Yönlendirme (beta)", "models.tabsLabel": "Model yüzeyleri", - "models.subtitle.combos": "Tek bir kimlik olarak yanıt veren sıralı model grupları.", + "models.subtitle.combos": "Tek bir kimlik olarak yanıt veren sıralı model grupları. Hedefleri failover ile zincirleyin veya yükü dengeleme stratejisiyle dağıtın.", "models.subtitle.compatibility": "Lab projeksiyon kanıtından salt okunur uyumluluk matrisi.", "models.subtitle.routing": "Politika profilleri ve simülasyon değerlendirmesi.", "models.subtitle": "Codex'in göreceği modelleri açıp kapatın.", @@ -2048,13 +2048,15 @@ export const tr: Record = { "cws.noSearchResults": "Aramanızla eşleşen kombo yok.", "cws.group.failover": "Yedekli (Failover)", "cws.group.roundRobin": "Round-robin", + "cws.group.other": "Diğer stratejiler", "cws.targetCount": "{count} hedef", "cws.targetCountOne": "1 hedef", "cws.overviewTitle": "Kombolar", - "cws.overviewBlurb": "Sağlayıcı/model hedefleri arasında devreden sanal modeller.", + "cws.overviewBlurb": "Sağlayıcı/model hedefleri arasında failover, round-robin, ağırlıklı rastgele, en az kullanılan veya en yakın kota sıfırlamasıyla yönlendiren sanal modeller.", "cws.count.total": "Toplam", "cws.count.failover": "Failover", "cws.count.roundRobin": "Round-robin", + "cws.count.other": "Diğer", "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).", "cws.attentionTitle": "Dikkat gerekiyor", @@ -2074,8 +2076,14 @@ export const tr: Record = { "cws.strategy": "Strateji", "cws.strategy.failover": "Yedekli (Failover)", "cws.strategy.roundRobin": "Round-robin", + "cws.strategy.random": "Rastgele", + "cws.strategy.leastUsed": "En az kullanılan", + "cws.strategy.resetWindow": "Sıfırlama penceresi", "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.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.strategy.randomHint": "Her istek için ağırlığa orantılı olasılıkla bir uygun hedef çekilir. İstekler arasında yapışkanlık yoktur.", + "cws.strategy.leastUsedHint": "Her isteği, kayıtlı başarısı en az olan uygun hedefe yönlendirir. Sayaçlar proxy ile yeniden başlar.", + "cws.strategy.resetWindowHint": "Kota penceresi en yakında sıfırlanacak uygun hedefi tercih eder. Kota verisi yoksa yapılandırma sırasına döner.", "cws.field.id": "Kombo ID", "cws.field.idHint": "İstemciler {model} isteyecek", "cws.field.idInternalHint": "Dahili kombo ID. Oluşturduktan sonra değiştirebilirsiniz.", @@ -2101,6 +2109,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 çekilişin olasılığını kontrol eder; sıralamanın önemi yoktur.", + "cws.targets.leastUsedHint": "Sıralama yalnızca eşit kullanımlı hedefler arasındaki eşitliği bozar.", + "cws.targets.resetWindowHint": "Kota verisi eksik veya eşitse sıralama uygulanı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 6effb92e0f2..c2eb37f8d29 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1577,13 +1577,15 @@ export const zhTW: Record = { "cws.noSearchResults": "沒有符合的組合。", "cws.group.failover": "容錯移轉", "cws.group.roundRobin": "輪詢", + "cws.group.other": "其他策略", "cws.targetCount": "{count} 個目標", "cws.targetCountOne": "1 個目標", "cws.overviewTitle": "組合", - "cws.overviewBlurb": "可在供應商/模型目標間容錯移轉,或採用確定性平滑加權輪詢的虛擬模型。", + "cws.overviewBlurb": "在供應商/模型目標之間依容錯移轉、輪詢、加權隨機、最少使用或最早配額重置路由的虛擬模型。", "cws.count.total": "總計", "cws.count.failover": "容錯移轉", "cws.count.roundRobin": "輪詢", + "cws.count.other": "其他", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中請求組合的公開模型名稱;未設定時預設使用 combo/。OpenCodex 僅在可重試的上游錯誤時切換目標。若沒有可用目標,請求會直接失敗,不會回退到全域性預設供應商。", "cws.attentionTitle": "需要關注", @@ -1602,8 +1604,14 @@ export const zhTW: Record = { "cws.strategy": "策略", "cws.strategy.failover": "容錯移轉", "cws.strategy.roundRobin": "輪詢", + "cws.strategy.random": "隨機", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "重置視窗", "cws.strategy.failoverHint": "按順序嘗試目標。若出現可重試錯誤(限流、故障、訂閱門控),則跳到下一個。", "cws.strategy.roundRobinHint": "按權重確定性地分配流量。將所選目標保留一批成功請求後,再推進到下一個目標。", + "cws.strategy.randomHint": "每個請求按權重比例隨機抽取一個可用目標,請求之間不保持黏性。", + "cws.strategy.leastUsedHint": "將每個請求路由到成功次數最少的可用目標。計數隨代理重啟歸零。", + "cws.strategy.resetWindowHint": "優先選擇配額視窗最早重置的可用目標。缺少配額資料時回退到設定順序。", "cws.field.id": "組合 ID", "cws.field.idHint": "客戶端將請求 {model}", "cws.field.idInternalHint": "組合的內部 ID,建立後仍可修改。", @@ -1625,6 +1633,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": "權重", @@ -1960,7 +1971,7 @@ export const zhTW: Record = { "models.tab.compatibility": "相容性", "models.tab.routing": "路由 (beta)", "models.tabsLabel": "模型介面", - "models.subtitle.combos": "將多個模型合成一個 id 來回答。容錯移轉會依序嘗試目標;輪詢則分攤負載。", + "models.subtitle.combos": "將多個模型合成一個 id 來回答。用容錯移轉串接目標,或用均衡策略分攤負載。", "models.subtitle.compatibility": "來自實驗室投影證據的唯讀相容性判定矩陣。", "models.subtitle.routing": "原則設定檔、dry-run 評估,以及有來源依據的路由分析。", "models.contextSettings": "自訂視窗", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b2cb24f862a..bc36d0f752e 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -488,7 +488,7 @@ export const zh: Record = { "models.tab.compatibility": "兼容性", "models.tab.routing": "路由 (beta)", "models.tabsLabel": "模型界面", - "models.subtitle.combos": "把多个模型合成一个 id 依次应答。failover 按顺序尝试,round-robin 分摊负载。", + "models.subtitle.combos": "把多个模型合成一个 id 依次应答。用 failover 串联目标,或用均衡策略分摊负载。", "models.subtitle.compatibility": "来自实验室投影证据的只读兼容性判定矩阵。", "models.subtitle.routing": "策略配置、dry-run 评估,以及有据可查的路由分析。", "models.subtitle": "开关 Codex 可见的模型 — 原生 GPT passthrough 与已路由模型按提供方分组(点击标题可折叠)。隐藏的模型不会出现在目录和模型选择器中,但仍可按精确 id 直接调用。更改在下一个 Codex 回合生效 — opencodex 会使 Codex 的 5 分钟模型缓存失效,因此无需重启。", @@ -2038,13 +2038,15 @@ export const zh: Record = { "cws.noSearchResults": "没有匹配的组合。", "cws.group.failover": "故障转移", "cws.group.roundRobin": "轮询", + "cws.group.other": "其他策略", "cws.targetCount": "{count} 个目标", "cws.targetCountOne": "1 个目标", "cws.overviewTitle": "组合", - "cws.overviewBlurb": "可在提供方/模型目标间故障转移,或采用确定性平滑加权轮询的虚拟模型。", + "cws.overviewBlurb": "在提供方/模型目标之间按故障转移、轮询、加权随机、最少使用或最早配额重置路由的虚拟模型。", "cws.count.total": "总计", "cws.count.failover": "故障转移", "cws.count.roundRobin": "轮询", + "cws.count.other": "其他", "cws.howTitle": "工作原理", "cws.howBody": "在 Codex 中请求组合的公开模型名称;未设置时默认使用 combo/。OpenCodex 仅在可重试的上游错误时切换目标。若没有可用目标,请求会直接失败,不会回退到全局默认提供方。", "cws.attentionTitle": "需要关注", @@ -2064,8 +2066,14 @@ export const zh: Record = { "cws.strategy": "策略", "cws.strategy.failover": "故障转移", "cws.strategy.roundRobin": "轮询", + "cws.strategy.random": "随机", + "cws.strategy.leastUsed": "最少使用", + "cws.strategy.resetWindow": "重置窗口", "cws.strategy.failoverHint": "按顺序尝试目标。若出现可重试错误(限流、故障、订阅门控),则跳到下一个。", "cws.strategy.roundRobinHint": "按权重确定性地分配流量。将所选目标保留一批成功请求后,再推进到下一个目标。", + "cws.strategy.randomHint": "每个请求按权重比例随机抽取一个可用目标,请求之间不保持粘性。", + "cws.strategy.leastUsedHint": "把每个请求路由到成功次数最少的可用目标。计数随代理重启归零。", + "cws.strategy.resetWindowHint": "优先选择配额窗口最早重置的可用目标。缺少配额数据时回退到配置顺序。", "cws.field.id": "组合 ID", "cws.field.idHint": "客户端将请求 {model}", "cws.field.idInternalHint": "组合的内部 ID,创建后仍可修改。", @@ -2091,6 +2099,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/gui/tests/combo-strategy-roundtrip.test.ts b/gui/tests/combo-strategy-roundtrip.test.ts index 9a28e458309..fcf3d81a53e 100644 --- a/gui/tests/combo-strategy-roundtrip.test.ts +++ b/gui/tests/combo-strategy-roundtrip.test.ts @@ -6,7 +6,7 @@ * combo silently rewrote its strategy (and stripped weights for random). */ import { expect, test } from "bun:test"; -import { parseComboList, toPutBody } from "../src/combo-workspace-data"; +import { groupCombos, parseComboList, toPutBody } from "../src/combo-workspace-data"; const strategies = ["failover", "round-robin", "random", "least-used", "reset-window"] as const; @@ -64,3 +64,15 @@ test("round-robin still sends weights and stickyLimit", () => { expect(body.combo.targets[0]).toEqual({ provider: "openai", model: "gpt-5", weight: 2 }); expect(body.combo.stickyLimit).toBe(3); }); + +test("groupCombos keeps the three newer strategies in their own bucket", () => { + const combos = strategies.map((strategy) => parseComboList(payloadWith(strategy))[0]!); + const sections = groupCombos(combos); + expect(sections.failover.map((c) => c.strategy)).toEqual(["failover"]); + expect(sections.roundRobin.map((c) => c.strategy)).toEqual(["round-robin"]); + expect(sections.other.map((c) => c.strategy)).toEqual([ + "random", + "least-used", + "reset-window", + ]); +});