mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 14:35:22 +08:00
4f8a79c273
- 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.
239 lines
7.6 KiB
TypeScript
239 lines
7.6 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from "react";
|
|
|
|
interface UseModelStateProps {
|
|
settingsConfig: string;
|
|
onConfigChange: (config: string) => void;
|
|
}
|
|
|
|
export type ClaudeModelEnvField =
|
|
| "ANTHROPIC_MODEL"
|
|
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
|
|
| "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME"
|
|
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
|
|
| "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME"
|
|
| "ANTHROPIC_DEFAULT_OPUS_MODEL"
|
|
| "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME"
|
|
| "ANTHROPIC_DEFAULT_FABLE_MODEL"
|
|
| "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME";
|
|
|
|
export const CLAUDE_ONE_M_MARKER = "[1M]";
|
|
|
|
export function hasClaudeOneMMarker(model: string): boolean {
|
|
return model.trimEnd().toLowerCase().endsWith("[1m]");
|
|
}
|
|
|
|
export function stripClaudeOneMMarker(model: string): string {
|
|
const trimmedEnd = model.trimEnd();
|
|
if (!trimmedEnd.toLowerCase().endsWith("[1m]")) return model;
|
|
return trimmedEnd.slice(0, -CLAUDE_ONE_M_MARKER.length).trimEnd();
|
|
}
|
|
|
|
export function setClaudeOneMMarker(model: string, enabled: boolean): string {
|
|
const base = stripClaudeOneMMarker(model).trim();
|
|
if (!base) return "";
|
|
return enabled ? `${base}${CLAUDE_ONE_M_MARKER}` : base;
|
|
}
|
|
|
|
/**
|
|
* Parse model values from settings config JSON
|
|
*/
|
|
function parseModelsFromConfig(settingsConfig: string) {
|
|
try {
|
|
const cfg = settingsConfig ? JSON.parse(settingsConfig) : {};
|
|
const env = cfg?.env || {};
|
|
const model =
|
|
typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : "";
|
|
const small =
|
|
typeof env.ANTHROPIC_SMALL_FAST_MODEL === "string"
|
|
? env.ANTHROPIC_SMALL_FAST_MODEL
|
|
: "";
|
|
const haiku =
|
|
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string"
|
|
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
|
: small || model;
|
|
const haikuName =
|
|
typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME === "string"
|
|
? env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
|
|
: stripClaudeOneMMarker(haiku);
|
|
const sonnet =
|
|
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === "string"
|
|
? env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
|
: model || small;
|
|
const sonnetName =
|
|
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME === "string"
|
|
? env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
|
|
: stripClaudeOneMMarker(sonnet);
|
|
const opus =
|
|
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === "string"
|
|
? env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
|
: model || small;
|
|
const opusName =
|
|
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,
|
|
fable,
|
|
fableName,
|
|
};
|
|
} catch {
|
|
return {
|
|
model: "",
|
|
haiku: "",
|
|
haikuName: "",
|
|
sonnet: "",
|
|
sonnetName: "",
|
|
opus: "",
|
|
opusName: "",
|
|
fable: "",
|
|
fableName: "",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 管理模型选择状态
|
|
* 支持 ANTHROPIC_MODEL 和各类型默认模型
|
|
*/
|
|
export function useModelState({
|
|
settingsConfig,
|
|
onConfigChange,
|
|
}: UseModelStateProps) {
|
|
const initial = useState(() => parseModelsFromConfig(settingsConfig))[0];
|
|
const [claudeModel, setClaudeModel] = useState(initial.model);
|
|
const [defaultHaikuModel, setDefaultHaikuModel] = useState(initial.haiku);
|
|
const [defaultHaikuModelName, setDefaultHaikuModelName] = useState(
|
|
initial.haikuName,
|
|
);
|
|
const [defaultSonnetModel, setDefaultSonnetModel] = useState(initial.sonnet);
|
|
const [defaultSonnetModelName, setDefaultSonnetModelName] = useState(
|
|
initial.sonnetName,
|
|
);
|
|
const [defaultOpusModel, setDefaultOpusModel] = useState(initial.opus);
|
|
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);
|
|
const latestConfigRef = useRef(settingsConfig);
|
|
|
|
latestConfigRef.current = settingsConfig;
|
|
|
|
// 仅在 settingsConfig 外部变化时同步(表单加载 / 切换预设);
|
|
// 用户正在编辑时 (isUserEditingRef) 跳过一次以避免回填覆盖。
|
|
useEffect(() => {
|
|
if (lastConfigRef.current === settingsConfig) {
|
|
return;
|
|
}
|
|
if (isUserEditingRef.current) {
|
|
isUserEditingRef.current = false;
|
|
lastConfigRef.current = settingsConfig;
|
|
return;
|
|
}
|
|
lastConfigRef.current = settingsConfig;
|
|
|
|
const parsed = parseModelsFromConfig(settingsConfig);
|
|
setClaudeModel(parsed.model);
|
|
setDefaultHaikuModel(parsed.haiku);
|
|
setDefaultHaikuModelName(parsed.haikuName);
|
|
setDefaultSonnetModel(parsed.sonnet);
|
|
setDefaultSonnetModelName(parsed.sonnetName);
|
|
setDefaultOpusModel(parsed.opus);
|
|
setDefaultOpusModelName(parsed.opusName);
|
|
setDefaultFableModel(parsed.fable);
|
|
setDefaultFableModelName(parsed.fableName);
|
|
}, [settingsConfig]);
|
|
|
|
const handleModelChange = useCallback(
|
|
(field: ClaudeModelEnvField, value: string) => {
|
|
isUserEditingRef.current = true;
|
|
|
|
if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
|
|
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL")
|
|
setDefaultHaikuModel(value);
|
|
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME")
|
|
setDefaultHaikuModelName(value);
|
|
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL")
|
|
setDefaultSonnetModel(value);
|
|
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME")
|
|
setDefaultSonnetModelName(value);
|
|
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
|
|
? JSON.parse(latestConfigRef.current)
|
|
: { env: {} };
|
|
if (!currentConfig.env) currentConfig.env = {};
|
|
const env = currentConfig.env as Record<string, unknown>;
|
|
|
|
// 新键仅写入;旧键不再写入
|
|
const trimmed = value.trim();
|
|
if (trimmed) {
|
|
env[field] = trimmed;
|
|
} else {
|
|
delete env[field];
|
|
}
|
|
// 删除旧键
|
|
delete env["ANTHROPIC_SMALL_FAST_MODEL"];
|
|
|
|
const updatedConfig = JSON.stringify(currentConfig, null, 2);
|
|
latestConfigRef.current = updatedConfig;
|
|
onConfigChange(updatedConfig);
|
|
} catch (err) {
|
|
console.error("Failed to update model config:", err);
|
|
}
|
|
},
|
|
[onConfigChange],
|
|
);
|
|
|
|
return {
|
|
claudeModel,
|
|
setClaudeModel,
|
|
defaultHaikuModel,
|
|
setDefaultHaikuModel,
|
|
defaultHaikuModelName,
|
|
setDefaultHaikuModelName,
|
|
defaultSonnetModel,
|
|
setDefaultSonnetModel,
|
|
defaultSonnetModelName,
|
|
setDefaultSonnetModelName,
|
|
defaultOpusModel,
|
|
setDefaultOpusModel,
|
|
defaultOpusModelName,
|
|
setDefaultOpusModelName,
|
|
defaultFableModel,
|
|
setDefaultFableModel,
|
|
defaultFableModelName,
|
|
setDefaultFableModelName,
|
|
handleModelChange,
|
|
};
|
|
}
|