use std::fmt;
use std::str::FromStr;
use async_trait::async_trait;
use serde::de::DeserializeOwned;
use crate::backend::usage::{
GenerateResult, MaterializeFailure, MaterializeReport, MaterializeResult,
};
use crate::backend::{LLMClient, MediaFile, ModelInfo};
use crate::error::{ApiErrorKind, RStructorError, Result};
use crate::model::Instructor;
#[cfg(feature = "anthropic")]
use crate::backend::anthropic::AnthropicClient;
#[cfg(feature = "gemini")]
use crate::backend::gemini::GeminiClient;
#[cfg(feature = "grok")]
use crate::backend::grok::GrokClient;
#[cfg(feature = "openai")]
use crate::backend::openai::OpenAIClient;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
#[cfg(feature = "openai")]
OpenAI,
#[cfg(feature = "anthropic")]
Anthropic,
#[cfg(feature = "gemini")]
Gemini,
#[cfg(feature = "grok")]
Grok,
}
impl FromStr for Provider {
type Err = RStructorError;
fn from_str(name: &str) -> Result<Self> {
match name.to_ascii_lowercase().as_str() {
"openai" => {
#[cfg(feature = "openai")]
{
Ok(Self::OpenAI)
}
#[cfg(not(feature = "openai"))]
{
Err(provider_feature_disabled("openai", "openai"))
}
}
"anthropic" => {
#[cfg(feature = "anthropic")]
{
Ok(Self::Anthropic)
}
#[cfg(not(feature = "anthropic"))]
{
Err(provider_feature_disabled("anthropic", "anthropic"))
}
}
"gemini" => {
#[cfg(feature = "gemini")]
{
Ok(Self::Gemini)
}
#[cfg(not(feature = "gemini"))]
{
Err(provider_feature_disabled("gemini", "gemini"))
}
}
"grok" | "xai" => {
#[cfg(feature = "grok")]
{
Ok(Self::Grok)
}
#[cfg(not(feature = "grok"))]
{
Err(provider_feature_disabled(name, "grok"))
}
}
_ => Err(RStructorError::Unsupported(format!(
"unknown provider `{name}`; valid providers: openai, anthropic, gemini, grok (alias: xai)"
))),
}
}
}
impl fmt::Display for Provider {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "openai")]
Self::OpenAI => formatter.write_str("openai"),
#[cfg(feature = "anthropic")]
Self::Anthropic => formatter.write_str("anthropic"),
#[cfg(feature = "gemini")]
Self::Gemini => formatter.write_str("gemini"),
#[cfg(feature = "grok")]
Self::Grok => formatter.write_str("grok"),
}
}
}
#[cfg(any(
not(feature = "openai"),
not(feature = "anthropic"),
not(feature = "gemini"),
not(feature = "grok")
))]
fn provider_feature_disabled(provider: &str, feature: &str) -> RStructorError {
RStructorError::Unsupported(format!(
"provider `{provider}` is disabled; enable the `{feature}` Cargo feature"
))
}
#[derive(Clone)]
pub enum AnyClient {
#[cfg(feature = "openai")]
OpenAI(OpenAIClient),
#[cfg(feature = "anthropic")]
Anthropic(AnthropicClient),
#[cfg(feature = "grok")]
Grok(GrokClient),
#[cfg(feature = "gemini")]
Gemini(GeminiClient),
}
impl AnyClient {
pub fn from_env_for(provider: Provider) -> Result<Self> {
match provider {
#[cfg(feature = "openai")]
Provider::OpenAI => Ok(Self::OpenAI(OpenAIClient::from_env()?)),
#[cfg(feature = "anthropic")]
Provider::Anthropic => Ok(Self::Anthropic(AnthropicClient::from_env()?)),
#[cfg(feature = "grok")]
Provider::Grok => Ok(Self::Grok(GrokClient::from_env()?)),
#[cfg(feature = "gemini")]
Provider::Gemini => Ok(Self::Gemini(GeminiClient::from_env()?)),
}
}
#[must_use]
pub fn provider(&self) -> Provider {
match self {
#[cfg(feature = "openai")]
Self::OpenAI(_) => Provider::OpenAI,
#[cfg(feature = "anthropic")]
Self::Anthropic(_) => Provider::Anthropic,
#[cfg(feature = "grok")]
Self::Grok(_) => Provider::Grok,
#[cfg(feature = "gemini")]
Self::Gemini(_) => Provider::Gemini,
}
}
}
#[cfg(feature = "openai")]
impl From<OpenAIClient> for AnyClient {
fn from(client: OpenAIClient) -> Self {
Self::OpenAI(client)
}
}
#[cfg(feature = "anthropic")]
impl From<AnthropicClient> for AnyClient {
fn from(client: AnthropicClient) -> Self {
Self::Anthropic(client)
}
}
#[cfg(feature = "grok")]
impl From<GrokClient> for AnyClient {
fn from(client: GrokClient) -> Self {
Self::Grok(client)
}
}
#[cfg(feature = "gemini")]
impl From<GeminiClient> for AnyClient {
fn from(client: GeminiClient) -> Self {
Self::Gemini(client)
}
}
macro_rules! dispatch {
($self:expr, $client:ident => $call:expr) => {
match $self {
#[cfg(feature = "openai")]
Self::OpenAI($client) => $call,
#[cfg(feature = "anthropic")]
Self::Anthropic($client) => $call,
#[cfg(feature = "grok")]
Self::Grok($client) => $call,
#[cfg(feature = "gemini")]
Self::Gemini($client) => $call,
}
};
}
#[async_trait]
impl LLMClient for AnyClient {
async fn materialize<T>(&self, prompt: &str) -> Result<T>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize(prompt).await)
}
async fn materialize_with_media<T>(&self, prompt: &str, media: &[MediaFile]) -> Result<T>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_with_media(prompt, media).await)
}
async fn materialize_with_metadata<T>(&self, prompt: &str) -> Result<MaterializeResult<T>>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_with_metadata(prompt).await)
}
async fn materialize_with_attempts<T>(
&self,
prompt: &str,
) -> std::result::Result<MaterializeReport<T>, MaterializeFailure>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_with_attempts(prompt).await)
}
async fn materialize_with_media_and_attempts<T>(
&self,
prompt: &str,
media: &[MediaFile],
) -> std::result::Result<MaterializeReport<T>, MaterializeFailure>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_with_media_and_attempts(prompt, media).await)
}
async fn materialize_request<T>(
&self,
system: Option<&str>,
prompt: &str,
media: &[MediaFile],
) -> Result<T>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_request(system, prompt, media).await)
}
async fn materialize_request_with_attempts<T>(
&self,
system: Option<&str>,
prompt: &str,
media: &[MediaFile],
) -> std::result::Result<MaterializeReport<T>, MaterializeFailure>
where
T: Instructor + DeserializeOwned + Send + 'static,
{
dispatch!(self, c => c.materialize_request_with_attempts(system, prompt, media).await)
}
async fn generate(&self, prompt: &str) -> Result<String> {
dispatch!(self, c => c.generate(prompt).await)
}
async fn generate_with_media(&self, prompt: &str, media: &[MediaFile]) -> Result<String> {
dispatch!(self, c => c.generate_with_media(prompt, media).await)
}
async fn generate_with_metadata(&self, prompt: &str) -> Result<GenerateResult> {
dispatch!(self, c => c.generate_with_metadata(prompt).await)
}
async fn generate_request(
&self,
system: Option<&str>,
prompt: &str,
media: &[MediaFile],
) -> Result<String> {
dispatch!(self, c => c.generate_request(system, prompt, media).await)
}
#[cfg(feature = "streaming")]
fn generate_stream_request<'a>(
&'a self,
system: Option<String>,
prompt: String,
) -> crate::backend::streaming::TextStream<'a>
where
Self: Sync,
{
dispatch!(self, c => c.generate_stream_request(system, prompt))
}
#[cfg(feature = "streaming")]
fn materialize_stream_request<'a, T>(
&'a self,
system: Option<String>,
prompt: String,
) -> crate::backend::streaming::ObjectStream<'a, T>
where
T: Instructor + DeserializeOwned + Send + 'static,
Self: Sync,
{
dispatch!(self, c => c.materialize_stream_request::<T>(system, prompt))
}
#[cfg(feature = "streaming")]
fn materialize_iter_request<'a, T>(
&'a self,
system: Option<String>,
prompt: String,
) -> crate::backend::streaming::ItemStream<'a, T>
where
T: Instructor + DeserializeOwned + Send + 'static,
Self: Sync,
{
dispatch!(self, c => c.materialize_iter_request::<T>(system, prompt))
}
fn from_env() -> Result<Self> {
#[cfg(feature = "openai")]
if std::env::var("OPENAI_API_KEY").is_ok() {
return Ok(Self::OpenAI(OpenAIClient::from_env()?));
}
#[cfg(feature = "anthropic")]
if std::env::var("ANTHROPIC_API_KEY").is_ok() {
return Ok(Self::Anthropic(AnthropicClient::from_env()?));
}
#[cfg(feature = "gemini")]
if std::env::var("GEMINI_API_KEY").is_ok() || std::env::var("GOOGLE_API_KEY").is_ok() {
return Ok(Self::Gemini(GeminiClient::from_env()?));
}
#[cfg(feature = "grok")]
if std::env::var("XAI_API_KEY").is_ok() {
return Ok(Self::Grok(GrokClient::from_env()?));
}
Err(RStructorError::api_error(
"AnyClient",
ApiErrorKind::AuthenticationFailed,
))
}
async fn list_models(&self) -> Result<Vec<ModelInfo>> {
dispatch!(self, c => c.list_models().await)
}
}