diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f70fae9d2..e88e5e55c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 --- diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 02ea42418f..83a60f567b 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -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; diff --git a/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx b/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx deleted file mode 100644 index b080f567b8..0000000000 --- a/src/app/(dashboard)/dashboard/auto-combo/AutoComboModal.tsx +++ /dev/null @@ -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 ( - -
- setFormData({ ...formData, name: e.target.value })} - required - pattern="^[a-zA-Z0-9_\/\.\-]+$" - disabled={!!combo} // Cannot change name if editing - /> - -
- - -
- -
- -

- Select which providers this engine evaluates. -

-
- {activeProviders.map((p) => ( - - ))} - {activeProviders.length === 0 && ( - No active APIs found - )} -
-
- -
- setFormData({ ...formData, explorationRate: e.target.value })} - /> -
- - -
-
- - setFormData({ ...formData, budgetCap: e.target.value })} - /> - -
- - -
-
-
- ); -} diff --git a/src/app/(dashboard)/dashboard/auto-combo/page.tsx b/src/app/(dashboard)/dashboard/auto-combo/page.tsx index bf2cc1928d..50ee42302d 100644 --- a/src/app/(dashboard)/dashboard/auto-combo/page.tsx +++ b/src/app/(dashboard)/dashboard/auto-combo/page.tsx @@ -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; -} - -interface ExclusionEntry { - provider: string; - excludedAt: string; - cooldownMs: number; - reason: string; -} - -type AutoComboRecord = { - candidatePool?: unknown; - weights?: unknown; -}; - -type HealthRecord = { - providerHealth?: Record; - circuitBreakers?: Array<{ - provider?: string; - name?: string; - state?: string; - lastFailure?: string | null; - }>; -}; - -export default function AutoComboDashboard() { - const [scores, setScores] = useState([]); - const [exclusions, setExclusions] = useState([]); - const [incidentMode, setIncidentMode] = useState(false); - const [modePack, setModePack] = useState("ship-fast"); - - const notify = useNotificationStore(); - const [combos, setCombos] = useState([]); - const [showCreateModal, setShowCreateModal] = useState(false); - const [editingCombo, setEditingCombo] = useState(null); - const [activeProviders, setActiveProviders] = useState([]); - - 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) - : {}; - 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 = { - 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 ( -
-
-
-

⚡ Auto-Combo Engine

-

- Smart routing automatically adapting to latency, health, and throughput -

-
- -
- - {/* ──── CRUD Auto Combos List ──── */} - {combos.length > 0 && ( - -

Configured Auto-Combos

-
- {combos.map((combo) => ( -
-
-

- {combo.name} - - {combo.strategy} - -

-

- Pool: {combo.config?.candidatePool?.length || "All"} APIs | Pack:{" "} - {combo.config?.modePack || "fast"} -

-
-
- - -
-
- ))} -
-
- )} - - {/* Forms */} - {showCreateModal && ( - setShowCreateModal(false)} - onSave={handleCreate} - activeProviders={activeProviders} - combo={null} - /> - )} - {editingCombo && ( - setEditingCombo(null)} - onSave={(data: any) => handleUpdate(editingCombo.id, data)} - activeProviders={activeProviders} - combo={editingCombo} - /> - )} - - -
-
-

Status Overview

-
-
-
- - {incidentMode ? "warning" : "check_circle"} - -
-

- {incidentMode ? "Incident Mode" : "Normal Operation"} -

-

- {incidentMode - ? "High circuit breaker trip rate detected" - : "All providers reporting healthy metrics"} -

-
-
-
- -
-
- tune -
-

Active Mode Pack

-

- {MODE_PACKS.find((m) => m.id === modePack)?.label || modePack} -

-
-
-
-
-
- -
-

Mode Pack

-
- {MODE_PACKS.map((mp) => ( - - ))} -
-
-
-
- -
- -
-
- -
-

Provider Scores

-
- - {scores.length === 0 ? ( -

- No auto-combo configured... Create one to see live provider scores. -

- ) : ( -
- {scores.map((s) => ( -
-
- - {s.provider} / {s.model} - - - {(s.score * 100).toFixed(0)}% - -
- {/* Score Bar */} -
-
-
- {/* Factor Breakdown */} -
- {Object.entries(s.factors || {}).map(([key, val]) => ( -
- {FACTOR_LABELS[key] || key}:{" "} - - {((val as number) * 100).toFixed(0)}% - -
- ))} -
-
- ))} -
- )} - - - -
-
- -
-

Excluded Providers

