}
+ const numberValue = (value: string) => value === "" ? null : Math.trunc(Number(value));
+ const canDiscover = Boolean(draft.model.base_url.trim() && draft.model.api_key.trim());
+ 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
- {!editing && draft.providerMode === "new" && <>
-
setProvider({ base_url: event.target.value })} />
-
setProvider({ api_key: event.target.value })} />
- >}
-
- {(editing || draft.modelIds.length <= 1) &&
setModel({ display_name: event.target.value })} />}
-
{editing
- ? {discovering ? t("获取中…") : t("获取模型")}} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} />
- : {discovering ? t("获取中…") : t("获取模型")}} onChange={setModelIds} />
- }
-
- setModel({ context_window_tokens: event.target.value === "" ? null : Math.trunc(Number(event.target.value)) })} />
-
-
onChange({ ...draft, customRequestUrl, model: { ...draft.model, request_url: customRequestUrl ? draft.model.request_url : "" } })} />
- {draft.customRequestUrl &&
setModel({ request_url: event.target.value })} />}
- {!editing && draft.providerMode === "new" && <>
-
onChange({ ...draft, extraText })} />
-
onChange({ ...draft, headersText })} />
- >}
-
-
-
-
+
+ {draft.model.type === "openai" &&
}
+
+
+ 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_max_tokens: numberValue(event.target.value) })} />
+
+
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" },
+ ];
+}
diff --git a/apps/desktop/src/components/cursor/CursorModelTestResult.module.scss b/apps/desktop/src/components/cursor/CursorModelTestResult.module.scss
new file mode 100644
index 0000000..fc5dab6
--- /dev/null
+++ b/apps/desktop/src/components/cursor/CursorModelTestResult.module.scss
@@ -0,0 +1,64 @@
+@use "../../styles/typography" as type;
+
+.root {
+ min-width: 0;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 7px 9px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ font-size: type.$font-size-xs;
+}
+
+.idle {
+ color: var(--vscode-descriptionForeground);
+ background: color-mix(in srgb, var(--vscode-foreground) 6%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-foreground) 12%, transparent);
+}
+
+.testing {
+ color: var(--vscode-textLink-foreground);
+ background: color-mix(in srgb, var(--vscode-textLink-foreground) 10%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-textLink-foreground) 24%, transparent);
+}
+
+.success {
+ color: var(--vscode-testing-iconPassed, #73c991);
+ background: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 11%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-testing-iconPassed, #73c991) 28%, transparent);
+}
+
+.error {
+ color: var(--vscode-errorForeground, #f48771);
+ background: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 10%, transparent);
+ border-color: color-mix(in srgb, var(--vscode-errorForeground, #f48771) 28%, transparent);
+}
+
+.summary {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.details {
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0;
+ color: currentColor;
+ background: transparent;
+ border: 0;
+ font: inherit;
+ cursor: help;
+ white-space: nowrap;
+
+ &:focus-visible {
+ outline: 1px solid var(--vscode-focusBorder);
+ outline-offset: 2px;
+ }
+}
diff --git a/apps/desktop/src/components/cursor/CursorModelTestResult.tsx b/apps/desktop/src/components/cursor/CursorModelTestResult.tsx
index 9c979f9..9b2e0d5 100644
--- a/apps/desktop/src/components/cursor/CursorModelTestResult.tsx
+++ b/apps/desktop/src/components/cursor/CursorModelTestResult.tsx
@@ -2,13 +2,16 @@ import type { ModelConnectivityResult } from "../../api";
import { Icon } from "../ui/Icon";
import { TooltipTrigger } from "../ui/TooltipTrigger";
import { informationOutlineIcon } from "../ui/icons";
-import styles from "./CursorSettings.module.scss";
+import styles from "./CursorModelTestResult.module.scss";
export type CursorModelTestState =
| { status: "success"; result: ModelConnectivityResult }
| { status: "error"; error: string };
-export function CursorModelTestResult({ state }: { state: CursorModelTestState }) {
+export function CursorModelTestResult({ state, testing = false }: { state?: CursorModelTestState; testing?: boolean }) {
+ if (testing) return
{t("测试中…")}
;
+ if (!state) return
{t("未测试")}
;
+
const success = state.status === "success";
const summary = success
? t("速度:{speed} tokens/s", { speed: formatSpeed(state.result.tokens_per_second) })
@@ -24,9 +27,9 @@ export function CursorModelTestResult({ state }: { state: CursorModelTestState }
})
: t("测试失败:{error}", { error: state.error });
- return
-
{summary}
-
+ return
+ {summary}
+
;
}
diff --git a/apps/desktop/src/components/cursor/CursorSettings.module.scss b/apps/desktop/src/components/cursor/CursorSettings.module.scss
index d83ef8f..a90eaf1 100644
--- a/apps/desktop/src/components/cursor/CursorSettings.module.scss
+++ b/apps/desktop/src/components/cursor/CursorSettings.module.scss
@@ -5,6 +5,12 @@
gap: 16px;
}
+.gateActions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
.gate {
min-height: 250px;
display: flex;
@@ -29,103 +35,123 @@
}
}
-.groups {
- display: flex;
- flex-direction: column;
+.modelGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
}
-.providerTitle {
+.modelCard {
+ position: relative;
+ padding: 16px;
+}
+.modelCardContent {
+ height: 150px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 12px;
+}
+.modelCardTop {
min-width: 0;
display: flex;
- flex-direction: row;
- align-items: center;
- gap: 3px;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+.modelCardName {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ strong,
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-}
-.models {
- display: flex;
- flex-direction: column;
-}
-.modelRow {
- min-height: 54px;
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- align-items: center;
- gap: 10px;
- padding: 8px 14px;
- // border-top: 1px solid var(--vscode-sideBar-border);
-}
-.modelName {
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 2px;
strong {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ color: var(--vscode-foreground);
+ font-size: type.$font-size-base;
}
- small {
+ span {
color: var(--vscode-descriptionForeground);
font-size: type.$font-size-xs;
}
}
-.badge {
- padding: 2px 7px;
- color: var(--vscode-badge-foreground);
- background: var(--vscode-badge-background);
+.modelTypeBadge {
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 7px;
+ color: var(--vscode-foreground);
+ border: 1px solid var(--vscode-sideBar-border);
border-radius: 999px;
font-size: type.$font-size-2xs;
}
-.rowActions {
- display: flex;
- gap: 2px;
-}
-.testResult {
- min-width: 0;
- max-width: 220px;
+.modelCardTest {
+ min-height: 31px;
display: flex;
align-items: center;
- gap: 5px;
- font-size: type.$font-size-xs;
}
-.testSuccess {
- color: var(--vscode-testing-iconPassed, #73c991);
+.modelCardActions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 8px;
}
-.testError {
+.deleteButton:hover {
color: var(--vscode-errorForeground, #f48771);
}
-.testResultText {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.testResultHint {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- color: currentColor;
- cursor: help;
- &:focus-visible {
- outline: 1px solid var(--vscode-focusBorder);
- outline-offset: 2px;
+.sortHandle {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ z-index: 1;
+ width: 30px;
+ height: 30px;
+ display: grid;
+ place-items: center;
+ padding: 0;
+ color: transparent;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ opacity: 0;
+ cursor: grab;
+ touch-action: none;
+ transition: opacity 160ms ease, color 160ms ease, border-color 160ms ease, background-color 160ms ease;
+
+ &:focus-visible,
+ .modelCard:hover & {
+ color: var(--vscode-foreground);
+ background: var(--vscode-list-hoverBackground);
+ border-color: var(--vscode-sideBar-border);
+ opacity: 1;
}
+ &:active {
+ cursor: grabbing;
+ }
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.3;
+ }
+}
+.sortGhost {
+ opacity: 0.4;
+}
+.sortChosen {
+ border-color: var(--vscode-focusBorder);
+}
+.sortDragging {
+ cursor: grabbing;
}
.editor {
display: flex;
flex-direction: column;
gap: 14px;
}
-.editorTestResult {
- margin-top: 14px;
-}
.command {
margin: 0;
padding: 10px;
@@ -145,6 +171,17 @@
width: 100%;
grid-column: 1 / -1;
}
+.discoverButton {
+ min-height: 34px;
+ height: 34px;
+ flex: 0 0 auto;
+}
+.urlField {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
.switches {
display: flex;
flex-direction: column;
@@ -160,6 +197,21 @@
}
}
+.jsonOption {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ > label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
diff --git a/apps/desktop/src/components/cursor/TabSettingsCard.module.scss b/apps/desktop/src/components/cursor/TabSettingsCard.module.scss
deleted file mode 100644
index 39b7489..0000000
--- a/apps/desktop/src/components/cursor/TabSettingsCard.module.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@use "../../styles/typography" as type;
-
-.title {
- min-width: 0;
- display: flex;
- align-items: center;
- gap: 5px;
-}
-
-.row {
- min-height: 62px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
- padding: 12px 16px;
-
- & + & {
- border-top: 1px solid var(--vscode-sideBar-border);
- }
-}
-
-.description {
- min-width: 0;
- display: grid;
- gap: 5px;
-
- small {
- color: var(--vscode-descriptionForeground);
- font-size: type.$font-size-xs;
- }
-}
-
-.selectControl,
-.addressInput {
- width: min(300px, 44%);
- flex: 0 0 auto;
-}
-
-@media (max-width: 620px) {
- .row {
- align-items: stretch;
- flex-direction: column;
- gap: 10px;
- }
-
- .selectControl,
- .addressInput {
- width: 100%;
- }
-}
diff --git a/apps/desktop/src/components/cursor/TabSettingsCard.tsx b/apps/desktop/src/components/cursor/TabSettingsCard.tsx
deleted file mode 100644
index 69d5a45..0000000
--- a/apps/desktop/src/components/cursor/TabSettingsCard.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import cursorIconUrl from "../../assets/icons/cursor.svg";
-import type { TabMode, TabSettings } from "../../api";
-import { Button } from "../ui/Button";
-import { TextInput } from "../ui/FormControls";
-import { Icon } from "../ui/Icon";
-import { Select } from "../ui/Select";
-import { TitledCard } from "../ui/TitledCard";
-import styles from "./TabSettingsCard.module.scss";
-
-export function TabSettingsCard({ settings, saving, onChange, onSave }: {
- settings: TabSettings;
- saving: boolean;
- onChange: (settings: TabSettings) => void;
- onSave: () => void;
-}) {
- return
{t("TAB 设置")}}
- action={
}
- >
-
-
- {t("TAB 选择")}
- {t("控制 Cursor TAB 相关接口的连接方式。")}
-
-
-
-
- {settings.mode === "custom" &&
-
- {t("TAB 服务地址")}
- {t("原接口路径会追加到此服务地址。")}
-
-
onChange({ ...settings, address: event.target.value })}
- onKeyDown={(event) => { if (event.key === "Enter") onSave(); }}
- />
- }
- ;
-}
diff --git a/apps/desktop/src/components/models/LegacyModelImport.module.scss b/apps/desktop/src/components/models/LegacyModelImport.module.scss
new file mode 100644
index 0000000..c61a65b
--- /dev/null
+++ b/apps/desktop/src/components/models/LegacyModelImport.module.scss
@@ -0,0 +1,92 @@
+@use "../../styles/typography" as type;
+
+.content {
+ display: grid;
+ gap: 14px;
+}
+
+.source {
+ display: grid;
+ gap: 6px;
+
+ code {
+ overflow-wrap: anywhere;
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.counts {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+
+ > div {
+ display: grid;
+ gap: 3px;
+ padding: 10px;
+ background: var(--vscode-input-background);
+ border: 1px solid var(--vscode-input-border);
+ border-radius: 5px;
+ }
+
+ small {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.models {
+ display: grid;
+ gap: 6px;
+}
+
+.model {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ min-width: 0;
+ padding: 9px 10px;
+ background: var(--vscode-list-inactiveSelectionBackground);
+ border-radius: 5px;
+
+ > div {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+ }
+
+ small {
+ overflow: hidden;
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ > span {
+ flex: 0 0 auto;
+ font-size: type.$font-size-xs;
+ }
+}
+
+.new {
+ color: var(--vscode-testing-iconPassed, #73c991);
+}
+
+.existing,
+.hint {
+ color: var(--vscode-descriptionForeground);
+}
+
+.hint {
+ font-size: type.$font-size-xs;
+}
+
+@media (max-width: 680px) {
+ .model {
+ align-items: stretch;
+ flex-direction: column;
+ }
+}
diff --git a/apps/desktop/src/components/models/LegacyModelImport.tsx b/apps/desktop/src/components/models/LegacyModelImport.tsx
new file mode 100644
index 0000000..4e5b3ef
--- /dev/null
+++ b/apps/desktop/src/components/models/LegacyModelImport.tsx
@@ -0,0 +1,92 @@
+import { useState, type ReactNode } from "react";
+import { api, type LegacyModelImportPreview } from "../../api";
+import { appStore } from "../../store/appStore";
+import { ConfirmDialog } from "../ui/ConfirmDialog";
+import { useMessage } from "../ui/message";
+import styles from "./LegacyModelImport.module.scss";
+
+type LegacyModelImportControl = {
+ busy: boolean;
+ previewing: boolean;
+ open: () => void;
+};
+
+export function LegacyModelImport({ children }: { children: (control: LegacyModelImportControl) => ReactNode }) {
+ const message = useMessage();
+ const [preview, setPreview] = useState
(null);
+ const [previewing, setPreviewing] = useState(false);
+ const [importing, setImporting] = useState(false);
+
+ const open = async () => {
+ try {
+ setPreviewing(true);
+ setPreview(await api.previewV0049Models());
+ } catch (cause) {
+ message(errorText(cause));
+ } finally {
+ setPreviewing(false);
+ }
+ };
+
+ const confirm = async () => {
+ try {
+ setImporting(true);
+ const result = await appStore.importV0049Models();
+ if (!result) {
+ const error = appStore.getSnapshot().error;
+ if (error) message(error);
+ return;
+ }
+ setPreview(null);
+ message(result.imported > 0
+ ? t("导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型", { imported: result.imported, skipped: result.skipped })
+ : t("配置中的 {count} 个模型均已存在,无需重复导入", { count: result.skipped }));
+ } catch (cause) {
+ message(errorText(cause));
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ return <>
+ {children({ busy: previewing || importing, previewing, open: () => void open() })}
+ setPreview(null)}
+ onConfirm={() => void confirm()}
+ >
+ {preview &&
+
+ {t("配置文件")}
+ {preview.source}
+
+
+
{preview.total}{t("配置模型")}
+
{preview.new_models}{t("将新增")}
+
{preview.existing_models}{t("已存在")}
+
+
+ {preview.models.map((model) =>
+
+ {model.display_name}
+ {model.model_id} · {model.type === "openai" ? "OpenAI" : "Anthropic"}
+
+
+ {model.existing ? t("已存在,跳过") : t("新增")}
+
+
)}
+
+
{t("重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。")}
+
}
+
+ >;
+}
+
+function errorText(cause: unknown) {
+ return cause instanceof Error ? cause.message : String(cause);
+}
diff --git a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx
index 6cc03a4..919c72c 100644
--- a/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx
+++ b/apps/desktop/src/components/overview/OverviewTimeRangeFilter.tsx
@@ -11,22 +11,19 @@ import styles from "./OverviewTimeRangeFilter.module.scss";
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
-export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, providerOptions, selectedModels, selectedProviders, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onSelectedProvidersChange, onCustomApply, onRefresh }: {
+export function OverviewTimeRangeFilter({ value, customOpen, customStart, customEnd, modelOptions, selectedModels, busy, onSelect, onCustomOpenChange, onCustomStartChange, onCustomEndChange, onSelectedModelsChange, onCustomApply, onRefresh }: {
value: OverviewRangePreset;
customOpen: boolean;
customStart: string;
customEnd: string;
modelOptions: MultiSelectOption[];
- providerOptions: MultiSelectOption[];
selectedModels: string[];
- selectedProviders: string[];
busy: boolean;
onSelect: (value: Exclude) => void;
onCustomOpenChange: (open: boolean) => void;
onCustomStartChange: (value: string) => void;
onCustomEndChange: (value: string) => void;
onSelectedModelsChange: (value: string[]) => void;
- onSelectedProvidersChange: (value: string[]) => void;
onCustomApply: () => void;
onRefresh: () => void;
}) {
@@ -109,7 +106,6 @@ export function OverviewTimeRangeFilter({ value, customOpen, customStart, custom
-
diff --git a/apps/desktop/src/components/settings/TabSettingsCard.module.scss b/apps/desktop/src/components/settings/TabSettingsCard.module.scss
new file mode 100644
index 0000000..da9f8ba
--- /dev/null
+++ b/apps/desktop/src/components/settings/TabSettingsCard.module.scss
@@ -0,0 +1,78 @@
+@use "../../styles/typography" as type;
+
+.title {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.content {
+ display: grid;
+ gap: 14px;
+ padding: 16px;
+}
+
+.row {
+ min-width: 0;
+ min-height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+}
+
+.description {
+ min-width: 0;
+ display: grid;
+ gap: 5px;
+
+ small {
+ color: var(--vscode-descriptionForeground);
+ font-size: type.$font-size-xs;
+ }
+}
+
+.control {
+ width: min(420px, 62%);
+}
+
+.value {
+ min-width: 0;
+ max-width: 70%;
+ color: var(--vscode-descriptionForeground);
+ overflow-wrap: anywhere;
+ text-align: right;
+}
+
+.headerAction {
+ padding: 4px;
+ color: var(--vscode-textLink-foreground);
+ background: transparent;
+ border: 0;
+ font-size: type.$font-size-xs;
+
+ &:hover { color: var(--vscode-textLink-activeForeground); }
+ &:disabled { opacity: .5; }
+}
+
+.actionGroup {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+@media (max-width: 680px) {
+ .row {
+ align-items: stretch;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .control,
+ .value {
+ width: 100%;
+ max-width: none;
+ text-align: left;
+ }
+}
diff --git a/apps/desktop/src/components/settings/TabSettingsCard.tsx b/apps/desktop/src/components/settings/TabSettingsCard.tsx
new file mode 100644
index 0000000..f380a78
--- /dev/null
+++ b/apps/desktop/src/components/settings/TabSettingsCard.tsx
@@ -0,0 +1,90 @@
+import cursorIconUrl from "../../assets/icons/cursor.svg";
+import type { TabMode, TabSettings } from "../../api";
+import { Button } from "../ui/Button";
+import { TextInput } from "../ui/FormControls";
+import { Icon } from "../ui/Icon";
+import { Select } from "../ui/Select";
+import { TitledCard } from "../ui/TitledCard";
+import styles from "./TabSettingsCard.module.scss";
+
+export function TabSettingsCard({
+ settings,
+ draft,
+ editing,
+ saving,
+ onDraftChange,
+ onEdit,
+ onCancel,
+ onSave,
+}: {
+ settings: TabSettings | null;
+ draft: TabSettings;
+ editing: boolean;
+ saving: boolean;
+ onDraftChange: (settings: TabSettings) => void;
+ onEdit: () => void;
+ onCancel: () => void;
+ onSave: () => void;
+}) {
+ const modeLabel = (mode: TabMode) => {
+ if (mode === "public") return t("使用公益服务");
+ if (mode === "direct") return t("直连");
+ return t("自定义");
+ };
+ const action = editing ? (
+
+
+
+
+ ) : (
+
+ );
+
+ return
{t("TAB 设置")} }
+ action={action}
+ >
+
+ {editing ? <>
+
+
+ {t("TAB 选择")}
+ {t("控制 Cursor TAB 相关接口的连接方式。")}
+
+
+
+ {draft.mode === "custom" &&
+
+ {t("TAB 服务地址")}
+ {t("原接口路径会追加到此服务地址。")}
+
+
onDraftChange({ ...draft, address: event.target.value })}
+ onKeyDown={(event) => { if (event.key === "Enter") onSave(); }}
+ />
+
}
+ > : <>
+
+ {t("TAB 选择")}
+ {settings ? modeLabel(settings.mode) : t("加载中…")}
+
+ {settings?.mode === "custom" &&
+ {t("TAB 服务地址")}
+ {settings.address}
+
}
+ >}
+
+ ;
+}
diff --git a/apps/desktop/src/components/ui/Card.tsx b/apps/desktop/src/components/ui/Card.tsx
index 6d42605..d3b480e 100644
--- a/apps/desktop/src/components/ui/Card.tsx
+++ b/apps/desktop/src/components/ui/Card.tsx
@@ -1,12 +1,11 @@
-import type { ElementType, ReactNode } from "react";
+import type { ElementType, HTMLAttributes, ReactNode } from "react";
import styles from "./Card.module.scss";
-type CardProps = {
+type CardProps = HTMLAttributes & {
as?: ElementType;
- className?: string;
children: ReactNode;
};
-export function Card({ as: Component = "div", className, children }: CardProps) {
- return {children};
+export function Card({ as: Component = "div", className, children, ...props }: CardProps) {
+ return {children};
}
diff --git a/apps/desktop/src/components/ui/ConfirmDialog.tsx b/apps/desktop/src/components/ui/ConfirmDialog.tsx
new file mode 100644
index 0000000..78a5742
--- /dev/null
+++ b/apps/desktop/src/components/ui/ConfirmDialog.tsx
@@ -0,0 +1,33 @@
+import { useId, type ReactNode } from "react";
+import { Modal } from "./Modal";
+
+export function ConfirmDialog({ id, open, title, children, busy, wide, cancelLabel = t("取消"), confirmLabel = t("确认"), onCancel, onConfirm }: {
+ id?: string;
+ open: boolean;
+ title: string;
+ children: ReactNode;
+ busy?: boolean;
+ wide?: boolean;
+ cancelLabel?: string;
+ confirmLabel?: string;
+ onCancel: () => void;
+ onConfirm: () => void;
+}) {
+ const contentId = useId();
+ return
+ {children}
+ ;
+}
diff --git a/apps/desktop/src/components/ui/Modal.module.scss b/apps/desktop/src/components/ui/Modal.module.scss
index eb9137d..8f7c931 100644
--- a/apps/desktop/src/components/ui/Modal.module.scss
+++ b/apps/desktop/src/components/ui/Modal.module.scss
@@ -52,6 +52,12 @@
width: min(1080px, calc(100vw - 48px));
}
+.banner {
+ flex: 0 0 auto;
+ padding: 12px 18px;
+ border-bottom: 1px solid var(--vscode-editorWidget-border);
+}
+
.body {
flex: 0 1 auto;
min-height: 0;
diff --git a/apps/desktop/src/components/ui/Modal.tsx b/apps/desktop/src/components/ui/Modal.tsx
index 13954a9..9b9bd95 100644
--- a/apps/desktop/src/components/ui/Modal.tsx
+++ b/apps/desktop/src/components/ui/Modal.tsx
@@ -1,36 +1,98 @@
-import { useEffect, useRef, type ReactNode } from "react";
+import { useEffect, useId, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { ScrollableContent } from "../virtual/ScrollableContent";
import controls from "./Controls.module.scss";
import styles from "./Modal.module.scss";
-export function Modal({ id, open, title, children, busy, wide, onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存") }: { id?: string; open: boolean; title: string; children: ReactNode; busy?: boolean; wide?: boolean; onClose: () => void; onSubmit?: () => void; secondaryAction?: ReactNode; closeLabel?: string; submitLabel?: string }) {
+type ModalProps = {
+ id?: string;
+ open: boolean;
+ title: string;
+ children: ReactNode;
+ banner?: ReactNode;
+ busy?: boolean;
+ wide?: boolean;
+ role?: "dialog" | "alertdialog";
+ ariaDescribedBy?: string;
+ initialFocus?: "first" | "submit";
+ onClose: () => void;
+ onSubmit?: () => void;
+ secondaryAction?: ReactNode;
+ closeLabel?: string;
+ submitLabel?: string;
+};
+
+const focusableSelector = [
+ "a[href]",
+ "button:not([disabled])",
+ "input:not([disabled])",
+ "select:not([disabled])",
+ "textarea:not([disabled])",
+ "[tabindex]:not([tabindex='-1'])",
+].join(",");
+
+function focusableElements(root: HTMLElement) {
+ return [...root.querySelectorAll(focusableSelector)]
+ .filter((element) => element.getClientRects().length > 0);
+}
+
+export function Modal({ id, open, title, children, banner, busy, wide, role = "dialog", ariaDescribedBy, initialFocus = "first", onClose, onSubmit, secondaryAction, closeLabel = t("取消"), submitLabel = t("保存") }: ModalProps) {
const dialog = useRef(null);
+ const submitButton = useRef(null);
const closeRef = useRef(onClose);
const busyRef = useRef(Boolean(busy));
+ const titleId = useId();
closeRef.current = onClose;
busyRef.current = Boolean(busy);
useEffect(() => {
if (!open) return;
const previous = document.activeElement as HTMLElement | null;
- const onKey = (event: KeyboardEvent) => { if (event.key === "Escape" && !busyRef.current) closeRef.current(); };
+ const onKey = (event: KeyboardEvent) => {
+ if (event.key === "Escape" && !busyRef.current) {
+ event.preventDefault();
+ closeRef.current();
+ return;
+ }
+ if (event.key !== "Tab" || event.defaultPrevented || !dialog.current) return;
+ const focusable = focusableElements(dialog.current);
+ if (!focusable.length) {
+ event.preventDefault();
+ dialog.current.focus();
+ return;
+ }
+ const first = focusable[0];
+ const last = focusable.at(-1)!;
+ const active = document.activeElement;
+ if (event.shiftKey && (active === first || !dialog.current.contains(active))) {
+ event.preventDefault();
+ last.focus();
+ } else if (!event.shiftKey && (active === last || !dialog.current.contains(active))) {
+ event.preventDefault();
+ first.focus();
+ }
+ };
document.addEventListener("keydown", onKey);
- requestAnimationFrame(() => dialog.current?.querySelector("input,button")?.focus());
+ const focusFrame = requestAnimationFrame(() => {
+ const target = initialFocus === "submit" ? submitButton.current : focusableElements(dialog.current!)[0];
+ (target ?? dialog.current)?.focus();
+ });
return () => {
+ cancelAnimationFrame(focusFrame);
document.removeEventListener("keydown", onKey);
if (previous && document.contains(previous)) previous.focus();
};
- }, [open]);
+ }, [initialFocus, open]);
if (!open) return null;
return createPortal(
-
-
+
+
+ {banner &&
{banner}
}
{children}
, document.body);
diff --git a/apps/desktop/src/components/ui/Tooltip.tsx b/apps/desktop/src/components/ui/Tooltip.tsx
index f3ed639..eb0b088 100644
--- a/apps/desktop/src/components/ui/Tooltip.tsx
+++ b/apps/desktop/src/components/ui/Tooltip.tsx
@@ -5,7 +5,7 @@ import styles from "./Tooltip.module.scss";
export type TooltipAnchor = VirtualElement;
-export function Tooltip({ anchor, children }: { anchor: TooltipAnchor | null; children: ReactNode }) {
+export function Tooltip({ id, anchor, children }: { id?: string; anchor: TooltipAnchor | null; children: ReactNode }) {
const tooltipRef = useRef
(null);
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
@@ -32,6 +32,7 @@ export function Tooltip({ anchor, children }: { anchor: TooltipAnchor | null; ch
return createPortal(
;
- onMouseLeave?: MouseEventHandler
;
+ "aria-describedby"?: string;
+ onPointerMove?: PointerEventHandler;
+ onPointerLeave?: PointerEventHandler;
+ onPointerDown?: PointerEventHandler;
onFocus?: FocusEventHandler;
onBlur?: FocusEventHandler;
};
export function TooltipTrigger({ label, children }: { label: string; children: ReactElement }) {
const [anchor, setAnchor] = useState(null);
+ const tooltipId = useId();
+ useEffect(() => {
+ if (!anchor) return;
+ const close = (event: KeyboardEvent) => { if (event.key === "Escape") setAnchor(null); };
+ document.addEventListener("keydown", close);
+ return () => document.removeEventListener("keydown", close);
+ }, [anchor]);
const trigger = cloneElement(children, {
- onMouseEnter: (event) => {
- children.props.onMouseEnter?.(event);
- setAnchor(anchorFor(event.currentTarget));
+ "aria-describedby": [children.props["aria-describedby"], anchor ? tooltipId : null].filter(Boolean).join(" ") || undefined,
+ onPointerMove: (event) => {
+ children.props.onPointerMove?.(event);
+ if (event.pointerType !== "touch" && event.buttons === 0) {
+ const element = event.currentTarget;
+ setAnchor((current) => current ?? anchorFor(element));
+ }
},
- onMouseLeave: (event) => {
- children.props.onMouseLeave?.(event);
+ onPointerLeave: (event) => {
+ children.props.onPointerLeave?.(event);
+ setAnchor(null);
+ },
+ onPointerDown: (event) => {
+ children.props.onPointerDown?.(event);
setAnchor(null);
},
onFocus: (event) => {
children.props.onFocus?.(event);
- setAnchor(anchorFor(event.currentTarget));
+ setAnchor(event.currentTarget.matches(":focus-visible") ? anchorFor(event.currentTarget) : null);
},
onBlur: (event) => {
children.props.onBlur?.(event);
@@ -35,6 +52,6 @@ export function TooltipTrigger({ label, children }: { label: string; children: R
return <>
{trigger}
- {label}
+ {label}
>;
}
diff --git a/apps/desktop/src/components/ui/icons.ts b/apps/desktop/src/components/ui/icons.ts
index 4329579..f42a6b1 100644
--- a/apps/desktop/src/components/ui/icons.ts
+++ b/apps/desktop/src/components/ui/icons.ts
@@ -32,6 +32,7 @@ export const informationOutlineIcon = icon('', 512, 512); // cil:badge
export const refreshIcon = icon(''); // mdi:refresh
export const trashIcon = icon(''); // mdi:delete-outline
+export const dragIcon = icon(''); // mdi:drag-vertical
export const checkIcon = icon('');
export const chevronDownIcon = icon('');
export const chevronLeftIcon = icon('');
diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json
index 3125c20..8e3c0f0 100644
--- a/apps/desktop/src/i18n/generated/catalog.json
+++ b/apps/desktop/src/i18n/generated/catalog.json
@@ -1,5 +1,17 @@
{
"entries": {
+ "0006d696d8e1ec28": {
+ "source": "新增",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 80,
+ "column": 47
+ }
+ ]
+ },
"00929f23850e4ff0": {
"source": "成功调用:{count}",
"kind": "template",
@@ -21,25 +33,8 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 75,
- "column": 42
- },
- {
- "file": "components/ProviderEditor.tsx",
- "line": 26,
- "column": 102
- }
- ]
- },
- "025b70bde54ed1ef": {
- "source": "模型是否允许被 Cursor 选择使用。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 95,
- "column": 39
+ "line": 88,
+ "column": 40
}
]
},
@@ -80,14 +75,24 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 106,
+ "column": 85
+ },
{
"file": "components/settings/ProxySettingsCard.tsx",
"line": 37,
"column": 8
},
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 40,
+ "column": 98
+ },
{
"file": "pages/SettingsPage.tsx",
- "line": 148,
+ "line": 180,
"column": 82
}
]
@@ -102,7 +107,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 99,
+ "line": 101,
"column": 27
}
]
@@ -119,18 +124,6 @@
}
]
},
- "05912a17829faacc": {
- "source": "所有调用记录和详细追踪数据都会被删除。供应商、模型、CA 和应用设置不会受到影响,此操作无法撤销。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/SettingsPage.tsx",
- "line": 254,
- "column": 14
- }
- ]
- },
"076832c1b2de22c3": {
"source": "缓存写入:{tokens}",
"kind": "template",
@@ -159,15 +152,20 @@
}
]
},
- "07ec86e0f1d44f91": {
- "source": "创建时间",
+ "07c657ed4747126e": {
+ "source": "Anthropic 额外参数",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/ProviderTable.tsx",
- "line": 23,
- "column": 31
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 118,
+ "column": 16
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 230,
+ "column": 73
}
]
},
@@ -178,11 +176,23 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 245,
+ "line": 289,
"column": 16
}
]
},
+ "092b520558eff5f2": {
+ "source": "未测试",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelTestResult.tsx",
+ "line": 13,
+ "column": 105
+ }
+ ]
+ },
"099008ea7a42ebd1": {
"source": "自定义 Headers 的值必须都是字符串",
"kind": "text",
@@ -190,7 +200,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 257,
+ "line": 241,
"column": 89
}
]
@@ -207,18 +217,6 @@
}
]
},
- "0a5f9a892960a00a": {
- "source": "更新时间",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderTable.tsx",
- "line": 24,
- "column": 31
- }
- ]
- },
"0b96da34f6fbdd3b": {
"source": "缓存读取:{tokens} × ${price}/1M = {cost}",
"kind": "template",
@@ -235,30 +233,6 @@
}
]
},
- "0bd20614caf56647": {
- "source": "支持选择或输入多个模型;批量添加时显示名称默认使用对应模型名称。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 81,
- "column": 90
- }
- ]
- },
- "0c2fee621d406ea7": {
- "source": "选择已有上游,或创建一个新的上游。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 67,
- "column": 51
- }
- ]
- },
"0c70665b6eb65f1a": {
"source": "否",
"kind": "text",
@@ -327,11 +301,26 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 228,
+ "line": 272,
"column": 26
}
]
},
+ "0e67021ebf0a3580": {
+ "source": "导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型",
+ "kind": "template",
+ "placeholders": [
+ "imported",
+ "skipped"
+ ],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 42,
+ "column": 11
+ }
+ ]
+ },
"0ec1e85b0c3cfa65": {
"source": "调用详情",
"kind": "text",
@@ -355,19 +344,24 @@
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 159,
- "column": 242
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 105,
+ "column": 95
+ },
+ {
+ "file": "components/cursor/CursorModelTestResult.tsx",
+ "line": 12,
+ "column": 109
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 199,
- "column": 182
+ "line": 200,
+ "column": 231
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 205,
- "column": 349
+ "line": 203,
+ "column": 480
}
]
},
@@ -389,13 +383,13 @@
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 256,
- "column": 36
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 105,
+ "column": 16
},
{
- "file": "pages/ProvidersPage.tsx",
- "line": 91,
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 240,
"column": 36
}
]
@@ -419,8 +413,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 199,
- "column": 194
+ "line": 200,
+ "column": 243
}
]
},
@@ -455,25 +449,25 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 82,
- "column": 234
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 83,
- "column": 233
+ "line": 90,
+ "column": 283
}
]
},
- "15d76f1c211cc140": {
- "source": "留空表示保留当前 API Key。",
+ "15730c19fd7eef51": {
+ "source": "请求协议",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/ProviderEditor.tsx",
- "line": 26,
- "column": 77
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 79,
+ "column": 59
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 79,
+ "column": 124
}
]
},
@@ -484,53 +478,26 @@
"refs": [
{
"file": "components/cursor/CursorGates.tsx",
- "line": 37,
- "column": 59
+ "line": 38,
+ "column": 77
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 202,
- "column": 40
+ "line": 201,
+ "column": 51
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 202,
- "column": 119
+ "line": 201,
+ "column": 130
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 205,
+ "line": 203,
"column": 63
}
]
},
- "16aa4ae1427fccce": {
- "source": "自定义 Headers JSON",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
- "column": 56
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
- "column": 141
- },
- {
- "file": "components/ProviderEditor.tsx",
- "line": 27,
- "column": 52
- },
- {
- "file": "components/ProviderEditor.tsx",
- "line": 27,
- "column": 148
- }
- ]
- },
"16d0d7e2b332af72": {
"source": "总调用:{count}",
"kind": "template",
@@ -557,18 +524,6 @@
}
]
},
- "18d25065b3f7db39": {
- "source": "请求完整地址不能为空",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 237,
- "column": 82
- }
- ]
- },
"19658d9fa9aa8de4": {
"source": "安装中…",
"kind": "text",
@@ -622,18 +577,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 161,
- "column": 32
- },
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 161,
- "column": 119
- },
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 218,
- "column": 44
+ "line": 211,
+ "column": 52
}
]
},
@@ -644,7 +589,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 232,
+ "line": 276,
"column": 60
}
]
@@ -670,17 +615,17 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 55,
+ "line": 63,
"column": 42
},
{
"file": "pages/SettingsPage.tsx",
- "line": 153,
+ "line": 185,
"column": 22
},
{
"file": "pages/SettingsPage.tsx",
- "line": 180,
+ "line": 212,
"column": 58
}
]
@@ -697,15 +642,15 @@
}
]
},
- "1cef73a53d7ad0f2": {
- "source": "例如:OpenAI",
+ "1d27f02ed278ebc9": {
+ "source": "配置文件",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/ProviderEditor.tsx",
- "line": 19,
- "column": 85
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 65,
+ "column": 20
}
]
},
@@ -724,35 +669,39 @@
}
]
},
- "21296ab18ad9af25": {
- "source": "额外参数 JSON",
+ "202064bb84804852": {
+ "source": "留空时使用默认值。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 91,
- "column": 56
+ "line": 94,
+ "column": 49
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 91,
- "column": 123
+ "line": 96,
+ "column": 50
},
{
- "file": "components/ProviderEditor.tsx",
- "line": 28,
- "column": 52
- },
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 99,
+ "column": 50
+ }
+ ]
+ },
+ "20e14248fd4fb981": {
+ "source": "{label} 必须是大于 0 的整数",
+ "kind": "template",
+ "placeholders": [
+ "label"
+ ],
+ "refs": [
{
- "file": "components/ProviderEditor.tsx",
- "line": 28,
- "column": 119
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 22,
- "column": 29
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 234,
+ "column": 89
}
]
},
@@ -763,7 +712,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 206,
+ "line": 250,
"column": 43
}
]
@@ -775,7 +724,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 110,
+ "line": 118,
"column": 15
}
]
@@ -794,6 +743,30 @@
}
]
},
+ "24a0a24864454575": {
+ "source": "已存在,跳过",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 80,
+ "column": 33
+ }
+ ]
+ },
+ "2555d6c7fbb7e070": {
+ "source": "可以直接输入模型标识,也可以读取接口返回的模型列表。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 90,
+ "column": 42
+ }
+ ]
+ },
"29585d7193539200": {
"source": "当前版本 {version}",
"kind": "template",
@@ -815,7 +788,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 224,
+ "line": 238,
"column": 14
}
]
@@ -832,14 +805,36 @@
}
]
},
+ "2caeaec539e78898": {
+ "source": "思考预算 Token",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 101,
+ "column": 27
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 233,
+ "column": 186
+ }
+ ]
+ },
"2cd0f3be8738a86c": {
"source": "取消",
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 58,
+ "column": 20
+ },
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 114,
+ "line": 110,
"column": 105
},
{
@@ -847,30 +842,45 @@
"line": 32,
"column": 66
},
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 36,
+ "column": 66
+ },
+ {
+ "file": "components/ui/ConfirmDialog.tsx",
+ "line": 4,
+ "column": 86
+ },
{
"file": "components/ui/Modal.tsx",
- "line": 7,
- "column": 113
+ "line": 39,
+ "column": 179
},
{
"file": "layouts/AppLayout.tsx",
- "line": 225,
- "column": 19
+ "line": 228,
+ "column": 20
+ },
+ {
+ "file": "layouts/AppLayout.tsx",
+ "line": 239,
+ "column": 20
},
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 218,
- "column": 67
+ "line": 211,
+ "column": 76
},
{
"file": "pages/SettingsPage.tsx",
- "line": 144,
+ "line": 176,
"column": 81
},
{
"file": "pages/SettingsPage.tsx",
- "line": 247,
- "column": 21
+ "line": 291,
+ "column": 22
}
]
},
@@ -905,15 +915,15 @@
}
]
},
- "2f1b67cdaa23351a": {
- "source": "自定义请求完整地址",
+ "2f280a8120c0db12": {
+ "source": "仅用于界面展示,不会改变发送给模型服务的模型名称。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 88,
- "column": 58
+ "line": 91,
+ "column": 42
}
]
},
@@ -926,12 +936,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 226,
- "column": 70
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 98,
+ "line": 247,
"column": 70
}
]
@@ -942,8 +947,8 @@
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 183,
+ "file": "pages/SettingsPage.tsx",
+ "line": 142,
"column": 15
}
]
@@ -972,6 +977,18 @@
}
]
},
+ "2f7ba5fd1d12f7f9": {
+ "source": "打开使用教程?",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "layouts/AppLayout.tsx",
+ "line": 227,
+ "column": 14
+ }
+ ]
+ },
"2f7dec3be28d7597": {
"source": "已选择 {count} 个",
"kind": "template",
@@ -991,10 +1008,32 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 108,
+ "column": 119
+ },
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 218,
- "column": 89
+ "line": 211,
+ "column": 99
+ }
+ ]
+ },
+ "346ff60e6c7c5181": {
+ "source": "读取中…",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorGates.tsx",
+ "line": 39,
+ "column": 101
+ },
+ {
+ "file": "pages/SettingsPage.tsx",
+ "line": 235,
+ "column": 27
}
]
},
@@ -1003,10 +1042,27 @@
"kind": "text",
"placeholders": [],
"refs": [
+ {
+ "file": "components/ui/ConfirmDialog.tsx",
+ "line": 4,
+ "column": 110
+ },
{
"file": "layouts/AppLayout.tsx",
- "line": 226,
- "column": 20
+ "line": 240,
+ "column": 21
+ }
+ ]
+ },
+ "37125ef2e1d707cb": {
+ "source": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 232,
+ "column": 123
}
]
},
@@ -1114,7 +1170,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 239,
+ "line": 283,
"column": 14
}
]
@@ -1131,15 +1187,15 @@
}
]
},
- "3bd5b3f538e3270a": {
- "source": "新建上游",
+ "3a8c76b2ce785f96": {
+ "source": "查看并导入",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 68,
- "column": 30
+ "file": "pages/SettingsPage.tsx",
+ "line": 235,
+ "column": 39
}
]
},
@@ -1176,12 +1232,12 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 198,
+ "line": 242,
"column": 22
},
{
"file": "pages/SettingsPage.tsx",
- "line": 204,
+ "line": 248,
"column": 26
}
]
@@ -1204,9 +1260,9 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 41,
- "column": 17
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 68,
+ "column": 21
}
]
},
@@ -1217,8 +1273,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 211,
- "column": 71
+ "line": 208,
+ "column": 80
}
]
},
@@ -1229,7 +1285,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 223,
+ "line": 267,
"column": 52
}
]
@@ -1301,7 +1357,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 104,
+ "line": 106,
"column": 27
}
]
@@ -1381,18 +1437,11 @@
"file": "components/settings/ProxySettingsCard.tsx",
"line": 69,
"column": 135
- }
- ]
- },
- "493e097fac6bd217": {
- "source": "上游配置",
- "kind": "text",
- "placeholders": [],
- "refs": [
+ },
{
- "file": "layouts/AppLayout.tsx",
- "line": 74,
- "column": 48
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 81,
+ "column": 81
}
]
},
@@ -1413,6 +1462,47 @@
}
]
},
+ "499c729eb09aa2a6": {
+ "source": "上下文窗口 Token",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 94,
+ "column": 25
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 233,
+ "column": 34
+ }
+ ]
+ },
+ "4a8d6841b4023edf": {
+ "source": "确认导入",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 59,
+ "column": 21
+ }
+ ]
+ },
+ "4b458e6e147221d7": {
+ "source": "系统会根据请求协议自动追加标准端点路径。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 85,
+ "column": 145
+ }
+ ]
+ },
"4d0680f9efaef147": {
"source": "未读",
"kind": "text",
@@ -1425,24 +1515,31 @@
},
{
"file": "layouts/AppLayout.tsx",
- "line": 197,
+ "line": 200,
"column": 67
}
]
},
+ "4e30d7c9ed2b0eee": {
+ "source": "不设置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 143,
+ "column": 41
+ }
+ ]
+ },
"4eafa9e925b30bcd": {
"source": "自定义",
"kind": "text",
"placeholders": [],
"refs": [
- {
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 32,
- "column": 39
- },
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 89,
+ "line": 86,
"column": 9
},
{
@@ -1454,6 +1551,16 @@
"file": "components/settings/ProxySettingsCard.tsx",
"line": 46,
"column": 169
+ },
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 32,
+ "column": 12
+ },
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 60,
+ "column": 41
}
]
},
@@ -1464,7 +1571,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 35,
+ "line": 32,
"column": 30
}
]
@@ -1478,7 +1585,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 147,
+ "line": 138,
"column": 11
}
]
@@ -1490,7 +1597,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 110,
+ "line": 107,
"column": 70
}
]
@@ -1501,8 +1608,8 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 17,
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 44,
"column": 90
}
]
@@ -1514,7 +1621,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 142,
+ "line": 174,
"column": 26
}
]
@@ -1531,6 +1638,18 @@
}
]
},
+ "54e6745ff43c9c74": {
+ "source": "排序失败",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 163,
+ "column": 47
+ }
+ ]
+ },
"550eddc3c7fefa99": {
"source": "推广",
"kind": "text",
@@ -1543,23 +1662,6 @@
}
]
},
- "555737734a6371e6": {
- "source": "删除上游",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderTable.tsx",
- "line": 27,
- "column": 30
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 27,
- "column": 117
- }
- ]
- },
"56432ba297009bdc": {
"source": "请先初始化 CA",
"kind": "text",
@@ -1567,8 +1669,54 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 202,
- "column": 52
+ "line": 201,
+ "column": 63
+ }
+ ]
+ },
+ "56627c94a9decee6": {
+ "source": "最大输出 Token",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 96,
+ "column": 27
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 99,
+ "column": 27
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 233,
+ "column": 83
+ }
+ ]
+ },
+ "576d81bb0631b165": {
+ "source": "导入",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/SettingsPage.tsx",
+ "line": 228,
+ "column": 78
+ }
+ ]
+ },
+ "5886afc1c71df1fe": {
+ "source": "显示在 Cursor 模型说明中。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 92,
+ "column": 69
}
]
},
@@ -1578,13 +1726,30 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 40,
- "column": 18
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 67,
+ "column": 22
},
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 47,
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 73,
+ "column": 25
+ },
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 84,
+ "column": 20
+ }
+ ]
+ },
+ "5a284a1a2be8da0e": {
+ "source": "从本机旧版配置读取模型;确认前会显示新增和已存在的模型。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/SettingsPage.tsx",
+ "line": 232,
"column": 21
}
]
@@ -1595,26 +1760,14 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
+ "file": "components/settings/TabSettingsCard.tsx",
"line": 30,
- "column": 39
- }
- ]
- },
- "5ae715656ffbc35d": {
- "source": "合并到该上游所有模型的请求体。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 91,
- "column": 78
+ "column": 35
},
{
- "file": "components/ProviderEditor.tsx",
- "line": 28,
- "column": 74
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 58,
+ "column": 41
}
]
},
@@ -1627,7 +1780,7 @@
"refs": [
{
"file": "components/cursor/CursorModelTestResult.tsx",
- "line": 15,
+ "line": 18,
"column": 7
}
]
@@ -1644,15 +1797,15 @@
}
]
},
- "5cca0b7972a11f3a": {
- "source": "自定义上下文必须是大于 0 的整数",
+ "5cae248525cb9140": {
+ "source": "所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 238,
- "column": 171
+ "file": "pages/SettingsPage.tsx",
+ "line": 298,
+ "column": 14
}
]
},
@@ -1689,7 +1842,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 230,
+ "line": 244,
"column": 11
}
]
@@ -1789,11 +1942,23 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 115,
+ "line": 111,
"column": 109
}
]
},
+ "63d90d977348ab1f": {
+ "source": "复制",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 107,
+ "column": 90
+ }
+ ]
+ },
"6478a5f1218c484e": {
"source": "请在桌面应用中复制到系统剪贴板",
"kind": "text",
@@ -1801,7 +1966,7 @@
"refs": [
{
"file": "api.ts",
- "line": 294,
+ "line": 313,
"column": 43
}
]
@@ -1832,7 +1997,7 @@
"refs": [
{
"file": "components/cursor/CursorModelTestResult.tsx",
- "line": 17,
+ "line": 20,
"column": 7
}
]
@@ -1892,15 +2057,15 @@
}
]
},
- "68152165b3348852": {
- "source": "选择上游服务使用的请求协议。",
+ "68ad603fafe4e0d6": {
+ "source": "导入旧版配置",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/ProviderEditor.tsx",
- "line": 20,
- "column": 38
+ "file": "components/cursor/CursorGates.tsx",
+ "line": 39,
+ "column": 113
}
]
},
@@ -1923,7 +2088,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 109,
+ "line": 106,
"column": 21
}
]
@@ -1934,9 +2099,9 @@
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 159,
- "column": 254
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 105,
+ "column": 107
}
]
},
@@ -2005,25 +2170,20 @@
}
]
},
- "71eb2c6fce991a29": {
- "source": "添加上游",
+ "736c9dc2a04c65fd": {
+ "source": "模型配置已发生变化,请刷新后重试",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "pages/ProvidersPage.tsx",
- "line": 82,
- "column": 41
+ "file": "store/appStore.ts",
+ "line": 153,
+ "column": 23
},
{
- "file": "pages/ProvidersPage.tsx",
- "line": 82,
- "column": 104
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 84,
- "column": 63
+ "file": "store/appStore.ts",
+ "line": 160,
+ "column": 25
}
]
},
@@ -2034,23 +2194,11 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 132,
+ "line": 164,
"column": 16
}
]
},
- "75844712d53a24de": {
- "source": "值必须是字符串;编辑时 null 表示保留对应敏感 Header 的原值。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderEditor.tsx",
- "line": 27,
- "column": 81
- }
- ]
- },
"788db1cfec2a3db5": {
"source": "主题",
"kind": "text",
@@ -2058,7 +2206,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 215,
+ "line": 259,
"column": 26
}
]
@@ -2082,7 +2230,7 @@
"refs": [
{
"file": "api.ts",
- "line": 289,
+ "line": 308,
"column": 43
}
]
@@ -2106,8 +2254,25 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 248,
- "column": 22
+ "line": 292,
+ "column": 23
+ }
+ ]
+ },
+ "7cea2f3c46565d29": {
+ "source": "OpenAI 额外参数",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 112,
+ "column": 16
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 228,
+ "column": 67
}
]
},
@@ -2120,7 +2285,7 @@
"refs": [
{
"file": "App.tsx",
- "line": 52,
+ "line": 50,
"column": 19
}
]
@@ -2131,29 +2296,12 @@
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 180,
+ "file": "pages/SettingsPage.tsx",
+ "line": 136,
"column": 83
}
]
},
- "7e1845870b528392": {
- "source": "启用模型",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 95,
- "column": 73
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 95,
- "column": 122
- }
- ]
- },
"7e1f06318e80c3af": {
"source": "统计数据已清理",
"kind": "text",
@@ -2161,20 +2309,20 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 85,
+ "line": 93,
"column": 15
}
]
},
- "7e9ab9ada2cbf2cb": {
- "source": "名称和 Base URL 不能为空",
+ "7e7df68f2a82e09e": {
+ "source": "重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "pages/ProvidersPage.tsx",
- "line": 57,
- "column": 46
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 84,
+ "column": 41
}
]
},
@@ -2202,18 +2350,6 @@
}
]
},
- "80a57e03f0717f91": {
- "source": "未配置",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderTable.tsx",
- "line": 20,
- "column": 126
- }
- ]
- },
"811a3b22a5a7f2d5": {
"source": "无法连接本地管理服务",
"kind": "text",
@@ -2221,7 +2357,7 @@
"refs": [
{
"file": "api.ts",
- "line": 235,
+ "line": 258,
"column": 21
}
]
@@ -2233,13 +2369,8 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 82,
- "column": 246
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 83,
- "column": 245
+ "line": 90,
+ "column": 295
}
]
},
@@ -2260,15 +2391,15 @@
}
]
},
- "84924374710e03bd": {
- "source": "Base URL 必须是有效地址",
+ "843ac7e15a5047a7": {
+ "source": "确认导入旧版模型配置",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "pages/CursorSettingsPage.tsx",
- "line": 251,
- "column": 21
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 55,
+ "column": 14
}
]
},
@@ -2335,15 +2466,15 @@
}
]
},
- "883cc47637fe70f3": {
- "source": "附加到该上游所有请求的自定义请求头,值必须是字符串。",
+ "89a101b809be7cfc": {
+ "source": "系统会原样使用此地址,不追加或修改请求路径。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
- "column": 85
+ "line": 85,
+ "column": 115
}
]
},
@@ -2376,6 +2507,18 @@
}
]
},
+ "8ccaf87ddb9ca3f4": {
+ "source": "旧版配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/SettingsPage.tsx",
+ "line": 231,
+ "column": 22
+ }
+ ]
+ },
"8d0c47eb9eac2d34": {
"source": "调用类型",
"kind": "text",
@@ -2400,20 +2543,49 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 232,
+ "line": 246,
"column": 16
}
]
},
+ "8e2d04638a11a7cb": {
+ "source": "只决定请求与响应的格式,不会改变请求地址。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 79,
+ "column": 76
+ }
+ ]
+ },
+ "8ea973394446abba": {
+ "source": "Cursor 配置",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "layouts/AppLayout.tsx",
+ "line": 76,
+ "column": 53
+ },
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 202,
+ "column": 25
+ }
+ ]
+ },
"8f9b0d6cc477d334": {
"source": "控制 Cursor TAB 相关接口的连接方式。",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 23,
- "column": 17
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 52,
+ "column": 21
}
]
},
@@ -2426,12 +2598,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 227,
- "column": 87
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 99,
+ "line": 248,
"column": 87
}
]
@@ -2455,23 +2622,11 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 72,
+ "line": 73,
"column": 39
}
]
},
- "91ee087c3d4f3220": {
- "source": "支持完整 HTTP(S) 地址或以 / 开头、与上游地址组合的相对路径。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 89,
- "column": 100
- }
- ]
- },
"940a168911ade998": {
"source": "每页条数",
"kind": "text",
@@ -2491,32 +2646,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 219,
- "column": 11
- }
- ]
- },
- "94803f35c825e47a": {
- "source": "请求完整地址",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 89,
- "column": 81
- }
- ]
- },
- "94df1e7f04815daf": {
- "source": "仅用于界面展示,不会改变发送给上游的模型名称。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 80,
- "column": 86
+ "line": 211,
+ "column": 250
}
]
},
@@ -2526,14 +2657,19 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 22,
- "column": 18
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 51,
+ "column": 22
},
{
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 28,
- "column": 22
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 56,
+ "column": 24
+ },
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 80,
+ "column": 20
}
]
},
@@ -2563,27 +2699,15 @@
}
]
},
- "9a026819dd1af5c5": {
- "source": "可以直接输入模型标识,也可以从当前上游返回的模型列表中选择。",
+ "9b1b7ed518ee401d": {
+ "source": "将在系统浏览器中打开使用教程,是否继续?",
"kind": "text",
"placeholders": [],
"refs": [
{
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 81,
- "column": 52
- }
- ]
- },
- "9ac0ac940982895d": {
- "source": "选择上游",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 67,
- "column": 94
+ "file": "layouts/AppLayout.tsx",
+ "line": 233,
+ "column": 11
}
]
},
@@ -2601,18 +2725,16 @@
"file": "components/CallTable.tsx",
"line": 47,
"column": 15
- }
- ]
- },
- "9d8f2ef4e85ea665": {
- "source": "自定义上下文 tokens",
- "kind": "text",
- "placeholders": [],
- "refs": [
+ },
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 86,
- "column": 63
+ "line": 100,
+ "column": 27
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 100,
+ "column": 58
}
]
},
@@ -2635,7 +2757,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 109,
+ "line": 106,
"column": 70
}
]
@@ -2647,7 +2769,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 195,
+ "line": 239,
"column": 26
}
]
@@ -2659,7 +2781,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 37,
+ "line": 34,
"column": 29
}
]
@@ -2683,11 +2805,25 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 48,
+ "line": 56,
"column": 34
}
]
},
+ "a0c42c24e74f8380": {
+ "source": "{name} 副本",
+ "kind": "template",
+ "placeholders": [
+ "name"
+ ],
+ "refs": [
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 147,
+ "column": 22
+ }
+ ]
+ },
"a1a42cd9b16e2162": {
"source": "应用设置",
"kind": "text",
@@ -2717,45 +2853,47 @@
}
]
},
- "a2901cd2743c1921": {
- "source": "模型服务的 API 根地址;修改后会同步更新该上游模型的路由身份。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderEditor.tsx",
- "line": 25,
- "column": 39
- }
- ]
- },
"a3030bf8f16dc63c": {
"source": "保存",
"kind": "text",
"placeholders": [],
"refs": [
- {
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 18,
- "column": 109
- },
{
"file": "components/settings/ProxySettingsCard.tsx",
"line": 33,
"column": 103
},
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 37,
+ "column": 103
+ },
{
"file": "components/ui/Modal.tsx",
- "line": 7,
- "column": 136
+ "line": 39,
+ "column": 202
},
{
"file": "pages/SettingsPage.tsx",
- "line": 145,
+ "line": 177,
"column": 133
}
]
},
+ "a340bdf12a15fd80": {
+ "source": "配置中的 {count} 个模型均已存在,无需重复导入",
+ "kind": "template",
+ "placeholders": [
+ "count"
+ ],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 43,
+ "column": 11
+ }
+ ]
+ },
"a363743025795ec7": {
"source": "我已初始化,刷新",
"kind": "text",
@@ -2792,6 +2930,18 @@
}
]
},
+ "a5fb6189a8ad011d": {
+ "source": "打开教程",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "layouts/AppLayout.tsx",
+ "line": 229,
+ "column": 21
+ }
+ ]
+ },
"a621ab606db2a11f": {
"source": "密码",
"kind": "text",
@@ -2816,8 +2966,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 205,
- "column": 361
+ "line": 203,
+ "column": 492
}
]
},
@@ -2835,6 +2985,40 @@
"file": "components/CallTable.tsx",
"line": 184,
"column": 25
+ },
+ {
+ "file": "components/cursor/CursorModelTestResult.tsx",
+ "line": 32,
+ "column": 86
+ }
+ ]
+ },
+ "a7617f42f898b2bf": {
+ "source": "使用完整请求地址",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 86,
+ "column": 61
+ }
+ ]
+ },
+ "a8036485f9227f2c": {
+ "source": "拖动排序",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 87,
+ "column": 93
+ },
+ {
+ "file": "components/cursor/CursorModelCards.tsx",
+ "line": 87,
+ "column": 111
}
]
},
@@ -2855,64 +3039,8 @@
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 80,
- "column": 69
- }
- ]
- },
- "a9ab292ea9feecdc": {
- "source": "上游",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 67,
- "column": 36
- },
- {
- "file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 112,
- "column": 61
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 83,
- "column": 31
- }
- ]
- },
- "aa678e13bda90e3c": {
- "source": "自定义 Headers 的值必须是字符串或 null",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 92,
- "column": 107
- }
- ]
- },
- "ab2f31f30acf7fda": {
- "source": "协议",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderEditor.tsx",
- "line": 20,
- "column": 23
- },
- {
- "file": "components/ProviderEditor.tsx",
- "line": 20,
- "column": 78
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 18,
- "column": 28
+ "line": 91,
+ "column": 25
}
]
},
@@ -2987,22 +3115,22 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 91,
+ "line": 88,
"column": 28
},
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 91,
+ "line": 88,
"column": 89
},
{
"file": "layouts/AppLayout.tsx",
- "line": 246,
+ "line": 260,
"column": 64
},
{
"file": "layouts/AppLayout.tsx",
- "line": 246,
+ "line": 260,
"column": 125
}
]
@@ -3013,9 +3141,26 @@
"placeholders": [],
"refs": [
{
- "file": "components/cursor/TabSettingsCard.tsx",
+ "file": "components/settings/TabSettingsCard.tsx",
"line": 31,
- "column": 39
+ "column": 35
+ },
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 59,
+ "column": 41
+ }
+ ]
+ },
+ "b16c3b2ecedd6fe1": {
+ "source": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorGates.tsx",
+ "line": 36,
+ "column": 12
}
]
},
@@ -3072,18 +3217,6 @@
}
]
},
- "b710ec36ad312918": {
- "source": "模型服务的 API 根地址,例如 https://api.openai.com/v1。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 74,
- "column": 43
- }
- ]
- },
"b75a46aad3e7c132": {
"source": "非缓存输入:{tokens}",
"kind": "template",
@@ -3105,7 +3238,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 231,
+ "line": 275,
"column": 22
}
]
@@ -3117,7 +3250,7 @@
"refs": [
{
"file": "components/cursor/CursorModelTestResult.tsx",
- "line": 22,
+ "line": 25,
"column": 50
}
]
@@ -3129,8 +3262,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 211,
- "column": 93
+ "line": 208,
+ "column": 103
}
]
},
@@ -3163,23 +3296,6 @@
}
]
},
- "b97ad406809572e1": {
- "source": "启用推理",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 96,
- "column": 66
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 96,
- "column": 115
- }
- ]
- },
"b9af2de88d903be7": {
"source": "代理地址",
"kind": "text",
@@ -3207,7 +3323,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 148,
+ "line": 139,
"column": 11
}
]
@@ -3236,7 +3352,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 223,
+ "line": 267,
"column": 40
}
]
@@ -3248,7 +3364,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 237,
+ "line": 251,
"column": 24
}
]
@@ -3260,29 +3376,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 73,
- "column": 51
- }
- ]
- },
- "bf84379b75735037": {
- "source": "编辑上游",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderTable.tsx",
- "line": 26,
- "column": 30
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 26,
- "column": 93
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 84,
+ "line": 70,
"column": 51
}
]
@@ -3332,35 +3426,6 @@
}
]
},
- "c62a58459251b02c": {
- "source": "图片生成",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 97,
- "column": 66
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 97,
- "column": 115
- }
- ]
- },
- "c6cc835023617457": {
- "source": "自定义上下文",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 85,
- "column": 25
- }
- ]
- },
"c7ea2c9bc43134bd": {
"source": "编辑模型",
"kind": "text",
@@ -3368,21 +3433,28 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 160,
- "column": 32
- },
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 160,
- "column": 95
- },
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 205,
+ "line": 203,
"column": 51
}
]
},
+ "c8c14507b2d37395": {
+ "source": "推理强度",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 97,
+ "column": 27
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 97,
+ "column": 58
+ }
+ ]
+ },
"c8df3c14a003bfcd": {
"source": "无法加载调用详情",
"kind": "text",
@@ -3402,7 +3474,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 111,
+ "line": 108,
"column": 61
}
]
@@ -3450,7 +3522,7 @@
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 81,
+ "line": 90,
"column": 25
}
]
@@ -3479,12 +3551,12 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 130,
+ "line": 162,
"column": 22
},
{
"file": "pages/SettingsPage.tsx",
- "line": 136,
+ "line": 168,
"column": 20
}
]
@@ -3496,23 +3568,11 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 167,
+ "line": 199,
"column": 21
}
]
},
- "cfe999e50be8ef54": {
- "source": "是否声明模型支持图片生成。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 97,
- "column": 39
- }
- ]
- },
"d0bfccc77315d887": {
"source": "近一个月",
"kind": "text",
@@ -3520,37 +3580,20 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 38,
+ "line": 35,
"column": 30
}
]
},
- "d15a909c3490a7e0": {
- "source": "端点类型",
+ "d1251cd752d4ec25": {
+ "source": "留空时使用 adaptive thinking。",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 77,
- "column": 25
- },
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 77,
- "column": 86
- }
- ]
- },
- "d27db596b73a0a66": {
- "source": "例如:272000",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 86,
- "column": 96
+ "line": 101,
+ "column": 50
}
]
},
@@ -3571,18 +3614,6 @@
}
]
},
- "d30d35c5ec4a888d": {
- "source": "请至少选择或输入一个模型",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 235,
- "column": 41
- }
- ]
- },
"d34335433395cd3a": {
"source": "登录系统后自动启动 Cursor BYOK。",
"kind": "text",
@@ -3595,6 +3626,18 @@
}
]
},
+ "d3716cc5a2f5a810": {
+ "source": "服务器地址",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 85,
+ "column": 70
+ }
+ ]
+ },
"d3d21191f32e79a5": {
"source": "处理中…",
"kind": "text",
@@ -3602,37 +3645,8 @@
"refs": [
{
"file": "components/ui/Modal.tsx",
- "line": 33,
- "column": 116
- }
- ]
- },
- "d44e9b3d3b31d37b": {
- "source": "名称",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderEditor.tsx",
- "line": 19,
- "column": 52
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 17,
- "column": 28
- }
- ]
- },
- "d667c1616e31c4c4": {
- "source": "留空以保留当前密钥",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderEditor.tsx",
- "line": 26,
- "column": 180
+ "line": 95,
+ "column": 135
}
]
},
@@ -3660,7 +3674,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 183,
+ "line": 186,
"column": 54
}
]
@@ -3672,8 +3686,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 213,
- "column": 18
+ "line": 209,
+ "column": 47
}
]
},
@@ -3684,20 +3698,37 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 34,
+ "line": 31,
"column": 29
}
]
},
- "db0a1393f05aa4bd": {
- "source": "默认继承上游,可为当前模型单独修改。",
+ "daede9881787abe7": {
+ "source": "备注",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 77,
- "column": 42
+ "line": 27,
+ "column": 19
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 92,
+ "column": 54
+ }
+ ]
+ },
+ "dbd3596e4a86f3c2": {
+ "source": "配置模型",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 69,
+ "column": 56
}
]
},
@@ -3713,18 +3744,6 @@
}
]
},
- "de8184da1ef88d03": {
- "source": "已配置",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/ProviderTable.tsx",
- "line": 20,
- "column": 115
- }
- ]
- },
"dea7749c4cd77e6d": {
"source": "总请求 Token 包含提示词和模型输出。",
"kind": "text",
@@ -3737,6 +3756,18 @@
}
]
},
+ "df1baa9f706d970b": {
+ "source": "将新增",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 70,
+ "column": 61
+ }
+ ]
+ },
"df3d58c7d84b85f2": {
"source": "设置",
"kind": "text",
@@ -3744,7 +3775,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 262,
+ "line": 306,
"column": 30
}
]
@@ -3799,7 +3830,7 @@
"refs": [
{
"file": "components/cursor/CursorModelTestResult.tsx",
- "line": 14,
+ "line": 17,
"column": 7
}
]
@@ -3828,18 +3859,6 @@
}
]
},
- "e14a5eee9b0b0f9f": {
- "source": "是否声明模型支持推理能力。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 96,
- "column": 39
- }
- ]
- },
"e14f20d572c02611": {
"source": "上游调用序号",
"kind": "text",
@@ -3852,6 +3871,18 @@
}
]
},
+ "e17a5b9c90cda6ab": {
+ "source": "模型已复制",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 159,
+ "column": 26
+ }
+ ]
+ },
"e18516550b9a5105": {
"source": "无用量",
"kind": "text",
@@ -3873,7 +3904,7 @@
"refs": [
{
"file": "components/cursor/CursorModelTestResult.tsx",
- "line": 25,
+ "line": 28,
"column": 7
}
]
@@ -3885,7 +3916,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 73,
+ "line": 74,
"column": 44
}
]
@@ -3902,6 +3933,18 @@
}
]
},
+ "e4760c6a1df24f17": {
+ "source": "完整请求 URL",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 85,
+ "column": 54
+ }
+ ]
+ },
"e5043c7a2b408271": {
"source": "近10分钟",
"kind": "text",
@@ -3909,7 +3952,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 36,
+ "line": 33,
"column": 36
}
]
@@ -3938,35 +3981,11 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 61,
+ "line": 69,
"column": 17
}
]
},
- "e671f8c7598139ef": {
- "source": "输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorModelEditor.tsx",
- "line": 85,
- "column": 44
- }
- ]
- },
- "e6ca887f22288cde": {
- "source": "Model ID 和显示名称不能为空",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 236,
- "column": 68
- }
- ]
- },
"e77e3d58b0dcffaa": {
"source": "耗时",
"kind": "text",
@@ -3984,6 +4003,23 @@
}
]
},
+ "e825a2a42c22380e": {
+ "source": "模型类型",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 75,
+ "column": 25
+ },
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 75,
+ "column": 55
+ }
+ ]
+ },
"e828bd3a0151edc2": {
"source": "需要在系统中信任本地 CA",
"kind": "text",
@@ -3996,6 +4032,18 @@
}
]
},
+ "e8b1268c1e3610f2": {
+ "source": "已存在",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/models/LegacyModelImport.tsx",
+ "line": 71,
+ "column": 66
+ }
+ ]
+ },
"ea26b760e930a7ca": {
"source": "调用观测",
"kind": "text",
@@ -4003,7 +4051,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 127,
+ "line": 159,
"column": 26
}
]
@@ -4039,8 +4087,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 214,
- "column": 16
+ "line": 209,
+ "column": 77
}
]
},
@@ -4053,11 +4101,6 @@
"file": "components/CallTable.tsx",
"line": 178,
"column": 15
- },
- {
- "file": "components/ProviderTable.tsx",
- "line": 25,
- "column": 31
}
]
},
@@ -4068,25 +4111,8 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 211,
- "column": 45
- }
- ]
- },
- "ede6d08f397df283": {
- "source": "Cursor 设置",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "layouts/AppLayout.tsx",
- "line": 78,
+ "line": 208,
"column": 53
- },
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 204,
- "column": 25
}
]
},
@@ -4109,28 +4135,11 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 186,
+ "line": 218,
"column": 16
}
]
},
- "f04c91a6bc3a6926": {
- "source": "额外参数",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "pages/CursorSettingsPage.tsx",
- "line": 67,
- "column": 187
- },
- {
- "file": "pages/ProvidersPage.tsx",
- "line": 64,
- "column": 46
- }
- ]
- },
"f2bdc88464c51c2e": {
"source": "显示 API Key",
"kind": "text",
@@ -4143,18 +4152,6 @@
}
]
},
- "f396118b8afd2a21": {
- "source": "Cursor 接管已生效;添加上游及其模型配置后即可使用 BYOK 模型。",
- "kind": "text",
- "placeholders": [],
- "refs": [
- {
- "file": "components/cursor/CursorGates.tsx",
- "line": 36,
- "column": 12
- }
- ]
- },
"f3a76d896853c1df": {
"source": "未命中",
"kind": "text",
@@ -4213,7 +4210,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 110,
+ "line": 107,
"column": 21
}
]
@@ -4239,7 +4236,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 199,
+ "line": 243,
"column": 21
}
]
@@ -4251,7 +4248,7 @@
"refs": [
{
"file": "components/overview/OverviewTimeRangeFilter.tsx",
- "line": 99,
+ "line": 96,
"column": 19
}
]
@@ -4313,7 +4310,7 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 154,
+ "line": 186,
"column": 21
}
]
@@ -4405,7 +4402,7 @@
"refs": [
{
"file": "pages/HomePage.tsx",
- "line": 160,
+ "line": 148,
"column": 25
}
]
@@ -4417,7 +4414,7 @@
"refs": [
{
"file": "layouts/AppLayout.tsx",
- "line": 75,
+ "line": 78,
"column": 48
}
]
@@ -4427,19 +4424,19 @@
"kind": "text",
"placeholders": [],
"refs": [
- {
- "file": "components/cursor/TabSettingsCard.tsx",
- "line": 18,
- "column": 97
- },
{
"file": "components/settings/ProxySettingsCard.tsx",
"line": 33,
"column": 91
},
+ {
+ "file": "components/settings/TabSettingsCard.tsx",
+ "line": 37,
+ "column": 91
+ },
{
"file": "pages/SettingsPage.tsx",
- "line": 145,
+ "line": 177,
"column": 121
}
]
@@ -4463,17 +4460,17 @@
"refs": [
{
"file": "pages/SettingsPage.tsx",
- "line": 56,
+ "line": 64,
"column": 46
},
{
"file": "pages/SettingsPage.tsx",
- "line": 166,
+ "line": 198,
"column": 22
},
{
"file": "pages/SettingsPage.tsx",
- "line": 181,
+ "line": 213,
"column": 58
}
]
diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json
index c8d105d..2e4ed62 100644
--- a/apps/desktop/src/i18n/locales/en-US.json
+++ b/apps/desktop/src/i18n/locales/en-US.json
@@ -1,27 +1,25 @@
{
+ "0006d696d8e1ec28": "New",
"00929f23850e4ff0": "Successful calls: {count}",
"01f3e69a5a9b2c9b": "The key required to access the model service.",
- "025b70bde54ed1ef": "Whether Cursor can select and use this model.",
"028a4de61bff743d": "Regular input: {tokens} × ${price}/1M = {cost}",
"03ff62ab4b818492": "Cache write: {tokens} × ${price}/1M = {cost}",
"051836569928a9f9": "Edit",
"05468af47054d488": "Connectivity test for {model} succeeded ({duration} ms)",
"0580e0a99a6f1afc": "Artifacts",
- "05912a17829faacc": "All call records and detailed traces will be deleted. Providers, models, CA, and application settings are unaffected. This action cannot be undone.",
"076832c1b2de22c3": "Cache write: {tokens}",
"07879e064ae16542": "Estimated output: {tokens}",
- "07ec86e0f1d44f91": "Created",
+ "07c657ed4747126e": "Anthropic extra parameters",
"08d742566ffc9fa3": "Clear all statistics?",
+ "092b520558eff5f2": "Not tested",
"099008ea7a42ebd1": "All custom Header values must be strings",
"09ebc2643631ba25": "Estimated value",
- "0a5f9a892960a00a": "Updated",
"0b96da34f6fbdd3b": "Cache read: {tokens} × ${price}/1M = {cost}",
- "0bd20614caf56647": "Select or enter multiple models. When adding in bulk, each display name defaults to its model name.",
- "0c2fee621d406ea7": "Select an existing provider or create a new one.",
"0c70665b6eb65f1a": "No",
"0c72229b7db0e1a9": "Model output",
"0d5e2bdb15579fc4": "Messages",
"0e41f8e3d59ec47b": "Storage management",
+ "0e67021ebf0a3580": "Import complete: added {imported} models and skipped {skipped} existing models",
"0ec1e85b0c3cfa65": "Call details",
"105a9082c346f958": "Testing…",
"124be3f86f197802": "Token usage",
@@ -31,12 +29,10 @@
"13a9ac7a68c5fd96": "The CA is stored only on this device and is used to securely inspect Cursor HTTPS requests.",
"13b61c5f697b6700": "Cache hit rate",
"146da2e2a991493e": "Fetching…",
- "15d76f1c211cc140": "Leave blank to keep the current API Key.",
+ "15730c19fd7eef51": "Request protocol",
"168e845a86bc3703": "Add model",
- "16aa4ae1427fccce": "Custom Headers JSON",
"16d0d7e2b332af72": "Total calls: {count}",
"1813d362a82fd437": "Maximize window",
- "18d25065b3f7db39": "The full request URL is required",
"19658d9fa9aa8de4": "Installing…",
"1a3f0617d6de8e52": "Username",
"1ae6b0a0f8266382": "Close window",
@@ -45,26 +41,33 @@
"1c6926877b1bbfb2": "Total requests: {tokens}",
"1cb7e646ac883fb2": "Proxy port",
"1cdde778cfe6a130": "Formula: cache read / (cache read + non-cached input)",
- "1cef73a53d7ad0f2": "For example: OpenAI",
+ "1d27f02ed278ebc9": "Configuration file",
"1f9b46e61efcccb5": "Filter: {label}, {summary}",
- "21296ab18ad9af25": "Extra parameters JSON",
+ "202064bb84804852": "Leave blank to use the default.",
+ "20e14248fd4fb981": "{label} must be an integer greater than 0",
"217cfe7db1e3d10a": "Use system language",
"22c6b4eb4caee6ae": "Proxy settings saved",
"23e49479e15e6770": "Version {version} is available",
+ "24a0a24864454575": "Existing, skipped",
+ "2555d6c7fbb7e070": "Enter a model ID directly or load models returned by the API.",
"29585d7193539200": "Current version {version}",
"29fbbef32a6eb58b": "Do not show this ad again",
"2a2773134a829016": "Aggregated from historical LLM calls; in-progress calls are excluded.",
+ "2caeaec539e78898": "Thinking budget tokens",
"2cd0f3be8738a86c": "Cancel",
"2d30c2a98ebb5278": "Current: {rate}",
"2eb2bf7c6597ab9a": "Detailed records",
- "2f1b67cdaa23351a": "Custom full request URL",
+ "2f280a8120c0db12": "Used only for display and does not change the model name sent to the model service.",
"2f4a361f878176d1": "{label} must be valid JSON",
"2f4a9609285d8f49": "TAB settings saved",
"2f5f1d6fbfb061ed": "Not set",
"2f6416a2c424856b": "Final request URL",
+ "2f7ba5fd1d12f7f9": "Open the tutorial?",
"2f7dec3be28d7597": "{count} selected",
"2f9daa828907b93f": "Delete",
+ "346ff60e6c7c5181": "Reading…",
"36f33adaf0942634": "Confirm",
+ "37125ef2e1d707cb": "Server address or complete request URL, API Key, model name, display name, and note are required",
"378bb0eec39fa8a2": "Last page",
"37cb98ff4d5dcfcc": "Successful {successful} / failed {failed}",
"393df9bb13ea4900": "Hit",
@@ -74,7 +77,7 @@
"3a0fb74abe2460b6": "Chunks",
"3a3f595df70ec8ff": "Clear storage",
"3a5040b68abf75f9": "Select all",
- "3bd5b3f538e3270a": "New provider",
+ "3a8c76b2ce785f96": "Review and import",
"3c94b4c75940c178": "Input Tokens",
"3cfae5728b92b334": "Token usage: {tokens}",
"3d13868593ae4eeb": "Display language",
@@ -93,9 +96,12 @@
"48d8db17bae06246": "{count} total",
"492042ed1fdc29ed": "Version {version} is ready to install",
"4927a53bcc886afb": "Loading…",
- "493e097fac6bd217": "Providers",
"497c85690c4cc0fc": "No data",
+ "499c729eb09aa2a6": "Context window tokens",
+ "4a8d6841b4023edf": "Confirm import",
+ "4b458e6e147221d7": "The standard endpoint path is appended automatically for the selected protocol.",
"4d0680f9efaef147": "Unread",
+ "4e30d7c9ed2b0eee": "Not set",
"4eafa9e925b30bcd": "Custom",
"51d04bc3d286f018": "Last calendar day",
"51de3bcec137ab1b": "Connectivity tests succeeded for all {count} models",
@@ -103,15 +109,18 @@
"5401344227e49e2f": "TAB settings",
"54644705e9c61009": "Port settings",
"54c53e5fe791d1f3": "Initialize CA",
+ "54e6745ff43c9c74": "Unable to save model order",
"550eddc3c7fefa99": "Sponsored",
- "555737734a6371e6": "Delete provider",
"56432ba297009bdc": "Initialize the CA first",
+ "56627c94a9decee6": "Maximum output tokens",
+ "576d81bb0631b165": "Import",
+ "5886afc1c71df1fe": "Shown in the Cursor model description.",
"59346e82b3dd2998": "TAB service address",
+ "5a284a1a2be8da0e": "Read models from the local legacy configuration. New and existing models are shown before confirmation.",
"5a3bd99fa69a40c1": "Use public service",
- "5ae715656ffbc35d": "Merge into the request body for every model from this provider.",
"5b17f59d33bde39e": "Error: {error}",
"5c55a67935af8f45": "All",
- "5cca0b7972a11f3a": "Custom context must be an integer greater than 0",
+ "5cae248525cb9140": "All call records and detailed traces will be deleted. Model configuration, CA, and application settings are unaffected. This action cannot be undone.",
"5d59857bf039cac9": "Cursor Assistant v{version}",
"5f8d556a9c47da3c": "Launch at login disabled",
"5f9acfb945229062": "Are you sure you no longer want to see this ad?",
@@ -122,6 +131,7 @@
"621f63a5f08384ac": "Cache read: {tokens}",
"6320b4a8722a851f": "Status",
"63c73c4730f4473e": "Apply",
+ "63d90d977348ab1f": "Duplicate",
"6478a5f1218c484e": "Use the desktop app to copy to the system clipboard",
"651f274470153a05": "Software updates",
"652ec5d40c29fd6a": "Speed {speed} tokens/s · first token {firstText} ms · total {duration} ms · output {tokens} tokens{estimated} · response: {output}",
@@ -129,7 +139,7 @@
"656ab25e264cc4e4": "No models are available to Cursor yet",
"65a6318e07ec1e07": "Tools",
"65cb9a7b4f620b6b": "Prompt {tokens}",
- "68152165b3348852": "Select the request protocol used by this provider.",
+ "68ad603fafe4e0d6": "Import legacy configuration",
"68ea5dd4d7af20e6": "System settings",
"6a9906c79f26c0ba": "Start time",
"6aa8f49cc992dfd7": "Test",
@@ -137,71 +147,68 @@
"6d1876364ac6457d": "Proxy mode",
"6e86570183c3cdd0": "You're up to date",
"7005693f4f050bce": "Cache I/O {cost}",
- "71eb2c6fce991a29": "Add provider",
+ "736c9dc2a04c65fd": "The model configuration changed. Refresh and try again.",
"7392e20d61abaa07": "Also store complete requests and streamed responses; by default only timing, status, and usage are stored.",
- "75844712d53a24de": "Values must be strings; when editing, null keeps the original value of the corresponding sensitive Header.",
"788db1cfec2a3db5": "Theme",
"7995087e5a3dfe66": "Restore window",
"7a2229f6a6d330a5": "Open a terminal from the desktop app to install the CA",
"7a3cec4ca715de80": "Call statistics",
"7ba2d6728fe2531b": "Confirm clear",
+ "7cea2f3c46565d29": "OpenAI extra parameters",
"7d9f043f8f7ab45c": "Version {version} is available in Settings",
"7e0891860c9e6374": "TAB service address is required",
- "7e1845870b528392": "Enable model",
"7e1f06318e80c3af": "Statistics cleared",
- "7e9ab9ada2cbf2cb": "Name and Base URL are required",
+ "7e7df68f2a82e09e": "Importing the same configuration again will not create duplicate models. Existing models are skipped automatically.",
"7f3c8312816fe26a": "Refreshing…",
"7f68ebad19ba6bcd": "Check for updates",
- "80a57e03f0717f91": "Not configured",
"811a3b22a5a7f2d5": "Unable to connect to the local management service",
"83fcfb4c1f2c1641": "Fetch models",
"842b9f11cdd96bda": "Launch at login",
- "84924374710e03bd": "Base URL must be a valid URL",
+ "843ac7e15a5047a7": "Confirm legacy model configuration import",
"864597982c308d72": "Silent start enabled",
"86b7355ec3bd55ef": "Hide API Key",
"8716e1344b0daddb": "Cursor official",
"878a8ab176429a86": "View instructions",
- "883cc47637fe70f3": "Custom request headers appended to every request for this provider. Values must be strings.",
+ "89a101b809be7cfc": "This address is used exactly as entered without changing or appending the request path.",
"8a8542f6964852dc": "Next page",
"8b6ff498515bcc2f": "Time",
+ "8ccaf87ddb9ca3f4": "Legacy configuration",
"8d0c47eb9eac2d34": "Call type",
"8df48894086d6fbd": "Reason (optional)",
+ "8e2d04638a11a7cb": "Only determines the request and response format; it does not change the request URL.",
+ "8ea973394446abba": "Cursor Configuration",
"8f9b0d6cc477d334": "Choose how Cursor connects to TAB endpoints.",
"90800c48a1dd0655": "{label} must be a JSON object",
"919cb0ce0c8db4e7": "Leave blank to keep the current password",
"91aaf184cfc17ffd": "Overview",
- "91ee087c3d4f3220": "Supports a complete HTTP(S) URL or a relative path beginning with /, combined with the provider URL.",
"940a168911ade998": "Items per page",
"946b3ffc02f026c0": "Delete this model?",
- "94803f35c825e47a": "Full request URL",
- "94df1e7f04815daf": "Used only for display; does not change the model name sent to the provider.",
"966498853d801a52": "TAB connection",
"9850ed41a5bfbb0c": "{count} selected",
"997ec8201c2adeda": "Open terminal to install CA",
- "9a026819dd1af5c5": "Enter a model identifier directly or select one returned by the current provider.",
- "9ac0ac940982895d": "Select provider",
+ "9b1b7ed518ee401d": "This will open the tutorial in your system browser. Continue?",
"9c41b3a9e12ac994": "Reasoning effort",
- "9d8f2ef4e85ea665": "Custom context tokens",
"9e356080c56877f8": "Silent start disabled",
"9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago",
"9f6fee1aba17a565": "Language",
"9fb48101d237ff96": "Last week",
"a026f37e613cf48b": "Output Tokens",
"a03a1a0cb35414f8": " must be an integer from 0 to 65535",
+ "a0c42c24e74f8380": "{name} Copy",
"a1a42cd9b16e2162": "Application",
"a1b8c98f29374a2f": "Silent start",
- "a2901cd2743c1921": "The API root URL for the model service. Changing it also updates the routing identity of this provider's models.",
"a3030bf8f16dc63c": "Save",
+ "a340bdf12a15fd80": "All {count} models in this configuration already exist; nothing needs to be imported",
"a363743025795ec7": "I've initialized it — refresh",
"a3ab741ceb188e9e": "Request content was not recorded. Enable detailed records and try again.",
"a49ffd73bc85333d": "Average",
+ "a5fb6189a8ad011d": "Open tutorial",
"a621ab606db2a11f": "Password",
"a693d69af48bfe48": "Save and test",
"a748cc074f78de00": "View details",
+ "a7617f42f898b2bf": "Use complete request URL",
+ "a8036485f9227f2c": "Drag to reorder",
"a98585871c5313ff": "Display name",
- "a9ab292ea9feecdc": "Provider",
- "aa678e13bda90e3c": "Custom Header values must be strings or null",
- "ab2f31f30acf7fda": "Protocol",
"ab9084a640fbb864": "Deselect all",
"abecab6701177721": "Launch at login enabled",
"ac69f68b7010ec79": "Download and install",
@@ -209,30 +216,27 @@
"ae2d0b7f79cea4a3": "Model output: {tokens} × ${price}/1M = {cost}",
"aee88743413144a2": "Refresh",
"b06325c5660f0c29": "Direct",
+ "b16c3b2ecedd6fe1": "Cursor integration is active. Add a model configuration to use a BYOK model.",
"b4411558b932266f": "Provider type",
"b502b1d414664337": "Prompt: {tokens}",
"b5141d3d19e9a048": "Yes",
- "b710ec36ad312918": "The model service API root URL, for example https://api.openai.com/v1.",
"b75a46aad3e7c132": "Non-cached input: {tokens}",
"b79354009c614ae9": "Statistics",
"b86967982067d295": " (estimated)",
"b89a0e4584f27ab5": "Open terminal",
"b8c9b486c83b5778": "Hide ad",
"b9670c85a4ab939e": "Route",
- "b97ad406809572e1": "Enable reasoning",
"b9af2de88d903be7": "Proxy address",
"baff6c144180b185": "Connectivity tests completed: {successful} succeeded, {failed} failed",
"bb2b7736433ae867": "Cursor tracing",
"bb7efdcb6af6e805": "Default dark",
"bda62ce1d5e4ace9": "Tell us why",
"bf57afd709694b55": "Overview time range",
- "bf84379b75735037": "Edit provider",
"bfc01caf9fe0c841": "Cache hit rate {rate}",
"c1e98892a77f7a19": "{count} per page",
"c3760858cdb6d9f4": "Request body",
- "c62a58459251b02c": "Image generation",
- "c6cc835023617457": "Custom context",
"c7ea2c9bc43134bd": "Edit model",
+ "c8c14507b2d37395": "Reasoning effort",
"c8df3c14a003bfcd": "Unable to load call details",
"c98e118e0a43f078": "Model",
"c9b9ae7a61444ab7": "Previous page",
@@ -241,24 +245,21 @@
"cea1aafe9416de7b": "Request headers",
"cfae1a14d2120c57": "Detailed mode",
"cfe085015632e9c8": "The local management service port used by the desktop frontend. Enter 0 to select a random port at startup.",
- "cfe999e50be8ef54": "Whether the model declares image-generation support.",
"d0bfccc77315d887": "Last month",
- "d15a909c3490a7e0": "Endpoint type",
- "d27db596b73a0a66": "For example: 272000",
+ "d1251cd752d4ec25": "Leave blank to use adaptive thinking.",
"d2d648bd1c94b7f9": "Authentication",
- "d30d35c5ec4a888d": "Select or enter at least one model",
"d34335433395cd3a": "Start Cursor BYOK automatically after signing in.",
+ "d3716cc5a2f5a810": "Server address",
"d3d21191f32e79a5": "Processing…",
- "d44e9b3d3b31d37b": "Name",
- "d667c1616e31c4c4": "Leave blank to keep the current key",
"d86fa42c3848c680": "Use system proxy",
"d8c47e9776cf1082": "Main menu",
"da521d1c1cbd36af": "Authorization is required to install the certificate",
"da7ae985487c38e6": "Last hour",
- "db0a1393f05aa4bd": "Inherits the provider by default; override it for this model if needed.",
+ "daede9881787abe7": "Notes",
+ "dbd3596e4a86f3c2": "Configured models",
"ddde16f8839da3ce": "Total requests",
- "de8184da1ef88d03": "Configured",
"dea7749c4cd77e6d": "Total request Tokens include the prompt and model output.",
+ "df1baa9f706d970b": "To add",
"df3d58c7d84b85f2": "Settings",
"df8b71c74d9b8478": "Response stream",
"dfb802238b38fbd4": "Enabled",
@@ -266,31 +267,29 @@
"e0fae77446a389a3": "Speed: {speed} tokens/s",
"e1295adecbb77755": "Close ad",
"e14115de7f7c5795": "Token usage over the past year",
- "e14a5eee9b0b0f9f": "Whether the model declares reasoning support.",
"e14f20d572c02611": "Provider call sequence",
+ "e17a5b9c90cda6ab": "Model duplicated",
"e18516550b9a5105": "No usage",
"e24ebe4a866d69bf": "Test failed: {error}",
"e25bf3f419bb68f0": "Call history",
"e3fee05f688708b4": "LLM calls",
+ "e4760c6a1df24f17": "Complete request URL",
"e5043c7a2b408271": "Last 10 minutes",
"e59ae97924d62f01": "First page",
"e5b9961a0d5242e3": "Port settings saved. Restart the app to apply them.",
- "e671f8c7598139ef": "After entering a token count, it is added as an extra option to the Cursor model Context list. It only takes effect when selected in Cursor.",
- "e6ca887f22288cde": "Model ID and display name are required",
"e77e3d58b0dcffaa": "Duration",
+ "e825a2a42c22380e": "Model type",
"e828bd3a0151edc2": "The local CA must be trusted by the system",
+ "e8b1268c1e3610f2": "Existing",
"ea26b760e930a7ca": "Call observability",
"eb11e2df1d8ae387": "Provider URL",
"eb1be07f2ca6e506": "Estimated using Claude Opus 4.7 pricing.",
"eb77492c9f76a7e1": "The install command has been copied. Click “Open terminal”, paste it into the terminal, and enter your password when prompted.",
"ed31fbb483ee1b0a": "Actions",
"edc70de18c6da1a6": "Install local CA",
- "ede6d08f397df283": "Cursor settings",
"ee239f3943293f87": "Sunday",
"ee6b89a6a740a4c4": "If a port is occupied, a new random port is selected and saved automatically. Restart the app after changing these settings.",
- "f04c91a6bc3a6926": "Extra parameters",
"f2bdc88464c51c2e": "Show API Key",
- "f396118b8afd2a21": "Cursor interception is active. Add a provider and its model configuration to use BYOK models.",
"f3a76d896853c1df": "Miss",
"f4694c46b1e19602": "Final request type",
"f4dcb6a3ceb32247": "Page {page} of {count}",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json
index 7dea200..855879f 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.json
+++ b/apps/desktop/src/i18n/locales/zh-CN.json
@@ -1,27 +1,25 @@
{
+ "0006d696d8e1ec28": "新增",
"00929f23850e4ff0": "成功调用:{count}",
"01f3e69a5a9b2c9b": "访问模型服务所需的密钥。",
- "025b70bde54ed1ef": "模型是否允许被 Cursor 选择使用。",
"028a4de61bff743d": "普通输入:{tokens} × ${price}/1M = {cost}",
"03ff62ab4b818492": "缓存写入:{tokens} × ${price}/1M = {cost}",
"051836569928a9f9": "编辑",
"05468af47054d488": "模型 {model} 连通性测试成功({duration} ms)",
"0580e0a99a6f1afc": "工件数",
- "05912a17829faacc": "所有调用记录和详细追踪数据都会被删除。供应商、模型、CA 和应用设置不会受到影响,此操作无法撤销。",
"076832c1b2de22c3": "缓存写入:{tokens}",
"07879e064ae16542": "输出推算:{tokens}",
- "07ec86e0f1d44f91": "创建时间",
+ "07c657ed4747126e": "Anthropic 额外参数",
"08d742566ffc9fa3": "确定要清理所有统计数据吗?",
+ "092b520558eff5f2": "未测试",
"099008ea7a42ebd1": "自定义 Headers 的值必须都是字符串",
"09ebc2643631ba25": "价值估算",
- "0a5f9a892960a00a": "更新时间",
"0b96da34f6fbdd3b": "缓存读取:{tokens} × ${price}/1M = {cost}",
- "0bd20614caf56647": "支持选择或输入多个模型;批量添加时显示名称默认使用对应模型名称。",
- "0c2fee621d406ea7": "选择已有上游,或创建一个新的上游。",
"0c70665b6eb65f1a": "否",
"0c72229b7db0e1a9": "模型输出",
"0d5e2bdb15579fc4": "消息数",
"0e41f8e3d59ec47b": "存储管理",
+ "0e67021ebf0a3580": "导入完成:新增 {imported} 个模型,跳过 {skipped} 个已存在模型",
"0ec1e85b0c3cfa65": "调用详情",
"105a9082c346f958": "测试中…",
"124be3f86f197802": "Token 消耗",
@@ -31,12 +29,10 @@
"13a9ac7a68c5fd96": "CA 仅保存在本机,用于安全解析 Cursor 的 HTTPS 请求。",
"13b61c5f697b6700": "缓存命中率",
"146da2e2a991493e": "获取中…",
- "15d76f1c211cc140": "留空表示保留当前 API Key。",
+ "15730c19fd7eef51": "请求协议",
"168e845a86bc3703": "添加模型",
- "16aa4ae1427fccce": "自定义 Headers JSON",
"16d0d7e2b332af72": "总调用:{count}",
"1813d362a82fd437": "最大化窗口",
- "18d25065b3f7db39": "请求完整地址不能为空",
"19658d9fa9aa8de4": "安装中…",
"1a3f0617d6de8e52": "用户名",
"1ae6b0a0f8266382": "关闭窗口",
@@ -45,26 +41,33 @@
"1c6926877b1bbfb2": "总请求:{tokens}",
"1cb7e646ac883fb2": "代理端口",
"1cdde778cfe6a130": "公式:缓存读取 /(缓存读取 + 非缓存输入)",
- "1cef73a53d7ad0f2": "例如:OpenAI",
+ "1d27f02ed278ebc9": "配置文件",
"1f9b46e61efcccb5": "筛选项:{label},{summary}",
- "21296ab18ad9af25": "额外参数 JSON",
+ "202064bb84804852": "留空时使用默认值。",
+ "20e14248fd4fb981": "{label} 必须是大于 0 的整数",
"217cfe7db1e3d10a": "跟随系统",
"22c6b4eb4caee6ae": "代理设置已保存",
"23e49479e15e6770": "发现新版本 {version}",
+ "24a0a24864454575": "已存在,跳过",
+ "2555d6c7fbb7e070": "可以直接输入模型标识,也可以读取接口返回的模型列表。",
"29585d7193539200": "当前版本 {version}",
"29fbbef32a6eb58b": "不再显示此广告",
"2a2773134a829016": "按历史 LLM 调用记录汇总,进行中的调用不计入。",
+ "2caeaec539e78898": "思考预算 Token",
"2cd0f3be8738a86c": "取消",
"2d30c2a98ebb5278": "当前:{rate}",
"2eb2bf7c6597ab9a": "详细记录",
- "2f1b67cdaa23351a": "自定义请求完整地址",
+ "2f280a8120c0db12": "仅用于界面展示,不会改变发送给模型服务的模型名称。",
"2f4a361f878176d1": "{label} 必须是有效 JSON",
"2f4a9609285d8f49": "TAB 设置已保存",
"2f5f1d6fbfb061ed": "未设置",
"2f6416a2c424856b": "最终请求地址",
+ "2f7ba5fd1d12f7f9": "打开使用教程?",
"2f7dec3be28d7597": "已选择 {count} 个",
"2f9daa828907b93f": "删除",
+ "346ff60e6c7c5181": "读取中…",
"36f33adaf0942634": "确认",
+ "37125ef2e1d707cb": "服务器地址或完整请求 URL、API Key、模型名称、显示名称和备注不能为空",
"378bb0eec39fa8a2": "最后一页",
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
"393df9bb13ea4900": "命中",
@@ -74,7 +77,7 @@
"3a0fb74abe2460b6": "分块数",
"3a3f595df70ec8ff": "清理存储空间",
"3a5040b68abf75f9": "全选",
- "3bd5b3f538e3270a": "新建上游",
+ "3a8c76b2ce785f96": "查看并导入",
"3c94b4c75940c178": "输入 Token",
"3cfae5728b92b334": "Token 用量:{tokens}",
"3d13868593ae4eeb": "界面语言",
@@ -93,9 +96,12 @@
"48d8db17bae06246": "共 {count} 条",
"492042ed1fdc29ed": "版本 {version} 可以安装",
"4927a53bcc886afb": "加载中…",
- "493e097fac6bd217": "上游配置",
"497c85690c4cc0fc": "暂无数据",
+ "499c729eb09aa2a6": "上下文窗口 Token",
+ "4a8d6841b4023edf": "确认导入",
+ "4b458e6e147221d7": "系统会根据请求协议自动追加标准端点路径。",
"4d0680f9efaef147": "未读",
+ "4e30d7c9ed2b0eee": "不设置",
"4eafa9e925b30bcd": "自定义",
"51d04bc3d286f018": "近1自然日",
"51de3bcec137ab1b": "全部 {count} 个模型连通性测试成功",
@@ -103,15 +109,18 @@
"5401344227e49e2f": "TAB 设置",
"54644705e9c61009": "端口设置",
"54c53e5fe791d1f3": "初始化 CA",
+ "54e6745ff43c9c74": "排序失败",
"550eddc3c7fefa99": "推广",
- "555737734a6371e6": "删除上游",
"56432ba297009bdc": "请先初始化 CA",
+ "56627c94a9decee6": "最大输出 Token",
+ "576d81bb0631b165": "导入",
+ "5886afc1c71df1fe": "显示在 Cursor 模型说明中。",
"59346e82b3dd2998": "TAB 服务地址",
+ "5a284a1a2be8da0e": "从本机旧版配置读取模型;确认前会显示新增和已存在的模型。",
"5a3bd99fa69a40c1": "使用公益服务",
- "5ae715656ffbc35d": "合并到该上游所有模型的请求体。",
"5b17f59d33bde39e": "错误:{error}",
"5c55a67935af8f45": "全部",
- "5cca0b7972a11f3a": "自定义上下文必须是大于 0 的整数",
+ "5cae248525cb9140": "所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
"5d59857bf039cac9": "Cursor 助手 v{version}",
"5f8d556a9c47da3c": "已关闭开机启动",
"5f9acfb945229062": "你确认不想再看到此广告吗?",
@@ -122,6 +131,7 @@
"621f63a5f08384ac": "缓存读取:{tokens}",
"6320b4a8722a851f": "状态",
"63c73c4730f4473e": "应用",
+ "63d90d977348ab1f": "复制",
"6478a5f1218c484e": "请在桌面应用中复制到系统剪贴板",
"651f274470153a05": "软件更新",
"652ec5d40c29fd6a": "速度 {speed} tokens/s · 首字 {firstText} ms · 总耗时 {duration} ms · 输出 {tokens} tokens{estimated} · 返回:{output}",
@@ -129,7 +139,7 @@
"656ab25e264cc4e4": "还没有可供 Cursor 使用的模型",
"65a6318e07ec1e07": "工具数",
"65cb9a7b4f620b6b": "提示词 {tokens}",
- "68152165b3348852": "选择上游服务使用的请求协议。",
+ "68ad603fafe4e0d6": "导入旧版配置",
"68ea5dd4d7af20e6": "系统设置",
"6a9906c79f26c0ba": "开始时间",
"6aa8f49cc992dfd7": "测试",
@@ -137,71 +147,68 @@
"6d1876364ac6457d": "代理方式",
"6e86570183c3cdd0": "当前已是最新版本",
"7005693f4f050bce": "缓存读写 {cost}",
- "71eb2c6fce991a29": "添加上游",
+ "736c9dc2a04c65fd": "模型配置已发生变化,请刷新后重试",
"7392e20d61abaa07": "额外保存完整请求和流响应;默认只保存时间、状态与用量。",
- "75844712d53a24de": "值必须是字符串;编辑时 null 表示保留对应敏感 Header 的原值。",
"788db1cfec2a3db5": "主题",
"7995087e5a3dfe66": "还原窗口",
"7a2229f6a6d330a5": "请在桌面应用中打开终端安装 CA",
"7a3cec4ca715de80": "调用统计",
"7ba2d6728fe2531b": "确认清理",
+ "7cea2f3c46565d29": "OpenAI 额外参数",
"7d9f043f8f7ab45c": "发现新版本 {version},可在设置中安装",
"7e0891860c9e6374": "TAB 服务地址不能为空",
- "7e1845870b528392": "启用模型",
"7e1f06318e80c3af": "统计数据已清理",
- "7e9ab9ada2cbf2cb": "名称和 Base URL 不能为空",
+ "7e7df68f2a82e09e": "重复导入相同配置不会创建重复模型;已经存在的模型会自动跳过。",
"7f3c8312816fe26a": "刷新中…",
"7f68ebad19ba6bcd": "检查更新",
- "80a57e03f0717f91": "未配置",
"811a3b22a5a7f2d5": "无法连接本地管理服务",
"83fcfb4c1f2c1641": "获取模型",
"842b9f11cdd96bda": "开机启动",
- "84924374710e03bd": "Base URL 必须是有效地址",
+ "843ac7e15a5047a7": "确认导入旧版模型配置",
"864597982c308d72": "已开启静默启动",
"86b7355ec3bd55ef": "隐藏 API Key",
"8716e1344b0daddb": "Cursor 官方",
"878a8ab176429a86": "查看说明",
- "883cc47637fe70f3": "附加到该上游所有请求的自定义请求头,值必须是字符串。",
+ "89a101b809be7cfc": "系统会原样使用此地址,不追加或修改请求路径。",
"8a8542f6964852dc": "下一页",
"8b6ff498515bcc2f": "时间",
+ "8ccaf87ddb9ca3f4": "旧版配置",
"8d0c47eb9eac2d34": "调用类型",
"8df48894086d6fbd": "原因(可选)",
+ "8e2d04638a11a7cb": "只决定请求与响应的格式,不会改变请求地址。",
+ "8ea973394446abba": "Cursor 配置",
"8f9b0d6cc477d334": "控制 Cursor TAB 相关接口的连接方式。",
"90800c48a1dd0655": "{label} 必须是 JSON 对象",
"919cb0ce0c8db4e7": "留空表示保留当前密码",
"91aaf184cfc17ffd": "数据概览",
- "91ee087c3d4f3220": "支持完整 HTTP(S) 地址或以 / 开头、与上游地址组合的相对路径。",
"940a168911ade998": "每页条数",
"946b3ffc02f026c0": "确定删除这个模型吗?",
- "94803f35c825e47a": "请求完整地址",
- "94df1e7f04815daf": "仅用于界面展示,不会改变发送给上游的模型名称。",
"966498853d801a52": "TAB 选择",
"9850ed41a5bfbb0c": "已选 {count} 项",
"997ec8201c2adeda": "打开终端安装 CA",
- "9a026819dd1af5c5": "可以直接输入模型标识,也可以从当前上游返回的模型列表中选择。",
- "9ac0ac940982895d": "选择上游",
+ "9b1b7ed518ee401d": "将在系统浏览器中打开使用教程,是否继续?",
"9c41b3a9e12ac994": "思考强度",
- "9d8f2ef4e85ea665": "自定义上下文 tokens",
"9e356080c56877f8": "已关闭静默启动",
"9e46da6923836182": "如:2026-08-23 09:00、1小时前",
"9f6fee1aba17a565": "语言",
"9fb48101d237ff96": "近一周",
"a026f37e613cf48b": "输出 Token",
"a03a1a0cb35414f8": "必须是 0–65535 之间的整数",
+ "a0c42c24e74f8380": "{name} 副本",
"a1a42cd9b16e2162": "应用设置",
"a1b8c98f29374a2f": "静默启动",
- "a2901cd2743c1921": "模型服务的 API 根地址;修改后会同步更新该上游模型的路由身份。",
"a3030bf8f16dc63c": "保存",
+ "a340bdf12a15fd80": "配置中的 {count} 个模型均已存在,无需重复导入",
"a363743025795ec7": "我已初始化,刷新",
"a3ab741ceb188e9e": "未记录请求内容,请开启详细记录后重试。",
"a49ffd73bc85333d": "平均",
+ "a5fb6189a8ad011d": "打开教程",
"a621ab606db2a11f": "密码",
"a693d69af48bfe48": "保存并测试",
"a748cc074f78de00": "查看详情",
+ "a7617f42f898b2bf": "使用完整请求地址",
+ "a8036485f9227f2c": "拖动排序",
"a98585871c5313ff": "显示名称",
- "a9ab292ea9feecdc": "上游",
- "aa678e13bda90e3c": "自定义 Headers 的值必须是字符串或 null",
- "ab2f31f30acf7fda": "协议",
"ab9084a640fbb864": "全不选",
"abecab6701177721": "已开启开机启动",
"ac69f68b7010ec79": "下载并安装",
@@ -209,30 +216,27 @@
"ae2d0b7f79cea4a3": "模型输出:{tokens} × ${price}/1M = {cost}",
"aee88743413144a2": "刷新",
"b06325c5660f0c29": "直连",
+ "b16c3b2ecedd6fe1": "Cursor 接管已生效;添加模型配置后即可使用 BYOK 模型。",
"b4411558b932266f": "上游类型",
"b502b1d414664337": "提示词:{tokens}",
"b5141d3d19e9a048": "是",
- "b710ec36ad312918": "模型服务的 API 根地址,例如 https://api.openai.com/v1。",
"b75a46aad3e7c132": "非缓存输入:{tokens}",
"b79354009c614ae9": "统计数据",
"b86967982067d295": "(估算)",
"b89a0e4584f27ab5": "打开终端",
"b8c9b486c83b5778": "不再显示广告",
"b9670c85a4ab939e": "路由",
- "b97ad406809572e1": "启用推理",
"b9af2de88d903be7": "代理地址",
"baff6c144180b185": "连通性测试完成:成功 {successful},失败 {failed}",
"bb2b7736433ae867": "Cursor 追踪",
"bb7efdcb6af6e805": "默认暗色",
"bda62ce1d5e4ace9": "可以告诉我们原因",
"bf57afd709694b55": "概览时间范围",
- "bf84379b75735037": "编辑上游",
"bfc01caf9fe0c841": "缓存命中率 {rate}",
"c1e98892a77f7a19": "{count} 条/页",
"c3760858cdb6d9f4": "请求体",
- "c62a58459251b02c": "图片生成",
- "c6cc835023617457": "自定义上下文",
"c7ea2c9bc43134bd": "编辑模型",
+ "c8c14507b2d37395": "推理强度",
"c8df3c14a003bfcd": "无法加载调用详情",
"c98e118e0a43f078": "模型",
"c9b9ae7a61444ab7": "上一页",
@@ -241,24 +245,21 @@
"cea1aafe9416de7b": "请求头",
"cfae1a14d2120c57": "详细模式",
"cfe085015632e9c8": "桌面前端连接的本地管理服务端口;填写 0 时启动时随机选择。",
- "cfe999e50be8ef54": "是否声明模型支持图片生成。",
"d0bfccc77315d887": "近一个月",
- "d15a909c3490a7e0": "端点类型",
- "d27db596b73a0a66": "例如:272000",
+ "d1251cd752d4ec25": "留空时使用 adaptive thinking。",
"d2d648bd1c94b7f9": "认证",
- "d30d35c5ec4a888d": "请至少选择或输入一个模型",
"d34335433395cd3a": "登录系统后自动启动 Cursor BYOK。",
+ "d3716cc5a2f5a810": "服务器地址",
"d3d21191f32e79a5": "处理中…",
- "d44e9b3d3b31d37b": "名称",
- "d667c1616e31c4c4": "留空以保留当前密钥",
"d86fa42c3848c680": "使用系统代理",
"d8c47e9776cf1082": "主菜单",
"da521d1c1cbd36af": "需要授权安装证书",
"da7ae985487c38e6": "近1小时",
- "db0a1393f05aa4bd": "默认继承上游,可为当前模型单独修改。",
+ "daede9881787abe7": "备注",
+ "dbd3596e4a86f3c2": "配置模型",
"ddde16f8839da3ce": "总请求",
- "de8184da1ef88d03": "已配置",
"dea7749c4cd77e6d": "总请求 Token 包含提示词和模型输出。",
+ "df1baa9f706d970b": "将新增",
"df3d58c7d84b85f2": "设置",
"df8b71c74d9b8478": "响应流",
"dfb802238b38fbd4": "已启用",
@@ -266,31 +267,29 @@
"e0fae77446a389a3": "速度:{speed} tokens/s",
"e1295adecbb77755": "关闭广告",
"e14115de7f7c5795": "过去一年的 Token 用量",
- "e14a5eee9b0b0f9f": "是否声明模型支持推理能力。",
"e14f20d572c02611": "上游调用序号",
+ "e17a5b9c90cda6ab": "模型已复制",
"e18516550b9a5105": "无用量",
"e24ebe4a866d69bf": "测试失败:{error}",
"e25bf3f419bb68f0": "调用详细",
"e3fee05f688708b4": "LLM 调用",
+ "e4760c6a1df24f17": "完整请求 URL",
"e5043c7a2b408271": "近10分钟",
"e59ae97924d62f01": "第一页",
"e5b9961a0d5242e3": "端口设置已保存,重启软件后生效",
- "e671f8c7598139ef": "输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。",
- "e6ca887f22288cde": "Model ID 和显示名称不能为空",
"e77e3d58b0dcffaa": "耗时",
+ "e825a2a42c22380e": "模型类型",
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
+ "e8b1268c1e3610f2": "已存在",
"ea26b760e930a7ca": "调用观测",
"eb11e2df1d8ae387": "上游地址",
"eb1be07f2ca6e506": "按 Claude Opus 4.7 价格估算。",
"eb77492c9f76a7e1": "安装命令已自动复制。点击“打开终端”,将命令粘贴到终端中执行,并按提示输入密码。",
"ed31fbb483ee1b0a": "操作",
"edc70de18c6da1a6": "安装本地 CA",
- "ede6d08f397df283": "Cursor 设置",
"ee239f3943293f87": "周日",
"ee6b89a6a740a4c4": "端口被占用时会自动选择新的随机端口并保存。修改后需要重启软件才会生效。",
- "f04c91a6bc3a6926": "额外参数",
"f2bdc88464c51c2e": "显示 API Key",
- "f396118b8afd2a21": "Cursor 接管已生效;添加上游及其模型配置后即可使用 BYOK 模型。",
"f3a76d896853c1df": "未命中",
"f4694c46b1e19602": "最终请求类型",
"f4dcb6a3ceb32247": "第 {page} / {count} 页",
diff --git a/apps/desktop/src/layouts/AppLayout.tsx b/apps/desktop/src/layouts/AppLayout.tsx
index 2289baa..4fb8f27 100644
--- a/apps/desktop/src/layouts/AppLayout.tsx
+++ b/apps/desktop/src/layouts/AppLayout.tsx
@@ -9,11 +9,11 @@ import { FloatingAd } from "../components/ads/FloatingAd";
import { AdActionType, type AdAction, type AdSlot } from "../components/ads/types";
import { PageLayout } from "../components/layout/PageLayout";
import { Card } from "../components/ui/Card";
+import { ConfirmDialog } from "../components/ui/ConfirmDialog";
import controls from "../components/ui/Controls.module.scss";
import { Icon } from "../components/ui/Icon";
-import { Modal } from "../components/ui/Modal";
import { TooltipTrigger } from "../components/ui/TooltipTrigger";
-import { flatColorAboutIcon, flatColorAreaChartIcon, flatColorOrganizationIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../components/ui/icons";
+import { flatColorAboutIcon, flatColorAreaChartIcon, flatColorSalesPerformanceIcon, flatColorSettingsIcon, refreshIcon } from "../components/ui/icons";
import { useMessage } from "../components/ui/message";
import { VirtualList } from "../components/virtual/VirtualList";
import { useI18n } from "../i18n/store";
@@ -27,7 +27,7 @@ type MenuItem =
| { kind: "external"; id: string; label: string; icon: IconifyIcon | string }
| { kind: "group"; label: string };
-const keptAlivePages = ["/", "/calls", "/providers", "/settings", "/harness/cursor"];
+const keptAlivePages = ["/", "/calls", "/settings", "/harness/cursor"];
const readAdStorageKey = "cursor-byok:read-ad-ids";
const dismissedAdStorageKey = "cursor-byok:dismissed-ad-ids";
const tutorialReadStorageKey = "cursor-byok:tutorial-read";
@@ -54,6 +54,7 @@ export function AppLayout() {
const [activeAd, setActiveAd] = useState(null);
const [dismissCandidate, setDismissCandidate] = useState(null);
const [dismissReason, setDismissReason] = useState("");
+ const [confirmTutorial, setConfirmTutorial] = useState(false);
const [tutorialRead, setTutorialRead] = useState(() => {
try {
return localStorage.getItem(tutorialReadStorageKey) === "true";
@@ -71,21 +72,23 @@ export function AppLayout() {
const menuItems: MenuItem[] = [
{ kind: "page", path: "/", label: t("数据概览"), icon: flatColorAreaChartIcon },
{ kind: "page", path: "/calls", label: t("调用详细"), icon: flatColorSalesPerformanceIcon },
- { kind: "page", path: "/providers", label: t("上游配置"), icon: flatColorOrganizationIcon },
- { kind: "external", id: "tutorial", label: t("使用教程"), icon: flatColorAboutIcon },
{ kind: "group", label: "Harness" },
+ { kind: "page", path: "/harness/cursor", label: t("Cursor 配置"), icon: cursorIconUrl },
{ kind: "page", path: "/settings", label: t("系统设置"), icon: flatColorSettingsIcon },
- { kind: "page", path: "/harness/cursor", label: t("Cursor 设置"), icon: cursorIconUrl },
+ { kind: "external", id: "tutorial", label: t("使用教程"), icon: flatColorAboutIcon },
];
const openTutorial = useCallback(() => {
- setTutorialRead(true);
- try {
- localStorage.setItem(tutorialReadStorageKey, "true");
- } catch {
- // Read state remains valid for the current session when storage is unavailable.
- }
+ setConfirmTutorial(false);
void api.openExternalUrl(tutorialUrl)
+ .then(() => {
+ setTutorialRead(true);
+ try {
+ localStorage.setItem(tutorialReadStorageKey, "true");
+ } catch {
+ // Read state remains valid for the current session when storage is unavailable.
+ }
+ })
.catch((cause) => message(cause instanceof Error ? cause.message : String(cause)));
}, [message]);
@@ -195,7 +198,7 @@ export function AppLayout() {
void saveProxy()} />
+ void saveTab()} />
+ {({ busy, previewing, open }) =>
+
+
+ {t("旧版配置")}
+ {t("从本机旧版配置读取模型;确认前会显示新增和已存在的模型。")}
+
+
+ {previewing ? t("读取中…") : t("查看并导入")}
+
+
+ }
@@ -240,24 +284,24 @@ export function SettingsPage() {
-
setConfirmClear(false)}
- onSubmit={() => void clearStorage()}
+ cancelLabel={t("取消")}
+ confirmLabel={t("确认清理")}
+ onCancel={() => setConfirmClear(false)}
+ onConfirm={() => void clearStorage()}
>
{t(
- "所有调用记录和详细追踪数据都会被删除。供应商、模型、CA 和应用设置不会受到影响,此操作无法撤销。",
+ "所有调用记录和详细追踪数据都会被删除。模型配置、CA 和应用设置不会受到影响,此操作无法撤销。",
)}
-
+
);
- return ;
+ return ;
}
diff --git a/apps/desktop/src/store/appStore.ts b/apps/desktop/src/store/appStore.ts
index e384294..e8dde89 100644
--- a/apps/desktop/src/store/appStore.ts
+++ b/apps/desktop/src/store/appStore.ts
@@ -1,11 +1,8 @@
import { useSyncExternalStore } from "react";
-import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PortSettings, type Provider, type ProviderInput, type ProviderSelection } from "../api";
+import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PortSettings } from "../api";
import { applyTheme, isThemeId, type ThemeId } from "../theme/theme";
-type Discovery = { provider: Provider; modelIds: string[] };
-
export type AppSnapshot = {
- providers: Provider[];
models: Model[];
calls: LlmCall[];
overview: Overview;
@@ -13,8 +10,6 @@ export type AppSnapshot = {
ports: PortSettings;
busy: boolean;
error: string | null;
- discoveringProviderId: number | null;
- discovery: Discovery | null;
theme: ThemeId;
cursorHarness: CursorHarnessStatus | null;
cursorBusy: boolean;
@@ -26,7 +21,6 @@ const savedTheme = (): ThemeId => {
};
let snapshot: AppSnapshot = {
- providers: [],
models: [],
calls: [],
overview: {
@@ -48,8 +42,6 @@ let snapshot: AppSnapshot = {
ports: { proxy_port: 0, service_port: 0 },
busy: false,
error: null,
- discoveringProviderId: null,
- discovery: null,
theme: savedTheme(),
cursorHarness: null,
cursorBusy: false,
@@ -81,8 +73,7 @@ export const appStore = {
async refresh() {
update({ busy: true, error: null });
try {
- const [providers, models, calls, overview, settings, ports, cursorHarness] = await Promise.all([
- api.providers(),
+ const [models, calls, overview, settings, ports, cursorHarness] = await Promise.all([
api.models(),
api.calls(),
api.overview(),
@@ -90,7 +81,7 @@ export const appStore = {
api.ports(),
api.cursorHarness(),
]);
- update({ providers, models, calls, overview, detailed: settings.detailed, ports, cursorHarness });
+ update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness });
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
} finally {
@@ -98,68 +89,6 @@ export const appStore = {
}
},
- async createProvider(input: ProviderInput) {
- try {
- update({ error: null });
- await api.createProvider(input);
- await appStore.refresh();
- return true;
- } catch (cause) {
- update({ error: cause instanceof Error ? cause.message : String(cause) });
- return false;
- }
- },
- async updateProvider(providerId: number, input: ProviderInput) {
- try {
- update({ error: null });
- await api.updateProvider(providerId, input);
- await appStore.refresh();
- return true;
- } catch (cause) {
- update({ error: cause instanceof Error ? cause.message : String(cause) });
- return false;
- }
- },
- async deleteProvider(providerId: number) {
- await perform(async () => {
- await api.deleteProvider(providerId);
- await appStore.refresh();
- });
- },
-
- async discoverModels(provider: Provider) {
- update({ discoveringProviderId: provider.provider_id, error: null });
- try {
- const result = await api.discoverModels(provider.provider_id);
- const existing = new Set(snapshot.models.filter((model) => model.provider_id === provider.provider_id).map((model) => model.model_id));
- update({ discovery: { provider, modelIds: result.models.filter((id) => !existing.has(id)) } });
- } catch (cause) {
- update({ error: cause instanceof Error ? cause.message : String(cause) });
- } finally {
- update({ discoveringProviderId: null });
- }
- },
- async addModel(modelId: string) {
- const discovery = snapshot.discovery;
- if (!discovery) return;
- await perform(async () => {
- await api.saveModels(discovery.provider.provider_id, [{
- model_id: modelId,
- display_name: modelId,
- endpoint_type: discovery.provider.provider_type,
- request_url: "",
- enabled: true,
- sort_order: snapshot.models.length,
- context_window_tokens: null,
- max_output_tokens: null,
- reasoning_enabled: false,
- reasoning_effort: null,
- supports_image_generation: false,
- }]);
- update({ discovery: { ...discovery, modelIds: discovery.modelIds.filter((id) => id !== modelId) } });
- await appStore.refresh();
- });
- },
async deleteModel(modelHash: string) {
await perform(async () => {
await api.deleteModel(modelHash);
@@ -184,15 +113,26 @@ export const appStore = {
catch (cause) { update({ error: cause instanceof Error ? cause.message : String(cause) }); }
finally { update({ cursorBusy: false }); }
},
- async createCursorModels(provider: ProviderSelection, models: ModelInput[]) {
+ async createModels(models: ModelInput[]) {
update({ cursorBusy: true, error: null });
try {
- await api.createCursorModels(provider, models);
+ const created = await api.createModels(models);
await appStore.refresh();
- return true;
+ return created;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
- return false;
+ return null;
+ } finally { update({ cursorBusy: false }); }
+ },
+ async importV0049Models() {
+ update({ cursorBusy: true, error: null });
+ try {
+ const result = await api.importV0049Models();
+ await appStore.refresh();
+ return result;
+ } catch (cause) {
+ update({ error: cause instanceof Error ? cause.message : String(cause) });
+ return null;
} finally { update({ cursorBusy: false }); }
},
async updateCursorModel(hash: string, model: ModelInput) {
@@ -206,6 +146,36 @@ export const appStore = {
return null;
} finally { update({ cursorBusy: false }); }
},
+ async reorderCursorModels(modelHashes: string[]) {
+ const previous = snapshot.models;
+ const byHash = new Map(previous.map((model) => [model.model_hash, model]));
+ if (modelHashes.length !== previous.length || new Set(modelHashes).size !== previous.length) {
+ update({ error: t("模型配置已发生变化,请刷新后重试") });
+ return false;
+ }
+ const reordered: Model[] = [];
+ for (const [index, hash] of modelHashes.entries()) {
+ const model = byHash.get(hash);
+ if (!model) {
+ update({ error: t("模型配置已发生变化,请刷新后重试") });
+ return false;
+ }
+ reordered.push({ ...model, sort_order: index + 1 });
+ }
+ update({ models: reordered, cursorBusy: true, error: null });
+ try {
+ update({ models: await api.reorderModels(modelHashes) });
+ return true;
+ } catch (cause) {
+ update({
+ models: previous,
+ error: cause instanceof Error ? cause.message : String(cause),
+ });
+ return false;
+ } finally {
+ update({ cursorBusy: false });
+ }
+ },
async openCallDetails(callId: string) {
await perform(() => api.openCallDetails(callId));
diff --git a/apps/desktop/src/utils/providerDefaults.ts b/apps/desktop/src/utils/modelDefaults.ts
similarity index 100%
rename from apps/desktop/src/utils/providerDefaults.ts
rename to apps/desktop/src/utils/modelDefaults.ts
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 1f54e88..9b39ed3 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -39,6 +39,7 @@ rcgen = { version = "0.14", features = ["aws_lc_rs", "pem", "x509-parser"] }
scraper = "0.24"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+serde_yaml = "0.9"
semble-core = { path = "../crates/semble-core" }
sha1 = "0.10"
sha2 = "0.10"
diff --git a/server/migrations/0004_flatten_model_configuration.sql b/server/migrations/0004_flatten_model_configuration.sql
new file mode 100644
index 0000000..64c7438
--- /dev/null
+++ b/server/migrations/0004_flatten_model_configuration.sql
@@ -0,0 +1,197 @@
+PRAGMA defer_foreign_keys = ON;
+
+CREATE TABLE model_configs (
+ model_hash TEXT PRIMARY KEY,
+ sort_order INTEGER NOT NULL DEFAULT 0,
+ display_name TEXT NOT NULL,
+ model_type TEXT NOT NULL CHECK(model_type IN ('openai', 'anthropic')),
+ base_url TEXT NOT NULL,
+ use_full_url INTEGER NOT NULL DEFAULT 0 CHECK(use_full_url IN (0, 1)),
+ api_key TEXT NOT NULL,
+ tooltip_data TEXT NOT NULL,
+ model_id TEXT NOT NULL,
+ reasoning_effort TEXT,
+ openai_endpoint TEXT NOT NULL DEFAULT '',
+ openai_extra_params_enabled INTEGER NOT NULL DEFAULT 0 CHECK(openai_extra_params_enabled IN (0, 1)),
+ openai_extra_params_json TEXT NOT NULL DEFAULT '{}',
+ custom_headers_enabled INTEGER NOT NULL DEFAULT 0 CHECK(custom_headers_enabled IN (0, 1)),
+ custom_headers_json TEXT NOT NULL DEFAULT '{}',
+ anthropic_extra_params_enabled INTEGER NOT NULL DEFAULT 0 CHECK(anthropic_extra_params_enabled IN (0, 1)),
+ anthropic_extra_params_json TEXT NOT NULL DEFAULT '{}',
+ context_window_tokens INTEGER,
+ max_completion_tokens INTEGER,
+ anthropic_max_tokens INTEGER,
+ anthropic_thinking_effort TEXT,
+ thinking_budget_tokens INTEGER,
+ created_at_ms INTEGER NOT NULL,
+ updated_at_ms INTEGER NOT NULL
+);
+
+INSERT INTO model_configs (
+ model_hash,
+ sort_order,
+ display_name,
+ model_type,
+ base_url,
+ use_full_url,
+ api_key,
+ tooltip_data,
+ model_id,
+ reasoning_effort,
+ openai_endpoint,
+ openai_extra_params_enabled,
+ openai_extra_params_json,
+ custom_headers_enabled,
+ custom_headers_json,
+ anthropic_extra_params_enabled,
+ anthropic_extra_params_json,
+ context_window_tokens,
+ max_completion_tokens,
+ anthropic_max_tokens,
+ anthropic_thinking_effort,
+ thinking_budget_tokens,
+ created_at_ms,
+ updated_at_ms
+)
+SELECT
+ model.model_hash,
+ model.sort_order,
+ model.display_name,
+ CASE model.endpoint_type WHEN 'anthropic' THEN 'anthropic' ELSE 'openai' END,
+ CASE
+ WHEN model.request_url = '' THEN endpoint.base_url
+ WHEN model.request_url LIKE 'http://%' OR model.request_url LIKE 'https://%' THEN model.request_url
+ ELSE replace(rtrim(endpoint.base_url, '/') || '/' || ltrim(model.request_url, '/'), '/v1/v1/', '/v1/')
+ END,
+ CASE WHEN model.request_url = '' THEN 0 ELSE 1 END,
+ endpoint.api_key,
+ model.display_name,
+ model.model_id,
+ CASE
+ WHEN model.endpoint_type != 'anthropic' AND model.reasoning_enabled = 1
+ THEN COALESCE(NULLIF(trim(model.reasoning_effort), ''), 'medium')
+ ELSE NULL
+ END,
+ CASE model.endpoint_type
+ WHEN 'openai-responses' THEN '/v1/responses'
+ WHEN 'openai-chat' THEN '/v1/chat/completions'
+ ELSE ''
+ END,
+ CASE WHEN model.endpoint_type != 'anthropic' AND endpoint.extra_params_json != '{}' THEN 1 ELSE 0 END,
+ CASE WHEN model.endpoint_type != 'anthropic' THEN endpoint.extra_params_json ELSE '{}' END,
+ CASE WHEN endpoint.custom_headers_json != '{}' THEN 1 ELSE 0 END,
+ endpoint.custom_headers_json,
+ CASE WHEN model.endpoint_type = 'anthropic' AND endpoint.extra_params_json != '{}' THEN 1 ELSE 0 END,
+ CASE WHEN model.endpoint_type = 'anthropic' THEN endpoint.extra_params_json ELSE '{}' END,
+ model.context_window_tokens,
+ CASE WHEN model.endpoint_type != 'anthropic' THEN model.max_output_tokens ELSE NULL END,
+ CASE WHEN model.endpoint_type = 'anthropic' THEN model.max_output_tokens ELSE NULL END,
+ CASE WHEN model.endpoint_type = 'anthropic' THEN 'xhigh' ELSE NULL END,
+ NULL,
+ model.created_at_ms,
+ model.updated_at_ms
+FROM provider_models AS model
+JOIN provider_endpoints AS endpoint ON endpoint.provider_id = model.provider_id;
+
+CREATE TABLE llm_calls_new (
+ call_id TEXT PRIMARY KEY,
+ run_id TEXT NOT NULL,
+ conversation_id TEXT NOT NULL,
+ provider_call_index INTEGER NOT NULL,
+ model_hash TEXT,
+ provider_type TEXT NOT NULL,
+ provider_url TEXT NOT NULL,
+ request_type TEXT NOT NULL,
+ request_url TEXT NOT NULL,
+ model_id TEXT NOT NULL,
+ display_name TEXT NOT NULL,
+ status TEXT NOT NULL,
+ finish_reason TEXT,
+ created_at_ms INTEGER NOT NULL,
+ request_started_at_ms INTEGER,
+ response_headers_at_ms INTEGER,
+ first_event_at_ms INTEGER,
+ first_text_at_ms INTEGER,
+ finished_at_ms INTEGER,
+ queue_ms INTEGER,
+ ttfb_ms INTEGER,
+ ttft_ms INTEGER,
+ duration_ms INTEGER,
+ input_tokens INTEGER,
+ output_tokens INTEGER,
+ total_tokens INTEGER,
+ cache_read_tokens INTEGER,
+ cache_write_tokens INTEGER,
+ reasoning_tokens INTEGER,
+ usage_json TEXT,
+ message_count INTEGER NOT NULL,
+ tool_count INTEGER NOT NULL,
+ request_bytes INTEGER,
+ response_bytes INTEGER NOT NULL DEFAULT 0,
+ stream_event_count INTEGER NOT NULL DEFAULT 0,
+ http_status INTEGER,
+ error_kind TEXT,
+ error_message TEXT,
+ detailed INTEGER NOT NULL,
+ reasoning_effort TEXT,
+ fast INTEGER NOT NULL DEFAULT 0 CHECK (fast IN (0, 1)),
+ FOREIGN KEY(model_hash) REFERENCES model_configs(model_hash)
+);
+
+INSERT INTO llm_calls_new (
+ call_id, run_id, conversation_id, provider_call_index, model_hash, provider_type,
+ provider_url, request_type, request_url, model_id, display_name, status, finish_reason,
+ created_at_ms, request_started_at_ms, response_headers_at_ms, first_event_at_ms,
+ first_text_at_ms, finished_at_ms, queue_ms, ttfb_ms, ttft_ms, duration_ms,
+ input_tokens, output_tokens, total_tokens, cache_read_tokens, cache_write_tokens,
+ reasoning_tokens, usage_json, message_count, tool_count, request_bytes, response_bytes,
+ stream_event_count, http_status, error_kind, error_message, detailed, reasoning_effort, fast
+)
+SELECT
+ call_id, run_id, conversation_id, provider_call_index, model_hash, provider_type,
+ provider_url, request_type, request_url, model_id, display_name, status, finish_reason,
+ created_at_ms, request_started_at_ms, response_headers_at_ms, first_event_at_ms,
+ first_text_at_ms, finished_at_ms, queue_ms, ttfb_ms, ttft_ms, duration_ms,
+ input_tokens, output_tokens, total_tokens, cache_read_tokens, cache_write_tokens,
+ reasoning_tokens, usage_json, message_count, tool_count, request_bytes, response_bytes,
+ stream_event_count, http_status, error_kind, error_message, detailed, reasoning_effort, fast
+FROM llm_calls;
+
+CREATE TABLE llm_call_requests_new (
+ call_id TEXT PRIMARY KEY,
+ headers_json TEXT NOT NULL,
+ body_json TEXT NOT NULL,
+ byte_count INTEGER NOT NULL,
+ FOREIGN KEY(call_id) REFERENCES llm_calls_new(call_id) ON DELETE CASCADE
+);
+
+INSERT INTO llm_call_requests_new(call_id, headers_json, body_json, byte_count)
+SELECT call_id, headers_json, body_json, byte_count FROM llm_call_requests;
+
+CREATE TABLE llm_call_response_chunks_new (
+ call_id TEXT NOT NULL,
+ seq INTEGER NOT NULL,
+ received_offset_ms INTEGER NOT NULL,
+ data BLOB NOT NULL,
+ byte_count INTEGER NOT NULL,
+ PRIMARY KEY(call_id, seq),
+ FOREIGN KEY(call_id) REFERENCES llm_calls_new(call_id) ON DELETE CASCADE
+);
+
+INSERT INTO llm_call_response_chunks_new(call_id, seq, received_offset_ms, data, byte_count)
+SELECT call_id, seq, received_offset_ms, data, byte_count FROM llm_call_response_chunks;
+
+DROP TABLE llm_call_requests;
+DROP TABLE llm_call_response_chunks;
+DROP TABLE llm_calls;
+DROP TABLE provider_models;
+DROP TABLE provider_endpoints;
+
+ALTER TABLE llm_calls_new RENAME TO llm_calls;
+ALTER TABLE llm_call_requests_new RENAME TO llm_call_requests;
+ALTER TABLE llm_call_response_chunks_new RENAME TO llm_call_response_chunks;
+
+CREATE INDEX model_configs_sort ON model_configs(sort_order, display_name);
+CREATE INDEX llm_calls_created ON llm_calls(created_at_ms DESC);
+CREATE INDEX llm_calls_run ON llm_calls(run_id, provider_call_index);
+CREATE INDEX llm_calls_model ON llm_calls(model_hash, created_at_ms DESC);
diff --git a/server/src/config.rs b/server/src/config.rs
index 0dc5199..ba82881 100644
--- a/server/src/config.rs
+++ b/server/src/config.rs
@@ -7,6 +7,8 @@ use crate::{Error, Result};
const DATA_DIR_NAME: &str = ".cursor-byok-v3";
const DATABASE_FILE_NAME: &str = "cursor-byok.db";
+const V0049_DATA_DIR_NAME: &str = ".cursor-local-assistant-v2";
+const V0049_CONFIG_FILE_NAME: &str = "config.yaml";
pub fn managed_data_dir() -> Result {
let home_dir = dirs::home_dir()
@@ -18,6 +20,14 @@ pub fn managed_data_dir() -> Result {
Ok(data_dir)
}
+pub fn v0049_config_path() -> Result {
+ let home_dir = dirs::home_dir()
+ .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?;
+ Ok(home_dir
+ .join(V0049_DATA_DIR_NAME)
+ .join(V0049_CONFIG_FILE_NAME))
+}
+
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProviderKind {
OpenAiChat,
diff --git a/server/src/control/cursor_models.rs b/server/src/control/cursor_models.rs
deleted file mode 100644
index 94fc809..0000000
--- a/server/src/control/cursor_models.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use axum::{extract::State, http::StatusCode, Json};
-use serde::{Deserialize, Serialize};
-
-use crate::{
- model::{ProviderEndpoint, ProviderEndpointInput, ProviderModel, ProviderModelInput},
- Result,
-};
-
-use super::{ControlService, DiscoveredModels};
-
-#[derive(Debug, Deserialize)]
-#[serde(tag = "kind", rename_all = "snake_case")]
-pub enum ProviderSelection {
- Existing { provider_id: i64 },
- New { input: ProviderEndpointInput },
-}
-
-#[derive(Debug, Deserialize)]
-pub struct CreateCursorModels {
- pub provider: ProviderSelection,
- pub models: Vec,
-}
-
-#[derive(Debug, Serialize)]
-pub struct CreatedCursorModels {
- pub provider: ProviderEndpoint,
- pub models: Vec,
-}
-
-#[derive(Debug, Deserialize)]
-pub struct DiscoverCursorModels {
- pub provider: ProviderSelection,
-}
-
-pub async fn create(
- State(service): State,
- Json(input): Json,
-) -> Result<(StatusCode, Json)> {
- let (provider, models) = match input.provider {
- ProviderSelection::Existing { provider_id } => {
- let provider = service
- .providers()
- .await?
- .into_iter()
- .find(|provider| provider.provider_id == provider_id)
- .ok_or_else(|| crate::Error::RunNotFound(format!("provider {provider_id}")))?;
- let models = service.save_models(provider_id, &input.models).await?;
- (provider, models)
- }
- ProviderSelection::New { input: provider } => {
- service
- .create_provider_with_models(&provider, &input.models)
- .await?
- }
- };
- Ok((
- StatusCode::CREATED,
- Json(CreatedCursorModels { provider, models }),
- ))
-}
-
-pub async fn discover(
- State(service): State,
- Json(input): Json,
-) -> Result> {
- match input.provider {
- ProviderSelection::Existing { provider_id } => {
- Ok(Json(service.discover_models(provider_id).await?))
- }
- ProviderSelection::New { input } => Ok(Json(service.discover_input(&input).await?)),
- }
-}
diff --git a/server/src/control/mod.rs b/server/src/control/mod.rs
index ca2a0e6..411e2c3 100644
--- a/server/src/control/mod.rs
+++ b/server/src/control/mod.rs
@@ -1,10 +1,8 @@
mod ads;
mod calls;
-mod cursor_models;
mod harness;
mod models;
mod overview;
-mod providers;
mod service;
mod settings;
@@ -22,8 +20,8 @@ use tower_http::{
use url::{Host, Url};
pub use service::{
- CallDetail, CallSummary, ControlService, DiscoveredModels, ModelConnectivityResult,
- ObservabilitySettings,
+ CallDetail, CallSummary, ControlService, DiscoveredModels, LegacyModelImportPreview,
+ LegacyModelImportResult, ModelConnectivityResult, ModelDiscoveryInput, ObservabilitySettings,
};
pub fn web_router(service: ControlService, assets: impl AsRef) -> Router {
@@ -117,22 +115,15 @@ pub fn api_router(service: ControlService) -> Router {
post(ads::dismiss),
)
.route(
- "/__byok-api__/api/providers",
- get(providers::list).post(providers::create),
+ "/__byok-api__/api/models",
+ get(models::list).post(models::create),
)
+ .route("/__byok-api__/api/models/discover", post(models::discover))
.route(
- "/__byok-api__/api/providers/{provider_id}",
- put(providers::update).delete(providers::remove),
+ "/__byok-api__/api/models/import-v0049",
+ get(models::preview_v0049).post(models::import_v0049),
)
- .route(
- "/__byok-api__/api/providers/{provider_id}/models/discover",
- post(models::discover),
- )
- .route(
- "/__byok-api__/api/providers/{provider_id}/models",
- post(models::save),
- )
- .route("/__byok-api__/api/models", get(models::list))
+ .route("/__byok-api__/api/models/order", put(models::reorder))
.route("/__byok-api__/api/overview", get(overview::get))
.route(
"/__byok-api__/api/models/{model_hash}",
@@ -180,14 +171,6 @@ pub fn api_router(service: ControlService) -> Router {
"/__byok-api__/api/harness/cursor/enabled",
put(harness::set_enabled),
)
- .route(
- "/__byok-api__/api/harness/cursor/models",
- post(cursor_models::create),
- )
- .route(
- "/__byok-api__/api/harness/cursor/models/discover",
- post(cursor_models::discover),
- )
.with_state(service)
.layer(desktop_cors())
}
@@ -265,7 +248,7 @@ mod tests {
.clone()
.oneshot(
Request::builder()
- .uri("/__byok-api__/api/providers")
+ .uri("/__byok-api__/api/models")
.header(header::ORIGIN, "tauri://localhost")
.body(Body::empty())
.unwrap(),
@@ -293,7 +276,7 @@ mod tests {
let response = router
.oneshot(
Request::builder()
- .uri("/api/providers")
+ .uri("/api/models")
.body(Body::empty())
.unwrap(),
)
diff --git a/server/src/control/models.rs b/server/src/control/models.rs
index 51a1228..003ea7f 100644
--- a/server/src/control/models.rs
+++ b/server/src/control/models.rs
@@ -6,32 +6,46 @@ use axum::{
use serde::Deserialize;
use crate::{
- model::{ProviderModel, ProviderModelInput},
+ model::{ModelConfig, ModelConfigInput},
Result,
};
-use super::{ControlService, DiscoveredModels, ModelConnectivityResult};
+use super::{
+ ControlService, DiscoveredModels, LegacyModelImportPreview, LegacyModelImportResult,
+ ModelConnectivityResult, ModelDiscoveryInput,
+};
#[derive(Deserialize)]
pub struct SaveModels {
- pub models: Vec,
+ pub models: Vec,
}
-pub async fn list(State(service): State) -> Result>> {
+#[derive(Deserialize)]
+pub struct ModelOrder {
+ pub model_hashes: Vec,
+}
+
+pub async fn list(State(service): State) -> Result>> {
Ok(Json(service.models().await?))
}
-pub async fn save(
+pub async fn create(
State(service): State,
- Path(provider_id): Path,
Json(input): Json,
-) -> Result<(StatusCode, Json>)> {
+) -> Result<(StatusCode, Json>)> {
Ok((
StatusCode::CREATED,
- Json(service.save_models(provider_id, &input.models).await?),
+ Json(service.create_models(&input.models).await?),
))
}
+pub async fn reorder(
+ State(service): State,
+ Json(input): Json,
+) -> Result>> {
+ Ok(Json(service.reorder_models(&input.model_hashes).await?))
+}
+
pub async fn remove(
State(service): State,
Path(model_hash): Path,
@@ -43,8 +57,8 @@ pub async fn remove(
pub async fn update(
State(service): State,
Path(model_hash): Path,
- Json(input): Json,
-) -> Result> {
+ Json(input): Json,
+) -> Result> {
Ok(Json(service.update_model(&model_hash, &input).await?))
}
@@ -57,7 +71,19 @@ pub async fn test(
pub async fn discover(
State(service): State,
- Path(provider_id): Path,
+ Json(input): Json,
) -> Result> {
- Ok(Json(service.discover_models(provider_id).await?))
+ Ok(Json(service.discover_models(&input).await?))
+}
+
+pub async fn import_v0049(
+ State(service): State,
+) -> Result> {
+ Ok(Json(service.import_v0049_models().await?))
+}
+
+pub async fn preview_v0049(
+ State(service): State,
+) -> Result> {
+ Ok(Json(service.preview_v0049_models().await?))
}
diff --git a/server/src/control/overview.rs b/server/src/control/overview.rs
index 4809ad8..0c9120c 100644
--- a/server/src/control/overview.rs
+++ b/server/src/control/overview.rs
@@ -15,7 +15,6 @@ pub struct OverviewRange {
start_ms: Option,
end_ms: Option,
model_hashes: Option,
- provider_ids: Option,
}
pub async fn get(
@@ -24,12 +23,7 @@ pub async fn get(
) -> Result> {
Ok(Json(
service
- .overview(
- range.start_ms,
- range.end_ms,
- range.model_hashes.as_deref(),
- range.provider_ids.as_deref(),
- )
+ .overview(range.start_ms, range.end_ms, range.model_hashes.as_deref())
.await?,
))
}
diff --git a/server/src/control/providers.rs b/server/src/control/providers.rs
deleted file mode 100644
index 0c67600..0000000
--- a/server/src/control/providers.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use axum::{
- extract::{Path, State},
- http::StatusCode,
- Json,
-};
-
-use crate::{
- model::{ProviderEndpoint, ProviderEndpointInput},
- Result,
-};
-
-use super::ControlService;
-
-pub async fn list(State(service): State) -> Result>> {
- Ok(Json(service.providers().await?))
-}
-
-pub async fn create(
- State(service): State,
- Json(input): Json,
-) -> Result<(StatusCode, Json)> {
- Ok((
- StatusCode::CREATED,
- Json(service.create_provider(&input).await?),
- ))
-}
-
-pub async fn update(
- State(service): State,
- Path(provider_id): Path,
- Json(input): Json,
-) -> Result> {
- Ok(Json(service.update_provider(provider_id, &input).await?))
-}
-
-pub async fn remove(
- State(service): State,
- Path(provider_id): Path,
-) -> Result {
- service.delete_provider(provider_id).await?;
- Ok(StatusCode::NO_CONTENT)
-}
diff --git a/server/src/control/service.rs b/server/src/control/service.rs
index 4f95d7f..20dd892 100644
--- a/server/src/control/service.rs
+++ b/server/src/control/service.rs
@@ -16,9 +16,9 @@ use crate::{
harness::CursorHarness,
model::{
ContentPart, CursorRunTraceArtifact, CursorRunTraceSummary, LlmCallRequest,
- LlmCallResponseChunk, LlmCallSummary, ModelInvocation, ModelRequest, ModelSpec, Overview,
- ProjectedContent, ProjectedMessage, PromptSpec, ProviderEndpoint, ProviderEndpointInput,
- ProviderModel, ProviderModelInput, ProviderType, Role,
+ LlmCallResponseChunk, LlmCallSummary, ModelConfig, ModelConfigInput, ModelInvocation,
+ ModelRequest, ModelSpec, ModelType, Overview, ProjectedContent, ProjectedMessage,
+ PromptSpec, ProviderType, Role,
},
provider::{ModelEvent, Provider},
store::{
@@ -40,6 +40,53 @@ pub struct DiscoveredModels {
pub models: Vec,
}
+#[derive(Clone, Debug, Serialize)]
+pub struct LegacyModelImportResult {
+ pub imported: usize,
+ pub skipped: usize,
+ pub total: usize,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct LegacyModelImportPreview {
+ pub source: String,
+ pub total: usize,
+ pub new_models: usize,
+ pub existing_models: usize,
+ pub models: Vec,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct LegacyModelImportPreviewItem {
+ pub model_hash: String,
+ pub display_name: String,
+ pub model_id: String,
+ #[serde(rename = "type")]
+ pub model_type: ModelType,
+ pub existing: bool,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct ModelDiscoveryInput {
+ #[serde(rename = "type")]
+ pub model_type: ModelType,
+ pub base_url: String,
+ pub api_key: String,
+ #[serde(default)]
+ pub custom_headers_enabled: bool,
+ #[serde(default = "empty_json_object")]
+ pub custom_headers: serde_json::Value,
+}
+
+fn empty_json_object() -> serde_json::Value {
+ serde_json::json!({})
+}
+
+fn empty_json_object_ref() -> &'static serde_json::Value {
+ static EMPTY: std::sync::OnceLock = std::sync::OnceLock::new();
+ EMPTY.get_or_init(empty_json_object)
+}
+
#[derive(Clone, Debug, Serialize)]
pub struct ModelConnectivityResult {
pub duration_ms: u64,
@@ -163,28 +210,8 @@ impl ControlService {
Ok(())
}
- pub async fn providers(&self) -> Result> {
- self.store.providers().await
- }
-
- pub async fn create_provider(&self, input: &ProviderEndpointInput) -> Result {
- self.store.create_provider(input).await
- }
-
- pub async fn update_provider(
- &self,
- provider_id: i64,
- input: &ProviderEndpointInput,
- ) -> Result {
- self.store.update_provider(provider_id, input).await
- }
-
- pub async fn delete_provider(&self, provider_id: i64) -> Result<()> {
- self.store.delete_provider(provider_id).await
- }
-
- pub async fn models(&self) -> Result> {
- self.store.provider_models(false).await
+ pub async fn models(&self) -> Result> {
+ self.store.models().await
}
pub async fn overview(
@@ -192,31 +219,28 @@ impl ControlService {
start_ms: Option,
end_ms: Option,
model_hashes: Option<&str>,
- provider_ids: Option<&str>,
) -> Result {
- self.store
- .overview(start_ms, end_ms, model_hashes, provider_ids)
- .await
+ self.store.overview(start_ms, end_ms, model_hashes).await
}
- pub async fn save_models(
- &self,
- provider_id: i64,
- models: &[ProviderModelInput],
- ) -> Result> {
- self.store.save_provider_models(provider_id, models).await
+ pub async fn create_models(&self, models: &[ModelConfigInput]) -> Result> {
+ self.store.create_models(models).await
+ }
+
+ pub async fn reorder_models(&self, model_hashes: &[String]) -> Result> {
+ self.store.reorder_models(model_hashes).await
}
pub async fn delete_model(&self, model_hash: &str) -> Result<()> {
- self.store.delete_provider_model(model_hash).await
+ self.store.delete_model(model_hash).await
}
pub async fn update_model(
&self,
model_hash: &str,
- input: &ProviderModelInput,
- ) -> Result {
- self.store.update_provider_model(model_hash, input).await
+ input: &ModelConfigInput,
+ ) -> Result {
+ self.store.update_model(model_hash, input).await
}
pub async fn test_model(&self, model_hash: &str) -> Result {
@@ -225,19 +249,12 @@ impl ControlService {
let configured = self
.store
- .provider_model(model_hash)
+ .model(model_hash)
.await?
.ok_or_else(|| Error::RunNotFound(format!("model {model_hash}")))?;
let mut model = ModelSpec::new(model_hash);
- if configured.reasoning_enabled {
- model.reasoning.enabled = true;
- model.reasoning.effort = Some(
- configured
- .reasoning_effort
- .filter(|effort| !effort.trim().is_empty())
- .unwrap_or_else(|| "medium".into()),
- );
- }
+ configured.configure(&mut model);
+ model.max_output_tokens = Some(configured.max_output_tokens().unwrap_or(65_536));
let test_id = format!("model-test-{}", uuid::Uuid::new_v4());
let call_id = test_id.clone();
let invocation = ModelInvocation {
@@ -317,6 +334,11 @@ impl ControlService {
}
let elapsed = started.elapsed();
let output = output.trim().to_string();
+ if first_text_at.is_none() {
+ return Err(Error::Provider(
+ "model connectivity test received no text output".into(),
+ ));
+ }
let tokens_estimated = output_tokens.is_none();
let output_tokens = output_tokens.unwrap_or_else(|| estimate_output_tokens(&output));
Ok(ModelConnectivityResult {
@@ -338,44 +360,58 @@ impl ControlService {
})
}
- pub async fn create_provider_with_models(
- &self,
- provider: &ProviderEndpointInput,
- models: &[ProviderModelInput],
- ) -> Result<(ProviderEndpoint, Vec)> {
- self.store
- .create_provider_with_models(provider, models)
- .await
- }
-
- pub async fn discover_input(&self, input: &ProviderEndpointInput) -> Result {
+ pub async fn discover_models(&self, input: &ModelDiscoveryInput) -> Result {
let client = crate::network::client(&self.store).await?;
- let base_url = crate::model::normalize_base_url(&input.base_url)?;
- discover_provider_models(
+ let base_url = crate::model::normalize_request_url(&input.base_url)?;
+ discover_models_from_endpoint(
&client,
- input.provider_type,
+ match input.model_type {
+ ModelType::OpenAi => ProviderType::OpenAiResponses,
+ ModelType::Anthropic => ProviderType::Anthropic,
+ },
&base_url,
- input.api_key.as_deref().unwrap_or_default(),
- &input.custom_headers,
+ &input.api_key,
+ if input.custom_headers_enabled {
+ &input.custom_headers
+ } else {
+ empty_json_object_ref()
+ },
)
.await
}
- pub async fn discover_models(&self, provider_id: i64) -> Result {
- let client = crate::network::client(&self.store).await?;
- let provider = self
- .store
- .provider(provider_id)
- .await?
- .ok_or_else(|| Error::RunNotFound(format!("provider {provider_id}")))?;
- discover_provider_models(
- &client,
- provider.endpoint.provider_type,
- &provider.endpoint.base_url,
- provider.endpoint.api_key.as_deref().unwrap_or_default(),
- &provider.custom_headers,
- )
- .await
+ pub async fn import_v0049_models(&self) -> Result {
+ let path = crate::config::v0049_config_path()?;
+ let outcome = self.store.import_v0049_model_config(&path).await?;
+ Ok(LegacyModelImportResult {
+ imported: outcome.imported,
+ skipped: outcome.skipped,
+ total: outcome.total,
+ })
+ }
+
+ pub async fn preview_v0049_models(&self) -> Result {
+ let path = crate::config::v0049_config_path()?;
+ let plan = self.store.preview_v0049_model_config(&path).await?;
+ let total = plan.models.len();
+ let existing_models = plan.models.iter().filter(|model| model.existing).count();
+ Ok(LegacyModelImportPreview {
+ source: path.display().to_string(),
+ total,
+ new_models: total - existing_models,
+ existing_models,
+ models: plan
+ .models
+ .into_iter()
+ .map(|model| LegacyModelImportPreviewItem {
+ model_hash: model.model_hash,
+ display_name: model.input.display_name,
+ model_id: model.input.model_id,
+ model_type: model.input.model_type,
+ existing: model.existing,
+ })
+ .collect(),
+ })
}
pub async fn calls(&self, limit: i64) -> Result> {
@@ -595,7 +631,7 @@ fn readable_utf8(data: &[u8]) -> Option<&str> {
.then_some(value)
}
-async fn discover_provider_models(
+async fn discover_models_from_endpoint(
client: &reqwest::Client,
provider_type: ProviderType,
base_url: &str,
@@ -617,10 +653,10 @@ async fn discover_provider_models(
fn model_discovery_url(base_url: &str) -> Result {
let mut url = Url::parse(base_url)
- .map_err(|error| Error::Config(format!("invalid provider base URL: {error}")))?;
+ .map_err(|error| Error::Config(format!("invalid model request URL: {error}")))?;
if url.host_str().is_none() {
return Err(Error::Config(
- "provider base URL must contain a host".into(),
+ "model request URL must contain a host".into(),
));
}
url.set_path("/v1/models");
@@ -756,10 +792,7 @@ mod tests {
use tokio_util::sync::CancellationToken;
use crate::{
- model::{
- ModelInvocation, ProjectedContent, ProviderEndpointInput, ProviderModelInput,
- ProviderType,
- },
+ model::{ModelConfigInput, ModelInvocation, ModelType, ProjectedContent},
provider::{FinishReason, ModelEvent, Provider, ProviderStream},
store::Store,
};
@@ -803,34 +836,30 @@ mod tests {
.await
.unwrap();
let invocation = Arc::new(Mutex::new(None));
- let provider = store
- .create_provider(&ProviderEndpointInput {
- name: "Test".into(),
- provider_type: ProviderType::OpenAiResponses,
- base_url: "https://example.com/v1".into(),
- api_key: None,
- custom_headers: serde_json::json!({}),
- extra_params: serde_json::json!({}),
- })
- .await
- .unwrap();
let model = store
- .save_provider_model(
- provider.provider_id,
- &ProviderModelInput {
- model_id: "reasoning-model".into(),
- display_name: "Reasoning Model".into(),
- endpoint_type: ProviderType::OpenAiResponses,
- request_url: String::new(),
- enabled: true,
- sort_order: 0,
- context_window_tokens: None,
- max_output_tokens: None,
- reasoning_enabled: true,
- reasoning_effort: None,
- supports_image_generation: false,
- },
- )
+ .create_model(&ModelConfigInput {
+ model_id: "reasoning-model".into(),
+ display_name: "Reasoning Model".into(),
+ model_type: ModelType::OpenAi,
+ base_url: "https://example.com/v1/responses".into(),
+ use_full_url: true,
+ api_key: "secret".into(),
+ tooltip_data: "Reasoning Model".into(),
+ sort_order: 0,
+ reasoning_effort: Some("medium".into()),
+ openai_endpoint: "/v1/responses".into(),
+ openai_extra_params_enabled: false,
+ openai_extra_params: serde_json::json!({}),
+ custom_headers_enabled: false,
+ custom_headers: serde_json::json!({}),
+ anthropic_extra_params_enabled: false,
+ anthropic_extra_params: serde_json::json!({}),
+ context_window_tokens: None,
+ max_completion_tokens: None,
+ anthropic_max_tokens: None,
+ anthropic_thinking_effort: None,
+ thinking_budget_tokens: None,
+ })
.await
.unwrap();
let service = ControlService::new(
@@ -925,16 +954,15 @@ mod tests {
)
.unwrap();
let result = service
- .discover_input(&ProviderEndpointInput {
- name: "Test".into(),
- provider_type: ProviderType::OpenAiResponses,
+ .discover_models(&super::ModelDiscoveryInput {
+ model_type: ModelType::OpenAi,
base_url: format!("http://{address}/custom/responses"),
- api_key: Some("secret".into()),
+ api_key: "secret".into(),
+ custom_headers_enabled: true,
custom_headers: serde_json::json!({
"uSeR-aGeNt": "inherited-user-agent",
"x-tenant": "tenant-a"
}),
- extra_params: serde_json::json!({ "temperature": 0.7 }),
})
.await
.unwrap();
diff --git a/server/src/cursor/handlers.rs b/server/src/cursor/handlers.rs
index 5fe9d5e..ef3b72b 100644
--- a/server/src/cursor/handlers.rs
+++ b/server/src/cursor/handlers.rs
@@ -128,7 +128,7 @@ async fn bidi_append_handler(
let conversation_id = decoded.conversation_id().map(str::to_owned);
let trace_metadata = decoded.trace_metadata();
let local = if let Some(model_id) = decoded.model_id() {
- if registry.store().provider_model(model_id).await?.is_some() {
+ if registry.store().model(model_id).await?.is_some() {
tracing::info!(
request_id = decoded.request_id,
model_id,
diff --git a/server/src/cursor/model_catalog.rs b/server/src/cursor/model_catalog.rs
index aefe280..73d7b5c 100644
--- a/server/src/cursor/model_catalog.rs
+++ b/server/src/cursor/model_catalog.rs
@@ -1,5 +1,3 @@
-use std::collections::HashMap;
-
use axum::{
body::{Body, Bytes},
extract::{Extension, State},
@@ -14,7 +12,7 @@ use crate::{
proxy::{self, CursorProxy},
CursorSessionRegistry,
},
- model::{format_token_count, parse_token_count, ProviderModel},
+ model::{format_token_count, parse_token_count, ModelConfig, ModelType},
Error, Result,
};
@@ -205,7 +203,7 @@ const EFFORTS: [(&str, &str); 5] = [
];
const DEFAULT_CONTEXT: &str = "200k";
-fn context_options(model: &ProviderModel) -> Vec<(String, String)> {
+fn context_options(model: &ModelConfig) -> Vec<(String, String)> {
let mut contexts = CONTEXTS
.into_iter()
.map(|(value, display_name)| (value.to_owned(), display_name.to_owned()))
@@ -227,30 +225,12 @@ pub async fn available_models(
Extension(proxy): Extension,
request: Request,
) -> Result> {
- let models = registry.store().provider_models(true).await?;
- let provider_names = registry
- .store()
- .providers()
- .await?
- .into_iter()
- .map(|provider| (provider.provider_id, provider.name))
- .collect::>();
+ let models = registry.store().models().await?;
tracing::info!(
model_count = models.len(),
"appending BYOK models to Cursor AvailableModels"
);
- let available_models = models
- .iter()
- .map(|model| {
- let provider_name = provider_names.get(&model.provider_id).ok_or_else(|| {
- Error::Config(format!(
- "provider {} for model {} does not exist",
- model.provider_id, model.model_hash
- ))
- })?;
- Ok(available_model(model, provider_name))
- })
- .collect::>>()?;
+ let available_models = models.iter().map(available_model).collect::>();
let local = AvailableModelsAddition {
model_names: models
.iter()
@@ -273,7 +253,7 @@ pub async fn usable_models(
Extension(proxy): Extension,
request: Request,
) -> Result> {
- let models = registry.store().provider_models(true).await?;
+ let models = registry.store().models().await?;
tracing::info!(
model_count = models.len(),
"appending BYOK models to Cursor GetUsableModels"
@@ -340,14 +320,14 @@ fn unary_payload(body: &Bytes) -> Result<(bool, &[u8])> {
Ok((true, &body[5..]))
}
-fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel {
+fn available_model(model: &ModelConfig) -> AvailableModel {
let contexts = context_options(model);
let variants = model_variants(model, &contexts);
let legacy_slugs = variants
.iter()
.filter_map(|variant| variant.legacy_slug.clone())
.collect();
- let tooltip = model_tooltip(model, "200K", "high", false);
+ let tooltip = model_tooltip(model);
AvailableModel {
name: model.model_hash.clone(),
default_on: true,
@@ -376,7 +356,10 @@ fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel
display_name: "Cursor".into(),
}),
model_picker_badges: vec![ModelPickerBadge {
- label: provider_name.into(),
+ label: match model.model_type {
+ ModelType::OpenAi => "OpenAI".into(),
+ ModelType::Anthropic => "Anthropic".into(),
+ },
variant: 1,
dismiss_on_selection: false,
}],
@@ -447,7 +430,7 @@ fn model_parameters(contexts: &[(String, String)]) -> Vec Vec {
+fn model_variants(model: &ModelConfig, contexts: &[(String, String)]) -> Vec {
let mut variants = Vec::with_capacity(contexts.len() * EFFORTS.len() * 2);
for (context, context_name) in contexts {
for (effort, effort_name) in EFFORTS {
@@ -467,7 +450,7 @@ fn model_variants(model: &ProviderModel, contexts: &[(String, String)]) -> Vec TooltipData {
- let fast_label = if fast { " (Fast)" } else { "" };
+fn model_tooltip(model: &ModelConfig) -> TooltipData {
TooltipData {
- markdown_content: Some(format!(
- "**{}{fast_label}**
{context_name} context window
*Version: {effort} effort*",
- model.display_name
- )),
+ markdown_content: Some(model.tooltip_data.clone()),
}
}
-fn usable_model(model: &ProviderModel) -> agent::ModelDetails {
+fn usable_model(model: &ModelConfig) -> agent::ModelDetails {
agent::ModelDetails {
model_id: model.model_hash.clone(),
display_model_id: model.model_hash.clone(),
@@ -555,25 +529,34 @@ mod tests {
#[test]
fn maps_byok_model_to_cursor_catalog_fields() {
- let model = ProviderModel {
+ let model = ModelConfig {
model_hash: "33ceed20".into(),
- provider_id: 1,
- model_id: "deepseek-v4-flash".into(),
- display_name: "DeepSeek V4 Flash".into(),
- endpoint_type: crate::model::ProviderType::OpenAiResponses,
- request_url: String::new(),
- enabled: true,
sort_order: 0,
- context_window_tokens: Some(272_000),
- max_output_tokens: None,
- reasoning_enabled: false,
+ display_name: "DeepSeek V4 Flash".into(),
+ model_type: ModelType::OpenAi,
+ base_url: "https://example.com/v1/responses".into(),
+ use_full_url: true,
+ api_key: "secret".into(),
+ tooltip_data: "DeepSeek V4 Flash".into(),
+ model_id: "deepseek-v4-flash".into(),
reasoning_effort: None,
- supports_image_generation: false,
+ openai_endpoint: "/v1/responses".into(),
+ openai_extra_params_enabled: false,
+ openai_extra_params: serde_json::json!({}),
+ custom_headers_enabled: false,
+ custom_headers: serde_json::json!({}),
+ anthropic_extra_params_enabled: false,
+ anthropic_extra_params: serde_json::json!({}),
+ context_window_tokens: Some(272_000),
+ max_completion_tokens: None,
+ anthropic_max_tokens: None,
+ anthropic_thinking_effort: None,
+ thinking_budget_tokens: None,
created_at_ms: 0,
updated_at_ms: 0,
};
- let mapped = available_model(&model, "OpenRouter");
+ let mapped = available_model(&model);
assert_eq!(mapped.name, "33ceed20");
assert!(mapped.default_on);
assert_eq!(mapped.supports_agent, Some(true));
@@ -591,6 +574,13 @@ mod tests {
);
assert_eq!(mapped.server_model_name.as_deref(), Some("33ceed20"));
assert_eq!(mapped.named_model_section_index, Some(1));
+ assert_eq!(
+ mapped
+ .tooltip_data
+ .as_ref()
+ .and_then(|tooltip| tooltip.markdown_content.as_deref()),
+ Some("DeepSeek V4 Flash")
+ );
assert_eq!(mapped.vendor_name.as_deref(), Some("cursor"));
assert_eq!(mapped.parameter_definitions.len(), 3);
let context = mapped
@@ -643,7 +633,7 @@ mod tests {
assert_eq!(mapped.variants.len(), 50);
assert_eq!(mapped.legacy_slugs.len(), 50);
assert_eq!(mapped.model_picker_badges.len(), 1);
- assert_eq!(mapped.model_picker_badges[0].label, "OpenRouter");
+ assert_eq!(mapped.model_picker_badges[0].label, "OpenAI");
assert!(!mapped.model_picker_badges[0].dismiss_on_selection);
let default = mapped
.variants
diff --git a/server/src/cursor/request/prepare.rs b/server/src/cursor/request/prepare.rs
index c2d18bf..00a27da 100644
--- a/server/src/cursor/request/prepare.rs
+++ b/server/src/cursor/request/prepare.rs
@@ -135,12 +135,8 @@ pub(crate) async fn prepare(
mode_from_proto(mode_number)?
};
let mut model = model::requested_model(request)?;
- if let Some(provider_model) = store
- .provider_model(&model.model_id)
- .await?
- .filter(|model| model.enabled)
- {
- provider_model.configure(&mut model);
+ if let Some(configured_model) = store.model(&model.model_id).await? {
+ configured_model.configure(&mut model);
}
let dynamic = context::dynamic_mcp(request, &request_context)?;
let subagent_model_overrides = model::overrides(request)?;
diff --git a/server/src/harness/mod.rs b/server/src/harness/mod.rs
index f618ab4..913d7ea 100644
--- a/server/src/harness/mod.rs
+++ b/server/src/harness/mod.rs
@@ -94,9 +94,9 @@ impl CursorHarness {
}
pub async fn status(&self) -> Result {
- let models = self.inner.store.provider_models(false).await?;
+ let models = self.inner.store.models().await?;
let configured_models = models.len();
- let enabled_models = models.iter().filter(|model| model.enabled).count();
+ let enabled_models = configured_models;
let ca = self.inner.ca.state()?;
if integration_prerequisites_ready(&ca, self.inner.backend_addr.read().is_some()) {
self.enable().await?;
diff --git a/server/src/model/configuration.rs b/server/src/model/configuration.rs
new file mode 100644
index 0000000..dd81796
--- /dev/null
+++ b/server/src/model/configuration.rs
@@ -0,0 +1,508 @@
+use std::{fmt, str::FromStr};
+
+use reqwest::Url;
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+use crate::{Error, Result};
+
+pub const OPENAI_RESPONSES_ENDPOINT: &str = "/v1/responses";
+pub const OPENAI_CHAT_ENDPOINT: &str = "/v1/chat/completions";
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
+pub enum ProviderType {
+ #[serde(rename = "openai-chat")]
+ OpenAiChat,
+ #[serde(rename = "openai-responses")]
+ OpenAiResponses,
+ #[serde(rename = "anthropic")]
+ Anthropic,
+}
+
+impl ProviderType {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::OpenAiChat => "openai-chat",
+ Self::OpenAiResponses => "openai-responses",
+ Self::Anthropic => "anthropic",
+ }
+ }
+}
+
+impl fmt::Display for ProviderType {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.as_str())
+ }
+}
+
+impl FromStr for ProviderType {
+ type Err = Error;
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "openai-chat" => Ok(Self::OpenAiChat),
+ "openai-responses" => Ok(Self::OpenAiResponses),
+ "anthropic" => Ok(Self::Anthropic),
+ _ => Err(Error::Config(format!("unsupported provider type: {value}"))),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ModelType {
+ OpenAi,
+ Anthropic,
+}
+
+impl ModelType {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::OpenAi => "openai",
+ Self::Anthropic => "anthropic",
+ }
+ }
+}
+
+impl FromStr for ModelType {
+ type Err = Error;
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "openai" => Ok(Self::OpenAi),
+ "anthropic" => Ok(Self::Anthropic),
+ _ => Err(Error::Config(format!("unsupported model type: {value}"))),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ModelConfigInput {
+ #[serde(default)]
+ pub sort_order: i64,
+ pub display_name: String,
+ #[serde(rename = "type")]
+ pub model_type: ModelType,
+ pub base_url: String,
+ #[serde(default)]
+ pub use_full_url: bool,
+ pub api_key: String,
+ pub tooltip_data: String,
+ pub model_id: String,
+ #[serde(default)]
+ pub reasoning_effort: Option,
+ #[serde(default)]
+ pub openai_endpoint: String,
+ #[serde(default)]
+ pub openai_extra_params_enabled: bool,
+ #[serde(default = "empty_object")]
+ pub openai_extra_params: serde_json::Value,
+ #[serde(default)]
+ pub custom_headers_enabled: bool,
+ #[serde(default = "empty_object")]
+ pub custom_headers: serde_json::Value,
+ #[serde(default)]
+ pub anthropic_extra_params_enabled: bool,
+ #[serde(default = "empty_object")]
+ pub anthropic_extra_params: serde_json::Value,
+ pub context_window_tokens: Option,
+ pub max_completion_tokens: Option,
+ pub anthropic_max_tokens: Option,
+ #[serde(default)]
+ pub anthropic_thinking_effort: Option,
+ pub thinking_budget_tokens: Option,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ModelConfig {
+ pub model_hash: String,
+ pub sort_order: i64,
+ pub display_name: String,
+ #[serde(rename = "type")]
+ pub model_type: ModelType,
+ pub base_url: String,
+ pub use_full_url: bool,
+ pub api_key: String,
+ pub tooltip_data: String,
+ pub model_id: String,
+ pub reasoning_effort: Option,
+ pub openai_endpoint: String,
+ pub openai_extra_params_enabled: bool,
+ pub openai_extra_params: serde_json::Value,
+ pub custom_headers_enabled: bool,
+ pub custom_headers: serde_json::Value,
+ pub anthropic_extra_params_enabled: bool,
+ pub anthropic_extra_params: serde_json::Value,
+ pub context_window_tokens: Option,
+ pub max_completion_tokens: Option,
+ pub anthropic_max_tokens: Option,
+ pub anthropic_thinking_effort: Option,
+ pub thinking_budget_tokens: Option,
+ pub created_at_ms: i64,
+ pub updated_at_ms: i64,
+}
+
+impl ModelConfig {
+ pub fn provider_type(&self) -> ProviderType {
+ match self.model_type {
+ ModelType::Anthropic => ProviderType::Anthropic,
+ ModelType::OpenAi if self.openai_endpoint == OPENAI_RESPONSES_ENDPOINT => {
+ ProviderType::OpenAiResponses
+ }
+ ModelType::OpenAi => ProviderType::OpenAiChat,
+ }
+ }
+
+ pub fn request_url(&self) -> Result {
+ resolve_request_url(
+ self.model_type,
+ &self.base_url,
+ &self.openai_endpoint,
+ self.use_full_url,
+ )
+ }
+
+ pub fn max_output_tokens(&self) -> Option {
+ match self.model_type {
+ ModelType::OpenAi => self.max_completion_tokens,
+ ModelType::Anthropic => self.anthropic_max_tokens.or(self.max_completion_tokens),
+ }
+ }
+
+ pub fn extra_params(&self) -> &serde_json::Value {
+ match self.model_type {
+ ModelType::OpenAi if self.openai_extra_params_enabled => &self.openai_extra_params,
+ ModelType::Anthropic if self.anthropic_extra_params_enabled => {
+ &self.anthropic_extra_params
+ }
+ _ => empty_object_ref(),
+ }
+ }
+
+ pub fn configure(&self, model: &mut super::ModelSpec) {
+ model.display_name = Some(self.display_name.clone());
+ if model.reasoning.effort.is_none() {
+ model.reasoning.effort = match self.model_type {
+ ModelType::OpenAi => self.reasoning_effort.clone(),
+ ModelType::Anthropic => self.anthropic_thinking_effort.clone(),
+ };
+ }
+ model.reasoning.enabled |= model.reasoning.effort.is_some();
+ }
+}
+
+pub fn normalize_model_input(input: &ModelConfigInput) -> Result {
+ let display_name = required(&input.display_name, "model display name")?;
+ let base_url = normalize_request_url(&input.base_url)?;
+ let api_key = required(&input.api_key, "model API key")?;
+ let tooltip_data = required(&input.tooltip_data, "model tooltip")?;
+ let model_id = required(&input.model_id, "model id")?;
+ let reasoning_effort = normalize_effort(input.reasoning_effort.as_deref(), true)?;
+ let anthropic_thinking_effort = match input.model_type {
+ ModelType::Anthropic => Some(
+ normalize_effort(
+ input.anthropic_thinking_effort.as_deref().or(Some("xhigh")),
+ false,
+ )?
+ .expect("Anthropic effort has a default"),
+ ),
+ ModelType::OpenAi => None,
+ };
+ let openai_endpoint = match input.model_type {
+ ModelType::OpenAi => normalize_openai_endpoint(&input.openai_endpoint)?,
+ ModelType::Anthropic => String::new(),
+ };
+ validate_object(&input.openai_extra_params, "OpenAI extra params")?;
+ validate_object(&input.anthropic_extra_params, "Anthropic extra params")?;
+ validate_headers(&input.custom_headers)?;
+
+ let normalized = ModelConfigInput {
+ sort_order: input.sort_order.max(0),
+ display_name,
+ model_type: input.model_type,
+ base_url,
+ use_full_url: input.use_full_url,
+ api_key,
+ tooltip_data,
+ model_id,
+ reasoning_effort: (input.model_type == ModelType::OpenAi)
+ .then_some(reasoning_effort)
+ .flatten(),
+ openai_endpoint,
+ openai_extra_params_enabled: input.model_type == ModelType::OpenAi
+ && input.openai_extra_params_enabled,
+ openai_extra_params: if input.model_type == ModelType::OpenAi {
+ input.openai_extra_params.clone()
+ } else {
+ empty_object()
+ },
+ custom_headers_enabled: input.custom_headers_enabled,
+ custom_headers: input.custom_headers.clone(),
+ anthropic_extra_params_enabled: input.model_type == ModelType::Anthropic
+ && input.anthropic_extra_params_enabled,
+ anthropic_extra_params: if input.model_type == ModelType::Anthropic {
+ input.anthropic_extra_params.clone()
+ } else {
+ empty_object()
+ },
+ context_window_tokens: positive(input.context_window_tokens, "context window")?,
+ max_completion_tokens: positive(input.max_completion_tokens, "max completion tokens")?,
+ anthropic_max_tokens: positive(input.anthropic_max_tokens, "Anthropic max tokens")?,
+ anthropic_thinking_effort,
+ thinking_budget_tokens: positive(input.thinking_budget_tokens, "thinking budget")?,
+ };
+ resolve_request_url(
+ normalized.model_type,
+ &normalized.base_url,
+ &normalized.openai_endpoint,
+ normalized.use_full_url,
+ )?;
+ Ok(normalized)
+}
+
+pub fn model_hash(input: &ModelConfigInput) -> Result {
+ let normalized = normalize_model_input(input)?;
+ let request_url = resolve_request_url(
+ normalized.model_type,
+ &normalized.base_url,
+ &normalized.openai_endpoint,
+ normalized.use_full_url,
+ )?;
+ let mut parts = vec![
+ request_url,
+ normalized.model_id,
+ normalized.api_key,
+ normalized.display_name,
+ ];
+ if normalized.model_type == ModelType::OpenAi {
+ parts.push(normalized.openai_endpoint);
+ }
+ let digest = Sha256::digest(parts.join("\n").as_bytes());
+ Ok(hex::encode(&digest[..8]))
+}
+
+pub fn normalize_request_url(value: &str) -> Result {
+ let value = value.trim();
+ let url = Url::parse(value)
+ .map_err(|error| Error::Config(format!("invalid model request URL: {error}")))?;
+ if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
+ return Err(Error::Config(
+ "model request URL must be an HTTP(S) URL with a host".into(),
+ ));
+ }
+ if url.fragment().is_some() {
+ return Err(Error::Config(
+ "model request URL cannot contain a fragment".into(),
+ ));
+ }
+ Ok(value.into())
+}
+
+pub fn resolve_request_url(
+ model_type: ModelType,
+ base_url: &str,
+ openai_endpoint: &str,
+ use_full_url: bool,
+) -> Result {
+ let base_url = normalize_request_url(base_url)?;
+ let endpoint = match model_type {
+ ModelType::OpenAi => normalize_openai_endpoint(openai_endpoint)?,
+ ModelType::Anthropic => "/v1/messages".into(),
+ };
+ if use_full_url {
+ return Ok(base_url);
+ }
+ append_standard_endpoint(&base_url, &endpoint)
+}
+
+fn append_standard_endpoint(base_url: &str, endpoint: &str) -> Result {
+ let mut url = Url::parse(base_url)
+ .map_err(|error| Error::Config(format!("invalid model server URL: {error}")))?;
+ let base_path = url.path().trim_end_matches('/').to_string();
+ let endpoint = if has_trailing_version(&base_path) {
+ endpoint.strip_prefix("/v1").unwrap_or(endpoint)
+ } else {
+ endpoint
+ };
+ url.set_path(&format!("{base_path}{endpoint}"));
+ normalize_request_url(url.as_str())
+}
+
+fn has_trailing_version(path: &str) -> bool {
+ let Some(segment) = path.rsplit('/').next() else {
+ return false;
+ };
+ segment.strip_prefix('v').is_some_and(|digits| {
+ !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
+ })
+}
+
+pub fn is_sensitive_header(name: &str) -> bool {
+ matches!(
+ name.to_ascii_lowercase().as_str(),
+ "authorization" | "proxy-authorization" | "x-api-key" | "api-key" | "cookie" | "set-cookie"
+ )
+}
+
+fn normalize_openai_endpoint(value: &str) -> Result {
+ match value.trim() {
+ "" | OPENAI_RESPONSES_ENDPOINT => Ok(OPENAI_RESPONSES_ENDPOINT.into()),
+ OPENAI_CHAT_ENDPOINT => Ok(OPENAI_CHAT_ENDPOINT.into()),
+ value => Err(Error::Config(format!(
+ "unsupported OpenAI endpoint: {value}"
+ ))),
+ }
+}
+
+fn normalize_effort(value: Option<&str>, allow_empty: bool) -> Result