Skip to main content

gglib_core/normalize/
error.rs

1//! Non-fatal error reporting from normalization parsers.
2//!
3//! Parsers in [`super::parsers`] never `Result`-fail at the trait level —
4//! a malformed dialect fragment is data, not an infrastructure problem.
5//! Instead, parsers attach a [`NormalizationError`] to their
6//! [`super::parser::ParserOutput`], and the surrounding stream wrapper
7//! surfaces those errors as
8//! `LlmStreamEvent::NormalizationError` events.
9//!
10//! Consumers are free to log, surface, or suppress these events.  The proxy
11//! drops them on the wire (V1 contract); in-process consumers (Axum/Tauri)
12//! receive them for diagnostics.
13
14/// Discriminates the kind of malformation a parser detected.
15///
16/// Each variant carries enough context that a developer reading a log line
17/// can identify the offending bytes without rerunning the model.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum NormalizationErrorKind {
20    /// Found a complete `<tool_call>...</tool_call>` block but its body did
21    /// not parse as a JSON object with at least a `name` field.
22    ///
23    /// `raw` holds the body bytes between the open and close tags.
24    MalformedToolCallJson { raw: String },
25
26    /// The stream ended while we were still inside an open `<tool_call>`
27    /// tag.  `partial` is the JSON body collected so far (which may be
28    /// empty if only the open tag was seen).
29    UnclosedToolCallTag { partial: String },
30
31    /// Found a complete `<tool_call>...</tool_call>` block whose body began
32    /// with `<function=` (the Qwen3/Hermes inner-XML dialect) but did not
33    /// match that dialect's `<function=NAME><parameter=KEY>VALUE</parameter>...</function>`
34    /// shape.
35    ///
36    /// `raw` holds the body bytes between the open and close tags.
37    MalformedFunctionXml { raw: String },
38}
39
40/// A non-fatal normalization issue surfaced from a parser.
41///
42/// `kind` carries the structured failure details; `raw` is a short snippet
43/// of the offending input suitable for log output.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct NormalizationError {
46    /// Structured detail about what went wrong.
47    pub kind: NormalizationErrorKind,
48    /// A short, human-readable excerpt of the offending input.  Parsers
49    /// should keep this small (≲ 256 bytes) so it is safe to attach to a
50    /// stream event.
51    pub raw: String,
52}
53
54impl NormalizationError {
55    /// Construct a `MalformedToolCallJson` error with the body as `raw`.
56    #[must_use]
57    pub fn malformed_tool_call(body: impl Into<String>) -> Self {
58        let raw = body.into();
59        Self {
60            kind: NormalizationErrorKind::MalformedToolCallJson { raw: raw.clone() },
61            raw,
62        }
63    }
64
65    /// Construct an `UnclosedToolCallTag` error from a partial body.
66    #[must_use]
67    pub fn unclosed_tool_call(partial: impl Into<String>) -> Self {
68        let partial = partial.into();
69        Self {
70            kind: NormalizationErrorKind::UnclosedToolCallTag {
71                partial: partial.clone(),
72            },
73            raw: partial,
74        }
75    }
76
77    /// Construct a `MalformedFunctionXml` error with the body as `raw`.
78    #[must_use]
79    pub fn malformed_function_xml(body: impl Into<String>) -> Self {
80        let raw = body.into();
81        Self {
82            kind: NormalizationErrorKind::MalformedFunctionXml { raw: raw.clone() },
83            raw,
84        }
85    }
86}