use std::borrow::Cow;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct SavingsRow {
pub baseline_tokens: u64,
pub actual_tokens: u64,
pub est_tokens_saved: u64,
pub baseline: &'static str,
}
pub(super) fn bytes_to_tokens(bytes: u64) -> u64 {
bytes / 4
}
fn tokens_for_text(text: &str) -> u64 {
super::tokens::count_tokens(text)
}
const GREP_READ_MULTIPLIER: u64 = 3;
const DEPENDENTS_READ_MULTIPLIER: u64 = 2;
const DOCUMENT_READ_MULTIPLIER: u64 = 5;
const LIST_FILES_READ_MULTIPLIER: u64 = 2;
const WEB_INGEST_MULTIPLIER: u64 = 3;
fn canonical_key(tool: &str) -> Cow<'_, str> {
if tool.contains(':') {
return Cow::Borrowed(tool);
}
let Some((domain, mode)) = tool.split_once('_') else {
return Cow::Borrowed(tool);
};
let known = super::mode::domain_modes()
.into_iter()
.any(|(d, modes)| d == domain && modes.contains(&mode));
if known {
Cow::Owned(format!("{domain}:{mode}"))
} else {
Cow::Borrowed(tool)
}
}
pub fn estimate_from_text(tool: &str, _corpus_bytes: u64, resp_text: &str) -> SavingsRow {
let actual = tokens_for_text(resp_text);
let (baseline, baseline_name) = match canonical_key(tool).as_ref() {
"code:outline" => (actual.saturating_mul(5), "full_file_read"),
"code:symbols" => (actual.saturating_mul(GREP_READ_MULTIPLIER), "grep_plus_read_top_hits"),
"code:references" | "code:callers" => (actual.saturating_mul(GREP_READ_MULTIPLIER), "grep_top_hits"),
"code:implementations" => (actual.saturating_mul(GREP_READ_MULTIPLIER), "grep_top_hits"),
"code:dependents" => (
actual.saturating_mul(DEPENDENTS_READ_MULTIPLIER),
"grep_imports_top_hits",
),
"git:churn" => (actual.saturating_mul(3), "git_log_per_file"),
"git:symbol_history" => (actual.saturating_mul(4), "per_commit_outline_diff"),
"code:grep" => (actual, "no_baseline"),
"graph:calls" | "graph:neighbors" | "graph:path" | "graph:subgraph" | "graph:communities" | "graph:map"
| "graph:export" | "graph:display" | "graph:open" => (actual, "no_baseline"),
"memory:documents" => (actual.saturating_mul(DOCUMENT_READ_MULTIPLIER), "full_document_read"),
"code:files" => (actual.saturating_mul(LIST_FILES_READ_MULTIPLIER), "find_plus_filter"),
"web:scrape" | "web:crawl" | "web:map" => (actual.saturating_mul(WEB_INGEST_MULTIPLIER), "manual_browse_paste"),
"memory:get"
| "memory:put"
| "memory:list"
| "memory:search"
| "memory:delete"
| "admin:telemetry"
| "admin:rescan"
| "admin:cache_stats"
| "admin:gc"
| "admin:cache_clear"
| "admin:status"
| "admin:repo"
| "workspace:workspaces"
| "workspace:worktrees"
| "workspace:branches"
| "workspace:claim"
| "workspace:release"
| "git:status"
| "git:recent"
| "git:touching"
| "git:by_path"
| "git:diff"
| "git:diff_outline"
| "git:blame"
| "git:blame_symbol"
| "git:search" => (actual, "no_baseline"),
_ => (actual, "unclassified"),
};
SavingsRow {
baseline_tokens: baseline,
actual_tokens: actual,
est_tokens_saved: baseline.saturating_sub(actual),
baseline: baseline_name,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_grep_model(s: &SavingsRow, expected_baseline: &str) {
assert_eq!(s.baseline, expected_baseline);
assert_eq!(s.baseline_tokens, s.actual_tokens.saturating_mul(GREP_READ_MULTIPLIER));
assert_eq!(s.est_tokens_saved, s.baseline_tokens.saturating_sub(s.actual_tokens));
}
#[test]
fn outline_baseline_is_5x_response() {
let s = estimate_from_text("code:outline", 1_000_000, &"a".repeat(400));
assert_eq!(s.baseline_tokens, s.actual_tokens.saturating_mul(5));
assert_eq!(s.baseline, "full_file_read");
#[cfg(not(feature = "documents"))]
{
assert_eq!(s.actual_tokens, 100);
assert_eq!(s.baseline_tokens, 500);
assert_eq!(s.est_tokens_saved, 400);
}
}
#[test]
fn search_symbols_savings_independent_of_corpus() {
let text = "a".repeat(400);
let big = estimate_from_text("code:symbols", 1_000_000, &text);
let empty = estimate_from_text("code:symbols", 0, &text);
assert_eq!(big.est_tokens_saved, empty.est_tokens_saved);
assert_grep_model(&big, "grep_plus_read_top_hits");
#[cfg(not(feature = "documents"))]
{
assert_eq!(big.actual_tokens, 100);
assert_eq!(big.baseline_tokens, 300);
assert_eq!(big.est_tokens_saved, 200);
}
}
#[test]
fn find_references_grep_baseline_floors_at_zero_for_empty_corpus() {
let s = estimate_from_text("code:references", 0, &"a".repeat(200));
assert_grep_model(&s, "grep_top_hits");
#[cfg(not(feature = "documents"))]
{
assert_eq!(s.actual_tokens, 50);
assert_eq!(s.baseline_tokens, 150);
assert_eq!(s.est_tokens_saved, 100);
}
}
#[test]
fn grep_savings_scale_with_response_not_corpus() {
let small = estimate_from_text("code:symbols", 1_000_000, &"word ".repeat(80));
let large = estimate_from_text("code:symbols", 1_000_000, &"word ".repeat(800));
assert!(
large.est_tokens_saved > small.est_tokens_saved,
"bigger response must yield bigger savings: {} !> {}",
large.est_tokens_saved,
small.est_tokens_saved
);
#[cfg(not(feature = "documents"))]
assert_eq!(large.est_tokens_saved, 2_000);
}
#[test]
fn no_baseline_tools_claim_zero_savings() {
for tool in [
"memory:get",
"memory:put",
"admin:status",
"admin:repo",
"admin:telemetry",
"admin:rescan",
"admin:cache_stats",
"workspace:worktrees",
"git:recent",
"git:touching",
"git:diff",
"git:blame",
"git:status",
"git:search",
"code:grep",
"code_grep",
"graph:calls",
"graph:display",
] {
let s = estimate_from_text(tool, 1_000_000, &"a".repeat(500));
assert_eq!(s.est_tokens_saved, 0, "{tool} must not claim savings");
assert_eq!(s.baseline, "no_baseline", "{tool} must label no_baseline");
}
}
#[test]
fn agent_tool_names_model_the_same_baseline_as_their_modes() {
let text = "a".repeat(400);
for (agent, mode) in [
("code_outline", "code:outline"),
("code_symbols", "code:symbols"),
("code_references", "code:references"),
("code_callers", "code:callers"),
("code_implementations", "code:implementations"),
("code_dependents", "code:dependents"),
("code_grep", "code:grep"),
("code_files", "code:files"),
("graph_calls", "graph:calls"),
("git_recent", "git:recent"),
("git_blame_symbol", "git:blame_symbol"),
("git_diff", "git:diff"),
] {
let via_agent = estimate_from_text(agent, 1_000_000, &text);
let via_mode = estimate_from_text(mode, 1_000_000, &text);
assert_eq!(
via_agent.baseline, via_mode.baseline,
"{agent} and {mode} must share a baseline"
);
assert_eq!(
via_agent.est_tokens_saved, via_mode.est_tokens_saved,
"{agent} and {mode} must estimate the same savings"
);
assert_ne!(
via_agent.baseline, "unclassified",
"{agent} must resolve to a real mode, not fall through"
);
}
}
#[test]
fn underscore_names_that_are_not_real_modes_stay_unclassified() {
for tool in ["shell_exec", "code_nonsense", "not_a_real_tool", "room_broadcast"] {
let s = estimate_from_text(tool, 1_000_000, &"a".repeat(400));
assert_eq!(s.baseline, "unclassified", "{tool} must not claim a baseline");
assert_eq!(s.est_tokens_saved, 0, "{tool} must not claim savings");
}
}
#[test]
fn search_documents_models_full_document_read_at_5x() {
let s = estimate_from_text("memory:documents", 1_000_000, &"a".repeat(400));
assert_eq!(s.baseline, "full_document_read");
assert_eq!(s.baseline_tokens, s.actual_tokens.saturating_mul(5));
assert_eq!(s.est_tokens_saved, s.baseline_tokens.saturating_sub(s.actual_tokens));
#[cfg(not(feature = "documents"))]
{
assert_eq!(s.actual_tokens, 100);
assert_eq!(s.baseline_tokens, 500);
assert_eq!(s.est_tokens_saved, 400);
}
}
#[test]
fn list_files_models_find_plus_filter_at_2x() {
let s = estimate_from_text("code:files", 1_000_000, &"a".repeat(400));
assert_eq!(s.baseline, "find_plus_filter");
assert_eq!(s.baseline_tokens, s.actual_tokens.saturating_mul(2));
assert_eq!(s.est_tokens_saved, s.baseline_tokens.saturating_sub(s.actual_tokens));
#[cfg(not(feature = "documents"))]
{
assert_eq!(s.actual_tokens, 100);
assert_eq!(s.baseline_tokens, 200);
assert_eq!(s.est_tokens_saved, 100);
}
}
#[test]
fn web_ingest_models_manual_browse_paste_at_3x() {
for tool in ["web:scrape", "web:crawl", "web:map"] {
let s = estimate_from_text(tool, 1_000_000, &"a".repeat(400));
assert_eq!(s.baseline, "manual_browse_paste", "{tool} baseline name");
assert_eq!(
s.baseline_tokens,
s.actual_tokens.saturating_mul(3),
"{tool} multiplier"
);
assert_eq!(
s.est_tokens_saved,
s.baseline_tokens.saturating_sub(s.actual_tokens),
"{tool} savings"
);
#[cfg(not(feature = "documents"))]
{
assert_eq!(s.actual_tokens, 100, "{tool} actual");
assert_eq!(s.baseline_tokens, 300, "{tool} baseline");
assert_eq!(s.est_tokens_saved, 200, "{tool} saved");
}
}
}
#[test]
fn unknown_tool_is_unclassified() {
let s = estimate_from_text("not_a_real_tool", 1_000_000, &"a".repeat(100));
assert_eq!(s.baseline, "unclassified");
assert_eq!(s.est_tokens_saved, 0);
}
#[cfg(not(feature = "documents"))]
#[test]
fn estimate_from_text_is_bytes_over_four_under_heuristic() {
let s = estimate_from_text("code:outline", 0, &"x".repeat(800));
assert_eq!(s.actual_tokens, 200);
assert_eq!(s.baseline_tokens, 1_000);
assert_eq!(s.est_tokens_saved, 800);
}
}