use crate::config::Config;
use leviath_runtime::ProviderRegistry;
pub use leviath_runtime::provider_creds::{ProviderCreds, build_provider_registry};
fn cache_ttl_key(ttl: leviath_providers::anthropic::CacheTtl) -> &'static str {
match ttl {
leviath_providers::anthropic::CacheTtl::Ephemeral5m => "5m",
leviath_providers::anthropic::CacheTtl::Ephemeral1h => "1h",
}
}
pub fn provider_creds_from_config(config: &Config) -> Vec<ProviderCreds> {
let caps = &config.model_capabilities;
let timeout = config.request_timeout_secs;
let mut creds = Vec::new();
let keyed = [
("anthropic", config.providers.anthropic_api_key.as_deref()),
("openai", config.providers.openai_api_key.as_deref()),
("google", config.providers.google_api_key.as_deref()),
("openrouter", config.openrouter_api_key.as_deref()),
];
for (name, key) in keyed {
if let Some(key) = key.map(str::trim).filter(|k| !k.is_empty()) {
let mut options = std::collections::HashMap::new();
if name == "anthropic"
&& let Some(ttl) = config.providers.anthropic_cache_ttl
{
options.insert("cache_ttl".to_string(), cache_ttl_key(ttl).to_string());
}
creds.push(ProviderCreds {
name: name.to_string(),
api_key: Some(key.to_string()),
base_url: None,
model_capabilities: caps.clone(),
request_timeout_secs: timeout,
rate_limit: config.rate_limits.get(name).cloned(),
options,
});
}
}
creds.push(ProviderCreds {
name: "ollama".to_string(),
api_key: None,
base_url: Some(
config
.ollama_base_url
.as_deref()
.unwrap_or("http://localhost:11434")
.to_string(),
),
model_capabilities: caps.clone(),
request_timeout_secs: timeout,
rate_limit: None,
options: std::collections::HashMap::new(),
});
if config.providers.claude_code_enabled {
let mut options = std::collections::HashMap::new();
if let Some(binary) = &config.providers.claude_code_binary {
options.insert("binary".to_string(), binary.clone());
}
if let Some(effort) = &config.providers.claude_code_effort {
options.insert("effort".to_string(), effort.clone());
}
creds.push(ProviderCreds {
name: "claude-code".to_string(),
api_key: None,
base_url: None,
model_capabilities: caps.clone(),
request_timeout_secs: None,
rate_limit: None,
options,
});
}
creds
}
pub fn build_provider_registry_from_config(
config: &Config,
) -> Result<ProviderRegistry, leviath_providers::ProviderError> {
build_provider_registry_from_config_with(
config,
&leviath_providers::provider::build_http_client,
)
}
pub fn build_provider_registry_from_config_with(
config: &Config,
build_client: leviath_providers::provider::HttpClientFactory<'_>,
) -> Result<ProviderRegistry, leviath_providers::ProviderError> {
let registry = leviath_runtime::provider_creds::build_provider_registry_with(
&provider_creds_from_config(config),
build_client,
)?;
Ok(attach_script_layer(
registry,
crate::config::providers_dir(),
config,
))
}
fn attach_script_layer(
registry: ProviderRegistry,
dir: Option<std::path::PathBuf>,
config: &Config,
) -> ProviderRegistry {
let Some(dir) = dir else {
return registry;
};
let overrides = config
.model_providers
.iter()
.map(|(name, mp)| (name.clone(), script_provider_spec(mp)))
.collect();
let layer = leviath_runtime::script_provider::ScriptProviderLayer::new(
dir,
overrides,
config.model_capabilities.clone(),
config.request_timeout_secs,
config.security.allow_env_vars.clone(),
);
registry.with_script_layer(std::sync::Arc::new(layer))
}
fn script_provider_spec(
mp: &crate::config::ModelProviderConfig,
) -> leviath_runtime::script_provider::ScriptProviderSpec {
let mut cfg = serde_json::Map::new();
if let Some(b) = &mp.base_url {
cfg.insert("base_url".to_string(), serde_json::Value::String(b.clone()));
}
if let Some(k) = &mp.api_key {
cfg.insert("api_key".to_string(), serde_json::Value::String(k.clone()));
}
for (k, v) in &mp.extra {
cfg.insert(
k.clone(),
serde_json::to_value(v).unwrap_or(serde_json::Value::Null),
);
}
leviath_runtime::script_provider::ScriptProviderSpec {
script: mp.script.clone(),
rate_limit: mp.rate_limit.clone(),
init_config: serde_json::Value::Object(cfg),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_provider_registry_with_empty_config() {
let config = Config::default();
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("ollama"));
assert!(!registry.has("claude-code"));
assert!(!registry.has("anthropic"));
assert!(!registry.has("openai"));
assert!(!registry.has("google"));
}
#[test]
fn build_provider_registry_with_anthropic_key() {
let config = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant-test-key-12345".to_string()),
..Config::default().providers
},
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("anthropic"));
}
#[test]
fn build_provider_registry_with_openai_key() {
let config = Config {
providers: crate::config::ProviderConfig {
openai_api_key: Some("sk-test-key-12345".to_string()),
..Config::default().providers
},
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("openai"));
}
#[test]
fn build_provider_registry_with_google_key() {
let config = Config {
providers: crate::config::ProviderConfig {
google_api_key: Some("AIzatest12345".to_string()),
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
..Config::default().providers
},
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("google"));
}
#[test]
fn build_provider_registry_with_openrouter_key() {
let config = Config {
openrouter_api_key: Some("sk-or-test-12345".to_string()),
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("openrouter"));
}
#[test]
fn build_provider_registry_custom_ollama_url() {
let config = Config {
ollama_base_url: Some("http://my-server:11434".to_string()),
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("ollama"));
}
#[test]
fn script_provider_spec_assembles_init_config() {
let mut extra = std::collections::HashMap::new();
extra.insert("region".to_string(), toml::Value::String("us".to_string()));
let mp = crate::config::ModelProviderConfig {
script: Some("groq".to_string()),
api_key: Some("k".to_string()),
base_url: Some("http://api".to_string()),
rate_limit: Some(leviath_providers::RateLimitConfig {
requests_per_minute: 30,
tokens_per_minute: 1000,
}),
extra,
};
let spec = script_provider_spec(&mp);
assert_eq!(spec.script.as_deref(), Some("groq"));
assert!(spec.rate_limit.is_some());
assert_eq!(spec.init_config["base_url"], "http://api");
assert_eq!(spec.init_config["api_key"], "k");
assert_eq!(spec.init_config["region"], "us");
}
#[test]
fn attach_script_layer_without_home_is_a_noop() {
let registry = attach_script_layer(ProviderRegistry::new(), None, &Config::default());
assert!(!registry.has("groq"));
}
#[test]
fn build_registry_resolves_a_configured_script_provider() {
let home = tempfile::tempdir().unwrap();
let providers = home.path().join(".leviath").join("providers");
std::fs::create_dir_all(&providers).unwrap();
std::fs::write(
providers.join("groq.rhai"),
"fn initialize(config) { #{} }\nfn inference(state, request) { #{ content: \"ok\" } }",
)
.unwrap();
let mut model_providers = std::collections::HashMap::new();
model_providers.insert(
"groq".to_string(),
crate::config::ModelProviderConfig::default(),
);
let config = Config {
model_providers,
..Config::default()
};
temp_env::with_var("LEVIATH_HOME", Some(home.path().as_os_str()), || {
let registry = build_provider_registry_from_config(&config)
.expect("an HTTPS client builds in tests");
assert!(registry.has("groq"));
assert!(registry.get("groq").is_some());
});
}
#[test]
fn build_provider_registry_all_keys_set() {
let config = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant-test".to_string()),
openai_api_key: Some("sk-test".to_string()),
google_api_key: Some("AIza-test".to_string()),
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
fallback_order: Vec::new(),
},
openrouter_api_key: Some("sk-or-test".to_string()),
ollama_base_url: Some("http://custom:11434".to_string()),
..Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("anthropic"));
assert!(registry.has("openai"));
assert!(registry.has("google"));
assert!(registry.has("openrouter"));
assert!(registry.has("ollama"));
assert!(!registry.has("claude-code"));
}
#[test]
fn provider_creds_carry_the_anthropic_cache_ttl() {
use leviath_providers::anthropic::CacheTtl;
let mut config = Config::default();
config.providers.anthropic_api_key = Some("k".to_string());
config.providers.openai_api_key = Some("k".to_string());
config.providers.anthropic_cache_ttl = Some(CacheTtl::Ephemeral1h);
let creds = provider_creds_from_config(&config);
let anthropic = creds
.iter()
.find(|c| c.name == "anthropic")
.expect("anthropic is registered");
assert_eq!(
anthropic.options.get("cache_ttl").map(String::as_str),
Some("1h")
);
let openai = creds.iter().find(|c| c.name == "openai").expect("openai");
assert!(!openai.options.contains_key("cache_ttl"));
}
#[test]
fn the_five_minute_ttl_is_carried_explicitly_too() {
use leviath_providers::anthropic::CacheTtl;
let mut config = Config::default();
config.providers.anthropic_api_key = Some("k".to_string());
config.providers.anthropic_cache_ttl = Some(CacheTtl::Ephemeral5m);
let creds = provider_creds_from_config(&config);
assert_eq!(
creds[0].options.get("cache_ttl").map(String::as_str),
Some("5m")
);
}
#[test]
fn no_configured_ttl_carries_nothing() {
let mut config = Config::default();
config.providers.anthropic_api_key = Some("k".to_string());
let creds = provider_creds_from_config(&config);
assert!(!creds[0].options.contains_key("cache_ttl"));
}
#[test]
fn provider_creds_from_config_includes_defaults_and_keyed() {
let config = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant".to_string()),
..Config::default().providers
},
ollama_base_url: Some("http://custom:11434".to_string()),
..Config::default()
};
let creds = provider_creds_from_config(&config);
let names: Vec<&str> = creds.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"anthropic"));
assert!(names.contains(&"ollama"));
assert!(!names.contains(&"claude-code"));
assert!(!names.contains(&"openai"));
assert!(!names.contains(&"google"));
assert!(!names.contains(&"openrouter"));
let ollama = creds.iter().find(|c| c.name == "ollama").unwrap();
assert_eq!(ollama.base_url.as_deref(), Some("http://custom:11434"));
assert!(ollama.api_key.is_none());
}
#[test]
fn provider_creds_from_config_ignores_blank_keys() {
let config = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some(String::new()),
openai_api_key: Some(" ".to_string()),
google_api_key: Some("AIza-real".to_string()),
..Config::default().providers
},
..Config::default()
};
let creds = provider_creds_from_config(&config);
let names: Vec<&str> = creds.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"google"),
"the configured provider must register: {names:?}"
);
assert!(!names.contains(&"anthropic"), "empty key must not register");
assert!(
!names.contains(&"openai"),
"whitespace-only key must not register"
);
}
#[test]
fn provider_creds_from_config_carries_rate_limits() {
let config = Config {
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant".to_string()),
openai_api_key: Some("sk-oa".to_string()),
..Config::default().providers
},
rate_limits: std::collections::HashMap::from([(
"anthropic".to_string(),
leviath_providers::RateLimitConfig {
requests_per_minute: 50,
tokens_per_minute: 40_000,
},
)]),
..Config::default()
};
let creds = provider_creds_from_config(&config);
let anthropic = creds.iter().find(|c| c.name == "anthropic").unwrap();
assert_eq!(
anthropic.rate_limit.as_ref().map(|r| r.requests_per_minute),
Some(50)
);
let openai = creds.iter().find(|c| c.name == "openai").unwrap();
assert!(openai.rate_limit.is_none());
}
#[test]
fn build_provider_registry_defaults_have_ollama_only() {
let config = Config::default();
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("ollama"));
assert!(!registry.has("claude-code"));
}
#[test]
fn enabling_claude_code_registers_it_with_its_options() {
let config = Config {
providers: crate::config::ProviderConfig {
claude_code_enabled: true,
claude_code_binary: Some("/opt/bin/claude".to_string()),
claude_code_effort: Some("low".to_string()),
..Config::default().providers
},
..Config::default()
};
let creds = provider_creds_from_config(&config);
let cc = creds
.iter()
.find(|c| c.name == "claude-code")
.expect("enabled ⇒ present");
assert_eq!(
cc.options.get("binary").map(String::as_str),
Some("/opt/bin/claude")
);
assert_eq!(cc.options.get("effort").map(String::as_str), Some("low"));
assert!(cc.api_key.is_none());
assert!(
build_provider_registry_from_config(&config)
.expect("an HTTPS client builds in tests")
.has("claude-code")
);
}
#[test]
fn enabling_claude_code_without_options_carries_none() {
let config = Config {
providers: crate::config::ProviderConfig {
claude_code_enabled: true,
..Config::default().providers
},
..Config::default()
};
let creds = provider_creds_from_config(&config);
let cc = creds.iter().find(|c| c.name == "claude-code").unwrap();
assert!(cc.options.is_empty());
}
#[test]
fn build_provider_registry_propagates_model_capabilities() {
use leviath_providers::ModelCapabilities;
let mut caps = std::collections::HashMap::new();
caps.insert(
"custom-model".to_string(),
ModelCapabilities {
supports_temperature: true,
supports_streaming: true,
supports_tools: true,
supports_system_prompt: true,
max_context_tokens: 9999,
max_output_tokens: 999,
}
.into(),
);
let config = crate::config::Config {
model_capabilities: caps,
providers: crate::config::ProviderConfig {
anthropic_api_key: Some("sk-ant-test".to_string()),
openai_api_key: None,
google_api_key: None,
claude_code_enabled: false,
claude_code_binary: None,
claude_code_effort: None,
anthropic_cache_ttl: None,
fallback_order: Vec::new(),
},
..crate::config::Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("anthropic"));
assert!(registry.has("ollama"));
}
#[test]
fn build_provider_registry_ollama_with_custom_url_propagates_caps() {
use leviath_providers::ModelCapabilities;
let mut caps = std::collections::HashMap::new();
caps.insert(
"llama3-8b".to_string(),
ModelCapabilities {
supports_temperature: false,
supports_streaming: false,
supports_tools: false,
supports_system_prompt: false,
max_context_tokens: 99,
max_output_tokens: 99,
}
.into(),
);
let config = crate::config::Config {
ollama_base_url: Some("http://custom-ollama:11434".to_string()),
model_capabilities: caps,
..crate::config::Config::default()
};
let registry =
build_provider_registry_from_config(&config).expect("an HTTPS client builds in tests");
assert!(registry.has("ollama"));
}
}