use supercode_interchange::{format_commas, ChatMessage};
use crate::ToolSchema;
pub use supercode_interchange::{estimate_tokens, estimate_view_tokens};
const GUARD_MARGIN_NUM: u64 = 5;
const GUARD_MARGIN_DEN: u64 = 4;
pub fn with_guard_margin(tokens: u64) -> u64 {
tokens
.saturating_mul(GUARD_MARGIN_NUM)
.div_ceil(GUARD_MARGIN_DEN)
}
pub const CONTEXT_RESPONSE_RESERVE_TOKENS: u64 = 16_384;
pub fn estimate_request_tokens(messages: &[ChatMessage], tools: &[ToolSchema]) -> u64 {
let tools_wire = serde_json::to_string(tools).unwrap_or_default();
estimate_view_tokens(messages).saturating_add(estimate_tokens(&tools_wire))
}
pub fn context_guard(
messages: &[ChatMessage],
tools: &[ToolSchema],
context_limit: u64,
) -> (bool, u64) {
let raw = estimate_request_tokens(messages, tools);
let projected = with_guard_margin(raw);
let fits = projected.saturating_add(CONTEXT_RESPONSE_RESERVE_TOKENS) <= context_limit;
(fits, projected)
}
pub fn estimate_deferred_schema_tokens(full: &[ToolSchema], advertised: &[ToolSchema]) -> u64 {
let full_tokens = estimate_tokens(&serde_json::to_string(full).unwrap_or_default());
let advertised_tokens = estimate_tokens(&serde_json::to_string(advertised).unwrap_or_default());
full_tokens.saturating_sub(advertised_tokens)
}
pub fn fmt_approx_tokens(n: u64) -> String {
format!("~{} tok", format_commas(n as usize))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_is_zero_tokens() {
assert_eq!(estimate_tokens(""), 0);
}
#[test]
fn four_byte_ascii_is_one_token() {
assert_eq!(estimate_tokens("abcd"), 1);
}
#[test]
fn ceil_behavior_rounds_up() {
assert_eq!(estimate_tokens("abcde"), 2);
}
#[test]
fn deterministic_same_input_same_output() {
let s = "the quick brown fox jumps over the lazy dog";
assert_eq!(estimate_tokens(s), estimate_tokens(s));
}
#[test]
fn monotone_under_concatenation() {
let words = [
"a",
"ab",
"abc",
"hello",
"world",
"",
"x",
"supercode",
"token",
"estimate",
"!",
" ",
"\n",
"quick brown fox",
"z",
"1234567890",
"the",
"lazy",
"dog",
"jumps",
"over",
"sidecar",
"reduction",
"archive",
"delete",
"session",
"store",
"family",
"meta",
"json",
];
for a in &words {
for b in &words {
let combined = format!("{a}{b}");
assert!(
estimate_tokens(&combined) >= estimate_tokens(a),
"est({a:?}+{b:?}) = {} should be >= est({a:?}) = {}",
estimate_tokens(&combined),
estimate_tokens(a)
);
}
}
}
#[test]
fn estimate_view_tokens_matches_serde_json_wire_form() {
let msgs = vec![
ChatMessage::user("hello there, this is a test message"),
ChatMessage::assistant("sure, here's a longer reply with more bytes in it"),
];
let expected: u64 = msgs
.iter()
.map(|m| estimate_tokens(&serde_json::to_string(m).unwrap()))
.sum();
assert_eq!(estimate_view_tokens(&msgs), expected);
}
#[test]
fn estimate_view_tokens_empty_slice_is_zero() {
assert_eq!(estimate_view_tokens(&[]), 0);
}
fn fat_schema(name: &str, filler_len: usize) -> ToolSchema {
ToolSchema {
name: name.to_string(),
description: "x".repeat(filler_len),
parameters: serde_json::json!({"type": "object", "properties": {}}),
}
}
#[test]
fn deferred_schema_tokens_full_equals_advertised_is_zero() {
let full = vec![fat_schema("shell", 500), fat_schema("read_file", 200)];
let advertised = full.clone();
assert_eq!(estimate_deferred_schema_tokens(&full, &advertised), 0);
}
#[test]
fn deferred_schema_tokens_measures_the_difference() {
let builtin = fat_schema("shell", 50);
let mcp_fat = fat_schema("mcp__github__search_issues", 4000);
let full = vec![builtin.clone(), mcp_fat];
let advertised = vec![builtin];
let deferred = estimate_deferred_schema_tokens(&full, &advertised);
let expected = estimate_tokens(&serde_json::to_string(&full).unwrap()).saturating_sub(
estimate_tokens(&serde_json::to_string(&advertised).unwrap()),
);
assert_eq!(deferred, expected);
assert!(deferred > 0, "a fat deferred MCP schema should cost tokens");
}
#[test]
fn deferred_schema_tokens_advertised_larger_than_full_saturates_to_zero() {
let full = vec![fat_schema("a", 1)];
let advertised = vec![fat_schema("a", 1000)];
assert_eq!(estimate_deferred_schema_tokens(&full, &advertised), 0);
}
#[test]
fn fmt_approx_tokens_style() {
assert_eq!(fmt_approx_tokens(0), "~0 tok");
assert_eq!(fmt_approx_tokens(21904), "~21,904 tok");
assert_eq!(fmt_approx_tokens(1000000), "~1,000,000 tok");
}
#[test]
fn guard_margin_adds_25_percent_and_rounds_up() {
assert_eq!(with_guard_margin(0), 0);
assert_eq!(with_guard_margin(4), 5); assert_eq!(with_guard_margin(100), 125);
assert_eq!(with_guard_margin(101), 127);
}
#[test]
fn guard_margin_never_decreases() {
for n in [0u64, 1, 3, 4, 17, 1_000, 1_048_576] {
assert!(
with_guard_margin(n) >= n,
"margin must never make the estimate smaller: {n} -> {}",
with_guard_margin(n)
);
}
}
#[test]
fn guard_margin_saturates_instead_of_overflowing() {
let result = with_guard_margin(u64::MAX);
assert_eq!(result, u64::MAX.div_ceil(4));
}
#[test]
fn message_with_content_never_estimates_to_zero_tokens() {
let m = ChatMessage::user("hello world, this has real content in it");
assert!(estimate_view_tokens(std::slice::from_ref(&m)) > 0);
}
#[test]
fn debug_fallback_is_conservative_not_smaller_than_wire_form() {
let m = ChatMessage::user("x".repeat(500));
let wire = serde_json::to_string(&m).unwrap();
let debug = format!("{m:?}");
assert!(
debug.len() >= wire.len(),
"Debug fallback ({} bytes) must be >= wire form ({} bytes) to stay conservative",
debug.len(),
wire.len()
);
}
fn schema(name: &str, desc_len: usize) -> ToolSchema {
ToolSchema::new(
name,
"x".repeat(desc_len),
serde_json::json!({"type":"object"}),
)
}
#[test]
fn estimate_request_tokens_includes_tool_schemas() {
let messages = vec![ChatMessage::user("hi")];
let no_tools = estimate_request_tokens(&messages, &[]);
let with_tools = estimate_request_tokens(&messages, &[schema("shell", 2000)]);
assert!(
with_tools > no_tools,
"a fat tool-schema array must increase the request-token estimate"
);
}
#[test]
fn estimate_request_tokens_matches_messages_plus_schema_sum() {
let messages = vec![
ChatMessage::system("system prompt"),
ChatMessage::user("user turn"),
];
let tools = vec![schema("read_file", 100), schema("edit", 100)];
let expected = estimate_view_tokens(&messages)
+ estimate_tokens(&serde_json::to_string(&tools).unwrap());
assert_eq!(estimate_request_tokens(&messages, &tools), expected);
}
#[test]
fn context_guard_passes_a_small_request() {
let messages = vec![ChatMessage::user("hi")];
let (fits, projected) = context_guard(&messages, &[], 200_000);
assert!(fits, "a tiny request must fit a 200k-token limit");
assert!(projected < 200_000);
}
#[test]
fn context_guard_refuses_when_reserve_alone_exceeds_limit() {
let messages = vec![ChatMessage::user("hi")];
let (fits, _) = context_guard(&messages, &[], CONTEXT_RESPONSE_RESERVE_TOKENS - 1);
assert!(!fits);
}
#[test]
fn context_guard_messages_only_fit_but_overhead_pushes_over() {
let context_limit = 10_000u64;
let big_text = "x".repeat(36_000);
let messages = vec![ChatMessage::user(big_text)];
let messages_only = estimate_view_tokens(&messages);
assert!(
messages_only < context_limit,
"fixture must fit messages-only for this test to prove anything: {messages_only} vs {context_limit}"
);
let tools = vec![
schema("shell", 200),
schema("read_file", 200),
schema("edit", 200),
];
let (fits, projected) = context_guard(&messages, &tools, context_limit);
assert!(
!fits,
"messages alone fit but margin + schema overhead + reserve should push this over: projected={projected} limit={context_limit}"
);
}
#[test]
fn context_guard_is_the_single_formula_both_call_sites_share() {
let messages = vec![
ChatMessage::user("hello"),
ChatMessage::assistant("hi there"),
];
let tools = vec![schema("shell", 50)];
let limit = 1_000u64;
let raw = estimate_request_tokens(&messages, &tools);
let expected_projected = with_guard_margin(raw);
let expected_fits =
expected_projected.saturating_add(CONTEXT_RESPONSE_RESERVE_TOKENS) <= limit;
let (fits, projected) = context_guard(&messages, &tools, limit);
assert_eq!(projected, expected_projected);
assert_eq!(fits, expected_fits);
}
}