Separate enhanced route from profile routing settings

This commit is contained in:
musistudio
2026-08-05 20:23:08 +08:00
parent df42fe8683
commit b4e6185172
4 changed files with 103 additions and 55 deletions
@@ -45,6 +45,10 @@ This lets you create multiple configs for the same agent, such as "Claude Code -
| Bot | App entry | Bot forwarding only works for App mode opened from CCR. CLI does not forward Bot messages yet. |
| Environment variables | All | Extra environment variables injected into this config. Claude Code includes `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` by default so gateway model discovery is enabled. |
## Enhanced Routing
Claude Code and Codex profiles show **Use enhanced route** as its own advanced setting. It controls CCR's built-in Claude Code / Codex routing enhancements and is independent from the private profile rule list. Turning profile routing off does not change this switch; turn **Use enhanced route** off to disable the built-in route for that profile.
## Profile Routing
Each Agent Profiles entry can carry its own routing policy:
@@ -52,7 +56,6 @@ Each Agent Profiles entry can carry its own routing policy:
| Option | Behavior |
| --- | --- |
| Enable profile routing | Turns on the profile's private rule list. Disabled profile routing keeps the profile usable but skips its private rules. |
| Use enhanced route | Claude Code and Codex only. A profile-level switch for CCR's built-in Claude Code / Codex routing enhancements. It is independent from the private rule list, so each profile can enable or disable the built-in route separately. |
| Profile routes | Uses condition-based rules. Rules can match `request.header`, `request.body`, or `request.auth`, and can rewrite the request model or other fields. Node.js script rules are only supported on the global Routing page. |
Profile rules are evaluated before the global Routing page rules. They only see traffic from their own profile API key, so two profiles can use the same client model name and still route differently. For example, a "Claude Code - Work" profile can send image-heavy requests to a Fusion vision model while a "Claude Code - Low Cost" profile sends the same request shape to a cheaper provider.
@@ -45,6 +45,10 @@ lead: 为 Claude Code、Codex、Grok CLI、Kimi CLI、Pi、ZCode 创建可复用
| Bot | App 入口 | 只有从 CCR 打开的 App 模式会转发 Bot 消息。CLI 当前不转发 Bot 消息。 |
| 环境变量 | 全部 | 为该配置注入额外环境变量。Claude Code 默认带 `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,用于启用网关模型发现。 |
## 增强路由
Claude Code 和 Codex 配置会把 **使用增强路由** 显示为独立的高级设置。它只控制 CCR 内置的 Claude Code / Codex 路由增强,和配置级私有规则列表互相独立。关闭配置级路由不会改变这个开关;如果要关闭该配置的内置路由增强,需要单独关闭 **使用增强路由**
## 配置级路由
每个 Agent 配置档案都可以带自己的路由策略:
@@ -52,7 +56,6 @@ lead: 为 Claude Code、Codex、Grok CLI、Kimi CLI、Pi、ZCode 创建可复用
| 配置项 | 行为 |
| --- | --- |
| 启用配置级路由 | 打开该配置的私有规则列表。关闭后配置仍可使用,但不会执行私有规则。 |
| 使用增强路由 | 仅 Claude Code 和 Codex 显示。它是配置档案级别的内置 Claude Code / Codex 路由增强开关,独立于私有规则列表,因此每个配置档案都可以单独开启或关闭内置路由。 |
| 配置级路由规则 | 使用条件规则。规则可以匹配 `request.header``request.body``request.auth`,并改写请求模型或其他字段。Node.js 脚本规则只在全局路由页支持。 |
配置级规则会先于全局路由页规则执行。它们只看到自己的配置 API Key 发来的流量,因此两个配置即使用同一个客户端模型名,也可以走不同分流。例如,“Claude Code - 工作”可以把带图片的请求发到 Fusion 视觉模型,而“Claude Code - 低成本”可以把同样形态的请求发到更便宜的供应商。
@@ -954,7 +954,10 @@ export function AddProfileForm({
{showAdvancedSettings ? (
<div className="sm:col-span-2">
<button
className="flex min-h-9 w-full min-w-0 items-center justify-between gap-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-left outline-none transition-colors hover:bg-muted/35 focus-visible:ring-2 focus-visible:ring-ring/25"
className={cn(
"flex min-h-9 w-full min-w-0 items-center justify-between gap-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-left outline-none transition-colors hover:bg-muted/35 focus-visible:ring-2 focus-visible:ring-ring/25",
advancedOpen && "rounded-b-none"
)}
onClick={() => setAdvancedOpen((current) => !current)}
type="button"
>
@@ -978,26 +981,15 @@ export function AddProfileForm({
initial={{ height: 0, opacity: 0 }}
transition={{ duration: 0.16 }}
>
<div className="mt-3 grid grid-cols-1 gap-3 rounded-md border border-border bg-background/60 p-3 sm:grid-cols-2">
<div className="grid grid-cols-1 gap-3 rounded-b-md border border-t-0 border-border bg-background/60 p-3 sm:grid-cols-2">
{draft.agent === "claude-code" || draft.agent === "codex" ? (
<ProfileEnhancedRouteSetting draft={draft} onChange={onChange} />
) : null}
<ProfileRoutingSettings
draft={draft}
onChange={onChange}
providers={providers}
/>
{showAppPathField && appPathLabel ? (
<Field className="sm:col-span-2" label={t(appPathLabel)} requirement="optional" requirementLabel={optionalFieldLabel}>
<div className={cn(
"rounded-md border border-border bg-background p-1 transition-colors",
appPathDragActive ? "border-primary bg-primary/5" : "border-border"
)}>
<Input
placeholder={t("Drop the app here or paste the executable path")}
value={draft.appPath}
onChange={(event) => onChange({ appPath: event.target.value })}
/>
</div>
</Field>
) : null}
{draft.agent !== "claude-code" && draft.agent !== "grok" && draft.agent !== "kimi" && draft.agent !== "pi" ? (
<>
<Field label={t("Provider ID")} requirement="required" requirementLabel={requiredFieldLabel}>
@@ -1032,6 +1024,16 @@ export function AddProfileForm({
{validation.handoff ? <ProfileFieldHint>{t(validation.handoff)}</ProfileFieldHint> : null}
</div>
) : null}
{showAppPathField && appPathLabel ? (
<Field className="sm:col-span-2" label={t(appPathLabel)} requirement="optional" requirementLabel={optionalFieldLabel}>
<Input
className={appPathDragActive ? "border-primary bg-primary/5" : undefined}
placeholder={t("Drop the app here or paste the executable path")}
value={draft.appPath}
onChange={(event) => onChange({ appPath: event.target.value })}
/>
</Field>
) : null}
<Field className="sm:col-span-2" label={t("Environment variables")} requirement="optional" requirementLabel={optionalFieldLabel}>
<KeyValueRowsControl
addLabel={t("Add env variable")}
@@ -1074,11 +1076,6 @@ function ProfileRoutingSettings({
const t = useAppText();
const [ruleDialog, setRuleDialog] = useState<{ draft: AddRoutingRuleDraft; index?: number }>();
const canSubmitRule = ruleDialog ? isRoutingRuleDraftSubmittable(ruleDialog.draft) : false;
const showEnhancedRoute = draft.agent === "claude-code" || draft.agent === "codex";
const showRoutingControls = draft.routingEnabled || showEnhancedRoute;
const enhancedRouteDescription = draft.agent === "codex"
? t("Enhanced route description Codex")
: t("Enhanced route description Claude Code");
function openAddRuleDialog() {
setRuleDialog({
@@ -1142,37 +1139,9 @@ function ProfileRoutingSettings({
onChange={(routingEnabled) => onChange({ routingEnabled })}
/>
</div>
{showRoutingControls ? (
{draft.routingEnabled ? (
<div className="mt-3 grid grid-cols-1 gap-3 border-t border-border/70 pt-3">
{showEnhancedRoute ? (
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-background px-3 py-2">
<span className="flex min-w-0 items-center gap-1.5">
<span className="text-[12px] font-medium">{t("Enhanced route")}</span>
<Tooltip
aria-label={enhancedRouteDescription}
className="h-5 w-5 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
content={enhancedRouteDescription}
contentClassName="w-[260px] max-w-[calc(100vw-64px)] whitespace-normal px-2.5 py-2 text-left font-medium leading-4"
side="right"
tabIndex={0}
>
<button
aria-label={enhancedRouteDescription}
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-[5px] text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/25"
type="button"
>
<Info className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</Tooltip>
</span>
<Toggle
checked={draft.routingEnhancedRoute}
onChange={(routingEnhancedRoute) => onChange({ routingEnhancedRoute })}
/>
</div>
) : null}
{draft.routingEnabled ? (
<div className="rounded-md border border-border bg-background p-3">
<div className="rounded-md border border-border bg-background p-3">
<div className="flex min-w-0 items-center justify-between gap-3">
<span className="text-[12px] font-medium">{t("Profile routes")}</span>
<Button onClick={openAddRuleDialog} size="sm" type="button" variant="outline">
@@ -1216,7 +1185,6 @@ function ProfileRoutingSettings({
))}
</div>
</div>
) : null}
</div>
) : null}
{ruleDialog ? (
@@ -1235,6 +1203,49 @@ function ProfileRoutingSettings({
);
}
function ProfileEnhancedRouteSetting({
draft,
onChange
}: {
draft: AddProfileDraft;
onChange: (patch: Partial<AddProfileDraft>) => void;
}) {
const t = useAppText();
const enhancedRouteDescription = draft.agent === "codex"
? t("Enhanced route description Codex")
: t("Enhanced route description Claude Code");
return (
<div className="sm:col-span-2 rounded-md border border-border bg-muted/20 p-3">
<div className="flex min-w-0 items-center justify-between gap-3">
<span className="flex min-w-0 items-center gap-1.5">
<span className="text-[12px] font-semibold">{t("Enhanced route")}</span>
<Tooltip
aria-label={enhancedRouteDescription}
className="h-5 w-5 items-center justify-center rounded-full text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
content={enhancedRouteDescription}
contentClassName="w-[260px] max-w-[calc(100vw-64px)] whitespace-normal px-2.5 py-2 text-left font-medium leading-4"
side="right"
tabIndex={0}
>
<button
aria-label={enhancedRouteDescription}
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-[5px] text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/25"
type="button"
>
<Info className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</Tooltip>
</span>
<Toggle
checked={draft.routingEnhancedRoute}
onChange={(routingEnhancedRoute) => onChange({ routingEnhancedRoute })}
/>
</div>
</div>
);
}
function createProfileRoutingRuleDraftFromRule(rule: RouterRule): AddRoutingRuleDraft {
const draft = createRoutingRuleDraftFromRule(rule);
return draft.type === "condition"
+32 -1
View File
@@ -81,7 +81,7 @@ test("AddProfileForm keeps profile routing inside Advanced settings", () => {
assert.doesNotMatch(html, /Enhanced route/);
});
test("AddProfileForm shows profile-level enhanced route controls when private routing is disabled", () => {
test("AddProfileForm shows enhanced route as a sibling of profile routing", () => {
const config = appConfigFixture();
const html = renderToStaticMarkup(
<AddProfileForm
@@ -96,12 +96,18 @@ test("AddProfileForm shows profile-level enhanced route controls when private ro
/>
);
const advancedSettingsIndex = html.indexOf("Advanced settings");
const enhancedRouteIndex = html.indexOf("Enhanced route");
const profileRoutingIndex = html.indexOf("Profile routing");
assert.ok(advancedSettingsIndex >= 0);
assert.ok(enhancedRouteIndex > advancedSettingsIndex);
assert.ok(profileRoutingIndex > advancedSettingsIndex);
assert.ok(enhancedRouteIndex < profileRoutingIndex);
assert.doesNotMatch(html, /Routing disabled/);
assert.match(html, /Enhanced route/);
assert.match(html, /rounded-b-none/);
assert.match(html, /rounded-b-md border border-t-0/);
assert.doesNotMatch(html, /Profile routes/);
assert.match(html, /CCR built-in Claude Code routing optimizes requests to third-party models for this profile\./);
});
@@ -123,6 +129,7 @@ test("AddProfileForm shows private profile routes when profile routing is enable
assert.match(html, /Profile routing/);
assert.match(html, /Enhanced route/);
assert.match(html, /Profile routes/);
assert.ok(html.indexOf("Enhanced route") < html.indexOf("Profile routing"));
assert.match(html, /CCR built-in Claude Code routing optimizes requests to third-party models for this profile\./);
assert.match(html, /data-ui-tooltip-trigger/);
});
@@ -146,6 +153,30 @@ test("AddProfileForm uses Codex-specific enhanced route info for Codex profiles"
assert.match(html, /CCR built-in Codex routing optimizes requests to third-party models for this profile\./);
});
test("AddProfileForm places CLAUDE_APP_PATH below Bot settings", () => {
const config = appConfigFixture();
const html = renderToStaticMarkup(
<AddProfileForm
botConfigs={config.botConfigs ?? []}
draft={{ ...createProfileDraft("claude-code"), surface: "app" }}
error=""
mode="edit"
onChange={() => undefined}
onCreateBot={() => undefined}
providers={config.Providers}
virtualModelProfiles={config.virtualModelProfiles}
/>
);
const botIndex = html.indexOf(">Bot</span>");
const appPathIndex = html.indexOf("CLAUDE_APP_PATH");
const envIndex = html.indexOf("Environment variables");
assert.ok(botIndex >= 0);
assert.ok(appPathIndex > botIndex);
assert.ok(envIndex > appPathIndex);
assert.doesNotMatch(html, /rounded-md border border-border bg-background p-1 transition-colors/);
});
test("AddProfileForm marks required and optional fields", () => {
const config = appConfigFixture();
const html = renderToStaticMarkup(