feat(claude-code): role-based model mapping with display names and 1M flag

- Replace the four flat env inputs with a Sonnet/Opus/Haiku role table.
  Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name
  field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a
  "Declare 1M" checkbox that toggles the [1M] suffix.
- Strip the [1M] context-capability marker before forwarding non-Copilot
  requests upstream. Copilot keeps its existing [1m]->-1m normalization.
- Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as
  label_override, closing the Claude Code -> Claude Desktop displayName
  pipeline; add_route's merge logic is shared between hashmap branches.
- Unify the [1M] marker as ONE_M_CONTEXT_MARKER across
  claude_desktop_config and proxy::model_mapper; rename the strip
  helper to strip_one_m_suffix_for_upstream.
- Collapse useModelState's seven duplicated useState initializers and
  the useEffect parse block into a single parseModelsFromConfig call.
- Add tests/hooks/useModelState.test.tsx and a Claude Desktop import
  test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.
This commit is contained in:
Jason
2026-05-13 17:06:05 +08:00
parent 84bac6dce6
commit 4b57f7e113
11 changed files with 550 additions and 169 deletions
@@ -5,6 +5,33 @@ interface UseModelStateProps {
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";
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
*/
@@ -22,18 +49,38 @@ function parseModelsFromConfig(settingsConfig: string) {
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);
return { model, haiku, sonnet, opus };
return { model, haiku, haikuName, sonnet, sonnetName, opus, opusName };
} catch {
return { model: "", haiku: "", sonnet: "", opus: "" };
return {
model: "",
haiku: "",
haikuName: "",
sonnet: "",
sonnetName: "",
opus: "",
opusName: "",
};
}
}
@@ -45,18 +92,19 @@ export function useModelState({
settingsConfig,
onConfigChange,
}: UseModelStateProps) {
// Initialize state by parsing config directly (fixes edit mode backfill)
const [claudeModel, setClaudeModel] = useState(
() => parseModelsFromConfig(settingsConfig).model,
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 [defaultHaikuModel, setDefaultHaikuModel] = useState(
() => parseModelsFromConfig(settingsConfig).haiku,
const [defaultSonnetModel, setDefaultSonnetModel] = useState(initial.sonnet);
const [defaultSonnetModelName, setDefaultSonnetModelName] = useState(
initial.sonnetName,
);
const [defaultSonnetModel, setDefaultSonnetModel] = useState(
() => parseModelsFromConfig(settingsConfig).sonnet,
);
const [defaultOpusModel, setDefaultOpusModel] = useState(
() => parseModelsFromConfig(settingsConfig).opus,
const [defaultOpusModel, setDefaultOpusModel] = useState(initial.opus);
const [defaultOpusModelName, setDefaultOpusModelName] = useState(
initial.opusName,
);
const isUserEditingRef = useRef(false);
@@ -65,72 +113,45 @@ export function useModelState({
latestConfigRef.current = settingsConfig;
// 初始化读取:读新键;若缺失,按兼容优先级回退
// Haiku: DEFAULT_HAIKU || SMALL_FAST || MODEL
// Sonnet: DEFAULT_SONNET || MODEL || SMALL_FAST
// Opus: DEFAULT_OPUS || MODEL || SMALL_FAST
// 仅在 settingsConfig 变化时同步一次(表单加载/切换预设时)
// 仅在 settingsConfig 外部变化时同步(表单加载 / 切换预设);
// 用户正在编辑时 (isUserEditingRef) 跳过一次以避免回填覆盖。
useEffect(() => {
if (lastConfigRef.current === settingsConfig) {
return;
}
if (isUserEditingRef.current) {
isUserEditingRef.current = false;
lastConfigRef.current = settingsConfig;
return;
}
lastConfigRef.current = settingsConfig;
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 sonnet =
typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === "string"
? env.ANTHROPIC_DEFAULT_SONNET_MODEL
: model || small;
const opus =
typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === "string"
? env.ANTHROPIC_DEFAULT_OPUS_MODEL
: model || small;
setClaudeModel(model || "");
setDefaultHaikuModel(haiku || "");
setDefaultSonnetModel(sonnet || "");
setDefaultOpusModel(opus || "");
} catch {
// ignore
}
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);
}, [settingsConfig]);
const handleModelChange = useCallback(
(
field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string,
) => {
(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);
try {
const currentConfig = latestConfigRef.current
@@ -164,10 +185,16 @@ export function useModelState({
setClaudeModel,
defaultHaikuModel,
setDefaultHaikuModel,
defaultHaikuModelName,
setDefaultHaikuModelName,
defaultSonnetModel,
setDefaultSonnetModel,
defaultSonnetModelName,
setDefaultSonnetModelName,
defaultOpusModel,
setDefaultOpusModel,
defaultOpusModelName,
setDefaultOpusModelName,
handleModelChange,
};
}