mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
refactor(claude-desktop): drop displayName from model route schema
Claude Desktop's new model menu reads model IDs directly and ignores the display_name field, so a separate displayName slot added UI noise without any product value. Collapse the routeId / model / displayName tuple down to routeId / model, and let the route ID carry the user-visible name through a non-editable claude- prefix rendered next to the input. Drop display_name from ClaudeDesktopModelRoute, ClaudeDesktopDefaultRoute, and ResolvedModelRoute on the Rust side plus the matching TS interfaces, stop emitting it in /v1/models responses, derive route IDs from upstream model IDs when picked via the model dropdown, and update zh/en/ja copy to describe the new two-field layout.
This commit is contained in:
@@ -100,10 +100,12 @@ export interface ClaudeDesktopProviderFormProps {
|
||||
type RouteRow = {
|
||||
route: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
supports1m: boolean;
|
||||
};
|
||||
|
||||
const CLAUDE_ROUTE_PREFIX = "claude-";
|
||||
const ANTHROPIC_CLAUDE_ROUTE_PREFIX = "anthropic/claude-";
|
||||
|
||||
function envString(
|
||||
settingsConfig: Record<string, unknown> | undefined,
|
||||
key: string,
|
||||
@@ -121,13 +123,50 @@ function clonePlainRecord(value: unknown): Record<string, unknown> {
|
||||
return { ...(value as Record<string, unknown>) };
|
||||
}
|
||||
|
||||
function desktopRouteInputValue(route: string) {
|
||||
const trimmed = route.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower.startsWith(CLAUDE_ROUTE_PREFIX)) {
|
||||
return trimmed.slice(CLAUDE_ROUTE_PREFIX.length);
|
||||
}
|
||||
if (lower.startsWith(ANTHROPIC_CLAUDE_ROUTE_PREFIX)) {
|
||||
return trimmed.slice(ANTHROPIC_CLAUDE_ROUTE_PREFIX.length);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeDesktopRouteSuffix(value: string) {
|
||||
const trimmed = value.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower.startsWith(CLAUDE_ROUTE_PREFIX)) {
|
||||
return trimmed.slice(CLAUDE_ROUTE_PREFIX.length);
|
||||
}
|
||||
if (lower.startsWith(ANTHROPIC_CLAUDE_ROUTE_PREFIX)) {
|
||||
return trimmed.slice(ANTHROPIC_CLAUDE_ROUTE_PREFIX.length);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function desktopRouteIdFromInput(value: string) {
|
||||
const suffix = normalizeDesktopRouteSuffix(value).replace(/[/\\\s]+/g, "-");
|
||||
if (!suffix) return "";
|
||||
return `${CLAUDE_ROUTE_PREFIX}${suffix}`;
|
||||
}
|
||||
|
||||
function desktopRouteIdFromModel(model: string) {
|
||||
const suffix = model
|
||||
.trim()
|
||||
.replace(/^models\//i, "")
|
||||
.replace(/[/\\\s]+/g, "-");
|
||||
return desktopRouteIdFromInput(suffix);
|
||||
}
|
||||
|
||||
function initialRouteRows(
|
||||
routes: Record<string, ClaudeDesktopModelRoute> | undefined,
|
||||
): RouteRow[] {
|
||||
return Object.entries(routes ?? {}).map(([route, value]) => ({
|
||||
route,
|
||||
model: value.model ?? "",
|
||||
displayName: value.displayName ?? "",
|
||||
supports1m: value.supports1m ?? false,
|
||||
}));
|
||||
}
|
||||
@@ -135,8 +174,10 @@ function initialRouteRows(
|
||||
function isClaudeSafeRoute(route: string) {
|
||||
const normalized = route.trim().toLowerCase();
|
||||
return (
|
||||
normalized.startsWith("claude-") ||
|
||||
normalized.startsWith("anthropic/claude-")
|
||||
(normalized.startsWith(CLAUDE_ROUTE_PREFIX) &&
|
||||
normalized.length > CLAUDE_ROUTE_PREFIX.length) ||
|
||||
(normalized.startsWith(ANTHROPIC_CLAUDE_ROUTE_PREFIX) &&
|
||||
normalized.length > ANTHROPIC_CLAUDE_ROUTE_PREFIX.length)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,7 +188,6 @@ function defaultRouteRows(
|
||||
return defaults.map((route, index) => ({
|
||||
route: route.routeId,
|
||||
model: index === 0 ? defaultModel : "",
|
||||
displayName: route.displayName,
|
||||
supports1m: route.supports1m,
|
||||
}));
|
||||
}
|
||||
@@ -159,7 +199,6 @@ function nextRouteRow(current: RouteRow[], defaults: RouteRow[]): RouteRow {
|
||||
) ?? {
|
||||
route: "",
|
||||
model: "",
|
||||
displayName: "",
|
||||
supports1m: true,
|
||||
}
|
||||
);
|
||||
@@ -315,7 +354,6 @@ export function ClaudeDesktopProviderForm({
|
||||
preset.modelRoutes.map((r) => ({
|
||||
route: r.routeId,
|
||||
model: r.upstreamModel,
|
||||
displayName: r.displayName,
|
||||
supports1m: r.supports1m,
|
||||
})),
|
||||
);
|
||||
@@ -441,24 +479,22 @@ export function ClaudeDesktopProviderForm({
|
||||
}
|
||||
|
||||
const routeEntries = routes
|
||||
.map((route, index) => ({
|
||||
.map((route) => ({
|
||||
...route,
|
||||
route:
|
||||
route.route.trim() ||
|
||||
(mode === "proxy"
|
||||
? `claude-${route.model.trim().replace(/[/\\]/g, "-") || `model-${index + 1}`}`
|
||||
: ""),
|
||||
route: route.route.trim(),
|
||||
model: route.model.trim(),
|
||||
displayName: route.displayName.trim(),
|
||||
}))
|
||||
.filter((route) => route.route || route.model);
|
||||
|
||||
if (mode === "proxy") {
|
||||
const missing = routeEntries.find((route) => !route.model);
|
||||
if (missing) {
|
||||
const invalid = routeEntries.find(
|
||||
(route) =>
|
||||
!route.route || !route.model || !isClaudeSafeRoute(route.route),
|
||||
);
|
||||
if (invalid) {
|
||||
toast.error(
|
||||
t("claudeDesktop.routeInvalid", {
|
||||
defaultValue: "请填写上游模型名",
|
||||
defaultValue: "请填写 Desktop 显示模型和实际请求模型",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -466,7 +502,7 @@ export function ClaudeDesktopProviderForm({
|
||||
if (routeEntries.length === 0) {
|
||||
toast.error(
|
||||
t("claudeDesktop.routesRequired", {
|
||||
defaultValue: "至少填写一个上游模型",
|
||||
defaultValue: "至少填写一个模型映射",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -504,7 +540,6 @@ export function ClaudeDesktopProviderForm({
|
||||
>((acc, route) => {
|
||||
acc[route.route] = {
|
||||
model: route.model || route.route,
|
||||
displayName: route.displayName || undefined,
|
||||
supports1m: route.supports1m || undefined,
|
||||
};
|
||||
return acc;
|
||||
@@ -705,20 +740,20 @@ export function ClaudeDesktopProviderForm({
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t("claudeDesktop.routeMapHint", {
|
||||
defaultValue:
|
||||
"填写供应商实际提供的模型名,显示名为在 Claude Desktop 模型列表中展示的名称。",
|
||||
"左侧决定 Claude Desktop 模型菜单里的可见模型 ID,实际写入为 claude-*;右侧填写供应商实际请求的模型名。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden grid-cols-[1fr_1fr_92px_36px] gap-2 px-1 text-xs font-medium text-muted-foreground md:grid">
|
||||
<span>
|
||||
{t("claudeDesktop.upstreamModelLabel", {
|
||||
defaultValue: "实际请求模型",
|
||||
{t("claudeDesktop.routeModelLabel", {
|
||||
defaultValue: "Desktop 显示模型",
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t("claudeDesktop.displayNameLabel", {
|
||||
defaultValue: "显示名",
|
||||
{t("claudeDesktop.upstreamModelLabel", {
|
||||
defaultValue: "实际请求模型",
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
@@ -733,6 +768,21 @@ export function ClaudeDesktopProviderForm({
|
||||
key={`${route.route}-${index}`}
|
||||
className="grid grid-cols-1 gap-2 md:grid-cols-[1fr_1fr_92px_36px]"
|
||||
>
|
||||
<div className="flex">
|
||||
<span className="inline-flex h-9 items-center rounded-l-md border border-r-0 border-input bg-muted px-3 text-sm text-muted-foreground">
|
||||
{CLAUDE_ROUTE_PREFIX}
|
||||
</span>
|
||||
<Input
|
||||
value={desktopRouteInputValue(route.route)}
|
||||
onChange={(event) =>
|
||||
updateRoute(index, {
|
||||
route: desktopRouteIdFromInput(event.target.value),
|
||||
})
|
||||
}
|
||||
placeholder="gpt-5.5"
|
||||
className="rounded-l-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Input
|
||||
value={route.model}
|
||||
@@ -745,17 +795,15 @@ export function ClaudeDesktopProviderForm({
|
||||
{fetchedModels.length > 0 && (
|
||||
<ModelDropdown
|
||||
models={fetchedModels}
|
||||
onSelect={(id) => updateRoute(index, { model: id })}
|
||||
onSelect={(id) =>
|
||||
updateRoute(index, {
|
||||
model: id,
|
||||
route: route.route || desktopRouteIdFromModel(id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
value={route.displayName}
|
||||
onChange={(event) =>
|
||||
updateRoute(index, { displayName: event.target.value })
|
||||
}
|
||||
placeholder="Sonnet"
|
||||
/>
|
||||
<label className="flex h-9 items-center gap-2 text-sm text-muted-foreground">
|
||||
<Checkbox
|
||||
checked={route.supports1m}
|
||||
@@ -830,7 +878,6 @@ export function ClaudeDesktopProviderForm({
|
||||
{
|
||||
route: "",
|
||||
model: "",
|
||||
displayName: "",
|
||||
supports1m: false,
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*
|
||||
* 形态与 Claude Code 预设不同:
|
||||
* - baseUrl 是顶级字段,而不是 settingsConfig.env.ANTHROPIC_BASE_URL
|
||||
* - 模型信息以"请求模型 → 上游模型 → 显示模型"三段式表达,
|
||||
* 对应后端 ClaudeDesktopModelRoute 的 routeId / model / displayName
|
||||
* - 模型信息以"Desktop 可见模型 ID → 上游模型"表达,
|
||||
* 对应后端 ClaudeDesktopModelRoute 的 routeId / model
|
||||
*
|
||||
* 翻译来源:src/config/claudeProviderPresets.ts(排除 OAuth 与不兼容预设)
|
||||
*/
|
||||
@@ -20,7 +20,6 @@ export type ClaudeDesktopApiFormat =
|
||||
export interface ClaudeDesktopRoutePreset {
|
||||
routeId: string;
|
||||
upstreamModel: string;
|
||||
displayName: string;
|
||||
supports1m: boolean;
|
||||
}
|
||||
|
||||
@@ -50,19 +49,16 @@ const passthroughRoutes = (supports1m = false): ClaudeDesktopRoutePreset[] => [
|
||||
{
|
||||
routeId: "claude-sonnet-4-6",
|
||||
upstreamModel: "claude-sonnet-4-6",
|
||||
displayName: "Sonnet",
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-opus-4-7",
|
||||
upstreamModel: "claude-opus-4-7",
|
||||
displayName: "Opus",
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-haiku-4-5",
|
||||
upstreamModel: "claude-haiku-4-5",
|
||||
displayName: "Haiku",
|
||||
supports1m,
|
||||
},
|
||||
];
|
||||
@@ -76,52 +72,55 @@ const mappedRoutes = (
|
||||
{
|
||||
routeId: "claude-sonnet-4-6",
|
||||
upstreamModel: sonnet,
|
||||
displayName: "Sonnet",
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-opus-4-7",
|
||||
upstreamModel: opus,
|
||||
displayName: "Opus",
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-haiku-4-5",
|
||||
upstreamModel: haiku,
|
||||
displayName: "Haiku",
|
||||
supports1m,
|
||||
},
|
||||
];
|
||||
|
||||
const routeIdFromModel = (model: string) => {
|
||||
const trimmed = model.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (lower.startsWith("claude-") || lower.startsWith("anthropic/claude-")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return `claude-${trimmed.replace(/^models\//i, "").replace(/[/\\\s]+/g, "-")}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 非 Claude 上游模型用此工厂:displayName 直接等于 upstreamModel,
|
||||
* 让用户在 Claude Desktop 看到的就是实际请求的模型 ID(如 "deepseek-v4-pro")。
|
||||
* 非 Claude 上游模型用此工厂:用 claude-* route ID 承载品牌化模型名,
|
||||
* 让 Claude Desktop 菜单能从 ID 看到接近上游模型的名称。
|
||||
*/
|
||||
const brandedRoutes = (
|
||||
sonnet: string,
|
||||
opus: string,
|
||||
haiku: string,
|
||||
supports1m = false,
|
||||
): ClaudeDesktopRoutePreset[] => [
|
||||
{
|
||||
routeId: "claude-sonnet-4-6",
|
||||
upstreamModel: sonnet,
|
||||
displayName: sonnet,
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-opus-4-7",
|
||||
upstreamModel: opus,
|
||||
displayName: opus,
|
||||
supports1m,
|
||||
},
|
||||
{
|
||||
routeId: "claude-haiku-4-5",
|
||||
upstreamModel: haiku,
|
||||
displayName: haiku,
|
||||
supports1m,
|
||||
},
|
||||
];
|
||||
): ClaudeDesktopRoutePreset[] => {
|
||||
const seen = new Set<string>();
|
||||
return [sonnet, opus, haiku]
|
||||
.map((upstreamModel) => ({
|
||||
routeId: routeIdFromModel(upstreamModel),
|
||||
upstreamModel,
|
||||
supports1m,
|
||||
}))
|
||||
.filter((route) => {
|
||||
if (seen.has(route.routeId)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(route.routeId);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
|
||||
{
|
||||
|
||||
@@ -181,9 +181,9 @@
|
||||
"modelMappingOffHint": "Use this when the provider already exposes and accepts claude-* / anthropic/claude-* model IDs through Anthropic Messages. Claude Desktop connects to the provider directly.",
|
||||
"modelMappingOnHint": "Claude Desktop currently restricts model IDs. If your provider offers non-Claude models, enable this switch and keep local routing running while in use.",
|
||||
"routeMapTitle": "Model mapping",
|
||||
"routeMapHint": "Enter the model name provided by your provider. The display name is shown in the Claude Desktop model list.",
|
||||
"routeMapHint": "The left field controls the Claude Desktop-visible model ID and is written as claude-*. Enter the provider's actual requested model on the right.",
|
||||
"routeModelLabel": "Desktop display model",
|
||||
"upstreamModelLabel": "Requested model",
|
||||
"displayNameLabel": "Display name",
|
||||
"supports1mLabel": "1M",
|
||||
"directModelListTitle": "Manually specify Claude Desktop models (advanced, optional)",
|
||||
"directModelListCollapsedHint": "Native Claude model providers usually do not need this. Claude Desktop will fetch /v1/models automatically.",
|
||||
@@ -191,8 +191,8 @@
|
||||
"directModelInvalid": "Direct models must use claude-* / anthropic/claude-* model IDs",
|
||||
"addModel": "Add model",
|
||||
"addRoute": "Add model",
|
||||
"routeInvalid": "Please enter the upstream model name",
|
||||
"routesRequired": "At least one upstream model is required",
|
||||
"routeInvalid": "Please enter both the Desktop display model and requested model",
|
||||
"routesRequired": "At least one model mapping is required",
|
||||
"statusTitle": "Claude Desktop configuration needs attention",
|
||||
"statusUnsupported": "This platform does not support writing Claude Desktop 3P configuration yet.",
|
||||
"statusStaleRawModels": "The Claude Desktop profile contains non-claude-* model IDs. Newer Claude Desktop versions may reject it; switch to the current provider again to repair it.",
|
||||
|
||||
@@ -181,9 +181,9 @@
|
||||
"modelMappingOffHint": "プロバイダーが Anthropic Messages で claude-* / anthropic/claude-* のモデル ID を公開し、そのまま受け付ける場合に使います。Claude Desktop はプロバイダーへ直接接続します。",
|
||||
"modelMappingOnHint": "Claude Desktop は現在モデル ID を制限しています。お使いのプロバイダーが Claude 系列以外のモデルを提供する場合、このスイッチを有効にし、使用中はローカルルーティングを起動したままにしてください。",
|
||||
"routeMapTitle": "モデルマッピング",
|
||||
"routeMapHint": "プロバイダーが実際に提供するモデル名を入力してください。表示名は Claude Desktop のモデルリストに表示される名前です。",
|
||||
"routeMapHint": "左側は Claude Desktop のモデルメニューに表示されるモデル ID を指定し、claude-* として書き込まれます。右側にはプロバイダーへ実際にリクエストするモデル名を入力してください。",
|
||||
"routeModelLabel": "Desktop 表示モデル",
|
||||
"upstreamModelLabel": "リクエストモデル",
|
||||
"displayNameLabel": "表示名",
|
||||
"supports1mLabel": "1M",
|
||||
"directModelListTitle": "Claude Desktop モデルを手動指定(高度・任意)",
|
||||
"directModelListCollapsedHint": "ネイティブ Claude モデルのプロバイダーでは通常不要です。Claude Desktop が /v1/models を自動取得します。",
|
||||
@@ -191,8 +191,8 @@
|
||||
"directModelInvalid": "直結モデルは claude-* / anthropic/claude-* のモデル ID を使う必要があります",
|
||||
"addModel": "モデルを追加",
|
||||
"addRoute": "モデルを追加",
|
||||
"routeInvalid": "上流モデル名を入力してください",
|
||||
"routesRequired": "少なくとも 1 つの上流モデルが必要です",
|
||||
"routeInvalid": "Desktop 表示モデルとリクエストモデルを入力してください",
|
||||
"routesRequired": "少なくとも 1 つのモデルマッピングが必要です",
|
||||
"statusTitle": "Claude Desktop 設定の確認が必要です",
|
||||
"statusUnsupported": "このプラットフォームでは Claude Desktop 3P 設定の書き込みはまだサポートされていません。",
|
||||
"statusStaleRawModels": "Claude Desktop profile に claude-* ではないモデル ID が含まれています。新しい Claude Desktop では拒否される可能性があります。現在のプロバイダーへ再度切り替えると修復できます。",
|
||||
|
||||
@@ -181,9 +181,9 @@
|
||||
"modelMappingOffHint": "适合供应商已经暴露并接受 claude-* / anthropic/claude-* 模型名的 Anthropic Messages API;请求会由 Claude Desktop 直连供应商。",
|
||||
"modelMappingOnHint": "Claude Desktop 目前对模型 ID 进行了限制,如果您的供应商提供的模型不是 Claude 系列模型,则需要打开本开关,并在使用过程中保持本地路由开启。",
|
||||
"routeMapTitle": "模型映射",
|
||||
"routeMapHint": "填写供应商实际提供的模型名,显示名为在 Claude Desktop 模型列表中展示的名称。",
|
||||
"routeMapHint": "左侧决定 Claude Desktop 模型菜单里的可见模型 ID,实际写入为 claude-*;右侧填写供应商实际请求的模型名。",
|
||||
"routeModelLabel": "Desktop 显示模型",
|
||||
"upstreamModelLabel": "实际请求模型",
|
||||
"displayNameLabel": "显示名",
|
||||
"supports1mLabel": "1M",
|
||||
"directModelListTitle": "手动指定 Claude Desktop 模型列表(高级,可选)",
|
||||
"directModelListCollapsedHint": "原生 Claude 模型供应商通常不用填写,Claude Desktop 会自动读取 /v1/models。",
|
||||
@@ -191,8 +191,8 @@
|
||||
"directModelInvalid": "直连模型必须使用 claude-* / anthropic/claude-* 模型名",
|
||||
"addModel": "添加模型",
|
||||
"addRoute": "添加模型",
|
||||
"routeInvalid": "请填写上游模型名",
|
||||
"routesRequired": "至少填写一个上游模型",
|
||||
"routeInvalid": "请填写 Desktop 显示模型和实际请求模型",
|
||||
"routesRequired": "至少填写一个模型映射",
|
||||
"statusTitle": "Claude Desktop 配置需要检查",
|
||||
"statusUnsupported": "当前平台暂不支持 Claude Desktop 3P 配置写入。",
|
||||
"statusStaleRawModels": "Claude Desktop profile 中存在非 claude-* 模型名,新版 Claude Desktop 可能拒绝加载;重新切换当前供应商可修复。",
|
||||
|
||||
@@ -43,7 +43,6 @@ export interface ClaudeDesktopStatus {
|
||||
export interface ClaudeDesktopDefaultRoute {
|
||||
routeId: string;
|
||||
envKey: string;
|
||||
displayName: string;
|
||||
supports1m: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ export interface AuthBinding {
|
||||
|
||||
export interface ClaudeDesktopModelRoute {
|
||||
model: string;
|
||||
displayName?: string;
|
||||
supports1m?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user