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