use serde::{Deserialize, Serialize};
use super::{images::Image, tools::ToolCall};
use crate::{
error::OllamaError, generation::parameters::LogprobsData, history::ChatHistory, Ollama,
};
use request::ChatMessageRequest;
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
use async_stream::stream;
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
use std::sync::{Arc, Mutex};
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
use tokio_stream::StreamExt;
pub mod request;
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
pub type ChatMessageResponseStream =
std::pin::Pin<Box<dyn tokio_stream::Stream<Item = Result<ChatMessageResponse, ()>> + Send>>;
impl Ollama {
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
pub async fn send_chat_messages_stream(
&self,
request: ChatMessageRequest,
) -> crate::error::Result<ChatMessageResponseStream> {
let mut request = request;
request.stream = true;
let url = format!("{}api/chat", self.url_str());
let builder = self.reqwest_client.post(url);
#[cfg(feature = "headers")]
let builder = builder.headers(self.request_headers.clone());
let res = builder.json(&request).send().await?;
if !res.status().is_success() {
return Err(OllamaError::Other(
res.text().await.unwrap_or_else(|e| e.to_string()),
));
}
let s = stream! {
let mut buffer = String::new();
let mut stream = res.bytes_stream();
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => {
if let Ok(chunk_str) = String::from_utf8(chunk.to_vec()) {
buffer.push_str(&chunk_str);
let mut lines_to_process = Vec::new();
let mut start_pos = 0;
while let Some(pos) = buffer[start_pos..].find('\n') {
let actual_pos = start_pos + pos;
let line = buffer[start_pos..actual_pos].trim().to_string();
if !line.is_empty() {
lines_to_process.push(line);
}
start_pos = actual_pos + 1;
}
if start_pos > 0 {
buffer = buffer[start_pos..].to_string();
}
for line in lines_to_process {
match serde_json::from_str::<ChatMessageResponse>(&line) {
Ok(response) => yield Ok(response),
Err(e) => {
eprintln!("Failed to deserialize response: {e}");
}
}
}
}
}
Err(e) => {
eprintln!("Failed to read response: {e}");
yield Err(());
break;
}
}
}
if !buffer.is_empty() {
if let Ok(response) = serde_json::from_str::<ChatMessageResponse>(&buffer) {
yield Ok(response);
}
}
};
Ok(Box::pin(s))
}
pub async fn send_chat_messages(
&self,
request: ChatMessageRequest,
) -> crate::error::Result<ChatMessageResponse> {
let mut request = request;
request.stream = false;
let url = format!("{}api/chat", self.url_str());
let builder = self.reqwest_client.post(url);
#[cfg(feature = "headers")]
let builder = builder.headers(self.request_headers.clone());
let res = builder.json(&request).send().await?;
if !res.status().is_success() {
return Err(OllamaError::Other(
res.text().await.unwrap_or_else(|e| e.to_string()),
));
}
let bytes = res.bytes().await?;
let res = serde_json::from_slice::<ChatMessageResponse>(&bytes)?;
Ok(res)
}
}
impl Ollama {
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[cfg(feature = "stream")]
pub async fn send_chat_messages_with_history_stream<C: ChatHistory + Send + 'static>(
&self,
history: Arc<Mutex<C>>,
mut request: ChatMessageRequest,
) -> crate::error::Result<ChatMessageResponseStream> {
use async_stream::stream;
use tokio_stream::StreamExt;
{
let mut hist = history.lock().unwrap();
for m in request.messages {
hist.push(m);
}
}
request.messages = history.lock().unwrap().messages().to_vec();
request.stream = true;
let mut resp_stream: ChatMessageResponseStream =
self.send_chat_messages_stream(request.clone()).await?;
let s = stream! {
let mut result = String::new();
while let Some(item) = resp_stream.try_next().await.unwrap() {
let mut item = item;
let msg_part = item.message.content.clone();
result.push_str(&msg_part);
if item.done {
item.message.content = result.clone();
history.lock().unwrap().push(item.message.clone());
result.clear();
}
yield Ok(item);
}
};
Ok(Box::pin(s))
}
pub async fn send_chat_messages_with_history<C: ChatHistory>(
&self,
history: &mut C,
mut request: ChatMessageRequest,
) -> crate::error::Result<ChatMessageResponse> {
for m in request.messages {
history.push(m);
}
request.messages = history.messages().to_vec();
let result = self.send_chat_messages(request.clone()).await;
if let Ok(result) = result {
history.push(result.message.clone());
return Ok(result);
}
result
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessageResponse {
pub model: String,
pub created_at: String,
pub message: ChatMessage,
pub logprobs: Option<Vec<LogprobsData>>,
pub done: bool,
#[serde(flatten)]
pub final_data: Option<ChatMessageFinalResponseData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessageFinalResponseData {
pub total_duration: u64,
pub load_duration: u64,
pub prompt_eval_count: u64,
pub prompt_eval_duration: u64,
pub eval_count: u64,
pub eval_duration: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: MessageRole,
pub content: String,
#[serde(default)]
pub tool_calls: Vec<ToolCall>,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<Image>>,
pub thinking: Option<String>,
}
impl ChatMessage {
pub fn new(role: MessageRole, content: String) -> Self {
Self {
role,
content,
tool_calls: vec![],
images: None,
thinking: None,
}
}
pub fn user(content: String) -> Self {
Self::new(MessageRole::User, content)
}
pub fn assistant(content: String) -> Self {
Self::new(MessageRole::Assistant, content)
}
pub fn system(content: String) -> Self {
Self::new(MessageRole::System, content)
}
pub fn tool(content: String) -> Self {
Self::new(MessageRole::Tool, content)
}
pub fn with_images(mut self, images: Vec<Image>) -> Self {
self.images = Some(images);
self
}
pub fn add_image(mut self, image: Image) -> Self {
if let Some(images) = self.images.as_mut() {
images.push(image);
} else {
self.images = Some(vec![image]);
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MessageRole {
#[serde(rename = "user")]
User,
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "system")]
System,
#[serde(rename = "tool")]
Tool,
}
#[cfg(all(test, feature = "stream"))]
mod tests {
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_stream::StreamExt;
use super::*;
#[tokio::test]
async fn streamed_history_preserves_final_message_tool_calls() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test server");
let addr = listener.local_addr().expect("test server addr");
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.expect("accept request");
let mut request = Vec::new();
let mut buffer = [0; 1024];
loop {
let read = socket.read(&mut buffer).await.expect("read request");
if read == 0 {
break;
}
request.extend_from_slice(&buffer[..read]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
let body = concat!(
r#"{"model":"test","created_at":"2026-01-01T00:00:00Z","message":{"role":"assistant","content":"hello "},"done":false}"#,
"\n",
r#"{"model":"test","created_at":"2026-01-01T00:00:00Z","message":{"role":"assistant","content":"world","tool_calls":[{"function":{"name":"test_tool","arguments":{"city":"Paris"}}}]},"done":true}"#,
"\n",
);
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/x-ndjson\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(),
body
);
socket
.write_all(response.as_bytes())
.await
.expect("write response");
});
let ollama = Ollama::try_new(format!("http://{addr}")).expect("build test client");
let history = Arc::new(Mutex::new(Vec::new()));
let request = ChatMessageRequest::new(
"test".to_string(),
vec![ChatMessage::user("use the tool".to_string())],
);
let mut stream = ollama
.send_chat_messages_with_history_stream(history.clone(), request)
.await
.expect("stream starts");
let mut final_chunk = None;
while let Some(chunk) = stream.next().await {
let chunk = chunk.expect("chunk ok");
if chunk.done {
final_chunk = Some(chunk);
break;
}
}
server.await.expect("server completes");
let final_chunk = final_chunk.expect("received done chunk");
assert_eq!(final_chunk.message.content, "hello world");
assert_eq!(final_chunk.message.tool_calls.len(), 1);
let history = history.lock().expect("history lock");
assert_eq!(history.len(), 2);
assert_eq!(history[1].content, "hello world");
assert_eq!(history[1].tool_calls.len(), 1);
assert_eq!(history[1].tool_calls[0].function.name, "test_tool");
}
}