1use super::provider::{
2 apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{CodeConfig, StorageBackend};
5use crate::error::{CodeError, Result};
6use crate::llm::LlmConfig;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
15 keys.iter().find_map(|key| block.attributes.get(*key))
16}
17
18fn acl_string(value: &a3s_acl::Value) -> Option<String> {
19 match value {
20 a3s_acl::Value::String(s) => Some(s.clone()),
21 a3s_acl::Value::Call(name, args) if name == "env" => {
22 let var_name = args.first().and_then(acl_string)?;
23 std::env::var(var_name).ok()
24 }
25 _ => None,
26 }
27}
28
29fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
30 acl_attr(block, keys).and_then(acl_string)
31}
32
33fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
34 block
35 .labels
36 .first()
37 .cloned()
38 .or_else(|| acl_string_attr(block, keys))
39}
40
41fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
42 match acl_attr(block, keys) {
43 Some(a3s_acl::Value::Bool(value)) => Some(*value),
44 _ => None,
45 }
46}
47
48fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
49 match acl_attr(block, keys) {
50 Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
51 _ => None,
52 }
53}
54
55fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
56 let value = acl_attr(block, keys)?;
57 match value {
58 a3s_acl::Value::List(items) => Some(
59 items
60 .iter()
61 .filter_map(acl_string)
62 .map(PathBuf::from)
63 .collect(),
64 ),
65 _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
66 }
67}
68
69impl CodeConfig {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn from_file(path: &Path) -> Result<Self> {
84 let content = std::fs::read_to_string(path).map_err(|e| {
85 CodeError::Config(format!(
86 "Failed to read config file {}: {}",
87 path.display(),
88 e
89 ))
90 })?;
91
92 Self::from_acl(&content).map_err(|e| {
93 CodeError::Config(format!(
94 "Failed to parse ACL config {}: {}",
95 path.display(),
96 e
97 ))
98 })
99 }
100
101 pub fn from_acl(content: &str) -> Result<Self> {
106 use a3s_acl::parse_acl;
107
108 let doc = parse_acl(content)
109 .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
110
111 let mut config = Self::default();
112
113 for block in doc.blocks {
114 match block.name.as_str() {
115 "default_model" => {
116 if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
118 config.default_model = Some(default_model);
119 }
120 }
121 "storage_backend" => {
122 if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
123 config.storage_backend = match backend.to_ascii_lowercase().as_str() {
124 "memory" => StorageBackend::Memory,
125 "custom" => StorageBackend::Custom,
126 _ => StorageBackend::File,
127 };
128 }
129 }
130 "sessions_dir" => {
131 if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
132 config.sessions_dir = Some(PathBuf::from(path));
133 }
134 }
135 "storage_url" => {
136 if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
137 config.storage_url = Some(storage_url);
138 }
139 }
140 "skill_dirs" | "skills" => {
141 if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
142 config.skill_dirs = paths;
143 }
144 }
145 "agent_dirs" => {
146 if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
147 config.agent_dirs = paths;
148 }
149 }
150 "max_tool_rounds" => {
151 if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
152 config.max_tool_rounds = Some(max_tool_rounds);
153 }
154 }
155 "thinking_budget" => {
156 if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
157 config.thinking_budget = Some(thinking_budget);
158 }
159 }
160 "providers" => {
161 let provider_name = block.labels.first().cloned().ok_or_else(|| {
162 CodeError::Config(
163 "providers block requires a label (e.g., providers \"openai\" { ... })"
164 .into(),
165 )
166 })?;
167
168 let mut provider = ProviderConfig {
169 name: provider_name.clone(),
170 api_key: None,
171 base_url: None,
172 headers: HashMap::new(),
173 session_id_header: None,
174 models: Vec::new(),
175 };
176
177 for (key, value) in &block.attributes {
178 match key.as_str() {
179 "apiKey" | "api_key" => {
180 if let Some(api_key) = acl_string(value) {
181 provider.api_key = Some(api_key);
182 }
183 }
184 "baseUrl" | "base_url" => {
185 if let Some(base_url) = acl_string(value) {
186 provider.base_url = Some(base_url);
187 }
188 }
189 "sessionIdHeader" | "session_id_header" => {
190 if let Some(header) = acl_string(value) {
191 provider.session_id_header = Some(header);
192 }
193 }
194 _ => {}
195 }
196 }
197
198 for model_block in &block.blocks {
200 if model_block.name == "models" {
201 let model_name =
202 model_block.labels.first().cloned().ok_or_else(|| {
203 CodeError::Config(
204 "models block requires a label (e.g., models \"gpt-4\" { ... })"
205 .into(),
206 )
207 })?;
208
209 let mut model = ModelConfig {
210 id: model_name.clone(),
211 name: model_name.clone(),
212 family: String::new(),
213 api_key: None,
214 base_url: None,
215 headers: HashMap::new(),
216 session_id_header: None,
217 attachment: false,
218 reasoning: false,
219 tool_call: true,
220 temperature: true,
221 release_date: None,
222 modalities: ModelModalities::default(),
223 cost: ModelCost::default(),
224 limit: ModelLimit::default(),
225 };
226
227 for (key, value) in &model_block.attributes {
228 match key.as_str() {
229 "name" => {
230 if let Some(s) = acl_string(value) {
231 model.name = s;
232 }
233 }
234 "family" => {
235 if let Some(s) = acl_string(value) {
236 model.family = s;
237 }
238 }
239 "apiKey" | "api_key" => {
240 if let Some(api_key) = acl_string(value) {
241 model.api_key = Some(api_key);
242 }
243 }
244 "baseUrl" | "base_url" => {
245 if let Some(base_url) = acl_string(value) {
246 model.base_url = Some(base_url);
247 }
248 }
249 "sessionIdHeader" | "session_id_header" => {
250 if let Some(header) = acl_string(value) {
251 model.session_id_header = Some(header);
252 }
253 }
254 "attachment" => {
255 model.attachment =
256 acl_bool_attr(model_block, &["attachment"])
257 .unwrap_or(model.attachment);
258 }
259 "reasoning" => {
260 model.reasoning =
261 acl_bool_attr(model_block, &["reasoning"])
262 .unwrap_or(model.reasoning);
263 }
264 "toolCall" | "tool_call" => {
265 model.tool_call =
266 acl_bool_attr(model_block, &["toolCall", "tool_call"])
267 .unwrap_or(model.tool_call);
268 }
269 "temperature" => {
270 model.temperature =
271 acl_bool_attr(model_block, &["temperature"])
272 .unwrap_or(model.temperature);
273 }
274 "releaseDate" | "release_date" => {
275 if let Some(release_date) = acl_string(value) {
276 model.release_date = Some(release_date);
277 }
278 }
279 _ => {}
280 }
281 }
282
283 provider.models.push(model);
284 }
285 }
286
287 config.providers.push(provider);
288 }
289 _ => {
290 }
293 }
294 }
295
296 Ok(config)
297 }
298
299 pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
301 self.providers.iter().find(|p| p.name == name)
302 }
303
304 pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
306 let default = self.default_model.as_ref()?;
307 let (provider_name, _) = default.split_once('/')?;
308 self.find_provider(provider_name)
309 }
310
311 pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
313 let default = self.default_model.as_ref()?;
314 let (provider_name, model_id) = default.split_once('/')?;
315 let provider = self.find_provider(provider_name)?;
316 let model = provider.find_model(model_id)?;
317 Some((provider, model))
318 }
319
320 pub fn default_llm_config(&self) -> Option<LlmConfig> {
324 let (provider, model) = self.default_model_config()?;
325 let api_key = provider.get_api_key(model)?;
326 let base_url = provider.get_base_url(model);
327 let headers = provider.get_headers(model);
328 let session_id_header = provider.get_session_id_header(model);
329
330 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
331 if let Some(url) = base_url {
332 config = config.with_base_url(url);
333 }
334 if !headers.is_empty() {
335 config = config.with_headers(headers);
336 }
337 if let Some(header_name) = session_id_header {
338 config = config.with_session_id_header(header_name);
339 }
340 config = apply_model_caps(config, model, self.thinking_budget);
341 Some(config)
342 }
343
344 pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
348 let provider = self.find_provider(provider_name)?;
349 let model = provider.find_model(model_id)?;
350 let api_key = provider.get_api_key(model)?;
351 let base_url = provider.get_base_url(model);
352 let headers = provider.get_headers(model);
353 let session_id_header = provider.get_session_id_header(model);
354
355 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
356 if let Some(url) = base_url {
357 config = config.with_base_url(url);
358 }
359 if !headers.is_empty() {
360 config = config.with_headers(headers);
361 }
362 if let Some(header_name) = session_id_header {
363 config = config.with_session_id_header(header_name);
364 }
365 config = apply_model_caps(config, model, self.thinking_budget);
366 Some(config)
367 }
368
369 pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
371 self.providers
372 .iter()
373 .flat_map(|p| p.models.iter().map(move |m| (p, m)))
374 .collect()
375 }
376
377 pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
379 self.skill_dirs.push(dir.into());
380 self
381 }
382
383 pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
385 self.agent_dirs.push(dir.into());
386 self
387 }
388
389 pub fn has_directories(&self) -> bool {
391 !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
392 }
393
394 pub fn has_providers(&self) -> bool {
396 !self.providers.is_empty()
397 }
398}