{editing
+ {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" && <>
diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json
index e44b9b8..32969c8 100644
--- a/apps/desktop/src/i18n/generated/catalog.json
+++ b/apps/desktop/src/i18n/generated/catalog.json
@@ -38,7 +38,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
+ "line": 95,
"column": 39
}
]
@@ -190,7 +190,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 256,
+ "line": 257,
"column": 89
}
]
@@ -243,7 +243,7 @@
{
"file": "components/cursor/CursorModelEditor.tsx",
"line": 81,
- "column": 119
+ "column": 90
}
]
},
@@ -390,7 +390,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 255,
+ "line": 256,
"column": 36
},
{
@@ -511,12 +511,12 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 89,
+ "line": 92,
"column": 56
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 89,
+ "line": 92,
"column": 141
},
{
@@ -731,12 +731,12 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 88,
+ "line": 91,
"column": 56
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 88,
+ "line": 91,
"column": 123
},
{
@@ -912,7 +912,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 85,
+ "line": 88,
"column": 58
}
]
@@ -1596,7 +1596,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 88,
+ "line": 91,
"column": 78
},
{
@@ -1632,6 +1632,18 @@
}
]
},
+ "5cca0b7972a11f3a": {
+ "source": "自定义上下文必须是大于 0 的整数",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "pages/CursorSettingsPage.tsx",
+ "line": 238,
+ "column": 171
+ }
+ ]
+ },
"5d59857bf039cac9": {
"source": "Cursor 助手 v{version}",
"kind": "template",
@@ -1777,7 +1789,7 @@
"refs": [
{
"file": "api.ts",
- "line": 289,
+ "line": 290,
"column": 43
}
]
@@ -2070,7 +2082,7 @@
"refs": [
{
"file": "api.ts",
- "line": 284,
+ "line": 285,
"column": 43
}
]
@@ -2132,12 +2144,12 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
+ "line": 95,
"column": 73
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 92,
+ "line": 95,
"column": 122
}
]
@@ -2209,7 +2221,7 @@
"refs": [
{
"file": "api.ts",
- "line": 230,
+ "line": 231,
"column": 21
}
]
@@ -2255,7 +2267,7 @@
"refs": [
{
"file": "pages/CursorSettingsPage.tsx",
- "line": 250,
+ "line": 251,
"column": 21
}
]
@@ -2318,7 +2330,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 89,
+ "line": 92,
"column": 85
}
]
@@ -2443,7 +2455,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 86,
+ "line": 89,
"column": 100
}
]
@@ -2479,7 +2491,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 86,
+ "line": 89,
"column": 81
}
]
@@ -2559,7 +2571,7 @@
{
"file": "components/cursor/CursorModelEditor.tsx",
"line": 81,
- "column": 81
+ "column": 52
}
]
},
@@ -2592,6 +2604,18 @@
}
]
},
+ "9d8f2ef4e85ea665": {
+ "source": "自定义上下文 tokens",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 86,
+ "column": 63
+ }
+ ]
+ },
"9e46da6923836182": {
"source": "如:2026-08-23 09:00、1小时前",
"kind": "text",
@@ -3117,12 +3141,12 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 93,
+ "line": 96,
"column": 66
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 93,
+ "line": 96,
"column": 115
}
]
@@ -3286,16 +3310,28 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 94,
+ "line": 97,
"column": 66
},
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 94,
+ "line": 97,
"column": 115
}
]
},
+ "c6cc835023617457": {
+ "source": "自定义上下文",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 85,
+ "column": 25
+ }
+ ]
+ },
"c7ea2c9bc43134bd": {
"source": "编辑模型",
"kind": "text",
@@ -3386,7 +3422,7 @@
{
"file": "components/cursor/CursorModelEditor.tsx",
"line": 81,
- "column": 54
+ "column": 25
}
]
},
@@ -3443,7 +3479,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 94,
+ "line": 97,
"column": 39
}
]
@@ -3477,6 +3513,18 @@
}
]
},
+ "d27db596b73a0a66": {
+ "source": "例如:272000",
+ "kind": "text",
+ "placeholders": [],
+ "refs": [
+ {
+ "file": "components/cursor/CursorModelEditor.tsx",
+ "line": 86,
+ "column": 96
+ }
+ ]
+ },
"d2d648bd1c94b7f9": {
"source": "认证",
"kind": "text",
@@ -3555,7 +3603,7 @@
{
"file": "components/ProviderEditor.tsx",
"line": 26,
- "column": 190
+ "column": 180
}
]
},
@@ -3758,7 +3806,7 @@
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
- "line": 93,
+ "line": 96,
"column": 39
}
]
@@ -3866,6 +3914,18 @@
}
]
},
+ "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",
diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json
index bd18bfe..2f6f72b 100644
--- a/apps/desktop/src/i18n/locales/en-US.json
+++ b/apps/desktop/src/i18n/locales/en-US.json
@@ -110,6 +110,7 @@
"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",
"5d59857bf039cac9": "Cursor Assistant v{version}",
"5f8d556a9c47da3c": "Launch at login disabled",
"5f9acfb945229062": "Are you sure you no longer want to see this ad?",
@@ -180,6 +181,7 @@
"9a026819dd1af5c5": "Enter a model identifier directly or select one returned by the current provider.",
"9ac0ac940982895d": "Select provider",
"9c41b3a9e12ac994": "Reasoning effort",
+ "9d8f2ef4e85ea665": "Custom context tokens",
"9e46da6923836182": "For example: 2026-08-23 09:00, 1 hour ago",
"9f6fee1aba17a565": "Language",
"9fb48101d237ff96": "Last week",
@@ -227,6 +229,7 @@
"c1e98892a77f7a19": "{count} per page",
"c3760858cdb6d9f4": "Request body",
"c62a58459251b02c": "Image generation",
+ "c6cc835023617457": "Custom context",
"c7ea2c9bc43134bd": "Edit model",
"c8df3c14a003bfcd": "Unable to load call details",
"c98e118e0a43f078": "Model",
@@ -239,6 +242,7 @@
"cfe999e50be8ef54": "Whether the model declares image-generation support.",
"d0bfccc77315d887": "Last month",
"d15a909c3490a7e0": "Endpoint type",
+ "d27db596b73a0a66": "For example: 272000",
"d2d648bd1c94b7f9": "Authentication",
"d30d35c5ec4a888d": "Select or enter at least one model",
"d34335433395cd3a": "Start Cursor BYOK automatically after signing in.",
@@ -269,6 +273,7 @@
"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",
"e828bd3a0151edc2": "The local CA must be trusted by the system",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json
index 9ea7ce3..589fa4b 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.json
+++ b/apps/desktop/src/i18n/locales/zh-CN.json
@@ -110,6 +110,7 @@
"5ae715656ffbc35d": "合并到该上游所有模型的请求体。",
"5b17f59d33bde39e": "错误:{error}",
"5c55a67935af8f45": "全部",
+ "5cca0b7972a11f3a": "自定义上下文必须是大于 0 的整数",
"5d59857bf039cac9": "Cursor 助手 v{version}",
"5f8d556a9c47da3c": "已关闭开机启动",
"5f9acfb945229062": "你确认不想再看到此广告吗?",
@@ -180,6 +181,7 @@
"9a026819dd1af5c5": "可以直接输入模型标识,也可以从当前上游返回的模型列表中选择。",
"9ac0ac940982895d": "选择上游",
"9c41b3a9e12ac994": "思考强度",
+ "9d8f2ef4e85ea665": "自定义上下文 tokens",
"9e46da6923836182": "如:2026-08-23 09:00、1小时前",
"9f6fee1aba17a565": "语言",
"9fb48101d237ff96": "近一周",
@@ -227,6 +229,7 @@
"c1e98892a77f7a19": "{count} 条/页",
"c3760858cdb6d9f4": "请求体",
"c62a58459251b02c": "图片生成",
+ "c6cc835023617457": "自定义上下文",
"c7ea2c9bc43134bd": "编辑模型",
"c8df3c14a003bfcd": "无法加载调用详情",
"c98e118e0a43f078": "模型",
@@ -239,6 +242,7 @@
"cfe999e50be8ef54": "是否声明模型支持图片生成。",
"d0bfccc77315d887": "近一个月",
"d15a909c3490a7e0": "端点类型",
+ "d27db596b73a0a66": "例如:272000",
"d2d648bd1c94b7f9": "认证",
"d30d35c5ec4a888d": "请至少选择或输入一个模型",
"d34335433395cd3a": "登录系统后自动启动 Cursor BYOK。",
@@ -269,6 +273,7 @@
"e5043c7a2b408271": "近10分钟",
"e59ae97924d62f01": "第一页",
"e5b9961a0d5242e3": "端口设置已保存,重启软件后生效",
+ "e671f8c7598139ef": "输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。",
"e6ca887f22288cde": "Model ID 和显示名称不能为空",
"e77e3d58b0dcffaa": "耗时",
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
diff --git a/apps/desktop/src/pages/CursorSettingsPage.tsx b/apps/desktop/src/pages/CursorSettingsPage.tsx
index 0577f7f..c0e0d0f 100644
--- a/apps/desktop/src/pages/CursorSettingsPage.tsx
+++ b/apps/desktop/src/pages/CursorSettingsPage.tsx
@@ -55,7 +55,7 @@ export function CursorSettingsPage() {
next.model = {
model_id: model.model_id, display_name: model.display_name, enabled: model.enabled, sort_order: model.sort_order,
endpoint_type: model.endpoint_type, request_url: model.request_url,
- context_window_tokens: null, max_output_tokens: null,
+ context_window_tokens: model.context_window_tokens, max_output_tokens: null,
reasoning_enabled: model.reasoning_enabled, reasoning_effort: null,
supports_image_generation: model.supports_image_generation,
};
@@ -235,6 +235,7 @@ function cursorModelInputs(draft: CursorModelDraft, editing: boolean) {
if (!modelIds.length) throw new Error(t("请至少选择或输入一个模型"));
if (editing && !draft.model.display_name.trim()) throw new Error(t("Model ID 和显示名称不能为空"));
if (draft.customRequestUrl && !draft.model.request_url.trim()) throw new Error(t("请求完整地址不能为空"));
+ if (draft.model.context_window_tokens !== null && (!Number.isSafeInteger(draft.model.context_window_tokens) || draft.model.context_window_tokens <= 0)) throw new Error(t("自定义上下文必须是大于 0 的整数"));
return modelIds.map((modelId, index) => ({
...draft.model,
model_id: modelId,
diff --git a/server/src/cursor/model_catalog.rs b/server/src/cursor/model_catalog.rs
index 3b29e9f..2a7364e 100644
--- a/server/src/cursor/model_catalog.rs
+++ b/server/src/cursor/model_catalog.rs
@@ -205,6 +205,43 @@ const EFFORTS: [(&str, &str); 5] = [
];
const DEFAULT_CONTEXT: &str = "200k";
+fn context_options(model: &ProviderModel) -> Vec<(String, String)> {
+ let mut contexts = CONTEXTS
+ .into_iter()
+ .map(|(value, display_name)| (value.to_owned(), display_name.to_owned()))
+ .collect::>();
+ if let Some(tokens) = model.context_window_tokens {
+ let value = tokens.to_string();
+ let duplicate = contexts
+ .iter()
+ .any(|(existing, _)| parse_context_tokens(existing) == Some(tokens));
+ if !duplicate {
+ contexts.push((value, format!("{} (Custom)", format_token_count(tokens))));
+ }
+ }
+ contexts
+}
+
+fn parse_context_tokens(value: &str) -> Option {
+ let value = value.trim().to_ascii_lowercase();
+ let (number, multiplier) = match value.chars().last()? {
+ 'k' => (&value[..value.len() - 1], 1_000),
+ 'm' => (&value[..value.len() - 1], 1_000_000),
+ _ => (value.as_str(), 1),
+ };
+ number.parse::().ok()?.checked_mul(multiplier)
+}
+
+fn format_token_count(tokens: u64) -> String {
+ if tokens >= 1_000_000 && tokens.is_multiple_of(1_000_000) {
+ format!("{}M", tokens / 1_000_000)
+ } else if tokens >= 1_000 && tokens.is_multiple_of(1_000) {
+ format!("{}K", tokens / 1_000)
+ } else {
+ tokens.to_string()
+ }
+}
+
pub async fn available_models(
State(registry): State,
Extension(proxy): Extension,
@@ -324,7 +361,8 @@ fn unary_payload(body: &Bytes) -> Result<(bool, &[u8])> {
}
fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel {
- let variants = model_variants(model);
+ let contexts = context_options(model);
+ let variants = model_variants(model, &contexts);
let legacy_slugs = variants
.iter()
.filter_map(|variant| variant.legacy_slug.clone())
@@ -348,7 +386,7 @@ fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel
inputbox_short_model_name: Some(model.display_name.clone()),
supports_sandboxing: Some(true),
supports_cmd_k: Some(false),
- parameter_definitions: model_parameters(),
+ parameter_definitions: model_parameters(&contexts),
variants,
legacy_slugs,
named_model_section_index: Some(1),
@@ -365,7 +403,7 @@ fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel
}
}
-fn model_parameters() -> Vec {
+fn model_parameters(contexts: &[(String, String)]) -> Vec {
vec![
ModelParameterDefinition {
id: "context".into(),
@@ -374,11 +412,11 @@ fn model_parameters() -> Vec {
parameter_type: Some(ModelParameterType {
boolean_parameter: None,
enum_parameter: Some(EnumParameter {
- values: CONTEXTS
- .into_iter()
+ values: contexts
+ .iter()
.map(|(value, display_name)| EnumParameterValue {
- value: value.into(),
- display_name: Some(display_name.into()),
+ value: value.clone(),
+ display_name: Some(display_name.clone()),
})
.collect(),
}),
@@ -429,9 +467,9 @@ fn model_parameters() -> Vec {
]
}
-fn model_variants(model: &ProviderModel) -> Vec {
- let mut variants = Vec::with_capacity(CONTEXTS.len() * EFFORTS.len() * 2);
- for (context, context_name) in CONTEXTS {
+fn model_variants(model: &ProviderModel, 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 {
for fast in [false, true] {
variants.push(model_variant(
@@ -546,7 +584,7 @@ mod tests {
request_url: String::new(),
enabled: true,
sort_order: 0,
- context_window_tokens: Some(200_000),
+ context_window_tokens: Some(272_000),
max_output_tokens: None,
reasoning_enabled: false,
reasoning_effort: None,
@@ -591,7 +629,22 @@ mod tests {
.iter()
.map(|value| value.value.as_str())
.collect::>();
- assert_eq!(context_values, ["200k", "356k", "800k", "1m"]);
+ assert_eq!(context_values, ["200k", "356k", "800k", "1m", "272000"]);
+ let custom_context = context
+ .parameter_type
+ .as_ref()
+ .unwrap()
+ .enum_parameter
+ .as_ref()
+ .unwrap()
+ .values
+ .iter()
+ .find(|value| value.value == "272000")
+ .unwrap();
+ assert_eq!(
+ custom_context.display_name.as_deref(),
+ Some("272K (Custom)")
+ );
let effort = mapped
.parameter_definitions
.iter()
@@ -607,8 +660,8 @@ mod tests {
.values
.iter()
.any(|value| value.value == "max"));
- assert_eq!(mapped.variants.len(), 40);
- assert_eq!(mapped.legacy_slugs.len(), 40);
+ 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!(!mapped.model_picker_badges[0].dismiss_on_selection);