1use std::fmt::Write as _;
10
11use thiserror::Error;
12
13pub const TOKEN_CALIBRATION_SCHEMA_VERSION: &str = "leanctx.token-calibration/v1";
15pub const TOKEN_CALIBRATION_REPORT_VERSION: &str = "1.0.0";
17const MAX_ENTRIES: usize = 4_096;
18const MAX_TEXT_FIELD_CHARS: usize = 256;
19const MAX_AUTHORITY_REF_CHARS: usize = 512;
20
21#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
23pub struct TokenCalibrationEntry {
24 pub sample_ref: String,
26 pub provider_tokens: u64,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32pub struct TokenCalibrationReportV1 {
33 pub schema_version: String,
34 pub report_version: String,
35 pub provider: String,
36 pub model: String,
37 pub tokenizer_family: String,
38 pub authority_ref: Option<String>,
40 pub entries: Vec<TokenCalibrationEntry>,
42 pub corpus_digest: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Error)]
48pub enum TokenCalibrationError {
49 #[error("unsupported calibration schema version")]
50 UnsupportedSchema,
51 #[error("unsupported calibration report version")]
52 UnsupportedReportVersion,
53 #[error("{field} is empty or exceeds its bound")]
54 InvalidField { field: &'static str },
55 #[error("calibration authority is missing")]
56 MissingAuthority,
57 #[error("calibration authority reference is invalid")]
58 InvalidAuthority,
59 #[error("calibration entries are empty or exceed the bound")]
60 InvalidEntryCount,
61 #[error("calibration entries are not sorted and unique")]
62 NonCanonicalEntries,
63 #[error("calibration corpus digest is malformed")]
64 InvalidDigest,
65 #[error("calibration corpus digest does not match entries")]
66 DigestMismatch,
67 #[error("calibration report serialization failed")]
68 Serialization,
69}
70
71impl TokenCalibrationReportV1 {
72 pub fn new_provider_authoritative(
74 provider: impl Into<String>,
75 model: impl Into<String>,
76 tokenizer_family: impl Into<String>,
77 authority_ref: impl Into<String>,
78 mut entries: Vec<TokenCalibrationEntry>,
79 ) -> Result<Self, TokenCalibrationError> {
80 entries.sort_by(|left, right| left.sample_ref.cmp(&right.sample_ref));
81 let mut report = Self {
82 schema_version: TOKEN_CALIBRATION_SCHEMA_VERSION.to_string(),
83 report_version: TOKEN_CALIBRATION_REPORT_VERSION.to_string(),
84 provider: provider.into(),
85 model: model.into(),
86 tokenizer_family: tokenizer_family.into(),
87 authority_ref: Some(authority_ref.into()),
88 entries,
89 corpus_digest: String::new(),
90 };
91 report.corpus_digest = report.computed_digest();
92 report.validate()?;
93 Ok(report)
94 }
95
96 pub fn validate(&self) -> Result<(), TokenCalibrationError> {
98 if self.schema_version != TOKEN_CALIBRATION_SCHEMA_VERSION {
99 return Err(TokenCalibrationError::UnsupportedSchema);
100 }
101 if self.report_version != TOKEN_CALIBRATION_REPORT_VERSION {
102 return Err(TokenCalibrationError::UnsupportedReportVersion);
103 }
104 for (field, value) in [
105 ("provider", self.provider.as_str()),
106 ("model", self.model.as_str()),
107 ("tokenizer_family", self.tokenizer_family.as_str()),
108 ] {
109 if !valid_text(value, MAX_TEXT_FIELD_CHARS) {
110 return Err(TokenCalibrationError::InvalidField { field });
111 }
112 }
113 let Some(authority_ref) = self.authority_ref.as_deref() else {
114 return Err(TokenCalibrationError::MissingAuthority);
115 };
116 if !valid_text(authority_ref, MAX_AUTHORITY_REF_CHARS) {
117 return Err(TokenCalibrationError::InvalidAuthority);
118 }
119 if self.entries.is_empty() || self.entries.len() > MAX_ENTRIES {
120 return Err(TokenCalibrationError::InvalidEntryCount);
121 }
122 let mut previous: Option<&str> = None;
123 for entry in &self.entries {
124 if !valid_text(&entry.sample_ref, MAX_AUTHORITY_REF_CHARS)
125 || previous.is_some_and(|value| value >= entry.sample_ref.as_str())
126 {
127 return Err(TokenCalibrationError::NonCanonicalEntries);
128 }
129 previous = Some(&entry.sample_ref);
130 }
131 if !is_digest(&self.corpus_digest) {
132 return Err(TokenCalibrationError::InvalidDigest);
133 }
134 if self.corpus_digest != self.computed_digest() {
135 return Err(TokenCalibrationError::DigestMismatch);
136 }
137 Ok(())
138 }
139
140 pub fn canonical_json(&self) -> Result<Vec<u8>, TokenCalibrationError> {
142 self.validate()?;
143 serde_json::to_vec(self).map_err(|_| TokenCalibrationError::Serialization)
144 }
145
146 fn computed_digest(&self) -> String {
147 let mut payload = String::new();
148 append_field(&mut payload, TOKEN_CALIBRATION_SCHEMA_VERSION);
149 append_field(&mut payload, TOKEN_CALIBRATION_REPORT_VERSION);
150 append_field(&mut payload, &self.provider);
151 append_field(&mut payload, &self.model);
152 append_field(&mut payload, &self.tokenizer_family);
153 append_field(&mut payload, self.authority_ref.as_deref().unwrap_or(""));
154 for entry in &self.entries {
155 append_field(&mut payload, &entry.sample_ref);
156 let _ = write!(payload, "{}|", entry.provider_tokens);
157 }
158 format!("blake3:{}", blake3::hash(payload.as_bytes()).to_hex())
159 }
160}
161
162fn append_field(payload: &mut String, value: &str) {
163 let _ = write!(payload, "{}:", value.len());
164 payload.push_str(value);
165 payload.push('|');
166}
167
168fn valid_text(value: &str, max_chars: usize) -> bool {
169 !value.is_empty()
170 && value.chars().count() <= max_chars
171 && value.chars().all(|character| !character.is_control())
172}
173
174fn is_digest(value: &str) -> bool {
175 let Some(hex) = value.strip_prefix("blake3:") else {
176 return false;
177 };
178 hex.len() == 64
179 && hex
180 .bytes()
181 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn report() -> TokenCalibrationReportV1 {
189 TokenCalibrationReportV1::new_provider_authoritative(
190 "provider-a",
191 "model-a",
192 "provider-native-v1",
193 "https://evidence.example/report/1",
194 vec![
195 TokenCalibrationEntry {
196 sample_ref: "sample:b".to_string(),
197 provider_tokens: 11,
198 },
199 TokenCalibrationEntry {
200 sample_ref: "sample:a".to_string(),
201 provider_tokens: 7,
202 },
203 ],
204 )
205 .expect("valid report")
206 }
207
208 #[test]
209 fn constructor_sorts_entries_and_is_deterministic() {
210 let first = report();
211 let second = report();
212 assert_eq!(first, second);
213 assert_eq!(first.entries[0].sample_ref, "sample:a");
214 assert_eq!(first.canonical_json(), second.canonical_json());
215 }
216
217 #[test]
218 fn missing_authority_fails_closed() {
219 let mut value = report();
220 value.authority_ref = None;
221 assert_eq!(
222 value.validate(),
223 Err(TokenCalibrationError::MissingAuthority)
224 );
225 assert_eq!(
226 value.canonical_json(),
227 Err(TokenCalibrationError::MissingAuthority)
228 );
229 }
230
231 #[test]
232 fn digest_mutation_fails_closed() {
233 let mut value = report();
234 value.entries[0].provider_tokens += 1;
235 assert_eq!(value.validate(), Err(TokenCalibrationError::DigestMismatch));
236 }
237
238 #[test]
239 fn duplicate_or_unsorted_entries_fail_closed() {
240 let mut value = report();
241 value.entries.swap(0, 1);
242 value.corpus_digest = value.computed_digest();
243 assert_eq!(
244 value.validate(),
245 Err(TokenCalibrationError::NonCanonicalEntries)
246 );
247
248 value.entries[0].sample_ref = value.entries[1].sample_ref.clone();
249 value.corpus_digest = value.computed_digest();
250 assert_eq!(
251 value.validate(),
252 Err(TokenCalibrationError::NonCanonicalEntries)
253 );
254 }
255}