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