Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/components/cursor/CursorModelEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function CursorModelEditor({ draft, providers, editing, modelOptions, dis
? <Combobox value={draft.model.model_id} options={modelOptions} placeholder="例如:gpt-4.1" append={<button type="button" className={controls.secondary} disabled={discovering || !canDiscover} onClick={onDiscover}>{discovering ? t("获取中…") : t("获取模型")}</button>} onChange={(model_id) => setModel({ model_id, display_name: draft.model.display_name || model_id })} />
: <MultiCombobox value={draft.modelIds} options={modelOptions} placeholder="例如:gpt-4.1" append={<button type="button" className={controls.secondary} disabled={discovering || !canDiscover} onClick={onDiscover}>{discovering ? t("获取中…") : t("获取模型")}</button>} onChange={setModelIds} />
}</FormField>
<FormField label={t("自定义上下文")} hint={t("输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。")}>
<FormField label={t("自定义上下文")} hint={t("自定义模型上下文长度,配置后优先使用自定义项")}>
<TextInput type="number" min={1} step={1} aria-label={t("自定义上下文 tokens")} placeholder={t("例如:272000")} value={draft.model.context_window_tokens ?? ""} onChange={(event) => setModel({ context_window_tokens: event.target.value === "" ? null : Math.trunc(Number(event.target.value)) })} />
</FormField>
<div className={styles.fullWidth}><Checkbox label={t("自定义请求完整地址")} checked={draft.customRequestUrl} onChange={(customRequestUrl) => onChange({ ...draft, customRequestUrl, model: { ...draft.model, request_url: customRequestUrl ? draft.model.request_url : "" } })} /></div>
Expand Down
24 changes: 12 additions & 12 deletions apps/desktop/src/i18n/generated/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,18 @@
}
]
},
"312051befbdeb20e": {
"source": "自定义模型上下文长度,配置后优先使用自定义项",
"kind": "text",
"placeholders": [],
"refs": [
{
"file": "components/cursor/CursorModelEditor.tsx",
"line": 85,
"column": 44
}
]
},
"36f33adaf0942634": {
"source": "确认",
"kind": "text",
Expand Down Expand Up @@ -3914,18 +3926,6 @@
}
]
},
"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",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"2f6416a2c424856b": "Final request URL",
"2f7dec3be28d7597": "{count} selected",
"2f9daa828907b93f": "Delete",
"312051befbdeb20e": "Customize the model context length. Once configured, the custom option takes priority.",
"36f33adaf0942634": "Confirm",
"378bb0eec39fa8a2": "Last page",
"37cb98ff4d5dcfcc": "Successful {successful} / failed {failed}",
Expand Down Expand Up @@ -273,7 +274,6 @@
"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",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"2f6416a2c424856b": "最终请求地址",
"2f7dec3be28d7597": "已选择 {count} 个",
"2f9daa828907b93f": "删除",
"312051befbdeb20e": "自定义模型上下文长度,配置后优先使用自定义项",
"36f33adaf0942634": "确认",
"378bb0eec39fa8a2": "最后一页",
"37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}",
Expand Down Expand Up @@ -273,7 +274,6 @@
"e5043c7a2b408271": "近10分钟",
"e59ae97924d62f01": "第一页",
"e5b9961a0d5242e3": "端口设置已保存,重启软件后生效",
"e671f8c7598139ef": "输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。",
"e6ca887f22288cde": "Model ID 和显示名称不能为空",
"e77e3d58b0dcffaa": "耗时",
"e828bd3a0151edc2": "需要在系统中信任本地 CA",
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/store/appStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ export const appStore = {
update({ cursorBusy: true, error: null });
try {
const updated = await api.updateModel(hash, model);
await appStore.refresh();
update({
models: snapshot.models.map((current) => current.model_hash === hash ? updated : current),
});
return updated;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
Expand Down
66 changes: 47 additions & 19 deletions server/src/cursor/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ struct AvailableModel {
supports_images: Option<bool>,
#[prost(bool, optional, tag = "14")]
supports_max_mode: Option<bool>,
#[prost(int32, optional, tag = "15")]
context_token_limit: Option<i32>,
#[prost(int32, optional, tag = "16")]
context_token_limit_for_max_mode: Option<i32>,
#[prost(string, optional, tag = "17")]
client_display_name: Option<String>,
#[prost(string, optional, tag = "18")]
Expand Down Expand Up @@ -205,20 +209,32 @@ const EFFORTS: [(&str, &str); 5] = [
];
const DEFAULT_CONTEXT: &str = "200k";

fn context_options(model: &ProviderModel) -> Vec<(String, String)> {
let mut contexts = CONTEXTS
fn configured_context(model: &ProviderModel) -> Option<(String, String)> {
let tokens = model.context_window_tokens?;
CONTEXTS
.into_iter()
.find(|(value, _)| parse_token_count(value) == Some(tokens))
.map(|(value, display_name)| (value.to_owned(), display_name.to_owned()))
.collect::<Vec<_>>();
if let Some(tokens) = model.context_window_tokens {
let value = tokens.to_string();
let duplicate = contexts
.iter()
.any(|(existing, _)| parse_token_count(existing) == Some(tokens));
if !duplicate {
contexts.push((value, format!("{} (Custom)", format_token_count(tokens))));
}
.or_else(|| Some((tokens.to_string(), format_token_count(tokens))))
}

fn context_options(model: &ProviderModel) -> Vec<(String, String)> {
let configured = configured_context(model);
let mut contexts =
Vec::with_capacity(CONTEXTS.len() + if configured.is_some() { 1 } else { 0 });
if let Some(context) = configured.as_ref() {
contexts.push(context.clone());
}
contexts.extend(
CONTEXTS
.into_iter()
.filter(|(value, _)| {
configured.as_ref().map_or(true, |(configured_value, _)| {
configured_value.as_str() != *value
})
})
.map(|(value, display_name)| (value.to_owned(), display_name.to_owned())),
);
contexts
}

Expand Down Expand Up @@ -342,12 +358,18 @@ fn unary_payload(body: &Bytes) -> Result<(bool, &[u8])> {

fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel {
let contexts = context_options(model);
let default_context_name = configured_context(model)
.map(|(_, display_name)| display_name)
.unwrap_or_else(|| "200K".into());
let context_token_limit = model
.context_window_tokens
.map(|tokens| tokens.min(i32::MAX as u64) as i32);
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, &default_context_name, "high", false);
AvailableModel {
name: model.model_hash.clone(),
default_on: true,
Expand All @@ -357,6 +379,8 @@ fn available_model(model: &ProviderModel, provider_name: &str) -> AvailableModel
supports_thinking: Some(true),
supports_images: Some(true),
supports_max_mode: Some(true),
context_token_limit,
context_token_limit_for_max_mode: context_token_limit,
client_display_name: Some(model.display_name.clone()),
server_model_name: Some(model.model_hash.clone()),
supports_non_max_mode: Some(true),
Expand Down Expand Up @@ -448,6 +472,9 @@ fn model_parameters(contexts: &[(String, String)]) -> Vec<ModelParameterDefiniti
}

fn model_variants(model: &ProviderModel, contexts: &[(String, String)]) -> Vec<ModelVariant> {
let default_context = configured_context(model)
.map(|(value, _)| value)
.unwrap_or_else(|| DEFAULT_CONTEXT.to_owned());
let mut variants = Vec::with_capacity(contexts.len() * EFFORTS.len() * 2);
for (context, context_name) in contexts {
for (effort, effort_name) in EFFORTS {
Expand All @@ -456,6 +483,7 @@ fn model_variants(model: &ProviderModel, contexts: &[(String, String)]) -> Vec<M
model,
context,
context_name,
&default_context,
effort,
effort_name,
fast,
Expand All @@ -470,6 +498,7 @@ fn model_variant(
model: &ProviderModel,
context: &str,
context_name: &str,
default_context: &str,
effort: &str,
effort_name: &str,
fast: bool,
Expand All @@ -487,7 +516,7 @@ fn model_variant(
"{} <span style=\"color: var(--cursor-text-tertiary);\">{suffix}</span>",
model.display_name
);
let is_default = context == DEFAULT_CONTEXT && effort == "high" && !fast;
let is_default = context == default_context && effort == "high" && !fast;
ModelVariant {
parameter_values: vec![
ModelParameterValue {
Expand Down Expand Up @@ -581,6 +610,8 @@ mod tests {
assert_eq!(mapped.supports_thinking, Some(true));
assert_eq!(mapped.supports_images, Some(true));
assert_eq!(mapped.supports_max_mode, Some(true));
assert_eq!(mapped.context_token_limit, Some(272_000));
assert_eq!(mapped.context_token_limit_for_max_mode, Some(272_000));
assert_eq!(mapped.supports_non_max_mode, Some(true));
assert_eq!(mapped.supports_plan_mode, Some(true));
assert_eq!(mapped.supports_sandboxing, Some(true));
Expand Down Expand Up @@ -609,7 +640,7 @@ mod tests {
.iter()
.map(|value| value.value.as_str())
.collect::<Vec<_>>();
assert_eq!(context_values, ["200k", "356k", "800k", "1m", "272000"]);
assert_eq!(context_values, ["272000", "200k", "356k", "800k", "1m"]);
let custom_context = context
.parameter_type
.as_ref()
Expand All @@ -621,10 +652,7 @@ mod tests {
.iter()
.find(|value| value.value == "272000")
.unwrap();
assert_eq!(
custom_context.display_name.as_deref(),
Some("272K (Custom)")
);
assert_eq!(custom_context.display_name.as_deref(), Some("272K"));
let effort = mapped
.parameter_definitions
.iter()
Expand Down Expand Up @@ -652,7 +680,7 @@ mod tests {
.unwrap();
assert_eq!(
default.variant_string_representation.as_deref(),
Some("33ceed20[context=200k,effort=high,fast=false]")
Some("33ceed20[context=272000,effort=high,fast=false]")
);
assert_eq!(mapped.vendor.unwrap().display_name, "Cursor");
assert!(usable_model(&model).thinking_details.is_some());
Expand Down
7 changes: 4 additions & 3 deletions server/src/model/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pub struct ProviderModel {
impl ProviderModel {
pub fn configure(&self, model: &mut super::ModelSpec) {
model.display_name = Some(self.display_name.clone());
model.context_window_tokens = self.context_window_tokens.or(model.context_window_tokens);
model.supports_image_generation = self.supports_image_generation;
model.reasoning.enabled |= self.reasoning_enabled;
}
Expand Down Expand Up @@ -311,7 +312,7 @@ mod tests {
}

#[test]
fn requested_runtime_limits_are_not_overridden_by_provider_config() {
fn configured_context_window_has_priority_over_cursor_selection() {
let provider = ProviderModel {
model_hash: "12345678".into(),
provider_id: 1,
Expand All @@ -332,10 +333,10 @@ mod tests {
let mut selected = super::super::ModelSpec::new("12345678");
selected.context_window_tokens = Some(800_000);
provider.configure(&mut selected);
assert_eq!(selected.context_window_tokens, Some(800_000));
assert_eq!(selected.context_window_tokens, Some(200_000));

let mut defaulted = super::super::ModelSpec::new("12345678");
provider.configure(&mut defaulted);
assert_eq!(defaulted.context_window_tokens, None);
assert_eq!(defaulted.context_window_tokens, Some(200_000));
}
}