refactor(models): migrate to granular model configuration architecture

Upgrade Claude model configuration from dual-key to quad-key system for
better model tier differentiation.

**Breaking Changes:**
- Replace `ANTHROPIC_SMALL_FAST_MODEL` with three granular keys:
  - `ANTHROPIC_DEFAULT_HAIKU_MODEL`
  - `ANTHROPIC_DEFAULT_SONNET_MODEL`
  - `ANTHROPIC_DEFAULT_OPUS_MODEL`

**Backend (Rust):**
- Add `normalize_claude_models_in_value()` for automatic migration
- Implement fallback chain: `DEFAULT_* || SMALL_FAST || MODEL`
- Auto-cleanup: remove legacy `SMALL_FAST` key after normalization
- Apply normalization across 6 critical paths:
  - Add/update provider
  - Read from live config
  - Write to live config
  - Refresh config snapshot

**Frontend (React):**
- Expand UI from 2 to 4 model input fields
- Implement smart fallback in `useModelState` hook
- Update `useKimiModelSelector` for Kimi model picker
- Add i18n keys for Haiku/Sonnet/Opus labels (zh/en)

**Configuration:**
- Update all 7 provider presets to new format
- DeepSeek/Qwen/Moonshot: use same model for all tiers
- Zhipu: preserve tier differentiation (glm-4.5-air for Haiku)

**Backward Compatibility:**
- Old configs auto-upgrade on first read/write
- Fallback chain ensures graceful degradation
- No manual migration required

Closes #[issue-number]
This commit is contained in:
Jason
2025-11-02 18:02:22 +08:00
parent 2ebe34810c
commit 4811aa2dcd
9 changed files with 336 additions and 91 deletions

View File

