Skip to main content

contextgraph_types/
token.rs

1//! Canonical token accounting — the rule that makes budget honesty checkable
2//! (`SPEC.md` §B3, [ADR 0003](../../docs/adr/0003-canonical-token-accounting.md)).
3//!
4//! Budget honesty is CGP's flagship guarantee, but before this rule existed the
5//! conformance suite could only verify *arithmetic*: it summed the costs a
6//! provider declared and compared the total to the budget. A provider reporting
7//! `token_cost: 1` on a ten-thousand-token frame satisfied that check perfectly
8//! while destroying the host's actual budget. The one lie that mattered was the
9//! one lie the suite could not catch.
10//!
11//! [`budget_tokens`] closes that hole by making cost a function of bytes both
12//! sides observe:
13//!
14//! ```text
15//! budget_tokens(content) = ceil(utf8_byte_length(content) / 4)
16//! ```
17//!
18//! # This is an accounting unit, not a tokenizer
19//!
20//! A budget token is deliberately **not** a prediction of any model's
21//! tokenizer. Its job is to make every provider's cost claims comparable and
22//! verifiable, which no real tokenizer can do without being mandated in every
23//! language an implementation might be written in.
24//!
25//! The approximation is honest about its direction. At roughly four bytes per
26//! token it tracks English prose closely, and it **under-estimates** dense
27//! source code (≈3–3.5 bytes/token) and CJK text (≈3 bytes/token). A host
28//! therefore **MUST NOT** treat one budget token as one model token: it maps
29//! its real model budget into budget tokens with a safety factor. See
30//! [`SUGGESTED_HOST_SAFETY_FACTOR`].
31//!
32//! # Scope
33//!
34//! The count covers `ContextFrame::content` only — not `title`, not
35//! `citation_label`, not provenance, and not the fences and labels a host wraps
36//! around a frame. `content` is the one field the provider fully controls and
37//! whose exact bytes both sides observe identically, so it is the only input on
38//! which a byte-exact check can be built. The host's own rendering chrome is
39//! the host's cost to budget.
40
41/// Bytes per budget token. See the module docs for why this constant is an
42/// accounting convention rather than an empirical tokenizer ratio.
43pub const BYTES_PER_BUDGET_TOKEN: usize = 4;
44
45/// The factor a host is advised to apply when converting a real model context
46/// budget into budget tokens, compensating for the under-estimate on source
47/// code and CJK text.
48///
49/// Advisory, not normative: a host that knows its corpus is English prose can
50/// safely use less headroom, and one serving minified JSON may want more. It is
51/// stated as a constant so the reference host's choice is inspectable rather
52/// than buried in a literal.
53pub const SUGGESTED_HOST_SAFETY_FACTOR: f32 = 1.35;
54
55/// The canonical budget-token cost of a piece of frame content.
56///
57/// This is the value `ContextFrame::token_cost` **MUST** carry (`SPEC.md` §B3).
58/// Exact equality is required — there is no tolerance band, because any band
59/// wide enough to absorb genuine tokenizer disagreement is also wide enough to
60/// hide meaningful under-reporting, which puts the suite back to guessing. A
61/// provider cannot "disagree" with a byte count.
62///
63/// ```
64/// use contextgraph_types::budget_tokens;
65///
66/// assert_eq!(budget_tokens(""), 0);
67/// assert_eq!(budget_tokens("abcd"), 1);
68/// assert_eq!(budget_tokens("abcde"), 2); // ceil, never floor
69/// ```
70pub fn budget_tokens(content: &str) -> u32 {
71    // `str::len` is the UTF-8 byte length, which is exactly what the rule
72    // specifies — not `chars().count()`, which would make the count depend on
73    // Unicode normalization and diverge across implementations.
74    let bytes = content.len();
75    bytes.div_ceil(BYTES_PER_BUDGET_TOKEN) as u32
76}
77
78/// Convert a real model context budget into budget tokens, applying `factor` as
79/// headroom against the under-estimate documented on [`budget_tokens`].
80///
81/// A host asking for `model_tokens` worth of real context should request this
82/// many budget tokens, so that honest providers filling the budget exactly do
83/// not overflow the model window.
84pub fn budget_from_model_tokens(model_tokens: u32, factor: f32) -> u32 {
85    if factor <= 0.0 {
86        return model_tokens;
87    }
88    (model_tokens as f32 / factor) as u32
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn empty_content_costs_nothing() {
97        assert_eq!(budget_tokens(""), 0);
98    }
99
100    #[test]
101    fn the_count_rounds_up_so_a_partial_token_is_never_free() {
102        // The rounding direction is the whole point: floor would let a
103        // provider shave a token off every frame and call it arithmetic.
104        assert_eq!(budget_tokens("a"), 1);
105        assert_eq!(budget_tokens("abc"), 1);
106        assert_eq!(budget_tokens("abcd"), 1);
107        assert_eq!(budget_tokens("abcde"), 2);
108        assert_eq!(budget_tokens("abcdefgh"), 2);
109    }
110
111    #[test]
112    fn the_count_is_over_utf8_bytes_not_characters() {
113        // A single emoji is 4 UTF-8 bytes but 1 char. Counting characters
114        // would let a provider serve 4x the bytes it declared, and would make
115        // the count depend on Unicode normalization — so the rule names bytes
116        // explicitly and this test pins it.
117        let emoji = "😀";
118        assert_eq!(emoji.chars().count(), 1);
119        assert_eq!(emoji.len(), 4);
120        assert_eq!(budget_tokens(emoji), 1);
121
122        // Three-byte CJK: 3 chars, 9 bytes, 3 budget tokens (not 1).
123        let cjk = "文脈図";
124        assert_eq!(cjk.chars().count(), 3);
125        assert_eq!(cjk.len(), 9);
126        assert_eq!(budget_tokens(cjk), 3);
127    }
128
129    #[test]
130    fn the_rule_is_reproducible_from_bytes_alone() {
131        // The property that makes conformance possible in any language: the
132        // count depends on nothing but the bytes.
133        let content = "fn main() { println!(\"hello\"); }";
134        assert_eq!(budget_tokens(content), content.len().div_ceil(4) as u32);
135    }
136
137    #[test]
138    fn host_safety_factor_shrinks_the_requested_budget() {
139        // 10_000 real model tokens with 1.35x headroom means asking providers
140        // for ~7_407 budget tokens, so an honest fill does not overflow.
141        let budget = budget_from_model_tokens(10_000, SUGGESTED_HOST_SAFETY_FACTOR);
142        assert!(budget < 10_000);
143        assert_eq!(budget, 7407);
144    }
145
146    #[test]
147    fn a_nonsense_safety_factor_degrades_to_identity_rather_than_panicking() {
148        assert_eq!(budget_from_model_tokens(500, 0.0), 500);
149        assert_eq!(budget_from_model_tokens(500, -1.0), 500);
150    }
151}