Remove default routing fallback from config and docs

This commit is contained in:
musi 2026-07-01 22:27:16 +08:00
parent 22dad1afb5
commit 513fa11ead
21 changed files with 127 additions and 82 deletions

View file

@ -62,7 +62,7 @@ Open **Providers**, click **Add Provider**, then choose a built-in preset or **O
### 2. Configure routing
Open **Routing** to choose the default route, add conditional rules, configure request rewrites, and set fallback behavior.
Open **Routing** to add conditional rules, configure request rewrites, and set fallback behavior.
Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets.

View file

@ -24,7 +24,7 @@ Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 C
- **概览仪表盘**:查看系统状态、用量组件、账号余额、模型分布和分享卡片。
- **Provider 管理**:添加预设或自定义端点,探测协议支持,检测模型连通性,管理凭据,并在可用时查看账号余额。
- **路由规则**:配置默认路由、条件路由、模型前缀规则、失败降级和请求改写。
- **路由规则**:配置条件路由、模型前缀规则、失败降级和请求改写。
- **Agent配置**:为 Claude Code、Codex 和 ZCode 配置启动入口、模型、作用范围和多开 App 配置。
- **网关兼容层**:通过本地 CCR 模型网关转换支持的客户端请求。
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
@ -61,7 +61,7 @@ CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序
### 2. 设置路由
打开 **路由**选择默认路由,添加条件规则,配置请求改写和失败降级。
打开 **路由**,添加条件规则,配置请求改写和失败降级。
如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。

View file

@ -9,9 +9,9 @@ lead: Choose the model for a request, then automatically retry or switch to fall
### Claude Code
The built-in Claude Code route detects requests from Claude Code and routes main requests to the Claude Code Agent Config model or the default route model.
The built-in Claude Code route detects requests from Claude Code and routes main requests to the Claude Code Agent Config model.
Claude Code **main requests** use the Claude Code Agent Config model by default; if it is unset, they use the Routing page default model. CCR also automatically removes the first `x-anthropic-billing-header` system message injected by Claude Code so that billing helper messages do not affect later routing decisions. Claude Code Subagent, Task, and Workflow-created agents can still choose different models through the tag mechanism below.
Claude Code **main requests** use the Claude Code Agent Config model. If that model is unset, the built-in route remains inactive. CCR also automatically removes the first `x-anthropic-billing-header` system message injected by Claude Code so that billing helper messages do not affect later routing decisions. Claude Code Subagent, Task, and Workflow-created agents can still choose different models through the tag mechanism below.
#### Subagent / Workflow Auto-Routing
@ -176,7 +176,7 @@ Configure **Default on failure** at the top of the Routing page:
2. If you choose **Retry**, set `Retries`.
3. If you choose **Fallback targets**, add backup models in priority order.
Global Fallback applies to the default route and to rules that do not define their own Fallback.
Global Fallback applies to routing rules that do not define their own Fallback.
### Rule-Level Fallback

View file

@ -16,7 +16,7 @@ lead: 深入配置概览仪表盘、API 密钥、服务、供应商、路由、A
| 服务配置 | Host、Port、代理模式、系统代理、网络捕获和 CA 证书 |
| 供应商配置 | 上游服务、协议、基础 URL、模型列表和凭据 |
| 一键导入供应商 | Provider deeplink 协议、Manifest 导入、一键导入按钮和安全边界 |
| 路由配置 | 默认路由、条件规则、fallback 和请求改写 |
| 路由配置 | 条件规则、fallback 和请求改写 |
| 日志&观测 | 请求日志、Agent 执行追踪、工具调用和工具结果 |
| Fusion 组合模型 | 把基础模型与视觉、搜索、MCP 工具组合成新的可选模型 |
| Agent配置 | Agent 启动方式、模型、作用范围、多开和 Bot 绑定 |

View file

