pub const DEFAULT_OLLAMA_URL: &str = "http://localhost:11434";
pub const DEFAULT_OLLAMA_MODEL: &str = "llama3.2:1b";
pub const DEFAULT_OPENAI_MODEL: &str = "gpt-4o-mini";
pub const DEFAULT_ANTHROPIC_MODEL: &str = "claude-haiku-4-5";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::Display, strum::EnumString, strum::AsRefStr)]
#[strum(serialize_all = "lowercase")]
pub enum LlmKind {
Ollama,
OpenAI,
Anthropic,
}
#[derive(Debug, Clone)]
pub enum LlmConfig {
Ollama {
url: String,
model: String,
},
OpenAI {
api_key: String,
model: String,
base_url: Option<String>,
},
Anthropic {
api_key: String,
model: String,
},
}
impl LlmConfig {
#[must_use]
pub fn ollama(url: impl Into<String>, model: impl Into<String>) -> Self {
Self::Ollama {
url: url.into(),
model: model.into(),
}
}
#[must_use]
pub fn openai(api_key: impl Into<String>, model: impl Into<String>) -> Self {
Self::OpenAI {
api_key: api_key.into(),
model: model.into(),
base_url: None,
}
}
#[must_use]
pub fn openai_with_base_url(
api_key: impl Into<String>,
model: impl Into<String>,
base_url: impl Into<String>,
) -> Self {
Self::OpenAI {
api_key: api_key.into(),
model: model.into(),
base_url: Some(base_url.into()),
}
}
#[must_use]
pub fn anthropic(api_key: impl Into<String>, model: impl Into<String>) -> Self {
Self::Anthropic {
api_key: api_key.into(),
model: model.into(),
}
}
#[must_use]
pub fn kind(&self) -> LlmKind {
match self {
Self::Ollama { .. } => LlmKind::Ollama,
Self::OpenAI { .. } => LlmKind::OpenAI,
Self::Anthropic { .. } => LlmKind::Anthropic,
}
}
#[must_use]
pub fn model(&self) -> &str {
match self {
Self::Ollama { model, .. }
| Self::OpenAI { model, .. }
| Self::Anthropic { model, .. } => model,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_build_ollama_from_str_literals() {
let config = LlmConfig::ollama("http://localhost:11434", "llama3.2");
match config {
LlmConfig::Ollama { url, model } => {
assert_eq!(url, "http://localhost:11434");
assert_eq!(model, "llama3.2");
}
other => panic!("expected Ollama, got {other:?}"),
}
}
#[test]
fn should_build_openai_default_base_url_as_none() {
let config = LlmConfig::openai("sk-test", "gpt-4o-mini");
match config {
LlmConfig::OpenAI {
api_key,
model,
base_url,
} => {
assert_eq!(api_key, "sk-test");
assert_eq!(model, "gpt-4o-mini");
assert!(base_url.is_none());
}
other => panic!("expected OpenAI, got {other:?}"),
}
}
#[test]
fn should_build_openai_with_custom_base_url() {
let config =
LlmConfig::openai_with_base_url("sk-test", "gpt-4o-mini", "https://proxy.example.com");
match config {
LlmConfig::OpenAI { base_url, .. } => {
assert_eq!(base_url.as_deref(), Some("https://proxy.example.com"));
}
other => panic!("expected OpenAI, got {other:?}"),
}
}
#[test]
fn should_build_anthropic_from_str_literals() {
let config = LlmConfig::anthropic("sk-ant-test", "claude-haiku-4-5");
match config {
LlmConfig::Anthropic { api_key, model } => {
assert_eq!(api_key, "sk-ant-test");
assert_eq!(model, "claude-haiku-4-5");
}
other => panic!("expected Anthropic, got {other:?}"),
}
}
#[test]
fn should_report_kind_per_variant() {
assert_eq!(LlmConfig::ollama("u", "m").kind(), LlmKind::Ollama);
assert_eq!(LlmConfig::openai("k", "m").kind(), LlmKind::OpenAI);
assert_eq!(LlmConfig::anthropic("k", "m").kind(), LlmKind::Anthropic);
}
#[test]
fn should_report_model_per_variant() {
assert_eq!(LlmConfig::ollama("u", "llama").model(), "llama");
assert_eq!(LlmConfig::openai("k", "gpt-4o").model(), "gpt-4o");
assert_eq!(LlmConfig::anthropic("k", "claude").model(), "claude");
}
#[test]
fn should_render_kind_as_lowercase_string() {
assert_eq!(LlmKind::Ollama.as_ref(), "ollama");
assert_eq!(LlmKind::OpenAI.as_ref(), "openai");
assert_eq!(LlmKind::Anthropic.as_ref(), "anthropic");
}
#[test]
fn should_display_kind_matching_as_str() {
assert_eq!(LlmKind::Ollama.to_string(), "ollama");
}
}