1use serde::Serialize;
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18
19pub mod writer;
20
21pub use writer::{
22 default_telemetry_log_path, default_trace_log_path, ShutdownOutcome, TelemetryWriter,
23 WriterOptions, WriterStats, WriterTraceSink, DEFAULT_MAX_FILE_BYTES, DEFAULT_QUEUE_CAPACITY,
24 DEFAULT_SHUTDOWN_FLUSH_TIMEOUT,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum TelemetryLevel {
30 #[default]
31 Off,
32 Basic,
33 Full,
34}
35
36impl TelemetryLevel {
37 pub fn from_str_key(s: &str) -> Self {
38 match s.trim().to_ascii_lowercase().as_str() {
39 "basic" | "on" | "1" | "true" => Self::Basic,
40 "full" => Self::Full,
41 _ => Self::Off,
42 }
43 }
44
45 pub fn enabled(&self) -> bool {
46 !matches!(self, Self::Off)
47 }
48
49 pub fn as_str(&self) -> &'static str {
50 match self {
51 Self::Off => "off",
52 Self::Basic => "basic",
53 Self::Full => "full",
54 }
55 }
56}
57
58#[derive(Debug, Clone, Default, Serialize)]
60pub struct UsageRecord {
61 pub input: u64,
62 pub output: u64,
63 pub cache_read: u64,
64 pub cache_write: u64,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub cache_write_5m: Option<u64>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub cache_write_1h: Option<u64>,
71 pub hit_pct: f64,
73}
74
75impl UsageRecord {
76 pub fn compute_hit_pct(&mut self) {
77 let total = self.input + self.cache_read + self.cache_write;
78 self.hit_pct = if total > 0 {
79 (self.cache_read as f64 / total as f64 * 1000.0).round() / 10.0
80 } else {
81 0.0
82 };
83 }
84}
85
86#[derive(Debug, Clone, Default, Serialize)]
89pub struct RateLimitRecord {
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub requests_limit: Option<u64>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub requests_remaining: Option<u64>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub tokens_limit: Option<u64>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub tokens_remaining: Option<u64>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub input_tokens_remaining: Option<u64>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub output_tokens_remaining: Option<u64>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub tokens_reset: Option<String>,
105}
106
107impl RateLimitRecord {
108 pub fn is_empty(&self) -> bool {
109 self.requests_limit.is_none()
110 && self.requests_remaining.is_none()
111 && self.tokens_limit.is_none()
112 && self.tokens_remaining.is_none()
113 && self.input_tokens_remaining.is_none()
114 && self.output_tokens_remaining.is_none()
115 && self.tokens_reset.is_none()
116 }
117}
118
119#[derive(Debug, Clone, Serialize)]
122pub struct CacheDiagRecord {
123 pub miss_reason: String,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub missed_tokens: Option<u64>,
128}
129
130#[derive(Debug, Clone, Default, Serialize)]
132pub struct ContextRecord {
133 pub messages: usize,
134 pub tools: usize,
135 pub system_bytes: usize,
136 pub breakpoints: Vec<usize>,
138}
139
140#[derive(Debug, Clone, Default, Serialize)]
142pub struct TelemetryRecord {
143 pub ts: u64,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub request_id: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub msg_id: Option<String>,
150 pub model: String,
151 pub attempt: u32,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub refusal_retries_used: Option<u32>,
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub ttft_ms: Option<u64>,
159 pub total_ms: u64,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub stop_reason: Option<String>,
163 pub usage: UsageRecord,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub ratelimit: Option<RateLimitRecord>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub cache_diag: Option<CacheDiagRecord>,
168 pub context: ContextRecord,
169 #[serde(skip_serializing_if = "std::ops::Not::not")]
172 pub keepwarm: bool,
173}
174
175impl TelemetryRecord {
176 pub fn now_ms() -> u64 {
177 SystemTime::now()
178 .duration_since(UNIX_EPOCH)
179 .map(|d| d.as_millis() as u64)
180 .unwrap_or(0)
181 }
182}
183
184fn header_u64(headers: &reqwest::header::HeaderMap, name: &str) -> Option<u64> {
186 headers.get(name)?.to_str().ok()?.parse().ok()
187}
188
189fn header_string(headers: &reqwest::header::HeaderMap, name: &str) -> Option<String> {
191 Some(headers.get(name)?.to_str().ok()?.to_string())
192}
193
194pub fn ratelimit_from_headers(headers: &reqwest::header::HeaderMap) -> RateLimitRecord {
196 RateLimitRecord {
197 requests_limit: header_u64(headers, "anthropic-ratelimit-requests-limit"),
198 requests_remaining: header_u64(headers, "anthropic-ratelimit-requests-remaining"),
199 tokens_limit: header_u64(headers, "anthropic-ratelimit-tokens-limit"),
200 tokens_remaining: header_u64(headers, "anthropic-ratelimit-tokens-remaining"),
201 input_tokens_remaining: header_u64(headers, "anthropic-ratelimit-input-tokens-remaining"),
202 output_tokens_remaining: header_u64(headers, "anthropic-ratelimit-output-tokens-remaining"),
203 tokens_reset: header_string(headers, "anthropic-ratelimit-tokens-reset"),
204 }
205}
206
207pub fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
209 header_string(headers, "request-id")
210}
211
212pub const RETRY_DELAY_CAP: Duration = Duration::from_secs(60);
215
216pub fn retry_delay_from_headers(
233 headers: &reqwest::header::HeaderMap,
234 attempt: u32,
235) -> (Duration, bool) {
236 let now_secs = SystemTime::now()
237 .duration_since(UNIX_EPOCH)
238 .map(|d| d.as_secs())
239 .unwrap_or(0);
240
241 if let Some(ra) = header_string(headers, "retry-after") {
243 let ra = ra.trim();
244 if let Ok(secs) = ra.parse::<u64>() {
246 let d = Duration::from_secs(secs).min(RETRY_DELAY_CAP);
247 return (d, true);
248 }
249 if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(ra) {
251 let reset_secs = dt.timestamp().max(0) as u64;
252 let wait = reset_secs.saturating_sub(now_secs);
253 let d = Duration::from_secs(wait).min(RETRY_DELAY_CAP);
254 return (d, true);
255 }
256 }
257
258 let mut min_wait: Option<u64> = None;
260 for name in &[
261 "anthropic-ratelimit-tokens-reset",
262 "anthropic-ratelimit-requests-reset",
263 ] {
264 if let Some(ts) = header_string(headers, name) {
265 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts.trim()) {
266 let reset_secs = dt.timestamp().max(0) as u64;
267 let wait = reset_secs.saturating_sub(now_secs);
268 min_wait = Some(min_wait.map_or(wait, |prev| prev.min(wait)));
269 }
270 }
271 }
272 if let Some(wait) = min_wait {
273 let d = Duration::from_secs(wait).min(RETRY_DELAY_CAP);
274 return (d, true);
275 }
276
277 let d = Duration::from_millis(1000 * 2u64.pow(attempt.saturating_sub(1)));
279 (d, false)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn level_parses_known_values() {
288 assert_eq!(TelemetryLevel::from_str_key("off"), TelemetryLevel::Off);
289 assert_eq!(TelemetryLevel::from_str_key("basic"), TelemetryLevel::Basic);
290 assert_eq!(TelemetryLevel::from_str_key("full"), TelemetryLevel::Full);
291 assert_eq!(TelemetryLevel::from_str_key("FULL"), TelemetryLevel::Full);
292 assert_eq!(TelemetryLevel::from_str_key("true"), TelemetryLevel::Basic);
293 assert_eq!(TelemetryLevel::from_str_key("garbage"), TelemetryLevel::Off);
294 assert_eq!(TelemetryLevel::from_str_key(""), TelemetryLevel::Off);
295 }
296
297 #[test]
298 fn level_enabled() {
299 assert!(!TelemetryLevel::Off.enabled());
300 assert!(TelemetryLevel::Basic.enabled());
301 assert!(TelemetryLevel::Full.enabled());
302 }
303
304 #[test]
305 fn hit_pct_computation() {
306 let mut u = UsageRecord {
307 input: 100,
308 cache_read: 800,
309 cache_write: 100,
310 ..Default::default()
311 };
312 u.compute_hit_pct();
313 assert_eq!(u.hit_pct, 80.0);
314 }
315
316 #[test]
317 fn hit_pct_zero_total() {
318 let mut u = UsageRecord::default();
319 u.compute_hit_pct();
320 assert_eq!(u.hit_pct, 0.0);
321 }
322
323 #[test]
324 fn hit_pct_rounds_to_one_decimal() {
325 let mut u = UsageRecord {
326 input: 1,
327 cache_read: 2,
328 cache_write: 0,
329 ..Default::default()
330 };
331 u.compute_hit_pct();
332 assert_eq!(u.hit_pct, 66.7);
333 }
334
335 #[test]
336 fn record_serializes_skipping_none_fields() {
337 let record = TelemetryRecord {
338 ts: 1,
339 model: "claude-sonnet-4-6".to_string(),
340 attempt: 1,
341 total_ms: 100,
342 ..Default::default()
343 };
344 let json = serde_json::to_string(&record).unwrap();
345 assert!(!json.contains("request_id"));
346 assert!(!json.contains("ratelimit"));
347 assert!(!json.contains("cache_diag"));
348 assert!(!json.contains("keepwarm"), "turn records carry no keepwarm field");
349 assert!(json.contains("\"model\":\"claude-sonnet-4-6\""));
350 }
351
352 #[test]
353 fn keepwarm_record_is_marked() {
354 let record = TelemetryRecord {
355 ts: 1,
356 model: "claude-opus-4-7".to_string(),
357 keepwarm: true,
358 ..Default::default()
359 };
360 let json = serde_json::to_string(&record).unwrap();
361 assert!(json.contains("\"keepwarm\":true"), "{json}");
362 }
363
364 #[test]
365 fn ratelimit_empty_detection() {
366 assert!(RateLimitRecord::default().is_empty());
367 let r = RateLimitRecord {
368 requests_remaining: Some(10),
369 ..Default::default()
370 };
371 assert!(!r.is_empty());
372 }
373
374 #[test]
375 fn ratelimit_parses_headers() {
376 let mut headers = reqwest::header::HeaderMap::new();
377 headers.insert(
378 "anthropic-ratelimit-requests-limit",
379 "5000".parse().unwrap(),
380 );
381 headers.insert(
382 "anthropic-ratelimit-requests-remaining",
383 "4900".parse().unwrap(),
384 );
385 headers.insert(
386 "anthropic-ratelimit-tokens-reset",
387 "2026-06-11T01:46:00Z".parse().unwrap(),
388 );
389 let r = ratelimit_from_headers(&headers);
390 assert_eq!(r.requests_limit, Some(5000));
391 assert_eq!(r.requests_remaining, Some(4900));
392 assert_eq!(r.tokens_reset.as_deref(), Some("2026-06-11T01:46:00Z"));
393 assert_eq!(r.tokens_limit, None);
394 }
395
396 #[test]
397 fn ratelimit_ignores_malformed_values() {
398 let mut headers = reqwest::header::HeaderMap::new();
399 headers.insert(
400 "anthropic-ratelimit-requests-limit",
401 "not-a-number".parse().unwrap(),
402 );
403 let r = ratelimit_from_headers(&headers);
404 assert_eq!(r.requests_limit, None);
405 }
406}
407
408#[cfg(test)]
409mod retry_delay_tests {
410 use super::*;
411 use std::time::{Duration, SystemTime, UNIX_EPOCH};
412
413 #[test]
414 fn integer_retry_after() {
415 let mut h = reqwest::header::HeaderMap::new();
416 h.insert("retry-after", "45".parse().unwrap());
417 let (d, from_hdr) = retry_delay_from_headers(&h, 1);
418 assert_eq!(d, Duration::from_secs(45));
419 assert!(from_hdr);
420 }
421
422 #[test]
423 fn integer_retry_after_capped() {
424 let mut h = reqwest::header::HeaderMap::new();
425 h.insert("retry-after", "300".parse().unwrap()); let (d, from_hdr) = retry_delay_from_headers(&h, 1);
427 assert_eq!(d, RETRY_DELAY_CAP);
428 assert!(from_hdr);
429 }
430
431 #[test]
432 fn rfc3339_reset_future() {
433 let future_secs = SystemTime::now()
434 .duration_since(UNIX_EPOCH)
435 .unwrap()
436 .as_secs()
437 + 30;
438 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(future_secs as i64, 0).unwrap();
439 let ts = dt.to_rfc3339();
440
441 let mut h = reqwest::header::HeaderMap::new();
442 h.insert("anthropic-ratelimit-tokens-reset", ts.parse().unwrap());
443 let (d, from_hdr) = retry_delay_from_headers(&h, 1);
444 assert!(
445 d.as_secs() >= 28 && d.as_secs() <= 32,
446 "unexpected delay: {:?}",
447 d
448 );
449 assert!(from_hdr);
450 }
451
452 #[test]
453 fn rfc3339_reset_beyond_cap() {
454 let future_secs = SystemTime::now()
455 .duration_since(UNIX_EPOCH)
456 .unwrap()
457 .as_secs()
458 + 600;
459 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(future_secs as i64, 0).unwrap();
460 let ts = dt.to_rfc3339();
461
462 let mut h = reqwest::header::HeaderMap::new();
463 h.insert("anthropic-ratelimit-tokens-reset", ts.parse().unwrap());
464 let (d, from_hdr) = retry_delay_from_headers(&h, 1);
465 assert_eq!(d, RETRY_DELAY_CAP);
466 assert!(from_hdr);
467 }
468
469 #[test]
470 fn no_headers_exponential_fallback() {
471 let h = reqwest::header::HeaderMap::new();
472 let (d1, hdr1) = retry_delay_from_headers(&h, 1);
473 let (d2, hdr2) = retry_delay_from_headers(&h, 2);
474 let (d3, hdr3) = retry_delay_from_headers(&h, 3);
475 assert_eq!(d1, Duration::from_secs(1));
476 assert_eq!(d2, Duration::from_secs(2));
477 assert_eq!(d3, Duration::from_secs(4));
478 assert!(!hdr1 && !hdr2 && !hdr3);
479 }
480
481 #[test]
482 fn prefers_retry_after_over_rfc3339() {
483 let future_secs = SystemTime::now()
484 .duration_since(UNIX_EPOCH)
485 .unwrap()
486 .as_secs()
487 + 30;
488 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(future_secs as i64, 0).unwrap();
489 let ts = dt.to_rfc3339();
490
491 let mut h = reqwest::header::HeaderMap::new();
492 h.insert("retry-after", "10".parse().unwrap());
493 h.insert("anthropic-ratelimit-tokens-reset", ts.parse().unwrap());
494 let (d, from_hdr) = retry_delay_from_headers(&h, 1);
495 assert_eq!(d, Duration::from_secs(10));
496 assert!(from_hdr);
497 }
498
499 #[test]
500 fn min_of_multiple_ratelimit_reset_headers() {
501 let now = SystemTime::now()
503 .duration_since(UNIX_EPOCH)
504 .unwrap()
505 .as_secs();
506 let tokens_dt =
507 chrono::DateTime::<chrono::Utc>::from_timestamp((now + 15) as i64, 0).unwrap();
508 let requests_dt =
509 chrono::DateTime::<chrono::Utc>::from_timestamp((now + 45) as i64, 0).unwrap();
510
511 let mut h = reqwest::header::HeaderMap::new();
512 h.insert(
513 "anthropic-ratelimit-tokens-reset",
514 tokens_dt.to_rfc3339().parse().unwrap(),
515 );
516 h.insert(
517 "anthropic-ratelimit-requests-reset",
518 requests_dt.to_rfc3339().parse().unwrap(),
519 );
520 let (d, from_hdr) = retry_delay_from_headers(&h, 1);
521 assert!(
522 d.as_secs() >= 13 && d.as_secs() <= 17,
523 "should be ~15s, got {:?}",
524 d
525 );
526 assert!(from_hdr);
527 }
528}