@@ -21,8 +21,9 @@ export function useKimiModelSelector({
presetName = "",
}: UseKimiModelSelectorProps) {
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
useState("");
const [kimiDefaultHaikuModel, setKimiDefaultHaikuModel] = useState("");
const [kimiDefaultSonnetModel, setKimiDefaultSonnetModel] = useState("");
const [kimiDefaultOpusModel, setKimiDefaultOpusModel] = useState("");
// 判断是否显示 Kimi 模型选择器
const shouldShowKimiSelector =
@@ -53,12 +54,24 @@ export function useKimiModelSelector({
typeof config.env.ANTHROPIC_MODEL === "string"
? config.env.ANTHROPIC_MODEL
: "";
const smallFastModel =
typeof config.env.ANTHROPIC_SMALL_FAST_MODEL === "string"
? config.env.ANTHROPIC_SMALL_FAST_MODEL
: "";
const haiku =
typeof config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string"
? (config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL as string)
: (typeof config.env.ANTHROPIC_SMALL_FAST_MODEL === "string"
? (config.env.ANTHROPIC_SMALL_FAST_MODEL as string)
: model);
const sonnet =
typeof config.env.ANTHROPIC_DEFAULT_SONNET_MODEL === "string"
? (config.env.ANTHROPIC_DEFAULT_SONNET_MODEL as string)
: model;
const opus =
typeof config.env.ANTHROPIC_DEFAULT_OPUS_MODEL === "string"
? (config.env.ANTHROPIC_DEFAULT_OPUS_MODEL as string)
: model;
setKimiAnthropicModel(model);
setKimiAnthropicSmallFastModel(smallFastModel);
setKimiDefaultHaikuModel(haiku);
setKimiDefaultSonnetModel(sonnet);
setKimiDefaultOpusModel(opus);
}
}
}, [initialData]);
@@ -66,21 +79,25 @@ export function useKimiModelSelector({
// 处理 Kimi 模型变化
const handleKimiModelChange = useCallback(
(
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string,
) => {
if (field === "ANTHROPIC_MODEL") {
setKimiAnthropicModel(value);
} else {
setKimiAnthropicSmallFastModel(value);
}
if (field === "ANTHROPIC_MODEL") setKimiAnthropicModel(value);
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") setKimiDefaultHaikuModel(value);
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") setKimiDefaultSonnetModel(value);
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setKimiDefaultOpusModel(value);
// 更新配置 JSON
// 更新配置 JSON(只写新键并清理旧键)
try {
const currentConfig = JSON.parse(settingsConfig || "{}");
if (!currentConfig.env) currentConfig.env = {};
currentConfig.env[field] = value;
if (value.trim()) currentConfig.env[field] = value;
else delete currentConfig.env[field];
delete currentConfig.env["ANTHROPIC_SMALL_FAST_MODEL"];
const updatedConfigString = JSON.stringify(currentConfig, null, 2);
onConfigChange(updatedConfigString);
} catch (err) {
@@ -97,9 +114,16 @@ export function useKimiModelSelector({
const config = JSON.parse(settingsConfig);
if (config.env) {
const model = config.env.ANTHROPIC_MODEL || "";
const smallFastModel = config.env.ANTHROPIC_SMALL_FAST_MODEL || "";
const haiku =
config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ||
config.env.ANTHROPIC_SMALL_FAST_MODEL ||
model || "";
const sonnet = config.env.ANTHROPIC_DEFAULT_SONNET_MODEL || model || "";
const opus = config.env.ANTHROPIC_DEFAULT_OPUS_MODEL || model || "";
setKimiAnthropicModel(model);
setKimiAnthropicSmallFastModel(smallFastModel);
setKimiDefaultHaikuModel(haiku);
setKimiDefaultSonnetModel(sonnet);
setKimiDefaultOpusModel(opus);
}
} catch {
// ignore
@@ -110,7 +134,9 @@ export function useKimiModelSelector({
return {
shouldShow,
kimiAnthropicModel,
kimiAnthropicSmallFastModel,
kimiDefaultHaikuModel,
kimiDefaultSonnetModel,
kimiDefaultOpusModel,
handleKimiModelChange,
};
}

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
interface UseModelStateProps {
settingsConfig: string;
@@ -9,39 +9,76 @@ interface UseModelStateProps {
* 管理模型选择状态
* 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL
*/
export function useModelState({
settingsConfig,
onConfigChange,
}: UseModelStateProps) {
export function useModelState({ settingsConfig, onConfigChange }: UseModelStateProps) {
const [claudeModel, setClaudeModel] = useState("");
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
const [defaultHaikuModel, setDefaultHaikuModel] = useState("");
const [defaultSonnetModel, setDefaultSonnetModel] = useState("");
const [defaultOpusModel, setDefaultOpusModel] = useState("");
// 初始化读取:读新键;若缺失,按兼容优先级回退
// Haiku: DEFAULT_HAIKU || SMALL_FAST || MODEL
// Sonnet: DEFAULT_SONNET || MODEL || SMALL_FAST
// Opus: DEFAULT_OPUS || MODEL || SMALL_FAST
// 仅在 settingsConfig 变化时同步一次(表单加载/切换预设时)
useEffect(() => {
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
}
}, [settingsConfig]);
const handleModelChange = useCallback(
(
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string,
) => {
if (field === "ANTHROPIC_MODEL") {
setClaudeModel(value);
} else {
setClaudeSmallFastModel(value);
}
if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") setDefaultHaikuModel(value);
if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") setDefaultSonnetModel(value);
if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value);
try {
const currentConfig = settingsConfig
? JSON.parse(settingsConfig)
: { env: {} };
const currentConfig = settingsConfig ? JSON.parse(settingsConfig) : { env: {} };
if (!currentConfig.env) currentConfig.env = {};
if (value.trim()) {
currentConfig.env[field] = value.trim();
// 新键仅写入;旧键不再写入
const trimmed = value.trim();
if (trimmed) {
currentConfig.env[field] = trimmed;
} else {
delete currentConfig.env[field];
}
// 删除旧键
delete currentConfig.env["ANTHROPIC_SMALL_FAST_MODEL"];
onConfigChange(JSON.stringify(currentConfig, null, 2));
} catch (err) {
// 如果 JSON 解析失败,不做处理
console.error("Failed to update model config:", err);
}
},
@@ -51,8 +88,12 @@ export function useModelState({
return {
claudeModel,
setClaudeModel,
claudeSmallFastModel,
setClaudeSmallFastModel,
defaultHaikuModel,
setDefaultHaikuModel,
defaultSonnetModel,
setDefaultSonnetModel,
defaultOpusModel,
setDefaultOpusModel,
handleModelChange,
};
}