feat(proxy): move proxy configuration to dedicated System → Proxy page (#1907)

Integrated into release/v3.7.9
This commit is contained in:
Paijo
2026-05-04 02:16:18 +07:00
committed by GitHub
parent 879eda9379
commit 53fd36e4f2
13 changed files with 123 additions and 30 deletions

View File

@@ -538,9 +538,7 @@ export const setBudgetGuardTool: McpToolDefinition<
// --- Tool 11: omniroute_set_routing_strategy ---
export const setRoutingStrategyInput = z.object({
comboId: z.string().describe("Combo ID or name to update"),
strategy: z
.enum(ROUTING_STRATEGY_VALUES)
.describe("Routing strategy to apply"),
strategy: z.enum(ROUTING_STRATEGY_VALUES).describe("Routing strategy to apply"),
autoRoutingStrategy: z
.enum(AUTO_ROUTING_STRATEGY_VALUES)
.optional()

View File

@@ -457,7 +457,9 @@ export async function handleSetRoutingStrategy(args: {
name: toString(updatedCombo.name, toString(combo.name, comboId)),
strategy: toString(updatedCombo.strategy, normalizedStrategy),
autoRoutingStrategy:
toString(updatedCombo.strategy, normalizedStrategy) === "auto" ? resolvedAutoStrategy : null,
toString(updatedCombo.strategy, normalizedStrategy) === "auto"
? resolvedAutoStrategy
: null,
},
};

View File

@@ -15,8 +15,4 @@ export {
export { getTaskFitness, getTaskTypes } from "./taskFitness";
export { SelfHealingManager, getSelfHealingManager } from "./selfHealing";
export { MODE_PACKS, getModePack, getModePackNames } from "./modePacks";
export {
selectProvider,
type AutoComboConfig,
type SelectionResult,
} from "./engine";
export { selectProvider, type AutoComboConfig, type SelectionResult } from "./engine";

View File

@@ -665,14 +665,18 @@ function sortTargetsByContextSize(targets: ResolvedComboTarget[]) {
.filter((target): target is ResolvedComboTarget => target !== null);
}
function getP2CTargetScore(target: ResolvedComboTarget, metrics: ReturnType<typeof getComboMetrics>): number {
function getP2CTargetScore(
target: ResolvedComboTarget,
metrics: ReturnType<typeof getComboMetrics>
): number {
const breakerState = getCircuitBreaker(target.provider)?.getStatus?.()?.state;
if (breakerState === "OPEN") return -Infinity;
const modelMetric = metrics?.byModel?.[target.modelStr] || null;
const successRate = Number(modelMetric?.successRate);
const avgLatency = Number(modelMetric?.avgLatencyMs);
const successScore = Number.isFinite(successRate) ? successRate / 100 : 0.5;
const latencyScore = Number.isFinite(avgLatency) && avgLatency > 0 ? 1 / Math.log10(avgLatency + 10) : 0.25;
const latencyScore =
Number.isFinite(avgLatency) && avgLatency > 0 ? 1 / Math.log10(avgLatency + 10) : 0.25;
const breakerPenalty = breakerState === "HALF_OPEN" ? 0.25 : 0;
return successScore + latencyScore - breakerPenalty;
}
@@ -1465,7 +1469,10 @@ export async function handleComboChat({
orderedTargets = fisherYatesShuffle([...orderedTargets]);
log.info("COMBO", `Random shuffle: ${orderedTargets.length} targets`);
} else if (strategy === "fill-first") {
log.info("COMBO", `Fill-first ordering: preserving priority order (${orderedTargets.length} targets)`);
log.info(
"COMBO",
`Fill-first ordering: preserving priority order (${orderedTargets.length} targets)`
);
} else if (strategy === "p2c") {
orderedTargets = orderTargetsByPowerOfTwoChoices(orderedTargets, combo.name);
log.info("COMBO", `Power-of-two-choices ordering: selected ${orderedTargets[0]?.modelStr}`);

View File

@@ -1,5 +1,10 @@
import { redirect } from "next/navigation";
export const metadata = {
title: "Compression",
description: "Configure context compression settings to reduce token usage and costs.",
};
export default function CompressionPage() {
redirect("/dashboard/context/caveman");
}

View File

@@ -104,7 +104,8 @@ export default function ComboDefaultsTab() {
setComboDefaults((prev) => ({
...prev,
...sanitizeComboRuntimeConfig(comboData.comboDefaults),
strategy: comboData.comboDefaults?.strategy ?? settingsData.fallbackStrategy ?? prev.strategy,
strategy:
comboData.comboDefaults?.strategy ?? settingsData.fallbackStrategy ?? prev.strategy,
stickyRoundRobinLimit:
settingsData.stickyRoundRobinLimit ??
comboData.comboDefaults?.stickyRoundRobinLimit ??

View File

@@ -10,7 +10,6 @@ import SystemStorageTab from "./components/SystemStorageTab";
import SecurityTab from "./components/SecurityTab";
import RoutingTab from "./components/RoutingTab";
import ComboDefaultsTab from "./components/ComboDefaultsTab";
import ProxyTab from "./components/ProxyTab";
import AppearanceTab from "./components/AppearanceTab";
import ThinkingBudgetTab from "./components/ThinkingBudgetTab";
import SystemPromptTab from "./components/SystemPromptTab";
@@ -23,7 +22,6 @@ import ResilienceTab from "./components/ResilienceTab";
import CliproxyapiSettingsTab from "./components/CliproxyapiSettingsTab";
import PayloadRulesTab from "./components/PayloadRulesTab";
import VisionBridgeSettingsTab from "./components/VisionBridgeSettingsTab";
import MitmProxyTab from "./components/MitmProxyTab";
import ModelRoutingSection from "@/shared/components/ModelRoutingSection";
const tabs = [
@@ -33,7 +31,6 @@ const tabs = [
{ id: "security", labelKey: "security", icon: "shield" },
{ id: "routing", labelKey: "routing", icon: "route" },
{ id: "resilience", labelKey: "resilience", icon: "electrical_services" },
{ id: "mitm", labelKey: "mitmProxy", icon: "lan" },
{ id: "advanced", labelKey: "advanced", icon: "tune" },
];
@@ -138,12 +135,9 @@ export default function SettingsPage() {
{activeTab === "resilience" && <ResilienceTab />}
{activeTab === "mitm" && <MitmProxyTab />}
{activeTab === "advanced" && (
<div className="flex flex-col gap-6">
<PayloadRulesTab />
<ProxyTab />
<CliproxyapiSettingsTab />
</div>
)}

View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { cn } from "@/shared/utils/cn";
import ProxyTab from "@/app/(dashboard)/dashboard/settings/components/ProxyTab";
import MitmProxyTab from "@/app/(dashboard)/dashboard/settings/components/MitmProxyTab";
import OneproxyTab from "@/app/(dashboard)/dashboard/settings/components/OneproxyTab";
const subTabs = [
{ id: "http", labelKey: "httpProxy", icon: "dns" },
{ id: "mitm", labelKey: "mitmProxy", icon: "lan" },
{ id: "oneproxy", labelKey: "1proxy", icon: "public" },
];
export default function ProxyPage() {
const t = useTranslations("settings");
const [activeSubTab, setActiveSubTab] = useState("http");
return (
<div className="max-w-6xl mx-auto min-w-0">
<div className="flex flex-col gap-6">
<div className="w-full overflow-x-auto pb-1">
<div
role="tablist"
aria-label={t("proxySubTabsAria")}
className="inline-flex items-center p-1 rounded-lg bg-black/5 dark:bg-white/5 min-w-max"
>
{subTabs.map((subTab) => (
<button
key={subTab.id}
role="tab"
aria-selected={activeSubTab === subTab.id}
tabIndex={activeSubTab === subTab.id ? 0 : -1}
onClick={() => setActiveSubTab(subTab.id)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all text-sm",
activeSubTab === subTab.id
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
: "text-text-muted hover:text-text-main"
)}
>
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
{subTab.icon}
</span>
<span className="whitespace-nowrap">
{subTab.id === "http" ? t("proxy") : t(subTab.labelKey)}
</span>
</button>
))}
</div>
</div>
<div
role="tabpanel"
aria-label={t(subTabs.find((t) => t.id === activeSubTab)?.labelKey || "proxy")}
>
{activeSubTab === "http" && (
<div className="flex flex-col gap-6">
<ProxyTab />
</div>
)}
{activeSubTab === "mitm" && (
<div className="flex flex-col gap-6">
<MitmProxyTab />
</div>
)}
{activeSubTab === "oneproxy" && (
<div className="flex flex-col gap-6">
<OneproxyTab />
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -341,9 +341,6 @@ export {
setReasoningCache,
getReasoningCache,
deleteReasoningCache,
cleanupExpiredReasoning,
getReasoningCacheStats,
getReasoningCacheEntries,
clearAllReasoningCache,
} from "./db/reasoningCache";

View File

@@ -24,6 +24,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
"audit",
"webhooks",
"health",
"proxy",
"settings",
"docs",
"issues",
@@ -109,6 +110,7 @@ const SYSTEM_SIDEBAR_ITEMS: readonly SidebarItemDefinition[] = [
{ id: "audit", href: "/dashboard/audit", i18nKey: "auditLog", icon: "policy" },
{ id: "webhooks", href: "/dashboard/webhooks", i18nKey: "webhooks", icon: "webhook" },
{ id: "health", href: "/dashboard/health", i18nKey: "health", icon: "health_and_safety" },
{ id: "proxy", href: "/dashboard/system/proxy", i18nKey: "proxy", icon: "dns" },
{ id: "settings", href: "/dashboard/settings", i18nKey: "settings", icon: "settings" },
];

View File

@@ -39,9 +39,7 @@ export const comboSchema = z.object({
name: z.string().min(1, "Combo name is required").max(100),
model: z.string().min(1, "Model pattern is required"),
endpoint: z.enum(["chat", "embeddings", "images"]).default("chat"),
strategy: z
.enum(ROUTING_STRATEGY_VALUES)
.default("priority"),
strategy: z.enum(ROUTING_STRATEGY_VALUES).default("priority"),
nodes: z.array(comboNodeSchema).min(1, "At least one node is required"),
isActive: z.boolean().default(true),
maxRetries: z.number().int().min(0).max(10).default(2),

View File

@@ -138,7 +138,10 @@ test("auto strategy handles null and empty prompt edge cases without throwing",
});
assert.equal(
await selectedModelFor(combo, { model: combo.name, messages: [{ role: "user", content: null }] }),
await selectedModelFor(combo, {
model: combo.name,
messages: [{ role: "user", content: null }],
}),
"openai/gpt-4"
);
assert.equal(await selectedModelFor(combo, { model: combo.name, messages: [] }), "openai/gpt-4");

View File

@@ -527,12 +527,23 @@ test("CodexExecutor.transformRequest strips raw internal assistant commentary wi
true
);
assert.equal(
result.input.some((item) => JSON.stringify(item).includes("Visible assistant history without phase")),
result.input.some((item) =>
JSON.stringify(item).includes("Visible assistant history without phase")
),
true
);
assert.equal(
result.input.some((item) => item.type === "reasoning"),
true
);
assert.equal(
result.input.some((item) => item.type === "function_call"),
true
);
assert.equal(
result.input.some((item) => item.type === "function_call_output"),
true
);
assert.equal(result.input.some((item) => item.type === "reasoning"), true);
assert.equal(result.input.some((item) => item.type === "function_call"), true);
assert.equal(result.input.some((item) => item.type === "function_call_output"), true);
});
test("CodexExecutor.transformRequest strips internal assistant commentary before mapping messages to input", () => {