Skip to main content

context_harness/agent_model/
catalog.rs

1//! Trusted, compiled model-provider registration and construction.
2//!
3//! Configuration selects only factories already registered by the host. Static
4//! validation never constructs a provider or resolves credentials.
5
6use super::{fake, ollama, openai, ModelProvider, ModelResponse};
7use crate::agent_resource::ModelDefinition;
8use anyhow::{ensure, Context};
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12/// Stable provider implementation metadata for compatibility decisions.
13///
14/// The identifier names the adapter implementation, not a configured provider
15/// alias or model. The version changes when adapter behavior becomes
16/// checkpoint-incompatible. Neither value may contain credentials.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ModelProviderImplementation {
19    id: String,
20    version: String,
21}
22
23impl ModelProviderImplementation {
24    pub fn new(id: impl Into<String>, version: impl Into<String>) -> Self {
25        Self {
26            id: id.into(),
27            version: version.into(),
28        }
29    }
30
31    pub fn id(&self) -> &str {
32        &self.id
33    }
34
35    pub fn version(&self) -> &str {
36        &self.version
37    }
38
39    fn validate(&self) -> anyhow::Result<()> {
40        ensure!(
41            !self.id.trim().is_empty() && !self.version.trim().is_empty(),
42            "model provider implementation identity is required"
43        );
44        Ok(())
45    }
46}
47
48/// A compiled, trusted provider factory registered explicitly by the host.
49pub trait ModelProviderFactory: Send + Sync {
50    /// Configuration name used by `ModelDefinition::provider`.
51    fn provider_name(&self) -> &str;
52
53    /// Stable, non-secret adapter identity and compatibility version.
54    fn implementation(&self) -> ModelProviderImplementation;
55
56    /// Validate provider-specific configuration without side effects.
57    fn validate(&self, definition: &ModelDefinition) -> anyhow::Result<()>;
58
59    /// Construct the provider for runtime execution.
60    fn build(&self, definition: &ModelDefinition) -> anyhow::Result<Arc<dyn ModelProvider>>;
61}
62
63#[derive(Clone)]
64struct RegisteredFactory {
65    implementation: ModelProviderImplementation,
66    factory: Arc<dyn ModelProviderFactory>,
67}
68
69/// Catalog of trusted model-provider factories.
70///
71/// Provider names are case-sensitive and first-registration wins: registering
72/// an existing name returns an error and leaves the catalog unchanged.
73#[derive(Clone, Default)]
74pub struct ModelProviderCatalog {
75    factories: BTreeMap<String, RegisteredFactory>,
76}
77
78impl ModelProviderCatalog {
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    pub fn with_builtins() -> anyhow::Result<Self> {
84        let mut catalog = Self::new();
85        catalog.register(Arc::new(OpenAiFactory))?;
86        catalog.register(Arc::new(FakeFactory))?;
87        catalog.register(Arc::new(OllamaFactory))?;
88        Ok(catalog)
89    }
90
91    pub fn register(&mut self, factory: Arc<dyn ModelProviderFactory>) -> anyhow::Result<()> {
92        let name = factory.provider_name();
93        ensure!(!name.trim().is_empty(), "model provider name is required");
94        ensure!(
95            !self.factories.contains_key(name),
96            "model provider '{name}' is already registered"
97        );
98        let implementation = factory.implementation();
99        implementation.validate()?;
100        self.factories.insert(
101            name.to_owned(),
102            RegisteredFactory {
103                implementation,
104                factory,
105            },
106        );
107        Ok(())
108    }
109
110    pub fn implementation(
111        &self,
112        provider_name: &str,
113    ) -> anyhow::Result<&ModelProviderImplementation> {
114        Ok(&self
115            .factories
116            .get(provider_name)
117            .with_context(|| format!("unsupported model provider '{provider_name}'"))?
118            .implementation)
119    }
120
121    /// Validate definitions without constructing providers or resolving secrets.
122    pub fn validate_config(
123        &self,
124        models: &BTreeMap<String, ModelDefinition>,
125    ) -> anyhow::Result<()> {
126        for (alias, definition) in models {
127            self.validate_definition(alias, definition)?;
128        }
129        Ok(())
130    }
131
132    pub(crate) fn validate_definition(
133        &self,
134        alias: &str,
135        definition: &ModelDefinition,
136    ) -> anyhow::Result<()> {
137        definition
138            .validate()
139            .with_context(|| format!("model '{alias}'"))?;
140        let registered = self.factories.get(&definition.provider).with_context(|| {
141            format!(
142                "unsupported model provider '{}' for '{alias}'",
143                definition.provider
144            )
145        })?;
146        registered
147            .factory
148            .validate(definition)
149            .with_context(|| format!("model '{alias}'"))
150    }
151
152    pub(crate) fn build(
153        &self,
154        alias: &str,
155        definition: &ModelDefinition,
156    ) -> anyhow::Result<(Arc<dyn ModelProvider>, ModelProviderImplementation)> {
157        let registered = self.factories.get(&definition.provider).with_context(|| {
158            format!(
159                "unsupported model provider '{}' for '{alias}'",
160                definition.provider
161            )
162        })?;
163        let provider = registered
164            .factory
165            .build(definition)
166            .with_context(|| format!("model '{alias}'"))?;
167        Ok((provider, registered.implementation.clone()))
168    }
169}
170
171struct OpenAiFactory;
172
173impl ModelProviderFactory for OpenAiFactory {
174    fn provider_name(&self) -> &str {
175        "openai"
176    }
177
178    fn implementation(&self) -> ModelProviderImplementation {
179        ModelProviderImplementation::new("context-harness.openai-responses", "1")
180    }
181
182    fn validate(&self, definition: &ModelDefinition) -> anyhow::Result<()> {
183        ensure!(
184            definition.base_url.is_none() && definition.timeout_seconds.is_none(),
185            "base_url and timeout_seconds are only supported by the ollama provider"
186        );
187        Ok(())
188    }
189
190    fn build(&self, definition: &ModelDefinition) -> anyhow::Result<Arc<dyn ModelProvider>> {
191        Ok(Arc::new(openai::OpenAiProvider::new(
192            &definition.model,
193            definition
194                .api_key_env
195                .as_deref()
196                .unwrap_or("OPENAI_API_KEY"),
197        )?))
198    }
199}
200
201struct FakeFactory;
202
203impl ModelProviderFactory for FakeFactory {
204    fn provider_name(&self) -> &str {
205        "fake"
206    }
207
208    fn implementation(&self) -> ModelProviderImplementation {
209        ModelProviderImplementation::new("context-harness.fake", "1")
210    }
211
212    fn validate(&self, definition: &ModelDefinition) -> anyhow::Result<()> {
213        ensure!(
214            definition.base_url.is_none() && definition.timeout_seconds.is_none(),
215            "base_url and timeout_seconds are only supported by the ollama provider"
216        );
217        Ok(())
218    }
219
220    fn build(&self, _definition: &ModelDefinition) -> anyhow::Result<Arc<dyn ModelProvider>> {
221        Ok(Arc::new(fake::FakeModel::new([Ok(ModelResponse::text(
222            "Synthetic response from FakeModel; no model service was called.",
223        ))])))
224    }
225}
226
227struct OllamaFactory;
228
229impl ModelProviderFactory for OllamaFactory {
230    fn provider_name(&self) -> &str {
231        "ollama"
232    }
233
234    fn implementation(&self) -> ModelProviderImplementation {
235        ModelProviderImplementation::new("context-harness.ollama-chat", "1")
236    }
237
238    fn validate(&self, definition: &ModelDefinition) -> anyhow::Result<()> {
239        ollama::validate_definition(definition)
240    }
241
242    fn build(&self, definition: &ModelDefinition) -> anyhow::Result<Arc<dyn ModelProvider>> {
243        Ok(Arc::new(ollama::OllamaProvider::from_definition(
244            definition,
245        )?))
246    }
247}