1use std::collections::HashMap;
2
3use bon::Builder;
4use chimera_core::{
5 BackendCapabilities, CapabilitySupport, McpConfigMode, McpSupport, ResumeSemantics,
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12#[non_exhaustive]
13#[derive(Default)]
14pub enum OpenCodeProvider {
15 #[default]
18 Zen,
19 Go,
22 Direct { base_url: String },
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30#[non_exhaustive]
31pub enum OpenCodeExecutionMode {
32 #[default]
33 Http,
34 Agent,
35}
36
37impl OpenCodeProvider {
38 pub fn base_url(&self) -> &str {
39 match self {
40 Self::Zen => "https://opencode.ai/zen/v1",
41 Self::Go => "https://opencode.ai/zen/go/v1",
42 Self::Direct { base_url } => base_url.as_str(),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
51pub struct OpenCodeMcpServer {
52 #[builder(into)]
54 pub name: String,
55
56 #[builder(default)]
58 pub command: Vec<String>,
59
60 #[builder(default)]
62 pub env: HashMap<String, String>,
63}
64
65#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
66pub struct OpenCodeConfig {
67 #[builder(default)]
69 pub execution_mode: OpenCodeExecutionMode,
70
71 #[builder(into)]
73 pub api_key: Option<String>,
74
75 #[builder(default)]
77 pub provider: OpenCodeProvider,
78
79 pub timeout_secs: Option<u64>,
81
82 pub startup_timeout_secs: Option<u64>,
84
85 pub max_retries: Option<u32>,
87
88 pub max_tokens: Option<u32>,
90
91 pub temperature: Option<f32>,
93
94 #[builder(default = true)]
96 pub stream: bool,
97
98 #[builder(default)]
100 pub mcp_servers: Vec<OpenCodeMcpServer>,
101
102 #[builder(default)]
105 pub mcp_config_mode: McpConfigMode,
106
107 #[builder(into)]
109 pub agent_base_url: Option<String>,
110
111 #[builder(into)]
115 pub session_title: Option<String>,
116
117 #[builder(into)]
119 pub system_prompt: Option<String>,
120
121 #[builder(into)]
123 pub variant: Option<String>,
124}
125
126impl Default for OpenCodeConfig {
127 fn default() -> Self {
128 Self {
129 execution_mode: OpenCodeExecutionMode::Http,
130 api_key: None,
131 provider: OpenCodeProvider::default(),
132 timeout_secs: None,
133 startup_timeout_secs: None,
134 max_retries: None,
135 max_tokens: None,
136 temperature: None,
137 stream: true,
138 mcp_servers: Vec::new(),
139 mcp_config_mode: McpConfigMode::Merge,
140 agent_base_url: None,
141 session_title: None,
142 system_prompt: None,
143 variant: None,
144 }
145 }
146}
147
148impl OpenCodeConfig {
149 pub(crate) fn startup_timeout(&self) -> std::time::Duration {
150 std::time::Duration::from_secs(self.startup_timeout_secs.unwrap_or(30))
151 }
152
153 pub fn capabilities(&self) -> BackendCapabilities {
154 let agent_mode = self.execution_mode == OpenCodeExecutionMode::Agent;
155
156 BackendCapabilities {
157 input_images: CapabilitySupport::Unsupported,
158 output_schema: if agent_mode {
159 CapabilitySupport::Unsupported
160 } else {
161 CapabilitySupport::Supported
162 },
163 interrupt: if agent_mode {
164 CapabilitySupport::Unsupported
165 } else {
166 CapabilitySupport::Supported
167 },
168 resume: ResumeSemantics::Stateful,
169 mcp: McpSupport::MergeAndExplicitOnly,
170 tool_call_events: CapabilitySupport::Supported,
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn config_defaults() {
181 let config = OpenCodeConfig::default();
182 assert_eq!(config.execution_mode, OpenCodeExecutionMode::Http);
183 assert!(config.api_key.is_none());
184 assert!(matches!(config.provider, OpenCodeProvider::Zen));
185 assert!(config.timeout_secs.is_none());
186 assert!(config.startup_timeout_secs.is_none());
187 assert!(config.max_retries.is_none());
188 assert!(config.max_tokens.is_none());
189 assert!(config.temperature.is_none());
190 assert!(config.stream);
191 assert_eq!(config.mcp_config_mode, McpConfigMode::Merge);
192 assert!(config.agent_base_url.is_none());
193 assert!(config.session_title.is_none());
194 assert!(config.variant.is_none());
195 }
196
197 #[test]
198 fn config_builder() {
199 let config = OpenCodeConfig::builder()
200 .api_key("sk-test")
201 .provider(OpenCodeProvider::Direct {
202 base_url: "http://localhost:8080".into(),
203 })
204 .timeout_secs(60)
205 .startup_timeout_secs(90)
206 .max_retries(3)
207 .variant("medium")
208 .stream(false)
209 .build();
210
211 assert_eq!(config.api_key.as_deref(), Some("sk-test"));
212 assert_eq!(config.timeout_secs, Some(60));
213 assert_eq!(config.startup_timeout_secs, Some(90));
214 assert_eq!(config.max_retries, Some(3));
215 assert_eq!(config.variant.as_deref(), Some("medium"));
216 assert!(!config.stream);
217 }
218
219 #[test]
220 fn provider_base_url_zen() {
221 let p = OpenCodeProvider::Zen;
222 assert_eq!(p.base_url(), "https://opencode.ai/zen/v1");
223 }
224
225 #[test]
226 fn provider_base_url_go() {
227 let p = OpenCodeProvider::Go;
228 assert_eq!(p.base_url(), "https://opencode.ai/zen/go/v1");
229 }
230
231 #[test]
232 fn provider_base_url_direct() {
233 let p = OpenCodeProvider::Direct {
234 base_url: "http://localhost:3000/v1".into(),
235 };
236 assert_eq!(p.base_url(), "http://localhost:3000/v1");
237 }
238
239 #[test]
240 fn config_serde_roundtrip() {
241 let config = OpenCodeConfig::builder()
242 .api_key("key")
243 .max_tokens(4096)
244 .startup_timeout_secs(75)
245 .build();
246
247 let json = serde_json::to_string(&config).unwrap();
248 let parsed: OpenCodeConfig = serde_json::from_str(&json).unwrap();
249 assert_eq!(parsed.api_key.as_deref(), Some("key"));
250 assert_eq!(parsed.max_tokens, Some(4096));
251 assert_eq!(parsed.startup_timeout_secs, Some(75));
252 }
253
254 #[test]
255 fn startup_timeout_defaults_to_30_seconds() {
256 assert_eq!(
257 OpenCodeConfig::default().startup_timeout(),
258 std::time::Duration::from_secs(30)
259 );
260 }
261
262 #[test]
263 fn startup_timeout_uses_explicit_override() {
264 let config = OpenCodeConfig::builder().startup_timeout_secs(120).build();
265
266 assert_eq!(
267 config.startup_timeout(),
268 std::time::Duration::from_secs(120)
269 );
270 }
271
272 #[test]
273 fn http_mode_capabilities_report_schema_and_interrupt_support() {
274 let capabilities = OpenCodeConfig::default().capabilities();
275 assert_eq!(capabilities.input_images, CapabilitySupport::Unsupported);
276 assert_eq!(capabilities.output_schema, CapabilitySupport::Supported);
277 assert_eq!(capabilities.interrupt, CapabilitySupport::Supported);
278 assert_eq!(capabilities.resume, ResumeSemantics::Stateful);
279 }
280
281 #[test]
282 fn agent_mode_capabilities_disable_schema_and_interrupt() {
283 let capabilities = OpenCodeConfig::builder()
284 .execution_mode(OpenCodeExecutionMode::Agent)
285 .build()
286 .capabilities();
287
288 assert_eq!(capabilities.output_schema, CapabilitySupport::Unsupported);
289 assert_eq!(capabilities.interrupt, CapabilitySupport::Unsupported);
290 assert_eq!(capabilities.mcp, McpSupport::MergeAndExplicitOnly);
291 }
292}