use std::collections::BTreeSet;
use proptest::prelude::*;
use serde_json::{Map, Value, json};
use super::{estimate_input_tokens, estimate_output_tokens, estimate_tokens};
use crate::test_support::strategies::{edge_string, json_value, object_key};
use crate::test_support::{config, vectors};
use crate::wire;
const KINDS: [&str; 5] = ["bytes", "utf8", "input", "output", "divisor"];
fn expected(case: &Map<String, Value>) -> Option<i32> {
let output = &case["output"];
if output.is_null() {
return None;
}
let tokens = output
.as_i64()
.unwrap_or_else(|| panic!("{}: output is not an integer", vectors::name(case)));
Some(i32::try_from(tokens).unwrap())
}
fn check_case(case: &Map<String, Value>) -> &'static str {
let name = vectors::name(case);
let kind = KINDS
.into_iter()
.find(|kind| name == *kind || name.strip_prefix(kind).is_some_and(|r| r.starts_with(':')))
.unwrap_or_else(|| panic!("{name}: unknown kind"));
let input = case.get("input");
match kind {
"bytes" => {
let bytes = input.and_then(Value::as_u64).expect("bytes input");
assert_eq!(Some(estimate_tokens(bytes)), expected(case), "{name}");
}
"utf8" => {
let text = input.and_then(Value::as_str).expect("utf8 input");
let bytes = case["output"].as_u64().expect("utf8 output");
assert_eq!(u64::try_from(text.len()).unwrap(), bytes, "{name}");
}
"input" => assert_eq!(estimate_input_tokens(input), expected(case), "{name}"),
"output" => assert_eq!(estimate_output_tokens(input), expected(case), "{name}"),
"divisor" => {
let divisor = case["output"].as_f64().expect("divisor");
assert_eq!(divisor, 3.5, "the fixture pins the divisor 3.5");
for bytes in 0_u32..=2000 {
#[expect(clippy::cast_possible_truncation, reason = "at most 572 tokens")]
let tokens = (f64::from(bytes) / divisor).ceil() as i32;
assert_eq!(estimate_tokens(u64::from(bytes)), tokens, "{bytes} bytes");
}
}
_ => unreachable!("{name}: kind {kind:?} has no check"),
}
kind
}
#[test]
fn fixture_cases_hold() {
let seen: BTreeSet<&str> = vectors::cases("tokens").iter().map(check_case).collect();
assert_eq!(seen, BTreeSet::from(KINDS), "every kind has a case");
assert_eq!(estimate_input_tokens(None), None);
assert_eq!(estimate_output_tokens(None), None);
}
#[test]
fn estimate_tokens_never_overflows() {
assert_eq!(estimate_tokens(u64::MAX), i32::MAX);
assert_eq!(estimate_tokens(0), 0);
}
#[test]
fn estimates_match_tokens_cases() {
let seen: BTreeSet<&str> = vectors::cases("tokens_cases")
.iter()
.map(check_case)
.collect();
assert_eq!(
seen,
BTreeSet::from(["bytes", "input", "output"]),
"tokens_cases.json has cases of each kind"
);
}
fn stringify_tokens(value: &Value) -> i32 {
estimate_tokens(u64::try_from(wire::stringify(value).len()).unwrap())
}
fn utf8_len(text: &str) -> u64 {
u64::try_from(text.len()).unwrap()
}
fn json_but(keep: fn(&Value) -> bool) -> impl Strategy<Value = Value> + Clone {
json_value(1, 2).prop_filter("a value of the wrong kind", keep)
}
fn counted_block() -> impl Strategy<Value = (Value, u64)> {
let not_string: fn(&Value) -> bool = |value| !value.is_string();
let not_object: fn(&Value) -> bool = |value| !value.is_object();
let other_type = prop::sample::select(vec!["image", "audio", "resource_link", "Text", ""]);
prop_oneof![
edge_string(40).prop_map(|t| (json!({"type": "text", "text": t}), utf8_len(&t))),
(edge_string(40), edge_string(8)).prop_map(|(t, other)| {
let block = json!({"type": "text", "text": t, "resource": {"text": other}});
(block, utf8_len(&t))
}),
edge_string(40).prop_map(|t| {
let block = json!({"type": "resource", "resource": {"uri": "u", "text": t}});
(block, utf8_len(&t))
}),
(edge_string(40), edge_string(8)).prop_map(|(t, other)| {
let block =
json!({"type": "resource", "text": other, "resource": {"uri": "u", "text": t}});
(block, utf8_len(&t))
}),
Just((
json!({"type": "image", "data": "QUJD", "mimeType": "image/png"}),
0
)),
Just((
json!({"type": "resource", "resource": {"uri": "u", "blob": "QUJD"}}),
0
)),
json_but(not_string).prop_map(|t| (json!({"type": "text", "text": t}), 0)),
json_but(not_string).prop_map(|t| (
json!({"type": "resource", "resource": {"uri": "u", "text": t}}),
0
)),
json_but(not_object).prop_map(|r| (json!({"type": "resource", "resource": r}), 0)),
(other_type, edge_string(16)).prop_map(|(kind, t)| {
(json!({"type": kind, "text": t, "resource": {"text": t}}), 0)
}),
(json_but(not_string), edge_string(16)).prop_map(|(kind, t)| {
(json!({"type": kind, "text": t, "resource": {"text": t}}), 0)
}),
edge_string(16).prop_map(|t| (json!({"text": t, "resource": {"text": t}}), 0)),
json_but(not_object).prop_map(|block| (block, 0)),
]
}
fn blockless_result() -> impl Strategy<Value = Value> {
(
proptest::option::of(json_value(2, 3)),
proptest::collection::vec((object_key(), json_value(1, 2)), 0..3),
)
.prop_map(|(structured, extra)| {
let mut result: Map<String, Value> = extra.into_iter().collect();
result.insert("content".to_owned(), json!([]));
if let Some(structured) = structured {
result.insert("structuredContent".to_owned(), structured);
}
Value::Object(result)
})
}
proptest! {
#![proptest_config(config())]
#[test]
fn estimate_tokens_is_the_fewest_tokens_of_3_5_bytes_that_cover_the_bytes(
bytes in prop_oneof![0_u64..100_000, 0_u64..(1 << 53)],
more in 0_u64..1_000,
) {
let tokens = estimate_tokens(bytes);
let wide = |n: i32| 7 * u128::try_from(n).unwrap();
prop_assert!(tokens == i32::MAX || wide(tokens) >= 2 * u128::from(bytes));
if tokens > 0 {
prop_assert!(wide(tokens - 1) < 2 * u128::from(bytes));
}
prop_assert!(estimate_tokens(bytes + more) >= tokens);
}
#[test]
fn input_tokens_count_the_stringify_bytes(
entries in proptest::collection::vec((object_key(), json_value(5, 6)), 0..6),
) {
let value = Value::Object(entries.into_iter().collect());
prop_assert_eq!(estimate_input_tokens(Some(&value)), Some(stringify_tokens(&value)));
}
#[test]
fn output_tokens_count_the_block_texts(
blocks in proptest::collection::vec(counted_block(), 1..6),
structured in proptest::option::of(json_value(2, 3)),
extra in proptest::collection::vec((object_key(), json_value(1, 2)), 0..3),
) {
let bytes: u64 = blocks.iter().map(|(_, bytes)| bytes).sum();
let mut result: Map<String, Value> = extra.into_iter().collect();
let content = blocks.into_iter().map(|(block, _)| block).collect();
result.insert("content".to_owned(), Value::Array(content));
if let Some(structured) = structured {
result.insert("structuredContent".to_owned(), structured);
}
let tokens = i32::try_from((2 * bytes).div_ceil(7)).unwrap();
prop_assert_eq!(estimate_output_tokens(Some(&Value::Object(result))), Some(tokens));
}
#[test]
fn output_tokens_without_blocks_count_the_stringify_bytes(
value in prop_oneof![blockless_result(), json_value(4, 5)],
) {
let content = value.get("content").and_then(Value::as_array);
prop_assume!(content.is_none_or(Vec::is_empty));
let expected = match (content, value.get("structuredContent")) {
_ if value.is_null() => None,
(None, _) => Some(stringify_tokens(&value)),
(Some(_), None | Some(Value::Null)) => Some(0),
(Some(_), Some(structured)) => Some(stringify_tokens(structured)),
};
prop_assert_eq!(estimate_output_tokens(Some(&value)), expected);
}
}