use sim_kernel::Symbol;
use std::time::Duration;
pub const DEFAULT_MAX_OUTPUT_BYTES: usize = 1024 * 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProviderAuth {
None,
BearerEnv {
env: String,
},
OptionalBearerEnv {
env: String,
},
HeaderEnv {
header: String,
env: String,
},
}
impl ProviderAuth {
pub fn default_env(&self) -> Option<&str> {
match self {
Self::None => None,
Self::BearerEnv { env } | Self::HeaderEnv { env, .. } => Some(env),
Self::OptionalBearerEnv { .. } => None,
}
}
pub fn env_hint(&self) -> Option<&str> {
match self {
Self::None => None,
Self::BearerEnv { env }
| Self::OptionalBearerEnv { env }
| Self::HeaderEnv { env, .. } => Some(env),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProviderProfile {
pub provider: Symbol,
pub runner_symbol: Symbol,
pub codec: Symbol,
pub default_endpoint: String,
pub models_path: &'static str,
pub chat_path: &'static str,
pub auth: ProviderAuth,
pub default_locality: Symbol,
pub default_model: String,
pub default_timeout: Duration,
pub default_stream: bool,
pub default_tools: bool,
pub default_max_output_bytes: usize,
}
impl ProviderProfile {
pub fn openai() -> Self {
provider_profiles::openai()
}
pub fn anthropic() -> Self {
provider_profiles::anthropic()
}
pub fn ollama() -> Self {
provider_profiles::ollama()
}
pub fn lm_studio() -> Self {
provider_profiles::lm_studio()
}
pub fn lemonade() -> Self {
provider_profiles::lemonade()
}
pub fn openai_compatible() -> Self {
provider_profiles::openai_compatible()
}
}
pub mod provider_profiles {
use super::{DEFAULT_MAX_OUTPUT_BYTES, ProviderAuth, ProviderProfile};
use sim_kernel::Symbol;
use std::time::Duration;
pub fn all() -> Vec<ProviderProfile> {
vec![
openai(),
anthropic(),
ollama(),
lm_studio(),
lemonade(),
openai_compatible(),
]
}
pub fn openai() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("openai"),
runner_symbol: Symbol::qualified("runner", "openai"),
codec: Symbol::qualified("codec", "openai"),
default_endpoint: "https://api.openai.com/v1".to_owned(),
models_path: "/models",
chat_path: "/chat/completions",
auth: ProviderAuth::BearerEnv {
env: "OPENAI_API_KEY".to_owned(),
},
default_locality: Symbol::new("network"),
default_model: "gpt-5-mini".to_owned(),
default_timeout: Duration::from_secs(60),
default_stream: false,
default_tools: true,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn anthropic() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("anthropic"),
runner_symbol: Symbol::qualified("runner", "anthropic"),
codec: Symbol::qualified("codec", "anthropic"),
default_endpoint: "https://api.anthropic.com/v1".to_owned(),
models_path: "/models",
chat_path: "/messages",
auth: ProviderAuth::HeaderEnv {
header: "x-api-key".to_owned(),
env: "ANTHROPIC_API_KEY".to_owned(),
},
default_locality: Symbol::new("network"),
default_model: "claude-sonnet-latest".to_owned(),
default_timeout: Duration::from_secs(60),
default_stream: false,
default_tools: true,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn ollama() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("ollama"),
runner_symbol: Symbol::qualified("runner", "ollama"),
codec: Symbol::qualified("codec", "ollama"),
default_endpoint: "http://127.0.0.1:11434".to_owned(),
models_path: "/api/tags",
chat_path: "/api/chat",
auth: ProviderAuth::None,
default_locality: Symbol::new("local"),
default_model: "qwen3.5:4b".to_owned(),
default_timeout: Duration::from_secs(120),
default_stream: true,
default_tools: false,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn lm_studio() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("lm-studio"),
runner_symbol: Symbol::qualified("runner", "lm-studio"),
codec: Symbol::qualified("codec", "lm-studio"),
default_endpoint: "http://127.0.0.1:1234/v1".to_owned(),
models_path: "/models",
chat_path: "/chat/completions",
auth: ProviderAuth::OptionalBearerEnv {
env: "LM_STUDIO_API_KEY".to_owned(),
},
default_locality: Symbol::new("local"),
default_model: "local/default".to_owned(),
default_timeout: Duration::from_secs(120),
default_stream: false,
default_tools: true,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn lemonade() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("lemonade"),
runner_symbol: Symbol::qualified("runner", "lemonade"),
codec: Symbol::qualified("codec", "lemonade"),
default_endpoint: "http://127.0.0.1:13305/v1".to_owned(),
models_path: "/models",
chat_path: "/chat/completions",
auth: ProviderAuth::None,
default_locality: Symbol::new("local"),
default_model: "Qwen3-Coder".to_owned(),
default_timeout: Duration::from_secs(120),
default_stream: false,
default_tools: true,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn openai_compatible() -> ProviderProfile {
ProviderProfile {
provider: Symbol::new("openai-compatible"),
runner_symbol: Symbol::qualified("runner", "openai-compatible"),
codec: Symbol::qualified("codec", "openai"),
default_endpoint: String::new(),
models_path: "/models",
chat_path: "/chat/completions",
auth: ProviderAuth::BearerEnv {
env: "OPENAI_API_KEY".to_owned(),
},
default_locality: Symbol::new("network"),
default_model: "gpt-4.1-mini".to_owned(),
default_timeout: Duration::from_secs(60),
default_stream: false,
default_tools: true,
default_max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
}
pub fn openai_profile() -> ProviderProfile {
provider_profiles::openai()
}
pub fn anthropic_profile() -> ProviderProfile {
provider_profiles::anthropic()
}
pub fn ollama_profile() -> ProviderProfile {
provider_profiles::ollama()
}
pub fn lm_studio_profile() -> ProviderProfile {
provider_profiles::lm_studio()
}
pub fn lemonade_profile() -> ProviderProfile {
provider_profiles::lemonade()
}
pub fn openai_compatible_profile() -> ProviderProfile {
provider_profiles::openai_compatible()
}
#[cfg(test)]
mod tests {
use super::{ProviderAuth, ProviderProfile, provider_profiles};
use sim_kernel::Symbol;
#[test]
fn profiles_cover_known_providers_and_generic_fallback() {
let providers = provider_profiles::all()
.into_iter()
.map(|profile| profile.provider.to_string())
.collect::<Vec<_>>();
assert_eq!(
providers,
vec![
"openai",
"anthropic",
"ollama",
"lm-studio",
"lemonade",
"openai-compatible"
]
);
}
#[test]
fn anthropic_profile_records_header_auth_and_messages_path() {
let profile = ProviderProfile::anthropic();
assert_eq!(profile.provider, Symbol::new("anthropic"));
assert_eq!(
profile.runner_symbol,
Symbol::qualified("runner", "anthropic")
);
assert_eq!(profile.codec, Symbol::qualified("codec", "anthropic"));
assert_eq!(profile.default_endpoint, "https://api.anthropic.com/v1");
assert_eq!(profile.models_path, "/models");
assert_eq!(profile.chat_path, "/messages");
assert_eq!(
profile.auth,
ProviderAuth::HeaderEnv {
header: "x-api-key".to_owned(),
env: "ANTHROPIC_API_KEY".to_owned()
}
);
assert_eq!(profile.default_locality, Symbol::new("network"));
}
#[test]
fn local_profiles_default_to_loopback_without_auth() {
for profile in [ProviderProfile::ollama(), ProviderProfile::lemonade()] {
assert_eq!(profile.auth, ProviderAuth::None);
assert_eq!(profile.default_locality, Symbol::new("local"));
assert!(profile.default_endpoint.starts_with("http://127.0.0.1:"));
}
}
#[test]
fn lm_studio_profile_records_optional_bearer_auth() {
let profile = ProviderProfile::lm_studio();
assert_eq!(
profile.auth,
ProviderAuth::OptionalBearerEnv {
env: "LM_STUDIO_API_KEY".to_owned()
}
);
assert_eq!(profile.auth.default_env(), None);
assert_eq!(profile.auth.env_hint(), Some("LM_STUDIO_API_KEY"));
assert_eq!(profile.default_locality, Symbol::new("local"));
assert!(profile.default_endpoint.starts_with("http://127.0.0.1:"));
}
}