-
- - {exclusions.length === 0 ? ( -
- - verified - -

No providers currently excluded.

-
- ) : ( -
- {exclusions.map((e) => ( -
-
-
- - {e.provider} - -
- - Cooldown: {Math.round(e.cooldownMs / 60000)}m - -
-

- info - {e.reason} -

-
- ))} -
- )} -
-
-
- ); +export default function AutoComboRedirectPage() { + redirect("/dashboard/combos?filter=intelligent"); } diff --git a/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx b/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx new file mode 100644 index 0000000000..a0e58bc0d2 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/BuilderIntelligentStep.tsx @@ -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(); + + 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; + onChange: (nextConfig: Record) => void; + activeProviders: any[]; +}) { + const normalizedConfig = normalizeIntelligentRoutingConfig(config); + const providerOptions = useMemo(() => toProviderOptions(activeProviders), [activeProviders]); + + const updateConfig = (patch: Record) => { + 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 ( +
+ +
+
+

+ {getI18nOrFallback(t, "builderIntelligentTitle", "Intelligent Routing Configuration")} +

+

+ {getI18nOrFallback( + t, + "builderIntelligentDesc", + "Configure the multi-factor scoring engine for this auto-routing combo." + )} +

+
+ + auto_awesome + Intelligent + +
+
+ + +
+
+

+ {getI18nOrFallback(t, "candidatePoolLabel", "Candidate Pool")} +

+

+ {getI18nOrFallback( + t, + "candidatePoolHint", + "Select which providers this engine should evaluate. Leave empty to use all active providers." + )} +

+
+ + {normalizedConfig.candidatePool.length > 0 + ? `${normalizedConfig.candidatePool.length} selected` + : "All active providers"} + +
+ +
+ {providerOptions.length === 0 && ( + + {getI18nOrFallback(t, "candidatePoolEmpty", "No active providers available yet.")} + + )} + + {providerOptions.map((provider) => { + const isSelected = normalizedConfig.candidatePool.includes(provider.id); + return ( + + ); + })} +
+
+ +
+ + + + + + + + + +
+ +
+ + + updateConfig({ explorationRate: Number(event.target.value || 0) })} + className="mt-3 w-full accent-primary" + /> +

+ {getI18nOrFallback( + t, + "explorationRateHint", + "{percent}% of requests can explore non-optimal providers." + ).replace("{percent}", `${Math.round(normalizedConfig.explorationRate * 100)}`)} +

+
+ + + + + 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" + /> + +
+ +
+ + {getI18nOrFallback(t, "advancedWeightsTitle", "Advanced: Scoring Weights")} + +
+ {Object.entries(normalizedConfig.weights).map(([weightKey, weightValue]) => ( +
+
+ + + {Math.round(Number(weightValue) * 100)}% + +
+ + updateConfig({ + weights: { + ...normalizedConfig.weights, + [weightKey]: Number(event.target.value || 0), + }, + }) + } + className="mt-3 w-full accent-primary" + /> +
+ ))} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx new file mode 100644 index 0000000000..0a22d20e07 --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/IntelligentComboPanel.tsx @@ -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([]); + const [savingModePack, setSavingModePack] = useState(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 ( + +
+
+
+
+ + auto_awesome + +

+ {getI18nOrFallback(t, "intelligentPanelTitle", "Intelligent Routing Dashboard")} +

+
+

+ {getI18nOrFallback( + t, + "intelligentPanelDesc", + "Real-time scoring and health status for this auto-routing combo." + )} +

+
+ + {combo?.name} + + {allCombos.length} intelligent combo(s) + + {normalizedConfig.candidatePool.length || activeProviders.length} providers in scope + +
+
+ +
+ + {incidentMode ? "warning" : "verified"} + + {incidentMode + ? getI18nOrFallback(t, "incidentMode", "Incident Mode") + : getI18nOrFallback(t, "normalOperation", "Normal Operation")} +
+
+ +
+ +
+
+

+ {getI18nOrFallback(t, "statusOverview", "Status Overview")} +

+

+ {incidentMode + ? getI18nOrFallback( + t, + "highCircuitBreakerRate", + "High circuit breaker trip rate detected." + ) + : getI18nOrFallback( + t, + "allProvidersHealthy", + "Providers are reporting healthy routing conditions." + )} +

+
+
+

Exclusions

+

{exclusions.length}

+
+
+
+ + +
+
+

+ {getI18nOrFallback(t, "activeModePack", "Active Mode Pack")} +

+

+ {getI18nOrFallback( + t, + "modePackHint", + "Switch presets to bias the routing engine without rebuilding the combo." + )} +

+
+ {savingModePack && ( + Saving {savingModePack}… + )} +
+ +
+ {MODE_PACK_OPTIONS.map((modePack) => { + const isActive = normalizedConfig.modePack === modePack.id; + return ( + + ); + })} +
+
+
+ +
+ +
+
+

+ {getI18nOrFallback(t, "providerScores", "Provider Scores")} +

+

+ {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." + )} +

+
+
+ +
+ {providerScores.length === 0 ? ( +
+ {getI18nOrFallback( + t, + "allProvidersEvaluated", + "No candidate pool configured. All active providers are evaluated at runtime." + )} +
+ ) : ( + providerScores.map((entry) => { + const percentage = Math.round(entry.score * 100); + return ( +
+
+
+

+ {formatProviderLabel(entry.provider, activeProviders)} +

+

{entry.model}

+
+ + {percentage}% + +
+ +
+
+
+ +
+ {Object.entries(entry.factors).map(([factorKey, factorValue]) => ( + + {FACTOR_LABELS[factorKey as keyof typeof FACTOR_LABELS]}{" "} + {Math.round(Number(factorValue) * 100)}% + + ))} +
+
+ ); + }) + )} +
+ + + +
+
+

+ {getI18nOrFallback(t, "excludedProviders", "Excluded Providers")} +

+

+ {getI18nOrFallback( + t, + "excludedProvidersHint", + "Providers with an OPEN circuit breaker are temporarily excluded from routing." + )} +

+
+
+ +
+ {exclusions.length === 0 ? ( +
+
+ verified + {getI18nOrFallback( + t, + "noExcludedProviders", + "No providers are currently excluded." + )} +
+
+ ) : ( + exclusions.map((exclusion) => ( +
+
+
+

{exclusion.provider}

+

{exclusion.reason}

+
+ + {getI18nOrFallback(t, "cooldownMinutes", "Cooldown: {minutes}m").replace( + "{minutes}", + `${Math.ceil(exclusion.cooldownMs / 60000)}` + )} + +
+
+ )) + )} +
+
+
+
+ + ); +} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 34a529128d..a988deaa6a 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -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(null); const comboDragIndexRef = useRef(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) */} +
+ {[ + { + 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 ( + + ); + })} +
+ + {activeFilter === "intelligent" && selectedIntelligentCombo && ( + + )} + {/* Combos List */} {combos.length === 0 ? ( setShowCreateModal(true)} /> + ) : filteredCombos.length === 0 ? ( + +
+
+ filter_alt +

+ {getI18nOrFallback(t, "filterEmptyTitle", "No combos match this strategy filter.")} +

+
+

+ {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." + )} +

+
+ +
+
+
) : (
- {combos.map((combo, index) => ( + {filteredCombos.map((combo, index) => (
{ + 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]" : ""}`} >
@@ -1537,6 +1704,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const [agentContextCache, setAgentContextCache] = useState( !!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 }) {

- {currentStageIndex + 1}/{COMBO_FORM_STAGE_META.length} + {Math.max(currentStageIndex + 1, 1)}/{visibleStageMeta.length}
-
- {COMBO_FORM_STAGE_META.map((stageMeta, index) => { +
+ {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 (
+ {usesIntelligentBuilderStage && ( +
+
+ + auto_awesome + +

+ {getI18nOrFallback(t, "reviewIntelligentTitle", "Intelligent Routing Config")} +

+
+
+
+
+ {getI18nOrFallback(t, "modePackLabel", "Mode Pack")} +
+
{intelligentConfig.modePack}
+
+
+
+ {getI18nOrFallback(t, "routerStrategyLabel", "Router Strategy")} +
+
{intelligentConfig.routerStrategy}
+
+
+
+ {getI18nOrFallback(t, "explorationRateLabel", "Exploration Rate")} +
+
+ {Math.round(intelligentConfig.explorationRate * 100)}% +
+
+
+
+ {getI18nOrFallback(t, "candidatePoolLabel", "Candidate Pool")} +
+
+ {intelligentConfig.candidatePool.length > 0 + ? intelligentConfig.candidatePool.length + : getI18nOrFallback(t, "candidatePoolAllProviders", "All providers")} +
+
+
+
+ {getI18nOrFallback(t, "budgetCapLabel", "Budget Cap (USD / request)")} +
+
+ {intelligentConfig.budgetCap + ? `$${intelligentConfig.budgetCap}` + : getI18nOrFallback(t, "budgetCapPlaceholder", "No limit")} +
+
+
+
+ )} +

{getI18nOrFallback(t, "reviewSequence", "Execution sequence")} diff --git a/src/app/.well-known/agent.json/route.ts b/src/app/.well-known/agent.json/route.ts index 9ae88e92aa..0f9efbd8e0 100644 --- a/src/app/.well-known/agent.json/route.ts +++ b/src/app/.well-known/agent.json/route.ts @@ -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", ], }, { diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index ef7b74a145..3472f48804 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -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": "الجري", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index e81066337c..dd49e0b5d2 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -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": "Поточно предаване", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 54579d0e02..db9b005624 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -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í", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 89a14a96f3..74621601c1 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -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", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 91f9eeee4a..5a84fe4850 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -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", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index a24aa062dc..fdca460b08 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 3c33281c26..279c2271bd 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -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", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 1e2a05f485..fc0972a70f 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -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", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 00df82ee65..f18ad6e00a 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -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", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 16d0fc45f8..e2b9af068f 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -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": "סטרימינג", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index d712a51067..23e1a71e02 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -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", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 8935cfcd80..4495e05c18 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -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", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 9071dffc4e..31ed9455a6 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -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", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index c90ceee177..51e9f04854 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -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", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 40396a8126..3aeac027c7 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -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", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 7d13df25f7..a6d52a34a8 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -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", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index c4aec0b901..06e64f696e 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -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", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index e36a970499..48e8596b7b 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -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", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 7f6f889519..c49ab844d0 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -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", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index c181a802fd..90fcf91ea4 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -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", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 35ca704e86..39d72ece2c 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -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", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index dbe4b869be..de02bbf2a1 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -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", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index b482c165c1..ba64dcbe49 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -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", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 7e22cc8fb3..92fc3ebad7 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -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", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 7629aaabeb..e4eacef3d6 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -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": "Постоянная кросс-сессионная память для диалогов", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index bca85adac0..d764a2dcf9 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -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", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 42882a048d..68cfcd7381 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -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", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 4967617b01..d2cefd55c1 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -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": "สตรีมมิ่ง", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 9e546f1b76..b5869178ee 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -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ış", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 2762ceed55..0e5a01ea65 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -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": "Потокове передавання", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index d6253d21af..5cadd9e835 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -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", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 399ec51392..d679824d4d 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -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": "流式响应", diff --git a/src/lib/combos/builderDraft.ts b/src/lib/combos/builderDraft.ts index 985a5f8e80..c5ac13c407 100644 --- a/src/lib/combos/builderDraft.ts +++ b/src/lib/combos/builderDraft.ts @@ -3,9 +3,30 @@ import type { ComboModelStep } from "@/lib/combos/steps"; type JsonRecord = Record; 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 + checks: ReturnType, + 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]; } diff --git a/src/lib/combos/intelligentRouting.ts b/src/lib/combos/intelligentRouting.ts new file mode 100644 index 0000000000..1f2a285a6a --- /dev/null +++ b/src/lib/combos/intelligentRouting.ts @@ -0,0 +1,210 @@ +type JsonRecord = Record; + +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 = { + 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( + 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", + })), + }; +} diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index ebf27f05ec..094729ea54 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -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" }, diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 6e47033fa7..225a4aff37 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -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 diff --git a/tests/e2e/combo-unification.spec.ts b/tests/e2e/combo-unification.spec.ts new file mode 100644 index 0000000000..39631ba82b --- /dev/null +++ b/tests/e2e/combo-unification.spec.ts @@ -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(); + }); +}); diff --git a/tests/unit/autocombo-unification.test.mjs b/tests/unit/autocombo-unification.test.mjs new file mode 100644 index 0000000000..a622651fdc --- /dev/null +++ b/tests/unit/autocombo-unification.test.mjs @@ -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); +}); diff --git a/tests/unit/combo-builder-draft.test.mjs b/tests/unit/combo-builder-draft.test.mjs index 13677babc9..019f40f14e 100644 --- a/tests/unit/combo-builder-draft.test.mjs +++ b/tests/unit/combo-builder-draft.test.mjs @@ -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); +}); diff --git a/tests/unit/combo-routing-engine.test.mjs b/tests/unit/combo-routing-engine.test.mjs index 7fb30fc927..08bdaac5b3 100644 --- a/tests/unit/combo-routing-engine.test.mjs +++ b/tests/unit/combo-routing-engine.test.mjs @@ -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: { diff --git a/tests/unit/sidebar-visibility.test.mjs b/tests/unit/sidebar-visibility.test.mjs index 78b52d9de8..b20829c284 100644 --- a/tests/unit/sidebar-visibility.test.mjs +++ b/tests/unit/sidebar-visibility.test.mjs @@ -29,7 +29,6 @@ test("primary sidebar items place limits after cache", () => { "api-manager", "providers", "combos", - "auto-combo", "costs", "analytics", "cache",