Skip to main content

alex_core/
lib.rs

1pub mod translate;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum Provider {
9    Anthropic,
10    Openai,
11    Gemini,
12    Xai,
13}
14
15impl Provider {
16    pub fn as_str(self) -> &'static str {
17        match self {
18            Provider::Anthropic => "anthropic",
19            Provider::Openai => "openai",
20            Provider::Gemini => "gemini",
21            Provider::Xai => "xai",
22        }
23    }
24
25    pub fn from_str_loose(s: &str) -> Option<Provider> {
26        match s.to_lowercase().as_str() {
27            "anthropic" | "claude" => Some(Provider::Anthropic),
28            "openai" | "codex" | "chatgpt" => Some(Provider::Openai),
29            "gemini" | "google" => Some(Provider::Gemini),
30            "xai" | "grok" => Some(Provider::Xai),
31            _ => None,
32        }
33    }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ClientFormat {
38    AnthropicMessages,
39    OpenaiChat,
40    OpenaiResponses,
41    GeminiGenerate,
42}
43
44impl ClientFormat {
45    pub fn as_str(self) -> &'static str {
46        match self {
47            ClientFormat::AnthropicMessages => "anthropic",
48            ClientFormat::OpenaiChat => "openai-chat",
49            ClientFormat::OpenaiResponses => "openai-responses",
50            ClientFormat::GeminiGenerate => "gemini",
51        }
52    }
53
54    pub fn default_provider(self) -> Provider {
55        match self {
56            ClientFormat::AnthropicMessages => Provider::Anthropic,
57            ClientFormat::OpenaiChat | ClientFormat::OpenaiResponses => Provider::Openai,
58            ClientFormat::GeminiGenerate => Provider::Gemini,
59        }
60    }
61}
62
63const PREFIXES: &[(&str, Provider)] = &[
64    ("claude:", Provider::Anthropic),
65    ("anthropic:", Provider::Anthropic),
66    ("openai:", Provider::Openai),
67    ("codex:", Provider::Openai),
68    ("gemini:", Provider::Gemini),
69    ("grok:", Provider::Xai),
70    ("xai:", Provider::Xai),
71    ("claude/", Provider::Anthropic),
72    ("anthropic/", Provider::Anthropic),
73    ("openai/", Provider::Openai),
74    ("codex/", Provider::Openai),
75    ("chatgpt/", Provider::Openai),
76    ("gemini/", Provider::Gemini),
77    ("google/", Provider::Gemini),
78    ("grok/", Provider::Xai),
79    ("xai/", Provider::Xai),
80];
81
82const PASSTHROUGH: &[&str] = &["cove/", "alexandria/"];
83
84const ALIASES: &[(&str, &str)] = &[
85    ("opus-4.8", "claude-opus-4-8"),
86    ("opus-4.5", "claude-opus-4-5"),
87    ("sonnet-5", "claude-sonnet-5"),
88    ("sonnet-4.5", "claude-sonnet-4-5"),
89    ("haiku-4.5", "claude-haiku-4-5"),
90];
91
92pub fn model_aliases() -> &'static [(&'static str, &'static str)] {
93    ALIASES
94}
95
96fn hs<'a>(h: &'a Value, key: &str) -> Option<&'a str> {
97    h.get(key).and_then(|v| v.as_str())
98}
99
100fn hf(h: &Value, key: &str) -> Option<f64> {
101    hs(h, key).and_then(|s| s.parse().ok())
102}
103
104fn hi(h: &Value, key: &str) -> Option<i64> {
105    hs(h, key).and_then(|s| s.parse().ok())
106}
107
108pub fn parse_limit_headers(provider: Provider, h: &Value) -> Value {
109    match provider {
110        Provider::Anthropic => {
111            let mut windows = Vec::new();
112            for (name, prefix) in [
113                ("5h", "anthropic-ratelimit-unified-5h"),
114                ("7d", "anthropic-ratelimit-unified-7d"),
115            ] {
116                if let Some(util) = hf(h, &format!("{prefix}-utilization")) {
117                    windows.push(serde_json::json!({
118                        "window": name,
119                        "used_pct": util * 100.0,
120                        "status": hs(h, &format!("{prefix}-status")),
121                        "resets_at_s": hi(h, &format!("{prefix}-reset")),
122                    }));
123                }
124            }
125            serde_json::json!({
126                "windows": windows,
127                "representative_window": hs(h, "anthropic-ratelimit-unified-representative-claim"),
128                "overage": {
129                    "status": hs(h, "anthropic-ratelimit-unified-overage-status"),
130                    "reason": hs(h, "anthropic-ratelimit-unified-overage-disabled-reason"),
131                },
132            })
133        }
134        Provider::Openai => {
135            let mut windows = Vec::new();
136            for prefix in ["x-codex-primary", "x-codex-secondary"] {
137                if let Some(used) = hf(h, &format!("{prefix}-used-percent")) {
138                    let minutes = hi(h, &format!("{prefix}-window-minutes"));
139                    let name = match minutes {
140                        Some(300) => "5h".to_string(),
141                        Some(10080) => "7d".to_string(),
142                        Some(m) => format!("{m}m"),
143                        None => "unknown".to_string(),
144                    };
145                    windows.push(serde_json::json!({
146                        "window": name,
147                        "used_pct": used,
148                        "resets_at_s": hi(h, &format!("{prefix}-reset-at")),
149                    }));
150                }
151            }
152            serde_json::json!({
153                "plan": hs(h, "x-codex-plan-type"),
154                "active_limit": hs(h, "x-codex-active-limit"),
155                "windows": windows,
156                "credits": {
157                    "balance": hs(h, "x-codex-credits-balance"),
158                    "has_credits": hs(h, "x-codex-credits-has-credits"),
159                    "unlimited": hs(h, "x-codex-credits-unlimited"),
160                },
161            })
162        }
163        Provider::Xai => serde_json::json!({
164            "requests": {
165                "limit": hi(h, "x-ratelimit-limit-requests"),
166                "remaining": hi(h, "x-ratelimit-remaining-requests"),
167            },
168            "tokens": {
169                "limit": hi(h, "x-ratelimit-limit-tokens"),
170                "remaining": hi(h, "x-ratelimit-remaining-tokens"),
171            },
172        }),
173        Provider::Gemini => Value::Null,
174    }
175}
176
177fn resolve_alias(model: &str) -> String {
178    for (alias, full) in ALIASES {
179        if model == *alias {
180            return full.to_string();
181        }
182    }
183    model.to_string()
184}
185
186pub fn route_model(model: &str) -> (Option<Provider>, String) {
187    for prefix in PASSTHROUGH {
188        if let Some(rest) = model.strip_prefix(prefix) {
189            return route_model(rest);
190        }
191    }
192    for (prefix, provider) in PREFIXES {
193        if let Some(rest) = model.strip_prefix(prefix) {
194            return (Some(*provider), resolve_alias(rest));
195        }
196    }
197    let model = resolve_alias(model);
198    let lower = model.to_lowercase();
199    let inferred = if lower.starts_with("claude") {
200        Some(Provider::Anthropic)
201    } else if lower.starts_with("gpt")
202        || lower.starts_with("codex")
203        || lower.starts_with("chatgpt")
204        || is_o_series(&lower)
205    {
206        Some(Provider::Openai)
207    } else if lower.starts_with("gemini") {
208        Some(Provider::Gemini)
209    } else if lower.starts_with("grok") {
210        Some(Provider::Xai)
211    } else {
212        None
213    };
214    (inferred, model.to_string())
215}
216
217fn is_o_series(lower: &str) -> bool {
218    let mut chars = lower.chars();
219    chars.next() == Some('o') && chars.next().map(|c| c.is_ascii_digit()).unwrap_or(false)
220}
221
222#[derive(Debug, Default, Clone, Serialize, Deserialize)]
223pub struct Usage {
224    pub input_tokens: Option<i64>,
225    pub cached_input_tokens: Option<i64>,
226    pub cache_creation_tokens: Option<i64>,
227    pub output_tokens: Option<i64>,
228    pub reasoning_tokens: Option<i64>,
229}
230
231impl Usage {
232    pub fn merge(&mut self, other: Usage) {
233        if other.input_tokens.is_some() {
234            self.input_tokens = other.input_tokens;
235        }
236        if other.cached_input_tokens.is_some() {
237            self.cached_input_tokens = other.cached_input_tokens;
238        }
239        if other.cache_creation_tokens.is_some() {
240            self.cache_creation_tokens = other.cache_creation_tokens;
241        }
242        if other.output_tokens.is_some() {
243            self.output_tokens = other.output_tokens;
244        }
245        if other.reasoning_tokens.is_some() {
246            self.reasoning_tokens = other.reasoning_tokens;
247        }
248    }
249
250    pub fn is_empty(&self) -> bool {
251        self.input_tokens.is_none() && self.output_tokens.is_none()
252    }
253}
254
255fn path_i64(v: &Value, path: &[&str]) -> Option<i64> {
256    let mut cur = v;
257    for p in path {
258        cur = cur.get(p)?;
259    }
260    cur.as_i64()
261}
262
263pub fn usage_from_obj(o: &Value) -> Usage {
264    Usage {
265        input_tokens: path_i64(o, &["input_tokens"])
266            .or_else(|| path_i64(o, &["prompt_tokens"]))
267            .or_else(|| path_i64(o, &["promptTokenCount"])),
268        cached_input_tokens: path_i64(o, &["cache_read_input_tokens"])
269            .or_else(|| path_i64(o, &["prompt_tokens_details", "cached_tokens"]))
270            .or_else(|| path_i64(o, &["input_tokens_details", "cached_tokens"]))
271            .or_else(|| path_i64(o, &["cachedContentTokenCount"])),
272        cache_creation_tokens: path_i64(o, &["cache_creation_input_tokens"]),
273        output_tokens: path_i64(o, &["output_tokens"])
274            .or_else(|| path_i64(o, &["completion_tokens"]))
275            .or_else(|| path_i64(o, &["candidatesTokenCount"])),
276        reasoning_tokens: path_i64(o, &["completion_tokens_details", "reasoning_tokens"])
277            .or_else(|| path_i64(o, &["output_tokens_details", "reasoning_tokens"]))
278            .or_else(|| path_i64(o, &["thoughtsTokenCount"])),
279    }
280}
281
282pub fn usage_from_json(v: &Value) -> Usage {
283    let mut usage = Usage::default();
284    for loc in [
285        &v["usage"],
286        &v["message"]["usage"],
287        &v["response"]["usage"],
288        &v["usageMetadata"],
289        &v["response"]["usageMetadata"],
290    ] {
291        if loc.is_object() {
292            usage.merge(usage_from_obj(loc));
293        }
294    }
295    usage
296}
297
298pub fn parse_sse_usage(body: &str) -> Usage {
299    let mut usage = Usage::default();
300    for line in body.lines() {
301        let Some(data) = line.strip_prefix("data:") else {
302            continue;
303        };
304        let data = data.trim();
305        if data.is_empty() || data == "[DONE]" {
306            continue;
307        }
308        let Ok(v) = serde_json::from_str::<Value>(data) else {
309            continue;
310        };
311        usage.merge(usage_from_json(&v));
312    }
313    usage
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct Pricing {
318    pub input_per_m: f64,
319    pub cached_input_per_m: f64,
320    pub cache_creation_per_m: f64,
321    pub output_per_m: f64,
322}
323
324pub fn compute_cost(usage: &Usage, pricing: &Pricing, input_includes_cached: bool) -> f64 {
325    let input = usage.input_tokens.unwrap_or(0) as f64;
326    let cached = usage.cached_input_tokens.unwrap_or(0) as f64;
327    let creation = usage.cache_creation_tokens.unwrap_or(0) as f64;
328    let output = usage.output_tokens.unwrap_or(0) as f64;
329    let uncached_input = if input_includes_cached {
330        (input - cached).max(0.0)
331    } else {
332        input
333    };
334    (uncached_input * pricing.input_per_m
335        + cached * pricing.cached_input_per_m
336        + creation * pricing.cache_creation_per_m
337        + output * pricing.output_per_m)
338        / 1_000_000.0
339}
340
341#[derive(Debug, Default, Clone, Serialize, Deserialize)]
342pub struct TraceRecord {
343    pub id: String,
344    pub ts_request_ms: i64,
345    pub ts_response_ms: Option<i64>,
346    pub session_id: Option<String>,
347    pub harness: Option<String>,
348    pub client_format: Option<String>,
349    pub upstream_provider: Option<String>,
350    pub upstream_format: Option<String>,
351    pub requested_model: Option<String>,
352    pub routed_model: Option<String>,
353    pub method: Option<String>,
354    pub path: Option<String>,
355    pub status: Option<i64>,
356    pub streamed: Option<bool>,
357    pub usage: Usage,
358    pub cost_usd: Option<f64>,
359    pub billing_bucket: Option<String>,
360    pub req_body_path: Option<String>,
361    pub upstream_req_body_path: Option<String>,
362    pub resp_body_path: Option<String>,
363    pub req_headers_json: Option<String>,
364    pub resp_headers_json: Option<String>,
365    pub error: Option<String>,
366    pub account_id: Option<String>,
367    pub run_id: Option<String>,
368    pub tags: Option<String>,
369    pub client_ip: Option<String>,
370    pub key_fingerprint: Option<String>,
371}
372
373pub fn parse_trace_tags(values: &[&str]) -> Value {
374    let mut map = serde_json::Map::new();
375    for v in values {
376        for piece in v.split(',') {
377            let Some((k, val)) = piece.split_once('=') else {
378                continue;
379            };
380            let k = k.trim();
381            if k.is_empty() {
382                continue;
383            }
384            map.insert(k.to_string(), Value::String(val.trim().to_string()));
385        }
386    }
387    Value::Object(map)
388}
389
390pub fn conversation_root(format: ClientFormat, body: &Value) -> Option<String> {
391    let (system, user) = match format {
392        ClientFormat::AnthropicMessages => {
393            let system = translate::txt(&body["system"]);
394            let user = body["messages"]
395                .as_array()
396                .into_iter()
397                .flatten()
398                .find(|m| m["role"] == "user")
399                .map(|m| translate::txt(&m["content"]))
400                .unwrap_or_default();
401            (system, user)
402        }
403        ClientFormat::OpenaiChat => {
404            let msgs = body["messages"].as_array();
405            let find = |roles: &[&str]| {
406                msgs.into_iter()
407                    .flatten()
408                    .find(|m| roles.contains(&m["role"].as_str().unwrap_or("")))
409                    .map(|m| translate::txt(&m["content"]))
410                    .unwrap_or_default()
411            };
412            (find(&["system", "developer"]), find(&["user"]))
413        }
414        ClientFormat::OpenaiResponses => {
415            let system = body["instructions"].as_str().unwrap_or("").to_string();
416            let user = match &body["input"] {
417                Value::String(s) => s.clone(),
418                Value::Array(items) => items
419                    .iter()
420                    .find(|it| {
421                        it["role"] == "user"
422                            && it["type"].as_str().unwrap_or("message") == "message"
423                    })
424                    .map(|it| translate::txt(&it["content"]))
425                    .unwrap_or_default(),
426                _ => String::new(),
427            };
428            (system, user)
429        }
430        ClientFormat::GeminiGenerate => {
431            let system = translate::gemini_parts_text(&body["systemInstruction"]["parts"]);
432            let user = body["contents"]
433                .as_array()
434                .into_iter()
435                .flatten()
436                .find(|c| c["role"].as_str().unwrap_or("user") == "user")
437                .map(|c| translate::gemini_parts_text(&c["parts"]))
438                .unwrap_or_default();
439            (system, user)
440        }
441    };
442    let system = system.trim();
443    let user = user.trim();
444    if system.is_empty() && user.is_empty() {
445        None
446    } else {
447        Some(format!("{system}\n{user}"))
448    }
449}
450
451pub fn parse_since(s: &str, now_ms: i64) -> Option<i64> {
452    let s = s.trim();
453    if s.is_empty() {
454        return None;
455    }
456    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
457        return Some(dt.timestamp_millis());
458    }
459    let unit = s.chars().last()?;
460    let num = &s[..s.len() - unit.len_utf8()];
461    let n: i64 = num.parse().ok()?;
462    if n < 0 {
463        return None;
464    }
465    let ms = match unit {
466        's' => n.checked_mul(1_000)?,
467        'm' => n.checked_mul(60_000)?,
468        'h' => n.checked_mul(3_600_000)?,
469        'd' => n.checked_mul(86_400_000)?,
470        _ => return None,
471    };
472    Some(now_ms - ms)
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn routes_prefixes() {
481        assert_eq!(
482            route_model("claude:claude-sonnet-4-5").0,
483            Some(Provider::Anthropic)
484        );
485        assert_eq!(route_model("openai:gpt-5.1").1, "gpt-5.1");
486        assert_eq!(route_model("gpt-5-codex").0, Some(Provider::Openai));
487        assert_eq!(route_model("o3-mini").0, Some(Provider::Openai));
488        assert_eq!(route_model("mystery-model").0, None);
489    }
490
491    #[test]
492    fn routes_slash_prefixes() {
493        assert_eq!(
494            route_model("claude/claude-sonnet-4-5"),
495            (Some(Provider::Anthropic), "claude-sonnet-4-5".to_string())
496        );
497        assert_eq!(route_model("anthropic/x").0, Some(Provider::Anthropic));
498        assert_eq!(route_model("openai/gpt-5.5").1, "gpt-5.5");
499        assert_eq!(route_model("codex/gpt-5-codex").0, Some(Provider::Openai));
500        assert_eq!(route_model("chatgpt/gpt-5.1").0, Some(Provider::Openai));
501        assert_eq!(route_model("gemini/gemini-3-pro").0, Some(Provider::Gemini));
502        assert_eq!(route_model("google/gemini-3-pro").0, Some(Provider::Gemini));
503        assert_eq!(route_model("grok/grok-4").0, Some(Provider::Xai));
504        assert_eq!(route_model("xai/grok-4").0, Some(Provider::Xai));
505    }
506
507    #[test]
508    fn routes_passthrough_prefixes() {
509        assert_eq!(
510            route_model("alexandria/gpt-5.5"),
511            (Some(Provider::Openai), "gpt-5.5".to_string())
512        );
513        assert_eq!(
514            route_model("cove/claude-opus-4-8"),
515            (Some(Provider::Anthropic), "claude-opus-4-8".to_string())
516        );
517        assert_eq!(
518            route_model("cove/openai:gpt-5.1"),
519            (Some(Provider::Openai), "gpt-5.1".to_string())
520        );
521    }
522
523    #[test]
524    fn routes_aliases() {
525        assert_eq!(
526            route_model("opus-4.8"),
527            (Some(Provider::Anthropic), "claude-opus-4-8".to_string())
528        );
529        assert_eq!(
530            route_model("opus-4.5"),
531            (Some(Provider::Anthropic), "claude-opus-4-5".to_string())
532        );
533        assert_eq!(
534            route_model("sonnet-5"),
535            (Some(Provider::Anthropic), "claude-sonnet-5".to_string())
536        );
537        assert_eq!(
538            route_model("sonnet-4.5"),
539            (Some(Provider::Anthropic), "claude-sonnet-4-5".to_string())
540        );
541        assert_eq!(
542            route_model("haiku-4.5"),
543            (Some(Provider::Anthropic), "claude-haiku-4-5".to_string())
544        );
545        assert_eq!(
546            route_model("claude/opus-4.8"),
547            (Some(Provider::Anthropic), "claude-opus-4-8".to_string())
548        );
549        assert_eq!(
550            route_model("alexandria/sonnet-5"),
551            (Some(Provider::Anthropic), "claude-sonnet-5".to_string())
552        );
553        assert_eq!(model_aliases().len(), 5);
554    }
555
556    #[test]
557    fn parses_anthropic_sse() {
558        let sse = "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"usage\":{\"input_tokens\":10,\"cache_read_input_tokens\":4,\"output_tokens\":1}}}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"usage\":{\"output_tokens\":25}}\n\n";
559        let u = parse_sse_usage(sse);
560        assert_eq!(u.input_tokens, Some(10));
561        assert_eq!(u.cached_input_tokens, Some(4));
562        assert_eq!(u.output_tokens, Some(25));
563    }
564
565    #[test]
566    fn parses_trace_tags() {
567        let v = parse_trace_tags(&["suite=swebench", "case=astropy-123", "malformed", "=nokey"]);
568        assert_eq!(v["suite"], "swebench");
569        assert_eq!(v["case"], "astropy-123");
570        assert_eq!(v.as_object().unwrap().len(), 2);
571        assert_eq!(parse_trace_tags(&[]), serde_json::json!({}));
572        let padded = parse_trace_tags(&[" k = v "]);
573        assert_eq!(padded["k"], "v");
574    }
575
576    #[test]
577    fn parses_coalesced_trace_tags() {
578        let v = parse_trace_tags(&["harness=codex,task=smoke,model=gpt-5.5"]);
579        assert_eq!(v["harness"], "codex");
580        assert_eq!(v["task"], "smoke");
581        assert_eq!(v["model"], "gpt-5.5");
582        assert_eq!(v.as_object().unwrap().len(), 3);
583        let mixed = parse_trace_tags(&["a=1, b = 2 ,junk,=x", "c=3"]);
584        assert_eq!(mixed["a"], "1");
585        assert_eq!(mixed["b"], "2");
586        assert_eq!(mixed["c"], "3");
587        assert_eq!(mixed.as_object().unwrap().len(), 3);
588    }
589
590    #[test]
591    fn conversation_root_anthropic() {
592        let body = serde_json::json!({
593            "system": [{"type": "text", "text": "sys"}],
594            "messages": [
595                {"role": "assistant", "content": "prior"},
596                {"role": "user", "content": [{"type": "text", "text": "hi"}]},
597            ],
598        });
599        assert_eq!(
600            conversation_root(ClientFormat::AnthropicMessages, &body),
601            Some("sys\nhi".to_string())
602        );
603        let plain = serde_json::json!({
604            "system": "s",
605            "messages": [{"role": "user", "content": "u"}],
606        });
607        assert_eq!(
608            conversation_root(ClientFormat::AnthropicMessages, &plain),
609            Some("s\nu".to_string())
610        );
611        assert_eq!(
612            conversation_root(ClientFormat::AnthropicMessages, &serde_json::json!({})),
613            None
614        );
615    }
616
617    #[test]
618    fn conversation_root_openai_chat() {
619        let body = serde_json::json!({
620            "messages": [
621                {"role": "developer", "content": "dev"},
622                {"role": "user", "content": [{"type": "text", "text": "q"}]},
623            ],
624        });
625        assert_eq!(
626            conversation_root(ClientFormat::OpenaiChat, &body),
627            Some("dev\nq".to_string())
628        );
629        let user_only = serde_json::json!({"messages": [{"role": "user", "content": "solo"}]});
630        assert_eq!(
631            conversation_root(ClientFormat::OpenaiChat, &user_only),
632            Some("\nsolo".to_string())
633        );
634        assert_eq!(
635            conversation_root(ClientFormat::OpenaiChat, &serde_json::json!({"messages": []})),
636            None
637        );
638    }
639
640    #[test]
641    fn conversation_root_openai_responses() {
642        let body = serde_json::json!({
643            "instructions": "inst",
644            "input": [
645                {"type": "message", "role": "user",
646                 "content": [{"type": "input_text", "text": "first"}]},
647            ],
648        });
649        assert_eq!(
650            conversation_root(ClientFormat::OpenaiResponses, &body),
651            Some("inst\nfirst".to_string())
652        );
653        let string_input = serde_json::json!({"input": "plain"});
654        assert_eq!(
655            conversation_root(ClientFormat::OpenaiResponses, &string_input),
656            Some("\nplain".to_string())
657        );
658        assert_eq!(
659            conversation_root(ClientFormat::OpenaiResponses, &serde_json::json!({"input": []})),
660            None
661        );
662    }
663
664    #[test]
665    fn parses_since_relative() {
666        let now = 1_000_000_000_000;
667        assert_eq!(parse_since("45s", now), Some(now - 45_000));
668        assert_eq!(parse_since("30m", now), Some(now - 1_800_000));
669        assert_eq!(parse_since("2h", now), Some(now - 7_200_000));
670        assert_eq!(parse_since("7d", now), Some(now - 604_800_000));
671    }
672
673    #[test]
674    fn parses_since_rfc3339() {
675        assert_eq!(
676            parse_since("2024-01-01T00:00:00Z", 0),
677            Some(1_704_067_200_000)
678        );
679        assert_eq!(
680            parse_since("2024-01-01T02:00:00+02:00", 0),
681            Some(1_704_067_200_000)
682        );
683    }
684
685    #[test]
686    fn rejects_garbage_since() {
687        assert_eq!(parse_since("", 0), None);
688        assert_eq!(parse_since("yesterday", 0), None);
689        assert_eq!(parse_since("30x", 0), None);
690        assert_eq!(parse_since("m", 0), None);
691        assert_eq!(parse_since("-5m", 0), None);
692        assert_eq!(parse_since("3é", 0), None);
693    }
694
695    #[test]
696    fn parses_openai_responses_sse() {
697        let sse = "data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":100,\"input_tokens_details\":{\"cached_tokens\":20},\"output_tokens\":30,\"output_tokens_details\":{\"reasoning_tokens\":5}}}}\n";
698        let u = parse_sse_usage(sse);
699        assert_eq!(u.input_tokens, Some(100));
700        assert_eq!(u.cached_input_tokens, Some(20));
701        assert_eq!(u.reasoning_tokens, Some(5));
702    }
703}