diff --git a/apps/desktop/src/components/cursor/CursorModelEditor.tsx b/apps/desktop/src/components/cursor/CursorModelEditor.tsx index 49b5a014a..f3f47df83 100644 --- a/apps/desktop/src/components/cursor/CursorModelEditor.tsx +++ b/apps/desktop/src/components/cursor/CursorModelEditor.tsx @@ -82,7 +82,7 @@ export function CursorModelEditor({ draft, providers, editing, modelOptions, dis ? {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 : "" } })} />
diff --git a/apps/desktop/src/i18n/generated/catalog.json b/apps/desktop/src/i18n/generated/catalog.json index 32969c8ac..23049b1ea 100644 --- a/apps/desktop/src/i18n/generated/catalog.json +++ b/apps/desktop/src/i18n/generated/catalog.json @@ -998,6 +998,18 @@ } ] }, + "312051befbdeb20e": { + "source": "自定义模型上下文长度,配置后优先使用自定义项", + "kind": "text", + "placeholders": [], + "refs": [ + { + "file": "components/cursor/CursorModelEditor.tsx", + "line": 85, + "column": 44 + } + ] + }, "36f33adaf0942634": { "source": "确认", "kind": "text", @@ -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", diff --git a/apps/desktop/src/i18n/locales/en-US.json b/apps/desktop/src/i18n/locales/en-US.json index 2f6f72b7f..36fdf3e33 100644 --- a/apps/desktop/src/i18n/locales/en-US.json +++ b/apps/desktop/src/i18n/locales/en-US.json @@ -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}", @@ -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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json index 589fa4bc7..bfb6c16b2 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.json +++ b/apps/desktop/src/i18n/locales/zh-CN.json @@ -64,6 +64,7 @@ "2f6416a2c424856b": "最终请求地址", "2f7dec3be28d7597": "已选择 {count} 个", "2f9daa828907b93f": "删除", + "312051befbdeb20e": "自定义模型上下文长度,配置后优先使用自定义项", "36f33adaf0942634": "确认", "378bb0eec39fa8a2": "最后一页", "37cb98ff4d5dcfcc": "成功 {successful} / 异常 {failed}", @@ -273,7 +274,6 @@ "e5043c7a2b408271": "近10分钟", "e59ae97924d62f01": "第一页", "e5b9961a0d5242e3": "端口设置已保存,重启软件后生效", - "e671f8c7598139ef": "输入 token 数后,将作为额外选项添加到 Cursor 模型的 Context 列表;只有在 Cursor 中选中该选项时才会生效。", "e6ca887f22288cde": "Model ID 和显示名称不能为空", "e77e3d58b0dcffaa": "耗时", "e828bd3a0151edc2": "需要在系统中信任本地 CA", diff --git a/apps/desktop/src/store/appStore.ts b/apps/desktop/src/store/appStore.ts index e38429483..1ef44753f 100644 --- a/apps/desktop/src/store/appStore.ts +++ b/apps/desktop/src/store/appStore.ts @@ -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) }); diff --git a/server/src/cursor/model_catalog.rs b/server/src/cursor/model_catalog.rs index aefe2804a..496fb35ce 100644 --- a/server/src/cursor/model_catalog.rs +++ b/server/src/cursor/model_catalog.rs @@ -44,6 +44,10 @@ struct AvailableModel { supports_images: Option, #[prost(bool, optional, tag = "14")] supports_max_mode: Option, + #[prost(int32, optional, tag = "15")] + context_token_limit: Option, + #[prost(int32, optional, tag = "16")] + context_token_limit_for_max_mode: Option, #[prost(string, optional, tag = "17")] client_display_name: Option, #[prost(string, optional, tag = "18")] @@ -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::>(); - 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 } @@ -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, @@ -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), @@ -448,6 +472,9 @@ fn model_parameters(contexts: &[(String, String)]) -> Vec Vec { + 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 { @@ -456,6 +483,7 @@ fn model_variants(model: &ProviderModel, contexts: &[(String, String)]) -> Vec{suffix}", 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 { @@ -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)); @@ -609,7 +640,7 @@ mod tests { .iter() .map(|value| value.value.as_str()) .collect::>(); - 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() @@ -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() @@ -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()); diff --git a/server/src/model/provider.rs b/server/src/model/provider.rs index 932409ca4..95c7c65d1 100644 --- a/server/src/model/provider.rs +++ b/server/src/model/provider.rs @@ -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; } @@ -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, @@ -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)); } }