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        Event, LLMRequest, LanguageModelProvider, 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 LanguageModel for CloudProvider {
199    type Error = CloudError;
200
201    fn respond(
202        &self,
203        request: LLMRequest,
204    ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
205        let provider = match self {
206            Self::OpenAI(inner) => ProviderInner::OpenAI(inner.clone()),
207            Self::Claude(inner) => ProviderInner::Claude(inner.clone()),
208            Self::Gemini(inner) => ProviderInner::Gemini(inner.clone()),
209            Self::Copilot(inner) => ProviderInner::Copilot(inner.clone()),
210            Self::Codex(inner) => ProviderInner::Codex(inner.clone()),
211        };
212
213        async_stream::stream! {
214            match provider {
215                ProviderInner::OpenAI(inner) => {
216                    let mut stream = std::pin::pin!(inner.respond(request));
217                    while let Some(result) = stream.next().await {
218                        yield result.map_err(CloudError::from);
219                    }
220                }
221                ProviderInner::Claude(inner) => {
222                    let mut stream = std::pin::pin!(inner.respond(request));
223                    while let Some(result) = stream.next().await {
224                        yield result.map_err(CloudError::from);
225                    }
226                }
227                ProviderInner::Gemini(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::Copilot(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::Codex(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            }
246        }
247    }
248
249    async fn profile(&self) -> Profile {
250        match self {
251            Self::OpenAI(inner) => inner.profile().await,
252            Self::Claude(inner) => inner.profile().await,
253            Self::Gemini(inner) => inner.profile().await,
254            Self::Copilot(inner) => inner.profile().await,
255            Self::Codex(inner) => inner.profile().await,
256        }
257    }
258}
259
260/// Internal enum to allow cloning the provider for async block.
261enum ProviderInner {
262    OpenAI(OpenAI),
263    Claude(Claude),
264    Gemini(Gemini),
265    Copilot(Copilot),
266    Codex(Codex),
267}
268
269/// Unified model provider wrapping `OpenAI`, Claude, Gemini, and Copilot providers.
270///
271/// Implements `LanguageModelProvider` to allow unified model listing and instantiation.
272#[derive(Clone, Debug)]
273pub enum CloudModelProvider {
274    /// `OpenAI` provider.
275    OpenAI(OpenAIProvider),
276    /// Anthropic Claude provider.
277    Claude(ClaudeProvider),
278    /// Google Gemini provider.
279    Gemini(GeminiProvider),
280    /// GitHub Copilot provider.
281    Copilot(CopilotProvider),
282    /// ChatGPT Codex provider.
283    Codex(CodexProvider),
284}
285
286impl From<OpenAIProvider> for CloudModelProvider {
287    fn from(provider: OpenAIProvider) -> Self {
288        Self::OpenAI(provider)
289    }
290}
291
292impl From<ClaudeProvider> for CloudModelProvider {
293    fn from(provider: ClaudeProvider) -> Self {
294        Self::Claude(provider)
295    }
296}
297
298impl From<GeminiProvider> for CloudModelProvider {
299    fn from(provider: GeminiProvider) -> Self {
300        Self::Gemini(provider)
301    }
302}
303
304impl From<CopilotProvider> for CloudModelProvider {
305    fn from(provider: CopilotProvider) -> Self {
306        Self::Copilot(provider)
307    }
308}
309
310impl From<CodexProvider> for CloudModelProvider {
311    fn from(provider: CodexProvider) -> Self {
312        Self::Codex(provider)
313    }
314}
315
316impl LanguageModelProvider for CloudModelProvider {
317    type Model = CloudProvider;
318    type Error = CloudError;
319
320    fn list_models(
321        &self,
322    ) -> impl std::future::Future<Output = Result<Vec<Profile>, Self::Error>> + Send {
323        let provider = self.clone();
324        async move {
325            match provider {
326                Self::OpenAI(p) => p.list_models().await.map_err(CloudError::from),
327                Self::Claude(p) => p.list_models().await.map_err(CloudError::from),
328                Self::Gemini(p) => p.list_models().await.map_err(CloudError::from),
329                Self::Copilot(p) => p.list_models().await.map_err(CloudError::from),
330                Self::Codex(p) => p.list_models().await.map_err(CloudError::from),
331            }
332        }
333    }
334
335    fn get_model(
336        &self,
337        name: &str,
338    ) -> impl std::future::Future<Output = Result<Self::Model, Self::Error>> + Send {
339        let provider = self.clone();
340        let name = name.to_string();
341        async move {
342            match provider {
343                Self::OpenAI(p) => p
344                    .get_model(&name)
345                    .await
346                    .map(CloudProvider::from)
347                    .map_err(CloudError::from),
348                Self::Claude(p) => p
349                    .get_model(&name)
350                    .await
351                    .map(CloudProvider::from)
352                    .map_err(CloudError::from),
353                Self::Gemini(p) => p
354                    .get_model(&name)
355                    .await
356                    .map(CloudProvider::from)
357                    .map_err(CloudError::from),
358                Self::Copilot(p) => p
359                    .get_model(&name)
360                    .await
361                    .map(CloudProvider::from)
362                    .map_err(CloudError::from),
363                Self::Codex(p) => p
364                    .get_model(&name)
365                    .await
366                    .map(CloudProvider::from)
367                    .map_err(CloudError::from),
368            }
369        }
370    }
371
372    fn profile() -> ProviderProfile {
373        ProviderProfile::new("cloud", "Unified cloud provider")
374    }
375}