mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat: add Claude subagent model config (#4830)
* feat: add Claude subagent takeover config * feat: add Claude subagent model field * i18n: add Claude subagent model labels * fix(proxy): preserve configured subagent model mapping * fix(providers): exclude subagent model from Claude common config * style: format rust code
This commit is contained in:
@@ -12,6 +12,7 @@ pub struct ModelMapping {
|
||||
pub sonnet_model: Option<String>,
|
||||
pub opus_model: Option<String>,
|
||||
pub fable_model: Option<String>,
|
||||
pub subagent_model: Option<String>,
|
||||
pub default_model: Option<String>,
|
||||
}
|
||||
|
||||
@@ -41,6 +42,11 @@ impl ModelMapping {
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
subagent_model: env
|
||||
.and_then(|e| e.get("CLAUDE_CODE_SUBAGENT_MODEL"))
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from),
|
||||
default_model: env
|
||||
.and_then(|e| e.get("ANTHROPIC_MODEL"))
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -55,6 +61,7 @@ impl ModelMapping {
|
||||
|| self.sonnet_model.is_some()
|
||||
|| self.opus_model.is_some()
|
||||
|| self.fable_model.is_some()
|
||||
|| self.subagent_model.is_some()
|
||||
|| self.default_model.is_some()
|
||||
}
|
||||
|
||||
@@ -89,6 +96,13 @@ impl ModelMapping {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref m) = self.subagent_model {
|
||||
if strip_one_m_suffix_for_upstream(original_model) == strip_one_m_suffix_for_upstream(m)
|
||||
{
|
||||
return original_model.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 默认模型
|
||||
if let Some(ref m) = self.default_model {
|
||||
return m.clone();
|
||||
@@ -327,6 +341,42 @@ mod tests {
|
||||
assert_eq!(mapped, Some("default-model".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subagent_model_preserved_before_default_fallback() {
|
||||
let mut provider = create_provider_with_mapping();
|
||||
provider.settings_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_MODEL": "default-model",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini"
|
||||
}
|
||||
});
|
||||
|
||||
let body = json!({"model": "gpt-5.4-mini"});
|
||||
let (result, original, mapped) = apply_model_mapping(body, &provider);
|
||||
|
||||
assert_eq!(result["model"], "gpt-5.4-mini");
|
||||
assert_eq!(original, Some("gpt-5.4-mini".to_string()));
|
||||
assert!(mapped.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subagent_model_preserved_with_one_m_suffix_before_default_fallback() {
|
||||
let mut provider = create_provider_with_mapping();
|
||||
provider.settings_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_MODEL": "default-model",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini"
|
||||
}
|
||||
});
|
||||
|
||||
let body = json!({"model": "gpt-5.4-mini[1M]"});
|
||||
let (result, original, mapped) = apply_model_mapping(body, &provider);
|
||||
|
||||
assert_eq!(result["model"], "gpt-5.4-mini[1M]");
|
||||
assert_eq!(original, Some("gpt-5.4-mini[1M]".to_string()));
|
||||
assert!(mapped.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_mapping_configured() {
|
||||
let provider = create_provider_without_mapping();
|
||||
|
||||
@@ -672,6 +672,7 @@ mod tests {
|
||||
"AWS_BEARER_TOKEN_BEDROCK": "bedrock-tok",
|
||||
"ANTHROPIC_BASE_URL": "https://example.com",
|
||||
"ANTHROPIC_MODEL": "claude-x",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "gpt-5.4-mini",
|
||||
// 可共享、非机密配置(复数 _TOKENS 不应被误剥)
|
||||
"ENABLE_TOOL_SEARCH": "true",
|
||||
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "8192"
|
||||
@@ -714,6 +715,9 @@ mod tests {
|
||||
// 端点/模型(provider-specific 非机密)也应剥掉
|
||||
assert!(env.and_then(|e| e.get("ANTHROPIC_BASE_URL")).is_none());
|
||||
assert!(env.and_then(|e| e.get("ANTHROPIC_MODEL")).is_none());
|
||||
assert!(env
|
||||
.and_then(|e| e.get("CLAUDE_CODE_SUBAGENT_MODEL"))
|
||||
.is_none());
|
||||
|
||||
// 可共享的非机密配置必须保留(含复数 _TOKENS 不被误剥)
|
||||
assert_eq!(
|
||||
@@ -2635,6 +2639,7 @@ impl ProviderService {
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
"ANTHROPIC_BASE_URL",
|
||||
];
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const PROXY_TOKEN_PLACEHOLDER: &str = "PROXY_MANAGED";
|
||||
/// 原因:接管模式下 `*_MODEL` 必须由 CC Switch 写成稳定的 Claude 角色别名,
|
||||
/// 再由本地代理映射到当前供应商真实模型;`*_MODEL_NAME` 也需要同步接管,
|
||||
/// 否则 Claude Code 模型菜单会残留上一个供应商的显示名称。
|
||||
const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 9] = [
|
||||
const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 10] = [
|
||||
"ANTHROPIC_MODEL",
|
||||
"ANTHROPIC_REASONING_MODEL", // legacy: 已废弃,但旧配置可能残留
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
@@ -35,8 +35,8 @@ const CLAUDE_MODEL_OVERRIDE_ENV_KEYS: [&str; 9] = [
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
// Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型
|
||||
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||
"ANTHROPIC_SMALL_FAST_MODEL", // Legacy key (已废弃):历史版本使用该字段区分 small/fast 模型
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
];
|
||||
|
||||
const CLAUDE_TAKEOVER_HAIKU_MODEL: &str = "claude-haiku-4-5";
|
||||
@@ -222,7 +222,9 @@ impl ProxyService {
|
||||
.or(default_model)
|
||||
.or(small_fast_model);
|
||||
|
||||
let mut fields = Vec::with_capacity(6);
|
||||
let subagent_model = Self::claude_env_string(env, "CLAUDE_CODE_SUBAGENT_MODEL");
|
||||
|
||||
let mut fields = Vec::with_capacity(7);
|
||||
Self::push_claude_takeover_role_fields(
|
||||
&mut fields,
|
||||
env,
|
||||
@@ -250,6 +252,9 @@ impl ProxyService {
|
||||
true,
|
||||
opus_model,
|
||||
);
|
||||
if let Some(subagent_model) = subagent_model {
|
||||
fields.push(("CLAUDE_CODE_SUBAGENT_MODEL", subagent_model.to_string()));
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
@@ -2857,7 +2862,8 @@ mod tests {
|
||||
"ANTHROPIC_MODEL": "claude-sonnet-4.6",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-4.6"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-4.6",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4.6[1M]"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
@@ -2877,7 +2883,8 @@ mod tests {
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "stale-sonnet",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "stale-opus",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
}
|
||||
});
|
||||
ProxyService::apply_claude_takeover_fields_for_provider(
|
||||
@@ -2917,10 +2924,53 @@ mod tests {
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
Some("claude-sonnet-4.6"),
|
||||
);
|
||||
assert_env_str(
|
||||
env,
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
Some("claude-sonnet-4.6[1M]"),
|
||||
);
|
||||
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_account_claude_takeover_removes_stale_subagent_model_when_provider_omits_it() {
|
||||
let mut provider = Provider::with_id(
|
||||
"codex".to_string(),
|
||||
"Codex".to_string(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "provider-sonnet"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
provider.meta = Some(ProviderMeta {
|
||||
provider_type: Some("codex_oauth".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut live_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://stale.example.com",
|
||||
"ANTHROPIC_API_KEY": "stale-key",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
}
|
||||
});
|
||||
ProxyService::apply_claude_takeover_fields_for_provider(
|
||||
&mut live_config,
|
||||
"http://127.0.0.1:15721",
|
||||
&provider,
|
||||
);
|
||||
|
||||
let env = live_config
|
||||
.get("env")
|
||||
.and_then(|value| value.as_object())
|
||||
.expect("env should exist");
|
||||
assert_env_str(env, "CLAUDE_CODE_SUBAGENT_MODEL", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_account_claude_takeover_sources_codex_models_from_provider() {
|
||||
let mut provider = Provider::with_id(
|
||||
@@ -4564,7 +4614,8 @@ model = "gpt-5.1-codex"
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "DeepSeek V4 Flash",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1M]",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "DeepSeek V4 Pro",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-ultra [1m]"
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-ultra [1m]",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "deepseek-v4-pro[1M]"
|
||||
},
|
||||
"permissions": { "allow": ["Read"] }
|
||||
}),
|
||||
@@ -4591,7 +4642,8 @@ model = "gpt-5.1-codex"
|
||||
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721",
|
||||
"ANTHROPIC_API_KEY": PROXY_TOKEN_PLACEHOLDER,
|
||||
"ANTHROPIC_MODEL": "stale-model",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet"
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "stale-subagent"
|
||||
},
|
||||
"permissions": { "allow": ["Bash"] }
|
||||
}))
|
||||
@@ -4674,6 +4726,13 @@ model = "gpt-5.1-codex"
|
||||
Some("deepseek-v4-ultra"),
|
||||
"implicit display names should strip the local 1M marker"
|
||||
);
|
||||
assert_eq!(
|
||||
live_env
|
||||
.get("CLAUDE_CODE_SUBAGENT_MODEL")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("deepseek-v4-pro[1M]"),
|
||||
"subagent model should follow the target provider during hot switch"
|
||||
);
|
||||
|
||||
let backup = db
|
||||
.get_live_backup("claude")
|
||||
|
||||
@@ -126,6 +126,7 @@ interface ClaudeFormFieldsProps {
|
||||
defaultOpusModelName: string;
|
||||
defaultFableModel: string;
|
||||
defaultFableModelName: string;
|
||||
subagentModel: string;
|
||||
onModelChange: (field: ClaudeModelEnvField, value: string) => void;
|
||||
|
||||
// Speed Test Endpoints
|
||||
@@ -196,6 +197,7 @@ export function ClaudeFormFields({
|
||||
defaultOpusModelName,
|
||||
defaultFableModel,
|
||||
defaultFableModelName,
|
||||
subagentModel,
|
||||
onModelChange,
|
||||
speedTestEndpoints,
|
||||
apiFormat,
|
||||
@@ -221,6 +223,7 @@ export function ClaudeFormFields({
|
||||
defaultSonnetModel ||
|
||||
defaultOpusModel ||
|
||||
defaultFableModel ||
|
||||
subagentModel ||
|
||||
apiFormat !== "anthropic" ||
|
||||
apiKeyField !== "ANTHROPIC_AUTH_TOKEN" ||
|
||||
customUserAgent ||
|
||||
@@ -513,12 +516,12 @@ export function ClaudeFormFields({
|
||||
};
|
||||
|
||||
type ModelRoleRow = {
|
||||
role: "sonnet" | "opus" | "fable" | "haiku";
|
||||
role: "sonnet" | "opus" | "fable" | "haiku" | "subagent";
|
||||
label: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
displayName?: string;
|
||||
modelField: ClaudeModelEnvField;
|
||||
displayNameField: ClaudeModelEnvField;
|
||||
displayNameField?: ClaudeModelEnvField;
|
||||
inputId: string;
|
||||
supportsOneM: boolean;
|
||||
};
|
||||
@@ -564,6 +567,16 @@ export function ClaudeFormFields({
|
||||
inputId: "claudeDefaultHaikuModel",
|
||||
supportsOneM: false,
|
||||
},
|
||||
{
|
||||
role: "subagent",
|
||||
label: t("providerForm.modelRoleSubagent", {
|
||||
defaultValue: "Subagent",
|
||||
}),
|
||||
model: subagentModel,
|
||||
modelField: "CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
inputId: "claudeCodeSubagentModel",
|
||||
supportsOneM: true,
|
||||
},
|
||||
];
|
||||
|
||||
const handleRoleModelChange = (row: ModelRoleRow, value: string) => {
|
||||
@@ -572,10 +585,10 @@ export function ClaudeFormFields({
|
||||
? value
|
||||
: stripClaudeOneMMarker(value);
|
||||
const nextModelBase = stripClaudeOneMMarker(normalizedValue).trim();
|
||||
const displayName = row.displayName.trim();
|
||||
const displayName = row.displayName?.trim() ?? "";
|
||||
const shouldSyncDisplayName = !displayName || displayName === oldModelBase;
|
||||
onModelChange(row.modelField, normalizedValue);
|
||||
if (shouldSyncDisplayName) {
|
||||
if (row.displayNameField && shouldSyncDisplayName) {
|
||||
onModelChange(row.displayNameField, nextModelBase);
|
||||
}
|
||||
};
|
||||
@@ -815,17 +828,20 @@ export function ClaudeFormFields({
|
||||
defaultSonnetModel ||
|
||||
defaultOpusModel ||
|
||||
defaultFableModel ||
|
||||
defaultHaikuModel;
|
||||
defaultHaikuModel ||
|
||||
subagentModel;
|
||||
if (value) {
|
||||
for (const row of modelRoleRows) {
|
||||
const roleValue = row.supportsOneM
|
||||
? value
|
||||
: stripClaudeOneMMarker(value);
|
||||
onModelChange(row.modelField, roleValue);
|
||||
onModelChange(
|
||||
row.displayNameField,
|
||||
stripClaudeOneMMarker(roleValue),
|
||||
);
|
||||
if (row.displayNameField) {
|
||||
onModelChange(
|
||||
row.displayNameField,
|
||||
stripClaudeOneMMarker(roleValue),
|
||||
);
|
||||
}
|
||||
}
|
||||
toast.success(
|
||||
t("providerForm.quickSetSuccess", {
|
||||
@@ -839,7 +855,8 @@ export function ClaudeFormFields({
|
||||
!defaultHaikuModel &&
|
||||
!defaultSonnetModel &&
|
||||
!defaultOpusModel &&
|
||||
!defaultFableModel
|
||||
!defaultFableModel &&
|
||||
!subagentModel
|
||||
}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
@@ -907,19 +924,30 @@ export function ClaudeFormFields({
|
||||
<div className="flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm font-medium text-muted-foreground">
|
||||
{row.label}
|
||||
</div>
|
||||
<Input
|
||||
value={row.displayName}
|
||||
onChange={(event) =>
|
||||
onModelChange(row.displayNameField, event.target.value)
|
||||
}
|
||||
placeholder={
|
||||
modelBase ||
|
||||
t("providerForm.modelDisplayNamePlaceholder", {
|
||||
defaultValue: "例如 DeepSeek V4 Pro",
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{row.displayNameField ? (
|
||||
<Input
|
||||
value={row.displayName ?? ""}
|
||||
onChange={(event) =>
|
||||
onModelChange(
|
||||
row.displayNameField!,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
modelBase ||
|
||||
t("providerForm.modelDisplayNamePlaceholder", {
|
||||
defaultValue: "例如 DeepSeek V4 Pro",
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-9 items-center rounded-md border border-input bg-muted px-3 text-sm text-muted-foreground">
|
||||
{t("providerForm.modelNoDisplayName", {
|
||||
defaultValue: "不显示在 /model 菜单",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{renderModelInput(
|
||||
row.inputId,
|
||||
modelBase,
|
||||
|
||||
@@ -489,6 +489,7 @@ function ProviderFormFull({
|
||||
defaultOpusModelName,
|
||||
defaultFableModel,
|
||||
defaultFableModelName,
|
||||
subagentModel,
|
||||
handleModelChange,
|
||||
} = useModelState({
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
@@ -2099,6 +2100,7 @@ function ProviderFormFull({
|
||||
defaultOpusModelName={defaultOpusModelName}
|
||||
defaultFableModel={defaultFableModel}
|
||||
defaultFableModelName={defaultFableModelName}
|
||||
subagentModel={subagentModel}
|
||||
onModelChange={handleModelChange}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
apiFormat={localApiFormat}
|
||||
|
||||
@@ -14,7 +14,8 @@ export type ClaudeModelEnvField =
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME"
|
||||
| "ANTHROPIC_DEFAULT_FABLE_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME";
|
||||
| "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME"
|
||||
| "CLAUDE_CODE_SUBAGENT_MODEL";
|
||||
|
||||
export const CLAUDE_ONE_M_MARKER = "[1M]";
|
||||
|
||||
@@ -81,6 +82,10 @@ function parseModelsFromConfig(settingsConfig: string) {
|
||||
typeof env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME === "string"
|
||||
? env.ANTHROPIC_DEFAULT_FABLE_MODEL_NAME
|
||||
: stripClaudeOneMMarker(fable);
|
||||
const subagent =
|
||||
typeof env.CLAUDE_CODE_SUBAGENT_MODEL === "string"
|
||||
? env.CLAUDE_CODE_SUBAGENT_MODEL
|
||||
: "";
|
||||
|
||||
return {
|
||||
model,
|
||||
@@ -92,6 +97,7 @@ function parseModelsFromConfig(settingsConfig: string) {
|
||||
opusName,
|
||||
fable,
|
||||
fableName,
|
||||
subagent,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
@@ -104,6 +110,7 @@ function parseModelsFromConfig(settingsConfig: string) {
|
||||
opusName: "",
|
||||
fable: "",
|
||||
fableName: "",
|
||||
subagent: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -134,6 +141,7 @@ export function useModelState({
|
||||
const [defaultFableModelName, setDefaultFableModelName] = useState(
|
||||
initial.fableName,
|
||||
);
|
||||
const [subagentModel, setSubagentModel] = useState(initial.subagent);
|
||||
|
||||
const isUserEditingRef = useRef(false);
|
||||
const lastConfigRef = useRef(settingsConfig);
|
||||
@@ -164,6 +172,7 @@ export function useModelState({
|
||||
setDefaultOpusModelName(parsed.opusName);
|
||||
setDefaultFableModel(parsed.fable);
|
||||
setDefaultFableModelName(parsed.fableName);
|
||||
setSubagentModel(parsed.subagent);
|
||||
}, [settingsConfig]);
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
@@ -186,6 +195,7 @@ export function useModelState({
|
||||
setDefaultFableModel(value);
|
||||
if (field === "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME")
|
||||
setDefaultFableModelName(value);
|
||||
if (field === "CLAUDE_CODE_SUBAGENT_MODEL") setSubagentModel(value);
|
||||
|
||||
try {
|
||||
const currentConfig = latestConfigRef.current
|
||||
@@ -233,6 +243,8 @@ export function useModelState({
|
||||
setDefaultFableModel,
|
||||
defaultFableModelName,
|
||||
setDefaultFableModelName,
|
||||
subagentModel,
|
||||
setSubagentModel,
|
||||
handleModelChange,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1124,8 +1124,10 @@
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelRoleSubagent": "Subagent",
|
||||
"modelDisplayNameLabel": "Display name",
|
||||
"modelDisplayNamePlaceholder": "e.g. DeepSeek V4 Pro",
|
||||
"modelNoDisplayName": "Not shown in /model",
|
||||
"requestModelLabel": "Requested model",
|
||||
"modelOneMHeader": "Declare 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
|
||||
@@ -1124,8 +1124,10 @@
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelRoleSubagent": "Subagent",
|
||||
"modelDisplayNameLabel": "表示名",
|
||||
"modelDisplayNamePlaceholder": "例: DeepSeek V4 Pro",
|
||||
"modelNoDisplayName": "/model には表示されません",
|
||||
"requestModelLabel": "リクエストモデル",
|
||||
"modelOneMHeader": "1M 対応を宣言",
|
||||
"modelOneMLabel": "1M",
|
||||
|
||||
@@ -1096,8 +1096,10 @@
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelRoleSubagent": "Subagent",
|
||||
"modelDisplayNameLabel": "顯示名稱",
|
||||
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
|
||||
"modelNoDisplayName": "不顯示於 /model 選單",
|
||||
"requestModelLabel": "實際請求模型",
|
||||
"modelOneMHeader": "聲明支援 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
|
||||
@@ -1124,8 +1124,10 @@
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleFable": "Fable",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelRoleSubagent": "Subagent",
|
||||
"modelDisplayNameLabel": "显示名称",
|
||||
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
|
||||
"modelNoDisplayName": "不显示在 /model 菜单",
|
||||
"requestModelLabel": "实际请求模型",
|
||||
"modelOneMHeader": "声明支持 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
|
||||
@@ -85,6 +85,7 @@ const renderCopilotForm = (overrides: Partial<ClaudeFormFieldsProps> = {}) => {
|
||||
defaultOpusModelName: "",
|
||||
defaultFableModel: "",
|
||||
defaultFableModelName: "",
|
||||
subagentModel: "",
|
||||
onModelChange: vi.fn(),
|
||||
speedTestEndpoints: [],
|
||||
apiFormat: "anthropic",
|
||||
@@ -173,4 +174,25 @@ describe("ClaudeFormFields", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("一键设置会同时写入 Subagent 模型", () => {
|
||||
const onModelChange = vi.fn();
|
||||
renderCopilotForm({
|
||||
claudeModel: "shared-model[1M]",
|
||||
defaultSonnetModel: "",
|
||||
defaultSonnetModelName: "",
|
||||
onModelChange,
|
||||
});
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", {
|
||||
name: "一键设置",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onModelChange).toHaveBeenCalledWith(
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
"shared-model[1M]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("useModelState", () => {
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: "DeepSeek V4 Pro",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-k2",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: "Kimi K2",
|
||||
CLAUDE_CODE_SUBAGENT_MODEL: "subagent-model[1M]",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,6 +35,7 @@ describe("useModelState", () => {
|
||||
expect(result.current.defaultOpusModelName).toBe("Kimi K2");
|
||||
expect(result.current.defaultHaikuModel).toBe("legacy-small");
|
||||
expect(result.current.defaultHaikuModelName).toBe("legacy-small");
|
||||
expect(result.current.subagentModel).toBe("subagent-model[1M]");
|
||||
});
|
||||
|
||||
it("writes and clears role display-name env fields without changing model mapping", () => {
|
||||
@@ -94,6 +96,42 @@ describe("useModelState", () => {
|
||||
expect(result.current.defaultSonnetModelName).toBe("deepseek-v4-pro");
|
||||
});
|
||||
|
||||
it("writes and clears the Claude Code subagent model env field", () => {
|
||||
let latestConfig = JSON.stringify({
|
||||
env: {
|
||||
ANTHROPIC_MODEL: "fallback-model",
|
||||
},
|
||||
});
|
||||
const onConfigChange = vi.fn((config: string) => {
|
||||
latestConfig = config;
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useModelState({
|
||||
settingsConfig: latestConfig,
|
||||
onConfigChange,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.handleModelChange(
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||
"subagent-model[1M]",
|
||||
);
|
||||
});
|
||||
|
||||
let env = JSON.parse(latestConfig).env;
|
||||
expect(env.ANTHROPIC_MODEL).toBe("fallback-model");
|
||||
expect(env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("subagent-model[1M]");
|
||||
|
||||
act(() => {
|
||||
result.current.handleModelChange("CLAUDE_CODE_SUBAGENT_MODEL", "");
|
||||
});
|
||||
|
||||
env = JSON.parse(latestConfig).env;
|
||||
expect(env.CLAUDE_CODE_SUBAGENT_MODEL).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes Claude Code 1M markers for UI toggles", () => {
|
||||
expect(hasClaudeOneMMarker("deepseek-v4-pro[1m]")).toBe(true);
|
||||
expect(hasClaudeOneMMarker("deepseek-v4-pro [1M] ")).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user