Skip to main content

chimera_opencode/
config.rs

1use bon::Builder;
2use serde::{Deserialize, Serialize};
3
4/// OpenCode API provider.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7#[non_exhaustive]
8#[derive(Default)]
9pub enum OpenCodeProvider {
10    /// Official Zen gateway at <https://opencode.ai/zen/v1>.
11    #[default]
12    Zen,
13    /// Custom endpoint (self-hosted or alternative OpenAI-compatible API).
14    Direct { base_url: String },
15}
16
17impl OpenCodeProvider {
18    pub fn base_url(&self) -> &str {
19        match self {
20            Self::Zen => "https://opencode.ai/zen/v1",
21            Self::Direct { base_url } => base_url.as_str(),
22        }
23    }
24}
25
26#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
27pub struct OpenCodeConfig {
28    /// API key for bearer auth. Falls back to OPENCODE_ZEN_KEY / OPENCODE_API_KEY env vars.
29    #[builder(into)]
30    pub api_key: Option<String>,
31
32    /// Provider selection.
33    #[builder(default)]
34    pub provider: OpenCodeProvider,
35
36    /// HTTP request timeout in seconds.
37    pub timeout_secs: Option<u64>,
38
39    /// Maximum retry attempts for transient errors (429, 5xx).
40    pub max_retries: Option<u32>,
41
42    /// Maximum tokens in the completion response.
43    pub max_tokens: Option<u32>,
44
45    /// Sampling temperature.
46    pub temperature: Option<f32>,
47
48    /// Enable SSE streaming responses.
49    #[builder(default = true)]
50    pub stream: bool,
51}
52
53impl Default for OpenCodeConfig {
54    fn default() -> Self {
55        Self {
56            api_key: None,
57            provider: OpenCodeProvider::default(),
58            timeout_secs: None,
59            max_retries: None,
60            max_tokens: None,
61            temperature: None,
62            stream: true,
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn config_defaults() {
73        let config = OpenCodeConfig::default();
74        assert!(config.api_key.is_none());
75        assert!(matches!(config.provider, OpenCodeProvider::Zen));
76        assert!(config.timeout_secs.is_none());
77        assert!(config.max_retries.is_none());
78        assert!(config.max_tokens.is_none());
79        assert!(config.temperature.is_none());
80        assert!(config.stream);
81    }
82
83    #[test]
84    fn config_builder() {
85        let config = OpenCodeConfig::builder()
86            .api_key("sk-test")
87            .provider(OpenCodeProvider::Direct {
88                base_url: "http://localhost:8080".into(),
89            })
90            .timeout_secs(60)
91            .max_retries(3)
92            .stream(false)
93            .build();
94
95        assert_eq!(config.api_key.as_deref(), Some("sk-test"));
96        assert_eq!(config.timeout_secs, Some(60));
97        assert_eq!(config.max_retries, Some(3));
98        assert!(!config.stream);
99    }
100
101    #[test]
102    fn provider_base_url_zen() {
103        let p = OpenCodeProvider::Zen;
104        assert_eq!(p.base_url(), "https://opencode.ai/zen/v1");
105    }
106
107    #[test]
108    fn provider_base_url_direct() {
109        let p = OpenCodeProvider::Direct {
110            base_url: "http://localhost:3000/v1".into(),
111        };
112        assert_eq!(p.base_url(), "http://localhost:3000/v1");
113    }
114
115    #[test]
116    fn config_serde_roundtrip() {
117        let config = OpenCodeConfig::builder()
118            .api_key("key")
119            .max_tokens(4096)
120            .build();
121
122        let json = serde_json::to_string(&config).unwrap();
123        let parsed: OpenCodeConfig = serde_json::from_str(&json).unwrap();
124        assert_eq!(parsed.api_key.as_deref(), Some("key"));
125        assert_eq!(parsed.max_tokens, Some(4096));
126    }
127}