pub mod types;
pub mod options;
pub mod modelfile;
pub mod runner;
pub use options::OllamaOptionsBuilder;
pub use modelfile::ModelfileBuilder;
pub use runner::LocalOllamaRunner;
use crate::core::types::{GenerateOptions, GenerateResult, Prompt, ProviderSettings, StreamPart};
use crate::openai::OpenAIModel;
use async_trait::async_trait;
use futures::stream::BoxStream;
use reqwest::Client;
pub struct OllamaModel {
pub inner: OpenAIModel,
}
impl OllamaModel {
#[must_use]
pub fn new(api_key: String, base_url: String) -> Self {
Self {
inner: OpenAIModel {
api_key,
base_url,
client: Client::new(),
},
}
}
}
#[async_trait]
impl crate::core::LanguageModel for OllamaModel {
#[tracing::instrument(skip(self, prompt), fields(model = options.model_id))]
async fn generate(
&self,
prompt: Prompt,
options: GenerateOptions,
) -> crate::core::Result<GenerateResult> {
self.inner.generate(prompt, options).await
}
async fn generate_stream(
&self,
prompt: Prompt,
options: GenerateOptions,
) -> crate::core::Result<BoxStream<'static, StreamPart>> {
self.inner.generate_stream(prompt, options).await
}
}
#[async_trait]
impl crate::core::EmbeddingModel for OllamaModel {
#[tracing::instrument(skip(self, texts), fields(model = options.model_id))]
async fn embed(
&self,
texts: Vec<String>,
options: crate::core::types::EmbeddingOptions,
) -> crate::core::Result<crate::core::types::EmbeddingResult> {
let embedding_model = crate::openai::embedding::OpenAIEmbeddingModel {
api_key: self.inner.api_key.clone(),
base_url: self.inner.base_url.clone(),
client: self.inner.client.clone(),
};
embedding_model.embed(texts, options).await
}
}
pub struct OllamaProvider {
settings: ProviderSettings,
}
impl OllamaProvider {
#[must_use]
pub fn chat(&self, _model_id: &str) -> OllamaModel {
let api_key = self
.settings
.api_key
.clone()
.or_else(|| std::env::var("OLLAMA_API_KEY").ok())
.unwrap_or_default();
let base_url = self
.settings
.base_url
.clone()
.or_else(|| std::env::var("OLLAMA_BASE_URL").ok())
.unwrap_or_else(|| {
if !api_key.is_empty() {
types::DEFAULT_OLLAMA_CLOUD_URL.to_string()
} else {
types::DEFAULT_OLLAMA_LOCAL_URL.to_string()
}
});
OllamaModel::new(api_key, base_url)
}
#[must_use]
pub fn language_model(&self, model_id: &str) -> OllamaModel {
self.chat(model_id)
}
#[must_use]
pub fn embedding(&self, model_id: &str) -> OllamaModel {
self.chat(model_id)
}
fn get_api_base(&self) -> String {
let base = self
.settings
.base_url
.clone()
.or_else(|| std::env::var("OLLAMA_BASE_URL").ok())
.unwrap_or_else(|| types::DEFAULT_OLLAMA_LOCAL_URL.to_string());
if base.ends_with("/v1") {
base[..base.len() - 3].to_string()
} else if base.ends_with("/v1/") {
base[..base.len() - 4].to_string()
} else {
base
}
}
pub async fn list_models(&self) -> crate::core::Result<types::OllamaListResponse> {
let url = format!("{}/api/tags", self.get_api_base());
let client = Client::new();
let res = client.get(&url).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn list_running_models(&self) -> crate::core::Result<types::OllamaPsResponse> {
let url = format!("{}/api/ps", self.get_api_base());
let client = Client::new();
let res = client.get(&url).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn show_model_info(&self, req: types::OllamaShowRequest) -> crate::core::Result<types::OllamaShowResponse> {
let url = format!("{}/api/show", self.get_api_base());
let client = Client::new();
let res = client.post(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn copy_model(&self, req: types::OllamaCopyRequest) -> crate::core::Result<()> {
let url = format!("{}/api/copy", self.get_api_base());
let client = Client::new();
let res = client.post(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
Ok(())
}
pub async fn delete_model(&self, req: types::OllamaDeleteRequest) -> crate::core::Result<()> {
let url = format!("{}/api/delete", self.get_api_base());
let client = Client::new();
let res = client.delete(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
Ok(())
}
pub async fn create_model(&self, mut req: types::OllamaCreateRequest) -> crate::core::Result<()> {
req.stream = Some(false); let url = format!("{}/api/create", self.get_api_base());
let client = Client::new();
let res = client.post(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
Ok(())
}
pub async fn pull_model(&self, mut req: types::OllamaPullRequest) -> crate::core::Result<types::OllamaPullResponse> {
req.stream = Some(false); let url = format!("{}/api/pull", self.get_api_base());
let client = Client::new();
let res = client.post(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn push_model(&self, mut req: types::OllamaPushRequest) -> crate::core::Result<()> {
req.stream = Some(false); let url = format!("{}/api/push", self.get_api_base());
let client = Client::new();
let res = client.post(&url).json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
Ok(())
}
pub async fn get_version(&self) -> crate::core::Result<types::OllamaVersionResponse> {
let url = format!("{}/api/version", self.get_api_base());
let client = Client::new();
let res = client.get(&url).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn web_search(&self, req: types::WebSearchRequest) -> crate::core::Result<types::WebSearchResponse> {
let url = format!("{}/api/web_search", self.get_api_base());
let client = Client::new();
let mut request_builder = client.post(&url);
let api_key = self
.settings
.api_key
.clone()
.or_else(|| std::env::var("OLLAMA_API_KEY").ok())
.unwrap_or_default();
if !api_key.is_empty() {
request_builder = request_builder.bearer_auth(api_key);
}
let res = request_builder.json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
pub async fn web_fetch(&self, req: types::WebFetchRequest) -> crate::core::Result<types::WebFetchResponse> {
let url = format!("{}/api/web_fetch", self.get_api_base());
let client = Client::new();
let mut request_builder = client.post(&url);
let api_key = self
.settings
.api_key
.clone()
.or_else(|| std::env::var("OLLAMA_API_KEY").ok())
.unwrap_or_default();
if !api_key.is_empty() {
request_builder = request_builder.bearer_auth(api_key);
}
let res = request_builder.json(&req).send().await.map_err(|e| crate::core::error::ProviderError::Network(e.to_string()))?;
if !res.status().is_success() {
return Err(crate::core::error::ProviderError::InvalidResponse(format!("Ollama API error: {}", res.status())));
}
res.json().await.map_err(|e| crate::core::error::ProviderError::InvalidResponse(e.to_string()))
}
}
#[must_use]
pub fn create_ollama(settings: ProviderSettings) -> OllamaProvider {
OllamaProvider { settings }
}
impl crate::core::registry::Provider for OllamaProvider {
fn language_model(&self, model_id: &str) -> Option<Box<dyn crate::core::LanguageModel>> {
Some(Box::new(self.chat(model_id)))
}
fn embedding_model(&self, model_id: &str) -> Option<Box<dyn crate::core::EmbeddingModel>> {
Some(Box::new(self.embedding(model_id)))
}
}