use async_trait::async_trait;
use llm_api_rs::{
Anthropic, ChatCompletionRequest, ChatMessage, DeepSeek, Gemini, LlmProvider, OpenAI,
};
use ollama_rs::{Ollama, generation::completion::request::GenerationRequest};
#[derive(Debug)]
pub enum Error {
NotSupport(String),
RequireApiKey,
Ollama(ollama_rs::error::OllamaError),
LlmService(String),
Io(std::io::Error),
}
impl From<ollama_rs::error::OllamaError> for Error {
fn from(value: ollama_rs::error::OllamaError) -> Self {
Self::Ollama(value)
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Provider {
Ollama,
OpenAi,
Gemini,
Anthropic,
Deepseek,
}
impl Provider {
pub fn parse_str<T: AsRef<str>>(s: T) -> Result<Self, Error> {
match s.as_ref().trim().to_lowercase().as_str() {
"ollama" => Ok(Self::Ollama),
"openai" => Ok(Self::OpenAi),
"gemini" => Ok(Self::Gemini),
"anthropic" => Ok(Self::Anthropic),
"deepseek" => Ok(Self::Deepseek),
_ => Err(Error::NotSupport(s.as_ref().to_string())),
}
}
fn is_ollama(&self) -> bool {
self == &Self::Ollama
}
}
pub struct Llm {
provider: Provider,
model: String,
api_key: Option<String>,
temperature: Option<f32>,
max_tokens: Option<u32>,
base_url: Option<(String, u16)>,
}
#[async_trait]
pub trait LlmControl {
fn check_api_key(&self) -> bool;
fn new(
provider: Provider,
model: String,
api_key: Option<String>,
temperature: Option<f32>,
max_tokens: Option<u32>,
base_url: Option<(String, u16)>,
) -> Self;
async fn call<T>(&self, prompt: T) -> Result<String, Error>
where
T: AsRef<str> + Send + Sync + 'static;
fn sync_call<T>(&self, prompt: T) -> Result<String, Error>
where
T: AsRef<str> + Send + Sync + 'static;
}
#[async_trait]
impl LlmControl for Llm {
fn check_api_key(&self) -> bool {
match self.api_key.clone() {
Some(_) => true,
None => !self.provider.is_ollama(),
}
}
fn new(
provider: Provider,
model: String,
api_key: Option<String>,
temperature: Option<f32>,
max_tokens: Option<u32>,
base_url: Option<(String, u16)>,
) -> Self {
Self {
provider,
model,
api_key,
temperature,
max_tokens,
base_url,
}
}
async fn call<T>(&self, prompt: T) -> Result<String, Error>
where
T: AsRef<str> + Send + Sync + 'static,
{
async {
if self.provider == Provider::Ollama {
let base_url = self
.base_url
.clone()
.unwrap_or(("http://localhost:11434".to_string(), 11434));
let ollama_result = Ollama::new(base_url.0, base_url.1);
let res = ollama_result
.generate(GenerationRequest::new(self.model.clone(), prompt.as_ref()))
.await;
match res {
Ok(v) => Ok(v.response.to_string()),
Err(e) => Err(Error::Ollama(e)),
}
} else {
let api = self.api_key.clone().unwrap();
let client: Box<dyn LlmProvider + Send> = match self.provider {
Provider::OpenAi => Box::new(OpenAI::new(api)),
Provider::Gemini => Box::new(Gemini::new(api)),
Provider::Anthropic => Box::new(Anthropic::new(api)),
Provider::Deepseek => Box::new(DeepSeek::new(api)),
_ => unreachable!(),
};
let req = ChatCompletionRequest {
model: self.model.clone(),
messages: vec![ChatMessage {
role: "user".to_string(),
content: prompt.as_ref().to_string(),
}],
temperature: self.temperature,
max_tokens: self.max_tokens,
};
match client.chat_completion(req).await {
Ok(res) => {
if let Some(v) = res.choices.first() {
Ok(v.message.content.clone())
} else {
Ok(String::new())
}
}
Err(e) => Err(Error::LlmService(e.to_string())),
}
}
}
.await
}
fn sync_call<T>(&self, prompt: T) -> Result<String, Error>
where
T: AsRef<str> + Send + Sync + 'static,
{
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(self.call(prompt))
}
}
#[cfg(test)]
mod tests {
use crate::{
Provider,
};
#[test]
fn parse_test() {
let sources = [
"ollama",
"Ollama",
"openai",
"OpenAI",
"gemini",
"GEmini",
"anthropic",
"deepseek",
];
let right_cl = [
Provider::Ollama,
Provider::Ollama,
Provider::OpenAi,
Provider::OpenAi,
Provider::Gemini,
Provider::Gemini,
Provider::Anthropic,
Provider::Deepseek,
];
let res = sources
.iter()
.zip(right_cl)
.map(|f| (Provider::parse_str(f.0).unwrap(), f.1))
.all(|f| f.0 == f.1);
assert!(res);
}
}