gglib_core/normalize/parsers/
standard.rs

1//! Identity-passthrough parser for models that already speak strict `OpenAI`.
2//!
3//! The vast majority of models we run today emit clean
4//! `LlmStreamEvent::ToolCallDelta` events directly from llama-server's
5//! `tool_calls` field.  For those models the registry returns a
6//! [`StandardJsonParser`], which forwards every byte unchanged and
7//! never synthesises tool calls or errors.
8//!
9//! This parser is also the default fallback when no `format:*` tag matches.
10
11use super::super::parser::{ParserOutput, ToolCallParser};
12
13/// Identity-passthrough parser.  See module docs.
14#[derive(Default, Debug)]
15pub struct StandardJsonParser;
16
17impl StandardJsonParser {
18    /// Construct a fresh parser.  No state, so this is just `Default::default`.
19    #[must_use]
20    pub const fn new() -> Self {
21        Self
22    }
23}
24
25impl ToolCallParser for StandardJsonParser {
26    fn push_text(&mut self, chunk: &str) -> ParserOutput {
27        ParserOutput::text(chunk)
28    }
29
30    fn push_reasoning(&mut self, chunk: &str) -> ParserOutput {
31        ParserOutput::reasoning(chunk)
32    }
33
34    fn finish(&mut self) -> ParserOutput {
35        ParserOutput::default()
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn text_passthrough_is_byte_identical() {
45        let mut p = StandardJsonParser::new();
46        let out = p.push_text("hello world");
47        assert_eq!(out.forward_text, "hello world");
48        assert!(out.forward_reasoning.is_empty());
49        assert!(out.tool_calls.is_empty());
50        assert!(out.errors.is_empty());
51    }
52
53    #[test]
54    fn reasoning_passthrough_is_byte_identical() {
55        let mut p = StandardJsonParser::new();
56        let out = p.push_reasoning("thinking…");
57        assert_eq!(out.forward_reasoning, "thinking…");
58        assert!(out.forward_text.is_empty());
59    }
60
61    #[test]
62    fn finish_emits_nothing() {
63        let mut p = StandardJsonParser::new();
64        let _ = p.push_text("abc");
65        let out = p.finish();
66        assert!(out.is_empty());
67    }
68
69    #[test]
70    fn many_chunks_preserve_total_bytes() {
71        let mut p = StandardJsonParser::new();
72        let mut acc = String::new();
73        for c in [
74            "foo",
75            "<tool_call>",
76            "{\"name\":\"x\"}",
77            "</tool_call>",
78            "bar",
79        ] {
80            acc.push_str(&p.push_text(c).forward_text);
81        }
82        // StandardJsonParser is identity, so even XML-looking input is
83        // preserved verbatim.  Dispatch into the Qwen parser is the
84        // registry's job, not this parser's.
85        assert_eq!(acc, "foo<tool_call>{\"name\":\"x\"}</tool_call>bar");
86    }
87}