@ -9,9 +9,9 @@ lead: 设置请求如何选择模型,并在失败时通过 Fallback 自动重
### Claude Code
Claude Code 内置路由的作用是识别 Claude Code 发来的请求,并把主请求路由到 Claude Code Agent 配置或默认路由中的模型。
Claude Code 内置路由的作用是识别 Claude Code 发来的请求,并把主请求路由到 Claude Code Agent 配置中的模型。
Claude Code **主请求** 默认使用 Claude Code Agent 配置中的模型;如果未设置,则使用路由页的默认模型。CCR 也会自动删除 Claude Code 注入的第一条 `x-anthropic-billing-header` system 消息避免这类计费辅助消息影响后续路由判断。Claude Code 创建的 Subagent、Task 或 Workflow 内部 Agent 可以继续用下面的标签机制自动选择不同模型。
Claude Code **主请求** 使用 Claude Code Agent 配置中的模型;如果未设置,该内置路由不会生效。CCR 也会自动删除 Claude Code 注入的第一条 `x-anthropic-billing-header` system 消息避免这类计费辅助消息影响后续路由判断。Claude Code 创建的 Subagent、Task 或 Workflow 内部 Agent 可以继续用下面的标签机制自动选择不同模型。
#### Subagent / Workflow 自动路由
@ -176,7 +176,7 @@ Fallback 处理请求失败后的降级。第一次选模型由路由完成;
2. 如果选择 **继续重试**,填写 `Retries`
3. 如果选择 **失败降级目标**,按优先级添加备用模型。
全局 Fallback 会应用到默认路由,以及没有单独配置 Fallback 的规则。
全局 Fallback 会应用到没有单独配置 Fallback 的规则。
### 规则级失败降级

View file

@ -13,7 +13,7 @@ A: 服务运行状态、Agent 启动方式、配置应用状态和作用范围
### Q: 请求命中了错误模型怎么办?
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含默认路由、规则顺序、匹配条件和 fallback。
A: 请求日志会展示 `request model``resolved provider``resolved model`。路由配置页包含规则顺序、匹配条件和 fallback。
### Q: 供应商返回 401 或 403 怎么处理?

View file

@ -1190,10 +1190,6 @@ function parseRouter(value: unknown): Partial<RouterConfig> | undefined {
}
const router: Partial<RouterConfig> = {};
const defaultRoute = readString(value.default);
if (defaultRoute) {
router.default = defaultRoute;
}
const builtInRules = parseRouterBuiltInRules(value.builtInRules ?? value.builtinRules ?? value.agentRules);
if (builtInRules) {
router.builtInRules = builtInRules;

View file

@ -514,11 +514,7 @@ function profileGatewayConfigWithToken(config: AppConfig, profile: ReturnType<ty
key: token,
name: `Profile: ${profile.name?.trim() || profile.id || profile.agent}`
}
],
Router: {
...config.Router,
...(profile.model.trim() ? { default: profile.model.trim() } : {})
}
]
};
}

View file

