mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
feat(console): dual-protocol provider presets + fix sub-path model discovery
Console — provider presets inside the model editor: - Preset chips for five coding-plan providers (Zhipu GLM / Kimi / DeepSeek / Volcengine Ark / MiniMax) rendered at the top of the add/edit model dialog - All five providers expose both an Anthropic-protocol and an OpenAI-protocol endpoint; a click fills the endpoint matching the current model type, and switching the type automatically swaps to the same provider's other endpoint (no stale URL/protocol mismatches) - Switching to a different provider clears the API key; switching protocols within the same provider keeps it - Fills base URL (full-request-URL form for irregular paths), request protocol, claude-cli custom headers for Anthropic endpoints, token limits, remark, and the default model id + display name; known preset model ids merge into the model-name combobox Server — model discovery for sub-path endpoints: - model_discovery_url previously replaced the whole path with /v1/models, which strips the /api/anthropic, /coding, /api/paas/v4 style prefixes every coding-plan provider uses, so discovery always 404'd for them. It now appends to the existing path and normalizes endpoint/version suffixes (/chat/completions, /responses, /messages, /v1, /v3, /v4) - Verified live against Zhipu (10 models) and Kimi (4 models) coding plans; existing tests updated to the append semantics plus new URL cases Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5450fc76e2
commit
75babb7334
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M19.738 5.776c.163-.209.306-.4.457-.585.07-.087.064-.153-.004-.244-.655-.861-.717-1.817-.34-2.787.283-.73.909-1.072 1.674-1.145.477-.045.945.004 1.379.236.57.305.902.77 1.01 1.412.086.512.07 1.012-.075 1.508-.257.878-.888 1.333-1.753 1.448-.718.096-1.446.108-2.17.157-.056.004-.113 0-.178 0z" fill="#027AFF"></path><path d="M17.962 1.844h-4.326l-3.425 7.81H5.369V1.878H1.5V22h3.87v-8.477h6.824a3.025 3.025 0 002.743-1.75V22h3.87v-8.477a3.87 3.87 0 00-3.588-3.86v-.01h-2.125a3.94 3.94 0 002.323-2.12l2.545-5.689z"></path></svg>
|
||||
|
After Width: | Height: | Size: 711 B |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><defs><linearGradient id="lobe-icons-minimax-fill" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%"><stop offset="0%" stop-color="#E2167E"></stop><stop offset="100%" stop-color="#FE603C"></stop></linearGradient></defs><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" fill="url(#lobe-icons-minimax-fill)" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Zhipu</title><path d="M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z" fill="#3859FF" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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<ModelInput>) => 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 <div className={styles.editor}>
|
||||
<CursorPresetChips type={draft.model.type} baseUrl={draft.model.base_url} onPick={applyPreset} />
|
||||
<div className={styles.grid}>
|
||||
<FormField label={t("模型类型")}><Select ariaLabel={t("模型类型")} value={draft.model.type} options={[
|
||||
{ value: "openai", label: "OpenAI", icon: openAiIcon },
|
||||
@@ -87,7 +141,7 @@ export function CursorModelEditor({ draft, modelOptions, discovering, onChange,
|
||||
</div>
|
||||
<FormField label="API Key" hint={t("访问模型服务所需的密钥。")}> <SecretTextInput placeholder="sk-xxxxxx" autoComplete="off" value={draft.model.api_key} onChange={(event) => setModel({ api_key: event.target.value })} /></FormField>
|
||||
|
||||
<FormField label={t("模型名称")} hint={t("可以直接输入模型标识,也可以读取接口返回的模型列表。")}> <Combobox value={draft.model.model_id} options={modelOptions} placeholder="gpt-5" append={<Button className={styles.discoverButton} disabled={discovering || !canDiscover} onClick={onDiscover}>{discovering ? t("获取中…") : t("获取模型")}</Button>} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} /></FormField>
|
||||
<FormField label={t("模型名称")} hint={t("可以直接输入模型标识,也可以读取接口返回的模型列表。")}> <Combobox value={draft.model.model_id} options={combinedOptions} placeholder="gpt-5" append={<Button className={styles.discoverButton} disabled={discovering || !canDiscover} onClick={onDiscover}>{discovering ? t("获取中…") : t("获取模型")}</Button>} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} /></FormField>
|
||||
<FormField label={t("显示名称")} hint={t("仅用于界面展示,不会改变发送给模型服务的模型名称。")}> <TextInput value={draft.model.display_name} onChange={(event) => setModel({ display_name: event.target.value })} /></FormField>
|
||||
<FormField className={styles.fullWidth} label={t("备注")} hint={t("显示在 Cursor 模型说明中。")}> <TextInput value={draft.model.tooltip_data} onChange={(event) => setModel({ tooltip_data: event.target.value })} /></FormField>
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground, rgba(255, 255, 255, 0.7));
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border: 1px solid var(--vscode-editorWidget-border, rgba(255, 255, 255, 0.16));
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--vscode-focusBorder, #49b0ff);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--vscode-focusBorder, #49b0ff);
|
||||
color: var(--vscode-focusBorder, #49b0ff);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ModelType } from "../../api";
|
||||
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets";
|
||||
import styles from "./CursorPresetChips.module.scss";
|
||||
|
||||
/** 常用服务商预设:点击按当前协议类型自动填充对应端点与默认模型 */
|
||||
export function CursorPresetChips({ type, baseUrl, onPick }: { type: ModelType; baseUrl: string; onPick: (preset: ModelPreset) => void }) {
|
||||
return <div className={styles.wrap}>
|
||||
<span className={styles.label}>{t("常用预设")}</span>
|
||||
<div className={styles.chips}>
|
||||
{modelPresets.map((preset) => {
|
||||
const active = trimTrailingSlash(baseUrl) === trimTrailingSlash(presetEndpoint(preset, type).baseUrl);
|
||||
return <button
|
||||
type="button"
|
||||
key={preset.key}
|
||||
className={active ? `${styles.chip} ${styles.active}` : styles.chip}
|
||||
title={preset.keyHint}
|
||||
onClick={() => onPick(preset)}
|
||||
>
|
||||
<img className={styles.icon} src={preset.icon} alt="" />
|
||||
{preset.name}
|
||||
</button>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 88,
|
||||
"line": 137,
|
||||
"column": 40
|
||||
}
|
||||
]
|
||||
@@ -159,7 +159,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 118,
|
||||
"line": 167,
|
||||
"column": 16
|
||||
},
|
||||
{
|
||||
@@ -384,7 +384,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 105,
|
||||
"line": 154,
|
||||
"column": 16
|
||||
},
|
||||
{
|
||||
@@ -449,8 +449,8 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 90,
|
||||
"column": 283
|
||||
"line": 139,
|
||||
"column": 286
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -461,12 +461,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 79,
|
||||
"line": 128,
|
||||
"column": 59
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 79,
|
||||
"line": 128,
|
||||
"column": 124
|
||||
}
|
||||
]
|
||||
@@ -676,17 +676,17 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 94,
|
||||
"line": 143,
|
||||
"column": 49
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 96,
|
||||
"line": 145,
|
||||
"column": 50
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 99,
|
||||
"line": 148,
|
||||
"column": 50
|
||||
}
|
||||
]
|
||||
@@ -762,7 +762,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 90,
|
||||
"line": 139,
|
||||
"column": 42
|
||||
}
|
||||
]
|
||||
@@ -812,7 +812,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 101,
|
||||
"line": 150,
|
||||
"column": 27
|
||||
},
|
||||
{
|
||||
@@ -922,7 +922,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 91,
|
||||
"line": 140,
|
||||
"column": 42
|
||||
}
|
||||
]
|
||||
@@ -1469,7 +1469,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 94,
|
||||
"line": 143,
|
||||
"column": 25
|
||||
},
|
||||
{
|
||||
@@ -1498,7 +1498,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 85,
|
||||
"line": 134,
|
||||
"column": 145
|
||||
}
|
||||
]
|
||||
@@ -1527,7 +1527,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 143,
|
||||
"line": 192,
|
||||
"column": 41
|
||||
}
|
||||
]
|
||||
@@ -1681,12 +1681,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 96,
|
||||
"line": 145,
|
||||
"column": 27
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 99,
|
||||
"line": 148,
|
||||
"column": 27
|
||||
},
|
||||
{
|
||||
@@ -1715,7 +1715,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 92,
|
||||
"line": 141,
|
||||
"column": 69
|
||||
}
|
||||
]
|
||||
@@ -2266,7 +2266,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 112,
|
||||
"line": 161,
|
||||
"column": 16
|
||||
},
|
||||
{
|
||||
@@ -2369,8 +2369,8 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 90,
|
||||
"column": 295
|
||||
"line": 139,
|
||||
"column": 298
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2473,7 +2473,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 85,
|
||||
"line": 134,
|
||||
"column": 115
|
||||
}
|
||||
]
|
||||
@@ -2555,7 +2555,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 79,
|
||||
"line": 128,
|
||||
"column": 76
|
||||
}
|
||||
]
|
||||
@@ -2728,12 +2728,12 @@
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 100,
|
||||
"line": 149,
|
||||
"column": 27
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 100,
|
||||
"line": 149,
|
||||
"column": 58
|
||||
}
|
||||
]
|
||||
@@ -3000,7 +3000,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 86,
|
||||
"line": 135,
|
||||
"column": 61
|
||||
}
|
||||
]
|
||||
@@ -3039,7 +3039,7 @@
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 91,
|
||||
"line": 140,
|
||||
"column": 25
|
||||
}
|
||||
]
|
||||
@@ -3445,12 +3445,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 97,
|
||||
"line": 146,
|
||||
"column": 27
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 97,
|
||||
"line": 146,
|
||||
"column": 58
|
||||
}
|
||||
]
|
||||
@@ -3522,7 +3522,7 @@
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 90,
|
||||
"line": 139,
|
||||
"column": 25
|
||||
}
|
||||
]
|
||||
@@ -3592,7 +3592,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 101,
|
||||
"line": 150,
|
||||
"column": 50
|
||||
}
|
||||
]
|
||||
@@ -3633,7 +3633,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 85,
|
||||
"line": 134,
|
||||
"column": 70
|
||||
}
|
||||
]
|
||||
@@ -3645,11 +3645,23 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/ui/Modal.tsx",
|
||||
"line": 95,
|
||||
"line": 97,
|
||||
"column": 135
|
||||
}
|
||||
]
|
||||
},
|
||||
"d58c88688e1a949d": {
|
||||
"source": "常用预设",
|
||||
"kind": "text",
|
||||
"placeholders": [],
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorPresetChips.tsx",
|
||||
"line": 8,
|
||||
"column": 37
|
||||
}
|
||||
]
|
||||
},
|
||||
"d86fa42c3848c680": {
|
||||
"source": "使用系统代理",
|
||||
"kind": "text",
|
||||
@@ -3710,12 +3722,17 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 27,
|
||||
"line": 29,
|
||||
"column": 19
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 92,
|
||||
"line": 102,
|
||||
"column": 88
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 141,
|
||||
"column": 54
|
||||
}
|
||||
]
|
||||
@@ -3940,7 +3957,7 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 85,
|
||||
"line": 134,
|
||||
"column": 54
|
||||
}
|
||||
]
|
||||
@@ -4010,12 +4027,12 @@
|
||||
"refs": [
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 75,
|
||||
"line": 124,
|
||||
"column": 25
|
||||
},
|
||||
{
|
||||
"file": "components/cursor/CursorModelEditor.tsx",
|
||||
"line": 75,
|
||||
"line": 124,
|
||||
"column": 55
|
||||
}
|
||||
]
|
||||
|
||||
@@ -251,6 +251,7 @@
|
||||
"d34335433395cd3a": "Start Cursor BYOK automatically after signing in.",
|
||||
"d3716cc5a2f5a810": "Server address",
|
||||
"d3d21191f32e79a5": "Processing…",
|
||||
"d58c88688e1a949d": "Presets",
|
||||
"d86fa42c3848c680": "Use system proxy",
|
||||
"d8c47e9776cf1082": "Main menu",
|
||||
"da521d1c1cbd36af": "Authorization is required to install the certificate",
|
||||
|
||||
@@ -251,6 +251,7 @@
|
||||
"d34335433395cd3a": "登录系统后自动启动 Cursor BYOK。",
|
||||
"d3716cc5a2f5a810": "服务器地址",
|
||||
"d3d21191f32e79a5": "处理中…",
|
||||
"d58c88688e1a949d": "常用预设",
|
||||
"d86fa42c3848c680": "使用系统代理",
|
||||
"d8c47e9776cf1082": "主菜单",
|
||||
"da521d1c1cbd36af": "需要授权安装证书",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { ModelType } from "../api";
|
||||
import deepseekIcon from "../assets/provider-icons/deepseek.svg";
|
||||
import huoshanIcon from "../assets/provider-icons/huoshan.png";
|
||||
import kimiIcon from "../assets/provider-icons/kimi.svg";
|
||||
import minimaxIcon from "../assets/provider-icons/minimax.svg";
|
||||
import zhipuIcon from "../assets/provider-icons/zhipu.svg";
|
||||
import { defaultCustomHeaders } from "./modelDefaults";
|
||||
|
||||
export interface ModelPresetEntry {
|
||||
model_id: string;
|
||||
display_name: string;
|
||||
context_window_tokens: number | null;
|
||||
max_output_tokens: number | null;
|
||||
}
|
||||
|
||||
/** 一个服务商在某种协议(anthropic / openai)下的接入端点 */
|
||||
export interface ModelPresetEndpoint {
|
||||
baseUrl: string;
|
||||
/** true 时 baseUrl 即完整请求 URL;false 时由请求协议追加标准端点路径 */
|
||||
useFullUrl: boolean;
|
||||
/** openai 协议的请求端点(useFullUrl 为 true 时忽略) */
|
||||
openaiEndpoint: string;
|
||||
/** 非空时启用自定义 Headers(claude-cli 伪装头) */
|
||||
customHeaders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export interface ModelPreset {
|
||||
key: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
keyHint: string;
|
||||
/** 五家服务商均同时提供 Anthropic 与 OpenAI 兼容协议 */
|
||||
endpoints: { anthropic: ModelPresetEndpoint; openai: ModelPresetEndpoint };
|
||||
models: ModelPresetEntry[];
|
||||
}
|
||||
|
||||
const entry = (
|
||||
modelId: string,
|
||||
displayName: string,
|
||||
contextWindowTokens: number | null,
|
||||
maxOutputTokens: number | null,
|
||||
): ModelPresetEntry => ({
|
||||
model_id: modelId,
|
||||
display_name: displayName,
|
||||
context_window_tokens: contextWindowTokens,
|
||||
max_output_tokens: maxOutputTokens,
|
||||
});
|
||||
|
||||
const claudeHeaders = { ...defaultCustomHeaders };
|
||||
/** anthropic 协议:填 Base URL,自动追加 /v1/messages */
|
||||
const anthropic = (baseUrl: string): ModelPresetEndpoint => ({ baseUrl, useFullUrl: false, openaiEndpoint: "", customHeaders: claudeHeaders });
|
||||
/** openai 协议:填 Base URL,自动追加 /v1/chat/completions */
|
||||
const openaiChat = (baseUrl: string): ModelPresetEndpoint => ({ baseUrl, useFullUrl: false, openaiEndpoint: "/v1/chat/completions", customHeaders: null });
|
||||
/** openai 协议:路径不规则,直接给完整请求 URL */
|
||||
const openaiFullUrl = (url: string): ModelPresetEndpoint => ({ baseUrl: url, useFullUrl: true, openaiEndpoint: "/v1/chat/completions", customHeaders: null });
|
||||
|
||||
export const modelPresets: ModelPreset[] = [
|
||||
{
|
||||
key: "zhipu",
|
||||
name: "智谱 GLM",
|
||||
icon: zhipuIcon,
|
||||
keyHint: "bigmodel.cn → GLM Coding Plan → API Key(套餐 Key 与普通 Key 不通用)",
|
||||
endpoints: {
|
||||
anthropic: anthropic("https://open.bigmodel.cn/api/anthropic"),
|
||||
openai: openaiFullUrl("https://open.bigmodel.cn/api/coding/paas/v4/chat/completions"),
|
||||
},
|
||||
models: [
|
||||
entry("glm-5.3", "GLM 5.3", 1000000, 65536),
|
||||
entry("glm-5.2", "GLM 5.2", 200000, 32768),
|
||||
entry("glm-4.7", "GLM 4.7", 200000, 32768),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "kimi",
|
||||
name: "Kimi (Moonshot)",
|
||||
icon: kimiIcon,
|
||||
keyHint: "Kimi Code 编程套餐页获取 API Key(api.kimi.com/coding 端点)",
|
||||
endpoints: {
|
||||
anthropic: anthropic("https://api.kimi.com/coding"),
|
||||
openai: openaiChat("https://api.kimi.com/coding"),
|
||||
},
|
||||
models: [
|
||||
entry("k3", "Kimi K3", 1048576, 65536),
|
||||
entry("kimi-for-coding", "K2.7 Coding", 262144, 32768),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "deepseek",
|
||||
name: "DeepSeek",
|
||||
icon: deepseekIcon,
|
||||
keyHint: "platform.deepseek.com → API Keys",
|
||||
endpoints: {
|
||||
anthropic: anthropic("https://api.deepseek.com/anthropic"),
|
||||
openai: openaiChat("https://api.deepseek.com"),
|
||||
},
|
||||
models: [
|
||||
entry("deepseek-v4-pro", "DeepSeek V4 Pro", 1000000, 65536),
|
||||
entry("deepseek-v4-flash", "DeepSeek V4 Flash", 1000000, null),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "volcengine",
|
||||
name: "火山引擎方舟",
|
||||
icon: huoshanIcon,
|
||||
keyHint: "火山方舟 Coding Plan(ark-code-latest 路由多款代码模型)",
|
||||
endpoints: {
|
||||
anthropic: anthropic("https://ark.cn-beijing.volces.com/api/coding"),
|
||||
openai: openaiFullUrl("https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions"),
|
||||
},
|
||||
models: [entry("ark-code-latest", "Ark Code Latest", 256000, 32768)],
|
||||
},
|
||||
{
|
||||
key: "minimax",
|
||||
name: "MiniMax",
|
||||
icon: minimaxIcon,
|
||||
keyHint: "platform.minimaxi.com → 订阅 Coding Plan → API Key",
|
||||
endpoints: {
|
||||
anthropic: anthropic("https://api.minimaxi.com/anthropic"),
|
||||
openai: { baseUrl: "https://api.minimaxi.com", useFullUrl: false, openaiEndpoint: "/v1/responses", customHeaders: null },
|
||||
},
|
||||
models: [
|
||||
entry("MiniMax-M3", "MiniMax M3", 1000000, 65536),
|
||||
entry("MiniMax-M2.7", "MiniMax M2.7", 205000, 32768),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const trimTrailingSlash = (url: string) => url.replace(/\/+$/, "");
|
||||
|
||||
export const presetEndpoint = (preset: ModelPreset, type: ModelType): ModelPresetEndpoint => preset.endpoints[type];
|
||||
@@ -659,7 +659,32 @@ fn model_discovery_url(base_url: &str) -> Result<Url> {
|
||||
"model request URL must contain a host".into(),
|
||||
));
|
||||
}
|
||||
url.set_path("/v1/models");
|
||||
// 在现有路径上追加,而不是整段替换:多数编程套餐的 API 挂在子路径下
|
||||
// (/api/anthropic、/coding、/api/paas/v4 等),直接 set_path("/v1/models")
|
||||
// 会把这些前缀吃掉,发现请求必然 404
|
||||
let path = url.path().trim_end_matches('/');
|
||||
let last = path.rsplit('/').next().unwrap_or("");
|
||||
let versioned = last.len() > 1
|
||||
&& last.starts_with('v')
|
||||
&& last[1..].bytes().all(|byte| byte.is_ascii_digit());
|
||||
let new_path = if let Some(parent) = path.strip_suffix("/chat/completions") {
|
||||
// 完整请求 URL:剥掉端点段(chat/completions 是两段),换成 models
|
||||
format!("{parent}/models")
|
||||
} else if let Some(parent) = path
|
||||
.strip_suffix("/responses")
|
||||
.or_else(|| path.strip_suffix("/messages"))
|
||||
.or_else(|| path.strip_suffix("/completions"))
|
||||
{
|
||||
format!("{parent}/models")
|
||||
} else if path.is_empty() {
|
||||
"/v1/models".to_string()
|
||||
} else if versioned {
|
||||
// 已带版本段(/v1、/api/v3、/api/paas/v4):只补 models
|
||||
format!("{path}/models")
|
||||
} else {
|
||||
format!("{path}/v1/models")
|
||||
};
|
||||
url.set_path(&new_path);
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Ok(url)
|
||||
@@ -797,7 +822,25 @@ mod tests {
|
||||
store::Store,
|
||||
};
|
||||
|
||||
use super::ControlService;
|
||||
use super::{model_discovery_url, ControlService};
|
||||
|
||||
#[test]
|
||||
fn model_discovery_url_appends_to_path() {
|
||||
let cases = [
|
||||
("https://api.deepseek.com", "https://api.deepseek.com/v1/models"),
|
||||
("https://open.bigmodel.cn/api/anthropic", "https://open.bigmodel.cn/api/anthropic/v1/models"),
|
||||
("https://api.kimi.com/coding", "https://api.kimi.com/coding/v1/models"),
|
||||
("https://api.moonshot.cn/v1", "https://api.moonshot.cn/v1/models"),
|
||||
("https://ark.cn-beijing.volces.com/api/v3", "https://ark.cn-beijing.volces.com/api/v3/models"),
|
||||
(
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions",
|
||||
"https://open.bigmodel.cn/api/coding/paas/v4/models",
|
||||
),
|
||||
];
|
||||
for (base, expected) in cases {
|
||||
assert_eq!(model_discovery_url(base).unwrap().as_str(), expected, "base: {base}");
|
||||
}
|
||||
}
|
||||
|
||||
struct TestProvider {
|
||||
invocation: Arc<Mutex<Option<ModelInvocation>>>,
|
||||
@@ -899,12 +942,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_discovery_url_uses_only_the_provider_origin() {
|
||||
fn model_discovery_url_keeps_provider_path_prefix() {
|
||||
assert_eq!(
|
||||
super::model_discovery_url("https://example.com:8443/arbitrary/v1/chat/completions")
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
"https://example.com:8443/v1/models"
|
||||
"https://example.com:8443/arbitrary/v1/models"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -933,7 +976,7 @@ mod tests {
|
||||
|
||||
let (sender, mut requests) = tokio::sync::mpsc::unbounded_channel();
|
||||
let app = axum::Router::new()
|
||||
.route("/v1/models", axum::routing::get(models))
|
||||
.route("/custom/models", axum::routing::get(models))
|
||||
.with_state(sender);
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
@@ -970,7 +1013,8 @@ mod tests {
|
||||
assert_eq!(result.models, vec!["model-a"]);
|
||||
let (method, uri, headers, body) = requests.recv().await.unwrap();
|
||||
assert_eq!(method, axum::http::Method::GET);
|
||||
assert_eq!(uri.path(), "/v1/models");
|
||||
// /custom/responses 剥掉端点段后是 /custom,发现地址为 /custom/models
|
||||
assert_eq!(uri.path(), "/custom/models");
|
||||
assert!(body.is_empty());
|
||||
assert!(headers.get(axum::http::header::USER_AGENT).is_none());
|
||||
assert_eq!(headers.get("x-tenant").unwrap(), "tenant-a");
|
||||
|
||||
Reference in New Issue
Block a user