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