Skip to main content

oxicode_ai/providers/
mod.rs

1//! Provider abstraction layer
2
3use std::sync::OnceLock;
4
5mod anthropic;
6mod azure;
7mod bedrock;
8#[cfg(feature = "protobuf")]
9mod cursor;
10#[cfg(feature = "protobuf")]
11mod devin;
12mod event;
13mod gemini_cli;
14mod gitlab_duo;
15mod gitlab_duo_agent;
16mod google;
17mod google_shared;
18pub mod model_fetch;
19mod ollama;
20mod openai;
21mod openai_responses;
22pub mod openai_responses_shared;
23mod options;
24pub mod register_builtins;
25mod sse;
26
27#[allow(unused_imports)]
28pub use register_builtins::AuthMethod;
29#[allow(unused_imports)]
30pub use register_builtins::create_builtin_provider_with_options;
31mod trait_def;
32mod vertex;
33
34use futures::Stream;
35use std::pin::Pin;
36
37#[allow(unused_imports)]
38pub use crate::Api;
39pub use crate::CacheRetention;
40pub use crate::Context;
41pub use crate::Model;
42#[allow(unused_imports)]
43pub use crate::ThinkingLevel;
44use crate::error::ProviderError;
45#[allow(unused_imports)]
46pub use anthropic::AnthropicProvider;
47#[allow(unused_imports)]
48pub use azure::AzureProvider;
49#[allow(unused_imports)]
50pub use bedrock::BedrockProvider;
51pub use event::ProviderEvent;
52#[allow(unused_imports)]
53pub use gemini_cli::GeminiCliProvider;
54pub use google::GoogleProvider;
55#[allow(unused_imports)]
56pub use ollama::OllamaProvider;
57#[allow(unused_imports)]
58pub use openai::OpenAiProvider;
59pub use openai::normalize_messages;
60#[allow(unused_imports)]
61pub use openai_responses::OpenAiResponsesProvider;
62#[allow(unused_imports)]
63pub use options::{ProviderOptions, StreamOptions, ThinkingBudgets};
64pub use trait_def::{Provider, StreamResult};
65#[allow(unused_imports)]
66pub use vertex::VertexProvider;
67
68use parking_lot::RwLock;
69use std::collections::HashMap;
70use std::sync::Arc;
71use std::sync::LazyLock;
72
73/// Default HTTP client timeout for LLM provider streams.
74///
75/// Long enough for multi-minute reasoning responses, short enough that a
76/// stalled upstream (TLS handshake hang, dead proxy, etc.) surfaces to the
77/// caller within a usable window. Connect failures fail fast (10s) — the
78/// full timeout covers the entire request body including stream read.
79const DEFAULT_PROVIDER_TIMEOUT_SECS: u64 = 600;
80const DEFAULT_PROVIDER_CONNECT_TIMEOUT_SECS: u64 = 10;
81
82/// Shared client singleton.
83///
84/// All built-in providers (`AnthropicProvider`, `OpenAiProvider`,
85/// `GoogleProvider`, `GeminiCliProvider`, etc.) construct their
86/// accessor. A bare `reqwest::Client::new()` would have **no timeout**,
87/// which means a stalled TCP/TLS handshake or a silent upstream hang
88/// would freeze the CLI indefinitely. With `panic = "abort"` in the
89/// release profile the user has to kill the process to recover.
90///
91/// `oxicode-cli/src/util/http_client.rs::shared_http_client` follows the same
92/// pattern (30s timeout) for non-LLM HTTP. Keep these two in sync.
93pub fn shared_client() -> &'static reqwest::Client {
94    static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
95    CLIENT.get_or_init(|| {
96        // SAFETY: `reqwest::Client::builder()` with only connect/timeout settings
97        // cannot fail to build — no TLS backend misconfiguration or invalid
98        // proxy is involved. Infallible by construction.
99        #[allow(clippy::expect_used)]
100        reqwest::Client::builder()
101            .connect_timeout(std::time::Duration::from_secs(
102                DEFAULT_PROVIDER_CONNECT_TIMEOUT_SECS,
103            ))
104            .timeout(std::time::Duration::from_secs(
105                DEFAULT_PROVIDER_TIMEOUT_SECS,
106            ))
107            .build()
108            .expect("provider shared_client: reqwest builder should not fail")
109    })
110}
111
112// ── Instance-based provider registry ───────────────────────────────
113
114/// Type alias for the provider factory closure stored in [`ProviderRegistry`].
115pub type ProviderFactory = Box<dyn Fn() -> anyhow::Result<Arc<dyn Provider>> + Send + Sync>;
116
117/// Runtime registry for providers (custom + built-in resolution).
118///
119/// This is an instance-based alternative to the global `CUSTOM_PROVIDERS` static.
120/// It supports `register()`, `get()`, `remove()`, and `names()`, falling back
121/// to built-in providers from the built-in provider factory when a name isn't found locally.
122///
123/// Providers can also be registered as **factories** via [`Self::register_factory`].
124/// A factory is a closure that lazily creates the provider on first access. The
125/// result is cached, so the factory runs at most once per name.
126pub struct ProviderRegistry {
127    custom: RwLock<HashMap<String, Arc<dyn Provider>>>,
128    factories: RwLock<HashMap<String, ProviderFactory>>,
129}
130
131impl Default for ProviderRegistry {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl ProviderRegistry {
138    /// Create a new empty registry.
139    pub fn new() -> Self {
140        Self {
141            custom: RwLock::new(HashMap::new()),
142            factories: RwLock::new(HashMap::new()),
143        }
144    }
145
146    /// Register a custom provider.
147    pub fn register(&self, name: &str, provider: impl Provider + 'static) {
148        self.custom
149            .write()
150            .insert(name.to_string(), Arc::new(provider));
151    }
152
153    /// Register a pre-boxed provider.
154    pub fn register_arc(&self, name: &str, provider: Arc<dyn Provider>) {
155        self.custom.write().insert(name.to_string(), provider);
156    }
157
158    /// Remove a previously registered custom provider.
159    pub fn remove(&self, name: &str) {
160        self.custom.write().remove(name);
161    }
162
163    /// Return the set of currently registered custom provider names.
164    pub fn names(&self) -> Vec<String> {
165        self.custom.read().keys().cloned().collect()
166    }
167
168    /// Get a provider by name.
169    ///
170    /// Checks local custom providers first, then falls back to built-in providers.
171    pub fn get(&self, name: &str) -> Option<Arc<dyn Provider>> {
172        // 1. Check local custom providers
173        {
174            let guard = self.custom.read();
175            if let Some(provider) = guard.get(name) {
176                return Some(Arc::clone(provider));
177            }
178        }
179
180        // 2. Fall back to built-in providers
181        get_provider(name).map(Arc::from)
182    }
183
184    /// Get a provider by name, checking only custom providers (no built-in fallback).
185    ///
186    /// If the provider was registered via [`Self::register_factory`] and hasn't
187    /// been materialized yet, the factory is invoked and the result cached.
188    pub fn get_custom(&self, name: &str) -> Option<Arc<dyn Provider>> {
189        {
190            let guard = self.custom.read();
191            if let Some(provider) = guard.get(name) {
192                return Some(Arc::clone(provider));
193            }
194        }
195        // Try materializing from factory
196        self.materialize_factory(name)
197    }
198
199    /// Register a factory closure that lazily creates a provider on first access.
200    ///
201    /// When [`Self::get_custom`] or [`Self::get`] is called and the provider is
202    /// not yet in `custom`, the factory is invoked, the result is cached in
203    /// `custom`, and the factory entry is removed.
204    ///
205    /// # Example
206    ///
207    /// ```ignore
208    /// registry.register_factory("my_provider", || {
209    ///     let key = resolve_api_key("my_provider");
210    ///     Ok(Arc::new(MyProvider::new(key)))
211    /// });
212    /// ```
213    pub fn register_factory(
214        &self,
215        name: &str,
216        factory: impl Fn() -> anyhow::Result<Arc<dyn Provider>> + Send + Sync + 'static,
217    ) {
218        self.factories
219            .write()
220            .insert(name.to_string(), Box::new(factory));
221    }
222
223    /// Invoke a registered factory (if any) for the given name.
224    ///
225    /// On success the resulting provider is cached in `custom` and the factory
226    /// entry is removed. Returns `None` if no factory is registered.
227    fn materialize_factory(&self, name: &str) -> Option<Arc<dyn Provider>> {
228        let factory = {
229            let mut factories = self.factories.write();
230            factories.remove(name)?
231        };
232        match factory() {
233            Ok(provider) => {
234                self.custom
235                    .write()
236                    .insert(name.to_string(), Arc::clone(&provider));
237                Some(provider)
238            }
239            Err(e) => {
240                tracing::warn!(provider = name, error = %e, "Provider factory failed");
241                None
242            }
243        }
244    }
245}
246
247// ── Global custom provider registry (legacy) ───────────────────────
248
249/// Global custom provider registry (for backward compatibility with CLI).
250///
251/// Custom providers registered via [`register_provider`] are stored here
252/// and take priority over built-in providers in [`get_provider`].
253static CUSTOM_PROVIDERS: LazyLock<RwLock<HashMap<String, Arc<dyn Provider>>>> =
254    LazyLock::new(|| RwLock::new(HashMap::new()));
255
256/// Register a custom provider at runtime (global registry).
257///
258/// This is called from `oxicode-cli` during startup for each `[[custom_provider]]` entry
259/// found in settings.
260pub fn register_provider(name: &str, provider: impl Provider + 'static) {
261    CUSTOM_PROVIDERS
262        .write()
263        .insert(name.to_string(), Arc::new(provider));
264}
265
266/// Unregister a previously registered custom provider (global registry).
267pub fn unregister_provider(name: &str) {
268    CUSTOM_PROVIDERS.write().remove(name);
269}
270
271/// Return the set of currently registered custom provider names (global registry).
272pub fn custom_provider_names() -> Vec<String> {
273    CUSTOM_PROVIDERS.read().keys().cloned().collect()
274}
275
276/// Get a provider by name
277///
278/// Checks custom providers first (global registry), then falls back to the
279/// data-driven built-in provider factory.
280pub fn get_provider(name: &str) -> Option<Box<dyn Provider>> {
281    // 1. Check custom providers first (higher priority than builtins)
282    {
283        let custom = CUSTOM_PROVIDERS.read();
284        if let Some(provider) = custom.get(name) {
285            return Some(Box::new(ArcedProvider(provider.clone())));
286        }
287    }
288
289    // 2. Fall back to built-in provider factory (data-driven from BuiltinProvider metadata)
290    register_builtins::create_builtin_provider(name)
291}
292
293/// Get a provider by name, returning Arc (for router delegation).
294pub fn get_provider_arc(name: &str) -> Option<Arc<dyn Provider>> {
295    {
296        let custom = CUSTOM_PROVIDERS.read();
297        if let Some(provider) = custom.get(name) {
298            return Some(Arc::clone(provider));
299        }
300    }
301    register_builtins::create_builtin_provider(name).map(Arc::from)
302}
303
304/// Wrapper that lets us return a cloned `Arc<dyn Provider>` as `Box<dyn Provider>`.
305struct ArcedProvider(Arc<dyn Provider>);
306
307impl Provider for ArcedProvider {
308    fn stream<'a>(
309        &'a self,
310        model: &'a Model,
311        context: &'a Context,
312        options: Option<StreamOptions>,
313    ) -> Pin<Box<dyn Future<Output = StreamResult> + Send + 'a>> {
314        Box::pin(async move { self.0.stream(model, context, options).await })
315    }
316}
317
318/// Create a stream for a model using the appropriate provider
319pub async fn stream(
320    model: &Model,
321    context: &Context,
322    options: Option<StreamOptions>,
323) -> Result<Pin<Box<dyn Stream<Item = ProviderEvent> + Send>>, ProviderError> {
324    let provider = get_provider(&model.provider)
325        .ok_or_else(|| ProviderError::UnknownProvider(model.provider.clone()))?;
326
327    provider.stream(model, context, options).await
328}