reasoning_parser/traits.rs
1use std::fmt;
2
3/// Result of parsing text for reasoning content.
4#[derive(Debug, Clone, Default, PartialEq)]
5pub struct ParserResult {
6 /// The normal text outside reasoning blocks.
7 pub normal_text: String,
8
9 /// The extracted reasoning text from within reasoning blocks.
10 pub reasoning_text: String,
11}
12
13impl ParserResult {
14 /// Create a new ParserResult with the given normal and reasoning text.
15 pub fn new(normal_text: String, reasoning_text: String) -> Self {
16 Self {
17 normal_text,
18 reasoning_text,
19 }
20 }
21
22 /// Create a result with only normal text.
23 pub fn normal(text: String) -> Self {
24 Self {
25 normal_text: text,
26 reasoning_text: String::new(),
27 }
28 }
29
30 /// Create a result with only reasoning text.
31 pub fn reasoning(text: String) -> Self {
32 Self {
33 normal_text: String::new(),
34 reasoning_text: text,
35 }
36 }
37
38 /// Check if this result contains any text.
39 pub fn is_empty(&self) -> bool {
40 self.normal_text.is_empty() && self.reasoning_text.is_empty()
41 }
42}
43
44/// Trait for parsing reasoning content from LLM outputs.
45pub trait ReasoningParser: Send + Sync {
46 /// Detects and parses reasoning from the input text (one-time parsing).
47 ///
48 /// This method is used for non-streaming scenarios where the complete
49 /// text is available at once.
50 ///
51 /// Returns an error if the text exceeds buffer limits or contains invalid UTF-8.
52 fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError>;
53
54 /// Parses reasoning incrementally from streaming input.
55 ///
56 /// This method maintains internal state across calls to handle partial
57 /// tokens and chunk boundaries correctly.
58 ///
59 /// Returns an error if the buffer exceeds max_buffer_size.
60 fn parse_reasoning_streaming_incremental(
61 &mut self,
62 text: &str,
63 ) -> Result<ParserResult, ParseError>;
64
65 /// Reset the parser state for reuse.
66 ///
67 /// This should clear any buffers and reset flags to initial state.
68 fn reset(&mut self);
69
70 /// Get the model type this parser is designed for.
71 fn model_type(&self) -> &str;
72
73 /// Check if the parser is currently in reasoning mode.
74 ///
75 /// Returns true if the parser is currently parsing reasoning content.
76 fn is_in_reasoning(&self) -> bool;
77
78 /// Mark that reasoning has already started (e.g. `<think>` was injected in the prefill).
79 ///
80 /// Called when the chat template injects `<think>` in the generation prompt,
81 /// so the parser should treat output as reasoning from the start without
82 /// waiting for a `<think>` tag in the generated output.
83 fn mark_reasoning_started(&mut self);
84
85 /// Mark that the `<think>` start token was already consumed (in the prefill).
86 ///
87 /// Prevents the streaming parser from trying to find and strip `<think>`
88 /// from the model output when the template already included it.
89 fn mark_think_start_stripped(&mut self);
90}
91
92/// Error types for reasoning parsing operations.
93#[derive(Debug, thiserror::Error)]
94pub enum ParseError {
95 #[error("Invalid UTF-8 in stream: {0}")]
96 Utf8Error(#[from] std::str::Utf8Error),
97
98 #[error("Buffer overflow: {0} bytes exceeds maximum")]
99 BufferOverflow(usize),
100
101 #[error("Unknown model type: {0}")]
102 UnknownModel(String),
103
104 #[error("Parser configuration error: {0}")]
105 ConfigError(String),
106}
107
108/// Default maximum buffer size for reasoning parsers (4MB).
109pub const DEFAULT_MAX_BUFFER_SIZE: usize = 4 * 1024 * 1024;
110
111/// Configuration for parser behavior.
112#[derive(Debug, Clone)]
113pub struct ParserConfig {
114 /// The token that marks the start of reasoning content.
115 pub think_start_token: String,
116
117 /// The token that marks the end of reasoning content.
118 pub think_end_token: String,
119
120 /// Whether to stream reasoning content as it arrives.
121 pub stream_reasoning: bool,
122
123 /// Maximum buffer size in bytes.
124 pub max_buffer_size: usize,
125
126 /// Whether this model always starts in reasoning mode (e.g. DeepSeek R1).
127 /// For models with a template thinking toggle, this should be `false` —
128 /// the runtime will call `mark_reasoning_started()` when appropriate.
129 pub always_in_reasoning: bool,
130}
131
132impl Default for ParserConfig {
133 fn default() -> Self {
134 Self {
135 think_start_token: "<think>".to_string(),
136 think_end_token: "</think>".to_string(),
137 stream_reasoning: true,
138 max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
139 always_in_reasoning: false, // Default to false (explicit reasoning)
140 }
141 }
142}
143
144impl fmt::Display for ParserResult {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(
147 f,
148 "ParserResult {{ normal: {} chars, reasoning: {} chars }}",
149 self.normal_text.len(),
150 self.reasoning_text.len()
151 )
152 }
153}