use std::collections::HashMap;
use std::sync::LazyLock;
use alog::{MessageLevel, alog_channel, use_channel};
use_channel!("PROVD");
pub static PROVIDER_REGISTRY: LazyLock<base::ProviderFactory> = LazyLock::new(|| {
let mut factory = base::ProviderFactory::new();
factory.register::<openai::OpenAIProvider>("openai-compatible");
factory.register::<ollama::OllamaProvider>("ollama");
factory.register::<llamacpp::LlamaCppProvider>("llama-cpp");
factory.register::<lmstudio::LMStudioProvider>("lm-studio");
factory.register::<openrouter::OpenRouterProvider>("openrouter");
factory
});
pub struct ProviderSource {
config: crate::config::Config,
model_proxy: Option<crate::proxy::ProxyHandle>,
upstream: std::sync::Mutex<HashMap<String, std::sync::Arc<dyn Provider>>>,
proxied: std::sync::Mutex<HashMap<String, std::sync::Arc<dyn Provider>>>,
}
impl ProviderSource {
pub fn from_config(config: &crate::config::Config) -> Self {
Self::with_proxy(config, None)
}
pub fn with_proxy(
config: &crate::config::Config,
model_proxy: Option<crate::proxy::ProxyHandle>,
) -> Self {
Self {
config: config.clone(),
model_proxy,
upstream: std::sync::Mutex::new(HashMap::new()),
proxied: std::sync::Mutex::new(HashMap::new()),
}
}
pub fn get(&self, provider_id: &str) -> anyhow::Result<std::sync::Arc<dyn Provider>> {
let upstream = self.upstream(provider_id)?;
let Some(handle) = &self.model_proxy else {
return Ok(upstream);
};
if let Some(built) = self.proxied.lock().unwrap().get(provider_id) {
return Ok(built.clone());
}
let wrapped: std::sync::Arc<dyn Provider> = std::sync::Arc::new(
crate::proxy::ProxiedProvider::wrap(upstream, handle.local_base_url.clone()),
);
Ok(self
.proxied
.lock()
.unwrap()
.entry(provider_id.to_string())
.or_insert(wrapped)
.clone())
}
pub fn upstream(&self, provider_id: &str) -> anyhow::Result<std::sync::Arc<dyn Provider>> {
if let Some(built) = self.upstream.lock().unwrap().get(provider_id) {
return Ok(built.clone());
}
let provider_config = self
.config
.providers
.get(provider_id)
.ok_or_else(|| anyhow::anyhow!("provider '{provider_id}' is not configured"))?;
let built = PROVIDER_REGISTRY
.construct(
&provider_config.provider_type,
&provider_config.provider_id,
&provider_config.config,
)
.map_err(|e| e.about("provider", provider_id))?;
let built: std::sync::Arc<dyn Provider> = std::sync::Arc::from(built);
Ok(self
.upstream
.lock()
.unwrap()
.entry(provider_id.to_string())
.or_insert(built)
.clone())
}
}
impl crate::dependency::Configured<dyn Provider> for ProviderSource {
fn instances(&self) -> Vec<(String, std::sync::Arc<dyn Provider + 'static>)> {
self.config
.providers
.keys()
.filter_map(|id| match self.get(id) {
Ok(provider) => Some((id.clone(), provider)),
Err(e) => {
alog_channel!(MessageLevel::Warning, "{e}");
None
}
})
.collect()
}
fn catalog(&self) -> HashMap<&'static str, ProviderMetadata> {
PROVIDER_REGISTRY.entries()
}
fn config_schema(&self, type_name: &str) -> Option<schemars::Schema> {
PROVIDER_REGISTRY.config_schema(type_name)
}
}
mod base;
pub use base::{
ApiEndpoint, ApiType, AuthType, HealthStatus, ModelFormat, Provider, ProviderError,
ProviderMetadata, ProviderType, PullResult,
};
mod openai;
pub use openai::{OpenAIProvider, OpenAIProviderConfig};
mod ollama;
pub use ollama::{OllamaProvider, OllamaProviderConfig};
mod llamacpp;
pub use llamacpp::{LlamaCppProvider, LlamaCppProviderConfig};
mod lmstudio;
pub use lmstudio::{LMStudioProvider, LMStudioProviderConfig};
mod openrouter;
pub use openrouter::{OpenRouterProvider, OpenRouterProviderConfig};
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, ProviderConfig};
use crate::dependency::{Configured, DependsOn, Requirement, resolve};
fn openai_provider_config(id: &str, base_url: &str) -> ProviderConfig {
ProviderConfig {
provider_id: id.to_string(),
provider_type: "openai-compatible".to_string(),
config: serde_json::json!({ "base_url": base_url }),
}
}
fn config_with_two_named_instances() -> Config {
let mut config = Config::default();
config.providers.insert(
"llama-cpp".to_string(),
openai_provider_config("llama-cpp", "http://localhost:8080"),
);
config.providers.insert(
"ollama".to_string(),
openai_provider_config("ollama", "http://localhost:11434"),
);
config
}
#[test]
fn get_names_the_provider_whose_settings_cannot_be_read() {
let mut config = Config::default();
config.providers.insert(
"ollama".to_string(),
ProviderConfig {
provider_id: "ollama".to_string(),
provider_type: "ollama".to_string(),
config: serde_json::json!({ "timeout_secs": "ten" }),
},
);
let source = ProviderSource::from_config(&config);
let err = source.get("ollama").err().unwrap().to_string();
assert!(
err.contains("provider 'ollama'") && err.contains("invalid type"),
"expected the instance and what serde said, got: {err}"
);
}
#[test]
fn get_names_the_provider_whose_type_is_unknown() {
let mut config = Config::default();
config.providers.insert(
"mystery".to_string(),
ProviderConfig {
provider_id: "mystery".to_string(),
provider_type: "not-a-provider-type".to_string(),
config: serde_json::json!({}),
},
);
let source = ProviderSource::from_config(&config);
let err = source.get("mystery").err().unwrap().to_string();
assert!(
err.contains("mystery") && err.contains("not-a-provider-type"),
"expected the instance and the type it names, got: {err}"
);
}
#[test]
fn provider_source_constructs_one_instance_per_named_provider() {
let config = config_with_two_named_instances();
let source = ProviderSource::from_config(&config);
let mut ids: Vec<String> = source.instances().into_iter().map(|(id, _)| id).collect();
ids.sort();
assert_eq!(ids, vec!["llama-cpp".to_string(), "ollama".to_string()]);
}
struct AnyGguf;
impl Requirement<dyn Provider> for AnyGguf {
fn admits_type(&self, metadata: &ProviderMetadata) -> bool {
metadata.supported_formats.contains(&ModelFormat::GGUF)
}
fn admits_instance(&self, instance: &dyn Provider) -> bool {
instance.can_run_model("gguf", "fp16")
}
}
impl DependsOn<dyn Provider> for AnyGguf {
type Requirement = Self;
fn requirement(&self) -> Self {
AnyGguf
}
}
#[test]
fn resolve_surfaces_all_matching_named_instances() {
let config = config_with_two_named_instances();
let source = ProviderSource::from_config(&config);
let resolution = resolve(&AnyGguf, &source);
let mut ids = resolution.existing_instances;
ids.sort();
assert_eq!(ids, vec!["llama-cpp".to_string(), "ollama".to_string()]);
}
}