feat(combos): unify auto-combo into main combos page — LKGP standalone, intelligent routing panel, builder step, i18n consolidation

This commit is contained in:
diegosouzapw
2026-04-12 13:51:03 -03:00
parent 25815ae53d
commit 816db26a75
49 changed files with 1755 additions and 1478 deletions

View File

@@ -15,6 +15,11 @@
- **Observability Module:** Extracted health and telemetry payload construction into `src/lib/monitoring/observability.ts` with `buildHealthPayload()`, `buildTelemetryPayload()`, and `buildSessionsSummary()` builders. The health endpoint now returns session activity, quota monitor status, and per-provider breakdowns alongside existing system metrics
- **Session & Quota Monitor Dashboard:** Added live Session Activity and Quota Monitors panels to the Health dashboard, showing active session counts, sticky-bound sessions, per-API-key breakdowns, and top session details alongside quota monitor alerting/exhausted/error status with per-provider drill-down
- **Combo Health Per-Target Analytics:** The combo-health API now resolves per-target metrics using the new `resolveNestedComboTargets()` function, providing step-level success rates, latency, and historical usage breakdowns per execution key — enabling per-account, per-connection health visibility
- **Auto-Combo → Combos Unification:** Merged the separate `/dashboard/auto-combo` page into the main `/dashboard/combos` page. Auto/LKGP combos are now managed alongside all other combos with a new strategy filter tabs system (All / Intelligent / Deterministic). The old auto-combo route redirects to `/dashboard/combos?filter=intelligent`. Removed the `auto-combo` sidebar entry, consolidating navigation into the single `Combos` item
- **Intelligent Routing Panel (`IntelligentComboPanel`):** New inline panel (371 lines) within the combos page that shows real-time provider scores, 6-factor scoring breakdown (quota, health, cost, latency, task fitness, stability), mode pack selector, incident mode status, and excluded providers for `auto`/`lkgp` combos — replacing the former standalone auto-combo dashboard
- **Builder Intelligent Step (`BuilderIntelligentStep`):** New conditional wizard step (280 lines) that appears in the Builder v2 flow only when `strategy=auto` or `strategy=lkgp` is selected. Exposes candidate pool selection, mode pack presets, router sub-strategy selector, exploration rate slider, budget cap, and collapsible advanced scoring weights configuration
- **Intelligent Routing Module (`intelligentRouting.ts`):** Extracted strategy categorization and filtering logic into a dedicated shared module (210 lines) with `getStrategyCategory()`, `isIntelligentStrategy()`, `filterCombosByStrategyCategory()`, `normalizeIntelligentRoutingFilter()`, and `normalizeIntelligentRoutingConfig()` utility functions
- **LKGP Standalone Strategy:** Implemented `lkgp` (Last Known Good Provider) as a fully functional standalone combo strategy. Previously, `lkgp` as a combo strategy silently fell through to `priority` ordering — the LKGP lookup only ran inside the `auto` engine. Now `strategy: "lkgp"` correctly queries the LKGP state, moves the last successful provider to the top of the target list, and saves the LKGP state after each successful request. Falls back to priority ordering when no LKGP state exists
### ⚡ Performance
@@ -29,6 +34,8 @@
- **Call Logs Schema Expansion:** Added `requested_model`, `request_type`, `tokens_cache_read`, `tokens_cache_creation`, `tokens_reasoning`, `combo_step_id`, and `combo_execution_key` columns to `call_logs` with auto-migration. Added composite index `idx_cl_combo_target` for efficient per-target historical queries
- **Quota Monitor Enrichment:** Expanded `quotaMonitor.ts` with full lifecycle state tracking (`status`, `startedAt`, `lastPolledAt`, `consecutiveFailures`, `totalPolls`, `totalAlerts`), ISO-formatted snapshots via `getQuotaMonitorSnapshots()`, and sorted summary via `getQuotaMonitorSummary()`
- **Codex Quota Fetcher Hardening:** Improved `codexQuotaFetcher.ts` with safer connection registration and quota fetch error handling
- **LKGP Save Refactored to Async/Await:** Replaced fire-and-forget `.then()` chain for LKGP persistence after successful combo routing with proper `async/await` + `try/catch`, preventing unhandled promise rejections and ensuring LKGP state is reliably saved before the response is returned
- **Duplicate `auto` in Combo Strategy Schema:** Removed duplicate `"auto"` entry from `comboStrategySchema` (was listed on both line 104 and 108). Harmless to Zod runtime but cleaned up to avoid confusion. Schema now has exactly 13 unique strategy values
### 🔧 Maintenance & Infrastructure
@@ -36,6 +43,12 @@
- **Combo CRUD Normalization:** `db/combos.ts` now normalizes all stored combo entries through the step normalization pipeline on read, ensuring consistent step IDs and kind annotations regardless of when the combo was created
- **Playwright Config:** Updated Playwright configuration and `run-next-playwright.mjs` script for improved E2E test orchestration
- **Build Script:** Updated `build-next-isolated.mjs` with additional reliability improvements
- **Auto-Combo UI Cleanup:** Deleted `AutoComboModal.tsx` (161 lines), replaced `auto-combo/page.tsx` (478→5 lines) with a server-side redirect to `/dashboard/combos?filter=intelligent`
- **Sidebar Consolidation:** Removed `"auto-combo"` from `HIDEABLE_SIDEBAR_ITEM_IDS` and `PRIMARY_SIDEBAR_ITEMS``normalizeHiddenSidebarItems()` silently discards any stale `"auto-combo"` entries in user settings
- **Schema Cleanup:** Removed obsolete `createAutoComboSchema` from `schemas.ts`. Exported `comboStrategySchema` for direct use in test and filter modules
- **A2A Agent Card Update:** Renamed skill ID from `auto-combo` to `intelligent-routing` with updated description referencing the unified combos dashboard
- **Builder Draft Refactor:** Extended `builderDraft.ts` with dynamic stage list generation via `getComboBuilderStages()` and `isIntelligentBuilderStrategy()`. Stage navigation (`getNextComboBuilderStage`, `getPreviousComboBuilderStage`, `canAccessComboBuilderStage`) now accepts options to conditionally include/skip the `intelligent` wizard step
- **i18n Consolidation:** Removed the standalone `"autoCombo"` i18n block (22 keys) from all 30 language files. Migrated keys into the `"combos"` block with new additions for filter tabs, intelligent panel, and builder step labels
### 🧪 Tests
@@ -56,6 +69,12 @@
- `sse-auth.test.mjs` (154 lines) — P2C credential selection and quota preflight
- `telemetry-summary-route.test.mjs` (35 lines) — Telemetry summary endpoint
- Plus updates to 12 existing test files for compatibility with new step architecture
- **Auto-Combo Unification Tests:**
- `autocombo-unification.test.mjs` (156 lines) — Strategy categorization, schema deduplication, sidebar cleanup, and routing strategies metadata validation
- `combo-unification.spec.ts` (189 lines) — Playwright E2E tests for filter tabs, intelligent panel rendering, redirect from old route, sidebar entry removal, and Builder v2 intelligent step flow
- 3 new LKGP standalone tests in `combo-routing-engine.test.mjs` — Validates LKGP provider prioritization, fallback to priority when no state exists, and LKGP state persistence after successful requests
- Updated `combo-builder-draft.test.mjs` with intelligent stage navigation tests
- Updated `sidebar-visibility.test.mjs` to reflect `auto-combo` removal
---

View File

