feat: add expert combo configuration mode (#1547)

This commit is contained in:
Randi
2026-04-24 08:04:10 -04:00
committed by GitHub
parent a972293151
commit 4f6f97b369
17 changed files with 951 additions and 303 deletions

View File

@@ -244,7 +244,7 @@ Developers pay $20200/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

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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() {
</div>
</div>
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">
{getSettingsLabel("comboConfigMode", "Combo configuration mode")}
</p>
<p className="text-sm text-text-muted">
{getSettingsLabel(
"comboConfigModeDesc",
"Choose how the combo create and edit dialog is organized."
)}
</p>
</div>
<div
role="radiogroup"
aria-label={getSettingsLabel("comboConfigMode", "Combo configuration mode")}
className="grid grid-cols-1 sm:grid-cols-2 gap-2"
>
{comboConfigModeOptions.map((option) => {
const active = comboConfigMode === option.id;
return (
<button
key={option.id}
type="button"
role="radio"
aria-checked={active}
disabled={loading}
onClick={() => 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"
)}
>
<span className="material-symbols-outlined mt-0.5 text-[20px]" aria-hidden="true">
{option.icon}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold">{option.title}</span>
<span
className={cn(
"mt-0.5 block text-xs",
active ? "text-primary/80" : "text-text-muted"
)}
>
{option.description}
</span>
</span>
</button>
);
})}
</div>
</div>
<div className="pt-4 border-t border-border">
<div className="mb-3">
<p className="font-medium">{t("sidebarVisibilityToggle")}</p>

View File

@@ -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<string, unknown>) => {
@@ -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");
});
});

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -54,6 +54,7 @@ export async function getSettings() {
antigravitySignatureCacheMode: "enabled",
requireLogin: true,
hiddenSidebarItems: [],
comboConfigMode: "guided",
alwaysPreserveClientCache: "auto",
idempotencyWindowMs: 5000,
wsAuth: false,

View File

@@ -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";
}

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
});

View File

@@ -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({

View File

@@ -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);

View File

@@ -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);
});