mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-06 13:44:21 +08:00
feat(docs): add documentation site and demo features
- Introduced a new documentation site for Cursor BYOK using Next.js and Fumadocs. - Added a product demo page with a corresponding Vite configuration. - Implemented a demo API to simulate LLM calls and responses. - Enhanced the Makefile to include new build and development commands for the documentation. - Updated package.json scripts for building and running the documentation site. - Created various components and layouts for the documentation structure, including blog and user documentation sections. - Added styling for the new components and layouts to ensure a cohesive design. - Included a README and other necessary files for local development and deployment.
This commit is contained in:
@@ -181,6 +181,11 @@ impl ModelConfig {
|
||||
|
||||
pub fn configure(&self, model: &mut super::ModelSpec) {
|
||||
model.display_name = Some(self.display_name.clone());
|
||||
// A request-selected context is authoritative. Use the saved model
|
||||
// value only when Cursor did not send a context parameter.
|
||||
if model.context_window_tokens.is_none() {
|
||||
model.context_window_tokens = self.context_window_tokens;
|
||||
}
|
||||
if model.reasoning.effort.is_none() {
|
||||
model.reasoning.effort = match self.model_type {
|
||||
ModelType::OpenAi => self.reasoning_effort.clone(),
|
||||
@@ -505,4 +510,77 @@ mod tests {
|
||||
"https://example.com/v1/messages"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_context_window_does_not_override_the_client_request() {
|
||||
let input = input();
|
||||
let config = ModelConfig {
|
||||
model_hash: "hash".into(),
|
||||
sort_order: input.sort_order,
|
||||
display_name: input.display_name,
|
||||
model_type: input.model_type,
|
||||
base_url: input.base_url,
|
||||
use_full_url: input.use_full_url,
|
||||
api_key: input.api_key,
|
||||
tooltip_data: input.tooltip_data,
|
||||
model_id: input.model_id,
|
||||
reasoning_effort: input.reasoning_effort,
|
||||
openai_endpoint: input.openai_endpoint,
|
||||
openai_extra_params_enabled: input.openai_extra_params_enabled,
|
||||
openai_extra_params: input.openai_extra_params,
|
||||
custom_headers_enabled: input.custom_headers_enabled,
|
||||
custom_headers: input.custom_headers,
|
||||
anthropic_extra_params_enabled: input.anthropic_extra_params_enabled,
|
||||
anthropic_extra_params: input.anthropic_extra_params,
|
||||
context_window_tokens: Some(350_000),
|
||||
max_completion_tokens: input.max_completion_tokens,
|
||||
anthropic_max_tokens: input.anthropic_max_tokens,
|
||||
anthropic_thinking_effort: input.anthropic_thinking_effort,
|
||||
thinking_budget_tokens: input.thinking_budget_tokens,
|
||||
created_at_ms: 0,
|
||||
updated_at_ms: 0,
|
||||
};
|
||||
let mut requested = super::super::ModelSpec::new("model-a");
|
||||
requested.context_window_tokens = Some(200_000);
|
||||
|
||||
config.configure(&mut requested);
|
||||
|
||||
assert_eq!(requested.context_window_tokens, Some(200_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_context_window_fills_missing_client_value() {
|
||||
let input = input();
|
||||
let config = ModelConfig {
|
||||
model_hash: "hash".into(),
|
||||
sort_order: input.sort_order,
|
||||
display_name: input.display_name,
|
||||
model_type: input.model_type,
|
||||
base_url: input.base_url,
|
||||
use_full_url: input.use_full_url,
|
||||
api_key: input.api_key,
|
||||
tooltip_data: input.tooltip_data,
|
||||
model_id: input.model_id,
|
||||
reasoning_effort: input.reasoning_effort,
|
||||
openai_endpoint: input.openai_endpoint,
|
||||
openai_extra_params_enabled: input.openai_extra_params_enabled,
|
||||
openai_extra_params: input.openai_extra_params,
|
||||
custom_headers_enabled: input.custom_headers_enabled,
|
||||
custom_headers: input.custom_headers,
|
||||
anthropic_extra_params_enabled: input.anthropic_extra_params_enabled,
|
||||
anthropic_extra_params: input.anthropic_extra_params,
|
||||
context_window_tokens: Some(350_000),
|
||||
max_completion_tokens: input.max_completion_tokens,
|
||||
anthropic_max_tokens: input.anthropic_max_tokens,
|
||||
anthropic_thinking_effort: input.anthropic_thinking_effort,
|
||||
thinking_budget_tokens: input.thinking_budget_tokens,
|
||||
created_at_ms: 0,
|
||||
updated_at_ms: 0,
|
||||
};
|
||||
let mut requested = super::super::ModelSpec::new("model-a");
|
||||
|
||||
config.configure(&mut requested);
|
||||
|
||||
assert_eq!(requested.context_window_tokens, Some(350_000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ mod run;
|
||||
mod runtime_tag;
|
||||
mod token_count;
|
||||
mod tool;
|
||||
mod tool_result_replay;
|
||||
mod usage;
|
||||
|
||||
pub use configuration::*;
|
||||
@@ -26,4 +27,5 @@ pub use run::*;
|
||||
pub use runtime_tag::*;
|
||||
pub(crate) use token_count::*;
|
||||
pub use tool::*;
|
||||
pub(crate) use tool_result_replay::limit_tool_result_text;
|
||||
pub use usage::*;
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
use serde_json::Value;
|
||||
|
||||
const KIB: usize = 1024;
|
||||
|
||||
pub(crate) fn limit_tool_result_text(name: &str, content: &str) -> String {
|
||||
let Some(limit) = replay_limit(name) else {
|
||||
return content.to_string();
|
||||
};
|
||||
let content = match name.trim() {
|
||||
"GenerateImage" => compact_generate_image(content),
|
||||
"Shell" => compact_shell(content),
|
||||
"PatchEdit" | "PatchEditLines" | "PatchEditSpan" | "StrReplace" | "Edit" | "Write" => {
|
||||
compact_edit(name, content)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.unwrap_or_else(|| content.to_string());
|
||||
truncate_replay_text(name, &content, limit)
|
||||
}
|
||||
|
||||
fn replay_limit(name: &str) -> Option<usize> {
|
||||
match name.trim() {
|
||||
"GenerateImage" | "WebSearch" => Some(16 * KIB),
|
||||
"Read" => Some(64 * KIB),
|
||||
"Shell" => Some(128 * KIB),
|
||||
"Grep" | "Glob" => Some(32 * KIB),
|
||||
"PatchEdit" | "PatchEditLines" | "PatchEditSpan" | "StrReplace" => Some(4 * KIB),
|
||||
"Edit" | "EditNotebook" | "Write" | "WebFetch" => Some(32 * KIB),
|
||||
"CallMcpTool" | "FetchMcpResource" | "ListMcpResources" | "GetMcpTools"
|
||||
| "SembleSearch" | "SembleFindRelated" => Some(32 * KIB),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_replay_text(name: &str, content: &str, limit: usize) -> String {
|
||||
if content.len() <= limit {
|
||||
return content.to_string();
|
||||
}
|
||||
let original = content.len();
|
||||
let mut shown = limit;
|
||||
loop {
|
||||
let notice = format!(
|
||||
"\n\n[truncated: {name} result exceeded {limit} bytes; showing {shown} of {original} bytes]"
|
||||
);
|
||||
let available = limit.saturating_sub(notice.len());
|
||||
let kept = utf8_prefix(content, available);
|
||||
if kept.len() == shown {
|
||||
return format!("{}{notice}", kept.trim_end_matches('\n'));
|
||||
}
|
||||
shown = kept.len();
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_generate_image(content: &str) -> Option<String> {
|
||||
let mut value = serde_json::from_str::<Value>(content.trim()).ok()?;
|
||||
if !replace_image_data(&mut value) {
|
||||
return None;
|
||||
}
|
||||
serde_json::to_string(&value).ok()
|
||||
}
|
||||
|
||||
fn replace_image_data(value: &mut Value) -> bool {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
let mut changed = false;
|
||||
for (key, child) in object.iter_mut() {
|
||||
if matches!(key.as_str(), "image_data" | "imageData") {
|
||||
if let Value::String(data) = child {
|
||||
if data.starts_with("[base64 image data omitted from replay; bytes=") {
|
||||
continue;
|
||||
}
|
||||
*child = Value::String(format!(
|
||||
"[base64 image data omitted from replay; bytes={}]",
|
||||
data.trim().len()
|
||||
));
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changed |= replace_image_data(child);
|
||||
}
|
||||
changed
|
||||
}
|
||||
Value::Array(items) => items.iter_mut().any(replace_image_data),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_shell(content: &str) -> Option<String> {
|
||||
let mut value = serde_json::from_str::<Value>(content.trim()).ok()?;
|
||||
if !compact_shell_fields(&mut value) {
|
||||
return None;
|
||||
}
|
||||
serde_json::to_string(&value).ok()
|
||||
}
|
||||
|
||||
fn compact_shell_fields(value: &mut Value) -> bool {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
let mut changed = false;
|
||||
for (key, child) in object.iter_mut() {
|
||||
if let Value::String(text) = child {
|
||||
let limit = match key.as_str() {
|
||||
"stdout" | "stderr" => Some(16 * KIB),
|
||||
"interleaved_output" | "interleavedOutput" => Some(32 * KIB),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(limit) = limit {
|
||||
let next = truncate_middle(&format!("Shell {key}"), text, limit);
|
||||
if next != *text {
|
||||
*text = next;
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changed |= compact_shell_fields(child);
|
||||
}
|
||||
changed
|
||||
}
|
||||
Value::Array(items) => items.iter_mut().any(compact_shell_fields),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_edit(name: &str, content: &str) -> Option<String> {
|
||||
let value = serde_json::from_str::<Value>(content.trim()).ok()?;
|
||||
let success = value.get("success")?.as_object()?;
|
||||
let diff = success
|
||||
.get("diff_string")
|
||||
.or_else(|| success.get("diffString"))
|
||||
.and_then(Value::as_str)
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(|text| truncate_replay_text(name, text, edit_limit(name)));
|
||||
if let Some(diff) = diff {
|
||||
return Some(serde_json::json!({"success": {"diff_string": diff}}).to_string());
|
||||
}
|
||||
let after = success
|
||||
.get("after_full_file_content")
|
||||
.or_else(|| success.get("afterFullFileContent"))
|
||||
.and_then(Value::as_str)
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(|text| truncate_replay_text(name, text, edit_limit(name)));
|
||||
after
|
||||
.map(|after| serde_json::json!({"success": {"after_full_file_content": after}}).to_string())
|
||||
}
|
||||
|
||||
fn edit_limit(name: &str) -> usize {
|
||||
match name.trim() {
|
||||
"PatchEdit" | "PatchEditLines" | "PatchEditSpan" | "StrReplace" => 4 * KIB,
|
||||
_ => 32 * KIB,
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_middle(name: &str, content: &str, limit: usize) -> String {
|
||||
if content.len() <= limit {
|
||||
return content.to_string();
|
||||
}
|
||||
let original = content.len();
|
||||
let mut shown = limit;
|
||||
loop {
|
||||
let notice = format!(
|
||||
"\n\n[truncated: {name} result exceeded {limit} bytes; omitted middle; showing {shown} of {original} bytes]\n\n"
|
||||
);
|
||||
let available = limit.saturating_sub(notice.len());
|
||||
let head = utf8_prefix(content, available / 2);
|
||||
let tail = utf8_suffix(content, available.saturating_sub(head.len()));
|
||||
let next_shown = head.len() + tail.len();
|
||||
let next_notice = format!(
|
||||
"\n\n[truncated: {name} result exceeded {limit} bytes; omitted middle; showing {next_shown} of {original} bytes]\n\n"
|
||||
);
|
||||
let output = format!("{head}{next_notice}{tail}");
|
||||
if output.len() <= limit || next_notice == notice {
|
||||
return output;
|
||||
}
|
||||
shown = next_shown;
|
||||
}
|
||||
}
|
||||
|
||||
fn utf8_prefix(value: &str, limit: usize) -> &str {
|
||||
let mut end = limit.min(value.len());
|
||||
while end > 0 && !value.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
&value[..end]
|
||||
}
|
||||
|
||||
fn utf8_suffix(value: &str, limit: usize) -> &str {
|
||||
let mut start = value.len().saturating_sub(limit);
|
||||
while start < value.len() && !value.is_char_boundary(start) {
|
||||
start += 1;
|
||||
}
|
||||
&value[start..]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn truncation_preserves_utf8_and_limit() {
|
||||
let content = "前".repeat(32 * KIB);
|
||||
let truncated = truncate_replay_text("Grep", &content, 32 * KIB);
|
||||
|
||||
assert!(truncated.len() <= 32 * KIB);
|
||||
assert!(truncated.is_char_boundary(truncated.len()));
|
||||
assert!(truncated.contains("[truncated: Grep result exceeded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_replay_compacts_nested_image_data_and_shell_streams() {
|
||||
let image = serde_json::json!({"success": {"image_data": "x".repeat(64 * KIB)}});
|
||||
let image_result = limit_tool_result_text("GenerateImage", &image.to_string());
|
||||
assert!(image_result.contains("base64 image data omitted"));
|
||||
assert!(image_result.len() < 1024);
|
||||
assert_eq!(
|
||||
limit_tool_result_text("GenerateImage", &image_result),
|
||||
image_result
|
||||
);
|
||||
|
||||
let shell = serde_json::json!({"success": {"stdout": "x".repeat(64 * KIB)}});
|
||||
let shell_result = limit_tool_result_text("Shell", &shell.to_string());
|
||||
assert!(shell_result.len() <= 128 * KIB);
|
||||
assert!(shell_result.contains("omitted middle"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user