Skip to main content

brainwires_provider/
lib.rs

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