@@ -1236,6 +1236,29 @@ export async function handleComboChat({
} else {
log.warn("COMBO", "Auto strategy has no candidates, keeping default ordering");
}
} else if (strategy === "lkgp") {
try {
const { getLKGP } = await import("../../src/lib/localDb");
const lkgpProvider = await getLKGP(combo.name, combo.id || combo.name);
if (lkgpProvider) {
const lkgpIndex = orderedTargets.findIndex(
(target) =>
target.provider === lkgpProvider || target.modelStr.startsWith(`${lkgpProvider}/`)
);
if (lkgpIndex > 0) {
const [lkgpTarget] = orderedTargets.splice(lkgpIndex, 1);
orderedTargets.unshift(lkgpTarget);
log.info(
"COMBO",
`[LKGP] Prioritizing last known good provider ${lkgpProvider} for combo "${combo.name}"`
);
}
}
} catch (err) {
log.warn("COMBO", "Failed to retrieve Last Known Good Provider. This is non-fatal.", { err });
}
} else if (strategy === "strict-random") {
const selectedExecutionKey = await getNextFromDeck(
`combo:${combo.name}`,
@@ -1395,18 +1418,17 @@ export async function handleComboChat({
// Record last known good provider (LKGP) for this combo/model (#919)
if (provider) {
import("../../src/lib/localDb")
.then(({ setLKGP }) =>
Promise.all([
setLKGP(combo.name, target.executionKey, provider),
setLKGP(combo.name, combo.id || combo.name, provider),
])
)
.catch((err) =>
log.warn("COMBO", "Failed to record Last Known Good Provider. This is non-fatal.", {
err,
})
);
try {
const { setLKGP } = await import("../../src/lib/localDb");
await Promise.all([
setLKGP(combo.name, target.executionKey, provider),
setLKGP(combo.name, combo.id || combo.name, provider),
]);
} catch (err) {
log.warn("COMBO", "Failed to record Last Known Good Provider. This is non-fatal.", {
err,
});
}
}
return result;

View File

@@ -1,161 +0,0 @@
import { useState, useEffect } from "react";
import { Modal, Input, Button } from "@/shared/components";
import { useTranslations } from "next-intl";
export default function AutoComboModal({ isOpen, onClose, onSave, combo, activeProviders = [] }) {
const t = useTranslations("combos");
const tc = useTranslations("common");
const [formData, setFormData] = useState({
name: "",
strategy: "auto",
candidatePool: [],
explorationRate: 0.05,
modePack: "ship-fast",
budgetCap: "",
});
useEffect(() => {
if (combo) {
// eslint-disable-next-line
setFormData({
name: combo.name || "",
strategy: combo.strategy || "auto",
candidatePool: combo.config?.candidatePool || [],
explorationRate: combo.config?.explorationRate ?? 0.05,
modePack: combo.config?.modePack || "ship-fast",
budgetCap: combo.config?.budgetCap || "",
});
} else {
setFormData({
name: "",
strategy: "auto",
candidatePool: [],
explorationRate: 0.05,
modePack: "ship-fast",
budgetCap: "",
});
}
}, [combo, isOpen]);
const handleSubmit = (e) => {
e.preventDefault();
onSave({
name: formData.name,
strategy: formData.strategy,
config: {
candidatePool: formData.candidatePool,
explorationRate: Number(formData.explorationRate),
modePack: formData.modePack,
budgetCap: formData.budgetCap ? Number(formData.budgetCap) : undefined,
},
});
};
const handleProviderToggle = (providerId) => {
setFormData((prev) => {
const pool = prev.candidatePool.includes(providerId)
? prev.candidatePool.filter((id) => id !== providerId)
: [...prev.candidatePool, providerId];
return { ...prev, candidatePool: pool };
});
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={combo ? "Edit Auto-Combo" : "Create Auto-Combo"}
>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<Input
label="Combo Name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
pattern="^[a-zA-Z0-9_\/\.\-]+$"
disabled={!!combo} // Cannot change name if editing
/>
<div>
<label className="text-sm font-medium mb-1 block">Strategy</label>
<select
className="w-full text-sm rounded-lg border border-border bg-surface px-3 py-2 text-text-main focus:border-primary focus:outline-none"
value={formData.strategy}
onChange={(e) => setFormData({ ...formData, strategy: e.target.value })}
>
<option value="auto">Smart Auto-Routing</option>
<option value="lkgp">Last Known Good Provider (LKGP)</option>
</select>
</div>
<div>
<label className="text-sm font-medium mb-1 block">Candidate Pool</label>
<p className="text-xs text-text-muted mb-2">
Select which providers this engine evaluates.
</p>
<div className="flex flex-wrap gap-2 max-h-40 overflow-y-auto p-2 border border-border rounded-lg">
{activeProviders.map((p) => (
<button
key={p.id}
type="button"
onClick={() => handleProviderToggle(p.id)}
className={`px-2 py-1 text-xs rounded-md border transition-colors ${
formData.candidatePool.includes(p.id)
? "bg-primary border-primary text-white"
: "bg-surface border-border text-text-main"
}`}
>
{p.name || p.id}
</button>
))}
{activeProviders.length === 0 && (
<span className="text-xs text-text-muted">No active APIs found</span>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
label="Exploration Rate"
type="number"
step="0.01"
min="0"
max="1"
value={formData.explorationRate}
onChange={(e) => setFormData({ ...formData, explorationRate: e.target.value })}
/>
<div>
<label className="text-sm font-medium mb-1 block">Mode Pack</label>
<select
className="w-full text-sm rounded-lg border border-border bg-surface px-3 py-2 text-text-main focus:border-primary focus:outline-none"
value={formData.modePack}
onChange={(e) => setFormData({ ...formData, modePack: e.target.value })}
>
<option value="ship-fast">Ship Fast</option>
<option value="cost-saver">Cost Saver</option>
<option value="quality-first">Quality First</option>
<option value="offline-friendly">Offline Friendly</option>
</select>
</div>
</div>
<Input
label="Budget Cap ($ USD / request limit)"
type="number"
step="0.0001"
placeholder="Optional"
value={formData.budgetCap}
onChange={(e) => setFormData({ ...formData, budgetCap: e.target.value })}
/>
<div className="mt-4 flex justify-end gap-2">
<Button type="button" variant="ghost" onClick={onClose}>
{tc("cancel")}
</Button>
<Button type="submit">{tc("save")}</Button>
</div>
</form>
</Modal>
);
}

View File

@@ -1,477 +1,5 @@
/**
* Dashboard Auto-Combo Panel — /dashboard/auto-combo
*
* Shows provider scores, scoring factors, exclusions, mode packs, and routing history.
*/
import { redirect } from "next/navigation";
"use client";
import { useEffect, useState, useCallback } from "react";
import { Card, Button } from "@/shared/components";
import AutoComboModal from "./AutoComboModal";
import { useNotificationStore } from "@/store/notificationStore";
interface ProviderScore {
provider: string;
model: string;
score: number;
factors: Record<string, number>;
}
interface ExclusionEntry {
provider: string;
excludedAt: string;
cooldownMs: number;
reason: string;
}
type AutoComboRecord = {
candidatePool?: unknown;
weights?: unknown;
};
type HealthRecord = {
providerHealth?: Record<string, { state?: string; lastFailure?: string | null }>;
circuitBreakers?: Array<{
provider?: string;
name?: string;
state?: string;
lastFailure?: string | null;
}>;
};
export default function AutoComboDashboard() {
const [scores, setScores] = useState<ProviderScore[]>([]);
const [exclusions, setExclusions] = useState<ExclusionEntry[]>([]);
const [incidentMode, setIncidentMode] = useState(false);
const [modePack, setModePack] = useState("ship-fast");
const notify = useNotificationStore();
const [combos, setCombos] = useState<any[]>([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingCombo, setEditingCombo] = useState<any | null>(null);
const [activeProviders, setActiveProviders] = useState<any[]>([]);
const fetchCombos = useCallback(async () => {
try {
const res = await fetch("/api/combos");
if (res.ok) {
const payload = await res.json();
const allCombos = Array.isArray(payload?.combos) ? payload.combos : [];
const auto = allCombos.filter((c: any) => c.strategy === "auto" || c.strategy === "lkgp");
setCombos(auto);
// Refresh scores based on first auto combo found
const firstCombo = auto[0] || null;
const candidatePool = Array.isArray(firstCombo?.config?.candidatePool)
? firstCombo.config.candidatePool
: [];
const rawWeights =
firstCombo?.weights &&
typeof firstCombo.weights === "object" &&
!Array.isArray(firstCombo.weights)
? (firstCombo.weights as Record<string, unknown>)
: {};
const factors = Object.fromEntries(
Object.entries(rawWeights).map(([k, v]) => [k, typeof v === "number" ? v : 0])
);
const baseScore = candidatePool.length > 0 ? 1 / candidatePool.length : 0;
setScores(
candidatePool.map((provider) => ({
provider,
model: "auto",
score: baseScore,
factors,
}))
);
} else {
setScores([]);
}
} catch {
setScores([]);
}
}, []);
const fetchHealth = useCallback(async () => {
try {
const healthRes = await fetch("/api/monitoring/health");
if (healthRes.ok) {
const health = (await healthRes.json()) as HealthRecord;
const providerHealth =
health?.providerHealth && typeof health.providerHealth === "object"
? health.providerHealth
: {};
const breakersFromProviderHealth = Object.entries(providerHealth).map(
([provider, status]) => ({
provider,
state: status?.state || "CLOSED",
lastFailure: status?.lastFailure || null,
})
);
const breakersFromArray = Array.isArray(health?.circuitBreakers)
? health.circuitBreakers
: [];
const breakers =
breakersFromArray.length > 0
? breakersFromArray.map((breaker) => ({
provider: breaker.provider || breaker.name || "unknown",
state: breaker.state || "CLOSED",
lastFailure: breaker.lastFailure || null,
}))
: breakersFromProviderHealth;
const openBreakers = breakers.filter((breaker) => breaker.state === "OPEN");
setIncidentMode(openBreakers.length / Math.max(breakers.length, 1) > 0.5);
setExclusions(
openBreakers.map((breaker) => ({
provider: breaker.provider,
excludedAt: breaker.lastFailure || new Date().toISOString(),
cooldownMs: 5 * 60 * 1000,
reason: "Circuit breaker OPEN",
}))
);
} else {
setIncidentMode(false);
setExclusions([]);
}
} catch {
/* ignore */
}
}, []);
const fetchData = useCallback(async () => {
await Promise.all([fetchCombos(), fetchHealth()]);
// Fetch active providers for the Modal
try {
const pRes = await fetch("/api/providers");
if (pRes.ok) {
const pData = await pRes.json();
setActiveProviders(
(pData.connections || []).filter(
(c: any) => c.testStatus === "active" || c.testStatus === "success"
)
);
}
} catch {}
}, [fetchCombos, fetchHealth]);
useEffect(() => {
const id = setTimeout(fetchData, 0);
const interval = setInterval(fetchData, 30_000);
return () => {
clearTimeout(id);
clearInterval(interval);
};
}, [fetchData]);
const handleCreate = async (data: any) => {
try {
const res = await fetch("/api/combos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
await fetchCombos();
setShowCreateModal(false);
notify.success("Auto-Combo created successfully");
} else {
const err = await res.json();
notify.error(err.error?.message || err.error || "Failed to create combo");
}
} catch {
notify.error("Error creating combo");
}
};
const handleUpdate = async (id: string, data: any) => {
try {
const res = await fetch(`/api/combos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
await fetchCombos();
setEditingCombo(null);
notify.success("Auto-Combo updated");
} else {
const err = await res.json();
notify.error("Failed to update: " + (err.error?.message || err.error));
}
} catch {
notify.error("Error updating combo");
}
};
const handleDelete = async (id: string) => {
if (!confirm("Are you sure you want to delete this auto-combo?")) return;
try {
const res = await fetch(`/api/combos/${id}`, { method: "DELETE" });
if (res.ok) {
setCombos(combos.filter((c) => c.id !== id));
notify.success("Auto-combo deleted");
}
} catch {
notify.error("Error deleting combo");
}
};
const FACTOR_LABELS: Record<string, string> = {
quota: "📊 Quota",
health: "💚 Health",
costInv: "💰 Cost",
latencyInv: "⚡ Latency",
taskFit: "🎯 Task Fit",
stability: "📈 Stability",
tierPriority: "🏷️ Tier",
};
const MODE_PACKS = [
{ id: "ship-fast", label: "🚀 Ship Fast" },
{ id: "cost-saver", label: "💰 Cost Saver" },
{ id: "quality-first", label: "🎯 Quality First" },
{ id: "offline-friendly", label: "📡 Offline Friendly" },
];
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-semibold"> Auto-Combo Engine</h1>
<p className="text-sm text-text-muted mt-1">
Smart routing automatically adapting to latency, health, and throughput
</p>
</div>
<Button icon="add" onClick={() => setShowCreateModal(true)}>
Create Auto-Combo
</Button>
</div>
{/* ──── CRUD Auto Combos List ──── */}
{combos.length > 0 && (
<Card className="mb-2">
<h2 className="text-lg font-semibold mb-4">Configured Auto-Combos</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{combos.map((combo) => (
<div
key={combo.id}
className="p-4 border rounded-lg bg-surface flex justify-between items-center"
>
<div>
<h3 className="font-semibold text-text-main flex items-center gap-2">
{combo.name}
<span className="text-[10px] uppercase font-bold px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-500">
{combo.strategy}
</span>
</h3>
<p className="text-xs text-text-muted mt-1">
Pool: {combo.config?.candidatePool?.length || "All"} APIs | Pack:{" "}
{combo.config?.modePack || "fast"}
</p>
</div>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={() => setEditingCombo(combo)}>
Edit
</Button>
<Button size="sm" variant="ghost" onClick={() => handleDelete(combo.id)}>
Delete
</Button>
</div>
</div>
))}
</div>
</Card>
)}
{/* Forms */}
{showCreateModal && (
<AutoComboModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onSave={handleCreate}
activeProviders={activeProviders}
combo={null}
/>
)}
{editingCombo && (
<AutoComboModal
isOpen={!!editingCombo}
onClose={() => setEditingCombo(null)}
onSave={(data: any) => handleUpdate(editingCombo.id, data)}
activeProviders={activeProviders}
combo={editingCombo}
/>
)}
<Card>
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
<h2 className="text-lg font-semibold mb-4">Status Overview</h2>
<div className="flex flex-col gap-3">
<div
className={`p-4 rounded-lg border flex items-center justify-between ${
incidentMode
? "bg-red-500/10 border-red-500/30 text-red-700 dark:text-red-300"
: "bg-green-500/10 border-green-500/30 text-green-700 dark:text-green-300"
}`}
>
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[24px]">
{incidentMode ? "warning" : "check_circle"}
</span>
<div>
<h3 className="font-semibold">
{incidentMode ? "Incident Mode" : "Normal Operation"}
</h3>
<p className="text-sm opacity-80">
{incidentMode
? "High circuit breaker trip rate detected"
: "All providers reporting healthy metrics"}
</p>
</div>
</div>
</div>
<div className="p-4 rounded-lg border border-border/50 bg-surface/30 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[24px] text-blue-500">tune</span>
<div>
<h3 className="font-semibold">Active Mode Pack</h3>
<p className="text-sm text-text-muted">
{MODE_PACKS.find((m) => m.id === modePack)?.label || modePack}
</p>
</div>
</div>
</div>
</div>
</div>
<div className="flex-1">
<h2 className="text-lg font-semibold mb-4">Mode Pack</h2>
<div className="grid grid-cols-2 gap-2">
{MODE_PACKS.map((mp) => (
<button
key={mp.id}
onClick={() => setModePack(mp.id)}
className={`flex flex-col items-start p-3 rounded-lg border transition-all ${
modePack === mp.id
? "border-blue-500/50 bg-blue-500/5 ring-1 ring-blue-500/20"
: "border-border/50 hover:border-border hover:bg-surface/30"
}`}
>
<span className={`font-medium ${modePack === mp.id ? "text-blue-500" : ""}`}>
{mp.label}
</span>
</button>
))}
</div>
</div>
</div>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
leaderboard
</span>
</div>
<h3 className="text-lg font-semibold">Provider Scores</h3>
</div>
{scores.length === 0 ? (
<p className="text-sm text-text-muted py-4">
No auto-combo configured... Create one to see live provider scores.
</p>
) : (
<div className="space-y-3">
{scores.map((s) => (
<div
key={s.provider}
className="p-3 bg-surface/30 rounded-lg border border-border/50"
>
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-sm">
{s.provider} / {s.model}
</span>
<span className="font-bold text-lg text-blue-500">
{(s.score * 100).toFixed(0)}%
</span>
</div>
{/* Score Bar */}
<div className="h-1.5 bg-border/50 rounded-full overflow-hidden mb-3">
<div
className="h-full bg-blue-500 rounded-full transition-all duration-1000"
style={{ width: `${s.score * 100}%` }}
/>
</div>
{/* Factor Breakdown */}
<div className="flex flex-wrap gap-2 text-[11px] text-text-muted">
{Object.entries(s.factors || {}).map(([key, val]) => (
<div
key={key}
className="px-2 py-0.5 rounded-full bg-black/5 dark:bg-white/5 border border-border/30"
>
{FACTOR_LABELS[key] || key}:{" "}
<span className="font-medium text-text-main">
{((val as number) * 100).toFixed(0)}%
</span>
</div>
))}
</div>
</div>
))}
</div>
)}
</Card>
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-red-500/10 text-red-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
block
</span>
</div>
<h3 className="text-lg font-semibold">Excluded Providers</h3>
</div>
{exclusions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-text-muted">
<span className="material-symbols-outlined text-[32px] mb-2 text-border">
verified
</span>
<p className="text-sm">No providers currently excluded.</p>
</div>
) : (
<div className="space-y-2">
{exclusions.map((e) => (
<div
key={e.provider}
className="p-3 bg-red-500/5 rounded-lg border border-red-500/20"
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="font-medium text-red-600 dark:text-red-400">
{e.provider}
</span>
</div>
<span className="text-xs px-2 py-0.5 rounded bg-red-500/10 text-red-600 dark:text-red-400 font-medium">
Cooldown: {Math.round(e.cooldownMs / 60000)}m
</span>
</div>
<p className="text-xs text-text-muted mt-1.5 flex items-center gap-1">
<span className="material-symbols-outlined text-[12px]">info</span>
{e.reason}
</p>
</div>
))}
</div>
)}
</Card>
</div>
</div>
);
export default function AutoComboRedirectPage() {
redirect("/dashboard/combos?filter=intelligent");
}

View File

@@ -0,0 +1,280 @@
"use client";
import { useMemo } from "react";
import Card from "@/shared/components/Card";
import {
DEFAULT_INTELLIGENT_WEIGHTS,
FACTOR_LABELS,
MODE_PACK_OPTIONS,
ROUTER_STRATEGY_OPTIONS,
normalizeIntelligentRoutingConfig,
} from "@/lib/combos/intelligentRouting";
function getI18nOrFallback(t: any, key: string, fallback: string) {
if (typeof t?.has === "function" && t.has(key)) return t(key);
return fallback;
}
function toProviderOptions(activeProviders: any[] = []) {
const uniqueProviders = new Map<string, { id: string; label: string; connectionCount: number }>();
activeProviders.forEach((provider) => {
const providerId =
typeof provider?.provider === "string" && provider.provider.trim().length > 0
? provider.provider
: typeof provider?.id === "string" && provider.id.trim().length > 0
? provider.id
: null;
if (!providerId) return;
const currentEntry = uniqueProviders.get(providerId);
const fallbackLabel =
typeof provider?.name === "string" && provider.name.trim().length > 0
? provider.name
: providerId;
uniqueProviders.set(providerId, {
id: providerId,
label: currentEntry?.label || fallbackLabel,
connectionCount: (currentEntry?.connectionCount || 0) + 1,
});
});
return [...uniqueProviders.values()].sort((a, b) => a.label.localeCompare(b.label));
}
export default function BuilderIntelligentStep({
t,
config,
onChange,
activeProviders,
}: {
t: any;
config: Record<string, unknown>;
onChange: (nextConfig: Record<string, unknown>) => void;
activeProviders: any[];
}) {
const normalizedConfig = normalizeIntelligentRoutingConfig(config);
const providerOptions = useMemo(() => toProviderOptions(activeProviders), [activeProviders]);
const updateConfig = (patch: Record<string, unknown>) => {
onChange({
...normalizedConfig,
...patch,
weights: {
...normalizedConfig.weights,
...(patch.weights || {}),
},
});
};
const toggleCandidateProvider = (providerId: string) => {
const nextCandidatePool = normalizedConfig.candidatePool.includes(providerId)
? normalizedConfig.candidatePool.filter((entry) => entry !== providerId)
: [...normalizedConfig.candidatePool, providerId];
updateConfig({ candidatePool: nextCandidatePool });
};
return (
<div className="flex flex-col gap-3">
<Card.Section>
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "builderIntelligentTitle", "Intelligent Routing Configuration")}
</h3>
<p className="text-xs text-text-muted mt-1">
{getI18nOrFallback(
t,
"builderIntelligentDesc",
"Configure the multi-factor scoring engine for this auto-routing combo."
)}
</p>
</div>
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-primary">
<span className="material-symbols-outlined text-[12px]">auto_awesome</span>
Intelligent
</span>
</div>
</Card.Section>
<Card.Section>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-semibold text-text-main">
{getI18nOrFallback(t, "candidatePoolLabel", "Candidate Pool")}
</p>
<p className="text-[11px] text-text-muted mt-1">
{getI18nOrFallback(
t,
"candidatePoolHint",
"Select which providers this engine should evaluate. Leave empty to use all active providers."
)}
</p>
</div>
<span className="text-[10px] text-text-muted">
{normalizedConfig.candidatePool.length > 0
? `${normalizedConfig.candidatePool.length} selected`
: "All active providers"}
</span>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{providerOptions.length === 0 && (
<span className="text-[11px] text-text-muted">
{getI18nOrFallback(t, "candidatePoolEmpty", "No active providers available yet.")}
</span>
)}
{providerOptions.map((provider) => {
const isSelected = normalizedConfig.candidatePool.includes(provider.id);
return (
<button
key={provider.id}
type="button"
onClick={() => toggleCandidateProvider(provider.id)}
className={`rounded-full border px-3 py-1.5 text-xs transition-colors ${
isSelected
? "border-primary bg-primary/10 text-primary"
: "border-black/10 dark:border-white/10 text-text-main hover:border-primary/40 hover:bg-primary/5"
}`}
>
{provider.label}
<span className="ml-1 text-[10px] text-text-muted">
{provider.connectionCount} acct
{provider.connectionCount === 1 ? "" : "s"}
</span>
</button>
);
})}
</div>
</Card.Section>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card.Section>
<label className="text-xs font-semibold text-text-main block mb-2">
{getI18nOrFallback(t, "modePackLabel", "Mode Pack")}
</label>
<select
value={normalizedConfig.modePack}
onChange={(event) => updateConfig({ modePack: event.target.value })}
className="w-full text-xs py-2 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none"
>
{MODE_PACK_OPTIONS.map((modePack) => (
<option key={modePack.id} value={modePack.id}>
{modePack.label}
</option>
))}
</select>
</Card.Section>
<Card.Section>
<label className="text-xs font-semibold text-text-main block mb-2">
{getI18nOrFallback(t, "routerStrategyLabel", "Router Strategy")}
</label>
<select
value={normalizedConfig.routerStrategy}
onChange={(event) => updateConfig({ routerStrategy: event.target.value })}
className="w-full text-xs py-2 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none"
>
{ROUTER_STRATEGY_OPTIONS.map((strategy) => (
<option key={strategy.id} value={strategy.id}>
{strategy.id === "rules"
? getI18nOrFallback(t, "strategyRules", strategy.label)
: strategy.label}
</option>
))}
</select>
</Card.Section>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card.Section>
<label className="text-xs font-semibold text-text-main block">
{getI18nOrFallback(t, "explorationRateLabel", "Exploration Rate")}
</label>
<input
type="range"
min="0"
max="1"
step="0.01"
value={normalizedConfig.explorationRate}
onChange={(event) => updateConfig({ explorationRate: Number(event.target.value || 0) })}
className="mt-3 w-full accent-primary"
/>
<p className="text-[11px] text-text-muted mt-2">
{getI18nOrFallback(
t,
"explorationRateHint",
"{percent}% of requests can explore non-optimal providers."
).replace("{percent}", `${Math.round(normalizedConfig.explorationRate * 100)}`)}
</p>
</Card.Section>
<Card.Section>
<label className="text-xs font-semibold text-text-main block mb-2">
{getI18nOrFallback(t, "budgetCapLabel", "Budget Cap (USD / request)")}
</label>
<input
type="number"
min="0"
step="0.0001"
value={normalizedConfig.budgetCap ?? ""}
placeholder={getI18nOrFallback(t, "budgetCapPlaceholder", "No limit")}
onChange={(event) =>
updateConfig({
budgetCap: event.target.value ? Number(event.target.value) : undefined,
})
}
className="w-full text-xs py-2 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none"
/>
</Card.Section>
</div>
<details className="rounded-lg border border-black/8 dark:border-white/8 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<summary className="cursor-pointer text-xs font-semibold text-text-main">
{getI18nOrFallback(t, "advancedWeightsTitle", "Advanced: Scoring Weights")}
</summary>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
{Object.entries(normalizedConfig.weights).map(([weightKey, weightValue]) => (
<div
key={weightKey}
className="rounded-lg border border-black/6 dark:border-white/6 p-3"
>
<div className="flex items-center justify-between gap-2">
<label className="text-[11px] font-medium text-text-main">
{getI18nOrFallback(
t,
`weight${weightKey[0].toUpperCase()}${weightKey.slice(1)}`,
FACTOR_LABELS[weightKey as keyof typeof DEFAULT_INTELLIGENT_WEIGHTS]
)}
</label>
<span className="text-[11px] text-text-muted">
{Math.round(Number(weightValue) * 100)}%
</span>
</div>
<input
type="range"
min="0"
max="1"
step="0.05"
value={weightValue}
onChange={(event) =>
updateConfig({
weights: {
...normalizedConfig.weights,
[weightKey]: Number(event.target.value || 0),
},
})
}
className="mt-3 w-full accent-primary"
/>
</div>
))}
</div>
</details>
</div>
);
}

View File

@@ -0,0 +1,371 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import Button from "@/shared/components/Button";
import Card from "@/shared/components/Card";
import { useNotificationStore } from "@/store/notificationStore";
import {
FACTOR_LABELS,
MODE_PACK_OPTIONS,
buildIntelligentProviderScores,
extractIntelligentHealthState,
normalizeIntelligentRoutingConfig,
} from "@/lib/combos/intelligentRouting";
function getI18nOrFallback(t: any, key: string, fallback: string) {
if (typeof t?.has === "function" && t.has(key)) return t(key);
return fallback;
}
function formatProviderLabel(providerId: string, activeProviders: any[] = []) {
const matchedConnection = activeProviders.find(
(provider) =>
provider?.provider === providerId ||
provider?.id === providerId ||
provider?.name === providerId
);
if (matchedConnection?.name && matchedConnection.name !== providerId) {
return `${matchedConnection.name} (${providerId})`;
}
return providerId;
}
export default function IntelligentComboPanel({
t,
combo,
allCombos,
activeProviders,
onComboUpdated,
}: {
t: any;
combo: any;
allCombos: any[];
activeProviders: any[];
onComboUpdated?: (combo: any) => void;
}) {
const notify = useNotificationStore();
const [incidentMode, setIncidentMode] = useState(false);
const [exclusions, setExclusions] = useState<any[]>([]);
const [savingModePack, setSavingModePack] = useState<string | null>(null);
const normalizedConfig = useMemo(
() => normalizeIntelligentRoutingConfig(combo?.config),
[combo?.config]
);
const providerScores = useMemo(() => buildIntelligentProviderScores(combo), [combo]);
const fetchHealth = useCallback(async () => {
try {
const response = await fetch("/api/monitoring/health");
if (!response.ok) {
setIncidentMode(false);
setExclusions([]);
return;
}
const health = await response.json();
const nextState = extractIntelligentHealthState(health);
setIncidentMode(nextState.incidentMode);
setExclusions(nextState.exclusions);
} catch {
setIncidentMode(false);
setExclusions([]);
}
}, []);
useEffect(() => {
const timeoutId = setTimeout(fetchHealth, 0);
const intervalId = setInterval(fetchHealth, 30_000);
return () => {
clearTimeout(timeoutId);
clearInterval(intervalId);
};
}, [fetchHealth]);
const handleModePackChange = async (modePackId: string) => {
if (!combo?.id || modePackId === normalizedConfig.modePack) return;
setSavingModePack(modePackId);
try {
const response = await fetch(`/api/combos/${combo.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
config: {
...(combo?.config || {}),
modePack: modePackId,
},
}),
});
if (!response.ok) {
const errorBody = await response.json().catch(() => null);
throw new Error(
errorBody?.error?.message || errorBody?.error || "Failed to update mode pack"
);
}
const updatedCombo = await response.json();
onComboUpdated?.(updatedCombo);
notify.success(
getI18nOrFallback(t, "modePackUpdated", "Mode pack updated to {pack}.").replace(
"{pack}",
modePackId
)
);
} catch (error: any) {
notify.error(error?.message || "Failed to update mode pack.");
} finally {
setSavingModePack(null);
}
};
return (
<Card
className="border-primary/10 bg-gradient-to-br from-primary/[0.04] via-transparent to-transparent"
padding="sm"
>
<div className="flex flex-col gap-4">
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[18px]">
auto_awesome
</span>
<h2 className="text-lg font-semibold text-text-main">
{getI18nOrFallback(t, "intelligentPanelTitle", "Intelligent Routing Dashboard")}
</h2>
</div>
<p className="text-sm text-text-muted mt-1">
{getI18nOrFallback(
t,
"intelligentPanelDesc",
"Real-time scoring and health status for this auto-routing combo."
)}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-text-muted">
<code className="rounded bg-black/5 dark:bg-white/5 px-2 py-1 text-text-main">
{combo?.name}
</code>
<span>{allCombos.length} intelligent combo(s)</span>
<span>
{normalizedConfig.candidatePool.length || activeProviders.length} providers in scope
</span>
</div>
</div>
<div
className={`inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium ${
incidentMode
? "bg-amber-500/15 text-amber-600 dark:text-amber-300"
: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-300"
}`}
>
<span className="material-symbols-outlined text-[14px]">
{incidentMode ? "warning" : "verified"}
</span>
{incidentMode
? getI18nOrFallback(t, "incidentMode", "Incident Mode")
: getI18nOrFallback(t, "normalOperation", "Normal Operation")}
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<Card.Section>
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "statusOverview", "Status Overview")}
</p>
<p className="text-[11px] text-text-muted mt-1">
{incidentMode
? getI18nOrFallback(
t,
"highCircuitBreakerRate",
"High circuit breaker trip rate detected."
)
: getI18nOrFallback(
t,
"allProvidersHealthy",
"Providers are reporting healthy routing conditions."
)}
</p>
</div>
<div className="rounded-lg bg-black/5 dark:bg-white/5 px-3 py-2 text-right">
<p className="text-[10px] uppercase tracking-wide text-text-muted">Exclusions</p>
<p className="text-lg font-semibold text-text-main">{exclusions.length}</p>
</div>
</div>
</Card.Section>
<Card.Section>
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "activeModePack", "Active Mode Pack")}
</p>
<p className="text-[11px] text-text-muted mt-1">
{getI18nOrFallback(
t,
"modePackHint",
"Switch presets to bias the routing engine without rebuilding the combo."
)}
</p>
</div>
{savingModePack && (
<span className="text-[11px] text-text-muted">Saving {savingModePack}</span>
)}
</div>
<div className="grid grid-cols-2 gap-2 mt-3">
{MODE_PACK_OPTIONS.map((modePack) => {
const isActive = normalizedConfig.modePack === modePack.id;
return (
<Button
key={modePack.id}
variant={isActive ? "primary" : "secondary"}
size="sm"
onClick={() => handleModePackChange(modePack.id)}
disabled={Boolean(savingModePack)}
className="!justify-start"
>
<span className="material-symbols-outlined text-[14px]">{modePack.emoji}</span>
{modePack.label}
</Button>
);
})}
</div>
</Card.Section>
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<Card.Section>
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "providerScores", "Provider Scores")}
</p>
<p className="text-[11px] text-text-muted mt-1">
{normalizedConfig.candidatePool.length > 0
? `${normalizedConfig.candidatePool.length} providers currently ranked for this combo.`
: getI18nOrFallback(
t,
"allProvidersEvaluated",
"No candidate pool configured. All active providers are evaluated at runtime."
)}
</p>
</div>
</div>
<div className="mt-3 flex flex-col gap-2">
{providerScores.length === 0 ? (
<div className="rounded-lg border border-dashed border-black/10 dark:border-white/10 p-3 text-[11px] text-text-muted">
{getI18nOrFallback(
t,
"allProvidersEvaluated",
"No candidate pool configured. All active providers are evaluated at runtime."
)}
</div>
) : (
providerScores.map((entry) => {
const percentage = Math.round(entry.score * 100);
return (
<div
key={entry.provider}
className="rounded-lg border border-black/8 dark:border-white/8 bg-white/60 dark:bg-white/[0.03] p-3"
>
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-xs font-semibold text-text-main">
{formatProviderLabel(entry.provider, activeProviders)}
</p>
<p className="text-[11px] text-text-muted mt-0.5">{entry.model}</p>
</div>
<span className="rounded-full bg-primary/10 px-2 py-1 text-[11px] font-semibold text-primary">
{percentage}%
</span>
</div>
<div className="mt-2 h-2 rounded-full bg-black/8 dark:bg-white/8 overflow-hidden">
<div
className="h-full rounded-full bg-blue-500 transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{Object.entries(entry.factors).map(([factorKey, factorValue]) => (
<span
key={`${entry.provider}-${factorKey}`}
className="rounded-full bg-black/5 dark:bg-white/5 px-2 py-1 text-[10px] text-text-muted"
>
{FACTOR_LABELS[factorKey as keyof typeof FACTOR_LABELS]}{" "}
{Math.round(Number(factorValue) * 100)}%
</span>
))}
</div>
</div>
);
})
)}
</div>
</Card.Section>
<Card.Section>
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "excludedProviders", "Excluded Providers")}
</p>
<p className="text-[11px] text-text-muted mt-1">
{getI18nOrFallback(
t,
"excludedProvidersHint",
"Providers with an OPEN circuit breaker are temporarily excluded from routing."
)}
</p>
</div>
</div>
<div className="mt-3 flex flex-col gap-2">
{exclusions.length === 0 ? (
<div className="rounded-lg border border-dashed border-emerald-500/20 bg-emerald-500/5 p-3 text-[11px] text-emerald-700 dark:text-emerald-300">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[14px]">verified</span>
{getI18nOrFallback(
t,
"noExcludedProviders",
"No providers are currently excluded."
)}
</div>
</div>
) : (
exclusions.map((exclusion) => (
<div
key={`${exclusion.provider}-${exclusion.excludedAt}`}
className="rounded-lg border border-amber-500/20 bg-amber-500/10 p-3"
>
<div className="flex items-start justify-between gap-2">
<div>
<p className="text-xs font-semibold text-text-main">{exclusion.provider}</p>
<p className="text-[11px] text-text-muted mt-1">{exclusion.reason}</p>
</div>
<span className="rounded-full bg-amber-500/15 px-2 py-1 text-[10px] font-semibold text-amber-700 dark:text-amber-300">
{getI18nOrFallback(t, "cooldownMinutes", "Cooldown: {minutes}m").replace(
"{minutes}",
`${Math.ceil(exclusion.cooldownMs / 60000)}`
)}
</span>
</div>
</div>
))
)}
</div>
</Card.Section>
</div>
</div>
</Card>
);
}

