2025-12-24 17:17:35 +00:00
|
|
|
use crate::llm::types::{
|
|
|
|
|
CompletionResponse, FunctionCall, Message, ModelProvider, Role, ToolCall, ToolDefinition,
|
|
|
|
|
};
|
2025-12-24 17:32:46 +00:00
|
|
|
use async_trait::async_trait;
|
2025-12-27 16:50:18 +00:00
|
|
|
use futures::StreamExt;
|
2025-12-24 17:17:35 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
|
|
pub struct OllamaProvider {
|
|
|
|
|
base_url: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl OllamaProvider {
|
|
|
|
|
pub fn new(base_url: String) -> Self {
|
|
|
|
|
Self { base_url }
|
|
|
|
|
}
|
2025-12-25 12:21:58 +00:00
|
|
|
|
|
|
|
|
pub async fn get_models(base_url: &str) -> Result<Vec<String>, String> {
|
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let url = format!("{}/api/tags", base_url.trim_end_matches('/'));
|
|
|
|
|
|
|
|
|
|
let res = client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !res.status().is_success() {
|
|
|
|
|
let status = res.status();
|
|
|
|
|
let text = res.text().await.unwrap_or_default();
|
|
|
|
|
return Err(format!("Ollama API error {}: {}", status, text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let body: OllamaTagsResponse = res
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(body.models.into_iter().map(|m| m.name).collect())
|
|
|
|
|
}
|
2025-12-27 16:50:18 +00:00
|
|
|
|
2026-02-13 12:31:36 +00:00
|
|
|
/// Streaming chat that calls `on_token` for each token chunk.
|
|
|
|
|
pub async fn chat_stream<F>(
|
2025-12-27 16:50:18 +00:00
|
|
|
&self,
|
|
|
|
|
model: &str,
|
|
|
|
|
messages: &[Message],
|
|
|
|
|
tools: &[ToolDefinition],
|
2025-12-27 18:32:15 +00:00
|
|
|
cancel_rx: &mut tokio::sync::watch::Receiver<bool>,
|
2026-02-13 12:31:36 +00:00
|
|
|
mut on_token: F,
|
|
|
|
|
) -> Result<CompletionResponse, String>
|
|
|
|
|
where
|
|
|
|
|
F: FnMut(&str) + Send,
|
|
|
|
|
{
|
2025-12-27 16:50:18 +00:00
|
|
|
let client = reqwest::Client::new();
|
|
|
|
|
let url = format!("{}/api/chat", self.base_url.trim_end_matches('/'));
|
|
|
|
|
|
|
|
|
|
let ollama_messages: Vec<OllamaRequestMessage> = messages
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|m| {
|
|
|
|
|
let tool_calls = m.tool_calls.as_ref().map(|calls| {
|
|
|
|
|
calls
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|tc| {
|
|
|
|
|
let args_val: Value = serde_json::from_str(&tc.function.arguments)
|
|
|
|
|
.unwrap_or(Value::String(tc.function.arguments.clone()));
|
|
|
|
|
|
|
|
|
|
OllamaRequestToolCall {
|
|
|
|
|
kind: tc.kind.clone(),
|
|
|
|
|
function: OllamaRequestFunctionCall {
|
|
|
|
|
name: tc.function.name.clone(),
|
|
|
|
|
arguments: args_val,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
OllamaRequestMessage {
|
|
|
|
|
role: m.role.clone(),
|
|
|
|
|
content: m.content.clone(),
|
|
|
|
|
tool_calls,
|
|
|
|
|
tool_call_id: m.tool_call_id.clone(),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let request_body = OllamaRequest {
|
|
|
|
|
model,
|
|
|
|
|
messages: ollama_messages,
|
2026-02-13 12:31:36 +00:00
|
|
|
stream: true,
|
2025-12-27 16:50:18 +00:00
|
|
|
tools,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let res = client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.json(&request_body)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Request failed: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !res.status().is_success() {
|
|
|
|
|
let status = res.status();
|
|
|
|
|
let text = res.text().await.unwrap_or_default();
|
|
|
|
|
return Err(format!("Ollama API error {}: {}", status, text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut stream = res.bytes_stream();
|
|
|
|
|
let mut buffer = String::new();
|
|
|
|
|
let mut accumulated_content = String::new();
|
|
|
|
|
let mut final_tool_calls: Option<Vec<ToolCall>> = None;
|
|
|
|
|
|
2025-12-27 18:32:15 +00:00
|
|
|
loop {
|
|
|
|
|
if *cancel_rx.borrow() {
|
|
|
|
|
return Err("Chat cancelled by user".to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let chunk_result = tokio::select! {
|
|
|
|
|
chunk = stream.next() => {
|
|
|
|
|
match chunk {
|
|
|
|
|
Some(c) => c,
|
|
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ = cancel_rx.changed() => {
|
|
|
|
|
if *cancel_rx.borrow() {
|
|
|
|
|
return Err("Chat cancelled by user".to_string());
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-27 16:50:18 +00:00
|
|
|
let chunk = chunk_result.map_err(|e| format!("Stream error: {}", e))?;
|
|
|
|
|
buffer.push_str(&String::from_utf8_lossy(&chunk));
|
|
|
|
|
|
|
|
|
|
while let Some(newline_pos) = buffer.find('\n') {
|
|
|
|
|
let line = buffer[..newline_pos].trim().to_string();
|
|
|
|
|
buffer = buffer[newline_pos + 1..].to_string();
|
|
|
|
|
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stream_msg: OllamaStreamResponse =
|
|
|
|
|
serde_json::from_str(&line).map_err(|e| format!("JSON parse error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !stream_msg.message.content.is_empty() {
|
|
|
|
|
accumulated_content.push_str(&stream_msg.message.content);
|
2026-02-13 12:31:36 +00:00
|
|
|
on_token(&stream_msg.message.content);
|
2025-12-27 16:50:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(tool_calls) = stream_msg.message.tool_calls {
|
|
|
|
|
final_tool_calls = Some(
|
|
|
|
|
tool_calls
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|tc| ToolCall {
|
|
|
|
|
id: None,
|
|
|
|
|
kind: "function".to_string(),
|
|
|
|
|
function: FunctionCall {
|
|
|
|
|
name: tc.function.name,
|
|
|
|
|
arguments: tc.function.arguments.to_string(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if stream_msg.done {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(CompletionResponse {
|
|
|
|
|
content: if accumulated_content.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(accumulated_content)
|
|
|
|
|
},
|
|
|
|
|
tool_calls: final_tool_calls,
|
2026-02-20 11:51:19 +00:00
|
|
|
session_id: None,
|
2025-12-27 16:50:18 +00:00
|
|
|
})
|
|
|
|
|
}
|
2025-12-25 12:21:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct OllamaTagsResponse {
|
|
|
|
|
models: Vec<OllamaModelTag>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct OllamaModelTag {
|
|
|
|
|
name: String,
|
2025-12-24 17:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct OllamaRequest<'a> {
|
|
|
|
|
model: &'a str,
|
|
|
|
|
messages: Vec<OllamaRequestMessage>,
|
|
|
|
|
stream: bool,
|
|
|
|
|
#[serde(skip_serializing_if = "is_empty_tools")]
|
|
|
|
|
tools: &'a [ToolDefinition],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_empty_tools(tools: &&[ToolDefinition]) -> bool {
|
|
|
|
|
tools.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct OllamaRequestMessage {
|
|
|
|
|
role: Role,
|
|
|
|
|
content: String,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
tool_calls: Option<Vec<OllamaRequestToolCall>>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
tool_call_id: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct OllamaRequestToolCall {
|
|
|
|
|
function: OllamaRequestFunctionCall,
|
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
|
kind: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct OllamaRequestFunctionCall {
|
|
|
|
|
name: String,
|
|
|
|
|
arguments: Value,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
2026-02-13 12:31:36 +00:00
|
|
|
struct OllamaStreamResponse {
|
|
|
|
|
message: OllamaStreamMessage,
|
|
|
|
|
done: bool,
|
2025-12-24 17:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
2026-02-13 12:31:36 +00:00
|
|
|
struct OllamaStreamMessage {
|
|
|
|
|
#[serde(default)]
|
2025-12-24 17:17:35 +00:00
|
|
|
content: String,
|
2026-02-13 12:31:36 +00:00
|
|
|
#[serde(default)]
|
2025-12-24 17:17:35 +00:00
|
|
|
tool_calls: Option<Vec<OllamaResponseToolCall>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct OllamaResponseToolCall {
|
|
|
|
|
function: OllamaResponseFunctionCall,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct OllamaResponseFunctionCall {
|
|
|
|
|
name: String,
|
2026-02-13 12:31:36 +00:00
|
|
|
arguments: Value,
|
2025-12-27 16:50:18 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 17:32:46 +00:00
|
|
|
#[async_trait]
|
2025-12-24 17:17:35 +00:00
|
|
|
impl ModelProvider for OllamaProvider {
|
2025-12-24 17:32:46 +00:00
|
|
|
async fn chat(
|
2025-12-24 17:17:35 +00:00
|
|
|
&self,
|
2026-02-13 12:31:36 +00:00
|
|
|
_model: &str,
|
|
|
|
|
_messages: &[Message],
|
|
|
|
|
_tools: &[ToolDefinition],
|
2025-12-24 17:17:35 +00:00
|
|
|
) -> Result<CompletionResponse, String> {
|
2026-02-13 12:31:36 +00:00
|
|
|
Err("Non-streaming Ollama chat not implemented for server".to_string())
|
2025-12-24 17:17:35 +00:00
|
|
|
}
|
|
|
|
|
}
|