use std::collections::HashMap;
use anyhow::Result;
use gaoya::simhash::{SimHash, SimHashBits, SimSipHasher64};
use serde_json::Value;
use crate::llmtrim::gate::{GateKind, PlanEntry, Transform};
use crate::llmtrim::ir::Request;
use crate::llmtrim::provider::Provider;
pub struct DedupStage {
pub near: bool,
pub near_max_distance: u32,
}
impl Transform for DedupStage {
fn name(&self) -> &str {
"dedup"
}
fn gate_kind(&self) -> GateKind {
GateKind::InputTokens
}
fn apply(
&self,
req: &mut Request,
provider: &dyn Provider,
_plan: &mut Vec<PlanEntry>,
) -> Result<()> {
for ptr in crate::llmtrim::cache_zone::compressible_pointers(req, provider) {
let Some(s) = req.get_str(&ptr).map(str::to_string) else {
continue;
};
let deduped = if crate::llmtrim::stages::tools::is_structured_segment(&s) {
dedup_adjacent_lines(&s)
} else {
dedup_lines(&s, self.near, self.near_max_distance)
};
if deduped != s {
req.set(&ptr, Value::String(deduped));
}
}
Ok(())
}
}
fn solo_tag_line(line: &str) -> Option<(&str, bool)> {
let t = line.trim();
let body = t.strip_prefix('<')?.strip_suffix('>')?;
let (body, is_close) = match body.strip_prefix('/') {
Some(rest) => (rest, true),
None => (body, false),
};
let is_ident = |c: char| c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == ':';
let name_end = body.find(|c| !is_ident(c)).unwrap_or(body.len());
let (name, rest) = body.split_at(name_end);
if name.is_empty() || !name.starts_with(|c: char| c.is_alphabetic() || c == '_') {
return None;
}
if is_close {
rest.chars()
.all(char::is_whitespace)
.then_some((name, true))
} else {
((rest.is_empty() || rest.starts_with(char::is_whitespace))
&& !rest.contains(['<', '>'])
&& !rest.trim_end().ends_with('/'))
.then_some((name, false))
}
}
fn block_signatures(lines: &[&str]) -> Vec<Option<String>> {
let mut sigs: Vec<Option<String>> = vec![None; lines.len()];
let mut stack: Vec<(&str, usize)> = Vec::new(); for (i, line) in lines.iter().enumerate() {
match solo_tag_line(line) {
None => {}
Some((name, false)) => {
sigs[i] = Some(format!("\u{0}unmatched:{i}"));
stack.push((name, i));
}
Some((name, true)) => {
sigs[i] = Some(format!("\u{0}unmatched:{i}"));
if let Some(&(open_name, open_idx)) = stack.last()
&& open_name == name
{
stack.pop();
let block = lines[open_idx..=i].join("\n");
sigs[open_idx] = Some(block.clone());
sigs[i] = Some(block);
}
}
}
}
sigs
}
fn dedup_adjacent_lines(text: &str) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 2 {
return text.to_string();
}
let sigs = block_signatures(&lines);
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut collapsed = false;
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let mut n = 1;
if !line.trim().is_empty() {
while i + n < lines.len() && lines[i + n] == line && sigs[i + n] == sigs[i] {
n += 1;
}
}
if n > 1 {
collapsed = true;
out.push(format!("{line} [×{n}]"));
} else {
out.push(line.to_string());
}
i += n;
}
if !collapsed {
return text.to_string();
}
out.join("\n")
}
fn dedup_lines(text: &str, near: bool, max_dist: u32) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 2 {
return text.to_string();
}
let sigs = block_signatures(&lines);
let mut group_of: Vec<Option<usize>> = vec![None; lines.len()];
let mut reps: Vec<u64> = Vec::new(); let mut counts: Vec<usize> = Vec::new();
let mut exact: HashMap<(&str, Option<&str>), usize> = HashMap::new();
let hasher = make_simhasher();
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
continue; }
let sig = sigs[i].as_deref();
if let Some(&g) = exact.get(&(*line, sig)) {
group_of[i] = Some(g);
counts[g] += 1;
continue;
}
let sh = line_simhash(&hasher, line);
if near
&& sig.is_none()
&& let Some(g) = reps
.iter()
.position(|&rh| rh.hamming_distance(&sh) <= max_dist as usize)
{
group_of[i] = Some(g);
counts[g] += 1;
exact.insert((line, sig), g);
continue;
}
let g = reps.len();
reps.push(sh);
counts.push(1);
exact.insert((line, sig), g);
group_of[i] = Some(g);
}
if counts.iter().all(|&n| n <= 1) {
return text.to_string();
}
let mut emitted = vec![false; reps.len()];
let mut out: Vec<String> = Vec::with_capacity(lines.len());
for (i, line) in lines.iter().enumerate() {
match group_of[i] {
None => out.push((*line).to_string()),
Some(g) => {
if !emitted[g] {
emitted[g] = true;
let n = counts[g];
out.push(if n > 1 {
format!("{line} [×{n}]")
} else {
(*line).to_string()
});
}
}
}
}
out.join("\n")
}
pub(crate) fn make_simhasher() -> SimHash<SimSipHasher64, u64, 64> {
SimHash::<SimSipHasher64, u64, 64>::new(SimSipHasher64::new(1, 2))
}
fn line_simhash(hasher: &SimHash<SimSipHasher64, u64, 64>, s: &str) -> u64 {
let words = crate::llmtrim::stages::tools::lex_words(s);
if words.is_empty() {
return 0;
}
hasher.create_signature(words.iter())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llmtrim::ir::ProviderKind;
use crate::llmtrim::pipeline;
use crate::llmtrim::provider::OpenAiProvider;
use crate::llmtrim::tokenizer::counter_for;
use serde_json::json;
#[test]
fn exact_dedup_counts_repeats() {
let out = dedup_lines("a\nb\na\na", false, 0);
assert_eq!(out, "a [×3]\nb");
}
#[test]
fn blank_lines_are_preserved() {
let out = dedup_lines("x\n\nx\n\ny", false, 0);
assert_eq!(out, "x [×2]\n\n\ny");
}
#[test]
fn no_change_when_all_unique() {
let out = dedup_lines("alpha\nbeta\ngamma", false, 0);
assert_eq!(out, "alpha\nbeta\ngamma");
}
#[test]
fn unique_input_with_trailing_newline_is_returned_verbatim() {
let input = "alpha\nbeta\ngamma\n";
let out = dedup_lines(input, false, 0);
assert_eq!(
out, input,
"trailing newline preserved when nothing collapses"
);
let input2 = "one\ntwo\n\nthree\n";
assert_eq!(
dedup_lines(input2, false, 0),
input2,
"blank runs preserved too"
);
}
#[test]
fn simhash_distance_small_for_similar_large_for_different() {
let hasher = make_simhasher();
let a = line_simhash(&hasher, "the quick brown fox jumps over the lazy dog");
let b = line_simhash(&hasher, "the quick brown fox jumps over the lazy dogs");
let c = line_simhash(
&hasher,
"completely unrelated content about finance reports",
);
assert_eq!(a.hamming_distance(&a), 0);
assert!(
a.hamming_distance(&b) < a.hamming_distance(&c),
"near text is closer than unrelated"
);
}
#[test]
fn near_dedup_collapses_similar_lines() {
let text = "Connection retry attempt number one failed\n\
Connection retry attempt number two failed\n\
Connection retry attempt number three failed";
let exact = dedup_lines(text, false, 3);
assert_eq!(exact.lines().count(), 3, "exact mode keeps distinct lines");
let near = dedup_lines(text, true, 12);
assert!(
near.lines().count() < 3,
"near mode collapses the similar retry lines"
);
assert!(near.contains("[×"), "collapsed group carries a count");
}
#[test]
fn near_dedup_skips_structured_segments() {
let csv = "id,name,role\n1,Ann,Sales\n2,Bob,Sales\n3,Cy,Sales\n4,Di,Tech";
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":csv}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(DedupStage {
near: true,
near_max_distance: 12,
})];
let _ = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
let now = req.get_str("/messages/0/content").unwrap();
assert!(
now.contains("Bob") && now.contains("Cy") && now.contains("Di"),
"distinct CSV rows survive (near-dup disabled on structured data)"
);
assert!(!now.contains("[×"), "no near-dup collapse on a CSV");
}
#[test]
fn structured_segment_keeps_non_adjacent_duplicates() {
let csv = "id,name,role\n1,Ann,Sales\n2,Bob,Ops\n1,Ann,Sales\n3,Cy,Ops";
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":csv}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(DedupStage {
near: false,
near_max_distance: 0,
})];
let _ = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
let now = req.get_str("/messages/0/content").unwrap();
assert_eq!(
now, csv,
"non-adjacent duplicate rows in a CSV must not merge"
);
}
#[test]
fn adjacent_collapse_preserves_order() {
let log = "a,1\nb,2\nb,2\nb,2\nc,3";
let out = dedup_adjacent_lines(log);
assert_eq!(out, "a,1\nb,2 [×3]\nc,3");
let interleaved = "x\ny\nx\ny";
assert_eq!(dedup_adjacent_lines(interleaved), interleaved);
}
#[test]
fn differing_tag_blocks_keep_their_delimiters() {
let text = "<task-notification>\n\
<task-id>br0iqqk7p</task-id>\n\
<summary>Monitor event</summary>\n\
</task-notification>\n\
\n\
<task-notification>\n\
<task-id>br0iqqk7p</task-id>\n\
<status>completed</status>\n\
<summary>stream ended</summary>\n\
</task-notification>";
let out = dedup_lines(text, false, 0);
let opens = out.matches("<task-notification>").count();
let closes = out.matches("</task-notification>").count();
assert_eq!(opens, 2, "both open tags survive:\n{out}");
assert_eq!(closes, 2, "both close tags survive:\n{out}");
let second_open = out
.match_indices("<task-notification>")
.nth(1)
.expect("second open")
.0;
let second_close = out.rfind("</task-notification>").expect("second close");
let status = out.find("<status>completed</status>").expect("status kept");
assert!(
second_open < status && status < second_close,
"unique field stays inside second block's wrappers:\n{out}"
);
assert!(
out.contains("<task-id>br0iqqk7p</task-id> [×2]"),
"shared inner lines still fold:\n{out}"
);
let adj = dedup_adjacent_lines("<t>\n<t>\nx\n</t>\n</t>");
assert_eq!(
adj, "<t>\n<t>\nx\n</t>\n</t>",
"nested delimiters untouched"
);
}
#[test]
fn byte_identical_tag_blocks_still_fold() {
let block = "<note>\nsame body\n</note>";
let text = format!("{block}\n{block}");
let out = dedup_lines(&text, false, 0);
assert_eq!(out, "<note> [×2]\nsame body [×2]\n</note> [×2]");
}
#[test]
fn solo_tag_line_detection() {
assert_eq!(solo_tag_line("<name>"), Some(("name", false)));
assert_eq!(
solo_tag_line(" <a-b_c.d:e attr=\"1\"> "),
Some(("a-b_c.d:e", false))
);
assert_eq!(solo_tag_line("</name>"), Some(("name", true)));
assert_eq!(solo_tag_line("<x>y</x>"), None, "content on the line");
assert_eq!(
solo_tag_line("<x/>"),
None,
"self-closing is a whole element"
);
assert_eq!(solo_tag_line("<!-- c -->"), None);
assert_eq!(solo_tag_line("<?xml version=\"1.0\"?>"), None);
assert_eq!(solo_tag_line("plain text"), None);
assert_eq!(solo_tag_line("< name>"), None, "no space before name");
}
#[test]
fn stage_reduces_tokens_on_repetitive_content() {
let spam = std::iter::repeat_n("WARN cache miss for key user:session", 40)
.collect::<Vec<_>>()
.join("\n");
let body = json!({"model":"gpt-4o","messages":[{"role":"user","content":spam}]});
let mut req = Request::from_value(ProviderKind::OpenAi, body);
let counter = counter_for(ProviderKind::OpenAi, Some("gpt-4o")).unwrap();
let stages: Vec<Box<dyn Transform>> = vec![Box::new(DedupStage {
near: false,
near_max_distance: 3,
})];
let out = pipeline::run(&mut req, &OpenAiProvider, counter.as_ref(), &stages);
assert!(out.stages[0].applied);
assert!(out.input_tokens_after < out.input_tokens_before);
let now = req.get_str("/messages/0/content").unwrap();
assert!(now.contains("[×40]"), "40 identical lines collapse to one");
}
}