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:
Jason
2026-06-13 19:11:59 +08:00
parent 6e519a7496
commit 4f8a79c273
13 changed files with 343 additions and 41 deletions
@@ -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,
};
}