Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions gui/src/combo-workspace-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand All @@ -20,6 +21,30 @@ export const COMBO_STRATEGIES: readonly ComboStrategy[] = [
"reset-window",
] as const;

export const COMBO_STRATEGY_LABEL_KEYS: Record<ComboStrategy, TKey> = {
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<ComboStrategy, TKey> = {
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<ComboStrategy, TKey> = {
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<string>(COMBO_STRATEGIES);

/**
Expand Down Expand Up @@ -101,6 +126,7 @@ export interface ComboItem {
export interface ComboSections {
failover: ComboItem[];
roundRobin: ComboItem[];
other: ComboItem[];
}

export interface ComboAttentionItem {
Expand Down Expand Up @@ -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[] {
Expand Down
1 change: 1 addition & 0 deletions gui/src/components/ComboWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
<div key={key} className="combos-workspace-rail-group">
Expand Down
13 changes: 3 additions & 10 deletions gui/src/components/combo-workspace-add-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -163,11 +164,7 @@ export function AddComboModal({
onChange={(strategy) => setDraft((d) => ({ ...d, strategy }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover"
? t("cws.strategy.failoverHint")
: draft.strategy === "round-robin"
? t("cws.strategy.roundRobinHint")
: null}
{t(COMBO_STRATEGY_HINT_KEYS[draft.strategy])}
</p>
</div>
<div className="cwi-field">
Expand Down Expand Up @@ -216,11 +213,7 @@ export function AddComboModal({
onChange={(targets) => setDraft((d) => ({ ...d, targets }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover"
? t("cws.targets.failoverHint")
: draft.strategy === "round-robin"
? t("cws.targets.roundRobinHint")
: null}
{t(COMBO_TARGETS_HINT_KEYS[draft.strategy])}
</p>
</div>
<ComboCapabilities
Expand Down
4 changes: 2 additions & 2 deletions gui/src/components/combo-workspace-controls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import type { ComboEffort, ComboStrategy, ComboTarget, ProviderQuotaStates } from "../combo-workspace-data";
import { comboImagesSupported } from "../combo-capabilities";
import { COMBO_EFFORTS, newComboTarget } from "../combo-workspace-data";
import { COMBO_EFFORTS, COMBO_STRATEGY_LABEL_KEYS, newComboTarget } from "../combo-workspace-data";
import { IconArrowDown, IconArrowUp, IconGrip, IconPlus, IconTrash } from "../icons";
import { useT } from "../i18n/shared";
import { Switch } from "../ui";
Expand Down Expand Up @@ -45,7 +45,7 @@ export function StrategySeg({
className="btn btn-sm btn-primary"
disabled
>
{value}
{t(COMBO_STRATEGY_LABEL_KEYS[value])}
</button>
) : null}
</div>
Expand Down
13 changes: 3 additions & 10 deletions gui/src/components/combo-workspace-detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -317,11 +318,7 @@ export function DetailPanel({
onChange={(strategy) => updateDraft((d) => ({ ...d, strategy }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover"
? t("cws.strategy.failoverHint")
: draft.strategy === "round-robin"
? t("cws.strategy.roundRobinHint")
: null}
{t(COMBO_STRATEGY_HINT_KEYS[draft.strategy])}
</p>
</div>
<div className="cwi-field">
Expand Down Expand Up @@ -367,11 +364,7 @@ export function DetailPanel({
onChange={(targets) => updateDraft((d) => ({ ...d, targets }))}
/>
<p className="muted" style={{ fontSize: 12, margin: "8px 0 0" }}>
{draft.strategy === "failover"
? t("cws.targets.failoverHint")
: draft.strategy === "round-robin"
? t("cws.targets.roundRobinHint")
: null}
{t(COMBO_TARGETS_HINT_KEYS[draft.strategy])}
</p>
</div>
<ComboCapabilities
Expand Down
1 change: 1 addition & 0 deletions gui/src/components/combo-workspace-overview-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function OverviewPanel({
<div className="cwi-count-pill"><strong>{combos.length}</strong><span>{t("cws.count.total")}</span></div>
<div className="cwi-count-pill"><strong>{sections.failover.length}</strong><span>{t("cws.count.failover")}</span></div>
<div className="cwi-count-pill"><strong>{sections.roundRobin.length}</strong><span>{t("cws.count.roundRobin")}</span></div>
<div className="cwi-count-pill"><strong>{sections.other.length}</strong><span>{t("cws.count.other")}</span></div>
</div>

<section className="pwi-section" aria-label={t("cws.howTitle")}>
Expand Down
15 changes: 13 additions & 2 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ export const de: Record<TKey, string> = {
"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.",
Expand Down Expand Up @@ -2018,13 +2018,15 @@ export const de: Record<TKey, string> = {
"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/<id>. 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",
Expand All @@ -2044,8 +2046,14 @@ export const de: Record<TKey, string> = {
"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",
Expand All @@ -2071,6 +2079,9 @@ export const de: Record<TKey, string> = {
"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",
Expand Down
15 changes: 13 additions & 2 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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/<id>. 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",
Expand All @@ -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.",
Expand All @@ -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",
Expand Down
Loading
Loading