use serde::Serialize;
use super::savings::bytes_to_tokens;
pub(super) struct Budgeted<T> {
pub items: Vec<T>,
pub budgeted: bool,
}
pub(super) fn apply_budget<T: Serialize>(items: Vec<T>, max_tokens: Option<u32>) -> Budgeted<T> {
let Some(budget) = max_tokens.map(u64::from) else {
return Budgeted {
items,
budgeted: false,
};
};
let total = items.len();
let mut kept: Vec<T> = Vec::with_capacity(total);
let mut used: u64 = 0;
for item in items {
let cost = match serde_json::to_vec(&item) {
Ok(bytes) => bytes_to_tokens(bytes.len() as u64),
Err(_) => 0,
};
if kept.is_empty() || used.saturating_add(cost) <= budget {
used = used.saturating_add(cost);
kept.push(item);
} else {
break;
}
}
Budgeted {
budgeted: kept.len() < total,
items: kept,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
#[derive(Serialize)]
struct Item {
v: String,
}
fn item(tag: char) -> Item {
Item {
v: std::iter::repeat_n(tag, 10).collect(),
}
}
const ITEM_TOKENS: u64 = 4;
#[test]
fn none_budget_keeps_everything_without_serializing() {
let items: Vec<Item> = ('a'..='e').map(item).collect();
let input_len = 5;
let out = apply_budget(items, None);
assert_eq!(out.items.len(), 5);
assert!(!out.budgeted);
assert_eq!(input_len - out.items.len(), 0, "nothing dropped");
}
#[test]
fn keeps_exact_prefix_that_fits_and_reports_dropped_count() {
let items: Vec<Item> = ('a'..='e').map(item).collect();
let input_len = items.len();
let budget = ITEM_TOKENS * 2 + 1; let out = apply_budget(items, Some(budget as u32));
assert_eq!(out.items.len(), 2, "exactly the 2-item prefix fits");
assert_eq!(
out.items.iter().map(|i| i.v.clone()).collect::<Vec<_>>(),
vec!["aaaaaaaaaa".to_string(), "bbbbbbbbbb".to_string()],
"kept items must be the leading prefix, in order"
);
assert!(out.budgeted);
assert_eq!(input_len - out.items.len(), 3, "dropped count");
}
#[test]
fn budget_exactly_at_boundary_keeps_all() {
let items: Vec<Item> = ('a'..='c').map(item).collect();
let out = apply_budget(items, Some((ITEM_TOKENS * 3) as u32));
assert_eq!(out.items.len(), 3);
assert!(!out.budgeted);
}
#[test]
fn zero_budget_still_keeps_first_item_for_forward_progress() {
let items: Vec<Item> = ('a'..='c').map(item).collect();
let input_len = items.len();
let out = apply_budget(items, Some(0));
assert_eq!(out.items.len(), 1, "first item always admitted");
assert_eq!(out.items[0].v, "aaaaaaaaaa");
assert!(out.budgeted);
assert_eq!(input_len - out.items.len(), 2, "dropped count");
}
#[test]
fn empty_input_is_never_budgeted() {
let out = apply_budget(Vec::<Item>::new(), Some(0));
assert!(out.items.is_empty());
assert!(!out.budgeted);
}
}