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 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 #[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}