use serde_json::Value;
type JsonObject = serde_json::Map<String, Value>;
use super::{normalize_tool_name, truncate_chars};
pub fn trim_llm_payload(name: &str, data: &Value) -> Value {
let normalized = normalize_tool_name(name);
let trimmed = match normalized.as_str() {
"leindex_search" | "search" => trim_search(data),
"leindex_context" | "context" => trim_context(data),
"leindex_diagnostics" | "diagnostics" => trim_diagnostics(data),
"leindex_impact_analysis" | "impact_analysis" => trim_impact(data),
"leindex_symbol_lookup" | "symbol_lookup" => trim_symbol_lookup(data),
"leindex_file_summary" | "file_summary" => trim_file_summary(data),
"leindex_phase_analysis" | "phase_analysis" => trim_phase(data),
"leindex_git_status" | "git_status" => trim_git_status(data),
"leindex_read_file" | "read_file" => trim_read_file(data),
"leindex_read_symbol" | "read_symbol" => trim_read_symbol(data),
"leindex_grep_symbols" | "grep_symbols" => trim_grep_symbols(data),
"leindex_text_search" | "text_search" => trim_text_search(data),
"leindex_deep_analyze" | "deep_analyze" => trim_deep_analyze(data),
"leindex_write" | "write" => trim_write(data),
"leindex_index" | "index" => trim_index(data),
"leindex_edit_preview" | "edit_preview" => trim_edit(data),
"leindex_edit_apply" | "edit_apply" => trim_edit(data),
"leindex_rename_symbol" | "rename_symbol" => trim_rename_symbol(data),
_ => data.clone(),
};
merge_meta(data, &trimmed)
}
pub(crate) fn trim_search(data: &Value) -> Value {
let arr: Option<&Vec<Value>> = data
.as_array()
.or_else(|| data.get("results").and_then(|v| v.as_array()));
let trimmed: Vec<Value> = match arr {
Some(a) => a
.iter()
.map(|r| {
let snippet = r
.get("context")
.filter(|v| !v.is_null())
.or_else(|| r.get("content").filter(|v| !v.is_null()))
.or_else(|| r.get("signature").filter(|v| !v.is_null()))
.cloned()
.map(|v| match v {
Value::String(s) => {
let first = s.lines().next().unwrap_or("").trim();
Value::String(truncate_chars(first, 240))
}
other => other,
})
.unwrap_or(Value::Null);
let score = r
.get("score")
.and_then(|v| v.get("overall"))
.filter(|v| !v.is_null())
.cloned()
.or_else(|| r.get("score").filter(|v| !v.is_null()).cloned())
.unwrap_or(Value::Null);
serde_json::json!({
"file_path": r.get("file_path").cloned().unwrap_or(Value::Null),
"symbol": r
.get("symbol_name")
.filter(|v| !v.is_null())
.or_else(|| r.get("symbol").filter(|v| !v.is_null()))
.cloned()
.unwrap_or(Value::Null),
"symbol_type": r.get("symbol_type").cloned().unwrap_or(Value::Null),
"line_number": r.get("line_number").cloned().unwrap_or(Value::Null),
"score": score,
"snippet": snippet,
})
})
.collect(),
None => Vec::new(),
};
let mut out = serde_json::Map::new();
out.insert("count".to_string(), serde_json::json!(trimmed.len()));
out.insert("results".to_string(), serde_json::json!(trimmed));
if let Some(v) = data.get("offset") {
out.insert("offset".to_string(), v.clone());
}
if let Some(v) = data.get("has_more") {
out.insert("has_more".to_string(), v.clone());
}
if let Some(v) = data.get("suggestion") {
out.insert("suggestion".to_string(), v.clone());
}
Value::Object(out)
}
fn merge_meta(original: &Value, trimmed: &Value) -> Value {
let Some(original) = original.as_object() else {
return trimmed.clone();
};
let mut out = trimmed.clone();
let Some(output) = out.as_object_mut() else {
return trimmed.clone();
};
copy_missing_meta_fields(original, output);
merge_missing_meta_object(original, output);
out
}
fn copy_missing_meta_fields(original: &JsonObject, output: &mut JsonObject) {
for (key, value) in original {
if key.starts_with('_') && !output.contains_key(key) {
output.insert(key.clone(), value.clone());
}
}
}
fn merge_missing_meta_object(original: &JsonObject, output: &mut JsonObject) {
let Some(source) = original.get("_meta").and_then(Value::as_object) else {
return;
};
let Some(target) = output.get_mut("_meta").and_then(Value::as_object_mut) else {
return;
};
for (key, value) in source {
if !target.contains_key(key) {
target.insert(key.clone(), value.clone());
}
}
}
fn trim_context(data: &Value) -> Value {
let mut out = serde_json::Map::new();
for k in [
"query",
"results",
"context",
"tokens_used",
"processing_time_ms",
] {
if let Some(v) = data.get(k) {
out.insert(k.to_string(), v.clone());
}
}
Value::Object(out)
}
fn trim_diagnostics(data: &Value) -> Value {
serde_json::json!({
"project_path": data.get("project_path"),
"indexed_files": data.get("indexed_files"),
"symbol_count": data.get("symbol_count"),
"index_size_mb": data.get("index_size_mb"),
"memory_rss_mb": data.get("memory_rss_mb"),
"db_size_bytes": data.get("db_size_bytes"),
"stale": data.get("stale"),
"last_indexed_secs_ago": data.get("last_indexed_secs_ago"),
"embedding_model": data.get("embedding_model"),
"ort_version": data.get("ort_version"),
"ort_path": data.get("ort_path"),
"execution_provider": data.get("execution_provider"),
"freshness": data.get("freshness"),
"system_health": data.get("system_health"),
"issues": data.get("issues"),
})
}
fn trim_impact(data: &Value) -> Value {
serde_json::json!({
"symbol": data.get("symbol"),
"file": data.get("file"),
"change_type": data.get("change_type"),
"direct_callers": data.get("direct_callers"),
"transitive_affected_symbols": data.get("transitive_affected_symbols"),
"transitive_affected_files": data.get("transitive_affected_files"),
"transitive_callers": data.get("transitive_callers"),
"risk_level": data.get("risk_level"),
"summary": data.get("summary"),
})
}
fn trim_symbol_lookup(data: &Value) -> Value {
if data.get("batch").and_then(|v| v.as_bool()).unwrap_or(false) {
if let Some(arr) = data.get("results").and_then(|v| v.as_array()) {
let mut trimmed_results: Vec<Value> = Vec::with_capacity(arr.len());
for entry in arr {
trimmed_results.push(trim_symbol_lookup_single(entry));
}
let mut out = serde_json::Map::new();
out.insert("batch".to_string(), Value::Bool(true));
out.insert(
"count".to_string(),
data.get("count")
.cloned()
.unwrap_or(Value::from(trimmed_results.len())),
);
out.insert("results".to_string(), Value::Array(trimmed_results));
return Value::Object(out);
}
}
trim_symbol_lookup_single(data)
}
fn trim_symbol_lookup_single(data: &Value) -> Value {
let mut out = serde_json::Map::new();
for k in [
"symbol",
"type",
"file",
"byte_range",
"complexity",
"language",
"callers",
"callees",
"impact_radius",
"source",
] {
if let Some(v) = data.get(k) {
out.insert(k.to_string(), v.clone());
}
}
Value::Object(out)
}
fn trim_file_summary(data: &Value) -> Value {
serde_json::json!({
"file_path": data.get("file_path"),
"language": data.get("language"),
"line_count": data.get("line_count"),
"symbol_count": data.get("symbol_count"),
"symbols_shown": data.get("symbols_shown"),
"symbols_truncated": data.get("symbols_truncated"),
"symbols": data.get("symbols"),
"module_role": data.get("module_role"),
})
}
fn trim_phase(data: &Value) -> Value {
let mut out = serde_json::Map::new();
copy_present_fields(
data,
&mut out,
&[
"generation",
"executed_phases",
"cache_hit",
"changed_files",
"deleted_files",
"mode",
"phases",
"summary",
"file_symbols",
"phase_explanations",
],
);
for phase in 1u8..=5 {
let key = format!("phase{}", phase);
if let Some(value) = data.get(&key) {
out.insert(key, value.clone());
}
}
if let Some(value) = data.get("formatted_output") {
out.insert(
"formatted_output".to_string(),
trimmed_formatted_output(value),
);
}
Value::Object(out)
}
fn copy_present_fields(data: &Value, out: &mut serde_json::Map<String, Value>, keys: &[&str]) {
for key in keys {
if let Some(value) = data.get(key) {
out.insert((*key).to_string(), value.clone());
}
}
}
fn trimmed_formatted_output(value: &Value) -> Value {
let Some(text) = value.as_str() else {
return value.clone();
};
let capped = match text.char_indices().nth(4000) {
Some((index, _)) => &text[..index],
None => text,
};
Value::String(capped.to_string())
}
fn trim_git_status(data: &Value) -> Value {
let branch = data.get("branch");
let summary = data.get("summary");
let modified_files = data.get("modified_files");
let staged_files = data.get("staged_files");
let untracked_files = data.get("untracked_files");
let changed_symbols = data.get("changed_symbols").and_then(|v| {
let arr = v.as_array()?;
Some(serde_json::Value::Array(
arr.iter()
.map(|entry| {
serde_json::json!({
"file": entry.get("file"),
"status": entry.get("status"),
"symbols": take_n(entry.get("symbols").unwrap_or(&Value::Null), 5),
})
})
.collect(),
))
});
serde_json::json!({
"branch": branch,
"summary": summary,
"modified_files": take_n(modified_files.unwrap_or(&Value::Null), 50),
"staged_files": take_n(staged_files.unwrap_or(&Value::Null), 50),
"untracked_files": take_n(untracked_files.unwrap_or(&Value::Null), 50),
"changed_symbols": changed_symbols,
"pdg_enrichment": data.get("pdg_enrichment").unwrap_or(&Value::Null),
"impact_summary": data.get("impact_summary").unwrap_or(&Value::Null),
})
}
fn trim_read_file(data: &Value) -> Value {
let mut out = serde_json::Map::new();
out.insert(
"file_path".to_string(),
data.get("file_path").cloned().unwrap_or(Value::Null),
);
out.insert(
"language".to_string(),
data.get("language").cloned().unwrap_or(Value::Null),
);
out.insert(
"total_lines".to_string(),
data.get("total_lines").cloned().unwrap_or(Value::Null),
);
out.insert(
"start_line".to_string(),
data.get("start_line").cloned().unwrap_or(Value::Null),
);
out.insert(
"end_line".to_string(),
data.get("end_line").cloned().unwrap_or(Value::Null),
);
out.insert(
"content".to_string(),
data.get("content").cloned().unwrap_or(Value::Null),
);
if let Some(ctx) = data.get("context") {
if let Some(obj) = ctx.as_object() {
let mut trimmed_ctx = serde_json::Map::new();
if let Some(v) = obj.get("imports_from") {
trimmed_ctx.insert("imports_from".to_string(), take_n(v, 10));
}
if let Some(v) = obj.get("used_by") {
trimmed_ctx.insert("used_by".to_string(), take_n(v, 10));
}
if let Some(v) = obj.get("symbols_on_visible_lines") {
trimmed_ctx.insert("symbols_on_visible_lines".to_string(), take_n(v, 20));
}
if !trimmed_ctx.is_empty() {
out.insert("context".to_string(), Value::Object(trimmed_ctx));
}
}
}
if let Some(sm) = data.get("symbol_map") {
out.insert("symbol_map".to_string(), thin_symbol_map(sm));
}
Value::Object(out)
}
fn thin_symbol_map(sm: &Value) -> Value {
if let Some(arr) = sm.as_array() {
let trimmed: Vec<Value> = arr
.iter()
.map(|s| {
let mut s = s.clone();
if let Some(obj) = s.as_object_mut() {
obj.remove("complexity");
if let Some(callers) = obj.get("callers") {
obj.insert("callers".to_string(), take_n(callers, 5));
}
if let Some(callees) = obj.get("callees") {
obj.insert("callees".to_string(), take_n(callees, 5));
}
}
s
})
.collect();
Value::Array(trimmed)
} else {
Value::Array(Vec::new())
}
}
fn trim_read_symbol(data: &Value) -> Value {
let mut out = serde_json::Map::new();
for k in [
"symbol",
"type",
"file",
"language",
"complexity",
"line_start",
"line_end",
"doc_comment",
"dependencies",
] {
if let Some(v) = data.get(k) {
out.insert(k.to_string(), v.clone());
}
}
let safety_cap = 32_000usize;
let budget = data
.get("_source_char_budget")
.and_then(|v| v.as_u64())
.map(|v| v as usize)
.unwrap_or(2000);
let trim_cap = budget.min(safety_cap);
if let Some(src) = data.get("source").and_then(|v| v.as_str()) {
let (head, truncated) = match src.char_indices().nth(trim_cap) {
Some((idx, _)) => (format!("{}...", &src[..idx]), true),
None => (src.to_string(), false),
};
out.insert("source".to_string(), Value::String(head));
let handler_truncated = data
.get("source_truncated")
.and_then(|v| v.as_bool())
.unwrap_or(truncated);
out.insert(
"source_truncated".to_string(),
Value::Bool(handler_truncated),
);
}
if let Some(callers) = data.get("callers") {
let len = callers.as_array().map(|a| a.len()).unwrap_or(0);
out.insert("callers".to_string(), take_n(callers, 5));
out.insert("callers_more".to_string(), Value::Bool(len > 5));
}
if let Some(callees) = data.get("callees") {
let len = callees.as_array().map(|a| a.len()).unwrap_or(0);
out.insert("callees".to_string(), take_n(callees, 5));
out.insert("callees_more".to_string(), Value::Bool(len > 5));
}
Value::Object(out)
}
fn trim_grep_symbols(data: &Value) -> Value {
let arr = data
.get("results")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let trimmed: Vec<Value> = arr
.into_iter()
.map(|r| {
let mut obj = serde_json::Map::new();
for k in ["name", "type", "file", "complexity", "caller_count"] {
if let Some(v) = r.get(k) {
obj.insert(k.to_string(), v.clone());
}
}
obj.insert("callers".to_string(), take_n_for_key(&r, "callers", 5));
obj.insert("callees".to_string(), take_n_for_key(&r, "callees", 5));
if let Some(src) = r.get("source") {
obj.insert("source".to_string(), src.clone());
}
if let Some(score) = r.get("score") {
obj.insert("score".to_string(), score.clone());
}
Value::Object(obj)
})
.collect();
let mut out = serde_json::Map::new();
out.insert("results".to_string(), Value::Array(trimmed));
if let Some(v) = data.get("total_matches") {
out.insert("total_matches".to_string(), v.clone());
}
if let Some(v) = data.get("shown") {
out.insert("shown".to_string(), v.clone());
}
if let Some(v) = data.get("offset") {
out.insert("offset".to_string(), v.clone());
}
if let Some(v) = data.get("mode") {
out.insert("mode".to_string(), v.clone());
}
if let Some(v) = data.get("truncated") {
out.insert("truncated".to_string(), v.clone());
}
Value::Object(out)
}
fn trim_text_search(data: &Value) -> Value {
let arr = data
.get("results")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let trimmed: Vec<Value> = arr
.into_iter()
.map(|r| {
let mut obj = serde_json::Map::new();
for k in [
"file",
"line",
"content",
"before",
"after",
"in_symbol",
"symbol_type",
] {
if let Some(v) = r.get(k) {
obj.insert(k.to_string(), v.clone());
}
}
Value::Object(obj)
})
.collect();
let mut out = serde_json::Map::new();
out.insert(
"count".to_string(),
data.get("count").cloned().unwrap_or(Value::Null),
);
out.insert(
"total_matched".to_string(),
data.get("total_matched").cloned().unwrap_or(Value::Null),
);
out.insert(
"has_more".to_string(),
data.get("has_more").cloned().unwrap_or(Value::Null),
);
out.insert(
"offset".to_string(),
data.get("offset").cloned().unwrap_or(Value::Null),
);
out.insert("results".to_string(), Value::Array(trimmed));
Value::Object(out)
}
fn trim_deep_analyze(data: &Value) -> Value {
let results_len = data
.get("results")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
let trimmed: Vec<Value> = data
.get("results")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.take(10)
.map(|r| {
let mut obj = serde_json::Map::new();
for k in [
"rank",
"file_path",
"symbol_name",
"symbol_type",
"signature",
] {
if let Some(v) = r.get(k) {
obj.insert(k.to_string(), v.clone());
}
}
Value::Object(obj)
})
.collect()
})
.unwrap_or_default();
serde_json::json!({
"query": data.get("query"),
"tokens_used": data.get("tokens_used"),
"processing_time_ms": data.get("processing_time_ms"),
"context": data.get("context"),
"results": trimmed,
"results_more": results_len.saturating_sub(10),
})
}
fn trim_write(data: &Value) -> Value {
let trimmed: Vec<Value> = data
.get("symbols")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|s| {
let mut obj = serde_json::Map::new();
for k in ["name", "type"] {
if let Some(v) = s.get(k) {
obj.insert(k.to_string(), v.clone());
}
}
Value::Object(obj)
})
.collect()
})
.unwrap_or_default();
serde_json::json!({
"success": data.get("success"),
"file_path": data.get("file_path"),
"language": data.get("language"),
"symbols": trimmed,
})
}
fn trim_index(data: &Value) -> Value {
let failures = data.get("failed_parses").cloned().unwrap_or(Value::Null);
serde_json::json!({
"total_files": data.get("total_files"),
"files_parsed": data.get("files_parsed"),
"parse_failures": failures,
"indexing_time_ms": data.get("indexing_time_ms"),
"pdg_nodes": data.get("pdg_nodes"),
"pdg_edges": data.get("pdg_edges"),
"indexed_nodes": data.get("indexed_nodes"),
"total_signatures": data.get("total_signatures"),
"external_deps_unresolved": data.get("external_deps_unresolved"),
})
}
fn trim_edit(data: &Value) -> Value {
let mut out = serde_json::Map::new();
for k in [
"preview_token",
"diff",
"diff_text",
"affected_symbols",
"affected_files",
"breaking_changes",
"risk_level",
"change_count",
"success",
"changes_applied",
"file_path",
"edit_region",
"message",
] {
if let Some(v) = data.get(k) {
out.insert(k.to_string(), v.clone());
}
}
Value::Object(out)
}
fn trim_rename_symbol(data: &Value) -> Value {
let diffs = data.get("diffs").and_then(|v| v.as_array());
let total = diffs.map(|a| a.len()).unwrap_or(0);
let shown: Vec<Value> = diffs
.map(|arr| {
arr.iter()
.take(25)
.map(|d| {
let mut obj = serde_json::Map::new();
if let Some(v) = d.get("file") {
obj.insert("file".to_string(), v.clone());
}
if let Some(v) = d.get("diff") {
obj.insert("diff".to_string(), v.clone());
}
Value::Object(obj)
})
.collect()
})
.unwrap_or_default();
let files_affected = data
.get("files_affected")
.and_then(|v| v.as_u64())
.map(|v| v as usize)
.unwrap_or(total);
let mut out = serde_json::Map::new();
out.insert("diffs".to_string(), Value::Array(shown));
out.insert(
"diffs_more".to_string(),
Value::from(total.saturating_sub(25)),
);
out.insert(
"old_name".to_string(),
data.get("old_name").cloned().unwrap_or(Value::Null),
);
out.insert(
"new_name".to_string(),
data.get("new_name").cloned().unwrap_or(Value::Null),
);
out.insert("files_affected".to_string(), Value::from(files_affected));
out.insert(
"preview_only".to_string(),
data.get("preview_only")
.cloned()
.unwrap_or(Value::Bool(true)),
);
out.insert(
"applied".to_string(),
data.get("applied").cloned().unwrap_or(Value::Bool(false)),
);
Value::Object(out)
}
fn take_n(v: &Value, n: usize) -> Value {
match v.as_array() {
Some(arr) => Value::Array(arr.iter().take(n).cloned().collect()),
None => Value::Array(Vec::new()),
}
}
fn take_n_for_key(obj: &Value, key: &str, n: usize) -> Value {
match obj.get(key) {
Some(v) if v.is_array() => take_n(v, n),
Some(v) => v.clone(),
None => Value::Null,
}
}
#[cfg(test)]
#[path = "trim_test.rs"]
mod tests;