Skip to main content

codineer_api/
client.rs

1use crate::error::ApiError;
2use crate::providers::codineer_provider::{self, AuthSource, CodineerApiClient};
3use crate::providers::openai_compat::{self, OpenAiCompatClient, OpenAiCompatConfig};
4use crate::providers::{self, ProviderKind};
5use crate::types::{MessageRequest, MessageResponse, StreamEvent};
6
7#[derive(Debug, Clone)]
8pub enum ProviderClient {
9    CodineerApi(CodineerApiClient),
10    Xai(OpenAiCompatClient),
11    OpenAi(OpenAiCompatClient),
12    Custom(OpenAiCompatClient),
13}
14
15impl ProviderClient {
16    pub fn from_model(model: &str) -> Result<Self, ApiError> {
17        Self::from_model_with_default_auth(model, None)
18    }
19
20    pub fn from_model_with_default_auth(
21        model: &str,
22        default_auth: Option<AuthSource>,
23    ) -> Result<Self, ApiError> {
24        match providers::detect_provider_kind(model) {
25            ProviderKind::CodineerApi => Ok(Self::CodineerApi(match default_auth {
26                Some(auth) => CodineerApiClient::from_auth(auth),
27                None => CodineerApiClient::from_env()?,
28            })),
29            ProviderKind::Xai => Ok(Self::Xai(OpenAiCompatClient::from_env(
30                OpenAiCompatConfig::xai(),
31            )?)),
32            ProviderKind::OpenAi => Ok(Self::OpenAi(OpenAiCompatClient::from_env(
33                OpenAiCompatConfig::openai(),
34            )?)),
35            ProviderKind::Custom => Err(ApiError::Auth(
36                "custom provider models must be resolved via from_custom()".to_string(),
37            )),
38        }
39    }
40
41    /// Build a provider client using a pre-resolved credential from a `CredentialChain`.
42    pub fn from_model_with_credential(
43        model: &str,
44        credential: runtime::ResolvedCredential,
45    ) -> Result<Self, ApiError> {
46        let auth = AuthSource::from(credential);
47        match providers::detect_provider_kind(model) {
48            ProviderKind::CodineerApi => Ok(Self::CodineerApi(
49                CodineerApiClient::from_auth(auth)
50                    .with_base_url(codineer_provider::read_base_url()),
51            )),
52            ProviderKind::Xai => {
53                let config = OpenAiCompatConfig::xai();
54                Ok(Self::Xai(
55                    OpenAiCompatClient::new(auth.api_key().unwrap_or_default(), config)
56                        .with_base_url(openai_compat::read_base_url(config)),
57                ))
58            }
59            ProviderKind::OpenAi => {
60                let config = OpenAiCompatConfig::openai();
61                Ok(Self::OpenAi(
62                    OpenAiCompatClient::new(auth.api_key().unwrap_or_default(), config)
63                        .with_base_url(openai_compat::read_base_url(config)),
64                ))
65            }
66            ProviderKind::Custom => Err(ApiError::Auth(
67                "custom provider models must be resolved via from_custom()".to_string(),
68            )),
69        }
70    }
71
72    /// Construct a `Custom` provider client from a pre-configured `OpenAiCompatClient`.
73    #[must_use]
74    pub fn from_custom(client: OpenAiCompatClient) -> Self {
75        Self::Custom(client)
76    }
77
78    #[must_use]
79    pub const fn provider_kind(&self) -> ProviderKind {
80        match self {
81            Self::CodineerApi(_) => ProviderKind::CodineerApi,
82            Self::Xai(_) => ProviderKind::Xai,
83            Self::OpenAi(_) => ProviderKind::OpenAi,
84            Self::Custom(_) => ProviderKind::Custom,
85        }
86    }
87
88    pub async fn send_message(
89        &self,
90        request: &MessageRequest,
91    ) -> Result<MessageResponse, ApiError> {
92        match self {
93            Self::CodineerApi(client) => client.send_message(request).await,
94            Self::Xai(client) | Self::OpenAi(client) | Self::Custom(client) => {
95                client.send_message(request).await
96            }
97        }
98    }
99
100    pub async fn stream_message(
101        &self,
102        request: &MessageRequest,
103    ) -> Result<MessageStream, ApiError> {
104        match self {
105            Self::CodineerApi(client) => client
106                .stream_message(request)
107                .await
108                .map(MessageStream::CodineerApi),
109            Self::Xai(client) | Self::OpenAi(client) | Self::Custom(client) => client
110                .stream_message(request)
111                .await
112                .map(MessageStream::OpenAiCompat),
113        }
114    }
115}
116
117#[derive(Debug)]
118pub enum MessageStream {
119    CodineerApi(codineer_provider::MessageStream),
120    OpenAiCompat(openai_compat::MessageStream),
121}
122
123impl MessageStream {
124    #[must_use]
125    pub fn request_id(&self) -> Option<&str> {
126        match self {
127            Self::CodineerApi(stream) => stream.request_id(),
128            Self::OpenAiCompat(stream) => stream.request_id(),
129        }
130    }
131
132    pub async fn next_event(&mut self) -> Result<Option<StreamEvent>, ApiError> {
133        match self {
134            Self::CodineerApi(stream) => stream.next_event().await,
135            Self::OpenAiCompat(stream) => stream.next_event().await,
136        }
137    }
138}
139
140pub use codineer_provider::{
141    oauth_token_is_expired, resolve_saved_oauth_token, resolve_startup_auth_source, OAuthTokenSet,
142};
143#[must_use]
144pub fn read_base_url() -> String {
145    codineer_provider::read_base_url()
146}
147
148#[must_use]
149pub fn read_xai_base_url() -> String {
150    openai_compat::read_base_url(OpenAiCompatConfig::xai())
151}
152
153#[cfg(test)]
154mod tests {
155    use crate::providers::{detect_provider_kind, resolve_model_alias, ProviderKind};
156    use std::collections::BTreeMap;
157
158    #[test]
159    fn resolves_user_aliases_via_api() {
160        let mut aliases = BTreeMap::new();
161        aliases.insert("opus".into(), "claude-opus-4-6".into());
162        aliases.insert("grok".into(), "grok-3".into());
163        assert_eq!(resolve_model_alias("opus", &aliases), "claude-opus-4-6");
164        assert_eq!(resolve_model_alias("grok", &aliases), "grok-3");
165    }
166
167    #[test]
168    fn passthrough_without_aliases() {
169        let empty = BTreeMap::new();
170        assert_eq!(resolve_model_alias("grok-3", &empty), "grok-3");
171        assert_eq!(
172            resolve_model_alias("unknown-model", &empty),
173            "unknown-model"
174        );
175    }
176
177    #[test]
178    fn provider_detection_prefers_model_family() {
179        assert_eq!(detect_provider_kind("grok-3"), ProviderKind::Xai);
180        assert_eq!(
181            detect_provider_kind("claude-sonnet-4-6"),
182            ProviderKind::CodineerApi
183        );
184    }
185}