1use std::fmt;
9use std::sync::Arc;
10
11use locode_protocol::{ContentBlock, Usage};
12use locode_provider::{
13 AnthropicProvider, Completion, MockProvider, OpenAiResponsesProvider, Provider, StopReason,
14};
15
16pub struct ProviderInit {
18 pub session_id: String,
21}
22
23pub struct BuiltProvider {
26 pub provider: Arc<dyn Provider>,
28 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() }
38}
39
40#[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
53pub type ProviderFactory =
56 Box<dyn Fn(&ProviderInit) -> Result<BuiltProvider, ProviderBuildError> + Send + Sync>;
57
58pub struct ProviderRegistry {
64 entries: Vec<(String, ProviderFactory)>,
65}
66
67impl ProviderRegistry {
68 #[must_use]
70 pub fn new() -> Self {
71 ProviderRegistry {
72 entries: Vec::new(),
73 }
74 }
75
76 #[must_use]
86 pub fn builtin() -> Self {
87 Self::new()
88 .register("anthropic", |_init| {
89 let provider = AnthropicProvider::from_env()
90 .map_err(|e| ProviderBuildError(format!("anthropic wire: {e}")))?;
91 let model = provider.config().model.clone();
92 Ok(BuiltProvider {
93 provider: Arc::new(provider),
94 model,
95 })
96 })
97 .register("openai-responses", |init| {
98 let mut provider = OpenAiResponsesProvider::from_env()
99 .map_err(|e| ProviderBuildError(format!("openai-responses wire: {e}")))?;
100 provider.config_mut().prompt_cache_key = Some(init.session_id.clone());
103 let model = provider.config().model.clone();
104 Ok(BuiltProvider {
105 provider: Arc::new(provider),
106 model,
107 })
108 })
109 .register("mock", |_init| {
110 let script = match std::env::var("LOCODE_MOCK_SCRIPT") {
111 Ok(json) => mock_script(&json)
112 .map_err(|e| ProviderBuildError(format!("LOCODE_MOCK_SCRIPT: {e}")))?,
113 Err(_) => vec![Completion {
114 content: vec![ContentBlock::Text {
115 text: "Mock run complete.".to_string(),
116 }],
117 usage: Usage::default(),
118 stop: StopReason::EndTurn,
119 }],
120 };
121 Ok(BuiltProvider {
122 provider: Arc::new(MockProvider::new(script)),
123 model: "mock-1".to_string(),
124 })
125 })
126 }
127
128 #[must_use]
131 pub fn register<F>(mut self, name: impl Into<String>, factory: F) -> Self
132 where
133 F: Fn(&ProviderInit) -> Result<BuiltProvider, ProviderBuildError> + Send + Sync + 'static,
134 {
135 let name = name.into();
136 let factory: ProviderFactory = Box::new(factory);
137 match self.entries.iter_mut().find(|(n, _)| *n == name) {
138 Some(entry) => entry.1 = factory,
139 None => self.entries.push((name, factory)),
140 }
141 self
142 }
143
144 #[must_use]
146 pub fn names(&self) -> Vec<&str> {
147 self.entries.iter().map(|(n, _)| n.as_str()).collect()
148 }
149
150 pub fn build(
156 &self,
157 name: &str,
158 init: &ProviderInit,
159 ) -> Result<BuiltProvider, ProviderBuildError> {
160 let factory = self
161 .entries
162 .iter()
163 .find(|(n, _)| n == name)
164 .map(|(_, f)| f)
165 .ok_or_else(|| {
166 ProviderBuildError(format!(
167 "unknown --api-schema `{name}`; available: {}",
168 self.names().join(", ")
169 ))
170 })?;
171 factory(init)
172 }
173}
174
175impl Default for ProviderRegistry {
176 fn default() -> Self {
177 Self::builtin()
178 }
179}
180
181fn mock_script(json: &str) -> Result<Vec<Completion>, String> {
187 let value: serde_json::Value =
188 serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?;
189 let turns = value
190 .as_array()
191 .ok_or_else(|| "expected a JSON array of turns".to_string())?;
192 if turns.is_empty() {
193 return Err("expected at least one turn".to_string());
194 }
195 turns
196 .iter()
197 .enumerate()
198 .map(|(i, turn)| {
199 if let Some(text) = turn.get("text").and_then(serde_json::Value::as_str) {
200 Ok(Completion {
201 content: vec![ContentBlock::Text {
202 text: text.to_string(),
203 }],
204 usage: Usage::default(),
205 stop: StopReason::EndTurn,
206 })
207 } else if let Some(tool) = turn.get("tool").and_then(serde_json::Value::as_str) {
208 Ok(Completion {
209 content: vec![ContentBlock::ToolUse {
210 id: format!("call_{i}"),
211 name: tool.to_string(),
212 input: turn
213 .get("input")
214 .cloned()
215 .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
216 }],
217 usage: Usage::default(),
218 stop: StopReason::ToolUse,
219 })
220 } else {
221 Err(format!(
222 "turn {i}: expected {{\"text\": …}} or {{\"tool\": …, \"input\": …}}"
223 ))
224 }
225 })
226 .collect()
227}