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}