1pub fn systemone_url(base_url: &str) -> String {
5 let base = base_url.trim_end_matches('/');
6 if base.ends_with("/v1") { format!("{base}/systemone") } else { format!("{base}/v1/systemone") }
7}
8
9pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
11
12#[derive(Clone, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct SystemOneSection {
16 pub enabled: Option<bool>,
17 pub base_url: Option<String>,
18 pub api_key: Option<String>,
19 pub model: Option<String>,
20 pub timeout_ms: Option<u64>,
21}
22
23impl std::fmt::Debug for SystemOneSection {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("SystemOneSection")
27 .field("enabled", &self.enabled)
28 .field("base_url", &self.base_url)
29 .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]"))
30 .field("model", &self.model)
31 .field("timeout_ms", &self.timeout_ms)
32 .finish()
33 }
34}
35
36#[derive(Clone, PartialEq, Eq)]
38pub struct SystemOneConfig {
39 pub base_url: String,
40 pub api_key: Option<String>,
41 pub model: String,
42 pub timeout_ms: u64,
43}
44
45impl std::fmt::Debug for SystemOneConfig {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 f.debug_struct("SystemOneConfig")
49 .field("base_url", &self.base_url)
50 .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]"))
51 .field("model", &self.model)
52 .field("timeout_ms", &self.timeout_ms)
53 .finish()
54 }
55}
56
57impl SystemOneSection {
58 pub fn into_config(self) -> Result<Option<SystemOneConfig>, String> {
62 if !self.enabled.unwrap_or(true) {
63 return Ok(None);
64 }
65 let base_url = self
66 .base_url
67 .filter(|b| !b.trim().is_empty())
68 .ok_or("`base_url` is required when `[systemone]` is enabled")?;
69 let model = self
70 .model
71 .filter(|m| !m.trim().is_empty())
72 .ok_or("`model` is required when `[systemone]` is enabled")?;
73 let timeout_ms = self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS);
74 if timeout_ms == 0 {
75 return Err("`timeout_ms` must be greater than zero".to_owned());
76 }
77 Ok(Some(SystemOneConfig {
78 base_url: base_url.trim().to_owned(),
79 api_key: self.api_key.filter(|k| !k.trim().is_empty()),
80 model: model.trim().to_owned(),
81 timeout_ms,
82 }))
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn systemone_url_handles_base_shapes() {
92 assert_eq!(
93 systemone_url("https://api.typesafe.ai"),
94 "https://api.typesafe.ai/v1/systemone"
95 );
96 assert_eq!(
97 systemone_url("https://api.typesafe.ai/"),
98 "https://api.typesafe.ai/v1/systemone"
99 );
100 assert_eq!(
101 systemone_url("https://api.typesafe.ai/v1"),
102 "https://api.typesafe.ai/v1/systemone"
103 );
104 assert_eq!(
105 systemone_url("https://openrouter.ai/api"),
106 "https://openrouter.ai/api/v1/systemone"
107 );
108 assert_eq!(systemone_url("http://localhost:11434/"), "http://localhost:11434/v1/systemone");
109 }
110
111 #[test]
112 fn disabled_section_resolves_to_none() {
113 let section = SystemOneSection {
114 enabled: Some(false),
115 base_url: None,
116 api_key: None,
117 model: None,
118 timeout_ms: None,
119 };
120 assert_eq!(section.into_config(), Ok(None));
121 }
122
123 #[test]
124 fn missing_base_url_is_refused_when_enabled() {
125 let section = SystemOneSection {
126 enabled: None,
127 base_url: None,
128 api_key: None,
129 model: Some("jev-latest".to_owned()),
130 timeout_ms: None,
131 };
132 assert_eq!(
133 section.into_config(),
134 Err("`base_url` is required when `[systemone]` is enabled".to_owned())
135 );
136 }
137
138 #[test]
139 fn whitespace_base_url_is_treated_as_missing() {
140 let section = SystemOneSection {
141 enabled: None,
142 base_url: Some(" ".to_owned()),
143 api_key: None,
144 model: Some("jev-latest".to_owned()),
145 timeout_ms: None,
146 };
147 assert_eq!(
148 section.into_config(),
149 Err("`base_url` is required when `[systemone]` is enabled".to_owned())
150 );
151 }
152
153 #[test]
154 fn missing_model_is_refused_when_enabled() {
155 let section = SystemOneSection {
156 enabled: None,
157 base_url: Some("https://api.typesafe.ai".to_owned()),
158 api_key: None,
159 model: None,
160 timeout_ms: None,
161 };
162 assert_eq!(
163 section.into_config(),
164 Err("`model` is required when `[systemone]` is enabled".to_owned())
165 );
166 }
167
168 #[test]
169 fn zero_timeout_is_refused() {
170 let section = SystemOneSection {
171 enabled: None,
172 base_url: Some("https://api.typesafe.ai".to_owned()),
173 api_key: None,
174 model: Some("jev-latest".to_owned()),
175 timeout_ms: Some(0),
176 };
177 assert_eq!(section.into_config(), Err("`timeout_ms` must be greater than zero".to_owned()));
178 }
179
180 #[test]
181 fn full_section_resolves_with_defaults() {
182 let section = SystemOneSection {
183 enabled: None,
184 base_url: Some("https://api.typesafe.ai/".to_owned()),
185 api_key: Some("k".to_owned()),
186 model: Some("jev-latest".to_owned()),
187 timeout_ms: None,
188 };
189 assert_eq!(
190 section.into_config(),
191 Ok(Some(SystemOneConfig {
192 base_url: "https://api.typesafe.ai/".to_owned(),
193 api_key: Some("k".to_owned()),
194 model: "jev-latest".to_owned(),
195 timeout_ms: DEFAULT_TIMEOUT_MS,
196 }))
197 );
198 }
199
200 #[test]
201 fn blank_api_key_resolves_to_none() {
202 let section = SystemOneSection {
203 enabled: None,
204 base_url: Some("https://api.typesafe.ai".to_owned()),
205 api_key: Some(" ".to_owned()),
206 model: Some("jev-latest".to_owned()),
207 timeout_ms: None,
208 };
209 let cfg = section.into_config().expect("an enabled section with url and model resolves");
210 assert_eq!(cfg.expect("resolved").api_key, None);
211 }
212
213 #[test]
214 fn unknown_key_in_section_is_refused() {
215 let result: Result<SystemOneSection, _> = toml::from_str(
216 "enabled = true\nbase_uri = \"https://api.typesafe.ai\"\nmodel = \"jev-latest\"\n",
217 );
218 let err = result.expect_err("a mistyped key refuses the section");
219 assert!(err.to_string().contains("base_uri"), "{err}");
220 }
221
222 #[test]
223 fn section_deserializes_from_toml() {
224 let section: SystemOneSection = toml::from_str(
225 "enabled = false\nbase_url = \"https://api.typesafe.ai\"\nmodel = \"jev-latest\"\n",
226 )
227 .expect("the section parses");
228 assert_eq!(section.enabled, Some(false));
229 assert_eq!(section.base_url.as_deref(), Some("https://api.typesafe.ai"));
230 assert_eq!(section.into_config(), Ok(None));
231 }
232}