From 4f6f97b369f06cb004a7ee29eaa6eeeb59492888 Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:04:10 -0400 Subject: [PATCH] feat: add expert combo configuration mode (#1547) --- README.md | 2 +- scripts/build-next-isolated.mjs | 6 +- src/app/(dashboard)/dashboard/combos/page.tsx | 709 ++++++++++++------ .../settings/components/AppearanceTab.tsx | 87 +++ .../api/settings/__tests__/settings.test.ts | 12 + src/lib/combos/builderDraft.ts | 49 ++ src/lib/dataPaths.js | 66 -- src/lib/db/settings.ts | 1 + src/shared/constants/comboConfigMode.ts | 9 + src/shared/validation/schemas.ts | 2 + src/shared/validation/settingsSchemas.ts | 2 + src/sse/handlers/chat.ts | 3 +- tests/e2e/combos-flow.spec.ts | 223 ++++++ tests/unit/chat-combo-live-test.test.ts | 31 + tests/unit/combo-builder-draft.test.ts | 41 + tests/unit/db-settings-crud.test.ts | 1 + ...settings-schema-routing-strategies.test.ts | 10 + 17 files changed, 951 insertions(+), 303 deletions(-) delete mode 100644 src/lib/dataPaths.js create mode 100644 src/shared/constants/comboConfigMode.ts diff --git a/README.md b/README.md index ca3a177c2b..7897368a88 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ Developers pay $20–200/month for Claude Pro, Codex Pro, or GitHub Copilot. Eve - **Provider Limits Tracking** — Cached quota snapshots refresh on a server-side schedule (default `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70`) with manual refresh available in the UI - **Multi-Account Support** — Multiple accounts per provider with auto round-robin — when one runs out, switches to the next - **Custom Combos** — Customizable fallback chains with 13 balancing strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, strict-random, auto, lkgp, context-optimized, **context-relay**) -- **Structured Combo Builder** — Build combos step-by-step with explicit provider + model + account selection, including repeated providers and fixed-account targets +- **Structured Combo Builder** — Build combos with guided steps or expert single-page editing, including explicit provider + model + account selection, repeated providers, fixed-account targets, and direct model entry - **Quota-Aware P2C** — Power-of-two account selection now factors quota headroom, backoff, recent errors, and consecutive use - **Codex Business Quotas** — Business/Team workspace quota monitoring directly in the dashboard diff --git a/scripts/build-next-isolated.mjs b/scripts/build-next-isolated.mjs index 66ab563a52..f811076364 100644 --- a/scripts/build-next-isolated.mjs +++ b/scripts/build-next-isolated.mjs @@ -72,7 +72,7 @@ export async function movePath(sourcePath, destinationPath, fsImpl = fs) { function runNextBuild() { return new Promise((resolve) => { const nextBin = path.join(projectRoot, "node_modules", "next", "dist", "bin", "next"); - const child = spawn(process.execPath, [nextBin, "build"], { + const child = spawn(process.execPath, [nextBin, "build", resolveNextBuildBundlerFlag()], { cwd: projectRoot, stdio: "inherit", env: resolveNextBuildEnv(process.env), @@ -97,6 +97,10 @@ function runNextBuild() { }); } +export function resolveNextBuildBundlerFlag(baseEnv = process.env) { + return baseEnv.OMNIROUTE_USE_TURBOPACK === "1" ? "--turbopack" : "--webpack"; +} + export function resolveNextBuildEnv(baseEnv = process.env) { return { ...baseEnv, diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 376f037b49..35d751aaac 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -20,6 +20,7 @@ import { ROUTING_STRATEGIES } from "@/shared/constants/routingStrategies"; import { COMBO_BUILDER_AUTO_CONNECTION, COMBO_BUILDER_STAGES, + buildManualComboModelStep, buildPrecisionComboModelStep, canAccessComboBuilderStage, findNextSuggestedConnectionId, @@ -30,7 +31,9 @@ import { hasExactModelStepDuplicate, isIntelligentBuilderStrategy, parseQualifiedModel, + resolveComboBuilderProviderId, } from "@/lib/combos/builderDraft"; +import { normalizeComboConfigMode } from "@/shared/constants/comboConfigMode"; import BuilderIntelligentStep from "./BuilderIntelligentStep"; import IntelligentComboPanel from "./IntelligentComboPanel"; import { @@ -442,7 +445,11 @@ function getStrategyBadgeClass(strategy) { } function getI18nOrFallback(t, key, fallback) { - if (typeof t.has === "function" && t.has(key)) return t(key); + try { + if (typeof t.has === "function" && t.has(key)) return t(key); + } catch { + // Some translations require ICU variables; fallback keeps optional helper text safe. + } return fallback; } @@ -598,6 +605,7 @@ export default function CombosPage() { const [comboDragIndex, setComboDragIndex] = useState(null); const [comboDragOverIndex, setComboDragOverIndex] = useState(null); const [savingComboOrder, setSavingComboOrder] = useState(false); + const [comboConfigMode, setComboConfigMode] = useState("guided"); const [selectedIntelligentComboId, setSelectedIntelligentComboId] = useState(null); const comboDragIndexRef = useRef(null); const activeFilter = normalizeIntelligentRoutingFilter(searchParams.get("filter")); @@ -638,6 +646,10 @@ export default function CombosPage() { useEffect(() => { fetchData(); + fetch("/api/settings") + .then((r) => (r.ok ? r.json() : null)) + .then((settings) => setComboConfigMode(normalizeComboConfigMode(settings?.comboConfigMode))) + .catch(() => setComboConfigMode("guided")); fetch("/api/settings/proxy") .then((r) => (r.ok ? r.json() : null)) .then((c) => setProxyConfig(c)) @@ -1161,6 +1173,7 @@ export default function CombosPage() { onSave={handleCreate} activeProviders={activeProviders} combo={null} + comboConfigMode={comboConfigMode} /> {/* Edit Modal */} @@ -1171,6 +1184,7 @@ export default function CombosPage() { onClose={() => setEditingCombo(null)} onSave={(data) => handleUpdate(editingCombo.id, data)} activeProviders={activeProviders} + comboConfigMode={comboConfigMode} /> {/* Proxy Config Modal */} @@ -1380,20 +1394,22 @@ function StrategyRecommendationsPanel({ strategy, onApply, showNudge }) { ); } -function FieldLabelWithHelp({ label, help }) { +function FieldLabelWithHelp({ label, help, showHelp = true }) { return (
- - - help - - + {showHelp && ( + + + help + + + )}
); } -function ComboReadinessPanel({ checks, blockers }) { +function ComboReadinessPanel({ checks, blockers, showDescription = true }) { const t = useTranslations("combos"); const hasBlockers = blockers.length > 0; @@ -1421,13 +1437,15 @@ function ComboReadinessPanel({ checks, blockers }) {

-

- {getI18nOrFallback( - t, - "readinessDescription", - "Review the checklist before creating or updating this combo." - )} -

+ {showDescription && ( +

+ {getI18nOrFallback( + t, + "readinessDescription", + "Review the checklist before creating or updating this combo." + )} +

+ )}
{checks.map((check) => ( @@ -1772,7 +1790,7 @@ function TestResultsView({ results }) { // ───────────────────────────────────────────── // Combo Form Modal // ───────────────────────────────────────────── -function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { +function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, comboConfigMode }) { type CreateDraftSnapshot = { name: string; models: unknown[]; @@ -1804,6 +1822,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const tc = useTranslations("common"); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); const notify = useNotificationStore(); + const isExpertMode = normalizeComboConfigMode(comboConfigMode) === "expert"; const createDraftStateRef = useRef(getEmptyCreateDraftSnapshot()); const [name, setName] = useState(combo?.name || ""); const [models, setModels] = useState(() => { @@ -1821,6 +1840,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const [builderProviderId, setBuilderProviderId] = useState(""); const [builderModelId, setBuilderModelId] = useState(""); const [builderConnectionId, setBuilderConnectionId] = useState(COMBO_BUILDER_AUTO_CONNECTION); + const [manualModelInput, setManualModelInput] = useState(""); + const [manualModelError, setManualModelError] = useState(""); const [builderComboRefName, setBuilderComboRefName] = useState(""); const [builderError, setBuilderError] = useState(""); const [builderStage, setBuilderStage] = useState(COMBO_BUILDER_STAGES[0]); @@ -1860,13 +1881,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { setModels((nextCombo?.models || []).map((m) => normalizeModelEntry(m))); setStrategy(nextCombo?.strategy || comboDefaults?.strategy || "priority"); setConfig(nextConfig); - setShowAdvanced(false); + setShowAdvanced(isExpertMode); setNameError(""); setAgentSystemMessage(nextCombo?.system_message || ""); setAgentToolFilter(nextCombo?.tool_filter_regex || ""); setAgentContextCache(!!nextCombo?.context_cache_protection); }, - [setAgentContextCache] + [isExpertMode, setAgentContextCache] ); useEffect(() => { @@ -1948,6 +1969,12 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { : null; const builderHasDuplicate = builderCandidateStep && hasExactModelStepDuplicate(models, builderCandidateStep); + const manualModelStep = buildManualComboModelStep({ + value: manualModelInput, + providers: builderProviders, + }); + const manualModelHasDuplicate = + manualModelStep && hasExactModelStepDuplicate(models, manualModelStep); const weightTotal = models.reduce((sum, modelEntry) => sum + (modelEntry.weight || 0), 0); const pricedModelCount = models.reduce( (count, modelEntry) => count + (hasPricingForModel(modelEntry.model) ? 1 : 0), @@ -2055,6 +2082,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { ) ); } + const showInlineReadinessPanel = !isExpertMode || saveBlockers.length > 0; const fetchModalData = async () => { setBuilderLoading(true); @@ -2103,6 +2131,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { setBuilderProviderId(""); setBuilderModelId(""); setBuilderConnectionId(COMBO_BUILDER_AUTO_CONNECTION); + setManualModelInput(""); + setManualModelError(""); setBuilderComboRefName(""); setBuilderError(""); setBuilderStage("basics"); @@ -2133,7 +2163,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { draft.models.length === 0 && draft.strategy === "priority" && Object.keys(draft.config || {}).length === 0 && - draft.showAdvanced === false && + (draft.showAdvanced === false || (isExpertMode && draft.showAdvanced === true)) && draft.nameError.length === 0 && draft.agentSystemMessage.length === 0 && draft.agentToolFilter.length === 0 && @@ -2152,7 +2182,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { return () => { cancelled = true; }; - }, [combo, getEmptyCreateDraftSnapshot, isOpen, resetFormForCombo]); + }, [combo, getEmptyCreateDraftSnapshot, isExpertMode, isOpen, resetFormForCombo]); useEffect(() => { if (!isOpen) return; @@ -2271,6 +2301,54 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { ); }; + const handleAddManualModel = () => { + const parsedManualModel = parseQualifiedModel(manualModelInput); + if (!parsedManualModel) { + setManualModelError( + getI18nOrFallback(t, "manualModelInvalid", "Enter a model as provider/model.") + ); + return; + } + + const resolvedProviderId = resolveComboBuilderProviderId( + parsedManualModel.providerId, + builderProviders + ); + if (!resolvedProviderId) { + setManualModelError( + getI18nOrFallback(t, "manualModelUnknownProvider", "Unknown provider prefix.") + ); + return; + } + + const nextStep = buildManualComboModelStep({ + value: manualModelInput, + providers: builderProviders, + }); + + if (!nextStep) { + setManualModelError( + getI18nOrFallback(t, "manualModelInvalid", "Enter a model as provider/model.") + ); + return; + } + + if (hasExactModelStepDuplicate(models, nextStep)) { + setManualModelError( + getI18nOrFallback( + t, + "builderDuplicateExact", + "This exact provider/model/account step is already in the combo." + ) + ); + return; + } + + setModels([...models, nextStep]); + setManualModelInput(""); + setManualModelError(""); + }; + const handleAddComboReference = () => { if (!builderComboRefName) return; @@ -2501,6 +2579,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { }; const isEdit = !!combo; + const showBasicsSection = isExpertMode || builderStage === "basics"; + const showStepsSection = isExpertMode || builderStage === "steps"; + const showStrategySection = isExpertMode || builderStage === "strategy"; + const showIntelligentSection = + usesIntelligentBuilderStage && (isExpertMode || builderStage === "intelligent"); + const showReviewSection = !isExpertMode && builderStage === "review"; + const advancedConfigVisible = isExpertMode || showAdvanced; return ( <> @@ -2511,117 +2596,122 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { size="full" >
-
-
-
-

- {getI18nOrFallback(t, "builderFlowTitle", "Combo Builder Flow")} -

-

- {getI18nOrFallback( - t, - "builderStagesDescription", - "Move through the stages in order to define the combo, build the steps, choose the routing strategy and review the result." - )} -

+ {!isExpertMode && ( +
+
+
+

+ {getI18nOrFallback(t, "builderFlowTitle", "Combo Builder Flow")} +

+

+ {getI18nOrFallback( + t, + "builderStagesDescription", + "Move through the stages in order to define the combo, build the steps, choose the routing strategy and review the result." + )} +

+
+ + {Math.max(currentStageIndex + 1, 1)}/{visibleStageMeta.length} +
- - {Math.max(currentStageIndex + 1, 1)}/{visibleStageMeta.length} - -
-
- {visibleStageMeta.map((stageMeta, index) => { - const isActive = builderStage === stageMeta.id; - const canVisitStage = isActive - ? true - : canAccessComboBuilderStage(stageMeta.id, builderStageChecks, { strategy }); - const isCompleted = - stageMeta.id === "review" - ? false - : stageMeta.id === "basics" - ? builderStageChecks.basics - : stageMeta.id === "steps" - ? builderStageChecks.steps - : stageMeta.id === "intelligent" - ? usesIntelligentBuilderStage - : builderStageChecks.strategy; +
+ {visibleStageMeta.map((stageMeta, index) => { + const isActive = builderStage === stageMeta.id; + const canVisitStage = isActive + ? true + : canAccessComboBuilderStage(stageMeta.id, builderStageChecks, { strategy }); + const isCompleted = + stageMeta.id === "review" + ? false + : stageMeta.id === "basics" + ? builderStageChecks.basics + : stageMeta.id === "steps" + ? builderStageChecks.steps + : stageMeta.id === "intelligent" + ? usesIntelligentBuilderStage + : builderStageChecks.strategy; - return ( -
-

- {getI18nOrFallback( - t, - `builderStage.${stageMeta.id}.description`, - stageMeta.fallbackDescription - )} -

-

- {index < currentStageIndex - ? getI18nOrFallback(t, "builderStageVisited", "Visited") - : isActive - ? getI18nOrFallback(t, "builderStageCurrent", "Current") - : canVisitStage - ? getI18nOrFallback(t, "builderStagePending", "Pending") - : getI18nOrFallback(t, "builderStageLocked", "Locked")} -

- - ); - })} +

+

+ {index < currentStageIndex + ? getI18nOrFallback(t, "builderStageVisited", "Visited") + : isActive + ? getI18nOrFallback(t, "builderStageCurrent", "Current") + : canVisitStage + ? getI18nOrFallback(t, "builderStagePending", "Pending") + : getI18nOrFallback(t, "builderStageLocked", "Locked")} +

+ + ); + })} +
-
+ )} - {builderStage === "basics" && ( + {showBasicsSection && ( <> {/* Name */}
-

{t("nameHint")}

+ {!isExpertMode && ( +

{t("nameHint")}

+ )}
- {!isEdit && ( + {!isEdit && !isExpertMode && (

@@ -2680,15 +2770,17 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { )} {/* Strategy Toggle */} - {builderStage === "strategy" && ( + {showStrategySection && (

- - - help - - + {!isExpertMode && ( + + + help + + + )}
{STRATEGY_OPTIONS.map((s) => ( @@ -2696,11 +2788,12 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { key={s.value} onClick={() => setStrategy(s.value)} data-testid={`strategy-option-${s.value}`} - title={getStrategyDescription(t, s.value)} - aria-label={`${getStrategyLabel(t, s.value)}. ${getStrategyDescription( - t, - s.value - )}`} + title={!isExpertMode ? getStrategyDescription(t, s.value) : undefined} + aria-label={ + isExpertMode + ? getStrategyLabel(t, s.value) + : `${getStrategyLabel(t, s.value)}. ${getStrategyDescription(t, s.value)}` + } className={`py-1.5 px-2 rounded-md text-xs font-medium transition-all ${ strategy === s.value ? "bg-white dark:bg-bg-main shadow-sm text-primary" @@ -2714,23 +2807,27 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { ))}
-

- {getStrategyDescription(t, strategy)} -

-
- -
-
- -
+ {!isExpertMode && ( + <> +

+ {getStrategyDescription(t, strategy)} +

+
+ +
+
+ +
+ + )}
)} - {builderStage === "intelligent" && ( + {showIntelligentSection && (
@@ -2769,23 +2866,72 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {

{getI18nOrFallback(t, "builderTitle", "Precision Builder")}

-

- {getI18nOrFallback( - t, - "builderStepsDescription", - "Build each combo step in sequence: provider, model, then account. This allows repeating the same provider and model with different accounts." - )} -

+ {!isExpertMode && ( +

+ {getI18nOrFallback( + t, + "builderStepsDescription", + "Build each combo step in sequence: provider, model, then account. This allows repeating the same provider and model with different accounts." + )} +

+ )}
- + {!isExpertMode && ( + + )}
+ {isExpertMode && ( +
+ +
+ { + setManualModelInput(e.target.value); + setManualModelError(""); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddManualModel(); + } + }} + placeholder="provider/model" + data-testid="combo-manual-model-input" + className="flex-1 text-xs py-2 px-2 rounded border border-black/10 dark:border-white/10 bg-white dark:bg-bg-main text-text-main focus:border-primary focus:outline-none font-mono" + /> + +
+ {(manualModelError || manualModelHasDuplicate) && ( +
+ {manualModelError || + getI18nOrFallback( + t, + "builderDuplicateExact", + "This exact provider/model/account step is already in the combo." + )} +
+ )} +
+ )} +
-
-

- {getI18nOrFallback(t, "builderPreview", "Current step preview")} -

-

- {builderCandidateStep - ? formatModelDisplay(builderCandidateStep) - : getI18nOrFallback( - t, - "previewNextStep", - "Choose provider and model to preview the next step." - )} -

-
+ {isExpertMode ? ( +
-
+ ) : ( +
+

+ {getI18nOrFallback(t, "builderPreview", "Current step preview")} +

+

+ {builderCandidateStep + ? formatModelDisplay(builderCandidateStep) + : getI18nOrFallback( + t, + "previewNextStep", + "Choose provider and model to preview the next step." + )} +

+
+ + {builderHasDuplicate && ( + + {getI18nOrFallback( + t, + "builderDuplicateExact", + "This exact provider/model/account step is already in the combo." + )} + + )} +
+
+ )}
-

- {getI18nOrFallback( - t, - "pricingCoverageHint", - "Cost-optimized works best when all combo models have pricing." - )} -

+ {!isExpertMode && ( +

+ {getI18nOrFallback( + t, + "pricingCoverageHint", + "Cost-optimized works best when all combo models have pricing." + )} +

+ )}
)} @@ -3117,7 +3287,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
)} - {hasRoundRobinSingleModel && ( + {!isExpertMode && hasRoundRobinSingleModel && (
info @@ -3157,13 +3327,20 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
)} -
- -
+ {showInlineReadinessPanel && ( +
+ +
+ )} + {!isExpertMode && ( + + )} - {showAdvanced && ( + {advancedConfigVisible && (
@@ -3195,6 +3374,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { "advancedHelp.maxRetries", ADVANCED_FIELD_HELP_FALLBACK.maxRetries )} + showHelp={!isExpertMode} />
-
-

- {getI18nOrFallback( - t, - "contextRelayProviderNote", - "Context Relay currently generates handoffs for Codex account rotation. Pair it with multiple accounts of the same provider for the best continuity." - )} -

-
+ {!isExpertMode && ( +
+

+ {getI18nOrFallback( + t, + "contextRelayProviderNote", + "Context Relay currently generates handoffs for Codex account rotation. Pair it with multiple accounts of the same provider for the best continuity." + )} +

+
+ )}
)} -

{t("advancedHint")}

+ {!isExpertMode && ( +

{t("advancedHint")}

+ )}
)} )} {/* Agent Features (#399 / #401 / #454) */} - {builderStage === "strategy" && ( + {showStrategySection && (
smart_toy -

{t("agentFeaturesTitle")}

- {t("agentFeaturesDescription")} +

+ {getI18nOrFallback(t, "agentFeaturesTitle", "Agent features")} +

+ {!isExpertMode && ( + + {getI18nOrFallback( + t, + "agentFeaturesDescription", + "Tune agent prompts and tool access for this combo." + )} + + )}
{/* System Message Override */}