pub mod anthropic;
pub mod google;
pub mod ollama;
pub mod openai;
pub use anthropic::AnthropicProvider;
pub use google::GoogleProvider;
pub use ollama::OllamaProvider;
pub use openai::OpenAIProvider;
use super::{LLMProvider, ProviderConfig, ProviderType};
use std::sync::Arc;
pub fn create_provider(config: ProviderConfig) -> anyhow::Result<Arc<dyn LLMProvider>> {
if config.provider == ProviderType::Bedrock {
return Err(bedrock_refusal());
}
if let Some(placeholder) = unfilled_placeholder(&config.effective_base_url()) {
return Err(unfilled_base_url_refusal(config.provider, &placeholder));
}
let provider: Arc<dyn LLMProvider> = match config.provider {
ProviderType::OpenAI => Arc::new(OpenAIProvider::new(config)),
ProviderType::Anthropic => Arc::new(AnthropicProvider::new(config)),
ProviderType::Google => Arc::new(GoogleProvider::new(config)),
ProviderType::Ollama => Arc::new(OllamaProvider::new(config)),
ProviderType::IronGate => Arc::new(OpenAIProvider::new(config)),
ProviderType::Azure
| ProviderType::Together
| ProviderType::Groq
| ProviderType::Perplexity
| ProviderType::Fireworks
| ProviderType::DeepSeek
| ProviderType::XAI
| ProviderType::OpenRouter
| ProviderType::VLLM
| ProviderType::Local => Arc::new(OpenAIProvider::new(config)),
ProviderType::Bedrock => return Err(bedrock_refusal()),
ProviderType::Mistral => Arc::new(OpenAIProvider::new(config)),
ProviderType::Cohere => Arc::new(OpenAIProvider::new(config)),
ProviderType::TGI => Arc::new(OpenAIProvider::new(config)),
ProviderType::LlamaCpp => Arc::new(OpenAIProvider::new(config)),
};
Ok(provider)
}
fn bedrock_refusal() -> anyhow::Error {
crate::safety::unimplemented(
"ai",
"Amazon Bedrock is not supported: it needs AWS SigV4 request signing, which IronShell does not implement; NOTHING WAS SENT",
"use anthropic:, openai: or irongate: instead",
)
}
fn unfilled_placeholder(url: &str) -> Option<String> {
let start = url.find(['{', '}'])?;
let rest = &url[start..];
let len = rest.find('}').map_or(rest.len(), |end| end + 1);
Some(rest[..len].to_string())
}
fn unfilled_base_url_refusal(provider: ProviderType, placeholder: &str) -> anyhow::Error {
let state = match provider {
ProviderType::Azure => format!(
"azure: base URL still has the unfilled placeholder {placeholder}; set the real endpoint with ProviderConfig::with_base_url (AZURE_OPENAI_ENDPOINT is read only by the default ai() path, not by the provider layer); NOTHING WAS SENT"
),
other => format!(
"{}: base URL still has the unfilled placeholder {placeholder}; set the real URL with ProviderConfig::with_base_url; NOTHING WAS SENT",
other.scheme()
),
};
crate::safety::bad_state("ai", &state, "ProviderConfig::with_base_url")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::providers::bridge::UniversalBackend;
use crate::safety::{code_of, ErrorCode};
const PLANTED_SECRET: &str = "FAKE-AWS-SECRET-FOR-TEST-ONLY-0000";
#[test]
fn bedrock_scheme_still_parses_to_bedrock() {
assert_eq!(
ProviderType::from_scheme("bedrock").unwrap(),
ProviderType::Bedrock
);
}
#[test]
fn bedrock_is_refused_with_unimplemented_and_no_provider_is_built() {
let config = ProviderConfig::new(ProviderType::Bedrock);
let err = match create_provider(config) {
Ok(_) => panic!("Bedrock must not construct a provider"),
Err(e) => e,
};
assert_eq!(code_of(&err), ErrorCode::Unimplemented);
let text = format!("{err:#}");
assert!(text.contains("SigV4"), "refusal should say why: {text}");
}
#[test]
fn bedrock_never_reads_the_aws_secret_and_error_does_not_echo_it() {
std::env::set_var("AWS_SECRET_ACCESS_KEY", PLANTED_SECRET);
let config = ProviderConfig::from_env(ProviderType::Bedrock).with_env_key();
std::env::remove_var("AWS_SECRET_ACCESS_KEY");
assert_eq!(ProviderType::Bedrock.api_key_env_var(), None);
assert!(
config.api_key.is_none(),
"the AWS secret must not become a bearer key"
);
let err = match create_provider(config) {
Ok(_) => panic!("Bedrock must not construct a provider"),
Err(e) => e,
};
let text = format!("{err:#}");
assert!(
!text.contains(PLANTED_SECRET),
"error leaked the secret: {text}"
);
}
#[test]
fn bedrock_uri_is_refused_by_the_bridge_before_any_request() {
let err = match UniversalBackend::from_uri("bedrock:anthropic.claude-v2") {
Ok(_) => panic!("a bedrock: URI must not produce a backend"),
Err(e) => e,
};
assert_eq!(code_of(&err), ErrorCode::Unimplemented);
}
const PLANTED_AZURE_KEY: &str = "FAKE-AZURE-KEY-FOR-TEST-ONLY-0000";
#[test]
fn azure_without_a_real_endpoint_is_refused_and_the_key_is_not_echoed() {
std::env::set_var("AZURE_OPENAI_API_KEY", PLANTED_AZURE_KEY);
let config = ProviderConfig::from_env(ProviderType::Azure).with_env_key();
std::env::remove_var("AZURE_OPENAI_API_KEY");
assert_eq!(config.api_key.as_deref(), Some(PLANTED_AZURE_KEY));
let err = match create_provider(config) {
Ok(_) => panic!("azure with a placeholder URL must not construct a provider"),
Err(e) => e,
};
assert_eq!(code_of(&err), ErrorCode::BadState);
let text = format!("{err:#}");
assert!(
!text.contains(PLANTED_AZURE_KEY),
"error leaked the key: {text}"
);
assert!(
text.contains("azure"),
"refusal should name the provider: {text}"
);
assert!(
text.contains("with_base_url"),
"refusal should name the setting: {text}"
);
}
#[test]
fn azure_uri_is_refused_by_the_bridge_before_any_request() {
let err = match UniversalBackend::from_uri("azure:gpt-4o/my-deployment") {
Ok(_) => panic!("an azure: URI with no endpoint must not produce a backend"),
Err(e) => e,
};
assert_eq!(code_of(&err), ErrorCode::BadState);
}
#[test]
fn azure_with_a_real_endpoint_builds_a_config_without_braces() {
let config = ProviderConfig::new(ProviderType::Azure)
.with_base_url("https://fake-resource.openai.azure.com/openai/deployments/fake")
.with_api_key(PLANTED_AZURE_KEY);
assert!(!config.effective_base_url().contains('{'));
assert!(
create_provider(config).is_ok(),
"a real endpoint must build a provider"
);
}
#[test]
fn a_placeholder_override_is_refused_for_any_provider() {
let config = ProviderConfig::new(ProviderType::OpenAI).with_base_url("https://{host}/v1");
let err = match create_provider(config) {
Ok(_) => panic!("a templated base URL must not construct a provider"),
Err(e) => e,
};
assert_eq!(code_of(&err), ErrorCode::BadState);
}
#[test]
fn a_normal_url_provider_is_unaffected() {
let config = ProviderConfig::new(ProviderType::OpenAI);
assert_eq!(config.effective_base_url(), "https://api.openai.com/v1");
assert!(create_provider(config).is_ok());
}
}