pub mod health;
pub mod not_yet_implemented;
pub mod openai;
use std::sync::Arc;
use async_trait::async_trait;
use crate::error::{Error, Result};
use crate::models::reasoning::{
ClassificationRequest, ClassificationResponse, ReasoningError, SummaryRequest, SummaryResponse,
};
use crate::models::ReasoningConfig;
#[async_trait]
pub trait ReasoningProvider: Send + Sync {
async fn classify(
&self,
request: ClassificationRequest,
) -> std::result::Result<ClassificationResponse, ReasoningError>;
async fn summarize(
&self,
request: SummaryRequest,
) -> std::result::Result<SummaryResponse, ReasoningError>;
async fn health_check(&self) -> std::result::Result<(), ReasoningError>;
}
pub fn build_provider(config: &ReasoningConfig) -> Result<Arc<dyn ReasoningProvider>> {
match config.provider.as_str() {
"openai" | "openai-compatible" => Ok(Arc::new(openai::OpenAiProvider::new(config)?)),
other @ ("anthropic" | "ppqai" | "openclaw") => Ok(Arc::new(
not_yet_implemented::NotYetImplementedProvider::new(other),
)),
other => Err(Error::Config(format!(
"unknown reasoning provider '{other}'; supported: openai, openai-compatible \
(anthropic, ppqai, openclaw are declared but not yet implemented in Phase 3)"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::ReasoningConfig;
fn anthropic_cfg() -> ReasoningConfig {
ReasoningConfig {
provider: "anthropic".into(),
..ReasoningConfig::default()
}
}
#[tokio::test]
async fn build_provider_returns_nyi_for_anthropic() {
let provider = build_provider(&anthropic_cfg()).expect("builds");
let err = provider.health_check().await.unwrap_err();
match err {
ReasoningError::Unreachable(msg) => {
assert!(
msg.contains("anthropic"),
"error should name the provider: {msg}"
);
assert!(
msg.contains("not yet implemented"),
"error should flag NYI status: {msg}"
);
}
other => panic!("expected Unreachable, got {other:?}"),
}
}
#[tokio::test]
async fn build_provider_does_not_coerce_nyi_to_openai() {
let provider = build_provider(&anthropic_cfg()).expect("builds");
assert!(provider.health_check().await.is_err());
}
#[test]
fn build_provider_rejects_unknown_provider_name() {
let cfg = ReasoningConfig {
provider: "totally_made_up".into(),
..ReasoningConfig::default()
};
match build_provider(&cfg) {
Err(Error::Config(msg)) => {
assert!(
msg.contains("totally_made_up"),
"error should name the provider: {msg}"
);
}
Err(other) => panic!("expected Error::Config, got {other}"),
Ok(_) => panic!("unknown providers must not build"),
}
}
}