Skip to main content

rullama_provider/
lib.rs

1#![deny(missing_docs)]
2//! Provider layer for the rullama.
3//!
4//! Contains both low-level API client structs (HTTP transport, auth, rate
5//! limiting, serde) and high-level chat provider implementations that wrap
6//! them with the `rullama_core::Provider` trait.
7
8// Re-export core traits for convenience
9pub use rullama_core::provider::{ChatOptions, Provider};
10
11// Rate limiting and HTTP client
12#[cfg(feature = "native")]
13pub mod http_client;
14#[cfg(feature = "native")]
15pub mod rate_limiter;
16
17#[cfg(feature = "native")]
18pub use http_client::RateLimitedClient;
19#[cfg(feature = "native")]
20pub use rate_limiter::RateLimiter;
21
22// ── Protocol directories ──────────────────────────────────────────────
23
24/// OpenAI Chat Completions protocol (also used by Groq, Together, Fireworks, Anyscale).
25#[cfg(feature = "native")]
26pub mod openai_chat;
27
28/// OpenAI Responses API protocol (`/v1/responses`).
29#[cfg(feature = "native")]
30pub mod openai_responses;
31
32/// Anthropic Messages protocol (also used by Bedrock, Vertex AI).
33#[cfg(feature = "native")]
34pub mod anthropic;
35
36/// Google Gemini generateContent protocol.
37#[cfg(feature = "native")]
38pub mod gemini;
39
40/// Ollama native chat protocol.
41#[cfg(feature = "native")]
42pub mod ollama;
43
44// Speech (TTS / STT) provider clients live in `rullama-provider-speech`.
45
46// ── Registry ──────────────────────────────────────────────────────────
47
48/// Provider registry — protocol, auth, and endpoint metadata for all known providers.
49pub mod registry;
50
51// ── Model listing ─────────────────────────────────────────────────────
52
53/// Model listing — query available models from provider APIs.
54#[cfg(feature = "native")]
55pub mod model_listing;
56
57/// Chat provider factory — registry-driven protocol dispatch.
58#[cfg(feature = "native")]
59pub mod chat_factory;
60
61// ── Local LLM ─────────────────────────────────────────────────────────
62
63/// Local LLM inference (always compiled, llama.cpp behind feature flag).
64pub mod local_llm;
65
66// Browser-native `web_speech` lives in `rullama-provider-speech`.
67
68// ── Re-exports ────────────────────────────────────────────────────────
69
70// Chat-capable API clients
71#[cfg(feature = "native")]
72pub use anthropic::AnthropicClient;
73#[cfg(feature = "native")]
74pub use gemini::GoogleClient;
75#[cfg(feature = "native")]
76pub use ollama::OllamaProvider;
77#[cfg(feature = "native")]
78pub use openai_chat::OpenAiClient;
79
80// Chat providers
81#[cfg(feature = "native")]
82pub use anthropic::chat::AnthropicChatProvider;
83#[cfg(feature = "native")]
84pub use gemini::chat::GoogleChatProvider;
85#[cfg(feature = "native")]
86pub use ollama::chat::OllamaChatProvider;
87#[cfg(feature = "native")]
88pub use openai_chat::chat::OpenAiChatProvider;
89#[cfg(feature = "native")]
90pub use openai_responses::OpenAiResponsesProvider;
91
92// Audio/speech client re-exports live on `rullama-provider-speech`.
93
94// Model listing
95#[cfg(feature = "native")]
96pub use model_listing::{AvailableModel, ModelCapability, ModelLister, create_model_lister};
97
98// Factory
99#[cfg(feature = "native")]
100pub use chat_factory::ChatProviderFactory;
101
102// Local LLM
103pub use local_llm::*;
104
105use serde::{Deserialize, Serialize};
106use std::fmt;
107use std::str::FromStr;
108
109/// AI provider types
110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
111#[serde(rename_all = "lowercase")]
112pub enum ProviderType {
113    /// Anthropic (Claude).
114    Anthropic,
115    /// OpenAI (GPT).
116    OpenAI,
117    /// Google (Gemini).
118    Google,
119    /// Groq inference.
120    Groq,
121    /// Ollama local models.
122    Ollama,
123    /// Together AI.
124    Together,
125    /// Fireworks AI.
126    Fireworks,
127    /// Anyscale.
128    Anyscale,
129    /// Amazon Bedrock (Anthropic Messages via AWS SigV4).
130    Bedrock,
131    /// Google Vertex AI (Anthropic Messages via OAuth2).
132    VertexAI,
133    /// ElevenLabs.
134    ElevenLabs,
135    /// Deepgram.
136    Deepgram,
137    /// Azure Speech.
138    Azure,
139    /// Fish Audio.
140    Fish,
141    /// Cartesia.
142    Cartesia,
143    /// Murf AI.
144    Murf,
145    /// OpenAI Responses API.
146    OpenAiResponses,
147    /// MiniMax AI.
148    MiniMax,
149    /// Custom / user-defined provider.
150    Custom,
151}
152
153impl ProviderType {
154    /// Get the default model for this provider
155    pub fn default_model(&self) -> &'static str {
156        match self {
157            Self::Anthropic => "claude-sonnet-4-6",
158            Self::OpenAI => "gpt-5-mini",
159            Self::Google => "gemini-2.5-flash",
160            Self::Groq => "llama-3.3-70b-versatile",
161            Self::Ollama => "llama3.3",
162            Self::Together => "meta-llama/Llama-3.1-8B-Instruct",
163            Self::Fireworks => "accounts/fireworks/models/llama-v3p1-8b-instruct",
164            Self::Anyscale => "meta-llama/Meta-Llama-3.1-8B-Instruct",
165            Self::Bedrock => "anthropic.claude-sonnet-4-6-v1:0",
166            Self::VertexAI => "claude-sonnet-4-6",
167            Self::ElevenLabs => "eleven_multilingual_v2",
168            Self::Deepgram => "nova-2",
169            Self::Azure => "en-US-JennyNeural",
170            Self::Fish => "default",
171            Self::Cartesia => "sonic-english",
172            Self::Murf => "en-US-natalie",
173            Self::OpenAiResponses => "gpt-5-mini",
174            Self::MiniMax => "MiniMax-M2.7",
175            Self::Custom => "claude-sonnet-4-6",
176        }
177    }
178
179    /// Parse from string
180    pub fn from_str_opt(s: &str) -> Option<Self> {
181        match s.to_lowercase().as_str() {
182            "anthropic" => Some(Self::Anthropic),
183            "openai" => Some(Self::OpenAI),
184            "google" | "gemini" => Some(Self::Google),
185            "groq" => Some(Self::Groq),
186            "ollama" => Some(Self::Ollama),
187            "together" => Some(Self::Together),
188            "fireworks" => Some(Self::Fireworks),
189            "anyscale" => Some(Self::Anyscale),
190            "bedrock" => Some(Self::Bedrock),
191            "vertex-ai" | "vertexai" | "vertex_ai" => Some(Self::VertexAI),
192            "elevenlabs" => Some(Self::ElevenLabs),
193            "deepgram" => Some(Self::Deepgram),
194            "azure" => Some(Self::Azure),
195            "fish" => Some(Self::Fish),
196            "cartesia" => Some(Self::Cartesia),
197            "murf" => Some(Self::Murf),
198            "openai-responses" | "openai_responses" => Some(Self::OpenAiResponses),
199            "minimax" => Some(Self::MiniMax),
200            "custom" => Some(Self::Custom),
201            _ => None,
202        }
203    }
204
205    /// Convert to string
206    pub fn as_str(&self) -> &'static str {
207        match self {
208            Self::Anthropic => "anthropic",
209            Self::OpenAI => "openai",
210            Self::Google => "google",
211            Self::Groq => "groq",
212            Self::Ollama => "ollama",
213            Self::Together => "together",
214            Self::Fireworks => "fireworks",
215            Self::Anyscale => "anyscale",
216            Self::Bedrock => "bedrock",
217            Self::VertexAI => "vertex-ai",
218            Self::ElevenLabs => "elevenlabs",
219            Self::Deepgram => "deepgram",
220            Self::Azure => "azure",
221            Self::Fish => "fish",
222            Self::Cartesia => "cartesia",
223            Self::Murf => "murf",
224            Self::OpenAiResponses => "openai-responses",
225            Self::MiniMax => "minimax",
226            Self::Custom => "custom",
227        }
228    }
229
230    /// Whether this provider requires an API key
231    pub fn requires_api_key(&self) -> bool {
232        !matches!(self, Self::Ollama | Self::Bedrock | Self::VertexAI)
233    }
234}
235
236impl fmt::Display for ProviderType {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(f, "{}", self.as_str())
239    }
240}
241
242impl FromStr for ProviderType {
243    type Err = anyhow::Error;
244
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        Self::from_str_opt(s).ok_or_else(|| anyhow::anyhow!("Unknown provider: {}", s))
247    }
248}
249
250/// Provider configuration
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ProviderConfig {
253    /// Provider type
254    pub provider: ProviderType,
255    /// Model name
256    pub model: String,
257    /// API key (if required)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub api_key: Option<String>,
260    /// Base URL (for custom endpoints)
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub base_url: Option<String>,
263    /// Additional provider-specific options
264    #[serde(flatten)]
265    pub options: std::collections::HashMap<String, serde_json::Value>,
266    /// Analytics collector — not serialized, threaded through at runtime.
267    #[cfg(feature = "telemetry")]
268    #[serde(skip)]
269    pub analytics_collector: Option<std::sync::Arc<rullama_telemetry::AnalyticsCollector>>,
270}
271
272impl ProviderConfig {
273    /// Create a new provider config
274    pub fn new(provider: ProviderType, model: String) -> Self {
275        Self {
276            provider,
277            model,
278            api_key: None,
279            base_url: None,
280            options: std::collections::HashMap::new(),
281            #[cfg(feature = "telemetry")]
282            analytics_collector: None,
283        }
284    }
285
286    /// Set API key
287    pub fn with_api_key<S: Into<String>>(mut self, api_key: S) -> Self {
288        self.api_key = Some(api_key.into());
289        self
290    }
291
292    /// Set base URL
293    pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
294        self.base_url = Some(base_url.into());
295        self
296    }
297
298    /// Set a provider-specific option.
299    pub fn with_option(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
300        self.options.insert(key.into(), value);
301        self
302    }
303
304    /// Set the AWS region (for Bedrock) or GCP region (for Vertex AI).
305    pub fn with_region(self, region: impl Into<String>) -> Self {
306        self.with_option("region", serde_json::Value::String(region.into()))
307    }
308
309    /// Set the GCP project ID (for Vertex AI).
310    pub fn with_project_id(self, project_id: impl Into<String>) -> Self {
311        self.with_option("project_id", serde_json::Value::String(project_id.into()))
312    }
313
314    /// Attach an analytics collector — called by the factory layer before provider construction.
315    #[cfg(feature = "telemetry")]
316    pub fn with_analytics(
317        mut self,
318        collector: std::sync::Arc<rullama_telemetry::AnalyticsCollector>,
319    ) -> Self {
320        self.analytics_collector = Some(collector);
321        self
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_provider_type_default_model() {
331        assert_eq!(ProviderType::Anthropic.default_model(), "claude-sonnet-4-6");
332        assert_eq!(ProviderType::OpenAI.default_model(), "gpt-5-mini");
333        assert_eq!(ProviderType::Google.default_model(), "gemini-2.5-flash");
334        assert_eq!(
335            ProviderType::Groq.default_model(),
336            "llama-3.3-70b-versatile"
337        );
338        assert_eq!(ProviderType::Ollama.default_model(), "llama3.3");
339        assert_eq!(ProviderType::MiniMax.default_model(), "MiniMax-M2.7");
340    }
341
342    #[test]
343    fn test_provider_type_from_str() {
344        assert_eq!(
345            ProviderType::from_str_opt("anthropic"),
346            Some(ProviderType::Anthropic)
347        );
348        assert_eq!(
349            ProviderType::from_str_opt("openai"),
350            Some(ProviderType::OpenAI)
351        );
352        assert_eq!(
353            ProviderType::from_str_opt("google"),
354            Some(ProviderType::Google)
355        );
356        assert_eq!(
357            ProviderType::from_str_opt("gemini"),
358            Some(ProviderType::Google)
359        );
360        assert_eq!(ProviderType::from_str_opt("groq"), Some(ProviderType::Groq));
361        assert_eq!(
362            ProviderType::from_str_opt("ollama"),
363            Some(ProviderType::Ollama)
364        );
365        assert_eq!(
366            ProviderType::from_str_opt("together"),
367            Some(ProviderType::Together)
368        );
369        assert_eq!(
370            ProviderType::from_str_opt("fireworks"),
371            Some(ProviderType::Fireworks)
372        );
373        assert_eq!(
374            ProviderType::from_str_opt("anyscale"),
375            Some(ProviderType::Anyscale)
376        );
377        assert_eq!(
378            ProviderType::from_str_opt("elevenlabs"),
379            Some(ProviderType::ElevenLabs)
380        );
381        assert_eq!(
382            ProviderType::from_str_opt("deepgram"),
383            Some(ProviderType::Deepgram)
384        );
385        assert_eq!(
386            ProviderType::from_str_opt("custom"),
387            Some(ProviderType::Custom)
388        );
389        assert_eq!(
390            ProviderType::from_str_opt("minimax"),
391            Some(ProviderType::MiniMax)
392        );
393        assert_eq!(ProviderType::from_str_opt("unknown"), None);
394    }
395
396    #[test]
397    fn test_provider_type_requires_api_key() {
398        assert!(ProviderType::Anthropic.requires_api_key());
399        assert!(ProviderType::OpenAI.requires_api_key());
400        assert!(!ProviderType::Ollama.requires_api_key());
401        assert!(ProviderType::ElevenLabs.requires_api_key());
402        assert!(ProviderType::MiniMax.requires_api_key());
403    }
404
405    #[test]
406    fn test_provider_config() {
407        let config = ProviderConfig::new(ProviderType::Anthropic, "claude-3".to_string())
408            .with_api_key("sk-test")
409            .with_base_url("https://api.example.com");
410        assert_eq!(config.provider, ProviderType::Anthropic);
411        assert_eq!(config.api_key, Some("sk-test".to_string()));
412        assert_eq!(config.base_url, Some("https://api.example.com".to_string()));
413    }
414}