Skip to main content

locode_core/
providers.rs

1//! Custom-provider injection (ADR-0015): a name → factory registry the binary
2//! entry points thread through to session assembly.
3//!
4//! `ProviderRegistry::builtin()` carries the built-in wires; a downstream
5//! binary crate registers its own [`Provider`] implementations under new names
6//! and selects them with `--api-schema <name>` — no fork of the CLI needed.
7
8use std::fmt;
9use std::sync::Arc;
10
11use locode_protocol::{ContentBlock, Usage};
12use locode_provider::{
13    AnthropicProvider, Completion, MockProvider, OpenAiResponsesProvider, Provider, StopReason,
14};
15
16/// Per-run inputs a factory may use when constructing its provider.
17pub struct ProviderInit {
18    /// The session id — e.g. for cache-routing hints (the `openai-responses`
19    /// wire sets it as `prompt_cache_key`, codex's rule).
20    pub session_id: String,
21}
22
23/// A constructed provider plus the model name it resolved (for the report
24/// envelope and engine config).
25pub struct BuiltProvider {
26    /// The wire, ready to drive.
27    pub provider: Arc<dyn Provider>,
28    /// The resolved model id (config/env/default — the factory decides).
29    pub model: String,
30}
31
32impl fmt::Debug for BuiltProvider {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.debug_struct("BuiltProvider")
35            .field("model", &self.model)
36            .finish_non_exhaustive() // the dyn provider has no Debug bound
37    }
38}
39
40/// A provider construction failure (missing env, bad config, unknown name):
41/// pre-run by nature — surfaced before any session starts.
42#[derive(Debug)]
43pub struct ProviderBuildError(pub String);
44
45impl fmt::Display for ProviderBuildError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(&self.0)
48    }
49}
50
51impl std::error::Error for ProviderBuildError {}
52
53/// A named provider constructor: env/config resolution belongs inside the
54/// factory so selection stays lazy — only the chosen wire pays its setup.
55pub type ProviderFactory =
56    Box<dyn Fn(&ProviderInit) -> Result<BuiltProvider, ProviderBuildError> + Send + Sync>;
57
58/// The `--api-schema` name space: ordered name → factory entries.
59///
60/// Ordered so help/error listings are stable; [`ProviderRegistry::register`]
61/// replaces in place when the name already exists (letting a downstream binary
62/// override a built-in, e.g. a scripted `mock`).
63pub struct ProviderRegistry {
64    entries: Vec<(String, ProviderFactory)>,
65}
66
67impl ProviderRegistry {
68    /// An empty registry (no names). Most callers want [`Self::builtin`].
69    #[must_use]
70    pub fn new() -> Self {
71        ProviderRegistry {
72            entries: Vec::new(),
73        }
74    }
75
76    /// The built-in wires: `anthropic`, `openai-responses`, and the keyless
77    /// `mock` (one scripted no-tool text turn → `completed`, for CI).
78    #[must_use]
79    pub fn builtin() -> Self {
80        Self::new()
81            .register("anthropic", |_init| {
82                let provider = AnthropicProvider::from_env()
83                    .map_err(|e| ProviderBuildError(format!("anthropic wire: {e}")))?;
84                let model = provider.config().model.clone();
85                Ok(BuiltProvider {
86                    provider: Arc::new(provider),
87                    model,
88                })
89            })
90            .register("openai-responses", |init| {
91                let mut provider = OpenAiResponsesProvider::from_env()
92                    .map_err(|e| ProviderBuildError(format!("openai-responses wire: {e}")))?;
93                // Cache-routing hint = the session id (codex's rule; probe-verified
94                // harmless for xAI models).
95                provider.config_mut().prompt_cache_key = Some(init.session_id.clone());
96                let model = provider.config().model.clone();
97                Ok(BuiltProvider {
98                    provider: Arc::new(provider),
99                    model,
100                })
101            })
102            .register("mock", |_init| {
103                let provider = MockProvider::new(vec![Completion {
104                    content: vec![ContentBlock::Text {
105                        text: "Mock run complete.".to_string(),
106                    }],
107                    usage: Usage::default(),
108                    stop: StopReason::EndTurn,
109                }]);
110                Ok(BuiltProvider {
111                    provider: Arc::new(provider),
112                    model: "mock-1".to_string(),
113                })
114            })
115    }
116
117    /// Add a provider under `name`, replacing any existing entry of that name
118    /// (registration order is otherwise preserved for listings).
119    #[must_use]
120    pub fn register<F>(mut self, name: impl Into<String>, factory: F) -> Self
121    where
122        F: Fn(&ProviderInit) -> Result<BuiltProvider, ProviderBuildError> + Send + Sync + 'static,
123    {
124        let name = name.into();
125        let factory: ProviderFactory = Box::new(factory);
126        match self.entries.iter_mut().find(|(n, _)| *n == name) {
127            Some(entry) => entry.1 = factory,
128            None => self.entries.push((name, factory)),
129        }
130        self
131    }
132
133    /// The registered names, in registration order.
134    #[must_use]
135    pub fn names(&self) -> Vec<&str> {
136        self.entries.iter().map(|(n, _)| n.as_str()).collect()
137    }
138
139    /// Construct the provider registered under `name`.
140    ///
141    /// # Errors
142    /// [`ProviderBuildError`] when `name` is unknown (the message lists the
143    /// available names) or when the factory itself fails (missing env, …).
144    pub fn build(
145        &self,
146        name: &str,
147        init: &ProviderInit,
148    ) -> Result<BuiltProvider, ProviderBuildError> {
149        let factory = self
150            .entries
151            .iter()
152            .find(|(n, _)| n == name)
153            .map(|(_, f)| f)
154            .ok_or_else(|| {
155                ProviderBuildError(format!(
156                    "unknown --api-schema `{name}`; available: {}",
157                    self.names().join(", ")
158                ))
159            })?;
160        factory(init)
161    }
162}
163
164impl Default for ProviderRegistry {
165    fn default() -> Self {
166        Self::builtin()
167    }
168}