use std::sync::Arc;
use crate::capabilities::CapabilitySource;
use crate::config::Config;
use crate::launchers::LauncherSource;
use crate::models::ModelSource;
use crate::providers::ProviderSource;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SourceError {
NotConfigured,
Construct(crate::registry::ConstructError),
UnmetRequirement { model_id: String, unmet: String },
Unresolved(String),
}
impl SourceError {
pub(crate) fn about(&self, kind: &str, instance_id: &str) -> anyhow::Error {
use crate::registry::ConstructError;
match self {
Self::NotConfigured => anyhow::anyhow!("{kind} '{instance_id}' is not configured"),
Self::Construct(ConstructError::UnknownType { type_name }) => {
anyhow::anyhow!("{kind} '{instance_id}' has an unknown {kind} type '{type_name}'")
}
Self::Construct(ConstructError::Settings { detail }) => {
anyhow::anyhow!("the settings for {kind} '{instance_id}' are not valid: {detail}")
}
Self::UnmetRequirement { model_id, unmet } => anyhow::anyhow!(
"{kind} '{instance_id}' names model '{model_id}', which does not meet its \
requirement: {unmet}"
),
Self::Unresolved(detail) => anyhow::anyhow!("{kind} '{instance_id}': {detail}"),
}
}
}
impl From<crate::registry::ConstructError> for SourceError {
fn from(error: crate::registry::ConstructError) -> Self {
Self::Construct(error)
}
}
#[derive(Clone)]
pub(crate) struct Sources {
providers: Arc<ProviderSource>,
models: Arc<ModelSource>,
capabilities: Arc<CapabilitySource>,
launchers: Arc<LauncherSource>,
}
impl Sources {
pub(crate) fn build(config: &Config, model_proxy: Option<crate::proxy::ProxyHandle>) -> Self {
let providers = Arc::new(ProviderSource::with_proxy(config, model_proxy));
let models = Arc::new(ModelSource::with_providers(config, Arc::clone(&providers)));
let capabilities = Arc::new(CapabilitySource::with_models(config, Arc::clone(&models)));
Self {
providers,
models,
capabilities,
launchers: Arc::new(LauncherSource::from_config(config)),
}
}
pub(crate) fn providers(&self) -> Arc<ProviderSource> {
Arc::clone(&self.providers)
}
pub(crate) fn models(&self) -> Arc<ModelSource> {
Arc::clone(&self.models)
}
pub(crate) fn capabilities(&self) -> Arc<CapabilitySource> {
Arc::clone(&self.capabilities)
}
pub(crate) fn launchers(&self) -> Arc<LauncherSource> {
Arc::clone(&self.launchers)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ModelConfig, ProviderConfig};
use crate::utils::ui::base::tests::CaptureUi;
use crate::{AppContext, proxy::ProxyServer};
fn ctx_with_one_model() -> AppContext {
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!({ "base_url": "http://localhost:11434" }),
},
);
config.models.insert(
"granite-3.1-8b-instruct".to_string(),
ModelConfig {
model_id: "granite-3.1-8b-instruct".to_string(),
model_type: "granite-3.1-8b-instruct".to_string(),
config: serde_json::json!({}),
provider_id: "ollama".to_string(),
variant: None,
},
);
AppContext::new(config, Arc::new(CaptureUi::default()))
}
#[test]
fn two_asks_share_one_set_and_one_instance_per_id() {
let ctx = ctx_with_one_model();
let first = ctx.sources();
let second = ctx.sources();
assert!(Arc::ptr_eq(&first.models(), &second.models()));
assert!(Arc::ptr_eq(&first.providers(), &second.providers()));
let model = first.models().get("granite-3.1-8b-instruct").unwrap();
let again = second.models().get("granite-3.1-8b-instruct").unwrap();
assert!(Arc::ptr_eq(&model, &again));
}
#[test]
fn a_capability_and_a_command_resolve_the_same_provider() {
let ctx = ctx_with_one_model();
let sources = ctx.sources();
let through_model = sources
.models()
.provider_for("granite-3.1-8b-instruct")
.unwrap();
let by_id = sources.providers().get("ollama").unwrap();
assert!(Arc::ptr_eq(&through_model, &by_id));
}
#[test]
fn a_write_ends_the_snapshot() {
let mut ctx = ctx_with_one_model();
let before = ctx.sources();
let model_before = before.models().get("granite-3.1-8b-instruct").unwrap();
ctx.config_mut()
.models
.remove("granite-3.1-8b-instruct")
.unwrap();
let after = ctx.sources();
assert!(!Arc::ptr_eq(&before.models(), &after.models()));
assert!(
after.models().get("granite-3.1-8b-instruct").is_err(),
"the new set reads the configuration as it now stands"
);
assert_eq!(model_before.instance_id(), "granite-3.1-8b-instruct");
assert!(before.models().get("granite-3.1-8b-instruct").is_ok());
}
#[tokio::test]
async fn a_session_proxy_ends_the_snapshot_too() {
let mut ctx = ctx_with_one_model();
let upstream = ctx
.sources()
.providers()
.get("ollama")
.unwrap()
.base_url()
.to_string();
assert_eq!(upstream, "http://localhost:11434");
let server = ProxyServer::start().unwrap();
ctx.set_model_proxy(server.handle.clone());
let providers = ctx.sources().providers();
assert_eq!(
providers.get("ollama").unwrap().base_url(),
server.handle.local_base_url,
"after a launch starts its proxy, every provider handed out points at it"
);
assert_eq!(
providers.upstream("ollama").unwrap().base_url(),
"http://localhost:11434",
"the upstream view still carries the real details, for route targets"
);
server.shutdown().await;
}
}