mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user