From 56c1b666c60aad127b7d2c718e26a215f85b72df Mon Sep 17 00:00:00 2001 From: musistudio Date: Wed, 1 Jul 2026 17:55:48 +0800 Subject: [PATCH] Refactor router config and provider handling --- src/main/config.ts | 56 +- src/renderer/pages/home/App.tsx | 40 +- .../pages/home/components/network-logs.tsx | 68 +-- .../pages/home/components/providers.tsx | 217 +++++++- .../pages/home/components/routing.tsx | 135 +++-- src/renderer/pages/home/shared/i18n.tsx | 8 + src/renderer/pages/home/shared/index.tsx | 1 + src/renderer/pages/home/shared/providers.ts | 66 ++- src/renderer/pages/home/shared/routing.ts | 105 +++- src/renderer/pages/home/shared/types.ts | 7 + .../pages/home/shared/virtual-models.ts | 4 +- src/renderer/styles/globals.css | 32 +- .../gateway/claude-code-router-plugin.ts | 406 ++++++++++++++- src/server/gateway/service.ts | 65 +-- src/shared/app.ts | 17 +- src/shared/deep-link.ts | 42 ++ src/shared/default-config.ts | 9 +- tests/main/deep-link.test.mjs | 11 +- tests/main/router-builtins.test.mjs | 492 ++++++++++++++++++ tests/renderer/virtual-models.test.ts | 20 + 20 files changed, 1625 insertions(+), 176 deletions(-) create mode 100644 tests/main/router-builtins.test.mjs diff --git a/src/main/config.ts b/src/main/config.ts index 2e61d52..89a9c4d 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -36,6 +36,7 @@ import type { ProfileRuntimeConfig, ProxyRouteTarget, ProxyRuntimeConfig, + RouterBuiltInRulesConfig, RouterConfig, RouterFallbackConfig, RouterFallbackMode, @@ -965,6 +966,7 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined { const models = Array.isArray(item.models) ? item.models.map((model) => readString(model)).filter((model): model is string => Boolean(model)) : []; + const modelDescriptions = parseModelDescriptions(item.modelDescriptions ?? item.model_descriptions, models); const modelDisplayNames = parseModelDisplayNames(item.modelDisplayNames ?? item.model_display_names, models); if (!name) { @@ -986,6 +988,7 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined { extraHeaders: item.extraHeaders, icon: readString(item.icon), id: readString(item.id), + modelDescriptions, modelDisplayNames, models, name, @@ -1000,6 +1003,22 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined { return withProviderIds(providers); } +function parseModelDescriptions(value: unknown, models: string[]): Record | undefined { + if (!isObject(value)) { + return undefined; + } + + const modelIds = new Set(models); + const entries = Object.entries(value) + .map(([rawModel, rawDescription]) => [rawModel.trim(), readString(rawDescription)] as const) + .filter((entry): entry is [string, string] => { + const [model, description] = entry; + return Boolean(model && description && modelIds.has(model)); + }); + + return entries.length > 0 ? Object.fromEntries(entries) : undefined; +} + function parseModelDisplayNames(value: unknown, models: string[]): Record | undefined { if (!isObject(value)) { return undefined; @@ -1171,15 +1190,13 @@ function parseRouter(value: unknown): Partial | undefined { } const router: Partial = {}; - for (const key of ["background", "default"] as const) { - const route = readString(value[key]); - if (route) { - router[key] = route; - } + const defaultRoute = readString(value.default); + if (defaultRoute) { + router.default = defaultRoute; } - const threshold = readNumber(value.longContextThreshold); - if (threshold !== undefined && threshold > 0) { - router.longContextThreshold = threshold; + const builtInRules = parseRouterBuiltInRules(value.builtInRules ?? value.builtinRules ?? value.agentRules); + if (builtInRules) { + router.builtInRules = builtInRules; } const rules = parseRouterRules(value.rules); if (rules) { @@ -1194,6 +1211,29 @@ function parseRouter(value: unknown): Partial | undefined { return router; } +function parseRouterBuiltInRules(value: unknown): RouterBuiltInRulesConfig | undefined { + if (!isObject(value)) { + return undefined; + } + + return { + "claude-code": parseRouterBuiltInAgentRule(value["claude-code"] ?? value.claudeCode ?? value.claude), + codex: parseRouterBuiltInAgentRule(value.codex) + }; +} + +function parseRouterBuiltInAgentRule(value: unknown): { enabled: boolean } { + if (typeof value === "boolean") { + return { enabled: value }; + } + if (!isObject(value)) { + return { enabled: true }; + } + return { + enabled: typeof value.enabled === "boolean" ? value.enabled : true + }; +} + function parseRouterFallback(value: unknown): RouterFallbackConfig | undefined { if (!isObject(value)) { return undefined; diff --git a/src/renderer/pages/home/App.tsx b/src/renderer/pages/home/App.tsx index 633aea5..5298dbc 100644 --- a/src/renderer/pages/home/App.tsx +++ b/src/renderer/pages/home/App.tsx @@ -17,9 +17,9 @@ import { isCursorProxyPluginConfig, isMacPlatform, isPlainRecord, isProfileDraftSubmittable, isProviderNameDuplicate, isProviderProbeCandidateReady, isTraySupportedPlatform, isRoutingRewriteDraftRowValid, - LayoutGroup, mergeModelDisplayNames, mergeProviderCapabilities, mergeProviderModelLists, modelDisplayNamesForModels, + LayoutGroup, mergeModelDisplayNames, mergeProviderCapabilities, mergeProviderModelLists, modelDescriptionsForModels, modelDisplayNamesForModels, navigation, NavigationId, normalizeApiKeys, normalizeBotGatewaySavedConfigs, normalizeConfig, normalizeLanguagePreference, normalizeObservabilityConfig, normalizeOverviewWidgets, - normalizeProfileItem, normalizeProfileScope, normalizeProviderBaseUrl, normalizeRouterFallbackConfig, normalizeThemePreference, normalizeTrayBalanceProgressConfig, normalizeTrayIconPreference, + normalizeProfileItem, normalizeProfileScope, normalizeProviderBaseUrl, normalizeRouterBuiltInRules, normalizeRouterFallbackConfig, normalizeThemePreference, normalizeTrayBalanceProgressConfig, normalizeTrayIconPreference, normalizeTrayWidgets, normalizeTrayWindowModules, normalizeVirtualModelDraftPatch, numberValue, OnboardingReadinessOptions, OnboardingStepId, onboardingStepOrder, OverviewWidgetConfig, parsePluginAppsSettingsText, parsePluginConfigSettingsText, parseProviderAccountDraft, providerCredentialsFromDraft, @@ -1157,6 +1157,7 @@ function App() { return { ...next, + modelDescriptions: patch.modelDescriptions ?? current.modelDescriptions, modelDisplayNames: patch.modelDisplayNames, modelsText: mergeProviderModelLists(current.selectedModels, splitLines(next.modelsText)).join("\n"), selectedModels: [], @@ -1368,6 +1369,7 @@ function App() { } const protocolsToSave = selectedProtocols.length > 0 ? selectedProtocols : [fallbackProtocol]; + const modelDescriptions = modelDescriptionsForModels(providerDraft.modelDescriptions, models); const modelDisplayNames = modelDisplayNamesForModels(providerDraft.modelDisplayNames, models); const selectedProtocolSet = new Set(protocolsToSave); const capabilityCandidates = mergeProviderCapabilities( @@ -1436,6 +1438,7 @@ function App() { account: accountConfig, credentials: credentials.length > 0 ? credentials : undefined, icon: providerDraft.icon.trim() || undefined, + modelDescriptions, modelDisplayNames, models, name: providerName, @@ -1518,6 +1521,27 @@ function App() { return persistConfig(next, setActionError); } + function updateProviderModelDescription(providerIndex: number, model: string, description: string) { + const next = buildConfigUpdate((config) => { + const provider = config.Providers[providerIndex]; + const models = provider ? mergeProviderModelLists(provider.models) : []; + if (!provider || !models.includes(model)) { + return config; + } + const descriptions = { ...(provider.modelDescriptions ?? {}) }; + const trimmed = description.trim(); + if (trimmed) { + descriptions[model] = trimmed; + } else { + delete descriptions[model]; + } + provider.modelDescriptions = modelDescriptionsForModels(descriptions, models); + return config; + }); + setConfigDraft(next); + void persistConfig(next, setActionError); + } + async function confirmProviderDelete() { if (providerDeleteIndex === undefined) { return; @@ -2807,7 +2831,8 @@ function App() { updateFilter: updateRequestLogFilter }, models: { - config: draftConfig + config: draftConfig, + updateModelDescription: updateProviderModelDescription }, networking: { clearCaptures: () => void clearProxyNetworkCaptures(), @@ -2866,6 +2891,15 @@ function App() { moveRule: moveRoutingRule, providers: draftConfig.Providers, removeRule: setRoutingDeleteIndex, + updateBuiltInRule: (agent, enabled) => updateConfig((config) => { + config.Router.builtInRules = normalizeRouterBuiltInRules(config.Router.builtInRules); + if (agent === "claude-code") { + config.Router.builtInRules["claude-code"] = { enabled }; + } else { + config.Router.builtInRules.codex = { enabled }; + } + return config; + }), updateFallback: (fallback) => updateConfig((config) => { config.Router.fallback = normalizeRouterFallbackConfig(fallback); return config; diff --git a/src/renderer/pages/home/components/network-logs.tsx b/src/renderer/pages/home/components/network-logs.tsx index c92aa02..c209c4f 100644 --- a/src/renderer/pages/home/components/network-logs.tsx +++ b/src/renderer/pages/home/components/network-logs.tsx @@ -1063,41 +1063,43 @@ function LogJsonFullscreenViewer({
-
- {title} - {subtitle ? {subtitle} : null} - -
- -
- - - +
+
+ {title} + {subtitle ? {subtitle} : null} + +
+ +
+ + + +
); diff --git a/src/renderer/pages/home/components/providers.tsx b/src/renderer/pages/home/components/providers.tsx index 200701e..bde3e90 100644 --- a/src/renderer/pages/home/components/providers.tsx +++ b/src/renderer/pages/home/components/providers.tsx @@ -250,8 +250,22 @@ export function ProvidersView({ accountSnapshots, addProvider, editProvider, not ); } -export function ModelsView({ config }: { config: AppConfig }) { +export function ModelsView({ + config, + updateModelDescription +}: { + config: AppConfig; + updateModelDescription: (providerIndex: number, model: string, description: string) => void; +}) { const t = useAppText(); + const [descriptionDraft, setDescriptionDraft] = useState(""); + const [descriptionTarget, setDescriptionTarget] = useState<{ + description: string; + displayName?: string; + model: string; + providerIndex: number; + providerName?: string; + }>(); const [query, setQuery] = useState(""); const rows = useMemo(() => createModelCatalogItems(config), [config]); const normalizedQuery = query.trim().toLowerCase(); @@ -260,6 +274,34 @@ export function ModelsView({ config }: { config: AppConfig }) { [normalizedQuery, rows] ); + function openDescriptionDialog(row: (typeof rows)[number]) { + if (row.providerIndex === undefined) { + return; + } + const description = row.description ?? ""; + setDescriptionDraft(description); + setDescriptionTarget({ + description, + displayName: row.displayName, + model: row.model, + providerIndex: row.providerIndex, + providerName: row.providerName + }); + } + + function closeDescriptionDialog() { + setDescriptionDraft(""); + setDescriptionTarget(undefined); + } + + function saveDescriptionDialog() { + if (!descriptionTarget) { + return; + } + updateModelDescription(descriptionTarget.providerIndex, descriptionTarget.model, descriptionDraft); + closeDescriptionDialog(); + } + return ( 0 ? (
-
-
+
+
{t("Model")}
+
{t("Description")}
{visibleRows.map((row) => (
{row.displayName ?? row.model}
+
+ {row.providerName ? `${row.providerName}/${row.model}` : row.model} +
+ {row.providerName ? ( + + {row.providerName} + + ) : null} +
+
+ {row.providerIndex !== undefined ? ( +
+
+
+ {row.description || "-"} +
+
+ +
+ ) : ( +
+ {row.description || "-"} +
+ )}
))} @@ -322,10 +398,74 @@ export function ModelsView({ config }: { config: AppConfig }) { ) : null} + ); } +function ModelCatalogDescriptionDialog({ + draft, + onChange, + onClose, + onSave, + target +}: { + draft: string; + onChange: (value: string) => void; + onClose: () => void; + onSave: () => void; + target?: { + description: string; + displayName?: string; + model: string; + providerName?: string; + }; +}) { + const t = useAppText(); + const open = Boolean(target); + const title = target?.displayName || target?.model || t("Model"); + const subtitle = target?.providerName ? `${target.providerName}/${target.model}` : target?.model; + + return ( + { if (!nextOpen) onClose(); }}> + + + {t("Edit description")} + + +
+
{title}
+ {subtitle ?
{subtitle}
: null} +
+ +