rig_core/providers/ollama/
streaming.rs1use serde::Deserialize;
17use serde_json::{Map, Value};
18
19use crate::completion::{FinishReason, Usage};
20use crate::error::ProviderError;
21use crate::json_utils::Lenient;
22use crate::message::{CallId, ToolName};
23use crate::observe::ObservedError;
24use crate::operation::{Block, Completion, Finish};
25use crate::providers::internal::wire;
26use crate::wire::{AdapterVerdict, Decoder, Flow, ObservationSink, Out, WireEvent, WireFrame};
27
28const RECORD_KEYS: &[&str] = &["message", "done", "error"];
31
32const THINK_OPEN: &str = "<think>";
34const THINK_CLOSE: &str = "</think>";
36
37#[derive(Debug, Default, Deserialize)]
39#[serde(transparent)]
40pub struct ChatRecord(pub Map<String, Value>);
41
42#[derive(Debug, Default)]
59pub struct ChatDecoder {
60 split: Split,
62 called: bool,
64 model: Option<String>,
66}
67
68#[derive(Debug)]
70enum Split {
71 Opening(String),
74 Inside { held: String, started: bool },
78 Text { trim: bool },
81}
82
83impl Default for Split {
84 fn default() -> Self {
85 Self::Opening(String::new())
86 }
87}
88
89fn partial_suffix(text: &str, tag: &str) -> usize {
92 (1..tag.len())
93 .rev()
94 .find(|&len| text.ends_with(&tag[..len]))
95 .unwrap_or(0)
96}
97
98impl<'id> Decoder<'id, Completion> for ChatDecoder {
99 type Event = ChatRecord;
100
101 fn classify(&self, frame: WireFrame) -> WireEvent<ChatRecord> {
102 wire::classify_marker_keyed_frame(&frame.as_str(), RECORD_KEYS)
103 }
104
105 fn decode(
108 &mut self,
109 ChatRecord(fields): ChatRecord,
110 mut out: Out<'id, Completion>,
111 ) -> Result<Flow, ProviderError> {
112 let record = Value::Object(fields);
113 if let Some(error) = record.get("error").filter(|error| !error.is_null()) {
114 let body = serde_json::json!({ "error": error }).to_string();
115 return Err(ProviderError::from_provider_body(body));
116 }
117 if let Some(model) = record.str("model").filter(|model| !model.is_empty())
118 && self.model.as_deref() != Some(model)
119 {
120 tracing::Span::current().record("gen_ai.response.model", model);
121 self.model = Some(model.to_owned());
122 }
123 let message = record.get("message").unwrap_or(&Value::Null);
124 if let Some(thinking) = message
125 .str("thinking")
126 .filter(|thinking| !thinking.is_empty())
127 {
128 self.release(&mut out)?;
130 reason(thinking, &mut out)?;
131 }
132 if let Some(content) = message.str("content").filter(|content| !content.is_empty()) {
133 self.content(content, &mut out)?;
134 }
135 for call in message.arr("tool_calls") {
136 self.call(call, &mut out)?;
137 }
138 if record.bool("done") != Some(true) {
139 return Ok(Flow::More);
140 }
141 self.release(&mut out)?;
142 out.end_run()?;
143 let reason = record.str("done_reason").map(|reason| match reason {
144 "stop" if self.called => FinishReason::ToolCalls,
145 "stop" => FinishReason::Stop,
146 "length" => FinishReason::Length,
147 other => FinishReason::Other(other.to_owned()),
148 });
149 let (input, output) = (record.u64("prompt_eval_count"), record.u64("eval_count"));
150 let usage = Usage {
151 input_tokens: input,
152 output_tokens: output,
153 total_tokens: input.zip(output).map(|(input, output)| input + output),
154 cached_input_tokens: record.u64("prompt_eval_cached_count"),
155 ..Usage::default()
156 };
157 Ok(out.end(Finish {
158 usage,
159 reason,
160 model: self.model.take(),
161 ..Finish::default()
162 }))
163 }
164}
165
166impl ChatDecoder {
167 fn content(
169 &mut self,
170 content: &str,
171 out: &mut Out<'_, Completion>,
172 ) -> Result<(), ProviderError> {
173 match std::mem::replace(&mut self.split, Split::Text { trim: false }) {
174 Split::Text { trim } => {
175 let text = if trim { content.trim_start() } else { content };
176 self.split = Split::Text {
177 trim: trim && text.is_empty(),
178 };
179 if !text.is_empty() {
180 out.run(Block::Text, text)?;
181 }
182 Ok(())
183 }
184 Split::Opening(mut held) => {
185 held.push_str(content);
186 let trimmed = held.trim_start();
187 if let Some(rest) = trimmed.strip_prefix(THINK_OPEN) {
188 let rest = rest.to_owned();
189 self.split = Split::Inside {
190 held: String::new(),
191 started: false,
192 };
193 self.content(&rest, out)
194 } else if THINK_OPEN.starts_with(trimmed) {
195 self.split = Split::Opening(held);
196 Ok(())
197 } else {
198 out.run(Block::Text, &held)?;
199 Ok(())
200 }
201 }
202 Split::Inside {
203 mut held,
204 mut started,
205 } => {
206 held.push_str(content);
207 if let Some((reasoning, rest)) = held.split_once(THINK_CLOSE) {
208 write_reasoning(reasoning.trim_end(), started, out)?;
209 self.split = Split::Text { trim: true };
210 return self.content(rest, out);
211 }
212 let cut = held.len() - partial_suffix(&held, THINK_CLOSE);
213 let cut = held[..cut].trim_end().len();
214 started |= write_reasoning(&held[..cut], started, out)?;
215 self.split = Split::Inside {
216 held: held.split_off(cut),
217 started,
218 };
219 Ok(())
220 }
221 }
222 }
223
224 fn release(&mut self, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
227 match std::mem::replace(&mut self.split, Split::Text { trim: false }) {
228 Split::Opening(held) if !held.is_empty() => {
229 out.run(Block::Text, &held)?;
230 }
231 Split::Inside { held, started } => {
232 write_reasoning(held.trim_end(), started, out)?;
233 }
234 Split::Opening(_) | Split::Text { .. } => {}
235 }
236 Ok(())
237 }
238
239 fn call(&mut self, call: &Value, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
242 let Ok(name) = ToolName::new(
243 call.at("/function/name")
244 .and_then(Value::as_str)
245 .unwrap_or_default(),
246 ) else {
247 tracing::warn!("Ollama sent a tool call without a name; nothing can answer it");
248 return Ok(());
249 };
250 let arguments = match call.at("/function/arguments") {
251 None | Some(Value::Null) => "{}".to_owned(),
252 Some(Value::String(arguments)) => arguments.clone(),
253 Some(arguments) => arguments.to_string(),
254 };
255 self.release(out)?;
256 out.end_run()?;
257 self.called = true;
258 let id = CallId::from_wire(call.str("id").unwrap_or_default());
259 let index = out.fresh_index();
260 out.open(index, Block::Call { id, name }, call.clone())?;
261 out.push(index, &arguments)?;
262 out.finish(index)
263 }
264
265 pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
269 let Ok(record) = serde_json::from_slice::<Value>(payload) else {
270 return;
271 };
272 let verdict = match record.str("done_reason") {
273 Some(reason) => AdapterVerdict {
274 finish_reason: Some(sink.scrub(reason)),
275 block_reason: None,
276 detail: None,
277 model: record.str("model").map(|model| sink.scrub(model)),
278 },
279 None => AdapterVerdict::default(),
280 };
281 sink.provider(verdict, None);
282 if let Some(error) = ObservedError::of(&record) {
283 error.emit(sink);
284 }
285 }
286}
287
288fn write_reasoning(
291 text: &str,
292 started: bool,
293 out: &mut Out<'_, Completion>,
294) -> Result<bool, ProviderError> {
295 let text = if started { text } else { text.trim_start() };
296 if text.is_empty() {
297 return Ok(false);
298 }
299 reason(text, out)?;
300 Ok(true)
301}
302
303fn reason(text: &str, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
306 let index = out.run(Block::Reasoning { redacted: false }, text)?;
307 out.edit(index, |item| match item {
308 Value::Object(fields) => {
309 if let Some(Value::String(thinking)) = fields.get_mut("thinking") {
310 thinking.push_str(text);
311 }
312 }
313 _ => *item = serde_json::json!({ "thinking": text }),
314 })
315}
316
317pub(crate) mod document;
318
319#[cfg(test)]
320mod tests;