pub mod anthropic;
pub mod error;
pub mod openrouter;
pub mod pricing;
pub mod resolve;
#[cfg(feature = "bedrock")]
pub mod bedrock;
#[cfg(test)]
pub(crate) mod test_support;
pub use anthropic::AnthropicProvider;
pub use error::SmLlmError;
pub use openrouter::OpenRouterProvider;
pub use resolve::{
ProviderRegistry, ResolvedCall, SmModelTier, TierResolver, resolve_provider_and_model,
resolve_tier_model,
};
#[cfg(feature = "bedrock")]
pub use bedrock::BedrockProvider;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
pub const ANTHROPIC_MODEL_PREFIX: &str = "anthropic/";
pub const BEDROCK_MODEL_PREFIX: &str = "bedrock/";
pub const OPENROUTER_MODEL_PREFIX: &str = "openrouter/";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderKind {
Auto,
Anthropic,
Bedrock,
OpenRouter,
}
impl ProviderKind {
pub fn parse(s: &str) -> Result<Self, SmLlmError> {
match s.trim().to_ascii_lowercase().as_str() {
"" | "auto" => Ok(ProviderKind::Auto),
"anthropic" => Ok(ProviderKind::Anthropic),
"bedrock" => Ok(ProviderKind::Bedrock),
"openrouter" => Ok(ProviderKind::OpenRouter),
other => Err(SmLlmError::Validation(format!(
"unknown [session_manager.inference] provider {other:?}; \
expected one of: auto, anthropic, bedrock, openrouter"
))),
}
}
pub fn name(self) -> &'static str {
match self {
ProviderKind::Auto => "auto",
ProviderKind::Anthropic => "anthropic",
ProviderKind::Bedrock => "bedrock",
ProviderKind::OpenRouter => "openrouter",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmRequest {
pub model: String,
pub system: String,
pub messages: Vec<ChatMessage>,
pub temperature: f32,
pub max_tokens: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmResponse {
pub text: String,
pub model: String,
pub input_tokens: u32,
pub output_tokens: u32,
pub latency_ms: u64,
pub cost_usd: f64,
}
#[async_trait]
pub trait LlmProvider: Send + Sync {
fn name(&self) -> &str;
async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, SmLlmError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_kind_parse_ok() {
assert_eq!(ProviderKind::parse("auto").unwrap(), ProviderKind::Auto);
assert_eq!(ProviderKind::parse("").unwrap(), ProviderKind::Auto);
assert_eq!(
ProviderKind::parse(" Anthropic ").unwrap(),
ProviderKind::Anthropic
);
assert_eq!(
ProviderKind::parse("BEDROCK").unwrap(),
ProviderKind::Bedrock
);
assert_eq!(
ProviderKind::parse("openrouter").unwrap(),
ProviderKind::OpenRouter
);
}
#[test]
fn provider_kind_name_round_trips() {
for kind in [
ProviderKind::Auto,
ProviderKind::Anthropic,
ProviderKind::Bedrock,
ProviderKind::OpenRouter,
] {
assert_eq!(ProviderKind::parse(kind.name()).unwrap(), kind);
}
assert_eq!(ProviderKind::Anthropic.name(), "anthropic");
}
#[test]
fn provider_kind_parse_rejects_unknown() {
let err = ProviderKind::parse("gpt-self-host").unwrap_err();
assert!(matches!(err, SmLlmError::Validation(_)));
assert!(err.is_alarm(), "unknown provider is a config alarm");
}
#[test]
fn llm_response_serde_roundtrip() {
let resp = LlmResponse {
text: "ok".to_string(),
model: "claude-sonnet-4-6".to_string(),
input_tokens: 100,
output_tokens: 20,
latency_ms: 42,
cost_usd: 0.000_5,
};
let json = serde_json::to_string(&resp).expect("serialise");
let back: LlmResponse = serde_json::from_str(&json).expect("deserialise");
assert_eq!(back.text, "ok");
assert_eq!(back.input_tokens, 100);
assert!((back.cost_usd - 0.000_5_f64).abs() < 1e-12);
}
#[test]
fn provider_trait_object_compiles() {
fn _accepts(_p: &dyn LlmProvider) {}
}
}