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

@@ -112,6 +112,74 @@ mod tests {
} }
impl ProviderService { impl ProviderService {
/// 归一化 Claude 模型键:读旧键(ANTHROPIC_SMALL_FAST_MODEL),写新键(DEFAULT_*), 并删除旧键
fn normalize_claude_models_in_value(settings: &mut Value) -> bool {
let mut changed = false;
let env = match settings.get_mut("env") {
Some(v) if v.is_object() => v.as_object_mut().unwrap(),
_ => return changed,
};
let model = env
.get("ANTHROPIC_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let small_fast = env
.get("ANTHROPIC_SMALL_FAST_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let current_haiku = env
.get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let current_sonnet = env
.get("ANTHROPIC_DEFAULT_SONNET_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let current_opus = env
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let target_haiku = current_haiku.or_else(|| small_fast.clone()).or_else(|| model.clone());
let target_sonnet = current_sonnet.or_else(|| model.clone()).or_else(|| small_fast.clone());
let target_opus = current_opus.or_else(|| model.clone()).or_else(|| small_fast.clone());
if env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL").is_none() {
if let Some(v) = target_haiku {
env.insert("ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(), Value::String(v));
changed = true;
}
}
if env.get("ANTHROPIC_DEFAULT_SONNET_MODEL").is_none() {
if let Some(v) = target_sonnet {
env.insert("ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(), Value::String(v));
changed = true;
}
}
if env.get("ANTHROPIC_DEFAULT_OPUS_MODEL").is_none() {
if let Some(v) = target_opus {
env.insert("ANTHROPIC_DEFAULT_OPUS_MODEL".to_string(), Value::String(v));
changed = true;
}
}
if env.remove("ANTHROPIC_SMALL_FAST_MODEL").is_some() {
changed = true;
}
changed
}
fn normalize_provider_if_claude(app_type: &AppType, provider: &mut Provider) {
if matches!(app_type, AppType::Claude) {
let mut v = provider.settings_config.clone();
if Self::normalize_claude_models_in_value(&mut v) {
provider.settings_config = v;
}
}
}
fn run_transaction<R, F>(state: &AppState, f: F) -> Result<R, AppError> fn run_transaction<R, F>(state: &AppState, f: F) -> Result<R, AppError>
where where
F: FnOnce(&mut MultiAppConfig) -> Result<(R, Option<PostCommitAction>), AppError>, F: FnOnce(&mut MultiAppConfig) -> Result<(R, Option<PostCommitAction>), AppError>,
@@ -209,7 +277,8 @@ impl ProviderService {
"Claude settings file missing; cannot refresh snapshot", "Claude settings file missing; cannot refresh snapshot",
)); ));
} }
let live_after = read_json_file::<Value>(&settings_path)?; let mut live_after = read_json_file::<Value>(&settings_path)?;
let _ = Self::normalize_claude_models_in_value(&mut live_after);
{ {
let mut guard = state.config.write().map_err(AppError::from)?; let mut guard = state.config.write().map_err(AppError::from)?;
if let Some(manager) = guard.get_manager_mut(app_type) { if let Some(manager) = guard.get_manager_mut(app_type) {
@@ -308,6 +377,9 @@ impl ProviderService {
/// 新增供应商 /// 新增供应商
pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> { pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
let mut provider = provider;
// 归一化 Claude 模型键
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?; Self::validate_provider_settings(&app_type, &provider)?;
let app_type_clone = app_type.clone(); let app_type_clone = app_type.clone();
@@ -347,6 +419,9 @@ impl ProviderService {
app_type: AppType, app_type: AppType,
provider: Provider, provider: Provider,
) -> Result<bool, AppError> { ) -> Result<bool, AppError> {
let mut provider = provider;
// 归一化 Claude 模型键
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?; Self::validate_provider_settings(&app_type, &provider)?;
let provider_id = provider.id.clone(); let provider_id = provider.id.clone();
let app_type_clone = app_type.clone(); let app_type_clone = app_type.clone();
@@ -440,7 +515,9 @@ impl ProviderService {
"Claude settings file is missing", "Claude settings file is missing",
)); ));
} }
read_json_file(&settings_path)? let mut v = read_json_file::<Value>(&settings_path)?;
let _ = Self::normalize_claude_models_in_value(&mut v);
v
} }
}; };
@@ -848,7 +925,8 @@ impl ProviderService {
return Ok(()); return Ok(());
} }
let live = read_json_file::<Value>(&settings_path)?; let mut live = read_json_file::<Value>(&settings_path)?;
let _ = Self::normalize_claude_models_in_value(&mut live);
if let Some(manager) = config.get_manager_mut(&AppType::Claude) { if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
if let Some(current) = manager.providers.get_mut(&current_id) { if let Some(current) = manager.providers.get_mut(&current_id) {
current.settings_config = live; current.settings_config = live;
@@ -864,7 +942,10 @@ impl ProviderService {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
} }
write_json_file(&settings_path, &provider.settings_config)?; // 归一化后再写入
let mut content = provider.settings_config.clone();
let _ = Self::normalize_claude_models_in_value(&mut content);
write_json_file(&settings_path, &content)?;
Ok(()) Ok(())
} }

