mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
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:
@@ -7,6 +7,7 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { toast } from "sonner";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -50,6 +51,12 @@ import type {
|
||||
ClaudeApiFormat,
|
||||
ClaudeApiKeyField,
|
||||
} from "@/types";
|
||||
import {
|
||||
hasClaudeOneMMarker,
|
||||
setClaudeOneMMarker,
|
||||
stripClaudeOneMMarker,
|
||||
type ClaudeModelEnvField,
|
||||
} from "./hooks/useModelState";
|
||||
import {
|
||||
providerPresets,
|
||||
type TemplateValueConfig,
|
||||
@@ -109,16 +116,12 @@ interface ClaudeFormFieldsProps {
|
||||
shouldShowModelSelector: boolean;
|
||||
claudeModel: string;
|
||||
defaultHaikuModel: string;
|
||||
defaultHaikuModelName: string;
|
||||
defaultSonnetModel: string;
|
||||
defaultSonnetModelName: string;
|
||||
defaultOpusModel: string;
|
||||
onModelChange: (
|
||||
field:
|
||||
| "ANTHROPIC_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_HAIKU_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_SONNET_MODEL"
|
||||
| "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
value: string,
|
||||
) => void;
|
||||
defaultOpusModelName: string;
|
||||
onModelChange: (field: ClaudeModelEnvField, value: string) => void;
|
||||
|
||||
// Speed Test Endpoints
|
||||
speedTestEndpoints: EndpointCandidate[];
|
||||
@@ -172,8 +175,11 @@ export function ClaudeFormFields({
|
||||
shouldShowModelSelector,
|
||||
claudeModel,
|
||||
defaultHaikuModel,
|
||||
defaultHaikuModelName,
|
||||
defaultSonnetModel,
|
||||
defaultSonnetModelName,
|
||||
defaultOpusModel,
|
||||
defaultOpusModelName,
|
||||
onModelChange,
|
||||
speedTestEndpoints,
|
||||
apiFormat,
|
||||
@@ -285,14 +291,13 @@ export function ClaudeFormFields({
|
||||
const renderModelInput = (
|
||||
id: string,
|
||||
value: string,
|
||||
field: ClaudeFormFieldsProps["onModelChange"] extends (
|
||||
f: infer F,
|
||||
v: string,
|
||||
) => void
|
||||
? F
|
||||
: never,
|
||||
field: ClaudeModelEnvField,
|
||||
placeholder?: string,
|
||||
onValueChange?: (value: string) => void,
|
||||
) => {
|
||||
const updateValue =
|
||||
onValueChange ?? ((next: string) => onModelChange(field, next));
|
||||
|
||||
if (isCopilotPreset && copilotModels.length > 0) {
|
||||
// 按 vendor 分组
|
||||
const grouped: Record<string, CopilotModel[]> = {};
|
||||
@@ -309,7 +314,7 @@ export function ClaudeFormFields({
|
||||
id={id}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onModelChange(field, e.target.value)}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
@@ -331,7 +336,7 @@ export function ClaudeFormFields({
|
||||
{grouped[vendor].map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={model.id}
|
||||
onSelect={() => onModelChange(field, model.id)}
|
||||
onSelect={() => updateValue(model.id)}
|
||||
>
|
||||
{model.id}
|
||||
</DropdownMenuItem>
|
||||
@@ -351,7 +356,7 @@ export function ClaudeFormFields({
|
||||
id={id}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onModelChange(field, e.target.value)}
|
||||
onChange={(e) => updateValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
className="flex-1"
|
||||
@@ -368,7 +373,7 @@ export function ClaudeFormFields({
|
||||
<ModelInputWithFetch
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(v) => onModelChange(field, v)}
|
||||
onChange={updateValue}
|
||||
placeholder={placeholder}
|
||||
fetchedModels={fetchedModels}
|
||||
isLoading={isFetchingModels}
|
||||
@@ -376,6 +381,69 @@ export function ClaudeFormFields({
|
||||
);
|
||||
};
|
||||
|
||||
type ModelRoleRow = {
|
||||
role: "sonnet" | "opus" | "haiku";
|
||||
label: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
modelField: ClaudeModelEnvField;
|
||||
displayNameField: ClaudeModelEnvField;
|
||||
inputId: string;
|
||||
supportsOneM: boolean;
|
||||
};
|
||||
|
||||
const modelRoleRows: ModelRoleRow[] = [
|
||||
{
|
||||
role: "sonnet",
|
||||
label: t("providerForm.modelRoleSonnet", { defaultValue: "Sonnet" }),
|
||||
model: defaultSonnetModel,
|
||||
displayName: defaultSonnetModelName,
|
||||
modelField: "ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
displayNameField: "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
||||
inputId: "claudeDefaultSonnetModel",
|
||||
supportsOneM: true,
|
||||
},
|
||||
{
|
||||
role: "opus",
|
||||
label: t("providerForm.modelRoleOpus", { defaultValue: "Opus" }),
|
||||
model: defaultOpusModel,
|
||||
displayName: defaultOpusModelName,
|
||||
modelField: "ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
displayNameField: "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
||||
inputId: "claudeDefaultOpusModel",
|
||||
supportsOneM: true,
|
||||
},
|
||||
{
|
||||
role: "haiku",
|
||||
label: t("providerForm.modelRoleHaiku", { defaultValue: "Haiku" }),
|
||||
model: defaultHaikuModel,
|
||||
displayName: defaultHaikuModelName,
|
||||
modelField: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
displayNameField: "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
||||
inputId: "claudeDefaultHaikuModel",
|
||||
supportsOneM: false,
|
||||
},
|
||||
];
|
||||
|
||||
const handleRoleModelChange = (row: ModelRoleRow, value: string) => {
|
||||
const oldModelBase = stripClaudeOneMMarker(row.model).trim();
|
||||
const normalizedValue = row.supportsOneM
|
||||
? value
|
||||
: stripClaudeOneMMarker(value);
|
||||
const nextModelBase = stripClaudeOneMMarker(normalizedValue).trim();
|
||||
const displayName = row.displayName.trim();
|
||||
const shouldSyncDisplayName = !displayName || displayName === oldModelBase;
|
||||
onModelChange(row.modelField, normalizedValue);
|
||||
if (shouldSyncDisplayName) {
|
||||
onModelChange(row.displayNameField, nextModelBase);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleOneMChange = (row: ModelRoleRow, enabled: boolean) => {
|
||||
if (!row.supportsOneM) return;
|
||||
handleRoleModelChange(row, setClaudeOneMMarker(row.model, enabled));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* GitHub Copilot OAuth 认证 */}
|
||||
@@ -604,17 +672,23 @@ export function ClaudeFormFields({
|
||||
onClick={() => {
|
||||
const value =
|
||||
claudeModel ||
|
||||
defaultHaikuModel ||
|
||||
defaultSonnetModel ||
|
||||
defaultOpusModel;
|
||||
defaultOpusModel ||
|
||||
defaultHaikuModel;
|
||||
if (value) {
|
||||
onModelChange("ANTHROPIC_MODEL", value);
|
||||
onModelChange("ANTHROPIC_DEFAULT_HAIKU_MODEL", value);
|
||||
onModelChange("ANTHROPIC_DEFAULT_SONNET_MODEL", value);
|
||||
onModelChange("ANTHROPIC_DEFAULT_OPUS_MODEL", value);
|
||||
for (const row of modelRoleRows) {
|
||||
const roleValue = row.supportsOneM
|
||||
? value
|
||||
: stripClaudeOneMMarker(value);
|
||||
onModelChange(row.modelField, roleValue);
|
||||
onModelChange(
|
||||
row.displayNameField,
|
||||
stripClaudeOneMMarker(roleValue),
|
||||
);
|
||||
}
|
||||
toast.success(
|
||||
t("providerForm.quickSetSuccess", {
|
||||
defaultValue: "已将模型名称应用到所有字段",
|
||||
defaultValue: "已将模型名称应用到所有角色",
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -655,66 +729,106 @@ export function ClaudeFormFields({
|
||||
{t("providerForm.modelMappingHint")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 主模型 */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="claudeModel">
|
||||
{t("providerForm.anthropicModel", {
|
||||
defaultValue: "主模型",
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="hidden grid-cols-[120px_1fr_minmax(0,1fr)_104px] gap-2 px-1 text-xs font-medium text-muted-foreground md:grid">
|
||||
<span>
|
||||
{t("providerForm.modelRoleLabel", {
|
||||
defaultValue: "模型角色",
|
||||
})}
|
||||
</FormLabel>
|
||||
{renderModelInput(
|
||||
"claudeModel",
|
||||
claudeModel,
|
||||
"ANTHROPIC_MODEL",
|
||||
t("providerForm.modelPlaceholder", { defaultValue: "" }),
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{t("providerForm.modelDisplayNameLabel", {
|
||||
defaultValue: "显示名称",
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t("providerForm.requestModelLabel", {
|
||||
defaultValue: "实际请求模型",
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t("providerForm.modelOneMHeader", {
|
||||
defaultValue: "声明支持 1M",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 默认 Haiku */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="claudeDefaultHaikuModel">
|
||||
{t("providerForm.anthropicDefaultHaikuModel", {
|
||||
defaultValue: "Haiku 默认模型",
|
||||
})}
|
||||
</FormLabel>
|
||||
{renderModelInput(
|
||||
"claudeDefaultHaikuModel",
|
||||
defaultHaikuModel,
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
t("providerForm.haikuModelPlaceholder", { defaultValue: "" }),
|
||||
)}
|
||||
</div>
|
||||
{modelRoleRows.map((row) => {
|
||||
const modelBase = stripClaudeOneMMarker(row.model);
|
||||
const usesOneM =
|
||||
row.supportsOneM && hasClaudeOneMMarker(row.model);
|
||||
|
||||
{/* 默认 Sonnet */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="claudeDefaultSonnetModel">
|
||||
{t("providerForm.anthropicDefaultSonnetModel", {
|
||||
defaultValue: "Sonnet 默认模型",
|
||||
})}
|
||||
</FormLabel>
|
||||
{renderModelInput(
|
||||
"claudeDefaultSonnetModel",
|
||||
defaultSonnetModel,
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||
t("providerForm.modelPlaceholder", { defaultValue: "" }),
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={row.role}
|
||||
className="grid grid-cols-1 gap-2 md:grid-cols-[120px_1fr_minmax(0,1fr)_104px]"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
{renderModelInput(
|
||||
row.inputId,
|
||||
modelBase,
|
||||
row.modelField,
|
||||
t("providerForm.modelPlaceholder", { defaultValue: "" }),
|
||||
(value) =>
|
||||
handleRoleModelChange(
|
||||
row,
|
||||
row.supportsOneM
|
||||
? setClaudeOneMMarker(value, usesOneM)
|
||||
: stripClaudeOneMMarker(value),
|
||||
),
|
||||
)}
|
||||
{row.supportsOneM && (
|
||||
<label className="flex h-9 items-center gap-2 text-sm text-muted-foreground">
|
||||
<Checkbox
|
||||
checked={usesOneM}
|
||||
onCheckedChange={(checked) =>
|
||||
handleRoleOneMChange(row, checked === true)
|
||||
}
|
||||
/>
|
||||
{t("providerForm.modelOneMLabel", {
|
||||
defaultValue: "1M",
|
||||
})}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 默认 Opus */}
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="claudeDefaultOpusModel">
|
||||
{t("providerForm.anthropicDefaultOpusModel", {
|
||||
defaultValue: "Opus 默认模型",
|
||||
})}
|
||||
</FormLabel>
|
||||
{renderModelInput(
|
||||
"claudeDefaultOpusModel",
|
||||
defaultOpusModel,
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||
t("providerForm.modelPlaceholder", { defaultValue: "" }),
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<FormLabel htmlFor="claudeModel">
|
||||
{t("providerForm.fallbackModelLabel", {
|
||||
defaultValue: "默认兜底模型",
|
||||
})}
|
||||
</FormLabel>
|
||||
{renderModelInput(
|
||||
"claudeModel",
|
||||
claudeModel,
|
||||
"ANTHROPIC_MODEL",
|
||||
t("providerForm.modelPlaceholder", { defaultValue: "" }),
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("providerForm.fallbackModelHint", {
|
||||
defaultValue:
|
||||
"仅在 Claude Code 请求没有明确落到 Sonnet、Opus 或 Haiku 角色时使用;通常可以留空。",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
@@ -346,8 +346,11 @@ function ProviderFormFull({
|
||||
const {
|
||||
claudeModel,
|
||||
defaultHaikuModel,
|
||||
defaultHaikuModelName,
|
||||
defaultSonnetModel,
|
||||
defaultSonnetModelName,
|
||||
defaultOpusModel,
|
||||
defaultOpusModelName,
|
||||
handleModelChange,
|
||||
} = useModelState({
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
@@ -1815,8 +1818,11 @@ function ProviderFormFull({
|
||||
shouldShowModelSelector={category !== "official"}
|
||||
claudeModel={claudeModel}
|
||||
defaultHaikuModel={defaultHaikuModel}
|
||||
defaultHaikuModelName={defaultHaikuModelName}
|
||||
defaultSonnetModel={defaultSonnetModel}
|
||||
defaultSonnetModelName={defaultSonnetModelName}
|
||||
defaultOpusModel={defaultOpusModel}
|
||||
defaultOpusModelName={defaultOpusModelName}
|
||||
onModelChange={handleModelChange}
|
||||
speedTestEndpoints={speedTestEndpoints}
|
||||
apiFormat={localApiFormat}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -908,9 +908,20 @@
|
||||
"haikuModelPlaceholder": "",
|
||||
"modelHelper": "Optional: Specify default Claude model to use, leave blank to use system default.",
|
||||
"modelMappingLabel": "Model Mapping",
|
||||
"modelMappingHint": "Usually not needed if the provider natively serves Claude models. Only configure when you need to map requests to different model names.",
|
||||
"modelMappingHint": "Display names only affect the /model menu; 1M is only a Claude Code context-capability declaration.",
|
||||
"modelRoleLabel": "Model role",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "Display name",
|
||||
"modelDisplayNamePlaceholder": "e.g. DeepSeek V4 Pro",
|
||||
"requestModelLabel": "Requested model",
|
||||
"modelOneMHeader": "Declare 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "Default fallback model",
|
||||
"fallbackModelHint": "Used only when a Claude Code request does not clearly map to Sonnet, Opus, or Haiku. Usually safe to leave blank.",
|
||||
"quickSetModels": "Quick Set",
|
||||
"quickSetSuccess": "Model name applied to all fields",
|
||||
"quickSetSuccess": "Model name applied to all roles",
|
||||
"advancedOptionsToggle": "Advanced Options",
|
||||
"advancedOptionsHint": "Includes API format, auth field, and model mapping. Defaults work for most use cases.",
|
||||
"categoryOfficial": "Official",
|
||||
|
||||
@@ -908,9 +908,20 @@
|
||||
"haikuModelPlaceholder": "",
|
||||
"modelHelper": "任意: 既定で使いたい Claude モデルを指定。空欄ならシステム既定を使用します。",
|
||||
"modelMappingLabel": "モデルマッピング",
|
||||
"modelMappingHint": "プロバイダーが Claude モデルをネイティブ提供している場合、通常は設定不要です。リクエストを別のモデル名にマッピングする場合のみ設定してください。",
|
||||
"modelMappingHint": "表示名は /model メニューだけに影響します。1M は Claude Code へのコンテキスト能力宣言です。",
|
||||
"modelRoleLabel": "モデル役割",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "表示名",
|
||||
"modelDisplayNamePlaceholder": "例: DeepSeek V4 Pro",
|
||||
"requestModelLabel": "リクエストモデル",
|
||||
"modelOneMHeader": "1M 対応を宣言",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "既定フォールバックモデル",
|
||||
"fallbackModelHint": "Claude Code のリクエストが Sonnet、Opus、Haiku のいずれにも明確に対応しない場合のみ使われます。通常は空欄で構いません。",
|
||||
"quickSetModels": "一括設定",
|
||||
"quickSetSuccess": "モデル名をすべてのフィールドに適用しました",
|
||||
"quickSetSuccess": "モデル名をすべての役割に適用しました",
|
||||
"advancedOptionsToggle": "高級オプション",
|
||||
"advancedOptionsHint": "API フォーマット、認証フィールド、モデルマッピングの設定を含みます。通常はデフォルトのままで問題ありません。",
|
||||
"categoryOfficial": "公式",
|
||||
|
||||
@@ -908,9 +908,20 @@
|
||||
"haikuModelPlaceholder": "",
|
||||
"modelHelper": "可选:指定默认使用的 Claude 模型,留空则使用系统默认。",
|
||||
"modelMappingLabel": "模型映射",
|
||||
"modelMappingHint": "如果供应商原生提供 Claude 系列模型,通常无需配置。仅在需要将请求映射到不同模型名称时填写。",
|
||||
"modelMappingHint": "显示名称只影响 /model 菜单;1M 只是给 Claude Code 的上下文能力声明。",
|
||||
"modelRoleLabel": "模型角色",
|
||||
"modelRoleSonnet": "Sonnet",
|
||||
"modelRoleOpus": "Opus",
|
||||
"modelRoleHaiku": "Haiku",
|
||||
"modelDisplayNameLabel": "显示名称",
|
||||
"modelDisplayNamePlaceholder": "例如 DeepSeek V4 Pro",
|
||||
"requestModelLabel": "实际请求模型",
|
||||
"modelOneMHeader": "声明支持 1M",
|
||||
"modelOneMLabel": "1M",
|
||||
"fallbackModelLabel": "默认兜底模型",
|
||||
"fallbackModelHint": "仅在 Claude Code 请求没有明确落到 Sonnet、Opus 或 Haiku 角色时使用;通常可以留空。",
|
||||
"quickSetModels": "一键设置",
|
||||
"quickSetSuccess": "已将模型名称应用到所有字段",
|
||||
"quickSetSuccess": "已将模型名称应用到所有角色",
|
||||
"advancedOptionsToggle": "高级选项",
|
||||
"advancedOptionsHint": "包含 API 格式、认证字段、模型映射等配置。大多数场景下保持默认即可。",
|
||||
"categoryOfficial": "官方",
|
||||
|
||||
Reference in New Issue
Block a user