View File

@@ -1,7 +1,8 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import dynamic from "next/dynamic";
import { useRouter, useSearchParams } from "next/navigation";
import Button from "@/shared/components/Button";
import Card from "@/shared/components/Card";
import { CardSkeleton } from "@/shared/components/Loading";
@@ -20,11 +21,22 @@ import {
canAccessComboBuilderStage,
findNextSuggestedConnectionId,
getComboBuilderStageChecks,
getComboBuilderStages,
getNextComboBuilderStage,
getPreviousComboBuilderStage,
hasExactModelStepDuplicate,
isIntelligentBuilderStrategy,
parseQualifiedModel,
} from "@/lib/combos/builderDraft";
import BuilderIntelligentStep from "./BuilderIntelligentStep";
import IntelligentComboPanel from "./IntelligentComboPanel";
import {
filterCombosByStrategyCategory,
getStrategyCategory,
isIntelligentStrategy,
normalizeIntelligentRoutingFilter,
normalizeIntelligentRoutingConfig,
} from "@/lib/combos/intelligentRouting";
import { useTranslations } from "next-intl";
const ModelSelectModal = dynamic(() => import("@/shared/components/ModelSelectModal"), {
@@ -245,6 +257,12 @@ const COMBO_FORM_STAGE_META = [
fallbackDescription: "Routing behavior and advanced settings.",
icon: "looks_3",
},
{
id: "intelligent",
fallbackLabel: "Intelligent",
fallbackDescription: "Auto-routing candidate pool, presets and scoring.",
icon: "auto_awesome",
},
{
id: "review",
fallbackLabel: "Review",
@@ -512,6 +530,8 @@ function formatComboEntryDisplay(
export default function CombosPage() {
const t = useTranslations("combos");
const tc = useTranslations("common");
const router = useRouter();
const searchParams = useSearchParams();
const [combos, setCombos] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
@@ -530,7 +550,43 @@ export default function CombosPage() {
const [comboDragIndex, setComboDragIndex] = useState(null);
const [comboDragOverIndex, setComboDragOverIndex] = useState(null);
const [savingComboOrder, setSavingComboOrder] = useState(false);
const [selectedIntelligentComboId, setSelectedIntelligentComboId] = useState<string | null>(null);
const comboDragIndexRef = useRef<number | null>(null);
const activeFilter = normalizeIntelligentRoutingFilter(searchParams.get("filter"));
const intelligentCombos = useMemo(
() => combos.filter((combo) => isIntelligentStrategy(combo?.strategy)),
[combos]
);
const filteredCombos = useMemo(
() => filterCombosByStrategyCategory(combos, activeFilter),
[combos, activeFilter]
);
const selectedIntelligentCombo = useMemo(() => {
if (intelligentCombos.length === 0) return null;
const explicitlySelectedCombo =
intelligentCombos.find((combo) => combo.id === selectedIntelligentComboId) || null;
if (explicitlySelectedCombo) {
return explicitlySelectedCombo;
}
return activeFilter === "intelligent" ? intelligentCombos[0] : null;
}, [activeFilter, intelligentCombos, selectedIntelligentComboId]);
useEffect(() => {
if (intelligentCombos.length === 0) {
setSelectedIntelligentComboId(null);
return;
}
if (
selectedIntelligentComboId &&
!intelligentCombos.some((combo) => combo.id === selectedIntelligentComboId)
) {
setSelectedIntelligentComboId(null);
}
}, [intelligentCombos, selectedIntelligentComboId]);
useEffect(() => {
fetchData();
@@ -703,6 +759,25 @@ export default function CombosPage() {
} catch {}
};
const handleFilterChange = (nextFilter) => {
const params = new URLSearchParams(searchParams.toString());
if (nextFilter === "all") {
params.delete("filter");
} else {
params.set("filter", nextFilter);
}
const queryString = params.toString();
router.replace(`/dashboard/combos${queryString ? `?${queryString}` : ""}`, { scroll: false });
};
const handleIntelligentComboUpdated = (updatedCombo) => {
setCombos((previousCombos) =>
previousCombos.map((combo) => (combo.id === updatedCombo?.id ? updatedCombo : combo))
);
};
const resetComboDragState = () => {
comboDragIndexRef.current = null;
setComboDragIndex(null);
@@ -710,7 +785,7 @@ export default function CombosPage() {
};
const handleComboDragStart = (e, index) => {
if (savingComboOrder || combos.length < 2) {
if (savingComboOrder || activeFilter !== "all" || combos.length < 2) {
e.preventDefault();
return;
}
@@ -859,6 +934,62 @@ export default function CombosPage() {
{/* Model Routing Rules (#563) */}
<ModelRoutingSection combos={combos} />
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-black/8 dark:border-white/8 bg-black/[0.02] dark:bg-white/[0.02] p-1">
{[
{
id: "all",
icon: "layers",
label: getI18nOrFallback(t, "filterAll", "All"),
count: combos.length,
},
{
id: "intelligent",
icon: "auto_awesome",
label: getI18nOrFallback(t, "filterIntelligent", "Intelligent"),
count: combos.filter((combo) => getStrategyCategory(combo?.strategy) === "intelligent")
.length,
},
{
id: "deterministic",
icon: "sort",
label: getI18nOrFallback(t, "filterDeterministic", "Deterministic"),
count: combos.filter(
(combo) => getStrategyCategory(combo?.strategy) === "deterministic"
).length,
},
].map((tab) => {
const isActive = activeFilter === tab.id;
return (
<button
key={tab.id}
type="button"
onClick={() => handleFilterChange(tab.id)}
className={`inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-all ${
isActive
? "border border-primary/20 bg-primary/10 text-primary"
: "border border-transparent text-text-muted hover:bg-black/5 dark:hover:bg-white/5 hover:text-text-main"
}`}
>
<span className="material-symbols-outlined text-[16px]">{tab.icon}</span>
<span>{tab.label}</span>
<span className="rounded-full bg-black/5 dark:bg-white/5 px-1.5 py-0.5 text-[11px] text-text-muted">
{tab.count}
</span>
</button>
);
})}
</div>
{activeFilter === "intelligent" && selectedIntelligentCombo && (
<IntelligentComboPanel
t={t}
combo={selectedIntelligentCombo}
allCombos={intelligentCombos}
activeProviders={activeProviders}
onComboUpdated={handleIntelligentComboUpdated}
/>
)}
{/* Combos List */}
{combos.length === 0 ? (
<EmptyState
@@ -868,12 +999,46 @@ export default function CombosPage() {
actionLabel={t("createCombo")}
onAction={() => setShowCreateModal(true)}
/>
) : filteredCombos.length === 0 ? (
<Card padding="sm">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[18px]">filter_alt</span>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "filterEmptyTitle", "No combos match this strategy filter.")}
</p>
</div>
<p className="text-sm text-text-muted">
{activeFilter === "intelligent"
? getI18nOrFallback(
t,
"filterEmptyIntelligentDescription",
"Create an auto or LKGP combo to populate the intelligent routing dashboard."
)
: getI18nOrFallback(
t,
"filterEmptyDeterministicDescription",
"Only auto and LKGP combos exist right now. Switch back to All or create a deterministic combo."
)}
</p>
<div>
<Button size="sm" icon="add" onClick={() => setShowCreateModal(true)}>
{t("createCombo")}
</Button>
</div>
</div>
</Card>
) : (
<div className="flex flex-col gap-4">
{combos.map((combo, index) => (
{filteredCombos.map((combo, index) => (
<div
key={combo.id}
data-testid={`combo-card-${combo.id}`}
onClick={() => {
if (isIntelligentStrategy(combo?.strategy)) {
setSelectedIntelligentComboId(combo.id);
}
}}
onDragOver={(e) => handleComboDragOver(e, index)}
onDrop={(e) => handleComboDrop(e, index)}
>
@@ -891,9 +1056,10 @@ export default function CombosPage() {
onProxy={() => setProxyTargetCombo(combo)}
hasProxy={!!proxyConfig?.combos?.[combo.id]}
onToggle={() => handleToggleCombo(combo)}
dragDisabled={savingComboOrder || combos.length < 2}
dragDisabled={savingComboOrder || activeFilter !== "all" || combos.length < 2}
isDragged={comboDragIndex === index}
isDropTarget={comboDragOverIndex === index && comboDragIndex !== index}
isSelected={selectedIntelligentCombo?.id === combo.id}
onDragStart={(e) => handleComboDragStart(e, index)}
onDragEnd={handleComboDragEnd}
/>
@@ -1206,6 +1372,7 @@ function ComboCard({
dragDisabled,
isDragged,
isDropTarget,
isSelected,
onDragStart,
onDragEnd,
}) {
@@ -1223,7 +1390,7 @@ function ComboCard({
isDisabled ? "opacity-50" : ""
} ${isDropTarget ? "border border-primary/30 bg-primary/5" : ""} ${
isDragged ? "opacity-60" : ""
}`}
} ${isSelected ? "border-primary/30 bg-primary/[0.04]" : ""}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
@@ -1537,6 +1704,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
const [agentContextCache, setAgentContextCache] = useState<boolean>(
!!combo?.context_cache_protection
);
const comboBuilderStages = useMemo(() => getComboBuilderStages({ strategy }), [strategy]);
const visibleStageMeta = useMemo(
() => COMBO_FORM_STAGE_META.filter((stageMeta) => comboBuilderStages.includes(stageMeta.id)),
[comboBuilderStages]
);
const usesIntelligentBuilderStage = isIntelligentBuilderStrategy(strategy);
const intelligentConfig = useMemo(() => normalizeIntelligentRoutingConfig(config), [config]);
const resetFormForCombo = useCallback(
(nextCombo, comboDefaults = null) => {
@@ -1587,6 +1761,12 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
agentContextCache,
]);
useEffect(() => {
if (!comboBuilderStages.includes(builderStage)) {
setBuilderStage("strategy");
}
}, [builderStage, comboBuilderStages]);
// DnD state
const hasPricingForModel = useCallback(
(modelValue) => {
@@ -1607,7 +1787,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
const [dragIndex, setDragIndex] = useState(null);
const [dragOverIndex, setDragOverIndex] = useState(null);
const builderProviders = builderOptions.providers || [];
const builderProviders = useMemo(
() => builderOptions.providers || [],
[builderOptions.providers]
);
const builderComboRefs = (builderOptions.comboRefs || []).filter(
(comboRef) => comboRef.name !== combo?.name && comboRef.name !== name.trim()
);
@@ -1663,8 +1846,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
? builderStageChecks.basics
: builderStage === "steps"
? builderStageChecks.steps
: true;
const currentStageIndex = COMBO_FORM_STAGE_META.findIndex(
: builderStage === "intelligent"
? true
: true;
const currentStageIndex = visibleStageMeta.findIndex(
(stageMeta) => stageMeta.id === builderStage
);
const pinnedAccountCount = models.filter((entry) => Boolean(entry?.connectionId)).length;
@@ -1910,11 +2095,11 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
};
const handleGoToNextStage = () => {
setBuilderStage((currentStage) => getNextComboBuilderStage(currentStage));
setBuilderStage((currentStage) => getNextComboBuilderStage(currentStage, { strategy }));
};
const handleGoToPreviousStage = () => {
setBuilderStage((currentStage) => getPreviousComboBuilderStage(currentStage));
setBuilderStage((currentStage) => getPreviousComboBuilderStage(currentStage, { strategy }));
};
const handleAddBuilderStep = () => {
@@ -2210,16 +2395,16 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
</p>
</div>
<span className="text-[10px] uppercase tracking-wide text-text-muted">
{currentStageIndex + 1}/{COMBO_FORM_STAGE_META.length}
{Math.max(currentStageIndex + 1, 1)}/{visibleStageMeta.length}
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{COMBO_FORM_STAGE_META.map((stageMeta, index) => {
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{visibleStageMeta.map((stageMeta, index) => {
const isActive = builderStage === stageMeta.id;
const canVisitStage = isActive
? true
: canAccessComboBuilderStage(stageMeta.id, builderStageChecks);
: canAccessComboBuilderStage(stageMeta.id, builderStageChecks, { strategy });
const isCompleted =
stageMeta.id === "review"
? false
@@ -2227,7 +2412,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
? builderStageChecks.basics
: stageMeta.id === "steps"
? builderStageChecks.steps
: builderStageChecks.strategy;
: stageMeta.id === "intelligent"
? usesIntelligentBuilderStage
: builderStageChecks.strategy;
return (
<button
@@ -2412,6 +2599,24 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
</div>
)}
{builderStage === "intelligent" && (
<BuilderIntelligentStep
t={t}
config={config}
activeProviders={activeProviders}
onChange={(nextIntelligentConfig) =>
setConfig((previousConfig) => ({
...previousConfig,
...nextIntelligentConfig,
weights: {
...(previousConfig?.weights || {}),
...(nextIntelligentConfig?.weights || {}),
},
}))
}
/>
)}
{/* Models */}
{builderStage === "steps" && (
<div>
@@ -3239,6 +3444,61 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
</div>
</div>
{usesIntelligentBuilderStage && (
<div className="rounded-lg border border-primary/15 bg-primary/[0.04] p-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary text-[16px]">
auto_awesome
</span>
<p className="text-sm font-semibold text-text-main">
{getI18nOrFallback(t, "reviewIntelligentTitle", "Intelligent Routing Config")}
</p>
</div>
<dl className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-3 text-sm">
<div>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
{getI18nOrFallback(t, "modePackLabel", "Mode Pack")}
</dt>
<dd className="text-text-main mt-1">{intelligentConfig.modePack}</dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
{getI18nOrFallback(t, "routerStrategyLabel", "Router Strategy")}
</dt>
<dd className="text-text-main mt-1">{intelligentConfig.routerStrategy}</dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
{getI18nOrFallback(t, "explorationRateLabel", "Exploration Rate")}
</dt>
<dd className="text-text-main mt-1">
{Math.round(intelligentConfig.explorationRate * 100)}%
</dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
{getI18nOrFallback(t, "candidatePoolLabel", "Candidate Pool")}
</dt>
<dd className="text-text-main mt-1">
{intelligentConfig.candidatePool.length > 0
? intelligentConfig.candidatePool.length
: getI18nOrFallback(t, "candidatePoolAllProviders", "All providers")}
</dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
{getI18nOrFallback(t, "budgetCapLabel", "Budget Cap (USD / request)")}
</dt>
<dd className="text-text-main mt-1">
{intelligentConfig.budgetCap
? `$${intelligentConfig.budgetCap}`
: getI18nOrFallback(t, "budgetCapPlaceholder", "No limit")}
</dd>
</div>
</dl>
</div>
)}
<div className="rounded-lg border border-black/8 dark:border-white/8 bg-black/[0.02] dark:bg-white/[0.02] p-3">
<p className="text-xs font-semibold text-text-main">
{getI18nOrFallback(t, "reviewSequence", "Execution sequence")}

View File

@@ -63,17 +63,17 @@ export async function GET() {
],
},
{
id: "auto-combo",
name: "Auto-Managed Model Combos",
id: "intelligent-routing",
name: "Intelligent Model Combos",
description:
"Self-healing model chains that dynamically adapt to provider " +
"health and quota availability. Uses a scoring function based on " +
"quota, health, cost, latency, task fitness, and stability.",
tags: ["combo", "auto", "self-healing", "adaptive"],
"Self-healing model chains with auto and LKGP routing. " +
"Adapts to provider health, quota, latency, and cost using " +
"the unified combos dashboard intelligent routing controls.",
tags: ["combo", "intelligent-routing", "self-healing", "adaptive"],
examples: [
"Create an auto-managed combo for coding tasks",
"Switch to cost-saver mode",
"Show the Auto-Combo scoring breakdown",
"Show the intelligent routing scoring breakdown",
],
},
{

View File

@@ -143,7 +143,6 @@
"dashboard": "لوحة القيادة",
"providers": "المزوّدون",
"combos": "المجموعات",
"autoCombo": "Auto Combo",
"usage": "الاستخدام",
"analytics": "التحليلات",
"costs": "التكاليف",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "دردشة بسيطة",
"streaming": "الجري",

View File

@@ -143,7 +143,6 @@
"dashboard": "Табло",
"providers": "Доставчици",
"combos": "Комбота",
"autoCombo": "Auto Combo",
"usage": "Използване",
"analytics": "Анализ",
"costs": "Разходи",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Обикновен чат",
"streaming": "Поточно предаване",

View File

@@ -143,7 +143,6 @@
"dashboard": "Nástěnka",
"providers": "Poskytovatelé",
"combos": "Komba",
"autoCombo": "Auto Kombo",
"usage": "Použití",
"analytics": "Analytika",
"costs": "Náklady",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Opravte chybu příkaz nenalezen",
"setupGuideCommandMissingDesc": "Ujistěte se, že CLI příkaz existuje v PATH, otevřete novou relaci terminálu a znovu spusťte tlačítko Obnovit."
},
"autoCombo": {
"title": "Auto-Kombo Modul",
"statusNormal": "Normální",
"statusIncident": "Režim Incidentu",
"modePack": "Balíček režimů",
"providerScores": "Skóre poskytovatelů",
"noAutoCombo": "Není nastaveno žádné automatické kombo.",
"excludedProviders": "Vyloučení poskytovatelé",
"noExclusions": "V současné době nejsou vyloučeni žádní poskytovatelé.",
"factorQuota": "Kvóta",
"factorHealth": "Zdraví",
"factorCost": "Náklady",
"factorLatency": "Latence",
"factorTaskFit": "Přizpůsobení úkolu",
"factorStability": "Stabilita",
"factorTierPriority": "Priorita úrovně",
"factorTierPriorityDesc": "Preferuje účty s vyššími kvótami (Ultra/Pro před Free)",
"scoreFactorBreakdown": "Faktory bodování",
"modePackShipFast": "Rychlá doprava",
"modePackCostSaver": "Úspora nákladů",
"modePackQualityFirst": "Kvalita na prvním místě",
"modePackOfflineFriendly": "Offline přátelské"
},
"templateNames": {
"simple-chat": "Jednoduchý chat",
"streaming": "Streamování",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Udbydere",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Brug",
"analytics": "Analytics",
"costs": "Omkostninger",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simpel chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Anbieter",
"combos": "Kombinationen",
"autoCombo": "Auto Combo",
"usage": "Nutzung",
"analytics": "Analytik",
"costs": "Kosten",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Einfacher Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Providers",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Usage",
"analytics": "Analytics",
"costs": "Costs",
@@ -934,6 +933,12 @@
"warningRoundRobinSingleModel": "Round-robin is most useful with at least 2 models.",
"warningCostOptimizedPartialPricing": "Only {priced} of {total} models have pricing. Routing may be partially cost-aware.",
"warningCostOptimizedNoPricing": "No pricing data found for this combo. Cost-optimized may route unexpectedly.",
"filterAll": "All",
"filterIntelligent": "Intelligent",
"filterDeterministic": "Deterministic",
"filterEmptyTitle": "No combos match this strategy filter.",
"filterEmptyIntelligentDescription": "Create an auto or LKGP combo to populate the intelligent routing dashboard.",
"filterEmptyDeterministicDescription": "Only auto and LKGP combos exist right now. Switch back to All or create a deterministic combo.",
"readinessTitle": "Ready to save?",
"readinessDescription": "Review the checklist before creating or updating this combo.",
"readinessCheckName": "Combo name is valid",
@@ -951,6 +956,44 @@
"applyRecommendations": "Apply recommendations",
"recommendationsUpdated": "Recommendations updated for {strategy}.",
"recommendationsApplied": "Recommendations applied to this combo.",
"intelligentPanelTitle": "Intelligent Routing Dashboard",
"intelligentPanelDesc": "Real-time scoring and health status for this auto-routing combo.",
"statusOverview": "Status Overview",
"normalOperation": "Normal Operation",
"allProvidersHealthy": "Providers are reporting healthy routing conditions.",
"incidentMode": "Incident Mode",
"highCircuitBreakerRate": "High circuit breaker trip rate detected.",
"activeModePack": "Active Mode Pack",
"modePackUpdated": "Mode pack updated to {pack}.",
"modePackHint": "Switch presets to bias the routing engine without rebuilding the combo.",
"providerScores": "Provider Scores",
"allProvidersEvaluated": "No candidate pool configured. All active providers are evaluated at runtime.",
"excludedProviders": "Excluded Providers",
"excludedProvidersHint": "Providers with an OPEN circuit breaker are temporarily excluded from routing.",
"noExcludedProviders": "No providers are currently excluded.",
"cooldownMinutes": "Cooldown: {minutes}m",
"builderIntelligentTitle": "Intelligent Routing Configuration",
"builderIntelligentDesc": "Configure the multi-factor scoring engine for this auto-routing combo.",
"candidatePoolLabel": "Candidate Pool",
"candidatePoolHint": "Select which providers this engine should evaluate. Leave empty to use all active providers.",
"candidatePoolEmpty": "No active providers available yet.",
"candidatePoolAllProviders": "All providers",
"modePackLabel": "Mode Pack",
"routerStrategyLabel": "Router Strategy",
"strategyRules": "Rules (6-Factor Scoring)",
"explorationRateLabel": "Exploration Rate",
"explorationRateHint": "{percent}% of requests can explore non-optimal providers.",
"budgetCapLabel": "Budget Cap (USD / request)",
"budgetCapPlaceholder": "No limit",
"advancedWeightsTitle": "Advanced: Scoring Weights",
"weightQuota": "Quota",
"weightHealth": "Health",
"weightCostInv": "Cost",
"weightLatencyInv": "Latency",
"weightTaskFit": "Task Fit",
"weightStability": "Stability",
"weightTierPriority": "Tier",
"reviewIntelligentTitle": "Intelligent Routing Config",
"strategyRecommendations": {
"priority": {
"title": "Fail-safe baseline",
@@ -1004,10 +1047,10 @@
},
"templateFreeStack": "Free Stack ($0)",
"templateFreeStackDesc": "Round-robin across all free providers: Kiro (Claude), Qoder (5 models), Qwen (4 models), Gemini CLI. Zero cost, never stops coding.",
"auto": "Auto Combo",
"autoDesc": "Self-healing smart routing pool (Performance optimized)",
"auto": "Intelligent Auto",
"autoDesc": "Self-healing smart routing pool with multi-factor scoring",
"lkgp": "LKGP Mode",
"lkgpDesc": "Last Known Good Provider (Predictable resilience)"
"lkgpDesc": "Prioritizes the last provider that successfully completed a request"
},
"costs": {
"title": "Costs",
@@ -1779,9 +1822,6 @@
"email": "Email",
"healthCheckMinutes": "Health Check (min)",
"healthCheckHint": "Proactive token refresh interval. 0 = disabled.",
"filterModels": "Filter models...",
"showModel": "Show model",
"hideModel": "Hide model",
"selectAllModels": "Select all",
"deselectAllModels": "Deselect all",
"modelsActiveCount": "{active}/{total} active",
@@ -3046,29 +3086,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Panel de control",
"providers": "Proveedores",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Uso",
"analytics": "Analítica",
"costs": "Costos",
@@ -2965,29 +2964,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Kojelauta",
"providers": "Palveluntarjoajat",
"combos": "Yhdistelmät",
"autoCombo": "Auto Combo",
"usage": "Käyttö",
"analytics": "Analytics",
"costs": "Kustannukset",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Yksinkertainen chat",
"streaming": "Suoratoisto",

View File

@@ -143,7 +143,6 @@
"dashboard": "Tableau de bord",
"providers": "Fournisseurs",
"combos": "Combinaisons",
"autoCombo": "Auto Combo",
"usage": "Utilisation",
"analytics": "Analyse",
"costs": "Coûts",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "לוח מחוונים",
"providers": "ספקים",
"combos": "שילובים",
"autoCombo": "Auto Combo",
"usage": "שימוש",
"analytics": "אנליטיקס",
"costs": "עלויות",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "צ'אט פשוט",
"streaming": "סטרימינג",

View File

@@ -143,7 +143,6 @@
"dashboard": "डैशबोर्ड",
"providers": "प्रदाता",
"combos": "संयोजन",
"autoCombo": "Auto Combo",
"usage": "उपयोग",
"analytics": "विश्लेषिकी",
"costs": "लागत",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Irányítópult",
"providers": "Szolgáltatók",
"combos": "Kombók",
"autoCombo": "Auto Combo",
"usage": "Használat",
"analytics": "Analytics",
"costs": "Költségek",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Egyszerű csevegés",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dasbor",
"providers": "Penyedia",
"combos": "kombo",
"autoCombo": "Auto Combo",
"usage": "Penggunaan",
"analytics": "Analisis",
"costs": "Biaya",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Pannello di controllo",
"providers": "Fornitori",
"combos": "Combinazioni",
"autoCombo": "Auto Combo",
"usage": "Utilizzo",
"analytics": "Analitica",
"costs": "Costi",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "ダッシュボード",
"providers": "プロバイダー",
"combos": "コンボ",
"autoCombo": "Auto Combo",
"usage": "使用法",
"analytics": "分析",
"costs": "コスト",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "대시보드",
"providers": "공급자",
"combos": "콤보",
"autoCombo": "Auto Combo",
"usage": "사용법",
"analytics": "분석",
"costs": "비용",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Papan pemuka",
"providers": "Pembekal",
"combos": "Kombo",
"autoCombo": "Auto Combo",
"usage": "Penggunaan",
"analytics": "Analitis",
"costs": "Kos",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Sembang Mudah",
"streaming": "Penstriman",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Aanbieders",
"combos": "Combo's",
"autoCombo": "Auto Combo",
"usage": "Gebruik",
"analytics": "Analyses",
"costs": "Kosten",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Eenvoudig chatten",
"streaming": "Streamen",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashbord",
"providers": "Leverandører",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Bruk",
"analytics": "Analytics",
"costs": "Kostnader",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Enkel chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Mga provider",
"combos": "Mga combo",
"autoCombo": "Auto Combo",
"usage": "Paggamit",
"analytics": "Analytics",
"costs": "Mga gastos",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simpleng Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Pulpit nawigacyjny",
"providers": "Dostawcy",
"combos": "Kombinacje",
"autoCombo": "Auto Combo",
"usage": "Użycie",
"analytics": "Analityka",
"costs": "Koszty",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Painel",
"providers": "Provedores",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Uso",
"analytics": "Análises",
"costs": "Custos",
@@ -1632,7 +1631,7 @@
"adding": "Adicionando...",
"importingModelsTitle": "Importando Modelos",
"copyModel": "Copiar modelo",
"filterModels": "Filtrar modelos",
"filterModels": "Filtrar modelos...",
"modelsActive": "ativos",
"showModel": "Mostrar modelo",
"hideModel": "Ocultar modelo",
@@ -1770,9 +1769,6 @@
"email": "Email",
"healthCheckMinutes": "Health Check (min)",
"healthCheckHint": "Intervalo proativo de renovação de token. 0 = desativado.",
"filterModels": "Filtrar modelos...",
"showModel": "Mostrar modelo",
"hideModel": "Ocultar modelo",
"selectAllModels": "Selecionar todos",
"deselectAllModels": "Desmarcar todos",
"modelsActiveCount": "{active}/{total} ativos",
@@ -3033,29 +3029,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Bate-papo simples",
"streaming": "Transmissão",

View File

@@ -143,7 +143,6 @@
"dashboard": "Painel",
"providers": "Provedores",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Uso",
"analytics": "Análise",
"costs": "Custos",
@@ -1623,7 +1622,7 @@
"adding": "Adicionando...",
"importingModelsTitle": "Importando Modelos",
"copyModel": "Copiar modelo",
"filterModels": "Filtrar modelos",
"filterModels": "Filtrar modelos...",
"modelsActive": "ativos",
"showModel": "Mostrar modelo",
"hideModel": "Ocultar modelo",
@@ -1761,9 +1760,6 @@
"email": "E-mail",
"healthCheckMinutes": "Verificação de integridade (min)",
"healthCheckHint": "Intervalo de atualização de token proativo. 0 = desabilitado.",
"filterModels": "Filtrar modelos...",
"showModel": "Mostrar modelo",
"hideModel": "Ocultar modelo",
"selectAllModels": "Selecionar todos",
"deselectAllModels": "Desmarcar todos",
"modelsActiveCount": "{active}/{total} ativos",
@@ -3018,29 +3014,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Simple Chat",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Tabloul de bord",
"providers": "Furnizorii",
"combos": "Combo-uri",
"autoCombo": "Auto Combo",
"usage": "Utilizare",
"analytics": "Analytics",
"costs": "Costuri",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Chat simplu",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "Панель управления",
"providers": "Провайдеры",
"combos": "Комбо",
"autoCombo": "Автокомбо",
"usage": "Использование",
"analytics": "Аналитика",
"costs": "Затраты",
@@ -2989,29 +2988,6 @@
"setupGuideCommandMissingTitle": "Исправить 'command not found'",
"setupGuideCommandMissingDesc": "Убедитесь, что команда CLI есть в PATH, откройте новый терминал и снова нажмите «Обновить»."
},
"autoCombo": {
"title": "Движок автокомбо",
"statusNormal": "Нормальный",
"statusIncident": "Режим инцидента",
"modePack": "Пакет режимов",
"providerScores": "Оценки провайдеров",
"noAutoCombo": "Автокомбо не настроено.",
"excludedProviders": "Исключённые провайдеры",
"noExclusions": "Сейчас нет исключённых провайдеров.",
"factorQuota": "Квота",
"factorHealth": "Состояние",
"factorCost": "Стоимость",
"factorLatency": "Задержка",
"factorTaskFit": "Соответствие задаче",
"factorStability": "Стабильность",
"factorTierPriority": "Приоритет тарифа",
"factorTierPriorityDesc": "Предпочитает аккаунты с более высоким тарифом (Ultra/Pro выше Free)",
"scoreFactorBreakdown": "Факторы оценки",
"modePackShipFast": "Быстрое выполнение",
"modePackCostSaver": "Экономный режим",
"modePackQualityFirst": "Сначала качество",
"modePackOfflineFriendly": "Подходит для офлайна"
},
"memory": {
"title": "Память",
"description": "Постоянная кросс-сессионная память для диалогов",

View File

@@ -143,7 +143,6 @@
"dashboard": "Dashboard",
"providers": "Poskytovatelia",
"combos": "kombá",
"autoCombo": "Auto Combo",
"usage": "Použitie",
"analytics": "Analytics",
"costs": "náklady",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Jednoduchý chat",
"streaming": "Streamovanie",

View File

@@ -143,7 +143,6 @@
"dashboard": "Instrumentpanel",
"providers": "Leverantörer",
"combos": "Combos",
"autoCombo": "Auto Combo",
"usage": "Användning",
"analytics": "Analytics",
"costs": "Kostnader",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Enkel chatt",
"streaming": "Streaming",

View File

@@ -143,7 +143,6 @@
"dashboard": "แดชบอร์ด",
"providers": "ผู้ให้บริการ",
"combos": "คอมโบ",
"autoCombo": "Auto Combo",
"usage": "การใช้งาน",
"analytics": "การวิเคราะห์",
"costs": "ค่าใช้จ่าย",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "แชทง่ายๆ",
"streaming": "สตรีมมิ่ง",

View File

@@ -143,7 +143,6 @@
"dashboard": "Kontrol Paneli",
"providers": "Sağlayıcılar",
"combos": "Kombinasyonlar",
"autoCombo": "Otomatik Kombo",
"usage": "Kullanım",
"analytics": "Analizler",
"costs": "Maliyetler",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "'Komut bulunamadı' sorununu düzeltin",
"setupGuideCommandMissingDesc": "PATH'de CLI komutunun mevcut olduğundan emin olun, yeni bir terminal oturumu açın ve Yenile'yi yeniden çalıştırın."
},
"autoCombo": {
"title": "Otomatik Kombo Motoru",
"statusNormal": "Normal Mod",
"statusIncident": "Olay Modu",
"modePack": "Mod Paketi",
"providerScores": "Sağlayıcı Puanları",
"noAutoCombo": "Otomatik kombo yapılandırılmamış.",
"excludedProviders": "Hariç Tutulan Sağlayıcılar",
"noExclusions": "Şu anda hariç tutulan hiçbir sağlayıcı yok.",
"factorQuota": "Kota",
"factorHealth": "Sağlık",
"factorCost": "Maliyet",
"factorLatency": "Gecikme",
"factorTaskFit": "Görev Uyumu",
"factorStability": "Kararlılık",
"factorTierPriority": "Seviye Önceliği",
"factorTierPriorityDesc": "Daha yüksek kota kademelerine sahip hesapları tercih eder (Ücretsiz yerine Ultra/Pro)",
"scoreFactorBreakdown": "Puanlama Faktörleri",
"modePackShipFast": "Hızlı Teslim",
"modePackCostSaver": "Maliyet Tasarrufu",
"modePackQualityFirst": "Kalite Öncelikli",
"modePackOfflineFriendly": "Çevrimdışı Dostu"
},
"templateNames": {
"simple-chat": "Basit Sohbet",
"streaming": "Akış",

View File

@@ -143,7 +143,6 @@
"dashboard": "Приладова панель",
"providers": "Провайдери",
"combos": "Комбо",
"autoCombo": "Auto Combo",
"usage": "Використання",
"analytics": "Аналітика",
"costs": "Витрати",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Простий чат",
"streaming": "Потокове передавання",

View File

@@ -143,7 +143,6 @@
"dashboard": "Trang tổng quan",
"providers": "Nhà cung cấp",
"combos": "Combo",
"autoCombo": "Auto Combo",
"usage": "Cách sử dụng",
"analytics": "Phân tích",
"costs": "Chi phí",
@@ -2960,29 +2959,6 @@
"setupGuideCommandMissingTitle": "Fix 'command not found'",
"setupGuideCommandMissingDesc": "Ensure the CLI command exists in PATH, open a new terminal session, and rerun Refresh."
},
"autoCombo": {
"title": "Auto-Combo Engine",
"statusNormal": "Normal",
"statusIncident": "Incident Mode",
"modePack": "Mode Pack",
"providerScores": "Provider Scores",
"noAutoCombo": "No auto-combo configured.",
"excludedProviders": "Excluded Providers",
"noExclusions": "No providers currently excluded.",
"factorQuota": "Quota",
"factorHealth": "Health",
"factorCost": "Cost",
"factorLatency": "Latency",
"factorTaskFit": "Task Fit",
"factorStability": "Stability",
"factorTierPriority": "Tier Priority",
"factorTierPriorityDesc": "Prefers accounts with higher quota tiers (Ultra/Pro over Free)",
"scoreFactorBreakdown": "Scoring Factors",
"modePackShipFast": "Ship Fast",
"modePackCostSaver": "Cost Saver",
"modePackQualityFirst": "Quality First",
"modePackOfflineFriendly": "Offline Friendly"
},
"templateNames": {
"simple-chat": "Trò chuyện đơn giản",
"streaming": "Truyền phát",

View File

@@ -143,7 +143,6 @@
"dashboard": "仪表板",
"providers": "提供商",
"combos": "组合",
"autoCombo": "自动组合",
"usage": "用量",
"analytics": "分析",
"costs": "成本",
@@ -2986,29 +2985,6 @@
"setupGuideCommandMissingTitle": "修复“command not found”",
"setupGuideCommandMissingDesc": "请确认 CLI 命令已存在于 PATH 中,重新打开一个终端会话后再次点击“刷新”。"
},
"autoCombo": {
"title": "自动组合引擎",
"statusNormal": "正常",
"statusIncident": "事件模式",
"modePack": "模式包",
"providerScores": "提供商得分",
"noAutoCombo": "尚未配置自动组合。",
"excludedProviders": "已排除的提供商",
"noExclusions": "当前没有被排除的提供商。",
"factorQuota": "配额",
"factorHealth": "健康度",
"factorCost": "成本",
"factorLatency": "延迟",
"factorTaskFit": "任务匹配度",
"factorStability": "稳定性",
"factorTierPriority": "套餐优先级",
"factorTierPriorityDesc": "优先选择配额等级更高的账户Ultra/Pro 高于 Free",
"scoreFactorBreakdown": "评分因子",
"modePackShipFast": "快速交付",
"modePackCostSaver": "节省成本",
"modePackQualityFirst": "质量优先",
"modePackOfflineFriendly": "离线友好"
},
"templateNames": {
"simple-chat": "简单对话",
"streaming": "流式响应",

View File

@@ -3,9 +3,30 @@ import type { ComboModelStep } from "@/lib/combos/steps";
type JsonRecord = Record<string, unknown>;
export const COMBO_BUILDER_AUTO_CONNECTION = "__auto__";
export const COMBO_BUILDER_STAGES = ["basics", "steps", "strategy", "review"] as const;
export const COMBO_BUILDER_STAGES = [
"basics",
"steps",
"strategy",
"intelligent",
"review",
] as const;
export type ComboBuilderStage = (typeof COMBO_BUILDER_STAGES)[number];
export type ComboBuilderStageOptions = {
strategy?: string | null;
};
export function isIntelligentBuilderStrategy(strategy: unknown): boolean {
return strategy === "auto" || strategy === "lkgp";
}
export function getComboBuilderStages(options: ComboBuilderStageOptions = {}): ComboBuilderStage[] {
if (isIntelligentBuilderStrategy(options.strategy)) {
return [...COMBO_BUILDER_STAGES];
}
return COMBO_BUILDER_STAGES.filter((stage) => stage !== "intelligent");
}
function isRecord(value: unknown): value is JsonRecord {
return !!value && typeof value === "object" && !Array.isArray(value);
@@ -129,25 +150,37 @@ export function getComboBuilderStageChecks({
export function canAccessComboBuilderStage(
stage: ComboBuilderStage,
checks: ReturnType<typeof getComboBuilderStageChecks>
checks: ReturnType<typeof getComboBuilderStageChecks>,
options: ComboBuilderStageOptions = {}
): boolean {
const availableStages = getComboBuilderStages(options);
if (!availableStages.includes(stage)) return false;
if (stage === "basics") return true;
if (stage === "steps") return checks.basics;
if (stage === "strategy") return checks.basics && checks.steps;
if (stage === "intelligent") return checks.basics && checks.steps && checks.strategy;
if (stage === "review") return checks.basics && checks.steps;
return false;
}
export function getNextComboBuilderStage(stage: ComboBuilderStage): ComboBuilderStage {
const stageIndex = COMBO_BUILDER_STAGES.indexOf(stage);
if (stageIndex === -1 || stageIndex >= COMBO_BUILDER_STAGES.length - 1) {
export function getNextComboBuilderStage(
stage: ComboBuilderStage,
options: ComboBuilderStageOptions = {}
): ComboBuilderStage {
const stages = getComboBuilderStages(options);
const stageIndex = stages.indexOf(stage);
if (stageIndex === -1 || stageIndex >= stages.length - 1) {
return "review";
}
return COMBO_BUILDER_STAGES[stageIndex + 1];
return stages[stageIndex + 1];
}
export function getPreviousComboBuilderStage(stage: ComboBuilderStage): ComboBuilderStage {
const stageIndex = COMBO_BUILDER_STAGES.indexOf(stage);
export function getPreviousComboBuilderStage(
stage: ComboBuilderStage,
options: ComboBuilderStageOptions = {}
): ComboBuilderStage {
const stages = getComboBuilderStages(options);
const stageIndex = stages.indexOf(stage);
if (stageIndex <= 0) return "basics";
return COMBO_BUILDER_STAGES[stageIndex - 1];
return stages[stageIndex - 1];
}

View File

@@ -0,0 +1,210 @@
type JsonRecord = Record<string, unknown>;
export const INTELLIGENT_STRATEGIES = ["auto", "lkgp"] as const;
export const INTELLIGENT_ROUTING_FILTERS = ["all", "intelligent", "deterministic"] as const;
export type IntelligentRoutingFilter = (typeof INTELLIGENT_ROUTING_FILTERS)[number];
export type IntelligentRoutingWeights = {
quota: number;
health: number;
costInv: number;
latencyInv: number;
taskFit: number;
stability: number;
tierPriority: number;
};
export type IntelligentRoutingConfig = {
candidatePool: string[];
explorationRate: number;
modePack: string;
budgetCap?: number;
weights: IntelligentRoutingWeights;
routerStrategy: string;
};
export type IntelligentProviderScore = {
provider: string;
model: string;
score: number;
factors: IntelligentRoutingWeights;
};
export type IntelligentExclusionEntry = {
provider: string;
excludedAt: string;
cooldownMs: number;
reason: string;
};
export const DEFAULT_INTELLIGENT_WEIGHTS: IntelligentRoutingWeights = {
quota: 0.2,
health: 0.25,
costInv: 0.2,
latencyInv: 0.15,
taskFit: 0.1,
stability: 0.05,
tierPriority: 0.05,
};
export const MODE_PACK_OPTIONS = [
{ id: "ship-fast", label: "Ship Fast", emoji: "rocket_launch" },
{ id: "cost-saver", label: "Cost Saver", emoji: "savings" },
{ id: "quality-first", label: "Quality First", emoji: "target" },
{ id: "offline-friendly", label: "Offline Friendly", emoji: "cloud_off" },
] as const;
export const ROUTER_STRATEGY_OPTIONS = [
{ id: "rules", label: "Rules (6-Factor Scoring)" },
{ id: "cost", label: "Cost Optimized" },
{ id: "latency", label: "Latency Optimized" },
{ id: "lkgp", label: "Last Known Good Provider" },
] as const;
export const FACTOR_LABELS: Record<keyof IntelligentRoutingWeights, string> = {
quota: "Quota",
health: "Health",
costInv: "Cost",
latencyInv: "Latency",
taskFit: "Task Fit",
stability: "Stability",
tierPriority: "Tier",
};
function isRecord(value: unknown): value is JsonRecord {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function toFiniteNumber(value: unknown): number | null {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;
}
function toPositiveNumber(value: unknown): number | undefined {
const numericValue = toFiniteNumber(value);
return numericValue !== null && numericValue > 0 ? numericValue : undefined;
}
export function isIntelligentStrategy(strategy: unknown): boolean {
return typeof strategy === "string" && INTELLIGENT_STRATEGIES.includes(strategy as never);
}
export function getStrategyCategory(strategy: unknown): "intelligent" | "deterministic" {
return isIntelligentStrategy(strategy) ? "intelligent" : "deterministic";
}
export function normalizeIntelligentRoutingFilter(value: unknown): IntelligentRoutingFilter {
if (typeof value === "string" && INTELLIGENT_ROUTING_FILTERS.includes(value as never)) {
return value as IntelligentRoutingFilter;
}
return "all";
}
export function filterCombosByStrategyCategory<T extends { strategy?: unknown }>(
combos: T[],
filter: IntelligentRoutingFilter
): T[] {
if (filter === "all") return combos;
return combos.filter((combo) => getStrategyCategory(combo?.strategy) === filter);
}
export function normalizeIntelligentRoutingConfig(config: unknown): IntelligentRoutingConfig {
const configRecord = isRecord(config) ? config : {};
const rawWeights = isRecord(configRecord.weights) ? configRecord.weights : {};
return {
candidatePool: Array.isArray(configRecord.candidatePool)
? configRecord.candidatePool.filter((value): value is string => typeof value === "string")
: [],
explorationRate: Math.min(1, Math.max(0, toFiniteNumber(configRecord.explorationRate) ?? 0.05)),
modePack:
typeof configRecord.modePack === "string" && configRecord.modePack.trim().length > 0
? configRecord.modePack
: "ship-fast",
budgetCap: toPositiveNumber(configRecord.budgetCap),
weights: {
quota: toFiniteNumber(rawWeights.quota) ?? DEFAULT_INTELLIGENT_WEIGHTS.quota,
health: toFiniteNumber(rawWeights.health) ?? DEFAULT_INTELLIGENT_WEIGHTS.health,
costInv: toFiniteNumber(rawWeights.costInv) ?? DEFAULT_INTELLIGENT_WEIGHTS.costInv,
latencyInv: toFiniteNumber(rawWeights.latencyInv) ?? DEFAULT_INTELLIGENT_WEIGHTS.latencyInv,
taskFit: toFiniteNumber(rawWeights.taskFit) ?? DEFAULT_INTELLIGENT_WEIGHTS.taskFit,
stability: toFiniteNumber(rawWeights.stability) ?? DEFAULT_INTELLIGENT_WEIGHTS.stability,
tierPriority:
toFiniteNumber(rawWeights.tierPriority) ?? DEFAULT_INTELLIGENT_WEIGHTS.tierPriority,
},
routerStrategy:
typeof configRecord.routerStrategy === "string" &&
configRecord.routerStrategy.trim().length > 0
? configRecord.routerStrategy
: "rules",
};
}
export function buildIntelligentProviderScores(combo: {
config?: unknown;
weights?: unknown;
}): IntelligentProviderScore[] {
const configRecord = normalizeIntelligentRoutingConfig(combo?.config);
const comboWeights = isRecord(combo?.weights) ? combo.weights : combo?.config;
const weights = normalizeIntelligentRoutingConfig({
...(isRecord(comboWeights) ? comboWeights : {}),
weights: isRecord(combo?.weights) ? combo.weights : configRecord.weights,
}).weights;
const pool = configRecord.candidatePool;
const baseScore = pool.length > 0 ? 1 / pool.length : 0;
return pool.map((provider) => ({
provider,
model: "auto",
score: baseScore,
factors: weights,
}));
}
export function extractIntelligentHealthState(health: unknown): {
incidentMode: boolean;
exclusions: IntelligentExclusionEntry[];
} {
const healthRecord = isRecord(health) ? health : {};
const providerHealth = isRecord(healthRecord.providerHealth) ? healthRecord.providerHealth : {};
const providerBreakers = Object.entries(providerHealth).map(([provider, status]) => {
const statusRecord = isRecord(status) ? status : {};
return {
provider,
state: typeof statusRecord.state === "string" ? statusRecord.state : "CLOSED",
lastFailure: typeof statusRecord.lastFailure === "string" ? statusRecord.lastFailure : null,
};
});
const breakersFromArray = Array.isArray(healthRecord.circuitBreakers)
? healthRecord.circuitBreakers
.map((entry) => {
const breaker = isRecord(entry) ? entry : {};
const provider =
typeof breaker.provider === "string"
? breaker.provider
: typeof breaker.name === "string"
? breaker.name
: "unknown";
return {
provider,
state: typeof breaker.state === "string" ? breaker.state : "CLOSED",
lastFailure: typeof breaker.lastFailure === "string" ? breaker.lastFailure : null,
};
})
.filter((entry) => typeof entry.provider === "string")
: [];
const breakers = breakersFromArray.length > 0 ? breakersFromArray : providerBreakers;
const openBreakers = breakers.filter((breaker) => breaker.state === "OPEN");
return {
incidentMode: openBreakers.length / Math.max(breakers.length, 1) > 0.5,
exclusions: openBreakers.map((breaker) => ({
provider: breaker.provider,
excludedAt: breaker.lastFailure || new Date().toISOString(),
cooldownMs: 5 * 60 * 1000,
reason: "Circuit breaker OPEN",
})),
};
}

View File

@@ -4,7 +4,6 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
"api-manager",
"providers",
"combos",
"auto-combo",
"costs",
"analytics",
"cache",
@@ -52,7 +51,6 @@ const PRIMARY_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [
{ id: "api-manager", href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" },
{ id: "providers", href: "/dashboard/providers", i18nKey: "providers", icon: "dns" },
{ id: "combos", href: "/dashboard/combos", i18nKey: "combos", icon: "layers" },
{ id: "auto-combo", href: "/dashboard/auto-combo", i18nKey: "autoCombo", icon: "auto_awesome" },
{ id: "costs", href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" },
{ id: "analytics", href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" },
{ id: "cache", href: "/dashboard/cache", i18nKey: "cache", icon: "cached" },

View File

@@ -92,7 +92,7 @@ const comboModelEntry = z.union([
comboRefStepInputSchema,
]);
const comboStrategySchema = z.enum([
export const comboStrategySchema = z.enum([
"priority",
"weighted",
"round-robin",
@@ -105,7 +105,6 @@ const comboStrategySchema = z.enum([
"fill-first",
// #729 schema fixes for combo edit/save
"p2c",
"auto",
"lkgp",
"context-optimized",
]);
@@ -181,18 +180,6 @@ export const createComboSchema = z.object({
context_length: z.number().int().min(1000).max(2000000).optional(),
});
// ──── Auto-Combo Schemas ────
export const createAutoComboSchema = z.object({
id: z.string().trim().min(1, "id is required").max(100),
name: z.string().trim().min(1, "name is required").max(200),
candidatePool: z.array(z.string().min(1)).optional().default([]),
weights: scoringWeightsSchema,
modePack: z.string().max(100).optional(),
budgetCap: z.number().positive().optional(),
explorationRate: z.number().min(0).max(1).optional().default(0.05),
});
// ──── Settings Schemas ────
// FASE-01: Removed .passthrough() — only explicitly listed fields are accepted

View File

@@ -0,0 +1,189 @@
import { expect, test } from "@playwright/test";
async function mockCombosPageApis(page: import("@playwright/test").Page) {
await page.route("**/api/combos", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
combos: [
{
id: "combo-auto",
name: "combo-auto",
models: ["openai/gpt-4o-mini"],
strategy: "auto",
config: { candidatePool: ["openai", "anthropic"], modePack: "ship-fast" },
isActive: true,
},
{
id: "combo-priority",
name: "combo-priority",
models: ["anthropic/claude-sonnet-4-6"],
strategy: "priority",
isActive: true,
},
],
}),
});
});
await page.route("**/api/combos/metrics", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ metrics: {} }),
});
});
await page.route("**/api/providers", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
connections: [
{ id: "conn-openai", provider: "openai", name: "OpenAI", testStatus: "active" },
{
id: "conn-anthropic",
provider: "anthropic",
name: "Anthropic",
testStatus: "active",
},
],
}),
});
});
await page.route("**/api/provider-nodes", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ nodes: [] }),
});
});
await page.route("**/api/settings/proxy", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ combos: {} }),
});
});
await page.route("**/api/monitoring/health", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
circuitBreakers: [
{ provider: "openai", state: "CLOSED" },
{ provider: "anthropic", state: "OPEN", lastFailure: new Date().toISOString() },
],
}),
});
});
}
async function mockBuilderApis(page: import("@playwright/test").Page) {
await page.route("**/api/models/alias", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ aliases: {} }),
});
});
await page.route("**/api/pricing", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({}),
});
});
await page.route("**/api/settings/combo-defaults", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ comboDefaults: {} }),
});
});
await page.route("**/api/combos/builder/options", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
providers: [
{
providerId: "openai",
displayName: "OpenAI",
connectionCount: 1,
models: [{ id: "gpt-4o-mini", name: "gpt-4o-mini" }],
connections: [{ id: "conn-openai", label: "OpenAI Main", status: "active" }],
},
],
comboRefs: [],
}),
});
});
}
test.describe("Combo Unification", () => {
test.beforeEach(async ({ page }) => {
await mockCombosPageApis(page);
await mockBuilderApis(page);
});
test("combos page exposes strategy tabs and intelligent panel", async ({ page }) => {
await page.goto("/dashboard/combos?filter=intelligent");
await page.waitForLoadState("networkidle");
test.skip(page.url().includes("/login"), "Authentication enabled without a login fixture.");
await expect(page.getByRole("button", { name: /all/i })).toBeVisible();
await expect(page.getByRole("button", { name: /intelligent/i })).toBeVisible();
await expect(page.getByRole("button", { name: /deterministic/i })).toBeVisible();
await expect(page.getByText("Intelligent Routing Dashboard")).toBeVisible();
await expect(page.getByText("Provider Scores")).toBeVisible();
});
test("legacy auto-combo route redirects to intelligent combos filter", async ({ page }) => {
await page.goto("/dashboard/auto-combo");
await page.waitForURL(/\/dashboard\/combos\?filter=intelligent/);
await expect(page).toHaveURL(/\/dashboard\/combos\?filter=intelligent/);
});
test("sidebar no longer shows auto combo entry", async ({ page }) => {
await page.goto("/dashboard/combos");
await page.waitForLoadState("networkidle");
test.skip(page.url().includes("/login"), "Authentication enabled without a login fixture.");
const sidebar = page.locator("aside, nav").first();
await expect(sidebar.getByText("Combos")).toBeVisible();
await expect(sidebar.getByText("Auto Combo")).toHaveCount(0);
});
test("builder shows intelligent step when auto strategy is selected", async ({ page }) => {
await page.goto("/dashboard/combos");
await page.waitForLoadState("networkidle");
test.skip(page.url().includes("/login"), "Authentication enabled without a login fixture.");
await page.getByRole("button", { name: /create combo/i }).click();
await page.getByLabel(/combo name/i).fill("e2e-auto-builder");
await page.getByTestId("combo-builder-next").click();
await page.getByTestId("combo-builder-provider").selectOption("openai");
await page.getByTestId("combo-builder-model").selectOption("gpt-4o-mini");
await page.getByTestId("combo-builder-add-step").click();
await page.getByTestId("combo-builder-next").click();
await page.getByTestId("strategy-option-auto").click();
await page.getByTestId("combo-builder-next").click();
await expect(page.getByText("Candidate Pool")).toBeVisible();
await expect(page.getByText("Mode Pack")).toBeVisible();
await expect(page.getByText("Exploration Rate")).toBeVisible();
});
});

View File

@@ -0,0 +1,156 @@
import test from "node:test";
import assert from "node:assert/strict";
const intelligentRouting = await import("../../src/lib/combos/intelligentRouting.ts");
test("getStrategyCategory classifies intelligent and deterministic strategies correctly", () => {
assert.equal(intelligentRouting.getStrategyCategory("auto"), "intelligent");
assert.equal(intelligentRouting.getStrategyCategory("lkgp"), "intelligent");
[
"priority",
"weighted",
"round-robin",
"context-relay",
"random",
"least-used",
"cost-optimized",
"strict-random",
"fill-first",
"p2c",
"context-optimized",
].forEach((strategy) => {
assert.equal(intelligentRouting.getStrategyCategory(strategy), "deterministic");
});
});
test("filterCombosByStrategyCategory returns expected combo subsets", () => {
const combos = [
{ id: "1", strategy: "auto" },
{ id: "2", strategy: "priority" },
{ id: "3", strategy: "lkgp" },
];
assert.deepEqual(
intelligentRouting.filterCombosByStrategyCategory(combos, "all").map((combo) => combo.id),
["1", "2", "3"]
);
assert.deepEqual(
intelligentRouting
.filterCombosByStrategyCategory(combos, "intelligent")
.map((combo) => combo.id),
["1", "3"]
);
assert.deepEqual(
intelligentRouting
.filterCombosByStrategyCategory(combos, "deterministic")
.map((combo) => combo.id),
["2"]
);
});
test("combo strategies stay aligned between UI metadata and schema validation", async () => {
const { ROUTING_STRATEGIES } = await import("../../src/shared/constants/routingStrategies.ts");
const { comboStrategySchema, createComboSchema } =
await import("../../src/shared/validation/schemas.ts");
const strategyValues = ROUTING_STRATEGIES.map((strategy) => strategy.value);
assert.equal(strategyValues.length, 13);
assert.equal(new Set(strategyValues).size, 13);
assert.equal(strategyValues.includes("auto"), true);
assert.equal(strategyValues.includes("lkgp"), true);
assert.equal(comboStrategySchema.options.length, 13);
assert.equal(new Set(comboStrategySchema.options).size, 13);
strategyValues.forEach((strategy) => {
const parsed = createComboSchema.safeParse({
name: `combo-${strategy}`,
models: ["openai/gpt-4o-mini"],
strategy,
});
assert.equal(parsed.success, true, `schema should accept strategy ${strategy}`);
});
const invalidParse = createComboSchema.safeParse({
name: "combo-invalid",
models: ["openai/gpt-4o-mini"],
strategy: "not-a-strategy",
});
assert.equal(invalidParse.success, false);
});
test("intelligent combo selection defaults only inside the intelligent filter", () => {
const intelligentCombos = [
{ id: "combo-auto", strategy: "auto" },
{ id: "combo-lkgp", strategy: "lkgp" },
];
const resolveSelectedCombo = ({ activeFilter, selectedIntelligentComboId }) => {
const explicitlySelectedCombo =
intelligentCombos.find((combo) => combo.id === selectedIntelligentComboId) || null;
if (explicitlySelectedCombo) {
return explicitlySelectedCombo;
}
return activeFilter === "intelligent" ? intelligentCombos[0] : null;
};
assert.equal(
resolveSelectedCombo({ activeFilter: "all", selectedIntelligentComboId: null }),
null
);
assert.equal(
resolveSelectedCombo({ activeFilter: "intelligent", selectedIntelligentComboId: null })?.id,
"combo-auto"
);
assert.equal(
resolveSelectedCombo({
activeFilter: "all",
selectedIntelligentComboId: "combo-lkgp",
})?.id,
"combo-lkgp"
);
});
test("sidebar visibility excludes the removed auto-combo item", async () => {
const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts");
const primarySection = sidebarVisibility.SIDEBAR_SECTIONS.find(
(section) => section.id === "primary"
);
assert.equal(sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS.includes("auto-combo"), false);
assert.ok(primarySection);
assert.equal(
primarySection.items.some((item) => item.id === "auto-combo"),
false
);
assert.deepEqual(sidebarVisibility.normalizeHiddenSidebarItems(["auto-combo", "home"]), ["home"]);
});
test("intelligent routing helpers normalize config and health state", () => {
const normalizedConfig = intelligentRouting.normalizeIntelligentRoutingConfig({
candidatePool: ["openai", "anthropic"],
explorationRate: "0.25",
weights: { quota: 0.4 },
});
assert.deepEqual(normalizedConfig.candidatePool, ["openai", "anthropic"]);
assert.equal(normalizedConfig.explorationRate, 0.25);
assert.equal(normalizedConfig.weights.quota, 0.4);
assert.equal(
normalizedConfig.weights.health,
intelligentRouting.DEFAULT_INTELLIGENT_WEIGHTS.health
);
const healthState = intelligentRouting.extractIntelligentHealthState({
circuitBreakers: [
{ provider: "openai", state: "OPEN", lastFailure: "2026-04-12T12:00:00Z" },
{ provider: "anthropic", state: "OPEN", lastFailure: "2026-04-12T12:01:00Z" },
{ provider: "google", state: "CLOSED" },
],
});
assert.equal(healthState.incidentMode, true);
assert.equal(healthState.exclusions.length, 2);
});

View File

@@ -158,6 +158,27 @@ test("combo builder stage helpers expose completion state and linear navigation"
assert.equal(builderDraft.getNextComboBuilderStage("review"), "review");
assert.equal(builderDraft.getPreviousComboBuilderStage("review"), "strategy");
assert.equal(builderDraft.getPreviousComboBuilderStage("basics"), "basics");
assert.deepEqual(builderDraft.getComboBuilderStages({ strategy: "priority" }), [
"basics",
"steps",
"strategy",
"review",
]);
assert.deepEqual(builderDraft.getComboBuilderStages({ strategy: "auto" }), [
"basics",
"steps",
"strategy",
"intelligent",
"review",
]);
assert.equal(
builderDraft.getNextComboBuilderStage("strategy", { strategy: "auto" }),
"intelligent"
);
assert.equal(
builderDraft.getPreviousComboBuilderStage("review", { strategy: "auto" }),
"intelligent"
);
const checks = builderDraft.getComboBuilderStageChecks({
name: "codex-stack",
@@ -171,6 +192,10 @@ test("combo builder stage helpers expose completion state and linear navigation"
assert.equal(builderDraft.canAccessComboBuilderStage("steps", checks), true);
assert.equal(builderDraft.canAccessComboBuilderStage("strategy", checks), true);
assert.equal(builderDraft.canAccessComboBuilderStage("review", checks), true);
assert.equal(
builderDraft.canAccessComboBuilderStage("intelligent", checks, { strategy: "auto" }),
false
);
const lockedChecks = builderDraft.getComboBuilderStageChecks({
name: "",
@@ -184,3 +209,21 @@ test("combo builder stage helpers expose completion state and linear navigation"
assert.equal(builderDraft.canAccessComboBuilderStage("strategy", lockedChecks), false);
assert.equal(builderDraft.canAccessComboBuilderStage("review", lockedChecks), false);
});
test("intelligent builder stage is accessible only after strategy checks pass", () => {
const readyChecks = builderDraft.getComboBuilderStageChecks({
name: "auto-stack",
nameError: "",
modelsCount: 2,
hasInvalidWeightedTotal: false,
hasCostOptimizedWithoutPricing: false,
});
assert.equal(
builderDraft.canAccessComboBuilderStage("intelligent", readyChecks, { strategy: "auto" }),
true
);
assert.equal(builderDraft.isIntelligentBuilderStrategy("auto"), true);
assert.equal(builderDraft.isIntelligentBuilderStrategy("lkgp"), true);
assert.equal(builderDraft.isIntelligentBuilderStrategy("priority"), false);
});

View File

@@ -93,6 +93,7 @@ async function resetStorage() {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.resetAllPricing();
settingsDb.clearAllLKGP();
clearModelsDevCapabilities();
}
@@ -1278,6 +1279,81 @@ test("handleComboChat auto strategy honors LKGP after filtering to tool-capable
assert.equal(calls[0], "claude/claude-sonnet-4-6");
});
test("handleComboChat standalone lkgp strategy prioritizes the last known good provider", async () => {
await settingsDb.setLKGP("standalone-lkgp", "standalone-lkgp", "anthropic");
const calls = [];
const result = await handleComboChat({
body: {},
combo: {
id: "standalone-lkgp",
name: "standalone-lkgp",
strategy: "lkgp",
models: ["openai/gpt-4o-mini", "anthropic/claude-sonnet-4-6"],
},
handleSingleModel: async (_body, modelStr) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log: createLog(),
settings: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.equal(calls[0], "anthropic/claude-sonnet-4-6");
});
test("handleComboChat standalone lkgp strategy falls back to original order when no state exists", async () => {
const calls = [];
const result = await handleComboChat({
body: {},
combo: {
id: "standalone-lkgp-no-state",
name: "standalone-lkgp-no-state",
strategy: "lkgp",
models: ["openai/gpt-4o-mini", "anthropic/claude-sonnet-4-6"],
},
handleSingleModel: async (_body, modelStr) => {
calls.push(modelStr);
return okResponse();
},
isModelAvailable: async () => true,
log: createLog(),
settings: null,
allCombos: null,
});
assert.equal(result.ok, true);
assert.equal(calls[0], "openai/gpt-4o-mini");
});
test("handleComboChat standalone lkgp strategy updates LKGP after a successful call", async () => {
const result = await handleComboChat({
body: {},
combo: {
id: "standalone-lkgp-save",
name: "standalone-lkgp-save",
strategy: "lkgp",
models: ["openai/gpt-4o-mini"],
},
handleSingleModel: async () => okResponse(),
isModelAvailable: async () => true,
log: createLog(),
settings: null,
allCombos: null,
});
const persistedProvider = await settingsDb.getLKGP(
"standalone-lkgp-save",
"standalone-lkgp-save"
);
assert.equal(result.ok, true);
assert.equal(persistedProvider, "openai");
});
test("handleComboChat auto strategy falls back to the full pool when tool filtering empties candidates", async () => {
await settingsDb.updatePricing({
openai: {

View File

@@ -29,7 +29,6 @@ test("primary sidebar items place limits after cache", () => {
"api-manager",
"providers",
"combos",
"auto-combo",
"costs",
"analytics",
"cache",