import type { ModelInput, ModelType } from "../../shared/api"; import { defaultCustomHeadersText } from "../../shared/utils/modelDefaults"; import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../shared/utils/modelPresets"; import { Button } from "../../shared/ui/Button"; import { Checkbox } from "../../shared/ui/Checkbox"; import { FormField, SecretTextInput, TextInput } from "../../shared/ui/FormControls"; import { JsonEditor } from "../../shared/ui/JsonEditor"; import { Combobox, Select } from "../../shared/ui/Select"; import { Switch } from "../../shared/ui/Switch"; import { claudeIcon, openAiIcon } from "../../shared/ui/icons"; import { CursorPresetChips } from "./CursorPresetChips"; import styles from "./CursorSettings.module.scss"; export type CursorModelDraft = { model: ModelInput; openAIExtraParamsText: string; customHeadersText: string; anthropicExtraParamsText: string; }; export const emptyCursorModelDraft = (): CursorModelDraft => ({ model: { sort_order: 0, display_name: "", type: "openai", base_url: "", use_full_url: false, api_key: "", tooltip_data: "", model_id: "", reasoning_effort: null, openai_endpoint: "/v1/responses", openai_extra_params_enabled: false, openai_extra_params: {}, custom_headers_enabled: false, custom_headers: {}, anthropic_extra_params_enabled: false, anthropic_extra_params: {}, context_window_tokens: null, max_completion_tokens: null, anthropic_max_tokens: null, anthropic_thinking_effort: "xhigh", thinking_budget_tokens: null, }, openAIExtraParamsText: "{}", customHeadersText: defaultCustomHeadersText, anthropicExtraParamsText: "{}", }); export function CursorModelEditor({ draft, modelOptions, discovering, onChange, onDiscover }: { draft: CursorModelDraft; modelOptions: string[]; discovering: boolean; onChange: (draft: CursorModelDraft) => void; onDiscover: () => void; }) { const setModel = (patch: Partial) => onChange({ ...draft, model: { ...draft.model, ...patch } }); const setType = (type: ModelType) => { // 切换协议类型时,若当前地址命中某预设的另一协议端点,自动换到该预设对应协议的端点, // 避免出现「类型是 Anthropic、URL 却是 OpenAI chat/completions」的错配 const other: ModelType = type === "anthropic" ? "openai" : "anthropic"; const preset = modelPresets.find((candidate) => trimTrailingSlash(presetEndpoint(candidate, other).baseUrl) === trimTrailingSlash(draft.model.base_url.trim())); const endpoint = preset ? presetEndpoint(preset, type) : null; onChange({ ...draft, model: { ...draft.model, type, ...(endpoint ? { base_url: endpoint.baseUrl, use_full_url: endpoint.useFullUrl, custom_headers_enabled: endpoint.customHeaders !== null, custom_headers: endpoint.customHeaders ? { ...endpoint.customHeaders } : {}, } : {}), openai_endpoint: type === "openai" ? (endpoint?.openaiEndpoint || draft.model.openai_endpoint || "/v1/responses") : "", anthropic_thinking_effort: type === "anthropic" ? draft.model.anthropic_thinking_effort || "xhigh" : null, }, customHeadersText: endpoint?.customHeaders ? JSON.stringify(endpoint.customHeaders, null, 2) : draft.customHeadersText, }); }; const numberValue = (value: string) => value === "" ? null : Math.trunc(Number(value)); const canDiscover = Boolean(draft.model.base_url.trim() && draft.model.api_key.trim()); // 选中预设后,把该服务商已知的模型 id 并入下拉,方便直接选(仍可用「获取模型」发现) const presetModelOptions = modelPresets .filter((preset) => trimTrailingSlash(presetEndpoint(preset, draft.model.type).baseUrl) === trimTrailingSlash(draft.model.base_url.trim())) .flatMap((preset) => preset.models.map((item) => item.model_id)); const combinedOptions = [...new Set([...modelOptions, ...presetModelOptions])]; const applyPreset = (preset: ModelPreset) => { const endpoint = presetEndpoint(preset, draft.model.type); const first = preset.models[0]; // 切到别家服务商时清空 API Key(不同家的 Key 不能串用);同一家内切换协议则保留 const currentBase = trimTrailingSlash(draft.model.base_url.trim()); const sameProvider = [preset.endpoints.anthropic, preset.endpoints.openai] .some((candidate) => trimTrailingSlash(candidate.baseUrl) === currentBase); onChange({ ...draft, model: { ...draft.model, base_url: endpoint.baseUrl, use_full_url: endpoint.useFullUrl, openai_endpoint: draft.model.type === "openai" ? endpoint.openaiEndpoint : draft.model.openai_endpoint, custom_headers_enabled: endpoint.customHeaders !== null, custom_headers: endpoint.customHeaders ? { ...endpoint.customHeaders } : {}, api_key: sameProvider ? draft.model.api_key : "", model_id: first?.model_id ?? draft.model.model_id, display_name: first?.display_name ?? draft.model.display_name, tooltip_data: !draft.model.tooltip_data.trim() || draft.model.tooltip_data === t("备注") ? preset.name : draft.model.tooltip_data, context_window_tokens: first?.context_window_tokens ?? draft.model.context_window_tokens, ...(draft.model.type === "openai" ? { max_completion_tokens: first?.max_output_tokens ?? draft.model.max_completion_tokens } : { anthropic_max_tokens: first?.max_output_tokens ?? draft.model.anthropic_max_tokens }), }, customHeadersText: endpoint.customHeaders ? JSON.stringify(endpoint.customHeaders, null, 2) : draft.customHeadersText, }); }; const requestUrlPlaceholder = draft.model.use_full_url ? draft.model.type === "anthropic" ? "https://api.anthropic.com/v1/messages" : draft.model.openai_endpoint === "/v1/chat/completions" ? "https://api.openai.com/v1/chat/completions" : "https://api.openai.com/v1/responses" : draft.model.type === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com"; return
setModel({ openai_endpoint })} />}
setModel({ base_url: event.target.value })} /> setModel({ use_full_url })} />
setModel({ api_key: event.target.value })} /> {discovering ? t("获取中…") : t("获取模型")}} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} /> setModel({ display_name: event.target.value })} /> setModel({ tooltip_data: event.target.value })} /> setModel({ context_window_tokens: numberValue(event.target.value) })} /> {draft.model.type === "openai" ? <> setModel({ max_completion_tokens: numberValue(event.target.value) })} /> setModel({ anthropic_thinking_effort })} /> setModel({ thinking_budget_tokens: numberValue(event.target.value) })} /> } setModel({ custom_headers_enabled })} onTextChange={(customHeadersText) => onChange({ ...draft, customHeadersText })} /> {draft.model.type === "openai" ? setModel({ openai_extra_params_enabled })} onTextChange={(openAIExtraParamsText) => onChange({ ...draft, openAIExtraParamsText })} /> : setModel({ anthropic_extra_params_enabled })} onTextChange={(anthropicExtraParamsText) => onChange({ ...draft, anthropicExtraParamsText })} />}
; } function ToggleJsonField({ label, enabled, text, onEnabledChange, onTextChange }: { label: string; enabled: boolean; text: string; onEnabledChange: (enabled: boolean) => void; onTextChange: (text: string) => void; }) { return
{enabled && }
; } function effortOptions(optional: boolean) { return [ ...(optional ? [{ value: "", label: t("不设置") }] : []), { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "xhigh", label: "Extra High" }, { value: "max", label: "Max" }, ]; }