mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
feat: add Claude Fable 5 model mapping across Claude Code and Desktop
- Wire claude-fable-5 as a fourth tier on both proxy paths, with a fable -> opus -> default fallback mirroring the official downgrade. - Whitelist the fable- prefix for the Desktop 1.12603.1+ validator. - Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on third-party endpoints forwards the literal model name and 404s. Refs #3980, #4026, #4049.
This commit is contained in:
@@ -117,7 +117,7 @@ const CLAUDE_ROUTE_PREFIX = "claude-";
|
||||
const ANTHROPIC_CLAUDE_ROUTE_PREFIX = "anthropic/claude-";
|
||||
const LEGACY_ONE_M_MARKER = "[1m]";
|
||||
const ROLE_ROUTE_IDS = CLAUDE_DESKTOP_ROLE_ROUTE_IDS;
|
||||
const ROLE_ORDER: RouteRole[] = ["sonnet", "opus", "haiku"];
|
||||
const ROLE_ORDER: RouteRole[] = ["sonnet", "opus", "fable", "haiku"];
|
||||
|
||||
function envString(
|
||||
settingsConfig: Record<string, unknown> | undefined,
|
||||
@@ -138,8 +138,10 @@ function clonePlainRecord(value: unknown): Record<string, unknown> {
|
||||
|
||||
function routeRoleFromId(route: string): RouteRole {
|
||||
const normalized = route.trim().toLowerCase();
|
||||
// 与后端 claude_role_keyword 同序(opus → haiku → fable → sonnet)。
|
||||
if (normalized.includes("opus")) return "opus";
|
||||
if (normalized.includes("haiku")) return "haiku";
|
||||
if (normalized.includes("fable")) return "fable";
|
||||
return "sonnet";
|
||||
}
|
||||
|
||||
@@ -193,9 +195,10 @@ function initialRouteRows(
|
||||
});
|
||||
}
|
||||
|
||||
// Proxy 模式对齐 Claude Code:固定 Sonnet / Opus / Haiku 三档。
|
||||
// 把任意来源的 route 行按角色归类到固定三槽(缺档留空),保证 UI 永远三行、
|
||||
// 用户不会漏配 Haiku 导致子 agent 找不到模型。
|
||||
// Proxy 模式对齐 Claude Code:固定 Sonnet / Opus / Fable / Haiku 四档。
|
||||
// 把任意来源的 route 行按角色归类到固定四槽(缺档留空),保证 UI 永远四行、
|
||||
// 用户不会漏配某档导致子 agent 找不到模型。
|
||||
// (fable 自 Desktop 1.12603.1+ 起被 fail-all 校验放行,可作为独立档位。)
|
||||
function normalizeProxyRows(rows: RouteRow[]): RouteRow[] {
|
||||
return ROLE_ORDER.map((role) => {
|
||||
const match = rows.find(
|
||||
@@ -221,7 +224,8 @@ function isClaudeSafeRoute(route: string) {
|
||||
|
||||
// 角色前缀后必须还有实际模型标识,拒绝 claude-sonnet- 这类退化值
|
||||
// (否则会写入 profile 并触发 Claude Desktop fail-all 拒收整组)。
|
||||
return ["sonnet-", "opus-", "haiku-"].some(
|
||||
// 与后端 is_claude_safe_model_id 镜像;fable 自 Desktop 1.12603.1+ 起被校验放行。
|
||||
return ["sonnet-", "opus-", "haiku-", "fable-"].some(
|
||||
(prefix) =>
|
||||
routeTail.startsWith(prefix) && routeTail.length > prefix.length,
|
||||
);
|
||||
@@ -574,9 +578,9 @@ export function ClaudeDesktopProviderForm({
|
||||
.filter((route) => route.route || route.model);
|
||||
|
||||
if (mode === "proxy") {
|
||||
// 固定三档(Sonnet / Opus / Haiku),route_id 由 UI 生成、恒合法,
|
||||
// 固定四档(Sonnet / Opus / Fable / Haiku),route_id 由 UI 生成、恒合法,
|
||||
// 因此只要求至少填一个实际请求模型;留空档继承第一个已填档(Sonnet 优先),
|
||||
// 对齐 Claude Code 的兜底,保证落库三档齐全、子 agent 不会找不到 Haiku。
|
||||
// 对齐 Claude Code 的兜底,保证落库四档齐全、子 agent 不会找不到模型。
|
||||
const primary = routeEntries.find((route) => route.model);
|
||||
if (!primary) {
|
||||
toast.error(
|
||||
@@ -931,9 +935,13 @@ export function ClaudeDesktopProviderForm({
|
||||
? t("claudeDesktop.routeRoleHaiku", {
|
||||
defaultValue: "Haiku",
|
||||
})
|
||||
: t("claudeDesktop.routeRoleSonnet", {
|
||||
defaultValue: "Sonnet",
|
||||
});
|
||||
: role === "fable"
|
||||
? t("claudeDesktop.routeRoleFable", {
|
||||
defaultValue: "Fable",
|
||||
})
|
||||
: t("claudeDesktop.routeRoleSonnet", {
|
||||
defaultValue: "Sonnet",
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={route.rowId}
|
||||
|
||||
@@ -123,6 +123,8 @@ interface ClaudeFormFieldsProps {
|
||||
defaultSonnetModelName: string;
|
||||
defaultOpusModel: string;
|
||||
defaultOpusModelName: string;
|
||||
defaultFableModel: string;
|
||||
defaultFableModelName: string;
|
||||
onModelChange: (field: ClaudeModelEnvField, value: string) => void;
|
||||
|
||||
// Speed Test Endpoints
|
||||
@@ -187,6 +189,8 @@ export function ClaudeFormFields({
|
||||
defaultSonnetModelName,
|
||||
defaultOpusModel,
|
||||
defaultOpusModelName,
|
||||
defaultFableModel,
|
||||
defaultFableModelName,
|
||||
onModelChange,
|
||||
speedTestEndpoints,
|
||||
apiFormat,
|
||||
@@ -204,6 +208,7 @@ export function ClaudeFormFields({
|
||||
defaultHaikuModel ||
|
||||
defaultSonnetModel ||
|
||||
defaultOpusModel ||
|
||||
defaultFableModel ||
|
||||
apiFormat !== "anthropic" ||
|
||||
apiKeyField !== "ANTHROPIC_AUTH_TOKEN" ||
|
||||
customUserAgent
|
||||
@@ -495,7 +500,7 @@ export function ClaudeFormFields({
|
||||
};
|
||||
|
||||
type ModelRoleRow = {
|
||||
role: "sonnet" | "opus" | "haiku";
|
||||
role: "sonnet" | "opus" | "fable" | "haiku";
|
||||
label: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
@@ -526,6 +531,16 @@ export function ClaudeFormFields({
|
||||
inputId: "claudeDefaultOpusModel",
|
||||
supportsOneM: true,
|
||||
},
|
||||
{
|
||||
role: "fable",
|
||||
label: t("providerForm.modelRoleFable", { defaultValue: "Fable" }),
|
||||
model: defaultFableModel,
|
||||
displayName: defaultFableModelName,
|
||||
modelField: "ANTHROPIC_DEFAULT_FABLE_MODEL",
|
||||
displayNameField: "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME",
|
||||
inputId: "claudeDefaultFableModel",
|
||||
supportsOneM: true,
|
||||
},
|
||||
{
|
||||
role: "haiku",
|
||||
label: t("providerForm.modelRoleHaiku", { defaultValue: "Haiku" }),
|
||||
@@ -786,6 +801,7 @@ export function ClaudeFormFields({
|
||||
claudeModel ||
|
||||
defaultSonnetModel ||
|
||||
defaultOpusModel ||
|
||||
defaultFableModel ||
|
||||
defaultHaikuModel;
|
||||
if (value) {
|
||||
for (const row of modelRoleRows) {
|
||||
@@ -809,7 +825,8 @@ export function ClaudeFormFields({
|
||||
!claudeModel &&
|
||||
!defaultHaikuModel &&
|
||||
!defaultSonnetModel &&
|
||||
!defaultOpusModel
|
||||
!defaultOpusModel &&
|
||||
!defaultFableModel
|
||||
}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
@@ -936,7 +953,7 @@ export function ClaudeFormFields({
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.fallbackModelHint", {
|
||||
defaultValue:
|
||||
"仅在 Claude Code 请求没有明确落到 Sonnet、Opus 或 Haiku 角色时使用;通常可以留空。",
|
||||
"用于未明确落到 Sonnet、Opus、Fable、Haiku 角色的请求。使用第三方/中转端点时建议填写:否则这些请求(含 Haiku 后台子任务)会以原始 Claude 模型名透传给上游,可能因上游无此模型而报错。官方端点可留空。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -449,6 +449,8 @@ function ProviderFormFull({
|
||||
defaultSonnetModelName,
|
||||
defaultOpusModel,
|
||||
defaultOpusModelName,
|
||||
defaultFableModel,
|
||||
defaultFableModelName,
|
||||
handleModelChange,
|
||||
} = useModelState({
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
@@ -2009,6 +2011,8 @@ function ProviderFormFull({
|
||||
defaultSonnetModelName={defaultSonnetModelName}
|
||||
defaultOpusModel={defaultOpusModel}
|
||||
defaultOpusModelName={defaultOpusModelName}
|
||||
defaultFableModel={defaultFableModel}
|
||||
defaultFableModelName={defaultFableModelName}
|
||||
onModelChange={handleModelChange}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
apiFormat={localApiFormat}
|
||||
|
||||
@@ -12,7 +12,9 @@ export type ClaudeModelEnvField =
|
||||
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME"
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME";
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME"
|
||||
| "ANTHROPIC_DEFAULT_FABLE_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME";
|
||||
|
||||
export const CLAUDE_ONE_M_MARKER = "[1M]";
|
||||
|
||||
@@ -69,8 +71,28 @@ function parseModelsFromConfig(settingsConfig: string) {
|
||||
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME === "string"
|
||||
? env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
|
||||
: stripClaudeOneMMarker(opus);
|
||||
// 回填链镜像运行时映射链(fable → opus → default),保证 UI 展示
|
||||
// 与代理实际转发的模型一致。
|
||||
const fable =
|
||||
typeof env.ANTHROPIC_DEFAULT_FABLE_MODEL === "string"
|
||||
? env.ANTHROPIC_DEFAULT_FABLE_MODEL
|
||||
: opus;
|
||||
const fableName =
|
||||
typeof env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME === "string"
|
||||
? env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
|
||||
: stripClaudeOneMMarker(fable);
|
||||
|
||||
return { model, haiku, haikuName, sonnet, sonnetName, opus, opusName };
|
||||
return {
|
||||
model,
|
||||
haiku,
|
||||
haikuName,
|
||||
sonnet,
|
||||
sonnetName,
|
||||
opus,
|
||||
opusName,
|
||||
fable,
|
||||
fableName,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
model: "",
|
||||
@@ -80,6 +102,8 @@ function parseModelsFromConfig(settingsConfig: string) {
|
||||
sonnetName: "",
|
||||
opus: "",
|
||||
opusName: "",
|
||||
fable: "",
|
||||
fableName: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -106,6 +130,10 @@ export function useModelState({
|
||||
const [defaultOpusModelName, setDefaultOpusModelName] = useState(
|
||||
initial.opusName,
|
||||
);
|
||||
const [defaultFableModel, setDefaultFableModel] = useState(initial.fable);
|
||||
const [defaultFableModelName, setDefaultFableModelName] = useState(
|
||||
initial.fableName,
|
||||
);
|
||||
|
||||
const isUserEditingRef = useRef(false);
|
||||
const lastConfigRef = useRef(settingsConfig);
|
||||
@@ -134,6 +162,8 @@ export function useModelState({
|
||||
setDefaultSonnetModelName(parsed.sonnetName);
|
||||
setDefaultOpusModel(parsed.opus);
|
||||
setDefaultOpusModelName(parsed.opusName);
|
||||
setDefaultFableModel(parsed.fable);
|
||||
setDefaultFableModelName(parsed.fableName);
|
||||
}, [settingsConfig]);
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
@@ -152,6 +182,10 @@ export function useModelState({
|
||||
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value);
|
||||
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME")
|
||||
setDefaultOpusModelName(value);
|
||||
if (field === "ANTHROPIC_DEFAULT_FABLE_MODEL")
|
||||
setDefaultFableModel(value);
|
||||
if (field === "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME")
|
||||
setDefaultFableModelName(value);
|
||||
|
||||
try {
|
||||
const currentConfig = latestConfigRef.current
|
||||
@@ -195,6 +229,10 @@ export function useModelState({
|
||||
setDefaultOpusModel,
|
||||
defaultOpusModelName,
|
||||
setDefaultOpusModelName,
|
||||
defaultFableModel,
|
||||
setDefaultFableModel,
|
||||
defaultFableModelName,
|
||||
setDefaultFableModelName,
|
||||
handleModelChange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,13 +25,16 @@ export interface ClaudeDesktopRoutePreset {
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Desktop 3P fail-all 校验只接受 `claude-(sonnet|opus|haiku)-*` 形式的
|
||||
* routeId(1.6259.1+,实测 2026-05-13)。所有预设工厂、表单角色下拉、
|
||||
* 后端 `next_catalog_safe_route_id` 都从此映射派生 routeId,避免散落硬编码。
|
||||
* Claude Desktop 3P fail-all 校验接受的角色名。Desktop 1.12603.1+ 起白名单
|
||||
* 纳入 fable(app.asar 内 ["sonnet","opus","haiku","fable","mythos"],实测
|
||||
* 2026-06-13);此前 1.6259.1 仅接受 sonnet/opus/haiku。mythos 官方未公开
|
||||
* 发布,暂不暴露给用户。所有预设工厂、表单角色下拉、后端
|
||||
* `next_catalog_safe_route_id` 都从此映射派生 routeId,避免散落硬编码。
|
||||
*/
|
||||
export const CLAUDE_DESKTOP_ROLE_ROUTE_IDS = {
|
||||
sonnet: "claude-sonnet-4-6",
|
||||
opus: "claude-opus-4-8",
|
||||
fable: "claude-fable-5",
|
||||
haiku: "claude-haiku-4-5",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"routeModelLabel": "Model role",
|
||||
"routeRoleSonnet": "Sonnet",
|
||||
"routeRoleOpus": "Opus",
|
||||
"routeRoleFable": "Fable",
|
||||
"routeRoleHaiku": "Haiku",
|
||||
"labelOverrideLabel": "Menu display name",
|
||||
"upstreamModelLabel": "Requested model",
|
||||
@@ -1093,6 +1094,7 @@
|
||||
"modelRoleLabel": "Model role",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "Display name",
|
||||
"modelDisplayNamePlaceholder": "e.g. DeepSeek V4 Pro",
|
||||
@@ -1100,7 +1102,7 @@
|
||||
"modelOneMHeader": "Declare 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "Default fallback model",
|
||||
"fallbackModelHint": "Used only when a Claude Code request does not clearly map to Sonnet, Opus, or Haiku. Usually safe to leave blank.",
|
||||
"fallbackModelHint": "A fallback for requests that don't clearly map to Sonnet, Opus, Fable, or Haiku. Recommended for third-party/relay endpoints—otherwise such requests (including Haiku background subtasks) are forwarded under their original Claude model name and may fail if the upstream doesn't host it. Safe to leave blank for official endpoints.",
|
||||
"quickSetModels": "Quick Set",
|
||||
"quickSetSuccess": "Model name applied to all roles",
|
||||
"advancedOptionsToggle": "Advanced Options",
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"routeModelLabel": "モデル役割",
|
||||
"routeRoleSonnet": "Sonnet",
|
||||
"routeRoleOpus": "Opus",
|
||||
"routeRoleFable": "Fable",
|
||||
"routeRoleHaiku": "Haiku",
|
||||
"labelOverrideLabel": "メニュー表示名",
|
||||
"upstreamModelLabel": "リクエストモデル",
|
||||
@@ -1093,6 +1094,7 @@
|
||||
"modelRoleLabel": "モデル役割",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "表示名",
|
||||
"modelDisplayNamePlaceholder": "例: DeepSeek V4 Pro",
|
||||
@@ -1100,7 +1102,7 @@
|
||||
"modelOneMHeader": "1M 対応を宣言",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "既定フォールバックモデル",
|
||||
"fallbackModelHint": "Claude Code のリクエストが Sonnet、Opus、Haiku のいずれにも明確に対応しない場合のみ使われます。通常は空欄で構いません。",
|
||||
"fallbackModelHint": "Sonnet・Opus・Fable・Haiku のいずれにも明確に対応しないリクエストのフォールバックです。サードパーティ/中継エンドポイントでは設定を推奨します。設定しないと、これらのリクエスト(Haiku のバックグラウンドサブタスクを含む)が元の Claude モデル名のまま上流へ転送され、上流に該当モデルが無い場合は失敗することがあります。公式エンドポイントでは空欄で構いません。",
|
||||
"quickSetModels": "一括設定",
|
||||
"quickSetSuccess": "モデル名をすべての役割に適用しました",
|
||||
"advancedOptionsToggle": "高級オプション",
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"routeModelLabel": "模型角色",
|
||||
"routeRoleSonnet": "Sonnet",
|
||||
"routeRoleOpus": "Opus",
|
||||
"routeRoleFable": "Fable",
|
||||
"routeRoleHaiku": "Haiku",
|
||||
"labelOverrideLabel": "選單顯示名稱",
|
||||
"upstreamModelLabel": "實際請求模型",
|
||||
@@ -1065,6 +1066,7 @@
|
||||
"modelRoleLabel": "模型角色",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "顯示名稱",
|
||||
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
|
||||
@@ -1072,7 +1074,7 @@
|
||||
"modelOneMHeader": "聲明支援 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "備用模型",
|
||||
"fallbackModelHint": "僅在 Claude Code 請求沒有明確落到 Sonnet、Opus 或 Haiku 角色時使用;通常可以留空。",
|
||||
"fallbackModelHint": "用於未明確落到 Sonnet、Opus、Fable、Haiku 角色的請求。使用第三方/中轉端點時建議填寫:否則這些請求(含 Haiku 背景子任務)會以原始 Claude 模型名透傳給上游,可能因上游無此模型而報錯。官方端點可留空。",
|
||||
"quickSetModels": "一鍵設定",
|
||||
"quickSetSuccess": "已將模型名稱套用至所有角色",
|
||||
"advancedOptionsToggle": "進階選項",
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"routeModelLabel": "模型角色",
|
||||
"routeRoleSonnet": "Sonnet",
|
||||
"routeRoleOpus": "Opus",
|
||||
"routeRoleFable": "Fable",
|
||||
"routeRoleHaiku": "Haiku",
|
||||
"labelOverrideLabel": "菜单显示名",
|
||||
"upstreamModelLabel": "实际请求模型",
|
||||
@@ -1093,6 +1094,7 @@
|
||||
"modelRoleLabel": "模型角色",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "显示名称",
|
||||
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
|
||||
@@ -1100,7 +1102,7 @@
|
||||
"modelOneMHeader": "声明支持 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "默认兜底模型",
|
||||
"fallbackModelHint": "仅在 Claude Code 请求没有明确落到 Sonnet、Opus 或 Haiku 角色时使用;通常可以留空。",
|
||||
"fallbackModelHint": "用于未明确落到 Sonnet、Opus、Fable、Haiku 角色的请求。使用第三方/中转端点时建议填写:否则这些请求(含 Haiku 后台子任务)会以原始 Claude 模型名透传给上游,可能因上游无此模型而报错。官方端点可留空。",
|
||||
"quickSetModels": "一键设置",
|
||||
"quickSetSuccess": "已将模型名称应用到所有角色",
|
||||
"advancedOptionsToggle": "高级选项",
|
||||
|
||||
Reference in New Issue
Block a user