use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
pub const FORMAT_LEGEND: &str = include_str!("prompts/toon_legend.txt");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default = "DenseConfig::lossless")]
pub struct DenseConfig {
pub hygiene: bool,
pub serialize: bool,
pub output_control: bool,
pub serialize_min_rows: usize,
pub output_max_tokens: Option<u64>,
pub output_level: String,
pub output_token_budget: Option<u64>,
pub output_compact_code: bool,
pub output_frugal_tools: bool,
pub output_anti_overthink: bool,
pub serialize_nested: bool,
pub serialize_csv: bool,
pub serialize_flatten: bool,
pub serialize_buckets: bool,
pub json_crush: bool,
pub json_crush_max_rows: usize,
pub strip_base64: bool,
pub numeric_sig_figs: Option<u32>,
pub normalize_unicode: bool,
pub retrieve: bool,
pub retrieve_keep_ratio: f64,
pub retrieve_min_segment_chars: usize,
pub retrieve_reorder: bool,
pub retrieve_mmr: bool,
pub retrieve_mmr_lambda: f64,
pub retrieve_sentence: bool,
pub cache: bool,
pub cache_max_breakpoints: usize,
pub dedup: bool,
pub dedup_near: bool,
pub dedup_near_max_distance: u32,
pub ngram: bool,
pub ngram_max_entries: usize,
pub tool_select: bool,
pub tool_trim_desc: bool,
pub tool_minify_schema: bool,
pub tool_max_desc_chars: usize,
pub toolout: bool,
pub toolout_max_lines: usize,
pub toolout_min_lines: usize,
pub toolout_template: bool,
pub toolout_mode: String,
pub skeletonize: bool,
pub skeleton_keep_full_top_k: usize,
pub skeleton_drop_unmatched: bool,
pub skeleton_drop_min_body_lines: usize,
pub minify_code: bool,
pub multimodal: bool,
pub image_detail: Option<String>,
pub auto: bool,
pub memo: bool,
pub quality_gate: bool,
}
impl Default for DenseConfig {
fn default() -> Self {
Self::auto()
}
}
impl DenseConfig {
pub fn lossless() -> Self {
Self {
hygiene: true,
serialize: true,
output_control: false,
serialize_min_rows: 2,
output_max_tokens: None,
output_level: "terse".to_string(),
output_token_budget: None,
output_compact_code: false,
output_frugal_tools: false,
output_anti_overthink: false,
serialize_nested: true,
serialize_csv: false,
serialize_flatten: false,
serialize_buckets: false,
json_crush: false,
json_crush_max_rows: 50,
strip_base64: false,
numeric_sig_figs: None,
normalize_unicode: false,
retrieve: false,
retrieve_keep_ratio: 0.5,
retrieve_min_segment_chars: 600,
retrieve_reorder: false,
retrieve_mmr: false,
retrieve_mmr_lambda: 0.5,
retrieve_sentence: false,
cache: false,
cache_max_breakpoints: 4,
dedup: true,
dedup_near: false,
dedup_near_max_distance: 3,
ngram: false,
ngram_max_entries: 32,
tool_select: false,
tool_trim_desc: false,
tool_minify_schema: false,
tool_max_desc_chars: 300,
toolout: false,
toolout_max_lines: 40,
toolout_min_lines: 20,
toolout_template: true,
toolout_mode: "auto".to_string(),
skeletonize: false,
skeleton_keep_full_top_k: 5,
skeleton_drop_unmatched: false,
skeleton_drop_min_body_lines: 8,
minify_code: false,
multimodal: false,
image_detail: None,
auto: false,
memo: true,
quality_gate: true,
}
}
pub fn load() -> Result<Self> {
if let Some(name) = std::env::var("LLMTRIM_PRESET")
.ok()
.filter(|s| !s.is_empty())
{
return Self::preset(&name).with_context(|| {
format!("unknown LLMTRIM_PRESET '{name}' (auto|safe|rag|agent|code|aggressive|cache|reasoning)")
});
}
let Some(path) = config_path().filter(|p| p.exists()) else {
return Ok(Self::auto());
};
let text = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read {}", path.display()))?;
let value: toml::Value =
toml::from_str(&text).with_context(|| format!("failed to parse {}", path.display()))?;
Self::from_toml_value(value).with_context(|| format!("invalid config {}", path.display()))
}
fn from_toml_value(value: toml::Value) -> Result<Self> {
if let Some(name) = value.get("preset").and_then(toml::Value::as_str) {
return Self::preset(name).with_context(|| format!("unknown preset '{name}'"));
}
if let Some(table) = value.as_table()
&& !table
.keys()
.any(|k| !RUNTIME_ONLY_KEYS.contains(&k.as_str()) && k != "preset")
{
return Ok(Self::auto());
}
value.try_into().context("config does not match the schema")
}
pub fn load_for_interceptor() -> Self {
Self::load().unwrap_or_else(|e| {
eprintln!("llmtrim: {e}; using shape-routing defaults");
Self::auto()
})
}
pub fn auto() -> Self {
Self {
auto: true,
..Self::lossless()
}
}
pub fn preset(name: &str) -> Option<Self> {
let mut c = Self::lossless();
match name.to_ascii_lowercase().as_str() {
"safe" | "lossless" => {}
"auto" => c.auto = true,
"frugal" => c.output_frugal_tools = true,
"rag" => {
c.retrieve = true;
c.retrieve_sentence = true;
c.retrieve_keep_ratio = 0.35;
c.toolout = true;
c.json_crush = true;
c.output_control = true;
c.multimodal = true;
c.strip_base64 = true;
c.output_anti_overthink = true;
}
"agent" => {
c.tool_select = true;
c.tool_trim_desc = true;
c.tool_minify_schema = true;
c.cache = true;
c.toolout = true; c.serialize_flatten = true; c.serialize_buckets = true; c.json_crush = true; c.output_control = true;
c.multimodal = true; c.strip_base64 = true; c.output_frugal_tools = true;
c.output_anti_overthink = true;
}
"code" => {
c.skeletonize = true;
c.minify_code = true;
c.toolout = true;
c.json_crush = true;
c.output_control = true;
c.multimodal = true; c.strip_base64 = true; c.output_anti_overthink = true; }
"aggressive" => {
c.retrieve = true;
c.retrieve_sentence = true;
c.retrieve_keep_ratio = 0.35;
c.skeletonize = true;
c.skeleton_drop_unmatched = true;
c.minify_code = true;
c.dedup_near = true;
c.ngram = true;
c.normalize_unicode = true;
c.tool_select = true;
c.tool_trim_desc = true;
c.tool_minify_schema = true; c.cache = true;
c.toolout = true; c.serialize_flatten = true;
c.serialize_buckets = true;
c.json_crush = true;
c.output_control = true;
c.multimodal = true; c.strip_base64 = true; c.output_anti_overthink = true; }
"cache" => {
c.cache = true;
}
"reasoning" => {
c.output_control = true;
c.output_level = "draft".to_string();
}
_ => return None,
}
Some(c)
}
}
fn config_path() -> Option<PathBuf> {
if let Ok(p) = std::env::var("LLMTRIM_CONFIG") {
return Some(PathBuf::from(p));
}
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.ok()
.or_else(|| {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(|h| PathBuf::from(h).join(".config"))
})?;
Some(base.join("llmtrim").join("config.toml"))
}
pub(crate) const RUNTIME_ONLY_KEYS: &[&str] = &[
"extra_hosts",
"exclude_providers",
"exclude_hosts",
"upstream_proxy",
"capture_dir",
"db_path",
"no_update_check",
"bind",
"capture_max_mb",
"breakdown_window",
"retention_days",
"max_rows",
"max_breakdown_turns",
"theme",
"sub",
"compact",
];
pub fn config_file_path() -> Option<PathBuf> {
config_path()
}
pub fn write_sub_mapping(
provider: &str,
tiers: &std::collections::BTreeMap<String, String>,
) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
write_sub_mapping_at(&path, provider, tiers)
}
pub fn disable_sub() -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
disable_sub_at(&path)
}
fn disable_sub_at(path: &std::path::Path) -> Result<()> {
edit_sub_table_at(path, |t| {
if let Some(active) = t.get("active").and_then(toml::Value::as_str)
&& active != "off"
&& !active.is_empty()
{
let last = active.to_string();
t.insert("last".to_string(), toml::Value::String(last));
}
t.insert("active".to_string(), toml::Value::String("off".to_string()));
})
}
pub fn enable_sub(provider: &str) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
enable_sub_at(&path, provider)
}
fn enable_sub_at(path: &std::path::Path, provider: &str) -> Result<()> {
let provider = provider.to_string();
edit_sub_table_at(path, |t| {
t.insert("active".to_string(), toml::Value::String(provider));
})
}
pub fn sub_reenable_provider() -> Option<String> {
sub_reenable_provider_at(&config_path()?)
}
fn sub_reenable_provider_at(path: &std::path::Path) -> Option<String> {
let doc: toml::Value = toml::from_str(&std::fs::read_to_string(path).ok()?).ok()?;
let sub = doc.get("sub")?;
if let Some(s) = sub.as_str() {
return (s != "off" && !s.is_empty()).then(|| s.trim().to_ascii_lowercase());
}
let clean = |s: &str| {
let s = s.trim();
(s != "off" && !s.is_empty()).then(|| s.to_ascii_lowercase())
};
sub.get("active")
.and_then(toml::Value::as_str)
.and_then(clean)
.or_else(|| {
sub.get("last")
.and_then(toml::Value::as_str)
.and_then(clean)
})
}
fn write_sub_mapping_at(
path: &std::path::Path,
provider: &str,
tiers: &std::collections::BTreeMap<String, String>,
) -> Result<()> {
let mut tiers_tbl = toml::Table::new();
for (k, v) in tiers {
tiers_tbl.insert(k.clone(), toml::Value::String(v.clone()));
}
let provider = provider.to_string();
edit_sub_table_at(path, |t| {
t.insert("active".to_string(), toml::Value::String(provider.clone()));
let mut prov_tbl = toml::Table::new();
prov_tbl.insert("tiers".to_string(), toml::Value::Table(tiers_tbl));
t.insert(provider, toml::Value::Table(prov_tbl));
})
}
fn edit_sub_table_at(path: &std::path::Path, edit: impl FnOnce(&mut toml::Table)) -> Result<()> {
use anyhow::Context;
let existing = std::fs::read_to_string(path).unwrap_or_default();
let mut doc: toml::Table = if existing.trim().is_empty() {
toml::Table::new()
} else {
existing
.parse()
.context("existing config is not valid TOML")?
};
let mut sub_tbl = match doc.remove("sub") {
Some(toml::Value::Table(t)) => t,
Some(toml::Value::String(s)) if s != "off" && !s.is_empty() => {
let mut t = toml::Table::new();
t.insert("active".to_string(), toml::Value::String(s));
t
}
_ => toml::Table::new(),
};
edit(&mut sub_tbl);
doc.insert("sub".to_string(), toml::Value::Table(sub_tbl));
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating config dir {}", parent.display()))?;
}
let serialized = toml::to_string_pretty(&doc).context("serializing config")?;
std::fs::write(path, serialized).with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn write_sub_mode(fallback: bool) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
let mode = if fallback { "fallback" } else { "always" };
edit_sub_table_at(&path, |t| {
t.insert("mode".to_string(), toml::Value::String(mode.to_string()));
})
}
pub fn write_sub_chain(providers: &[String]) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
let values = providers
.iter()
.map(|p| toml::Value::String(p.trim().to_ascii_lowercase()))
.collect();
edit_sub_table_at(&path, |t| {
t.insert("chain".to_string(), toml::Value::Array(values));
})
}
pub fn write_sub_effort(provider: &str, level: &str) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
let (provider, level) = (provider.to_string(), level.to_ascii_lowercase());
edit_sub_table_at(&path, |t| {
let prov = t
.entry(provider)
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
let Some(prov) = prov.as_table_mut() else {
return;
};
if level == "none" || level.is_empty() {
prov.remove("effort");
} else {
prov.insert("effort".to_string(), toml::Value::String(level));
}
})
}
pub fn write_sub_map_entry(provider: &str, from: &str, to: &str) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
let (provider, from, to) = (
provider.to_string(),
from.to_ascii_lowercase(),
to.to_string(),
);
edit_sub_table_at(&path, |t| {
let prov = t
.entry(provider)
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
let Some(prov) = prov.as_table_mut() else {
return;
};
let tiers = prov
.entry("tiers".to_string())
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
if let Some(tiers) = tiers.as_table_mut() {
tiers.insert(from, toml::Value::String(to));
}
})
}
pub fn remove_sub_map_entry(provider: &str, from: &str) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
let (provider, from) = (provider.to_string(), from.to_ascii_lowercase());
edit_sub_table_at(&path, |t| {
if let Some(tiers) = t
.get_mut(&provider)
.and_then(toml::Value::as_table_mut)
.and_then(|p| p.get_mut("tiers"))
.and_then(toml::Value::as_table_mut)
{
tiers.remove(&from);
}
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RuntimeConfig {
pub extra_hosts: Vec<String>,
pub upstream_proxy: Option<String>,
pub capture_dir: Option<PathBuf>,
pub db_path: Option<PathBuf>,
pub no_update_check: bool,
pub bind: Option<String>,
pub capture_max_mb: Option<u64>,
pub breakdown_window: Option<i64>,
pub retention_days: Option<i64>,
pub max_rows: Option<i64>,
pub max_breakdown_turns: Option<i64>,
pub theme: Option<String>,
pub sub: Option<String>,
pub sub_tiers: std::collections::BTreeMap<String, String>,
pub sub_effort: Option<String>,
pub sub_fallback: bool,
pub sub_chain: Vec<String>,
pub sub_codex_previous_response_id: bool,
pub compact_models: Vec<String>,
}
impl RuntimeConfig {
pub fn get() -> &'static RuntimeConfig {
static CACHE: std::sync::OnceLock<RuntimeConfig> = std::sync::OnceLock::new();
CACHE.get_or_init(Self::load)
}
fn load() -> RuntimeConfig {
Self::resolve(|k| std::env::var(k).ok(), cached_config_file())
}
fn resolve(env: impl Fn(&str) -> Option<String>, file: Option<&toml::Value>) -> RuntimeConfig {
let env_set = |k: &str| env(k).filter(|s| !s.is_empty());
let fstr = |key: &str| {
file.and_then(|v| v.get(key))
.and_then(toml::Value::as_str)
.map(str::to_string)
};
let fint = |key: &str| {
file.and_then(|v| v.get(key))
.and_then(toml::Value::as_integer)
};
let fbool = |key: &str| file.and_then(|v| v.get(key)).and_then(toml::Value::as_bool);
let positive = |v: Option<i64>| v.filter(|n| *n > 0);
let extra_hosts = resolve_str_list(
env_set("LLMTRIM_EXTRA_HOSTS"),
file,
"extra_hosts",
normalize_host,
);
RuntimeConfig {
extra_hosts,
upstream_proxy: env_set("LLMTRIM_UPSTREAM_PROXY").or_else(|| fstr("upstream_proxy")),
capture_dir: env_set("LLMTRIM_CAPTURE_DIR")
.or_else(|| fstr("capture_dir"))
.map(PathBuf::from),
db_path: env_set("LLMTRIM_DB_PATH")
.or_else(|| fstr("db_path"))
.map(PathBuf::from),
no_update_check: env("LLMTRIM_NO_UPDATE_CHECK").is_some()
|| fbool("no_update_check").unwrap_or(false),
bind: env_set("LLMTRIM_BIND").or_else(|| fstr("bind")),
capture_max_mb: env_set("LLMTRIM_CAPTURE_MAX_MB")
.and_then(|s| s.trim().parse::<u64>().ok())
.or_else(|| fint("capture_max_mb").and_then(|n| u64::try_from(n).ok())),
breakdown_window: positive(
env_set("LLMTRIM_BREAKDOWN_WINDOW")
.and_then(|s| s.trim().parse::<i64>().ok())
.or_else(|| fint("breakdown_window")),
),
retention_days: positive(
env_set("LLMTRIM_RETENTION_DAYS")
.and_then(|s| s.trim().parse::<i64>().ok())
.or_else(|| fint("retention_days")),
),
max_rows: positive(
env_set("LLMTRIM_MAX_ROWS")
.and_then(|s| s.trim().parse::<i64>().ok())
.or_else(|| fint("max_rows")),
),
max_breakdown_turns: positive(
env_set("LLMTRIM_MAX_BREAKDOWN_TURNS")
.and_then(|s| s.trim().parse::<i64>().ok())
.or_else(|| fint("max_breakdown_turns")),
),
theme: env_set("LLMTRIM_THEME").or_else(|| fstr("theme")),
sub: {
let active = resolve_sub_provider(&env, file);
active.filter(|s| s != "off" && !s.is_empty())
},
sub_tiers: resolve_sub_tiers(&env, file),
sub_fallback: resolve_sub_fallback(&env, file),
sub_chain: resolve_sub_chain(&env, file),
sub_effort: resolve_sub_effort(&env, file),
sub_codex_previous_response_id: resolve_sub_codex_continuation(&env, file),
compact_models: resolve_compact_models(file),
}
}
}
fn resolve_sub_provider(
env: &impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> Option<String> {
if let Some(v) = env("LLMTRIM_SUB").filter(|s| !s.is_empty()) {
return Some(v.trim().to_ascii_lowercase());
}
let sub = file?.get("sub")?;
let s = sub.as_str().or_else(|| {
sub.get("active")
.or_else(|| sub.get("provider"))
.and_then(toml::Value::as_str)
})?;
Some(s.trim().to_ascii_lowercase())
}
fn resolve_sub_tiers(
env: &impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> std::collections::BTreeMap<String, String> {
let Some(provider) = resolve_sub_provider(env, file) else {
return std::collections::BTreeMap::new();
};
sub_tiers_from_file(file, &provider)
}
pub fn sub_tiers_for(provider: &str) -> std::collections::BTreeMap<String, String> {
let provider = provider.trim().to_ascii_lowercase();
if provider.is_empty() || provider == "off" {
return std::collections::BTreeMap::new();
}
let file = config_path()
.filter(|p| p.exists())
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| toml::from_str::<toml::Value>(&s).ok());
sub_tiers_from_file(file.as_ref(), &provider)
}
fn sub_tiers_from_file(
file: Option<&toml::Value>,
provider: &str,
) -> std::collections::BTreeMap<String, String> {
let mut map = std::collections::BTreeMap::new();
let Some(file) = file else {
return map;
};
if let Some(tiers) = file
.get("sub")
.and_then(|v| v.get(provider))
.and_then(|v| v.get("tiers"))
.and_then(toml::Value::as_table)
{
for (k, v) in tiers {
if let Some(model) = v.as_str() {
map.insert(k.to_ascii_lowercase(), model.to_string());
}
}
}
map
}
fn resolve_sub_effort(
env: &impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> Option<String> {
let clean = |s: String| {
let s = s.trim().to_ascii_lowercase();
(!s.is_empty() && s != "none").then_some(s)
};
if let Some(v) = env("LLMTRIM_CODEX_EFFORT").filter(|s| !s.is_empty()) {
return clean(v);
}
let provider = resolve_sub_provider(env, file)?;
file?
.get("sub")
.and_then(|v| v.get(&provider))
.and_then(|v| v.get("effort"))
.and_then(toml::Value::as_str)
.map(str::to_string)
.and_then(clean)
}
fn parse_sub_mode(raw: &str) -> Option<bool> {
match raw.trim().to_ascii_lowercase().as_str() {
"always" => Some(false),
"fallback" => Some(true),
"on_error" | "on-error" | "onerror" => {
eprintln!(
"llmtrim: sub mode '{}' is the old name for 'fallback' — run `llmtrim sub mode fallback` to update the config",
raw.trim()
);
Some(true)
}
_ => None,
}
}
fn resolve_sub_fallback(env: &impl Fn(&str) -> Option<String>, file: Option<&toml::Value>) -> bool {
let resolve = |raw: &str, source: &str| {
parse_sub_mode(raw).unwrap_or_else(|| {
eprintln!(
"llmtrim: unknown sub mode '{}' in {source} — using 'always' (expected always|fallback)",
raw.trim()
);
false
})
};
if let Some(v) = env("LLMTRIM_SUB_MODE").filter(|s| !s.is_empty()) {
return resolve(&v, "LLMTRIM_SUB_MODE");
}
file.and_then(|v| v.get("sub"))
.and_then(|v| v.get("mode"))
.and_then(toml::Value::as_str)
.map(|v| resolve(v, "[sub] mode"))
.unwrap_or(false)
}
fn resolve_sub_chain(
env: &impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> Vec<String> {
let parse = |raw: &str| {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|s| s.to_ascii_lowercase())
.collect::<Vec<_>>()
};
if let Some(v) = env("LLMTRIM_SUB_CHAIN").filter(|s| !s.is_empty()) {
return parse(&v);
}
let sub = file.and_then(|v| v.get("sub"));
let mut chain = sub
.and_then(|v| v.get("chain"))
.map(|v| match v {
toml::Value::Array(values) => values
.iter()
.filter_map(toml::Value::as_str)
.flat_map(parse)
.collect(),
toml::Value::String(s) => parse(s),
_ => Vec::new(),
})
.unwrap_or_default();
if chain.is_empty()
&& let Some(active) = resolve_sub_provider(env, file)
&& active != "off"
{
chain.push(active);
}
chain
}
fn resolve_sub_codex_continuation(
env: &impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> bool {
let is_true = |s: &str| {
matches!(
s.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
};
if let Some(v) = env("LLMTRIM_CODEX_PREVIOUS_RESPONSE_ID").filter(|s| !s.is_empty()) {
return is_true(&v);
}
if let Some(sub) = file.and_then(|v| v.get("sub"))
&& let Some(c) = sub.get("codex").or_else(|| sub.get("chatgpt"))
{
if let Some(b) = c
.get("previous_response_id")
.or_else(|| c.get("continuation"))
.and_then(toml::Value::as_bool)
{
return b;
}
if let Some(s) = c
.get("previous_response_id")
.or_else(|| c.get("continuation"))
.and_then(toml::Value::as_str)
{
return is_true(s);
}
}
false
}
fn resolve_compact_models(file: Option<&toml::Value>) -> Vec<String> {
let Some(values) = file
.and_then(|v| v.get("compact"))
.and_then(|v| v.get("models"))
.and_then(toml::Value::as_array)
else {
return Vec::new();
};
let mut models = Vec::new();
for value in values {
let Some(model) = value.as_str().map(str::trim).filter(|s| !s.is_empty()) else {
continue;
};
if !models.iter().any(|existing| existing == model) {
models.push(model.to_string());
}
}
models
}
pub fn write_compact_models(models: &[String]) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
write_compact_models_at(&path, models)
}
pub fn compact_models_configured() -> bool {
let Some(path) = config_path() else {
return false;
};
std::fs::read_to_string(path)
.ok()
.and_then(|text| text.parse::<toml::Value>().ok())
.and_then(|doc| doc.get("compact").cloned())
.and_then(|compact| compact.get("models").cloned())
.is_some()
}
fn write_compact_models_at(path: &std::path::Path, models: &[String]) -> Result<()> {
use anyhow::Context;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating config dir {}", parent.display()))?;
}
let values: Vec<String> = models
.iter()
.map(|m| m.trim())
.filter(|m| !m.is_empty())
.fold(Vec::new(), |mut out, m| {
if !out.iter().any(|existing| existing == m) {
out.push(m.to_string());
}
out
});
let line = format!(
"models = [{}]",
values
.iter()
.map(|m| format!("{m:?}"))
.collect::<Vec<_>>()
.join(", ")
);
let existing = std::fs::read_to_string(path).unwrap_or_default();
let mut in_compact = false;
let mut compact_found = false;
let mut replaced = false;
let mut out = Vec::new();
for raw in existing.lines() {
let trimmed = raw.trim();
let section = trimmed.split('#').next().unwrap_or_default().trim();
if section.starts_with('[') {
if in_compact && !replaced {
out.push(line.clone());
replaced = true;
}
in_compact = section == "[compact]";
compact_found |= in_compact;
}
if in_compact
&& trimmed
.split('=')
.next()
.is_some_and(|key| key.trim() == "models")
{
out.push(line.clone());
replaced = true;
} else {
out.push(raw.to_string());
}
}
if !replaced {
if !out.is_empty() && !out.last().is_some_and(|existing| existing.is_empty()) {
out.push(String::new());
}
if !compact_found {
out.push("[compact]".to_string());
}
out.push(line);
}
std::fs::write(path, format!("{}\n", out.join("\n")))
.with_context(|| format!("writing {}", path.display()))
}
pub fn save_theme(name: &str) -> Result<()> {
let path = config_path().ok_or_else(|| anyhow::anyhow!("no config path (HOME/XDG unset)"))?;
save_theme_at(&path, name)
}
fn save_theme_at(path: &std::path::Path, name: &str) -> Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)
.with_context(|| format!("failed to create {}", dir.display()))?;
}
let existing = std::fs::read_to_string(path).unwrap_or_default();
let line = format!("theme = \"{name}\"");
let mut replaced = false;
let mut out: Vec<String> = existing
.lines()
.map(|l| {
if l.split('=').next().is_some_and(|k| k.trim() == "theme") {
replaced = true;
line.clone()
} else {
l.to_string()
}
})
.collect();
if !replaced {
out.push(line);
}
let mut text = out.join("\n");
text.push('\n');
std::fs::write(path, text).with_context(|| format!("failed to write {}", path.display()))
}
fn canon_provider(raw: &str) -> Option<String> {
crate::llmtrim::ir::ProviderKind::from_str(raw.trim())
.ok()
.map(|k| k.as_str().to_string())
}
fn resolve_str_list(
env_value: Option<String>,
file: Option<&toml::Value>,
file_key: &str,
norm: fn(&str) -> Option<String>,
) -> Vec<String> {
let raw: Vec<String> = match env_value {
Some(s) => s.split(',').map(str::to_string).collect(),
None => file
.and_then(|v| v.get(file_key))
.and_then(toml::Value::as_array)
.map(|a| {
a.iter()
.filter_map(|e| e.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default(),
};
let mut out: Vec<String> = raw.iter().filter_map(|s| norm(s)).collect();
out.sort();
out.dedup();
out
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Exclusions {
pub providers: Vec<String>,
pub hosts: Vec<String>,
}
fn resolve_exclusions(
env: impl Fn(&str) -> Option<String>,
file: Option<&toml::Value>,
) -> Exclusions {
let env_set = |k: &str| env(k).filter(|s| !s.is_empty());
Exclusions {
providers: resolve_str_list(
env_set("LLMTRIM_EXCLUDE_PROVIDERS"),
file,
"exclude_providers",
canon_provider,
),
hosts: resolve_str_list(
env_set("LLMTRIM_EXCLUDE_HOSTS"),
file,
"exclude_hosts",
normalize_host,
),
}
}
pub fn exclusions() -> &'static Exclusions {
static CACHE: std::sync::OnceLock<Exclusions> = std::sync::OnceLock::new();
CACHE.get_or_init(|| resolve_exclusions(|k| std::env::var(k).ok(), cached_config_file()))
}
fn cached_config_file() -> Option<&'static toml::Value> {
static FILE: std::sync::OnceLock<Option<toml::Value>> = std::sync::OnceLock::new();
FILE.get_or_init(load_config_file).as_ref()
}
fn load_config_file() -> Option<toml::Value> {
config_path()
.filter(|p| p.exists())
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|t| toml::from_str::<toml::Value>(&t).ok())
}
fn normalize_host(raw: &str) -> Option<String> {
let h = raw.trim().trim_end_matches('.').to_ascii_lowercase();
if h.is_empty()
|| h.starts_with('.')
|| h.starts_with('-')
|| !h.contains('.')
|| h.contains(['/', ':', ' ', '\t', '*', '@', '?'])
{
return None;
}
if !h
.split('.')
.all(|l| !l.is_empty() && l.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-'))
{
return None;
}
if h.split('.').all(|l| l.bytes().all(|b| b.is_ascii_digit())) {
return None;
}
Some(h)
}
pub fn retention_days() -> Option<i64> {
RuntimeConfig::get().retention_days
}
pub fn max_rows() -> Option<i64> {
RuntimeConfig::get().max_rows
}
pub fn max_breakdown_turns() -> Option<i64> {
RuntimeConfig::get().max_breakdown_turns
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legend_is_embedded_and_nonempty() {
assert!(!FORMAT_LEGEND.trim().is_empty());
assert!(FORMAT_LEGEND.contains("TOON"));
}
#[test]
fn sub_reads_bare_string_and_env_wins() {
let file: toml::Value = toml::from_str("sub = \"codex\"").unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("codex")
);
let env = |k: &str| (k == "LLMTRIM_SUB").then(|| "kimi".to_string());
assert_eq!(
resolve_sub_provider(&env, Some(&file)).as_deref(),
Some("kimi")
);
}
#[test]
fn sub_reads_table_form_and_tiers() {
let file: toml::Value = toml::from_str(
"[sub]\nactive = \"codex\"\n[sub.codex.tiers]\nopus = \"gpt-5.5\"\nsonnet = \"gpt-5.4\"\n",
)
.unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("codex")
);
let tiers = resolve_sub_tiers(&no_env, Some(&file));
assert_eq!(tiers.get("opus").map(String::as_str), Some("gpt-5.5"));
assert_eq!(tiers.get("sonnet").map(String::as_str), Some("gpt-5.4"));
}
#[test]
fn write_then_read_sub_mapping_round_trips() {
let dir = std::env::temp_dir().join(format!("llmtrim-sub-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(&path, "preset = \"auto\"\n").unwrap();
let mut tiers = std::collections::BTreeMap::new();
tiers.insert("opus".to_string(), "gpt-5.5".to_string());
tiers.insert("haiku".to_string(), "gpt-5.4-mini".to_string());
write_sub_mapping_at(&path, "codex", &tiers).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let file: toml::Value = toml::from_str(&text).unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("codex")
);
let read = resolve_sub_tiers(&no_env, Some(&file));
assert_eq!(read.get("opus").map(String::as_str), Some("gpt-5.5"));
assert_eq!(read.get("haiku").map(String::as_str), Some("gpt-5.4-mini"));
assert_eq!(
file.get("preset").and_then(toml::Value::as_str),
Some("auto")
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sub_off_on_cycle_preserves_provider_and_mapping() {
let dir = std::env::temp_dir().join(format!("llmtrim-sub-onoff-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
let mut tiers = std::collections::BTreeMap::new();
tiers.insert("opus".to_string(), "gpt-5.5-custom".to_string());
write_sub_mapping_at(&path, "codex", &tiers).unwrap();
assert_eq!(sub_reenable_provider_at(&path).as_deref(), Some("codex"));
disable_sub_at(&path).unwrap();
let no_env = |_: &str| None;
let file: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("off")
);
assert_eq!(sub_reenable_provider_at(&path).as_deref(), Some("codex"));
let restore = sub_reenable_provider_at(&path).unwrap();
enable_sub_at(&path, &restore).unwrap();
let file: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("codex")
);
let read = resolve_sub_tiers(&no_env, Some(&file));
assert_eq!(read.get("opus").map(String::as_str), Some("gpt-5.5-custom"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sub_reenable_none_when_never_enabled() {
let dir = std::env::temp_dir().join(format!("llmtrim-sub-none-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(&path, "preset = \"auto\"\n").unwrap();
assert_eq!(sub_reenable_provider_at(&path), None);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn codex_continuation_is_off_unless_asked_for() {
let no_env = |_: &str| None;
assert!(!resolve_sub_codex_continuation(&no_env, None));
let plain: toml::Value = toml::from_str("sub = \"codex\"").unwrap();
assert!(!resolve_sub_codex_continuation(&no_env, Some(&plain)));
let on: toml::Value = toml::from_str("[sub.codex]\nprevious_response_id = true\n").unwrap();
assert!(resolve_sub_codex_continuation(&no_env, Some(&on)));
let env_on = |k: &str| (k == "LLMTRIM_CODEX_PREVIOUS_RESPONSE_ID").then(|| "1".to_string());
assert!(resolve_sub_codex_continuation(&env_on, None));
let env_off =
|k: &str| (k == "LLMTRIM_CODEX_PREVIOUS_RESPONSE_ID").then(|| "0".to_string());
assert!(!resolve_sub_codex_continuation(&env_off, Some(&on)));
}
#[test]
fn sub_fallback_env_wins_over_file_mode() {
let file: toml::Value =
toml::from_str("[sub]\nactive = \"codex\"\nmode = \"fallback\"\n").unwrap();
let no_env = |_: &str| None;
assert!(resolve_sub_fallback(&no_env, Some(&file)));
let plain: toml::Value = toml::from_str("sub = \"codex\"").unwrap();
assert!(!resolve_sub_fallback(&no_env, Some(&plain)));
let env_always = |k: &str| (k == "LLMTRIM_SUB_MODE").then(|| "always".to_string());
assert!(!resolve_sub_fallback(&env_always, Some(&file)));
let env_fallback = |k: &str| (k == "LLMTRIM_SUB_MODE").then(|| "fallback".to_string());
assert!(resolve_sub_fallback(&env_fallback, Some(&plain)));
let env_unknown = |k: &str| (k == "LLMTRIM_SUB_MODE").then(|| "wat".to_string());
assert!(!resolve_sub_fallback(&env_unknown, Some(&plain)));
}
#[test]
fn sub_mode_legacy_on_error_still_means_fallback() {
let no_env = |_: &str| None;
for legacy in ["on_error", "on-error", "onerror"] {
let file: toml::Value =
toml::from_str(&format!("[sub]\nactive = \"codex\"\nmode = \"{legacy}\"\n"))
.unwrap();
assert!(resolve_sub_fallback(&no_env, Some(&file)), "{legacy}");
}
let env = |k: &str| (k == "LLMTRIM_SUB_MODE").then(|| "on-error".to_string());
assert!(resolve_sub_fallback(&env, None));
}
#[test]
fn sub_chain_reads_order_and_falls_back_to_active() {
let file: toml::Value =
toml::from_str("[sub]\nactive = \"codex\"\nchain = [\"kimi\", \"codex\"]\n").unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_chain(&no_env, Some(&file)),
vec!["kimi".to_string(), "codex".to_string()]
);
let plain: toml::Value = toml::from_str("sub = \"kimi\"").unwrap();
assert_eq!(resolve_sub_chain(&no_env, Some(&plain)), vec!["kimi"]);
let env =
|key: &str| (key == "LLMTRIM_SUB_CHAIN").then(|| "codex, kimi, codex".to_string());
assert_eq!(
resolve_sub_chain(&env, Some(&file)),
vec!["codex".to_string(), "kimi".to_string(), "codex".to_string()]
);
}
#[test]
fn granular_sub_editors_preserve_active_mode_and_entries() {
let dir = std::env::temp_dir().join(format!("llmtrim-sub-edit-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
let mut tiers = std::collections::BTreeMap::new();
tiers.insert("opus".to_string(), "gpt-5.5".to_string());
write_sub_mapping_at(&path, "codex", &tiers).unwrap();
edit_sub_table_at(&path, |t| {
t.insert(
"mode".to_string(),
toml::Value::String("fallback".to_string()),
);
})
.unwrap();
let (provider, from, to) = ("codex", "claude-sonnet-4", "gpt-5.4");
let mut tt = std::collections::BTreeMap::new();
tt.insert(from.to_string(), to.to_string());
edit_sub_table_at(&path, |t| {
let prov = t
.entry(provider.to_string())
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
let tiers = prov
.as_table_mut()
.unwrap()
.entry("tiers".to_string())
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
tiers
.as_table_mut()
.unwrap()
.insert(from.to_string(), toml::Value::String(to.to_string()));
})
.unwrap();
let file: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_provider(&no_env, Some(&file)).as_deref(),
Some("codex")
);
assert!(resolve_sub_fallback(&no_env, Some(&file)));
let read = resolve_sub_tiers(&no_env, Some(&file));
assert_eq!(read.get("opus").map(String::as_str), Some("gpt-5.5"));
assert_eq!(
read.get("claude-sonnet-4").map(String::as_str),
Some("gpt-5.4")
);
edit_sub_table_at(&path, |t| {
t.insert("active".to_string(), toml::Value::String("off".to_string()));
})
.unwrap();
let off_file: toml::Value =
toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
resolve_sub_provider(&no_env, Some(&off_file)).as_deref(),
Some("off")
);
edit_sub_table_at(&path, |t| {
t.insert(
"active".to_string(),
toml::Value::String("codex".to_string()),
);
})
.unwrap();
let back: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert!(
resolve_sub_fallback(&no_env, Some(&back)),
"mode survived off/on"
);
let restored = resolve_sub_tiers(&no_env, Some(&back));
assert_eq!(restored.get("opus").map(String::as_str), Some("gpt-5.5"));
assert_eq!(
restored.get("claude-sonnet-4").map(String::as_str),
Some("gpt-5.4"),
"free-form entry survived off/on"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sub_effort_env_wins_and_write_round_trips() {
let file: toml::Value =
toml::from_str("[sub]\nactive = \"codex\"\n[sub.codex]\neffort = \"low\"\n").unwrap();
let no_env = |_: &str| None;
assert_eq!(
resolve_sub_effort(&no_env, Some(&file)).as_deref(),
Some("low")
);
let env_high = |k: &str| (k == "LLMTRIM_CODEX_EFFORT").then(|| "high".to_string());
assert_eq!(
resolve_sub_effort(&env_high, Some(&file)).as_deref(),
Some("high")
);
let none_env = |k: &str| (k == "LLMTRIM_CODEX_EFFORT").then(|| "none".to_string());
assert_eq!(resolve_sub_effort(&none_env, Some(&file)), None);
let dir = std::env::temp_dir().join(format!("llmtrim-effort-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
let mut tiers = std::collections::BTreeMap::new();
tiers.insert("opus".to_string(), "gpt-5.5".to_string());
write_sub_mapping_at(&path, "codex", &tiers).unwrap();
edit_sub_table_at(&path, |t| {
let prov = t
.entry("codex".to_string())
.or_insert_with(|| toml::Value::Table(toml::Table::new()));
prov.as_table_mut().unwrap().insert(
"effort".to_string(),
toml::Value::String("medium".to_string()),
);
})
.unwrap();
let f: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
resolve_sub_effort(&no_env, Some(&f)).as_deref(),
Some("medium")
);
assert_eq!(
resolve_sub_tiers(&no_env, Some(&f))
.get("opus")
.map(String::as_str),
Some("gpt-5.5"),
"effort write preserved the mapping"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lossless_baseline_enables_mvp_stages() {
let c = DenseConfig::lossless();
assert!(
!c.auto,
"lossless()/`safe` is the bare baseline, not shape-routing"
);
assert!(
c.hygiene && c.serialize,
"lossless input compression on by default"
);
assert!(
c.dedup && !c.dedup_near,
"exact dedup on by default (lossless); near-dedup opt-in (lossy)"
);
assert!(
!c.output_control,
"lossless()/`safe` is the lossless baseline — output shaping is on in the shipped `auto` default (via presets), not in this bare base"
);
assert!(
!c.retrieve,
"retrieval is opt-in (workload-dependent per eval)"
);
assert!(c.serialize_nested, "nested array encoding on by default");
assert!(!c.strip_base64, "base64 strip is opt-in (lossy)");
assert_eq!(c.serialize_min_rows, 2);
}
#[test]
fn auto_routed_presets_enable_output_control() {
for p in ["code", "rag", "aggressive", "reasoning", "agent"] {
assert!(
DenseConfig::preset(p).unwrap().output_control,
"preset `{p}` enables output control by default"
);
}
assert!(
!DenseConfig::preset("safe").unwrap().output_control,
"`safe` is the lossless mode — no output shaping"
);
}
#[test]
fn auto_routed_presets_downscale_images() {
for p in ["agent", "code", "rag", "aggressive"] {
assert!(
DenseConfig::preset(p).unwrap().multimodal,
"preset `{p}` downscales oversized images by default"
);
}
assert!(!DenseConfig::preset("safe").unwrap().multimodal);
assert!(
DenseConfig::preset("aggressive")
.unwrap()
.image_detail
.is_none()
);
}
#[test]
fn auto_routed_presets_strip_base64() {
for p in ["agent", "code", "rag", "aggressive"] {
assert!(
DenseConfig::preset(p).unwrap().strip_base64,
"preset `{p}` elides base64 blobs by default"
);
}
assert!(!DenseConfig::preset("safe").unwrap().strip_base64);
}
#[test]
fn tool_minify_schema_rides_with_trim_desc() {
for p in ["agent", "aggressive"] {
let c = DenseConfig::preset(p).unwrap();
assert!(
c.tool_minify_schema && c.tool_trim_desc,
"preset `{p}` minifies tool schemas alongside description trimming"
);
}
for p in ["safe", "code", "rag"] {
assert!(
!DenseConfig::preset(p).unwrap().tool_minify_schema,
"preset `{p}` does not minify tool schemas"
);
}
assert!(!DenseConfig::default().tool_minify_schema);
assert!(!DenseConfig::lossless().tool_minify_schema);
}
#[test]
fn agent_shrinks_the_tool_block_without_per_turn_churn() {
let agent = DenseConfig::preset("agent").unwrap();
assert!(
agent.tool_select && agent.tool_trim_desc && agent.tool_minify_schema,
"agent shrinks the tool block (selection is first-turn-only; trim/minify are cache-stable)"
);
}
#[test]
fn agent_enables_frugal_directive_but_safe_stays_lossless() {
assert!(
DenseConfig::preset("agent").unwrap().output_frugal_tools,
"agent (auto tool-call route) enables the frugality directive"
);
for p in ["safe", "lossless"] {
assert!(
!DenseConfig::preset(p).unwrap().output_frugal_tools,
"{p} stays lossless — no behavioral directive"
);
}
}
#[test]
fn config_selects_preset_by_name_else_flags() {
let agg = DenseConfig::from_toml_value(toml::from_str("preset = \"aggressive\"").unwrap())
.unwrap();
assert!(
agg.output_control && agg.retrieve,
"preset key selects the profile"
);
assert!(
DenseConfig::from_toml_value(toml::from_str("preset = \"nope\"").unwrap()).is_err()
);
let flags =
DenseConfig::from_toml_value(toml::from_str("hygiene = false").unwrap()).unwrap();
assert!(
!flags.hygiene,
"explicit flags parse when no preset is named"
);
}
#[test]
fn presets_layer_over_defaults() {
assert!(DenseConfig::preset("nope").is_none());
let rag = DenseConfig::preset("rag").unwrap();
assert!(rag.retrieve && rag.retrieve_sentence && rag.hygiene && rag.dedup);
assert!(
(rag.retrieve_keep_ratio - 0.35).abs() < 1e-9,
"tight sentence cap"
);
let code = DenseConfig::preset("code").unwrap();
assert!(code.minify_code && code.skeletonize && code.output_control);
assert!(
!code.output_compact_code,
"compact-code output dropped — bench-confirmed pass@1 harm"
);
let agg = DenseConfig::preset("aggressive").unwrap();
assert!(agg.retrieve && agg.skeletonize && agg.ngram && agg.minify_code);
assert!(DenseConfig::preset("ultra").is_none());
let cache = DenseConfig::preset("cache").unwrap();
assert!(cache.cache && !cache.retrieve && !cache.retrieve_reorder);
assert!(cache.hygiene && cache.serialize, "still lossless input");
let reasoning = DenseConfig::preset("reasoning").unwrap();
assert!(reasoning.output_control && reasoning.output_level == "draft");
}
#[test]
fn partial_toml_fills_remaining_from_default() {
let c: DenseConfig = toml::from_str("serialize = false\n").unwrap();
assert!(!c.serialize);
assert!(c.hygiene, "unset fields take the default");
assert_eq!(c.serialize_min_rows, 2);
assert!(
!c.auto,
"partial config deserialization fills from the lossless baseline, not auto"
);
}
fn resolve_file(toml_src: &str) -> RuntimeConfig {
let value: toml::Value = toml::from_str(toml_src).unwrap();
RuntimeConfig::resolve(|_| None, Some(&value))
}
fn resolve_env(env: &[(&str, &str)], toml_src: &str) -> RuntimeConfig {
let value: toml::Value = toml::from_str(toml_src).unwrap();
let env: std::collections::HashMap<String, String> = env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
RuntimeConfig::resolve(|k| env.get(k).cloned(), Some(&value))
}
#[test]
fn compact_models_preserve_order_and_deduplicate() {
let c = resolve_file("[compact]\nmodels = [\"haiku\", \"sonnet\", \"haiku\", \"\"]\n");
assert_eq!(c.compact_models, vec!["haiku", "sonnet"]);
}
#[test]
fn compact_only_config_keeps_auto_routing() {
let value: toml::Value =
toml::from_str("[compact]\nmodels = [\"haiku\", \"sonnet\"]\n").unwrap();
assert!(DenseConfig::from_toml_value(value).unwrap().auto);
}
#[test]
fn compact_writer_preserves_existing_config() {
let dir = std::env::temp_dir().join(format!("llmtrim-compact-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(
&path,
"capture_dir = \"/captures\"\n[sub]\nactive = \"off\"\n[sub.codex.tiers]\nopus = \"gpt-test\"\n",
)
.unwrap();
write_compact_models_at(&path, &["haiku".into(), "sonnet".into(), "haiku".into()]).unwrap();
let file: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(resolve_compact_models(Some(&file)), vec!["haiku", "sonnet"]);
assert_eq!(
file.get("capture_dir").and_then(toml::Value::as_str),
Some("/captures")
);
assert_eq!(
file.get("sub")
.and_then(|v| v.get("codex"))
.and_then(|v| v.get("tiers"))
.and_then(|v| v.get("opus"))
.and_then(toml::Value::as_str),
Some("gpt-test")
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn compact_writer_handles_commented_or_nonfinal_section() {
let dir = std::env::temp_dir().join(format!(
"llmtrim-compact-section-test-{}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(
&path,
"# keep me\n[compact] # routing policy\n# models intentionally omitted\n[sub]\nactive = \"off\"\n",
)
.unwrap();
write_compact_models_at(&path, &["haiku".into()]).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let file: toml::Value = toml::from_str(&text).unwrap();
assert_eq!(resolve_compact_models(Some(&file)), vec!["haiku"]);
assert_eq!(text.matches("[compact]").count(), 1);
assert!(text.contains("# keep me"));
assert!(text.find("models =").unwrap() < text.find("[sub]").unwrap());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn retention_days_parses_positive_only() {
assert_eq!(resolve_file("retention_days = 30").retention_days, Some(30));
assert_eq!(
resolve_file("retention_days = 0").retention_days,
None,
"0 disables age retention"
);
assert_eq!(resolve_file("retention_days = -5").retention_days, None);
assert_eq!(resolve_file("hygiene = true").retention_days, None);
}
#[test]
fn env_wins_over_file_for_every_runtime_setting() {
let file = "\
upstream_proxy = \"http://file:3128\"\n\
capture_dir = \"/file/cap\"\n\
db_path = \"/file/db.sqlite\"\n\
no_update_check = false\n\
bind = \"127.0.0.1\"\n\
capture_max_mb = 10\n\
retention_days = 7\n\
max_rows = 1000\n\
max_breakdown_turns = 100\n";
let c = resolve_env(
&[
("LLMTRIM_UPSTREAM_PROXY", "http://env:8080"),
("LLMTRIM_CAPTURE_DIR", "/env/cap"),
("LLMTRIM_DB_PATH", "/env/db.sqlite"),
("LLMTRIM_NO_UPDATE_CHECK", "1"),
("LLMTRIM_BIND", "0.0.0.0"),
("LLMTRIM_CAPTURE_MAX_MB", "99"),
("LLMTRIM_RETENTION_DAYS", "14"),
("LLMTRIM_MAX_ROWS", "2000"),
("LLMTRIM_MAX_BREAKDOWN_TURNS", "200"),
],
file,
);
assert_eq!(c.upstream_proxy.as_deref(), Some("http://env:8080"));
assert_eq!(c.capture_dir, Some(PathBuf::from("/env/cap")));
assert_eq!(c.db_path, Some(PathBuf::from("/env/db.sqlite")));
assert!(c.no_update_check);
assert_eq!(c.bind.as_deref(), Some("0.0.0.0"));
assert_eq!(c.capture_max_mb, Some(99));
assert_eq!(c.retention_days, Some(14));
assert_eq!(c.max_rows, Some(2000));
assert_eq!(c.max_breakdown_turns, Some(200));
}
#[test]
fn row_caps_parse_positive_only() {
assert_eq!(resolve_file("max_rows = 5000").max_rows, Some(5000));
assert_eq!(resolve_file("max_rows = 0").max_rows, None);
assert_eq!(resolve_file("max_rows = -1").max_rows, None);
assert_eq!(
resolve_file("max_breakdown_turns = 100000").max_breakdown_turns,
Some(100_000)
);
assert_eq!(
resolve_file("max_breakdown_turns = 0").max_breakdown_turns,
None
);
}
#[test]
fn file_used_when_env_absent() {
let c = resolve_file(
"upstream_proxy = \"http://file:3128\"\nbind = \"::1\"\ncapture_max_mb = 0\n",
);
assert_eq!(c.upstream_proxy.as_deref(), Some("http://file:3128"));
assert_eq!(c.bind.as_deref(), Some("::1"));
assert_eq!(c.capture_max_mb, Some(0), "0 from file disables the cap");
}
#[test]
fn no_update_check_true_on_env_presence_even_empty() {
let c = resolve_env(
&[("LLMTRIM_NO_UPDATE_CHECK", "")],
"no_update_check = false",
);
assert!(c.no_update_check);
}
#[test]
fn extra_hosts_env_replaces_file_and_normalizes() {
let c = resolve_env(
&[(
"LLMTRIM_EXTRA_HOSTS",
"LLM.Acme.com, api.acme.com, llm.acme.com",
)],
"extra_hosts = [\"ignored.example\"]",
);
assert_eq!(c.extra_hosts, vec!["api.acme.com", "llm.acme.com"]);
}
#[test]
fn extra_hosts_from_file_when_env_absent() {
let c = resolve_file("extra_hosts = [\"llm.acme.com\", \"gw.example.net\"]");
assert_eq!(c.extra_hosts, vec!["gw.example.net", "llm.acme.com"]);
}
fn exclusions_env(env: &[(&str, &str)], toml_src: &str) -> Exclusions {
let value: toml::Value = toml::from_str(toml_src).unwrap();
let env: std::collections::HashMap<String, String> = env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
resolve_exclusions(|k| env.get(k).cloned(), Some(&value))
}
#[test]
fn exclude_providers_canonicalizes_and_drops_unknown() {
let ex = exclusions_env(
&[(
"LLMTRIM_EXCLUDE_PROVIDERS",
"claude, anthropic, gemini, bogus",
)],
"exclude_providers = [\"ignored\"]",
);
assert_eq!(ex.providers, vec!["anthropic", "google"]);
}
#[test]
fn exclude_providers_from_file_when_env_absent() {
let value = toml::from_str("exclude_providers = [\"openai\", \"claude\"]").unwrap();
let ex = resolve_exclusions(|_| None, Some(&value));
assert_eq!(ex.providers, vec!["anthropic", "openai"]);
}
#[test]
fn exclude_hosts_normalizes_like_extra_hosts() {
let ex = exclusions_env(
&[(
"LLMTRIM_EXCLUDE_HOSTS",
"API.OpenAI.com, *.bad, openrouter.ai",
)],
"exclude_hosts = [\"ignored.example\"]",
);
assert_eq!(ex.hosts, vec!["api.openai.com", "openrouter.ai"]);
}
#[test]
fn exclude_env_replaces_file_for_both_lists() {
let ex = exclusions_env(
&[
("LLMTRIM_EXCLUDE_PROVIDERS", "openai"),
("LLMTRIM_EXCLUDE_HOSTS", "api.openai.com"),
],
"exclude_providers = [\"anthropic\"]\nexclude_hosts = [\"api.anthropic.com\"]\n",
);
assert_eq!(ex.providers, vec!["openai"]);
assert_eq!(ex.hosts, vec!["api.openai.com"]);
}
#[test]
fn exclude_keys_keep_auto_shape_routing() {
for src in [
"exclude_providers = [\"anthropic\"]",
"exclude_hosts = [\"api.anthropic.com\"]",
] {
let c = DenseConfig::from_toml_value(toml::from_str(src).unwrap()).unwrap();
assert!(c.auto, "exclude-only config `{src}` must keep auto routing");
}
}
#[test]
fn extra_hosts_drops_malformed_and_overbroad() {
for bad in [
"com",
"https://llm.acme.com",
"llm.acme.com/v1",
"llm.acme.com:443",
"*.acme.com",
".acme.com",
"-acme.com",
"ac me.com",
"1.2.3.4", "127.0.0.1", ] {
let c = resolve_env(&[("LLMTRIM_EXTRA_HOSTS", bad)], "");
assert!(c.extra_hosts.is_empty(), "expected `{bad}` to be dropped");
}
let c = resolve_env(&[("LLMTRIM_EXTRA_HOSTS", "llm.acme.com.")], "");
assert_eq!(c.extra_hosts, vec!["llm.acme.com"]);
}
#[test]
fn retention_key_does_not_disturb_compression_config() {
let c =
DenseConfig::from_toml_value(toml::from_str("retention_days = 30").unwrap()).unwrap();
assert!(
c.hygiene && c.serialize,
"retention_days is ignored by DenseConfig"
);
}
#[test]
fn runtime_only_keys_keep_auto_shape_routing() {
for src in [
"capture_dir = \"/tmp/cap\"",
"bind = \"0.0.0.0\"",
"upstream_proxy = \"http://p:3128\"",
"extra_hosts = [\"llm.acme.com\"]",
"no_update_check = true",
"db_path = \"/tmp/db\"\ncapture_max_mb = 100\nretention_days = 7",
] {
let c = DenseConfig::from_toml_value(toml::from_str(src).unwrap()).unwrap();
assert!(c.auto, "runtime-only config `{src}` must keep auto routing");
}
let c = DenseConfig::from_toml_value(
toml::from_str("capture_dir = \"/tmp/cap\"\nhygiene = false").unwrap(),
)
.unwrap();
assert!(
!c.auto && !c.hygiene,
"a compression key opts into explicit flags"
);
}
}