Skip to main content

phi_core/provider/
registry.rs

1//! Provider registry — maps ApiProtocol to StreamProvider implementations.
2/*
3ARCHITECTURE: ProviderRegistry — the "factory + router" for LLM providers
4
5The registry solves two problems:
6  1. Factory — owns one instance of each `StreamProvider` implementation
7  2. Router  — dispatches a stream() call to the right provider based on `ApiProtocol`
8
9Usage pattern:
10  let registry = ProviderRegistry::default(); // registers all 7 built-in providers
11  registry.stream(&model_config, stream_config, tx, cancel).await
12
13The caller never touches individual providers directly. It holds a `ProviderRegistry`
14and lets the registry pick the right backend. This makes it trivial to:
15  - Add a new provider: register it in `Default::default()`
16  - Use a custom provider: build a custom registry with `new()` + `register()`
17
18RUST QUIRK: `HashMap<ApiProtocol, Box<dyn StreamProvider>>` — heterogeneous collection
19
20The registry must hold DIFFERENT concrete types (AnthropicProvider, GoogleProvider, ...)
21behind a SINGLE common interface (`StreamProvider`). This is classic polymorphism.
22
23`Box<dyn StreamProvider>` is a "trait object" — a heap-allocated pointer to a value
24whose concrete type is erased. The `dyn` keyword means "dynamic dispatch" — method
25calls go through a vtable (a table of function pointers) at runtime.
26
27`ApiProtocol` is the key (an enum, implements Hash + Eq). For each protocol,
28there's exactly one `Box<dyn StreamProvider>` value.
29Python analogy:
30  registry: dict[ApiProtocol, StreamProvider] = {
31      ApiProtocol.ANTHROPIC: AnthropicProvider(),
32      ...
33  }
34*/
35
36use super::model::{ApiProtocol, ModelConfig};
37use super::traits::*;
38use crate::types::*;
39use std::collections::HashMap;
40use tokio::sync::mpsc;
41
42/// Registry of all available stream providers, keyed by API protocol.
43pub struct ProviderRegistry {
44    providers: HashMap<ApiProtocol, Box<dyn StreamProvider>>,
45}
46
47impl ProviderRegistry {
48    /// Create an empty registry (no providers registered).
49    /// Use `ProviderRegistry::default()` to get all built-in providers.
50    pub fn new() -> Self {
51        Self {
52            providers: HashMap::new(),
53        }
54    }
55
56    /// Register a provider for a given protocol.
57    /*
58    RUST QUIRK: `impl StreamProvider + 'static` — a generic bound with a lifetime constraint
59
60    `impl StreamProvider` means "any type that implements StreamProvider" — the
61    compiler generates a specific version of this method for each concrete type
62    passed in (monomorphization). No virtual dispatch here; `Box::new(provider)` then
63    erases the concrete type into `Box<dyn StreamProvider>`.
64
65    `+ 'static` means "the type must not contain any borrowed references that could
66    dangle." In practice: no `&'a str` fields. All the built-in providers (structs
67    with no fields, or fields that own their data) satisfy this naturally.
68
69    Why required? `Box<dyn StreamProvider>` is stored in `self.providers` which
70    may outlive the current stack frame. Rust requires `'static` to guarantee the
71    boxed value remains valid for as long as it's stored.
72
73    `self.providers.insert(protocol, Box::new(provider))` — boxes the value onto the
74    heap and inserts it. If a provider was already registered for this protocol,
75    `insert` overwrites it (and the old Box is dropped).
76    */
77    pub fn register(&mut self, protocol: ApiProtocol, provider: impl StreamProvider + 'static) {
78        self.providers.insert(protocol, Box::new(provider));
79    }
80
81    /// Get a reference to the provider for a given protocol, if registered.
82    /*
83    RUST QUIRK: `.map(|p| p.as_ref())` — `Box<dyn T>` → `&dyn T`
84
85    `self.providers.get(protocol)` returns `Option<&Box<dyn StreamProvider>>`.
86    We want to return `Option<&dyn StreamProvider>` (a reference to the trait object,
87    not a reference to the Box that contains it).
88
89    `p.as_ref()` on a `Box<T>` returns `&T` — it "peels off" the Box layer.
90    Here: `Box<dyn StreamProvider>.as_ref()` → `&dyn StreamProvider`.
91
92    Python analogy: just returning the value from the dict — no Box layer exists in Python.
93    */
94    pub fn get(&self, protocol: &ApiProtocol) -> Option<&dyn StreamProvider> {
95        self.providers.get(protocol).map(|p| p.as_ref())
96    }
97
98    /// Returns true if a provider is registered for the given protocol.
99    pub fn has(&self, protocol: &ApiProtocol) -> bool {
100        self.providers.contains_key(protocol)
101    }
102
103    /// List all protocols that have a registered provider.
104    /*
105    RUST QUIRK: `.keys().copied().collect()` — iterator chain on HashMap keys
106
107    `.keys()` — returns an iterator over `&ApiProtocol` (references to the keys)
108    `.copied()` — converts `&ApiProtocol` to `ApiProtocol` (valid because ApiProtocol
109                  implements Copy — it's a small enum with no heap data)
110    `.collect()` — consumes the iterator and builds a `Vec<ApiProtocol>`
111                   Rust infers the collection type from the return type `Vec<ApiProtocol>`
112
113    Python analogy: list(registry.keys())
114    */
115    pub fn protocols(&self) -> Vec<ApiProtocol> {
116        self.providers.keys().copied().collect()
117    }
118
119    /// Stream using the appropriate provider for the model's API protocol.
120    /*
121    ARCHITECTURE: The dispatch method — routes to the right backend
122
123    This is the primary entry point. It:
124      1. Looks up the provider by `model.api` (the ApiProtocol enum variant)
125      2. Returns `ProviderError::Other` if no provider is registered for that protocol
126      3. Delegates to `provider.stream()` — the actual HTTP+SSE call
127
128    `ok_or_else(|| ...)` converts `Option<&dyn StreamProvider>` → `Result<...>`:
129      `Some(provider)` → `Ok(provider)`
130      `None`           → `Err(ProviderError::Other("No provider registered for..."))`
131    The `?` then propagates the Err early.
132
133    `provider.stream(config, tx, cancel).await` — async call through the trait object.
134    The vtable dispatches to the concrete method (AnthropicProvider::stream, etc.)
135    at runtime. The `.await` suspends this task until the stream completes.
136    */
137    /*
138    DESIGN: Why `model` is separate from `config`
139      `model`  = ROUTING KEY — tells the registry WHICH provider to dispatch to (via model.api)
140      `config` = REQUEST PAYLOAD — forwarded unchanged to the selected provider
141    The registry itself never reads config; it just routes based on model.api, then passes
142    config through. Separating them makes the routing logic clear and config unopinionated.
143    */
144    pub async fn stream(
145        &self,
146        model: &ModelConfig, // ROUTER — model.api selects the provider; also carries base_url, headers
147        config: StreamConfig, // PAYLOAD — forwarded as-is to the selected provider
148        tx: mpsc::UnboundedSender<StreamEvent>, // OBSERVER — passed through to provider.stream()
149        cancel: tokio_util::sync::CancellationToken, // ABORT — passed through to provider.stream()
150    ) -> Result<Message, ProviderError> {
151        let provider = self.providers.get(&model.api).ok_or_else(|| {
152            ProviderError::Other(format!(
153                "No provider registered for protocol: {}",
154                model.api
155            ))
156        })?;
157
158        provider.stream(config, tx, cancel).await
159    }
160}
161
162impl Default for ProviderRegistry {
163    /// Create a registry with all 7 built-in providers registered.
164    /*
165    ARCHITECTURE: `Default` as a convenient "all batteries included" factory
166
167    `ProviderRegistry::default()` is the recommended way to get a working registry.
168    It registers every built-in provider. Custom apps that need to restrict which
169    providers are available can use `ProviderRegistry::new()` and register selectively.
170
171    RUST QUIRK: `use` inside a function — scoped imports
172    `use crate::provider::{ AnthropicProvider, ... }` inside the function body
173    is a scoped import. The names are only available within this block, which avoids
174    polluting the module's namespace with 7 provider names. Especially useful when
175    the providers are large dependencies only needed in one place.
176    */
177    fn default() -> Self {
178        use crate::provider::{
179            AnthropicProvider, AzureOpenAiProvider, BedrockProvider, GoogleProvider,
180            GoogleVertexProvider, OpenAiCompatProvider, OpenAiResponsesProvider,
181        };
182
183        let mut registry = Self::new();
184        registry.register(ApiProtocol::AnthropicMessages, AnthropicProvider);
185        registry.register(ApiProtocol::OpenAiCompletions, OpenAiCompatProvider);
186        registry.register(ApiProtocol::OpenAiResponses, OpenAiResponsesProvider);
187        registry.register(ApiProtocol::GoogleGenerativeAi, GoogleProvider);
188        registry.register(ApiProtocol::GoogleVertex, GoogleVertexProvider);
189        registry.register(ApiProtocol::BedrockConverseStream, BedrockProvider);
190        registry.register(ApiProtocol::AzureOpenAiResponses, AzureOpenAiProvider);
191
192        registry
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_default_registry_has_all_providers() {
202        let registry = ProviderRegistry::default();
203
204        assert!(registry.has(&ApiProtocol::AnthropicMessages));
205        assert!(registry.has(&ApiProtocol::OpenAiCompletions));
206        assert!(registry.has(&ApiProtocol::OpenAiResponses));
207        assert!(registry.has(&ApiProtocol::GoogleGenerativeAi));
208        assert!(registry.has(&ApiProtocol::GoogleVertex));
209        assert!(registry.has(&ApiProtocol::BedrockConverseStream));
210        assert!(registry.has(&ApiProtocol::AzureOpenAiResponses));
211    }
212
213    #[test]
214    fn test_registry_protocols() {
215        let registry = ProviderRegistry::default();
216        let protocols = registry.protocols();
217        assert_eq!(protocols.len(), 7);
218    }
219
220    #[test]
221    fn test_custom_registry() {
222        let mut registry = ProviderRegistry::new();
223        assert!(!registry.has(&ApiProtocol::AnthropicMessages));
224
225        registry.register(
226            ApiProtocol::AnthropicMessages,
227            crate::provider::AnthropicProvider,
228        );
229        assert!(registry.has(&ApiProtocol::AnthropicMessages));
230    }
231}