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:
秋澪Akimio
2026-07-07 10:28:36 +08:00
committed by GitHub
parent e606adfae7
commit b3e5e32c89
12 changed files with 257 additions and 33 deletions
@@ -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,
};
}
+2
View File
@@ -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",
+2
View File
@@ -1124,8 +1124,10 @@
"modelRoleOpus": "Opus",
"modelRoleFable": "Fable",
"modelRoleHaiku": "Haiku",
"modelRoleSubagent": "Subagent",
"modelDisplayNameLabel": "表示名",
"modelDisplayNamePlaceholder": "例: DeepSeek V4 Pro",
"modelNoDisplayName": "/model には表示されません",
"requestModelLabel": "リクエストモデル",
"modelOneMHeader": "1M 対応を宣言",
"modelOneMLabel": "1M",
+2
View File
@@ -1096,8 +1096,10 @@
"modelRoleOpus": "Opus",
"modelRoleFable": "Fable",
"modelRoleHaiku": "Haiku",
"modelRoleSubagent": "Subagent",
"modelDisplayNameLabel": "顯示名稱",
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
"modelNoDisplayName": "不顯示於 /model 選單",
"requestModelLabel": "實際請求模型",
"modelOneMHeader": "聲明支援 1M",
"modelOneMLabel": "1M",
+2
View File
@@ -1124,8 +1124,10 @@
"modelRoleOpus": "Opus",
"modelRoleFable": "Fable",
"modelRoleHaiku": "Haiku",
"modelRoleSubagent": "Subagent",
"modelDisplayNameLabel": "显示名称",
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
"modelNoDisplayName": "不显示在 /model 菜单",
"requestModelLabel": "实际请求模型",
"modelOneMHeader": "声明支持 1M",
"modelOneMLabel": "1M",