Skip to main content

obol_core/transcript/
mod.rs

1pub mod claude;
2pub mod codex;
3pub mod pi;
4
5use crate::error::ObolError;
6use crate::model::MessageUsage;
7use serde_json::Value;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Dialect {
11    Claude,
12    Codex,
13    Pi,
14}
15
16/// Detect dialect from content: Codex lines carry a top-level `payload`
17/// (session_meta/response_item/event_msg); Claude lines carry `message` with
18/// type user/assistant.
19pub fn detect(bytes: &[u8]) -> Result<Dialect, ObolError> {
20    let text = std::str::from_utf8(bytes).map_err(|_| ObolError::UnknownDialect)?;
21    for line in text.lines().take(20) {
22        let line = line.trim();
23        if line.is_empty() {
24            continue;
25        }
26        let v: Value = match serde_json::from_str(line) {
27            Ok(v) => v,
28            Err(_) => continue,
29        };
30        if v.get("payload").is_some() {
31            return Ok(Dialect::Codex);
32        }
33        if v.get("type").and_then(Value::as_str) == Some("session") {
34            return Ok(Dialect::Pi);
35        }
36        let ty = v.get("type").and_then(Value::as_str);
37        if matches!(ty, Some("user") | Some("assistant")) && v.get("message").is_some() {
38            return Ok(Dialect::Claude);
39        }
40    }
41    Err(ObolError::UnknownDialect)
42}
43
44pub fn parse(bytes: &[u8], dialect: Dialect) -> Result<Vec<MessageUsage>, ObolError> {
45    match dialect {
46        Dialect::Claude => Ok(claude::parse(bytes)?.usages),
47        Dialect::Codex => codex::parse(bytes),
48        Dialect::Pi => pi::parse(bytes),
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn detects_claude_and_codex() {
58        let claude = include_bytes!("../../tests/fixtures/claude-mini.jsonl");
59        let codex = include_bytes!("../../tests/fixtures/codex-mini.jsonl");
60        assert_eq!(detect(claude).unwrap(), Dialect::Claude);
61        assert_eq!(detect(codex).unwrap(), Dialect::Codex);
62    }
63
64    #[test]
65    fn detects_pi() {
66        let pi = include_bytes!("../../tests/fixtures/pi-mini.jsonl");
67        assert_eq!(detect(pi).unwrap(), Dialect::Pi);
68    }
69
70    #[test]
71    fn unknown_dialect_errors() {
72        assert!(matches!(detect(b"{}\n{}"), Err(ObolError::UnknownDialect)));
73    }
74}