diff --git a/apps/desktop/src/assets/provider-icons/deepseek.svg b/apps/desktop/src/assets/provider-icons/deepseek.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/apps/desktop/src/assets/provider-icons/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/apps/desktop/src/assets/provider-icons/huoshan.png b/apps/desktop/src/assets/provider-icons/huoshan.png new file mode 100644 index 0000000..dabe3b7 Binary files /dev/null and b/apps/desktop/src/assets/provider-icons/huoshan.png differ diff --git a/apps/desktop/src/assets/provider-icons/kimi.svg b/apps/desktop/src/assets/provider-icons/kimi.svg new file mode 100644 index 0000000..ec5db53 --- /dev/null +++ b/apps/desktop/src/assets/provider-icons/kimi.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/apps/desktop/src/assets/provider-icons/minimax.svg b/apps/desktop/src/assets/provider-icons/minimax.svg new file mode 100644 index 0000000..2a60bd4 --- /dev/null +++ b/apps/desktop/src/assets/provider-icons/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/apps/desktop/src/assets/provider-icons/zhipu.svg b/apps/desktop/src/assets/provider-icons/zhipu.svg new file mode 100644 index 0000000..0c6e61c --- /dev/null +++ b/apps/desktop/src/assets/provider-icons/zhipu.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file diff --git a/apps/desktop/src/components/cursor/CursorModelEditor.tsx b/apps/desktop/src/components/cursor/CursorModelEditor.tsx index e5e45db..b3c14c5 100644 --- a/apps/desktop/src/components/cursor/CursorModelEditor.tsx +++ b/apps/desktop/src/components/cursor/CursorModelEditor.tsx @@ -1,5 +1,6 @@ import type { ModelInput, ModelType } from "../../api"; import { defaultCustomHeadersText } from "../../utils/modelDefaults"; +import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets"; import { Button } from "../ui/Button"; import { Checkbox } from "../ui/Checkbox"; import { FormField, SecretTextInput, TextInput } from "../ui/FormControls"; @@ -7,6 +8,7 @@ import { JsonEditor } from "../ui/JsonEditor"; import { Combobox, Select } from "../ui/Select"; import { Switch } from "../ui/Switch"; import { claudeIcon, openAiIcon } from "../ui/icons"; +import { CursorPresetChips } from "./CursorPresetChips"; import styles from "./CursorSettings.module.scss"; export type CursorModelDraft = { @@ -53,13 +55,64 @@ export function CursorModelEditor({ draft, modelOptions, discovering, onChange, onDiscover: () => void; }) { const setModel = (patch: Partial) => onChange({ ...draft, model: { ...draft.model, ...patch } }); - const setType = (type: ModelType) => setModel({ - type, - openai_endpoint: type === "openai" ? draft.model.openai_endpoint || "/v1/responses" : "", - anthropic_thinking_effort: type === "anthropic" ? draft.model.anthropic_thinking_effort || "xhigh" : null, - }); + 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" @@ -71,6 +124,7 @@ export function CursorModelEditor({ draft, modelOptions, discovering, onChange, : "https://api.openai.com"; return
+