View File

@@ -38,17 +38,29 @@ interface ClaudeFormFieldsProps {
shouldShowKimiSelector: boolean; shouldShowKimiSelector: boolean;
shouldShowModelSelector: boolean; shouldShowModelSelector: boolean;
claudeModel: string; claudeModel: string;
claudeSmallFastModel: string; defaultHaikuModel: string;
defaultSonnetModel: string;
defaultOpusModel: string;
onModelChange: ( onModelChange: (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string, value: string,
) => void; ) => void;
// Kimi Model Selector // Kimi Model Selector
kimiAnthropicModel: string; kimiAnthropicModel: string;
kimiAnthropicSmallFastModel: string; kimiDefaultHaikuModel: string;
kimiDefaultSonnetModel: string;
kimiDefaultOpusModel: string;
onKimiModelChange: ( onKimiModelChange: (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string, value: string,
) => void; ) => void;
@@ -76,10 +88,14 @@ export function ClaudeFormFields({
shouldShowKimiSelector, shouldShowKimiSelector,
shouldShowModelSelector, shouldShowModelSelector,
claudeModel, claudeModel,
claudeSmallFastModel, defaultHaikuModel,
defaultSonnetModel,
defaultOpusModel,
onModelChange, onModelChange,
kimiAnthropicModel, kimiAnthropicModel,
kimiAnthropicSmallFastModel, kimiDefaultHaikuModel,
kimiDefaultSonnetModel,
kimiDefaultOpusModel,
onKimiModelChange, onKimiModelChange,
speedTestEndpoints, speedTestEndpoints,
}: ClaudeFormFieldsProps) { }: ClaudeFormFieldsProps) {
@@ -163,19 +179,53 @@ export function ClaudeFormFields({
{shouldShowModelSelector && ( {shouldShowModelSelector && (
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* ANTHROPIC_MODEL */} {/* 主模型 */}
<div className="space-y-2"> <div className="space-y-2">
<FormLabel htmlFor="claudeModel"> <FormLabel htmlFor="claudeModel">
{t("providerForm.anthropicModel", { {t("providerForm.anthropicModel", { defaultValue: "主模型" })}
defaultValue: "主模型",
})}
</FormLabel> </FormLabel>
<Input <Input
id="claudeModel" id="claudeModel"
type="text" type="text"
value={claudeModel} value={claudeModel}
onChange={(e) => onModelChange("ANTHROPIC_MODEL", e.target.value)}
placeholder={t("providerForm.modelPlaceholder", {
defaultValue: "claude-3-7-sonnet-20250219",
})}
autoComplete="off"
/>
</div>
{/* 默认 Haiku */}
<div className="space-y-2">
<FormLabel htmlFor="claudeDefaultHaikuModel">
{t("providerForm.anthropicDefaultHaikuModel", { defaultValue: "Haiku 默认模型" })}
</FormLabel>
<Input
id="claudeDefaultHaikuModel"
type="text"
value={defaultHaikuModel}
onChange={(e) => onChange={(e) =>
onModelChange("ANTHROPIC_MODEL", e.target.value) onModelChange("ANTHROPIC_DEFAULT_HAIKU_MODEL", e.target.value)
}
placeholder={t("providerForm.modelPlaceholder", {
defaultValue: "claude-3-5-haiku-20241022",
})}
autoComplete="off"
/>
</div>
{/* 默认 Sonnet */}
<div className="space-y-2">
<FormLabel htmlFor="claudeDefaultSonnetModel">
{t("providerForm.anthropicDefaultSonnetModel", { defaultValue: "Sonnet 默认模型" })}
</FormLabel>
<Input
id="claudeDefaultSonnetModel"
type="text"
value={defaultSonnetModel}
onChange={(e) =>
onModelChange("ANTHROPIC_DEFAULT_SONNET_MODEL", e.target.value)
} }
placeholder={t("providerForm.modelPlaceholder", { placeholder={t("providerForm.modelPlaceholder", {
defaultValue: "claude-3-7-sonnet-20250219", defaultValue: "claude-3-7-sonnet-20250219",
@@ -184,22 +234,20 @@ export function ClaudeFormFields({
/> />
</div> </div>
{/* ANTHROPIC_SMALL_FAST_MODEL */} {/* 默认 Opus */}
<div className="space-y-2"> <div className="space-y-2">
<FormLabel htmlFor="claudeSmallFastModel"> <FormLabel htmlFor="claudeDefaultOpusModel">
{t("providerForm.anthropicSmallFastModel", { {t("providerForm.anthropicDefaultOpusModel", { defaultValue: "Opus 默认模型" })}
defaultValue: "快速模型",
})}
</FormLabel> </FormLabel>
<Input <Input
id="claudeSmallFastModel" id="claudeDefaultOpusModel"
type="text" type="text"
value={claudeSmallFastModel} value={defaultOpusModel}
onChange={(e) => onChange={(e) =>
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", e.target.value) onModelChange("ANTHROPIC_DEFAULT_OPUS_MODEL", e.target.value)
} }
placeholder={t("providerForm.smallModelPlaceholder", { placeholder={t("providerForm.modelPlaceholder", {
defaultValue: "claude-3-5-haiku-20241022", defaultValue: "claude-3-7-opus-20250219",
})} })}
autoComplete="off" autoComplete="off"
/> />
@@ -207,8 +255,7 @@ export function ClaudeFormFields({
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("providerForm.modelHelper", { {t("providerForm.modelHelper", {
defaultValue: defaultValue: "可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
"可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
})} })}
</p> </p>
</div> </div>
@@ -219,7 +266,9 @@ export function ClaudeFormFields({
<KimiModelSelector <KimiModelSelector
apiKey={apiKey} apiKey={apiKey}
anthropicModel={kimiAnthropicModel} anthropicModel={kimiAnthropicModel}
anthropicSmallFastModel={kimiAnthropicSmallFastModel} defaultHaikuModel={kimiDefaultHaikuModel}
defaultSonnetModel={kimiDefaultSonnetModel}
defaultOpusModel={kimiDefaultOpusModel}
onModelChange={onKimiModelChange} onModelChange={onKimiModelChange}
disabled={category === "official"} disabled={category === "official"}
/> />

View File

@@ -12,9 +12,15 @@ interface KimiModel {
interface KimiModelSelectorProps { interface KimiModelSelectorProps {
apiKey: string; apiKey: string;
anthropicModel: string; anthropicModel: string;
anthropicSmallFastModel: string; defaultHaikuModel: string;
defaultSonnetModel: string;
defaultOpusModel: string;
onModelChange: ( onModelChange: (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field:
| "ANTHROPIC_MODEL"
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
value: string, value: string,
) => void; ) => void;
disabled?: boolean; disabled?: boolean;
@@ -23,7 +29,9 @@ interface KimiModelSelectorProps {
const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
apiKey, apiKey,
anthropicModel, anthropicModel,
anthropicSmallFastModel, defaultHaikuModel,
defaultSonnetModel,
defaultOpusModel,
onModelChange, onModelChange,
disabled = false, disabled = false,
}) => { }) => {
@@ -173,11 +181,19 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)} onChange={(value) => onModelChange("ANTHROPIC_MODEL", value)}
/> />
<ModelSelect <ModelSelect
label={t("kimiSelector.fastModel")} label={t("kimiSelector.haikuModel", { defaultValue: "Haiku 默认" })}
value={anthropicSmallFastModel} value={defaultHaikuModel}
onChange={(value) => onChange={(value) => onModelChange("ANTHROPIC_DEFAULT_HAIKU_MODEL", value)}
onModelChange("ANTHROPIC_SMALL_FAST_MODEL", value) />
} <ModelSelect
label={t("kimiSelector.sonnetModel", { defaultValue: "Sonnet 默认" })}
value={defaultSonnetModel}
onChange={(value) => onModelChange("ANTHROPIC_DEFAULT_SONNET_MODEL", value)}
/>
<ModelSelect
label={t("kimiSelector.opusModel", { defaultValue: "Opus 默认" })}
value={defaultOpusModel}
onChange={(value) => onModelChange("ANTHROPIC_DEFAULT_OPUS_MODEL", value)}
/> />
</div> </div>

View File

@@ -140,12 +140,17 @@ export function ProviderForm({
}, },
}); });
// 使用 Model hook // 使用 Model hook(新:主模型 + Haiku/Sonnet/Opus 默认模型)
const { claudeModel, claudeSmallFastModel, handleModelChange } = const {
useModelState({ claudeModel,
settingsConfig: form.watch("settingsConfig"), defaultHaikuModel,
onConfigChange: (config) => form.setValue("settingsConfig", config), defaultSonnetModel,
}); defaultOpusModel,
handleModelChange,
} = useModelState({
settingsConfig: form.watch("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
});
// 使用 Codex 配置 hook (仅 Codex 模式) // 使用 Codex 配置 hook (仅 Codex 模式)
const { const {
@@ -218,7 +223,9 @@ export function ProviderForm({
const { const {
shouldShow: shouldShowKimiSelector, shouldShow: shouldShowKimiSelector,
kimiAnthropicModel, kimiAnthropicModel,
kimiAnthropicSmallFastModel, kimiDefaultHaikuModel,
kimiDefaultSonnetModel,
kimiDefaultOpusModel,
handleKimiModelChange, handleKimiModelChange,
} = useKimiModelSelector({ } = useKimiModelSelector({
initialData, initialData,
@@ -500,10 +507,14 @@ export function ProviderForm({
category !== "official" && !shouldShowKimiSelector category !== "official" && !shouldShowKimiSelector
} }
claudeModel={claudeModel} claudeModel={claudeModel}
claudeSmallFastModel={claudeSmallFastModel} defaultHaikuModel={defaultHaikuModel}
defaultSonnetModel={defaultSonnetModel}
defaultOpusModel={defaultOpusModel}
onModelChange={handleModelChange} onModelChange={handleModelChange}
kimiAnthropicModel={kimiAnthropicModel} kimiAnthropicModel={kimiAnthropicModel}
kimiAnthropicSmallFastModel={kimiAnthropicSmallFastModel} kimiDefaultHaikuModel={kimiDefaultHaikuModel}
kimiDefaultSonnetModel={kimiDefaultSonnetModel}
kimiDefaultOpusModel={kimiDefaultOpusModel}
onKimiModelChange={handleKimiModelChange} onKimiModelChange={handleKimiModelChange}
speedTestEndpoints={speedTestEndpoints} speedTestEndpoints={speedTestEndpoints}
/> />

View File

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

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from "react"; import { useState, useCallback, useEffect } from "react";
interface UseModelStateProps { interface UseModelStateProps {
settingsConfig: string; settingsConfig: string;
@@ -9,39 +9,76 @@ interface UseModelStateProps {
* 管理模型选择状态 * 管理模型选择状态
* 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL * 支持 ANTHROPIC_MODEL 和 ANTHROPIC_SMALL_FAST_MODEL
*/ */
export function useModelState({ export function useModelState({ settingsConfig, onConfigChange }: UseModelStateProps) {
settingsConfig,
onConfigChange,
}: UseModelStateProps) {
const [claudeModel, setClaudeModel] = useState(""); 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( 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, value: string,
) => { ) => {
if (field === "ANTHROPIC_MODEL") { if (field === "ANTHROPIC_MODEL") setClaudeModel(value);
setClaudeModel(value); if (field === "ANTHROPIC_DEFAULT_HAIKU_MODEL") setDefaultHaikuModel(value);
} else { if (field === "ANTHROPIC_DEFAULT_SONNET_MODEL") setDefaultSonnetModel(value);
setClaudeSmallFastModel(value); if (field === "ANTHROPIC_DEFAULT_OPUS_MODEL") setDefaultOpusModel(value);
}
try { try {
const currentConfig = settingsConfig const currentConfig = settingsConfig ? JSON.parse(settingsConfig) : { env: {} };
? JSON.parse(settingsConfig)
: { env: {} };
if (!currentConfig.env) currentConfig.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 { } else {
delete currentConfig.env[field]; delete currentConfig.env[field];
} }
// 删除旧键
delete currentConfig.env["ANTHROPIC_SMALL_FAST_MODEL"];
onConfigChange(JSON.stringify(currentConfig, null, 2)); onConfigChange(JSON.stringify(currentConfig, null, 2));
} catch (err) { } catch (err) {
// 如果 JSON 解析失败,不做处理
console.error("Failed to update model config:", err); console.error("Failed to update model config:", err);
} }
}, },
@@ -51,8 +88,12 @@ export function useModelState({
return { return {
claudeModel, claudeModel,
setClaudeModel, setClaudeModel,
claudeSmallFastModel, defaultHaikuModel,
setClaudeSmallFastModel, setDefaultHaikuModel,
defaultSonnetModel,
setDefaultSonnetModel,
defaultOpusModel,
setDefaultOpusModel,
handleModelChange, handleModelChange,
}; };
} }

View File

@@ -61,7 +61,9 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic", ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "DeepSeek-V3.2-Exp", ANTHROPIC_MODEL: "DeepSeek-V3.2-Exp",
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.2-Exp", ANTHROPIC_DEFAULT_HAIKU_MODEL: "DeepSeek-V3.2-Exp",
ANTHROPIC_DEFAULT_SONNET_MODEL: "DeepSeek-V3.2-Exp",
ANTHROPIC_DEFAULT_OPUS_MODEL: "DeepSeek-V3.2-Exp",
}, },
}, },
category: "cn_official", category: "cn_official",
@@ -75,7 +77,6 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
// 兼容旧键名,保持前端读取一致 // 兼容旧键名,保持前端读取一致
ANTHROPIC_MODEL: "GLM-4.6", ANTHROPIC_MODEL: "GLM-4.6",
ANTHROPIC_SMALL_FAST_MODEL: "glm-4.5-air",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.5-air", ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.5-air",
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.6", ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.6",
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.6", ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.6",
@@ -92,7 +93,9 @@ export const providerPresets: ProviderPreset[] = [
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy", "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "qwen3-max", ANTHROPIC_MODEL: "qwen3-max",
ANTHROPIC_SMALL_FAST_MODEL: "qwen3-max", ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3-max",
ANTHROPIC_DEFAULT_SONNET_MODEL: "qwen3-max",
ANTHROPIC_DEFAULT_OPUS_MODEL: "qwen3-max",
}, },
}, },
category: "cn_official", category: "cn_official",
@@ -105,7 +108,9 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic", ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "kimi-k2-turbo-preview", ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview", ANTHROPIC_DEFAULT_HAIKU_MODEL: "kimi-k2-turbo-preview",
ANTHROPIC_DEFAULT_SONNET_MODEL: "kimi-k2-turbo-preview",
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-k2-turbo-preview",
}, },
}, },
category: "cn_official", category: "cn_official",
@@ -118,7 +123,9 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn", ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.6", ANTHROPIC_MODEL: "ZhipuAI/GLM-4.6",
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.6", ANTHROPIC_DEFAULT_HAIKU_MODEL: "ZhipuAI/GLM-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "ZhipuAI/GLM-4.6",
ANTHROPIC_DEFAULT_OPUS_MODEL: "ZhipuAI/GLM-4.6",
}, },
}, },
category: "aggregator", category: "aggregator",
@@ -133,7 +140,9 @@ export const providerPresets: ProviderPreset[] = [
"https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy", "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "KAT-Coder", ANTHROPIC_MODEL: "KAT-Coder",
ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder", ANTHROPIC_DEFAULT_HAIKU_MODEL: "KAT-Coder",
ANTHROPIC_DEFAULT_SONNET_MODEL: "KAT-Coder",
ANTHROPIC_DEFAULT_OPUS_MODEL: "KAT-Coder",
}, },
}, },
category: "cn_official", category: "cn_official",
@@ -155,7 +164,7 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://api.longcat.chat/anthropic", ANTHROPIC_BASE_URL: "https://api.longcat.chat/anthropic",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "LongCat-Flash-Chat", ANTHROPIC_MODEL: "LongCat-Flash-Chat",
ANTHROPIC_SMALL_FAST_MODEL: "LongCat-Flash-Chat", ANTHROPIC_DEFAULT_HAIKU_MODEL: "LongCat-Flash-Chat",
ANTHROPIC_DEFAULT_SONNET_MODEL: "LongCat-Flash-Chat", ANTHROPIC_DEFAULT_SONNET_MODEL: "LongCat-Flash-Chat",
ANTHROPIC_DEFAULT_OPUS_MODEL: "LongCat-Flash-Chat", ANTHROPIC_DEFAULT_OPUS_MODEL: "LongCat-Flash-Chat",
CLAUDE_CODE_MAX_OUTPUT_TOKENS: "6000", CLAUDE_CODE_MAX_OUTPUT_TOKENS: "6000",

View File

@@ -258,6 +258,9 @@
"visitWebsite": "Visit {{url}}", "visitWebsite": "Visit {{url}}",
"anthropicModel": "Main Model", "anthropicModel": "Main Model",
"anthropicSmallFastModel": "Fast Model", "anthropicSmallFastModel": "Fast Model",
"anthropicDefaultHaikuModel": "Default Haiku Model",
"anthropicDefaultSonnetModel": "Default Sonnet Model",
"anthropicDefaultOpusModel": "Default Opus Model",
"modelPlaceholder": "GLM-4.6", "modelPlaceholder": "GLM-4.6",
"smallModelPlaceholder": "GLM-4.5-Air", "smallModelPlaceholder": "GLM-4.5-Air",
"modelHelper": "Optional: Specify default Claude model to use, leave blank to use system default.", "modelHelper": "Optional: Specify default Claude model to use, leave blank to use system default.",
@@ -373,6 +376,9 @@
"modelConfig": "Model Configuration", "modelConfig": "Model Configuration",
"mainModel": "Main Model", "mainModel": "Main Model",
"fastModel": "Fast Model", "fastModel": "Fast Model",
"haikuModel": "Default Haiku",
"sonnetModel": "Default Sonnet",
"opusModel": "Default Opus",
"refreshModels": "Refresh Model List", "refreshModels": "Refresh Model List",
"pleaseSelectModel": "Please select a model", "pleaseSelectModel": "Please select a model",
"noModels": "No models available", "noModels": "No models available",

View File

@@ -258,6 +258,9 @@
"visitWebsite": "访问 {{url}}", "visitWebsite": "访问 {{url}}",
"anthropicModel": "主模型", "anthropicModel": "主模型",
"anthropicSmallFastModel": "快速模型", "anthropicSmallFastModel": "快速模型",
"anthropicDefaultHaikuModel": "Haiku 默认模型",
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
"anthropicDefaultOpusModel": "Opus 默认模型",
"modelPlaceholder": "GLM-4.6", "modelPlaceholder": "GLM-4.6",
"smallModelPlaceholder": "GLM-4.5-Air", "smallModelPlaceholder": "GLM-4.5-Air",
"modelHelper": "可选:指定默认使用的 Claude 模型,留空则使用系统默认。", "modelHelper": "可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
@@ -373,6 +376,9 @@
"modelConfig": "模型配置", "modelConfig": "模型配置",
"mainModel": "主模型", "mainModel": "主模型",
"fastModel": "快速模型", "fastModel": "快速模型",
"haikuModel": "Haiku 默认",
"sonnetModel": "Sonnet 默认",
"opusModel": "Opus 默认",
"refreshModels": "刷新模型列表", "refreshModels": "刷新模型列表",
"pleaseSelectModel": "请选择模型", "pleaseSelectModel": "请选择模型",
"noModels": "暂无模型", "noModels": "暂无模型",