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:
leokun
2026-08-27 21:45:28 +08:00
parent ee915ee760
commit c5d578c5b1
70 changed files with 13299 additions and 567 deletions
+78
View File
@@ -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));
}
}
+2
View File
@@ -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::*;
+226
View File
@@ -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"));
}
}