@ -1304,10 +1304,6 @@ function gatewayEndpoint(config: AppConfig): string {
}
function defaultClientModel(config: AppConfig): string {
const configuredDefault = normalizeClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

View file

@ -282,10 +282,6 @@ function gatewayEndpoint(config: AppConfig): string {
}
function defaultClientModel(config: AppConfig): string {
const configuredDefault = normalizeClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

View file

@ -6,8 +6,8 @@ export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputEle
}
const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
({ checked = false, className, disabled, onChange, onCheckedChange, ...props }, ref) => (
<span className={cn("relative inline-flex h-[30px] w-[54px] shrink-0", className)}>
({ checked = false, className, disabled, onChange, onCheckedChange, title, ...props }, ref) => (
<span className={cn("relative inline-flex h-[30px] w-[54px] shrink-0", className)} title={title}>
<input
aria-checked={checked}
checked={checked}
@ -19,6 +19,7 @@ const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
}}
ref={ref}
role="switch"
title={title}
type="checkbox"
{...props}
/>

View file

@ -4,11 +4,11 @@ import {
CardHeader, Check, CircleAlert, clampNumber, cn, createRouteModelOptions, createRoutingRewriteDraftRow,
Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle,
disclosureSpringTransition, Field, formatRouterRuleCondition, formatRouterRuleTarget, GatewayProviderConfig, Input,
Info, motion, normalizeRouterFallbackConfig, Pencil, Plus, Route, RouterFallbackConfig,
AppI18nContext, appCopy, ExternalLink, Info, motion, normalizeRouterFallbackConfig, Pencil, Plus, Route, RouterFallbackConfig,
RouterBuiltInAgentRuleId, RouterFallbackMode, routerConditionSourceOptions, routerFallbackModeOptions, RouterRule, routerRewriteOperationOptions, routerRuleOperatorOptions,
RouterBuiltInAgentRuleConfig,
RouteTargetControl, routingRuleRowMatchesQuery, Search, SelectControl, Toggle, translateOptions,
Trash2, uniqueStrings, useAppText, useMemo, useState, X
Trash2, uniqueStrings, useAppText, useContext, useMemo, useState, X
} from "../shared";
import { ROUTER_FALLBACK_MAX_RETRY_COUNT } from "../../../../shared/app";
export function RoutingView({
@ -100,6 +100,7 @@ export function RoutingView({
{visibleRules.map((row) => {
const rowSourceLabel = row.builtInAgent ? t(row.sourceLabel) : row.sourceLabel;
const rowTarget = row.target === "Profile model unset" ? t(row.target) : row.target;
const toggleDisabledReason = row.toggleDisabledReason ? t(row.toggleDisabledReason) : undefined;
return (
<AnimatedListItem
className="grid min-h-[58px] grid-cols-[minmax(160px,0.8fr)_minmax(220px,1fr)_minmax(240px,1.15fr)_84px_148px] items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/35"
@ -108,7 +109,7 @@ export function RoutingView({
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<div className="truncate text-[12px] font-semibold">{row.name || t("Unnamed")}</div>
{row.builtInAgent ? <BuiltInRouteInfoIcon description={builtInRouteDescription(row.builtInAgent, t)} /> : null}
{row.builtInAgent ? <BuiltInRouteInfoIcon agent={row.builtInAgent} /> : null}
{row.builtInAgent ? <Badge variant="outline">{t("Built-in")}</Badge> : row.readonly ? <Badge variant="outline">{t("Plugin")}</Badge> : null}
</div>
<div className="mt-0.5 truncate font-mono text-[11px] text-muted-foreground" title={`${rowSourceLabel}: ${row.ruleId}`}>
@ -129,17 +130,28 @@ export function RoutingView({
{row.builtInAgent ? null : rowTarget}
</div>
<div className="flex min-w-0 items-center gap-2">
<Toggle
checked={row.enabled}
disabled={row.readonly || row.toggleDisabled}
onChange={(enabled) => {
if (row.builtInAgent) {
updateBuiltInRule(row.builtInAgent, { enabled });
} else if (row.index !== undefined) {
updateRule(row.index, { enabled });
}
}}
/>
<span
aria-label={toggleDisabledReason}
className="group relative inline-flex rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
tabIndex={toggleDisabledReason ? 0 : undefined}
>
<Toggle
checked={row.enabled}
disabled={row.readonly || row.toggleDisabled}
onChange={(enabled) => {
if (row.builtInAgent) {
updateBuiltInRule(row.builtInAgent, { enabled });
} else if (row.index !== undefined) {
updateRule(row.index, { enabled });
}
}}
/>
{toggleDisabledReason ? (
<span className="pointer-events-none absolute right-full top-1/2 z-[80] mr-2 hidden w-[240px] -translate-y-1/2 rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card group-hover:block group-focus:block group-focus-within:block">
{toggleDisabledReason}
</span>
) : null}
</span>
</div>
<div className="flex items-center justify-end gap-1">
{!row.builtInAgent ? (
@ -185,22 +197,55 @@ export function RoutingView({
);
}
function BuiltInRouteInfoIcon({ description }: { description: string }) {
function BuiltInRouteInfoIcon({ agent }: { agent: RouterBuiltInAgentRuleId }) {
const t = useAppText();
const copy = useContext(AppI18nContext);
const description = builtInRouteDescription(agent, t);
const docsUrl = builtInRouteDocsUrl(agent, copy === appCopy.zh ? "zh" : "en");
return (
<span aria-label={description} className="group relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30" tabIndex={0} title={description}>
<span aria-label={description} className="group relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30" tabIndex={0}>
<Info className="h-3.5 w-3.5" aria-hidden="true" />
<span className="pointer-events-none absolute left-full top-1/2 z-[80] ml-2 hidden w-[260px] -translate-y-1/2 rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card group-hover:block group-focus:block">
{description}
<span className="absolute left-full top-1/2 z-[80] hidden w-[232px] -translate-y-1/2 pl-2 group-hover:block group-focus:block group-focus-within:block">
<span className="block rounded-md border border-border bg-popover px-2.5 py-2 text-left text-[11px] font-medium leading-4 text-popover-foreground shadow-card">
<span>{description}</span>
<a
className="ml-1 inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
href={docsUrl}
onClick={(event) => {
event.preventDefault();
openExternalUrl(docsUrl);
}}
rel="noreferrer"
target="_blank"
>
{t("Docs")}
<ExternalLink className="h-3 w-3" />
</a>
</span>
</span>
</span>
);
}
function builtInRouteDescription(agent: RouterBuiltInAgentRuleId, t: (value: string) => string): string {
if (agent === "claude-code") {
return t("Routes Claude Code requests by matching the Claude user-agent and setting request.body.model to the Claude Code profile or default model.");
return agent === "claude-code"
? t("Identifies the Claude Code user-agent to provide deep Claude Code integration.")
: t("Identifies the Codex user-agent to provide deep Codex integration.");
}
function builtInRouteDocsUrl(agent: RouterBuiltInAgentRuleId, language: "en" | "zh"): string {
const path = language === "zh" ? "/configuration/routing" : "/en/configuration/routing";
const hash = agent === "claude-code" ? "claude-code" : "codex";
return `https://ccrdesk.top${path}#${hash}`;
}
function openExternalUrl(url: string) {
if (window.ccr?.openExternal) {
void window.ccr.openExternal(url).catch(() => undefined);
return;
}
return t("Routes Codex requests by matching the Codex user-agent and setting request.body.model to the Codex profile or default model.");
window.open(url, "_blank", "noopener,noreferrer");
}
function RouterFallbackControl({

View file

@ -529,8 +529,8 @@ export function KeyValueRowsControl({
);
}
export function Toggle({ checked, disabled = false, onChange }: { checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void }) {
return <Switch checked={checked} disabled={disabled} onCheckedChange={onChange} />;
export function Toggle({ checked, disabled = false, onChange, title }: { checked: boolean; disabled?: boolean; onChange: (checked: boolean) => void; title?: string }) {
return <Switch checked={checked} disabled={disabled} onCheckedChange={onChange} title={title} />;
}
export type MetricTone = "amber" | "blue" | "indigo" | "rose" | "slate" | "teal";

View file

@ -644,8 +644,8 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Check trust": "检查信任",
"Choose where each agent uses CCR.": "选择每个 Agent 在哪里使用 CCR。",
"Built-in": "内置",
"Routes Claude Code requests by matching the Claude user-agent and setting request.body.model to the Claude Code profile or default model.": "通过匹配 Claude user-agent 路由 Claude Code 请求,并将 request.body.model 设置为 Claude Code 配置档案模型或默认模型。",
"Routes Codex requests by matching the Codex user-agent and setting request.body.model to the Codex profile or default model.": "通过匹配 Codex user-agent 路由 Codex 请求,并将 request.body.model 设置为 Codex 配置档案模型或默认模型。",
"Identifies the Claude Code user-agent to provide deep Claude Code integration.": "通过识别 Claude Code 的 user-agent实现对 Claude Code 的深度适配。",
"Identifies the Codex user-agent to provide deep Codex integration.": "通过识别 Codex 的 user-agent实现对 Codex 的深度适配。",
"Click Check Connection to verify connectivity with a real model request.": "点击检测连通性,用一次真实模型请求验证是否可用。",
"Click Add to create one": "点击添加创建一项",
"Click Install to add one": "点击安装添加一项",
@ -859,6 +859,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"None": "无",
"Not configured": "未配置",
"Not running": "未运行",
"Agent profiles are disabled.": "Agent 配置档案已关闭。",
"Open": "打开",
"Open Agent": "打开Agent",
"Open CA": "打开 CA",
@ -883,6 +884,10 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Phone Wi-Fi target": "手机 Wi-Fi 目标",
"Plugin": "插件",
"Profile model unset": "未设置配置档案模型",
"Enable a Claude Code profile before enabling this built-in route.": "请先启用一个 Claude Code 配置档案,再启用此内置路由。",
"Enable a Codex profile before enabling this built-in route.": "请先启用一个 Codex 配置档案,再启用此内置路由。",
"Set a model on the Claude Code profile before enabling this built-in route.": "请先为 Claude Code 配置档案设置模型,再启用此内置路由。",
"Set a model on the Codex profile before enabling this built-in route.": "请先为 Codex 配置档案设置模型,再启用此内置路由。",
"Plugin apps must be a JSON array.": "插件 App 必须是 JSON 数组。",
"Plugin config JSON": "插件配置 JSON",
"Plugin config must be a JSON object.": "插件配置必须是 JSON 对象。",
@ -1354,6 +1359,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Detecting icon": "正在检测图标",
"Detecting provider": "正在检测供应商",
"Disabled": "已禁用",
"Docs": "文档",
"Edit description": "编辑描述",
"Expand": "展开",
"Expand models": "展开模型",

View file

@ -389,10 +389,6 @@ export function gatewayEndpointFromConfig(config: AppConfig): string {
}
export function defaultProfileClientModel(config: AppConfig): string {
const configuredDefault = normalizeProfileClientModel(config.Router.default);
if (configuredDefault) {
return configuredDefault;
}
const preferred = config.Providers.find((provider) => provider.name === config.preferredProvider) ?? config.Providers[0];
if (preferred?.name && preferred.models[0]) {
return `${preferred.name}/${preferred.models[0]}`;

View file

@ -386,10 +386,11 @@ export function normalizeRouterConfig(value: Partial<RouterConfig> | undefined):
const router = {
...fallbackConfig.Router,
...(value || {})
};
} as RouterConfig & { default?: unknown };
const { default: _legacyDefault, ...routerWithoutLegacyDefault } = router;
const rules = normalizeRouterRules((value as Record<string, unknown> | undefined)?.rules) ?? [];
return {
...router,
...routerWithoutLegacyDefault,
builtInRules: normalizeRouterBuiltInRules((value as Record<string, unknown> | undefined)?.builtInRules),
fallback: normalizeRouterFallbackConfig((value as Record<string, unknown> | undefined)?.fallback),
rules
@ -847,7 +848,7 @@ export function buildRoutingRuleRows(config: AppConfig): RoutingRuleRow[] {
export function buildBuiltInAgentRoutingRows(config: AppConfig): RoutingRuleRow[] {
return routerBuiltInAgentRuleIds.map((agent): RoutingRuleRow => {
const target = routerBuiltInAgentRouteTarget(config, agent);
const profileEnabled = Boolean(routerBuiltInAgentProfile(config, agent));
const toggleDisabledReason = routerBuiltInAgentRuleDisabledReason(config, agent);
return {
builtInAgent: agent,
condition: `request.header.user-agent contains ${routerBuiltInAgentUserAgentNeedle(agent)}`,
@ -859,7 +860,8 @@ export function buildBuiltInAgentRoutingRows(config: AppConfig): RoutingRuleRow[
ruleId: `builtin-agent-${agent}`,
sourceLabel: "Built-in",
target: target ? `set request.body.model = ${target}` : "Profile model unset",
toggleDisabled: !profileEnabled || !target,
toggleDisabled: Boolean(toggleDisabledReason),
toggleDisabledReason,
typeLabel: "Condition"
};
});
@ -885,7 +887,22 @@ export function routerBuiltInAgentProfile(config: AppConfig, agent: RouterBuiltI
}
export function routerBuiltInAgentRouteTarget(config: AppConfig, agent: RouterBuiltInAgentRuleId): string {
return routerBuiltInAgentProfile(config, agent)?.model.trim() || config.Router.default?.trim() || "";
return routerBuiltInAgentProfile(config, agent)?.model.trim() || "";
}
export function routerBuiltInAgentRuleDisabledReason(config: AppConfig, agent: RouterBuiltInAgentRuleId): string | undefined {
if (config.profile.enabled === false) {
return "Agent profiles are disabled.";
}
const agentName = routerBuiltInAgentRuleName(agent);
const profile = routerBuiltInAgentProfile(config, agent);
if (!profile) {
return `Enable a ${agentName} profile before enabling this built-in route.`;
}
if (!profile.model.trim()) {
return `Set a model on the ${agentName} profile before enabling this built-in route.`;
}
return undefined;
}
export function routerBuiltInAgentRuleName(agent: RouterBuiltInAgentRuleId): string {

View file

@ -733,6 +733,7 @@ export type RoutingRuleRow = {
sourceLabel: string;
target: string;
toggleDisabled?: boolean;
toggleDisabledReason?: string;
typeLabel: string;
};

View file

@ -210,7 +210,7 @@ function resolveConfiguredRouteDecision(
}
}
return { fallback: router.fallback, model: normalizeRouteSelector(router.default) ?? explicitModel, reason: "default" };
return { fallback: router.fallback, model: explicitModel, reason: "default" };
}
function resolveBuiltInClaudeCodeSubagentRouteDecision(
@ -287,8 +287,7 @@ function resolveBuiltInAgentProfile(config: AppConfig, agent: RouterBuiltInAgent
}
function resolveBuiltInAgentRouteTarget(config: AppConfig, agent: RouterBuiltInAgentRuleId): string | undefined {
return normalizeRouteSelector(resolveBuiltInAgentProfile(config, agent)?.model) ??
normalizeRouteSelector(config.Router.default);
return normalizeRouteSelector(resolveBuiltInAgentProfile(config, agent)?.model);
}
function builtInAgentUserAgentNeedle(agent: RouterBuiltInAgentRuleId): string {

View file

@ -532,7 +532,6 @@ export type RouterBuiltInRulesConfig = Record<RouterBuiltInAgentRuleId, RouterBu
export type RouterConfig = {
builtInRules: RouterBuiltInRulesConfig;
default?: string;
fallback: RouterFallbackConfig;
rules: RouterRule[];
};

View file

@ -25,14 +25,13 @@ export type ClaudeAppGatewayInferenceModel = {
supports1m?: true;
};
export function inferClaudeAppGatewayTargetModel(config: Pick<AppConfig, "Router" | "profile">): string {
return config.Router.default?.trim() ||
inferGlobalClaudeProfileModel(config) ||
export function inferClaudeAppGatewayTargetModel(config: Pick<AppConfig, "profile">): string {
return inferGlobalClaudeProfileModel(config) ||
CLAUDE_APP_FALLBACK_MODEL;
}
export function buildClaudeAppGatewayModelRoutes(
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): ClaudeAppGatewayModelRoute[] {
const targetModels = claudeAppGatewayTargetModels(config);
@ -74,7 +73,7 @@ export function buildClaudeAppGatewayModelRoutes(
export function resolveClaudeAppGatewayRouteModel(
model: string,
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): string | undefined {
const normalized = model.trim().toLowerCase();
@ -99,7 +98,7 @@ export function resolveClaudeAppGatewayRouteModel(
}
export function buildClaudeAppGatewayInferenceModels(
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">,
options: ClaudeAppGatewayModelRouteOptions = {}
): ClaudeAppGatewayInferenceModel[] {
const routes = buildClaudeAppGatewayModelRoutes(config, options);
@ -129,7 +128,7 @@ function inferGlobalClaudeProfileModel(config: Pick<AppConfig, "profile">): stri
)?.model.trim() ?? "";
}
function claudeAppGatewayTargetModels(config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">): string[] {
function claudeAppGatewayTargetModels(config: Pick<AppConfig, "Providers" | "profile" | "virtualModelProfiles">): string[] {
const baseEntries = config.Providers.flatMap((provider) => {
const providerName = provider.name?.trim();
if (!providerName || !Array.isArray(provider.models)) {

View file

@ -20,7 +20,6 @@ function createRouterPlugin(options = {}) {
"claude-code": { enabled: options.claudeCodeRuleEnabled ?? true },
codex: { enabled: options.codexRuleEnabled ?? true }
},
default: options.defaultModel ?? "",
fallback: { mode: "off", models: [], retryCount: 1 },
rules: []
},
@ -59,10 +58,9 @@ test("built-in Claude Code route matches user-agent case-insensitively", async (
assert.equal(result.decision.reason, "builtin:claude-code");
});
test("built-in Codex route can use Router.default when profile model is unset", async () => {
test("built-in Codex route stays inactive when profile model is unset", async () => {
const plugin = createRouterPlugin({
agent: "codex",
defaultModel: "Provider/gpt-5-codex"
agent: "codex"
});
const result = await plugin.routeRequest({
body: {
@ -76,8 +74,8 @@ test("built-in Codex route can use Router.default when profile model is unset",
url: "/v1/messages"
});
assert.equal(result.body.model, "Provider/gpt-5-codex");
assert.equal(result.decision.reason, "builtin:codex");
assert.equal(result.body.model, "gpt-5");
assert.equal(result.decision.reason, "default");
});
test("built-in agent route stays off after the user disables it", async () => {
@ -417,7 +415,7 @@ test("built-in Claude Code route removes only the first billing system array ite
test("non-Claude-Code routes keep billing system prompts unchanged", async () => {
const plugin = createRouterPlugin({
agent: "codex",
defaultModel: "Provider/gpt-5-codex"
profileModel: "Provider/gpt-5-codex"
});
const result = await plugin.routeRequest({
body: {