mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Remove default routing fallback from config and docs
This commit is contained in:
parent
22dad1afb5
commit
513fa11ead
21 changed files with 127 additions and 82 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. 设置路由
|
||||
|
||||
打开 **路由**,选择默认路由,添加条件规则,配置请求改写和失败降级。
|
||||
打开 **路由**,添加条件规则,配置请求改写和失败降级。
|
||||
|
||||
如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ lead: 深入配置概览仪表盘、API 密钥、服务、供应商、路由、A
|
|||
| 服务配置 | Host、Port、代理模式、系统代理、网络捕获和 CA 证书 |
|
||||
| 供应商配置 | 上游服务、协议、基础 URL、模型列表和凭据 |
|
||||
| 一键导入供应商 | Provider deeplink 协议、Manifest 导入、一键导入按钮和安全边界 |
|
||||
| 路由配置 | 默认路由、条件规则、fallback 和请求改写 |
|
||||
| 路由配置 | 条件规则、fallback 和请求改写 |
|
||||
| 日志&观测 | 请求日志、Agent 执行追踪、工具调用和工具结果 |
|
||||
| Fusion 组合模型 | 把基础模型与视觉、搜索、MCP 工具组合成新的可选模型 |
|
||||
| Agent配置 | Agent 启动方式、模型、作用范围、多开和 Bot 绑定 |
|
||||
|
|
|
|||
|
|
@ -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 的规则。
|
||||
|
||||
### 规则级失败降级
|
||||
|
||||
|
|
|
|||
|
|
@ -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 怎么处理?
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() } : {})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]}`;
|
||||
|
|
|
|||
|
|
@ -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]}`;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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": "展开模型",
|
||||
|
|
|
|||
|
|
@ -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]}`;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -733,6 +733,7 @@ export type RoutingRuleRow = {
|
|||
sourceLabel: string;
|
||||
target: string;
|
||||
toggleDisabled?: boolean;
|
||||
toggleDisabledReason?: string;
|
||||
typeLabel: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -532,7 +532,6 @@ export type RouterBuiltInRulesConfig = Record<RouterBuiltInAgentRuleId, RouterBu
|
|||
|
||||
export type RouterConfig = {
|
||||
builtInRules: RouterBuiltInRulesConfig;
|
||||
default?: string;
|
||||
fallback: RouterFallbackConfig;
|
||||
rules: RouterRule[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue