context_harness/agent_model/
catalog.rs1use 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#[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
48pub trait ModelProviderFactory: Send + Sync {
50 fn provider_name(&self) -> &str;
52
53 fn implementation(&self) -> ModelProviderImplementation;
55
56 fn validate(&self, definition: &ModelDefinition) -> anyhow::Result<()>;
58
59 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#[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 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}