use serde::Serialize;
#[derive(Debug, Clone)]
pub struct TokenBudget {
pub total_window: usize,
pub system_prompt_pct: f64,
pub tool_defs_pct: f64,
pub output_pct: f64,
pub safety_pct: f64,
}
impl TokenBudget {
pub fn new(total_window: usize) -> Self {
Self {
total_window,
system_prompt_pct: 0.10,
tool_defs_pct: 0.05,
output_pct: 0.10,
safety_pct: 0.10,
}
}
pub fn with_allocations(
mut self,
system_pct: f64,
tool_pct: f64,
output_pct: f64,
safety_pct: f64,
) -> Self {
self.system_prompt_pct = system_pct;
self.tool_defs_pct = tool_pct;
self.output_pct = output_pct;
self.safety_pct = safety_pct;
self
}
pub fn system_prompt_budget(&self) -> usize {
(self.total_window as f64 * self.system_prompt_pct) as usize
}
pub fn tool_definitions_budget(&self) -> usize {
(self.total_window as f64 * self.tool_defs_pct) as usize
}
pub fn output_budget(&self) -> usize {
(self.total_window as f64 * self.output_pct) as usize
}
pub fn conversation_budget(&self) -> usize {
let allocated =
self.system_prompt_pct + self.tool_defs_pct + self.output_pct + self.safety_pct;
let remaining = (1.0 - allocated).max(0.0);
(self.total_window as f64 * remaining) as usize
}
pub fn allocate(
&self,
system_size: usize,
tool_defs_size: usize,
conversation_size: usize,
) -> TokenAllocation {
let sys_ok = system_size <= self.system_prompt_budget();
let tool_ok = tool_defs_size <= self.tool_definitions_budget();
let conv_ok = conversation_size <= self.conversation_budget();
let conversation_excess = if conv_ok {
0
} else {
conversation_size.saturating_sub(self.conversation_budget())
};
let total_used = system_size + tool_defs_size + conversation_size;
let usage_pct = if self.total_window > 0 {
(total_used as f64 / self.total_window as f64) * 100.0
} else {
0.0
};
let output_fits = self.output_budget() > 0;
TokenAllocation {
system_fits: sys_ok,
tool_defs_fit: tool_ok,
conversation_fits: conv_ok,
output_fits,
conversation_excess,
usage_pct,
}
}
pub fn report(
&self,
system_size: usize,
tool_defs_size: usize,
conversation_size: usize,
estimated_output: usize,
) -> BudgetReport {
let allocation = self.allocate(system_size, tool_defs_size, conversation_size);
BudgetReport {
total_window: self.total_window,
system_prompt: system_size,
system_prompt_budget: self.system_prompt_budget(),
tool_definitions: tool_defs_size,
tool_definitions_budget: self.tool_definitions_budget(),
conversation: conversation_size,
conversation_budget: self.conversation_budget(),
estimated_output,
output_budget: self.output_budget(),
usage_pct: allocation.usage_pct,
needs_compression: allocation.needs_compression(),
}
}
}
impl Default for TokenBudget {
fn default() -> Self {
Self::new(128_000)
}
}
#[derive(Debug, Clone)]
pub struct TokenAllocation {
pub system_fits: bool,
pub tool_defs_fit: bool,
pub conversation_fits: bool,
pub output_fits: bool,
pub conversation_excess: usize,
pub usage_pct: f64,
}
impl TokenAllocation {
pub fn ok(&self) -> bool {
self.system_fits && self.tool_defs_fit && self.conversation_fits && self.output_fits
}
pub fn needs_compression(&self) -> bool {
self.conversation_excess > 0
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BudgetReport {
pub total_window: usize,
pub system_prompt: usize,
pub system_prompt_budget: usize,
pub tool_definitions: usize,
pub tool_definitions_budget: usize,
pub conversation: usize,
pub conversation_budget: usize,
pub estimated_output: usize,
pub output_budget: usize,
pub usage_pct: f64,
pub needs_compression: bool,
}
#[derive(Debug, Clone)]
pub struct TokenBudgetConfig {
pub total_window: Option<usize>,
pub system_pct: f64,
pub tool_pct: f64,
pub output_pct: f64,
pub safety_pct: f64,
pub enabled: bool,
}
impl Default for TokenBudgetConfig {
fn default() -> Self {
Self {
total_window: None,
system_pct: 0.10,
tool_pct: 0.05,
output_pct: 0.10,
safety_pct: 0.10,
enabled: true,
}
}
}
impl TokenBudgetConfig {
pub fn enabled() -> Self {
Self::default()
}
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn with_total_window(mut self, window: usize) -> Self {
self.total_window = Some(window);
self
}
pub fn build(&self, fallback_window: usize) -> TokenBudget {
let window = self.total_window.unwrap_or(fallback_window);
TokenBudget::new(window).with_allocations(
self.system_pct,
self.tool_pct,
self.output_pct,
self.safety_pct,
)
}
}
pub fn context_window_for_model(model: &str) -> usize {
let lower = model.to_lowercase();
if lower.contains("claude-sonnet-4") || lower.contains("claude-opus-4") {
200_000
} else if lower.contains("claude") {
200_000
} else if lower.contains("gpt-4o") || lower.contains("gpt-4.1") {
128_000
} else if lower.contains("gpt-4") {
8_192
} else if lower.contains("gpt-3.5") {
16_384
} else if lower.contains("deepseek-v3") || lower.contains("deepseek-r1") {
64_000
} else if lower.contains("deepseek") {
128_000
} else if lower.contains("qwen3") || lower.contains("qwen-max") {
128_000
} else if lower.contains("qwen") {
32_768
} else if lower.contains("gemini-2") || lower.contains("gemini-1.5-pro") {
2_097_152 } else if lower.contains("gemini") {
32_768
} else if lower.contains("llama-4") {
128_000
} else if lower.contains("llama-3") {
8_192
} else if lower.contains("mixtral") || lower.contains("mistral") {
32_768
} else {
128_000 }
}
static MODEL_WINDOW_REGISTRY: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<String, usize>>,
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
pub fn register_model_window(model: &str, window_size: usize) {
if let Ok(mut reg) = MODEL_WINDOW_REGISTRY.lock() {
reg.insert(model.to_lowercase(), window_size);
}
}
pub fn resolve_model_window(model: &str) -> usize {
if let Ok(reg) = MODEL_WINDOW_REGISTRY.lock() {
if let Some(&size) = reg.get(&model.to_lowercase()) {
return size;
}
}
context_window_for_model(model)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_budget() {
let budget = TokenBudget::new(100_000);
assert_eq!(budget.system_prompt_budget(), 10_000); assert_eq!(budget.tool_definitions_budget(), 5_000); assert_eq!(budget.output_budget(), 10_000); assert_eq!(budget.conversation_budget(), 65_000);
}
#[test]
fn test_allocation_ok() {
let budget = TokenBudget::new(100_000);
let all = budget.allocate(5_000, 2_000, 30_000);
assert!(all.ok());
assert!(!all.needs_compression());
}
#[test]
fn test_allocation_needs_compression() {
let budget = TokenBudget::new(100_000);
let all = budget.allocate(5_000, 2_000, 70_000); assert!(!all.ok());
assert!(all.needs_compression());
assert_eq!(all.conversation_excess, 5_000);
}
#[test]
fn test_custom_allocations() {
let budget = TokenBudget::new(100_000).with_allocations(0.05, 0.05, 0.05, 0.05);
assert_eq!(budget.system_prompt_budget(), 5_000);
assert_eq!(budget.conversation_budget(), 80_000); }
#[test]
fn test_disabled_config() {
let config = TokenBudgetConfig::disabled();
assert!(!config.enabled);
}
#[test]
fn test_context_window_for_model() {
assert_eq!(context_window_for_model("claude-sonnet-4-6"), 200_000);
assert_eq!(context_window_for_model("gpt-4o"), 128_000);
assert_eq!(context_window_for_model("deepseek-v3"), 64_000);
assert_eq!(context_window_for_model("qwen3-max"), 128_000);
assert_eq!(context_window_for_model("unknown-model"), 128_000);
}
}