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 (
{label}
-
-
- 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 (
-
{
- if (!canVisitStage) return;
- setBuilderStage(stageMeta.id);
- }}
- disabled={!canVisitStage}
- className={`text-left rounded-lg border px-3 py-2 transition-all ${
- isActive
- ? "border-primary bg-primary/8"
- : canVisitStage
- ? "border-black/8 dark:border-white/8 bg-white/60 dark:bg-white/[0.02] hover:border-primary/40"
- : "border-black/6 dark:border-white/6 bg-black/[0.015] dark:bg-white/[0.015] opacity-60 cursor-not-allowed"
- }`}
- >
-
-
- {isCompleted && !isActive ? "check_circle" : stageMeta.icon}
-
-
+ return (
+ {
+ if (!canVisitStage) return;
+ setBuilderStage(stageMeta.id);
+ }}
+ disabled={!canVisitStage}
+ className={`text-left rounded-lg border px-3 py-2 transition-all ${
+ isActive
+ ? "border-primary bg-primary/8"
+ : canVisitStage
+ ? "border-black/8 dark:border-white/8 bg-white/60 dark:bg-white/[0.02] hover:border-primary/40"
+ : "border-black/6 dark:border-white/6 bg-black/[0.015] dark:bg-white/[0.015] opacity-60 cursor-not-allowed"
+ }`}
+ >
+
+
+ {isCompleted && !isActive ? "check_circle" : stageMeta.icon}
+
+
+ {getI18nOrFallback(
+ t,
+ `builderStage.${stageMeta.id}.label`,
+ stageMeta.fallbackLabel
+ )}
+
+
+
{getI18nOrFallback(
t,
- `builderStage.${stageMeta.id}.label`,
- stageMeta.fallbackLabel
+ `builderStage.${stageMeta.id}.description`,
+ stageMeta.fallbackDescription
)}
-
-
-
- {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 && (
{t("routingStrategy")}
-
-
- 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 && (
{t("models")}
@@ -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."
+ )}
+
+ )}
- setShowModelSelect(true)}
- className="text-[10px] shrink-0 text-primary hover:text-primary/80 transition-colors"
- >
- {getI18nOrFallback(t, "builderBrowseCatalog", "Legacy model browser")}
-
+ {!isExpertMode && (
+ setShowModelSelect(true)}
+ className="text-[10px] shrink-0 text-primary hover:text-primary/80 transition-colors"
+ >
+ {getI18nOrFallback(t, "builderBrowseCatalog", "Legacy model browser")}
+
+ )}
+ {isExpertMode && (
+
+
+ {getI18nOrFallback(t, "manualModel", "Manual model")}
+
+
+ {
+ 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"
+ />
+
+ {getI18nOrFallback(t, "addModel", "Add model")}
+
+
+ {(manualModelError || manualModelHasDuplicate) && (
+
+ {manualModelError ||
+ getI18nOrFallback(
+ t,
+ "builderDuplicateExact",
+ "This exact provider/model/account step is already in the combo."
+ )}
+
+ )}
+
+ )}
+
@@ -2864,20 +3010,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
-
-
- {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."
+ )}
+
+
+
+ {getI18nOrFallback(t, "builderAddStep", "Add detailed step")}
+
+ {builderHasDuplicate && (
+
+ {getI18nOrFallback(
+ t,
+ "builderDuplicateExact",
+ "This exact provider/model/account step is already in the combo."
+ )}
+
+ )}
+
+
+ )}
@@ -3091,13 +3259,15 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
style={{ width: `${pricingCoveragePercent}%` }}
/>
-
- {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 && (
+
+
+
+ )}
setShowModelSelect(true)}
className="w-full mt-2 py-2 border border-dashed border-black/10 dark:border-white/10 rounded-lg text-xs text-text-muted hover:text-primary hover:border-primary/30 transition-colors flex items-center justify-center gap-1"
+ data-testid="combo-browse-catalog"
>
travel_explore
{getI18nOrFallback(t, "browseLegacyCatalog", "Browse legacy model catalog")}
@@ -3172,19 +3349,21 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
)}
{/* Advanced Config Toggle */}
- {builderStage === "strategy" && (
+ {showStrategySection && (
<>
- setShowAdvanced(!showAdvanced)}
- className="flex items-center gap-1 text-xs text-text-muted hover:text-text-main transition-colors self-start"
- >
-
- {showAdvanced ? "expand_less" : "expand_more"}
-
- {t("advancedSettings")}
-
+ {!isExpertMode && (
+ setShowAdvanced(!showAdvanced)}
+ className="flex items-center gap-1 text-xs text-text-muted hover:text-text-main transition-colors self-start"
+ >
+
+ {showAdvanced ? "expand_less" : "expand_more"}
+
+ {t("advancedSettings")}
+
+ )}
- {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 */}
- {t("agentFeaturesSystemMessageOverride")}
+ {getI18nOrFallback(
+ t,
+ "agentFeaturesSystemMessageOverride",
+ "System message override"
+ )}
{/* Tool Filter Regex */}
- {t("agentFeaturesToolFilterRegex")}
+ {getI18nOrFallback(t, "agentFeaturesToolFilterRegex", "Tool filter regex")}
-
- {t("agentFeaturesToolFilterHint")}
-
+ {!isExpertMode && (
+
+ {getI18nOrFallback(
+ t,
+ "agentFeaturesToolFilterHint",
+ "Limit agent tools by name with a regular expression."
+ )}
+
+ )}
{/* Context Cache Protection */}
- {t("agentFeaturesContextCacheProtection")}
+ {getI18nOrFallback(
+ t,
+ "agentFeaturesContextCacheProtection",
+ "Context cache protection"
+ )}
-
- {t("agentFeaturesContextCacheHint")}
-
+ {!isExpertMode && (
+
+ {getI18nOrFallback(
+ t,
+ "agentFeaturesContextCacheHint",
+ "Keep cached context isolated when provider state changes."
+ )}
+
+ )}
)}
- {builderStage === "review" && (
+ {showReviewSection && (
@@ -3642,34 +3872,45 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
)}
{/* Actions */}
-
-
- {builderStage === "basics" ? tc("cancel") : getI18nOrFallback(tc, "back", "Back")}
-
- {builderStage === "review" ? (
+ {isExpertMode ? (
+
+
+ {tc("cancel")}
+
{saving ? t("saving") : isEdit ? tc("save") : t("createCombo")}
- ) : (
+
+ ) : (
+
- {getI18nOrFallback(tc, "next", "Next")}
+ {builderStage === "basics" ? tc("cancel") : getI18nOrFallback(tc, "back", "Back")}
- )}
-
+ {builderStage === "review" ? (
+
+ {saving ? t("saving") : isEdit ? tc("save") : t("createCombo")}
+
+ ) : (
+
+ {getI18nOrFallback(tc, "next", "Next")}
+
+ )}
+
+ )}
- {builderStage !== "review" && !canAdvanceFromCurrentStage && (
+ {(isExpertMode || builderStage !== "review") && !canAdvanceFromCurrentStage && (
{builderStage === "basics"
? getI18nOrFallback(
diff --git a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
index 89ef8242fd..aeef9e2255 100644
--- a/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
+++ b/src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx
@@ -6,6 +6,11 @@ import { useTheme } from "@/shared/hooks/useTheme";
import useThemeStore, { COLOR_THEMES } from "@/store/themeStore";
import { cn } from "@/shared/utils/cn";
import { useTranslations } from "next-intl";
+import {
+ COMBO_CONFIG_MODE_SETTING_KEY,
+ normalizeComboConfigMode,
+ type ComboConfigMode,
+} from "@/shared/constants/comboConfigMode";
import {
HIDDEN_SIDEBAR_ITEMS_SETTING_KEY,
SIDEBAR_SECTIONS,
@@ -30,6 +35,7 @@ export default function AppearanceTab() {
settings[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]
);
const hiddenSidebarSet = new Set(hiddenSidebarItems);
+ const comboConfigMode = normalizeComboConfigMode(settings[COMBO_CONFIG_MODE_SETTING_KEY]);
const getSettingsLabel = (key: string, fallback: string) =>
typeof t.has === "function" && t.has(key) ? t(key) : fallback;
@@ -107,6 +113,32 @@ export default function AppearanceTab() {
{ id: "cyan", color: COLOR_THEMES.cyan, label: t("themeCyan") },
];
+ const comboConfigModeOptions: Array<{
+ id: ComboConfigMode;
+ icon: string;
+ title: string;
+ description: string;
+ }> = [
+ {
+ id: "guided",
+ icon: "route",
+ title: getSettingsLabel("comboConfigModeGuided", "Guided"),
+ description: getSettingsLabel(
+ "comboConfigModeGuidedDesc",
+ "Use the current step-by-step combo builder."
+ ),
+ },
+ {
+ id: "expert",
+ icon: "tune",
+ title: getSettingsLabel("comboConfigModeExpert", "Expert"),
+ description: getSettingsLabel(
+ "comboConfigModeExpertDesc",
+ "Show every combo option on one page and enable direct model entry."
+ ),
+ },
+ ];
+
const showDebug = settings.debugMode === true;
const sidebarSections = SIDEBAR_SECTIONS.filter(
(section) => section.visibility !== "debug" || showDebug
@@ -223,6 +255,61 @@ export default function AppearanceTab() {
+
+
+
+ {getSettingsLabel("comboConfigMode", "Combo configuration mode")}
+
+
+ {getSettingsLabel(
+ "comboConfigModeDesc",
+ "Choose how the combo create and edit dialog is organized."
+ )}
+
+
+
+
+ {comboConfigModeOptions.map((option) => {
+ const active = comboConfigMode === option.id;
+ return (
+ updateSetting(COMBO_CONFIG_MODE_SETTING_KEY, option.id)}
+ className={cn(
+ "flex items-start gap-3 rounded-lg border p-3 text-left transition-colors disabled:opacity-60",
+ active
+ ? "border-primary bg-primary/10 text-primary"
+ : "border-border bg-surface/40 text-text-main hover:border-primary/40"
+ )}
+ >
+
+ {option.icon}
+
+
+ {option.title}
+
+ {option.description}
+
+
+
+ );
+ })}
+
+
+
{t("sidebarVisibilityToggle")}
diff --git a/src/app/api/settings/__tests__/settings.test.ts b/src/app/api/settings/__tests__/settings.test.ts
index c8d06b2fa7..6f0acf2cfe 100644
--- a/src/app/api/settings/__tests__/settings.test.ts
+++ b/src/app/api/settings/__tests__/settings.test.ts
@@ -29,6 +29,7 @@ describe("PATCH /api/settings", () => {
(getSettings as any).mockResolvedValue({
debugMode: false,
hiddenSidebarItems: [],
+ comboConfigMode: "guided",
});
// Mock updateSettings to merge updates into the original
(updateSettings as any).mockImplementation(async (updates: Record
) => {
@@ -61,4 +62,15 @@ describe("PATCH /api/settings", () => {
const calledWith = (updateSettings as any).mock.calls[0][0];
expect(calledWith.hiddenSidebarItems).toEqual([]);
});
+
+ it("updates comboConfigMode via PATCH", async () => {
+ const req = createPatchRequest({ comboConfigMode: "expert" });
+ const res = await PATCH(req as any);
+ expect(res.status).toBe(200);
+ const json = await res.json();
+ expect(json.comboConfigMode).toBe("expert");
+ expect(updateSettings).toHaveBeenCalledOnce();
+ const calledWith = (updateSettings as any).mock.calls[0][0];
+ expect(calledWith.comboConfigMode).toBe("expert");
+ });
});
diff --git a/src/lib/combos/builderDraft.ts b/src/lib/combos/builderDraft.ts
index c5ac13c407..fe51b616a1 100644
--- a/src/lib/combos/builderDraft.ts
+++ b/src/lib/combos/builderDraft.ts
@@ -84,6 +84,55 @@ export function buildPrecisionComboModelStep({
};
}
+type ComboBuilderProviderIdentity = {
+ providerId?: unknown;
+ alias?: unknown;
+ prefix?: unknown;
+};
+
+export function resolveComboBuilderProviderId(
+ providerIdOrAlias: unknown,
+ providers: ComboBuilderProviderIdentity[] = []
+): string | null {
+ const normalizedProviderId = toTrimmedString(providerIdOrAlias);
+ if (!normalizedProviderId) return null;
+
+ const matchedProvider = providers.find((provider) => {
+ const providerId = toTrimmedString(provider.providerId);
+ const alias = toTrimmedString(provider.alias);
+ const prefix = toTrimmedString(provider.prefix);
+ return (
+ providerId === normalizedProviderId ||
+ alias === normalizedProviderId ||
+ prefix === normalizedProviderId
+ );
+ });
+
+ return toTrimmedString(matchedProvider?.providerId) || null;
+}
+
+export function buildManualComboModelStep({
+ value,
+ providers = [],
+ weight = 0,
+}: {
+ value: unknown;
+ providers?: ComboBuilderProviderIdentity[];
+ weight?: number;
+}): ComboModelStep | null {
+ const parsed = parseQualifiedModel(value);
+ if (!parsed) return null;
+
+ const providerId = resolveComboBuilderProviderId(parsed.providerId, providers);
+ if (!providerId) return null;
+
+ return buildPrecisionComboModelStep({
+ providerId,
+ modelId: parsed.modelId,
+ weight,
+ });
+}
+
export function getExactModelStepSignature(entry: unknown): string | null {
if (!isRecord(entry) || entry.kind === "combo-ref") return null;
const modelValue = toTrimmedString(entry.model);
diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js
deleted file mode 100644
index 5078bbe9e6..0000000000
--- a/src/lib/dataPaths.js
+++ /dev/null
@@ -1,66 +0,0 @@
-"use strict";
-var __importDefault =
- (this && this.__importDefault) ||
- function (mod) {
- return mod && mod.__esModule ? mod : { default: mod };
- };
-Object.defineProperty(exports, "__esModule", { value: true });
-exports.APP_NAME = void 0;
-exports.getLegacyDotDataDir = getLegacyDotDataDir;
-exports.getDefaultDataDir = getDefaultDataDir;
-exports.resolveDataDir = resolveDataDir;
-exports.isSamePath = isSamePath;
-const path_1 = __importDefault(require("path"));
-const os_1 = __importDefault(require("os"));
-exports.APP_NAME = "omniroute";
-function fallbackHomeDir() {
- const envHome = process.env.HOME || process.env.USERPROFILE;
- if (typeof envHome === "string" && envHome.trim().length > 0) {
- return path_1.default.resolve(envHome);
- }
- return os_1.default.tmpdir();
-}
-function safeHomeDir() {
- try {
- return os_1.default.homedir();
- } catch {
- return fallbackHomeDir();
- }
-}
-function normalizeConfiguredPath(dir) {
- if (typeof dir !== "string") return null;
- const trimmed = dir.trim();
- if (!trimmed) return null;
- return path_1.default.resolve(trimmed);
-}
-function getLegacyDotDataDir() {
- return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`);
-}
-function getDefaultDataDir() {
- const homeDir = safeHomeDir();
- if (process.platform === "win32") {
- const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming");
- return path_1.default.join(appData, exports.APP_NAME);
- }
- // Support XDG on Linux/macOS when explicitly configured.
- const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME);
- if (xdgConfigHome) {
- return path_1.default.join(xdgConfigHome, exports.APP_NAME);
- }
- return getLegacyDotDataDir();
-}
-function resolveDataDir({ isCloud = false } = {}) {
- if (isCloud) return "/tmp";
- const configured = normalizeConfiguredPath(process.env.DATA_DIR);
- if (configured) return configured;
- return getDefaultDataDir();
-}
-function isSamePath(a, b) {
- if (!a || !b) return false;
- const normalizedA = path_1.default.resolve(a);
- const normalizedB = path_1.default.resolve(b);
- if (process.platform === "win32") {
- return normalizedA.toLowerCase() === normalizedB.toLowerCase();
- }
- return normalizedA === normalizedB;
-}
diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts
index 132189da36..69286d7993 100644
--- a/src/lib/db/settings.ts
+++ b/src/lib/db/settings.ts
@@ -54,6 +54,7 @@ export async function getSettings() {
antigravitySignatureCacheMode: "enabled",
requireLogin: true,
hiddenSidebarItems: [],
+ comboConfigMode: "guided",
alwaysPreserveClientCache: "auto",
idempotencyWindowMs: 5000,
wsAuth: false,
diff --git a/src/shared/constants/comboConfigMode.ts b/src/shared/constants/comboConfigMode.ts
new file mode 100644
index 0000000000..76efda2bba
--- /dev/null
+++ b/src/shared/constants/comboConfigMode.ts
@@ -0,0 +1,9 @@
+export const COMBO_CONFIG_MODE_SETTING_KEY = "comboConfigMode";
+
+export const COMBO_CONFIG_MODES = ["guided", "expert"] as const;
+
+export type ComboConfigMode = (typeof COMBO_CONFIG_MODES)[number];
+
+export function normalizeComboConfigMode(value: unknown): ComboConfigMode {
+ return value === "expert" ? "expert" : "guided";
+}
diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts
index 6737a33906..0adc1f3398 100644
--- a/src/shared/validation/schemas.ts
+++ b/src/shared/validation/schemas.ts
@@ -1,5 +1,6 @@
import { z } from "zod";
import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints";
+import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { isLocalProvider } from "@/shared/constants/providers";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
import { isForbiddenUpstreamHeaderName } from "@/shared/constants/upstreamHeaders";
@@ -434,6 +435,7 @@ export const updateSettingsSchema = z.object({
blockedProviders: z.array(z.string().max(100)).optional(),
hideHealthCheckLogs: z.boolean().optional(),
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
+ comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),
// Routing settings (#134)
fallbackStrategy: settingsFallbackStrategySchema.optional(),
wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(),
diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts
index 2222432771..480c08392d 100644
--- a/src/shared/validation/settingsSchemas.ts
+++ b/src/shared/validation/settingsSchemas.ts
@@ -6,6 +6,7 @@
* at runtime (see: https://github.com/vercel/next.js/issues/12557).
*/
import { z } from "zod";
+import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode";
import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility";
const fallbackStrategyValues = [
@@ -47,6 +48,7 @@ export const updateSettingsSchema = z.object({
hideHealthCheckLogs: z.boolean().optional(),
debugMode: z.boolean().optional(),
hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(),
+ comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(),
// Routing settings (#134)
fallbackStrategy: z.enum(fallbackStrategyValues).optional(),
wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(),
diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts
index 955ba39527..083a8a1267 100644
--- a/src/sse/handlers/chat.ts
+++ b/src/sse/handlers/chat.ts
@@ -534,7 +534,8 @@ async function handleSingleModelChat(
const baseRetrySettings = resolveCooldownAwareRetrySettings(
await getCachedSettings().catch(() => ({}))
);
- const disableCooldownAwareRetry = isCombo || runtimeOptions.emergencyFallbackTried === true;
+ const disableCooldownAwareRetry =
+ isCombo || forceLiveComboTest || runtimeOptions.emergencyFallbackTried === true;
const retrySettings = disableCooldownAwareRetry
? {
...baseRetrySettings,
diff --git a/tests/e2e/combos-flow.spec.ts b/tests/e2e/combos-flow.spec.ts
index e44315829f..986ed9e6e7 100644
--- a/tests/e2e/combos-flow.spec.ts
+++ b/tests/e2e/combos-flow.spec.ts
@@ -64,6 +64,14 @@ test.describe("Combos flow", () => {
});
});
+ await page.route(/\/api\/settings$/, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ comboConfigMode: "guided" }),
+ });
+ });
+
await page.route("**/api/settings/proxy", async (route) => {
await route.fulfill({
status: 200,
@@ -263,6 +271,213 @@ test.describe("Combos flow", () => {
await expect(testResultsModal).toContainText(/qa-test-model/i);
});
+ test("expert mode shows a single-page combo form with manual model entry", async ({ page }) => {
+ const state: {
+ combos: ComboStub[];
+ nextId: number;
+ lastPayload: ComboCreatePayload | null;
+ } = {
+ combos: [],
+ nextId: 1,
+ lastPayload: null,
+ };
+
+ await page.route("**/api/combos/metrics", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ metrics: {} }),
+ });
+ });
+
+ await page.route(/\/api\/settings$/, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ comboConfigMode: "expert" }),
+ });
+ });
+
+ await page.route("**/api/settings/proxy", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ combos: {} }),
+ });
+ });
+
+ await page.route("**/api/providers", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ connections: [
+ { id: "conn-codex", provider: "codex", testStatus: "active" },
+ { id: "conn-openrouter", provider: "openrouter", 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/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/combos/builder/options", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ providers: [
+ {
+ providerId: "codex",
+ alias: "cx",
+ displayName: "Codex",
+ connectionCount: 1,
+ models: [],
+ connections: [
+ {
+ id: "conn-codex",
+ label: "Codex Primary",
+ status: "active",
+ priority: 1,
+ },
+ ],
+ },
+ {
+ providerId: "openrouter",
+ alias: "openrouter",
+ displayName: "OpenRouter",
+ connectionCount: 1,
+ models: [],
+ connections: [
+ {
+ id: "conn-openrouter",
+ label: "OpenRouter Primary",
+ status: "active",
+ priority: 1,
+ },
+ ],
+ },
+ ],
+ comboRefs: [],
+ }),
+ });
+ });
+
+ await page.route("**/api/combos", async (route) => {
+ const method = route.request().method();
+ if (method === "GET") {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ combos: state.combos }),
+ });
+ return;
+ }
+
+ if (method === "POST") {
+ const payloadRaw = route.request().postDataJSON();
+ const payload =
+ payloadRaw && typeof payloadRaw === "object" ? (payloadRaw as ComboCreatePayload) : {};
+ state.lastPayload = payload;
+ const comboId = `combo-${state.nextId++}`;
+ const createdCombo = {
+ id: comboId,
+ name: payload.name || comboId,
+ strategy: payload.strategy || "priority",
+ models: payload.models || [],
+ config: payload.config || {},
+ isActive: true,
+ };
+ state.combos.push(createdCombo);
+
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ combo: createdCombo }),
+ });
+ return;
+ }
+
+ await route.fulfill({ status: 405, body: "Method not allowed in test stub" });
+ });
+
+ await gotoDashboardRoute(page, "/dashboard/combos", {
+ waitUntil: "domcontentloaded",
+ });
+ await page
+ .getByRole("button", { name: /create combo|criar combo/i })
+ .first()
+ .click();
+
+ const comboDialog = page.getByRole("dialog").first();
+ await expect(comboDialog).toBeVisible();
+ await expect(comboDialog.locator('[data-testid="combo-builder-next"]')).toHaveCount(0);
+ await expect(comboDialog.locator('[data-testid="combo-builder-stage-steps"]')).toHaveCount(0);
+ await expect(comboDialog.locator('[data-testid="combo-readiness-panel"]')).toBeVisible();
+ await expect(comboDialog.locator('[data-testid="combo-browse-catalog"]')).toBeVisible();
+ await comboDialog.locator('[data-testid="combo-browse-catalog"]').click();
+ const modelCatalogDialog = page.getByRole("dialog", {
+ name: /add model to combo|adicionar modelo ao combo/i,
+ });
+ await expect(modelCatalogDialog).toBeVisible();
+ await modelCatalogDialog.getByRole("button", { name: /close/i }).click();
+ await expect(comboDialog.getByText(/recommended setup|how to use this strategy/i)).toHaveCount(
+ 0
+ );
+
+ await comboDialog.locator('[data-testid="combo-name-input"]').fill("expert-stack");
+ await comboDialog.locator('[data-testid="combo-manual-model-input"]').fill("cx/gpt-5.5");
+ await comboDialog.locator('[data-testid="combo-manual-model-add"]').click();
+ await comboDialog
+ .locator('[data-testid="combo-manual-model-input"]')
+ .fill("openrouter/openai/gpt-5.5");
+ await comboDialog.locator('[data-testid="combo-manual-model-add"]').click();
+ await expect(comboDialog.locator('[data-testid="combo-readiness-panel"]')).toHaveCount(0);
+
+ await comboDialog
+ .getByRole("button", { name: /create combo|criar combo/i })
+ .last()
+ .click();
+ await expect(comboDialog).toBeHidden();
+
+ expect(state.lastPayload?.models).toEqual([
+ {
+ kind: "model",
+ providerId: "codex",
+ model: "codex/gpt-5.5",
+ weight: 0,
+ },
+ {
+ kind: "model",
+ providerId: "openrouter",
+ model: "openrouter/openai/gpt-5.5",
+ weight: 0,
+ },
+ ]);
+ });
+
test("allows dragging combo cards to persist manual order", async ({ page }) => {
const state: {
combos: ComboStub[];
@@ -308,6 +523,14 @@ test.describe("Combos flow", () => {
});
});
+ await page.route(/\/api\/settings$/, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({ comboConfigMode: "guided" }),
+ });
+ });
+
await page.route("**/api/settings/proxy", async (route) => {
await route.fulfill({
status: 200,
diff --git a/tests/unit/chat-combo-live-test.test.ts b/tests/unit/chat-combo-live-test.test.ts
index 12f416d7c4..6710c17cb2 100644
--- a/tests/unit/chat-combo-live-test.test.ts
+++ b/tests/unit/chat-combo-live-test.test.ts
@@ -9,6 +9,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
+const settingsDb = await import("../../src/lib/db/settings.ts");
const chatRoute = await import("../../src/app/api/v1/chat/completions/route.ts");
const { generateSignature, invalidateBySignature, setCachedResponse } =
await import("../../src/lib/semanticCache.ts");
@@ -197,3 +198,33 @@ test("combo live test bypasses semantic cache and forces a fresh upstream reques
invalidateBySignature(signature);
}
});
+
+test("combo live test does not use cooldown-aware request retry on upstream failures", async () => {
+ await seedHealthyConnection();
+ await settingsDb.updateSettings({
+ requestRetry: 3,
+ maxRetryIntervalSec: 5,
+ });
+
+ let fetchCalls = 0;
+ globalThis.fetch = async () => {
+ fetchCalls += 1;
+ return Response.json(
+ {
+ error: {
+ message: "upstream unavailable",
+ },
+ },
+ { status: 503 }
+ );
+ };
+
+ const liveResponse = await chatRoute.POST(
+ makeRequest({ "X-Internal-Test": "combo-health-check" })
+ );
+ const liveBody = (await liveResponse.json()) as any;
+
+ assert.equal(liveResponse.status, 503);
+ assert.equal(fetchCalls, 1);
+ assert.match(liveBody.error.message, /upstream unavailable/i);
+});
diff --git a/tests/unit/combo-builder-draft.test.ts b/tests/unit/combo-builder-draft.test.ts
index 019f40f14e..8da7f18c0e 100644
--- a/tests/unit/combo-builder-draft.test.ts
+++ b/tests/unit/combo-builder-draft.test.ts
@@ -35,6 +35,47 @@ test("buildPrecisionComboModelStep preserves provider/model/account triple", ()
);
});
+test("buildManualComboModelStep resolves provider aliases and uses dynamic account", () => {
+ assert.deepEqual(
+ builderDraft.buildManualComboModelStep({
+ value: "cx/gpt-5.5",
+ providers: [{ providerId: "codex", alias: "cx" }],
+ }),
+ {
+ kind: "model",
+ providerId: "codex",
+ model: "codex/gpt-5.5",
+ weight: 0,
+ }
+ );
+
+ assert.deepEqual(
+ builderDraft.buildManualComboModelStep({
+ value: "openrouter/openai/gpt-5.5",
+ providers: [{ providerId: "openrouter", alias: "openrouter" }],
+ }),
+ {
+ kind: "model",
+ providerId: "openrouter",
+ model: "openrouter/openai/gpt-5.5",
+ weight: 0,
+ }
+ );
+
+ assert.equal(
+ builderDraft.resolveComboBuilderProviderId("foo", [{ providerId: "codex", alias: "cx" }]),
+ null
+ );
+ assert.equal(
+ builderDraft.buildManualComboModelStep({
+ value: "foo/bar",
+ providers: [{ providerId: "codex", alias: "cx" }],
+ }),
+ null
+ );
+ assert.equal(builderDraft.buildManualComboModelStep({ value: "gpt-5.5" }), null);
+});
+
test("hasExactModelStepDuplicate blocks only exact provider/model/connection repeats", () => {
const existing = [
builderDraft.buildPrecisionComboModelStep({
diff --git a/tests/unit/db-settings-crud.test.ts b/tests/unit/db-settings-crud.test.ts
index 0f6843120a..6c00895dba 100644
--- a/tests/unit/db-settings-crud.test.ts
+++ b/tests/unit/db-settings-crud.test.ts
@@ -69,6 +69,7 @@ test("getSettings exposes defaults and updateSettings persists typed values", as
assert.equal(defaults.requestRetry, 3);
assert.equal(defaults.maxRetryIntervalSec, 30);
assert.equal(defaults.antigravitySignatureCacheMode, "enabled");
+ assert.equal(defaults.comboConfigMode, "guided");
assert.equal(updated.requireLogin, false);
assert.equal(updated.cloudEnabled, true);
assert.equal(updated.stickyRoundRobinLimit, 7);
diff --git a/tests/unit/settings-schema-routing-strategies.test.ts b/tests/unit/settings-schema-routing-strategies.test.ts
index c433eabc44..170dc94a0c 100644
--- a/tests/unit/settings-schema-routing-strategies.test.ts
+++ b/tests/unit/settings-schema-routing-strategies.test.ts
@@ -38,3 +38,13 @@ test("settings schemas accept wsAuth toggle", () => {
assert.equal(routeParsed.wsAuth, true);
assert.equal(sharedParsed.wsAuth, false);
});
+
+test("settings schemas accept combo configuration modes", () => {
+ const routeParsed = settingsRouteSchema.parse({ comboConfigMode: "expert" });
+ const sharedParsed = sharedSettingsSchema.parse({ comboConfigMode: "guided" });
+
+ assert.equal(routeParsed.comboConfigMode, "expert");
+ assert.equal(sharedParsed.comboConfigMode, "guided");
+ assert.equal(settingsRouteSchema.safeParse({ comboConfigMode: "compact" }).success, false);
+ assert.equal(sharedSettingsSchema.safeParse({ comboConfigMode: "compact" }).success, false);
+});