Skip to main content

aither_cloud/
lib.rs

1//! Unified cloud provider wrapping `OpenAI`, Claude, Gemini, and GitHub Copilot.
2//!
3//! This crate provides a single `CloudProvider` enum that implements `LanguageModel`,
4//! allowing seamless switching between providers.
5//!
6//! It also provides `CloudModelProvider` enum that implements `LanguageModelProvider`,
7//! allowing unified model listing and instantiation across providers.
8
9pub use aither_claude::{self as claude, Claude, ClaudeProvider};
10pub use aither_codex::{self as codex, Codex, CodexProvider};
11pub use aither_copilot::{self as copilot, Copilot, CopilotProvider};
12pub use aither_gemini::{self as gemini, Gemini, GeminiProvider};
13pub use aither_openai::{self as openai, OpenAI, OpenAIProvider};
14
15use aither_core::{
16    EmbeddingModel, LanguageModel,
17    llm::{
18        Classify, Event, LLMRequest, LanguageModelProvider, RetryPolicy, model::Profile,
19        provider::Profile as ProviderProfile,
20    },
21};
22use futures_core::Stream;
23use futures_lite::StreamExt;
24
25/// Unified cloud provider wrapping `OpenAI`, Claude, Gemini, and GitHub Copilot.
26#[derive(Clone)]
27pub enum CloudProvider {
28    /// `OpenAI` GPT models.
29    OpenAI(OpenAI),
30    /// Anthropic Claude models.
31    Claude(Claude),
32    /// Google Gemini models.
33    Gemini(Gemini),
34    /// GitHub Copilot models.
35    Copilot(Copilot),
36    /// `ChatGPT` Codex subscription models.
37    Codex(Codex),
38}
39
40/// Unified embedding-capable cloud provider.
41#[derive(Clone, Debug)]
42pub enum CloudEmbedder {
43    /// `OpenAI` embeddings.
44    OpenAI(OpenAI),
45    /// Google Gemini embeddings.
46    Gemini(Gemini),
47}
48
49impl From<OpenAI> for CloudEmbedder {
50    fn from(client: OpenAI) -> Self {
51        Self::OpenAI(client)
52    }
53}
54
55impl From<Gemini> for CloudEmbedder {
56    fn from(client: Gemini) -> Self {
57        Self::Gemini(client)
58    }
59}
60
61/// Error returned when a cloud provider cannot be used as an embedder.
62#[derive(Debug, thiserror::Error)]
63pub enum CloudEmbedderError {
64    /// The provider does not implement embeddings.
65    #[error("provider does not support embeddings: {0}")]
66    UnsupportedProvider(&'static str),
67}
68
69impl TryFrom<CloudProvider> for CloudEmbedder {
70    type Error = CloudEmbedderError;
71
72    fn try_from(value: CloudProvider) -> Result<Self, Self::Error> {
73        match value {
74            CloudProvider::OpenAI(client) => Ok(Self::OpenAI(client)),
75            CloudProvider::Gemini(client) => Ok(Self::Gemini(client)),
76            CloudProvider::Claude(_) => Err(CloudEmbedderError::UnsupportedProvider("claude")),
77            CloudProvider::Copilot(_) => Err(CloudEmbedderError::UnsupportedProvider("copilot")),
78            CloudProvider::Codex(_) => Err(CloudEmbedderError::UnsupportedProvider("codex")),
79        }
80    }
81}
82
83impl EmbeddingModel for CloudEmbedder {
84    fn dim(&self) -> usize {
85        match self {
86            Self::OpenAI(client) => client.dim(),
87            Self::Gemini(client) => client.dim(),
88        }
89    }
90
91    fn embed(
92        &self,
93        text: &str,
94    ) -> impl std::future::Future<Output = aither_core::Result<Vec<f32>>> + Send {
95        let embedder = self.clone();
96        let text = text.to_string();
97        async move {
98            match embedder {
99                Self::OpenAI(client) => client.embed(text.as_str()).await,
100                Self::Gemini(client) => client.embed(text.as_str()).await,
101            }
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::{CloudEmbedder, CloudEmbedderError, CloudProvider};
109    use crate::{Claude, Copilot, Gemini, OpenAI};
110
111    #[test]
112    fn openai_and_gemini_convert_to_cloud_embedder() {
113        assert!(matches!(
114            CloudEmbedder::try_from(CloudProvider::OpenAI(OpenAI::new("test"))),
115            Ok(CloudEmbedder::OpenAI(_))
116        ));
117        assert!(matches!(
118            CloudEmbedder::try_from(CloudProvider::Gemini(Gemini::new("test"))),
119            Ok(CloudEmbedder::Gemini(_))
120        ));
121    }
122
123    #[test]
124    fn unsupported_cloud_providers_fail_fast() {
125        assert!(matches!(
126            CloudEmbedder::try_from(CloudProvider::Claude(Claude::new("test"))),
127            Err(CloudEmbedderError::UnsupportedProvider("claude"))
128        ));
129        assert!(matches!(
130            CloudEmbedder::try_from(CloudProvider::Copilot(Copilot::new("test"))),
131            Err(CloudEmbedderError::UnsupportedProvider("copilot"))
132        ));
133    }
134}
135
136impl From<OpenAI> for CloudProvider {
137    fn from(client: OpenAI) -> Self {
138        Self::OpenAI(client)
139    }
140}
141
142impl From<Claude> for CloudProvider {
143    fn from(client: Claude) -> Self {
144        Self::Claude(client)
145    }
146}
147
148impl From<Gemini> for CloudProvider {
149    fn from(client: Gemini) -> Self {
150        Self::Gemini(client)
151    }
152}
153
154impl From<Copilot> for CloudProvider {
155    fn from(client: Copilot) -> Self {
156        Self::Copilot(client)
157    }
158}
159
160impl From<Codex> for CloudProvider {
161    fn from(client: Codex) -> Self {
162        Self::Codex(client)
163    }
164}
165
166impl std::fmt::Debug for CloudProvider {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::OpenAI(_) => f.debug_tuple("CloudProvider::OpenAI").finish(),
170            Self::Claude(_) => f.debug_tuple("CloudProvider::Claude").finish(),
171            Self::Gemini(_) => f.debug_tuple("CloudProvider::Gemini").finish(),
172            Self::Copilot(_) => f.debug_tuple("CloudProvider::Copilot").finish(),
173            Self::Codex(_) => f.debug_tuple("CloudProvider::Codex").finish(),
174        }
175    }
176}
177
178/// Error type for cloud provider operations.
179#[derive(Debug, thiserror::Error)]
180pub enum CloudError {
181    /// `OpenAI` API error.
182    #[error("OpenAI error: {0}")]
183    OpenAI(#[from] aither_openai::OpenAIError),
184    /// Claude API error.
185    #[error("Claude error: {0}")]
186    Claude(#[from] aither_claude::ClaudeError),
187    /// Gemini API error.
188    #[error("Gemini error: {0}")]
189    Gemini(#[from] aither_gemini::GeminiError),
190    /// GitHub Copilot API error.
191    #[error("Copilot error: {0}")]
192    Copilot(#[from] aither_copilot::CopilotError),
193    /// `ChatGPT` Codex API error.
194    #[error("Codex error: {0}")]
195    Codex(#[from] aither_codex::CodexError),
196}
197
198impl Classify for CloudError {
199    fn failure(&self) -> aither_core::llm::Failure {
200        match self {
201            Self::OpenAI(error) => error.failure(),
202            Self::Claude(error) => error.failure(),
203            Self::Gemini(error) => error.failure(),
204            Self::Copilot(error) => error.failure(),
205            Self::Codex(error) => error.failure(),
206        }
207    }
208}
209
210impl LanguageModel for CloudProvider {
211    type Error = CloudError;
212
213    fn respond(
214        &self,
215        request: LLMRequest,
216    ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
217        let provider = match self {
218            Self::OpenAI(inner) => ProviderInner::OpenAI(inner.clone()),
219            Self::Claude(inner) => ProviderInner::Claude(inner.clone()),
220            Self::Gemini(inner) => ProviderInner::Gemini(inner.clone()),
221            Self::Copilot(inner) => ProviderInner::Copilot(inner.clone()),
222            Self::Codex(inner) => ProviderInner::Codex(inner.clone()),
223        };
224
225        async_stream::stream! {
226            match provider {
227                ProviderInner::OpenAI(inner) => {
228                    let mut stream = std::pin::pin!(inner.respond(request));
229                    while let Some(result) = stream.next().await {
230                        yield result.map_err(CloudError::from);
231                    }
232                }
233                ProviderInner::Claude(inner) => {
234                    let mut stream = std::pin::pin!(inner.respond(request));
235                    while let Some(result) = stream.next().await {
236                        yield result.map_err(CloudError::from);
237                    }
238                }
239                ProviderInner::Gemini(inner) => {
240                    let mut stream = std::pin::pin!(inner.respond(request));
241                    while let Some(result) = stream.next().await {
242                        yield result.map_err(CloudError::from);
243                    }
244                }
245                ProviderInner::Copilot(inner) => {
246                    let mut stream = std::pin::pin!(inner.respond(request));
247                    while let Some(result) = stream.next().await {
248                        yield result.map_err(CloudError::from);
249                    }
250                }
251                ProviderInner::Codex(inner) => {
252                    let mut stream = std::pin::pin!(inner.respond(request));
253                    while let Some(result) = stream.next().await {
254                        yield result.map_err(CloudError::from);
255                    }
256                }
257            }
258        }
259    }
260
261    fn retry_policy(&self) -> RetryPolicy {
262        match self {
263            Self::OpenAI(inner) => inner.retry_policy(),
264            Self::Claude(inner) => inner.retry_policy(),
265            Self::Gemini(inner) => inner.retry_policy(),
266            Self::Copilot(inner) => inner.retry_policy(),
267            Self::Codex(inner) => inner.retry_policy(),
268        }
269    }
270
271    async fn profile(&self) -> Result<Profile, Self::Error> {
272        match self {
273            Self::OpenAI(inner) => inner.profile().await.map_err(CloudError::from),
274            Self::Claude(inner) => inner.profile().await.map_err(CloudError::from),
275            Self::Gemini(inner) => inner.profile().await.map_err(CloudError::from),
276            Self::Copilot(inner) => inner.profile().await.map_err(CloudError::from),
277            Self::Codex(inner) => inner.profile().await.map_err(CloudError::from),
278        }
279    }
280}
281
282/// Internal enum to allow cloning the provider for async block.
283enum ProviderInner {
284    OpenAI(OpenAI),
285    Claude(Claude),
286    Gemini(Gemini),
287    Copilot(Copilot),
288    Codex(Codex),
289}
290
291/// Unified model provider wrapping `OpenAI`, Claude, Gemini, and Copilot providers.
292///
293/// Implements `LanguageModelProvider` to allow unified model listing and instantiation.
294#[derive(Clone, Debug)]
295pub enum CloudModelProvider {
296    /// `OpenAI` provider.
297    OpenAI(OpenAIProvider),
298    /// Anthropic Claude provider.
299    Claude(ClaudeProvider),
300    /// Google Gemini provider.
301    Gemini(GeminiProvider),
302    /// GitHub Copilot provider.
303    Copilot(CopilotProvider),
304    /// `ChatGPT` Codex provider.
305    Codex(CodexProvider),
306}
307
308impl From<OpenAIProvider> for CloudModelProvider {
309    fn from(provider: OpenAIProvider) -> Self {
310        Self::OpenAI(provider)
311    }
312}
313
314impl From<ClaudeProvider> for CloudModelProvider {
315    fn from(provider: ClaudeProvider) -> Self {
316        Self::Claude(provider)
317    }
318}
319
320impl From<GeminiProvider> for CloudModelProvider {
321    fn from(provider: GeminiProvider) -> Self {
322        Self::Gemini(provider)
323    }
324}
325
326impl From<CopilotProvider> for CloudModelProvider {
327    fn from(provider: CopilotProvider) -> Self {
328        Self::Copilot(provider)
329    }
330}
331
332impl From<CodexProvider> for CloudModelProvider {
333    fn from(provider: CodexProvider) -> Self {
334        Self::Codex(provider)
335    }
336}
337
338impl LanguageModelProvider for CloudModelProvider {
339    type Model = CloudProvider;
340    type Error = CloudError;
341
342    fn list_models(
343        &self,
344    ) -> impl std::future::Future<Output = Result<Vec<Profile>, Self::Error>> + Send {
345        let provider = self.clone();
346        async move {
347            match provider {
348                Self::OpenAI(p) => p.list_models().await.map_err(CloudError::from),
349                Self::Claude(p) => p.list_models().await.map_err(CloudError::from),
350                Self::Gemini(p) => p.list_models().await.map_err(CloudError::from),
351                Self::Copilot(p) => p.list_models().await.map_err(CloudError::from),
352                Self::Codex(p) => p.list_models().await.map_err(CloudError::from),
353            }
354        }
355    }
356
357    fn get_model(
358        &self,
359        name: &str,
360    ) -> impl std::future::Future<Output = Result<Self::Model, Self::Error>> + Send {
361        let provider = self.clone();
362        let name = name.to_string();
363        async move {
364            match provider {
365                Self::OpenAI(p) => p
366                    .get_model(&name)
367                    .await
368                    .map(CloudProvider::from)
369                    .map_err(CloudError::from),
370                Self::Claude(p) => p
371                    .get_model(&name)
372                    .await
373                    .map(CloudProvider::from)
374                    .map_err(CloudError::from),
375                Self::Gemini(p) => p
376                    .get_model(&name)
377                    .await
378                    .map(CloudProvider::from)
379                    .map_err(CloudError::from),
380                Self::Copilot(p) => p
381                    .get_model(&name)
382                    .await
383                    .map(CloudProvider::from)
384                    .map_err(CloudError::from),
385                Self::Codex(p) => p
386                    .get_model(&name)
387                    .await
388                    .map(CloudProvider::from)
389                    .map_err(CloudError::from),
390            }
391        }
392    }
393
394    fn profile() -> ProviderProfile {
395        ProviderProfile::new("cloud", "Unified cloud provider")
396    }
397}