1use super::provider::{
2 apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{AutoDelegationConfig, 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_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
56 match acl_attr(block, keys) {
57 Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
58 _ => None,
59 }
60}
61
62fn parse_auto_delegation_block(
63 block: &a3s_acl::Block,
64 base: &AutoDelegationConfig,
65) -> AutoDelegationConfig {
66 let mut config = base.clone();
67 if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
68 config.enabled = enabled;
69 }
70 if let Some(auto_parallel) =
71 acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
72 {
73 config.auto_parallel = auto_parallel;
74 }
75 if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
76 config.min_confidence = min_confidence.clamp(0.0, 1.0);
77 }
78 if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
79 config.max_tasks = max_tasks.max(1);
80 }
81 config
82}
83
84fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
85 match value {
86 a3s_acl::Value::Number(value) if *value >= 0.0 => {
87 Some((*value as usize).min(u32::MAX as usize) as u32)
88 }
89 _ => None,
90 }
91}
92
93fn acl_object_u32_attr(value: &a3s_acl::Value, key: &str) -> Option<u32> {
94 match value {
95 a3s_acl::Value::Object(pairs) => pairs
96 .iter()
97 .find_map(|(candidate, value)| (candidate == key).then(|| acl_u32(value)).flatten()),
98 _ => None,
99 }
100}
101
102fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
103 let value = acl_attr(block, keys)?;
104 match value {
105 a3s_acl::Value::List(items) => Some(
106 items
107 .iter()
108 .filter_map(acl_string)
109 .map(PathBuf::from)
110 .collect(),
111 ),
112 _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
113 }
114}
115
116impl CodeConfig {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn from_file(path: &Path) -> Result<Self> {
131 let content = std::fs::read_to_string(path).map_err(|e| {
132 CodeError::Config(format!(
133 "Failed to read config file {}: {}",
134 path.display(),
135 e
136 ))
137 })?;
138
139 Self::from_acl(&content).map_err(|e| {
140 CodeError::Config(format!(
141 "Failed to parse ACL config {}: {}",
142 path.display(),
143 e
144 ))
145 })
146 }
147
148 pub fn from_acl(content: &str) -> Result<Self> {
153 use a3s_acl::parse_acl;
154
155 let doc = parse_acl(content)
156 .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
157
158 let mut config = Self::default();
159
160 for block in doc.blocks {
161 match block.name.as_str() {
162 "default_model" => {
163 if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
165 config.default_model = Some(default_model);
166 }
167 }
168 "storage_backend" => {
169 if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
170 config.storage_backend = match backend.to_ascii_lowercase().as_str() {
171 "memory" => StorageBackend::Memory,
172 "custom" => StorageBackend::Custom,
173 _ => StorageBackend::File,
174 };
175 }
176 }
177 "sessions_dir" => {
178 if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
179 config.sessions_dir = Some(PathBuf::from(path));
180 }
181 }
182 "storage_url" => {
183 if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
184 config.storage_url = Some(storage_url);
185 }
186 }
187 "skill_dirs" | "skills" => {
188 if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
189 config.skill_dirs = paths;
190 }
191 }
192 "agent_dirs" => {
193 if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
194 config.agent_dirs = paths;
195 }
196 }
197 "max_tool_rounds" => {
198 if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
199 config.max_tool_rounds = Some(max_tool_rounds);
200 }
201 }
202 "max_parallel_tasks" => {
203 if let Some(max_parallel_tasks) =
204 acl_usize_attr(&block, &["max_parallel_tasks"])
205 {
206 config.max_parallel_tasks = Some(max_parallel_tasks);
207 }
208 }
209 "auto_parallel" | "auto_parallel_enabled" => {
210 if let Some(auto_parallel) =
211 acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
212 {
213 config.auto_parallel = Some(auto_parallel);
214 }
215 }
216 "auto_delegation" => {
217 config.auto_delegation =
218 parse_auto_delegation_block(&block, &config.auto_delegation);
219 }
220 "thinking_budget" => {
221 if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
222 config.thinking_budget = Some(thinking_budget);
223 }
224 }
225 "providers" => {
226 let provider_name = block.labels.first().cloned().ok_or_else(|| {
227 CodeError::Config(
228 "providers block requires a label (e.g., providers \"openai\" { ... })"
229 .into(),
230 )
231 })?;
232
233 let mut provider = ProviderConfig {
234 name: provider_name.clone(),
235 api_key: None,
236 base_url: None,
237 headers: HashMap::new(),
238 session_id_header: None,
239 models: Vec::new(),
240 };
241
242 for (key, value) in &block.attributes {
243 match key.as_str() {
244 "apiKey" | "api_key" => {
245 if let Some(api_key) = acl_string(value) {
246 provider.api_key = Some(api_key);
247 }
248 }
249 "baseUrl" | "base_url" => {
250 if let Some(base_url) = acl_string(value) {
251 provider.base_url = Some(base_url);
252 }
253 }
254 "sessionIdHeader" | "session_id_header" => {
255 if let Some(header) = acl_string(value) {
256 provider.session_id_header = Some(header);
257 }
258 }
259 _ => {}
260 }
261 }
262
263 for model_block in &block.blocks {
265 if model_block.name == "models" {
266 let model_name =
267 model_block.labels.first().cloned().ok_or_else(|| {
268 CodeError::Config(
269 "models block requires a label (e.g., models \"gpt-4\" { ... })"
270 .into(),
271 )
272 })?;
273
274 let mut model = ModelConfig {
275 id: model_name.clone(),
276 name: model_name.clone(),
277 family: String::new(),
278 api_key: None,
279 base_url: None,
280 headers: HashMap::new(),
281 session_id_header: None,
282 attachment: false,
283 reasoning: false,
284 tool_call: true,
285 temperature: true,
286 release_date: None,
287 modalities: ModelModalities::default(),
288 cost: ModelCost::default(),
289 limit: ModelLimit::default(),
290 };
291
292 for (key, value) in &model_block.attributes {
293 match key.as_str() {
294 "name" => {
295 if let Some(s) = acl_string(value) {
296 model.name = s;
297 }
298 }
299 "family" => {
300 if let Some(s) = acl_string(value) {
301 model.family = s;
302 }
303 }
304 "apiKey" | "api_key" => {
305 if let Some(api_key) = acl_string(value) {
306 model.api_key = Some(api_key);
307 }
308 }
309 "baseUrl" | "base_url" => {
310 if let Some(base_url) = acl_string(value) {
311 model.base_url = Some(base_url);
312 }
313 }
314 "sessionIdHeader" | "session_id_header" => {
315 if let Some(header) = acl_string(value) {
316 model.session_id_header = Some(header);
317 }
318 }
319 "attachment" => {
320 model.attachment =
321 acl_bool_attr(model_block, &["attachment"])
322 .unwrap_or(model.attachment);
323 }
324 "reasoning" => {
325 model.reasoning =
326 acl_bool_attr(model_block, &["reasoning"])
327 .unwrap_or(model.reasoning);
328 }
329 "toolCall" | "tool_call" => {
330 model.tool_call =
331 acl_bool_attr(model_block, &["toolCall", "tool_call"])
332 .unwrap_or(model.tool_call);
333 }
334 "temperature" => {
335 model.temperature =
336 acl_bool_attr(model_block, &["temperature"])
337 .unwrap_or(model.temperature);
338 }
339 "releaseDate" | "release_date" => {
340 if let Some(release_date) = acl_string(value) {
341 model.release_date = Some(release_date);
342 }
343 }
344 "maxTokens" => {
345 tracing::warn!(
346 provider = %provider.name,
347 model = %model.id,
348 field = "maxTokens",
349 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
350 );
351 if let Some(output) = acl_u32(value) {
352 model.limit.output = output;
353 }
354 }
355 "contextTokens" => {
356 tracing::warn!(
357 provider = %provider.name,
358 model = %model.id,
359 field = "contextTokens",
360 "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
361 );
362 if let Some(context) = acl_u32(value) {
363 model.limit.context = context;
364 }
365 }
366 "limit" => {
367 if let Some(output) = acl_object_u32_attr(value, "output") {
368 model.limit.output = output;
369 }
370 if let Some(context) = acl_object_u32_attr(value, "context")
371 {
372 model.limit.context = context;
373 }
374 }
375 _ => {}
376 }
377 }
378
379 provider.models.push(model);
380 }
381 }
382
383 config.providers.push(provider);
384 }
385 _ => {
386 }
389 }
390 }
391
392 if let Some(auto_parallel) = config.auto_parallel {
393 config.auto_delegation.auto_parallel = auto_parallel;
394 }
395
396 Ok(config)
397 }
398
399 pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
401 self.providers.iter().find(|p| p.name == name)
402 }
403
404 pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
406 let default = self.default_model.as_ref()?;
407 let (provider_name, _) = default.split_once('/')?;
408 self.find_provider(provider_name)
409 }
410
411 pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
413 let default = self.default_model.as_ref()?;
414 let (provider_name, model_id) = default.split_once('/')?;
415 let provider = self.find_provider(provider_name)?;
416 let model = provider.find_model(model_id)?;
417 Some((provider, model))
418 }
419
420 pub fn default_llm_config(&self) -> Option<LlmConfig> {
424 let (provider, model) = self.default_model_config()?;
425 let api_key = provider.get_api_key(model)?;
426 let base_url = provider.get_base_url(model);
427 let headers = provider.get_headers(model);
428 let session_id_header = provider.get_session_id_header(model);
429
430 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
431 if let Some(url) = base_url {
432 config = config.with_base_url(url);
433 }
434 if !headers.is_empty() {
435 config = config.with_headers(headers);
436 }
437 if let Some(header_name) = session_id_header {
438 config = config.with_session_id_header(header_name);
439 }
440 config = apply_model_caps(config, model, self.thinking_budget);
441 Some(config)
442 }
443
444 pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
448 let provider = self.find_provider(provider_name)?;
449 let model = provider.find_model(model_id)?;
450 let api_key = provider.get_api_key(model)?;
451 let base_url = provider.get_base_url(model);
452 let headers = provider.get_headers(model);
453 let session_id_header = provider.get_session_id_header(model);
454
455 let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
456 if let Some(url) = base_url {
457 config = config.with_base_url(url);
458 }
459 if !headers.is_empty() {
460 config = config.with_headers(headers);
461 }
462 if let Some(header_name) = session_id_header {
463 config = config.with_session_id_header(header_name);
464 }
465 config = apply_model_caps(config, model, self.thinking_budget);
466 Some(config)
467 }
468
469 pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
471 self.providers
472 .iter()
473 .flat_map(|p| p.models.iter().map(move |m| (p, m)))
474 .collect()
475 }
476
477 pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
479 self.skill_dirs.push(dir.into());
480 self
481 }
482
483 pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
485 self.agent_dirs.push(dir.into());
486 self
487 }
488
489 pub fn has_directories(&self) -> bool {
491 !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
492 }
493
494 pub fn has_providers(&self) -> bool {
496 !self.providers.is_empty()
497 }
498}