Skip to main content

a3s_code_core/config/
loader.rs

1use super::provider::{
2    apply_model_caps, ModelConfig, ModelCost, ModelLimit, ModelModalities, ProviderConfig,
3};
4use super::{AutoDelegationConfig, CodeConfig, OsConfig, StorageBackend};
5use crate::error::{CodeError, Result};
6use crate::llm::LlmConfig;
7use crate::mcp::McpServerConfig;
8use crate::memory::MemoryConfig;
9use crate::queue::SessionQueueConfig;
10use a3s_memory::{PrunePolicy, RelevanceConfig};
11use serde_json::{Map as JsonMap, Value as JsonValue};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15// ============================================================================
16// ACL Parsing Helpers
17// ============================================================================
18
19fn acl_attr<'a>(block: &'a a3s_acl::Block, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
20    keys.iter().find_map(|key| block.attributes.get(*key))
21}
22
23fn acl_string(value: &a3s_acl::Value) -> Option<String> {
24    match value {
25        a3s_acl::Value::String(s) => Some(s.clone()),
26        a3s_acl::Value::Call(name, args) if name == "env" => {
27            let var_name = args.first().and_then(acl_string)?;
28            std::env::var(var_name).ok()
29        }
30        _ => None,
31    }
32}
33
34fn acl_string_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
35    acl_attr(block, keys).and_then(acl_string)
36}
37
38fn acl_label_or_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<String> {
39    block
40        .labels
41        .first()
42        .cloned()
43        .or_else(|| acl_string_attr(block, keys))
44}
45
46fn acl_bool_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<bool> {
47    match acl_attr(block, keys) {
48        Some(a3s_acl::Value::Bool(value)) => Some(*value),
49        _ => None,
50    }
51}
52
53fn acl_usize_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<usize> {
54    match acl_attr(block, keys) {
55        Some(a3s_acl::Value::Number(value)) if *value >= 0.0 => Some(*value as usize),
56        _ => None,
57    }
58}
59
60fn acl_f32_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<f32> {
61    match acl_attr(block, keys) {
62        Some(a3s_acl::Value::Number(value)) => Some(*value as f32),
63        _ => None,
64    }
65}
66
67fn parse_auto_delegation_block(
68    block: &a3s_acl::Block,
69    base: &AutoDelegationConfig,
70) -> AutoDelegationConfig {
71    let mut config = base.clone();
72    if let Some(enabled) = acl_bool_attr(block, &["enabled"]) {
73        config.enabled = enabled;
74    }
75    if let Some(auto_parallel) =
76        acl_bool_attr(block, &["auto_parallel", "autoParallel", "parallel"])
77    {
78        config.auto_parallel = auto_parallel;
79    }
80    if let Some(allow_manual_delegation) = acl_bool_attr(
81        block,
82        &[
83            "allow_manual_delegation",
84            "allowManualDelegation",
85            "manual_delegation",
86            "manualDelegation",
87        ],
88    ) {
89        config.allow_manual_delegation = allow_manual_delegation;
90    }
91    if let Some(min_confidence) = acl_f32_attr(block, &["min_confidence", "minConfidence"]) {
92        config.min_confidence = min_confidence.clamp(0.0, 1.0);
93    }
94    if let Some(max_tasks) = acl_usize_attr(block, &["max_tasks", "maxTasks"]) {
95        config.max_tasks = max_tasks.max(1);
96    }
97    config
98}
99
100fn parse_memory_block(block: &a3s_acl::Block, base: Option<&MemoryConfig>) -> MemoryConfig {
101    let mut config = base.cloned().unwrap_or_default();
102
103    if let Some(max_short_term) = acl_usize_attr(block, &["max_short_term", "maxShortTerm"]) {
104        config.max_short_term = max_short_term;
105    }
106    if let Some(max_working) = acl_usize_attr(block, &["max_working", "maxWorking"]) {
107        config.max_working = max_working;
108    }
109    if let Some(prune_interval_secs) =
110        acl_usize_attr(block, &["prune_interval_secs", "pruneIntervalSecs"])
111    {
112        config.prune_interval_secs = prune_interval_secs as u64;
113    }
114    if let Some(llm_extraction) = acl_bool_attr(block, &["llm_extraction", "llmExtraction"]) {
115        config.llm_extraction = llm_extraction;
116    }
117    if let Some(max_items) = acl_usize_attr(
118        block,
119        &["llm_extraction_max_items", "llmExtractionMaxItems"],
120    ) {
121        config.llm_extraction_max_items = max_items;
122    }
123    if let Some(max_input_chars) = acl_usize_attr(
124        block,
125        &[
126            "llm_extraction_max_input_chars",
127            "llmExtractionMaxInputChars",
128        ],
129    ) {
130        config.llm_extraction_max_input_chars = max_input_chars;
131    }
132
133    if let Some(relevance) = acl_attr(block, &["relevance"]) {
134        config.relevance = parse_relevance_value(relevance, &config.relevance);
135    }
136
137    if let Some(prune_policy) = acl_attr(block, &["prune", "prune_policy", "prunePolicy"]) {
138        config.prune_policy = Some(parse_prune_policy_value(
139            prune_policy,
140            config.prune_policy.as_ref(),
141        ));
142    }
143
144    for child in &block.blocks {
145        let value = a3s_acl::Value::Object(
146            child
147                .attributes
148                .iter()
149                .map(|(key, value)| (key.clone(), value.clone()))
150                .collect(),
151        );
152        match child.name.as_str() {
153            "relevance" => {
154                config.relevance = parse_relevance_value(&value, &config.relevance);
155            }
156            "prune" | "prune_policy" | "prunePolicy" => {
157                config.prune_policy = Some(parse_prune_policy_value(
158                    &value,
159                    config.prune_policy.as_ref(),
160                ));
161            }
162            _ => {}
163        }
164    }
165
166    config
167}
168
169fn parse_relevance_value(value: &a3s_acl::Value, base: &RelevanceConfig) -> RelevanceConfig {
170    let mut config = base.clone();
171    if let Some(decay_days) = acl_object_f32_attr(value, &["decay_days", "decayDays"]) {
172        config.decay_days = decay_days.max(0.1);
173    }
174    if let Some(importance_weight) =
175        acl_object_f32_attr(value, &["importance_weight", "importanceWeight"])
176    {
177        config.importance_weight = importance_weight.max(0.0);
178    }
179    if let Some(recency_weight) = acl_object_f32_attr(value, &["recency_weight", "recencyWeight"]) {
180        config.recency_weight = recency_weight.max(0.0);
181    }
182    config
183}
184
185fn parse_prune_policy_value(value: &a3s_acl::Value, base: Option<&PrunePolicy>) -> PrunePolicy {
186    let mut policy = base.cloned().unwrap_or_default();
187    if let Some(max_age_days) = acl_object_u32_attr(value, &["max_age_days", "maxAgeDays"]) {
188        policy.max_age_days = max_age_days;
189    }
190    if let Some(min_importance) =
191        acl_object_f32_attr(value, &["min_importance_to_keep", "minImportanceToKeep"])
192    {
193        policy.min_importance_to_keep = min_importance.clamp(0.0, 1.0);
194    }
195    if let Some(max_items) = acl_object_usize_attr(value, &["max_items", "maxItems"]) {
196        policy.max_items = max_items;
197    }
198    policy
199}
200
201fn acl_object_attr<'a>(value: &'a a3s_acl::Value, keys: &[&str]) -> Option<&'a a3s_acl::Value> {
202    match value {
203        a3s_acl::Value::Object(pairs) => keys.iter().find_map(|key| {
204            pairs
205                .iter()
206                .find_map(|(candidate, value)| (candidate == key).then_some(value))
207        }),
208        _ => None,
209    }
210}
211
212fn acl_f32(value: &a3s_acl::Value) -> Option<f32> {
213    match value {
214        a3s_acl::Value::Number(value) => Some(*value as f32),
215        _ => None,
216    }
217}
218
219fn acl_usize(value: &a3s_acl::Value) -> Option<usize> {
220    match value {
221        a3s_acl::Value::Number(value) if *value >= 0.0 => Some(*value as usize),
222        _ => None,
223    }
224}
225
226fn acl_u32(value: &a3s_acl::Value) -> Option<u32> {
227    match value {
228        a3s_acl::Value::Number(value) if *value >= 0.0 => {
229            Some((*value as usize).min(u32::MAX as usize) as u32)
230        }
231        _ => None,
232    }
233}
234
235fn acl_object_f32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f32> {
236    acl_object_attr(value, keys).and_then(acl_f32)
237}
238
239fn acl_object_usize_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<usize> {
240    acl_object_attr(value, keys).and_then(acl_usize)
241}
242
243fn acl_object_u32_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<u32> {
244    acl_object_attr(value, keys).and_then(acl_u32)
245}
246
247fn acl_path_list_attr(block: &a3s_acl::Block, keys: &[&str]) -> Option<Vec<PathBuf>> {
248    let value = acl_attr(block, keys)?;
249    match value {
250        a3s_acl::Value::List(items) => Some(
251            items
252                .iter()
253                .filter_map(acl_string)
254                .map(PathBuf::from)
255                .collect(),
256        ),
257        _ => acl_string(value).map(|s| vec![PathBuf::from(s)]),
258    }
259}
260
261fn snake_to_camel(value: &str) -> String {
262    let mut output = String::with_capacity(value.len());
263    let mut uppercase_next = false;
264    for ch in value.chars() {
265        if ch == '_' || ch == '-' {
266            uppercase_next = true;
267        } else if uppercase_next {
268            output.extend(ch.to_uppercase());
269            uppercase_next = false;
270        } else {
271            output.push(ch);
272        }
273    }
274    output
275}
276
277fn acl_value_to_json(value: &a3s_acl::Value) -> Option<JsonValue> {
278    match value {
279        a3s_acl::Value::String(value) => Some(JsonValue::String(value.clone())),
280        a3s_acl::Value::Number(value) if value.fract() == 0.0 && *value >= 0.0 => {
281            Some(JsonValue::Number(serde_json::Number::from(*value as u64)))
282        }
283        a3s_acl::Value::Number(value) if value.fract() == 0.0 => {
284            Some(JsonValue::Number(serde_json::Number::from(*value as i64)))
285        }
286        a3s_acl::Value::Number(value) => {
287            serde_json::Number::from_f64(*value).map(JsonValue::Number)
288        }
289        a3s_acl::Value::Bool(value) => Some(JsonValue::Bool(*value)),
290        a3s_acl::Value::List(items) => Some(JsonValue::Array(
291            items.iter().filter_map(acl_value_to_json).collect(),
292        )),
293        a3s_acl::Value::Object(pairs) => {
294            let mut object = JsonMap::new();
295            for (key, value) in pairs {
296                if let Some(value) = acl_value_to_json(value) {
297                    object.insert(key.clone(), value);
298                }
299            }
300            Some(JsonValue::Object(object))
301        }
302        a3s_acl::Value::Null => Some(JsonValue::Null),
303        a3s_acl::Value::Call(name, _) if name == "env" => acl_string(value).map(JsonValue::String),
304        a3s_acl::Value::Call(_, _) => None,
305    }
306}
307
308fn insert_nested_json(object: &mut JsonMap<String, JsonValue>, key: String, value: JsonValue) {
309    match object.remove(&key) {
310        None => {
311            object.insert(key, value);
312        }
313        Some(JsonValue::Array(mut values)) => {
314            values.push(value);
315            object.insert(key, JsonValue::Array(values));
316        }
317        Some(previous) => {
318            object.insert(key, JsonValue::Array(vec![previous, value]));
319        }
320    }
321}
322
323fn acl_block_to_json(block: &a3s_acl::Block) -> JsonValue {
324    let mut object = JsonMap::new();
325    for (key, value) in &block.attributes {
326        if let Some(value) = acl_value_to_json(value) {
327            object.insert(snake_to_camel(key), value);
328        }
329    }
330
331    for child in &block.blocks {
332        let key = snake_to_camel(&child.name);
333        let value = acl_block_to_json(child);
334        if let Some(label) = child.labels.first() {
335            let entry = object
336                .entry(key)
337                .or_insert_with(|| JsonValue::Object(JsonMap::new()));
338            if let JsonValue::Object(entries) = entry {
339                entries.insert(label.clone(), value);
340            }
341        } else {
342            insert_nested_json(&mut object, key, value);
343        }
344    }
345
346    JsonValue::Object(object)
347}
348
349fn normalize_lane_name(value: &str) -> Option<&'static str> {
350    match value.trim().to_ascii_lowercase().as_str() {
351        "control" => Some("Control"),
352        "query" => Some("Query"),
353        "execute" => Some("Execute"),
354        "generate" => Some("Generate"),
355        _ => None,
356    }
357}
358
359fn normalize_lane_map(value: &mut JsonValue, normalize_handler_mode: bool) {
360    let JsonValue::Object(entries) = value else {
361        return;
362    };
363    let previous = std::mem::take(entries);
364    for (name, mut value) in previous {
365        let Some(name) = normalize_lane_name(&name) else {
366            continue;
367        };
368        if normalize_handler_mode {
369            if let JsonValue::Object(handler) = &mut value {
370                rename_json_key(handler, "timeoutMs", "timeout_ms");
371                if let Some(JsonValue::String(mode)) = handler.get_mut("mode") {
372                    *mode = match mode.trim().to_ascii_lowercase().as_str() {
373                        "external" => "External".to_string(),
374                        "hybrid" => "Hybrid".to_string(),
375                        _ => "Internal".to_string(),
376                    };
377                }
378            }
379        }
380        entries.insert(name.to_string(), value);
381    }
382}
383
384fn parse_queue_block(block: &a3s_acl::Block) -> Result<SessionQueueConfig> {
385    let mut value = acl_block_to_json(block);
386    if let Some(lane_handlers) = value.get_mut("laneHandlers") {
387        normalize_lane_map(lane_handlers, true);
388    }
389    if let Some(lane_timeouts) = value.get_mut("laneTimeouts") {
390        normalize_lane_map(lane_timeouts, false);
391    }
392    serde_json::from_value(value)
393        .map_err(|error| CodeError::Config(format!("Invalid queue configuration: {error}")))
394}
395
396fn parse_search_block(block: &a3s_acl::Block) -> Result<super::SearchConfig> {
397    serde_json::from_value(acl_block_to_json(block))
398        .map_err(|error| CodeError::Config(format!("Invalid search configuration: {error}")))
399}
400
401fn parse_document_parser_block(block: &a3s_acl::Block) -> Result<super::DocumentParserConfig> {
402    serde_json::from_value(acl_block_to_json(block)).map_err(|error| {
403        CodeError::Config(format!("Invalid document parser configuration: {error}"))
404    })
405}
406
407fn rename_json_key(object: &mut JsonMap<String, JsonValue>, from: &str, to: &str) {
408    if let Some(value) = object.remove(from) {
409        object.insert(to.to_string(), value);
410    }
411}
412
413fn parse_mcp_server_block(block: &a3s_acl::Block) -> Result<McpServerConfig> {
414    let mut value = acl_block_to_json(block);
415    let object = value.as_object_mut().ok_or_else(|| {
416        CodeError::Config("Invalid MCP server configuration: expected an object".to_string())
417    })?;
418    if let Some(label) = block.labels.first() {
419        object.insert("name".to_string(), JsonValue::String(label.clone()));
420    }
421    if let Some(JsonValue::Object(oauth)) = object.get_mut("oauth") {
422        rename_json_key(oauth, "authUrl", "auth_url");
423        rename_json_key(oauth, "tokenUrl", "token_url");
424        rename_json_key(oauth, "clientId", "client_id");
425        rename_json_key(oauth, "clientSecret", "client_secret");
426        rename_json_key(oauth, "redirectUri", "redirect_uri");
427        rename_json_key(oauth, "accessToken", "access_token");
428    }
429    serde_json::from_value(value)
430        .map_err(|error| CodeError::Config(format!("Invalid MCP server configuration: {error}")))
431}
432
433fn acl_string_map(value: &a3s_acl::Value) -> HashMap<String, String> {
434    match value {
435        a3s_acl::Value::Object(pairs) => pairs
436            .iter()
437            .filter_map(|(key, value)| acl_string(value).map(|value| (key.clone(), value)))
438            .collect(),
439        _ => HashMap::new(),
440    }
441}
442
443fn acl_string_list(value: &a3s_acl::Value) -> Vec<String> {
444    match value {
445        a3s_acl::Value::List(items) => items.iter().filter_map(acl_string).collect(),
446        _ => Vec::new(),
447    }
448}
449
450fn acl_object_f64_attr(value: &a3s_acl::Value, keys: &[&str]) -> Option<f64> {
451    match acl_object_attr(value, keys) {
452        Some(a3s_acl::Value::Number(value)) => Some(*value),
453        _ => None,
454    }
455}
456
457// ============================================================================
458// CodeConfig Implementation
459// ============================================================================
460
461impl CodeConfig {
462    /// Create a new empty configuration
463    pub fn new() -> Self {
464        Self::default()
465    }
466
467    /// Load configuration from an ACL-compatible config file.
468    ///
469    /// `.acl` is the only supported config file extension. JSON and legacy
470    /// `.hcl` config files are not supported.
471    pub fn from_file(path: &Path) -> Result<Self> {
472        let content = std::fs::read_to_string(path).map_err(|e| {
473            CodeError::Config(format!(
474                "Failed to read config file {}: {}",
475                path.display(),
476                e
477            ))
478        })?;
479
480        Self::from_acl(&content).map_err(|e| {
481            CodeError::Config(format!(
482                "Failed to parse ACL config {}: {}",
483                path.display(),
484                e
485            ))
486        })
487    }
488
489    /// Parse configuration from an ACL string.
490    ///
491    /// ACL (Agent Configuration Language) uses labeled blocks like
492    /// `providers "openai" { }`.
493    pub fn from_acl(content: &str) -> Result<Self> {
494        use a3s_acl::parse_acl;
495
496        let doc = parse_acl(content)
497            .map_err(|e| CodeError::Config(format!("Failed to parse ACL: {}", e)))?;
498
499        let mut config = Self::default();
500
501        for block in doc.blocks {
502            match block.name.as_str() {
503                "default_model" => {
504                    // ACL: default_model = "openai/gpt-4" or just "openai/gpt-4" as label
505                    if let Some(default_model) = acl_label_or_attr(&block, &["default_model"]) {
506                        config.default_model = Some(default_model);
507                    }
508                }
509                "storage_backend" => {
510                    if let Some(backend) = acl_string_attr(&block, &["storage_backend"]) {
511                        config.storage_backend = match backend.to_ascii_lowercase().as_str() {
512                            "memory" => StorageBackend::Memory,
513                            "custom" => StorageBackend::Custom,
514                            _ => StorageBackend::File,
515                        };
516                    }
517                }
518                "sessions_dir" => {
519                    if let Some(path) = acl_string_attr(&block, &["sessions_dir"]) {
520                        config.sessions_dir = Some(PathBuf::from(path));
521                    }
522                }
523                "memory_dir" | "memoryDir" => {
524                    if let Some(path) = acl_string_attr(&block, &["memory_dir", "memoryDir"]) {
525                        config.memory_dir = Some(PathBuf::from(path));
526                    }
527                }
528                "memory" => {
529                    config.memory = Some(parse_memory_block(&block, config.memory.as_ref()));
530                }
531                "queue" => {
532                    config.queue = Some(parse_queue_block(&block)?);
533                }
534                "search" => {
535                    config.search = Some(parse_search_block(&block)?);
536                }
537                "document_parser" | "documentParser" => {
538                    config.document_parser = Some(parse_document_parser_block(&block)?);
539                }
540                "mcp_servers" | "mcpServers" | "mcp_server" => {
541                    config.mcp_servers.push(parse_mcp_server_block(&block)?);
542                }
543                "storage_url" => {
544                    if let Some(storage_url) = acl_string_attr(&block, &["storage_url"]) {
545                        config.storage_url = Some(storage_url);
546                    }
547                }
548                "skill_dirs" | "skills" => {
549                    if let Some(paths) = acl_path_list_attr(&block, &["skill_dirs", "skills"]) {
550                        config.skill_dirs = paths;
551                    }
552                }
553                "agent_dirs" => {
554                    if let Some(paths) = acl_path_list_attr(&block, &["agent_dirs"]) {
555                        config.agent_dirs = paths;
556                    }
557                }
558                "max_tool_rounds" => {
559                    if let Some(max_tool_rounds) = acl_usize_attr(&block, &["max_tool_rounds"]) {
560                        config.max_tool_rounds = Some(max_tool_rounds);
561                    }
562                }
563                "max_parallel_tasks" => {
564                    if let Some(max_parallel_tasks) =
565                        acl_usize_attr(&block, &["max_parallel_tasks"])
566                    {
567                        config.max_parallel_tasks = Some(max_parallel_tasks);
568                    }
569                }
570                "auto_parallel" | "auto_parallel_enabled" => {
571                    if let Some(auto_parallel) =
572                        acl_bool_attr(&block, &["auto_parallel", "auto_parallel_enabled"])
573                    {
574                        config.auto_parallel = Some(auto_parallel);
575                    }
576                }
577                "auto_delegation" => {
578                    config.auto_delegation =
579                        parse_auto_delegation_block(&block, &config.auto_delegation);
580                }
581                "thinking_budget" => {
582                    if let Some(thinking_budget) = acl_usize_attr(&block, &["thinking_budget"]) {
583                        config.thinking_budget = Some(thinking_budget);
584                    }
585                }
586                "llm_api_timeout_ms" | "api_timeout_ms" | "model_api_timeout_ms" => {
587                    if let Some(timeout_ms) = acl_usize_attr(
588                        &block,
589                        &[
590                            "llm_api_timeout_ms",
591                            "api_timeout_ms",
592                            "model_api_timeout_ms",
593                        ],
594                    ) {
595                        config.llm_api_timeout_ms = Some(timeout_ms as u64);
596                    }
597                }
598                "os" => {
599                    if let Some(address) =
600                        acl_label_or_attr(&block, &["os", "address", "url", "baseUrl", "base_url"])
601                            .map(|value| value.trim().to_string())
602                            .filter(|value| !value.is_empty())
603                    {
604                        config.os = Some(OsConfig { address });
605                    }
606                }
607                "providers" => {
608                    let provider_name = block.labels.first().cloned().ok_or_else(|| {
609                        CodeError::Config(
610                            "providers block requires a label (e.g., providers \"openai\" { ... })"
611                                .into(),
612                        )
613                    })?;
614
615                    let mut provider = ProviderConfig {
616                        name: provider_name.clone(),
617                        api_key: None,
618                        base_url: None,
619                        headers: HashMap::new(),
620                        session_id_header: None,
621                        models: Vec::new(),
622                    };
623
624                    for (key, value) in &block.attributes {
625                        match key.as_str() {
626                            "apiKey" | "api_key" => {
627                                if let Some(api_key) = acl_string(value) {
628                                    provider.api_key = Some(api_key);
629                                }
630                            }
631                            "baseUrl" | "base_url" => {
632                                if let Some(base_url) = acl_string(value) {
633                                    provider.base_url = Some(base_url);
634                                }
635                            }
636                            "sessionIdHeader" | "session_id_header" => {
637                                if let Some(header) = acl_string(value) {
638                                    provider.session_id_header = Some(header);
639                                }
640                            }
641                            "headers" => {
642                                provider.headers = acl_string_map(value);
643                            }
644                            _ => {}
645                        }
646                    }
647
648                    // Process nested models blocks
649                    for model_block in &block.blocks {
650                        if model_block.name == "models" {
651                            let model_name =
652                                model_block.labels.first().cloned().ok_or_else(|| {
653                                    CodeError::Config(
654                                        "models block requires a label (e.g., models \"gpt-4\" { ... })"
655                                            .into(),
656                                    )
657                                })?;
658
659                            let mut model = ModelConfig {
660                                id: model_name.clone(),
661                                name: model_name.clone(),
662                                family: String::new(),
663                                api_key: None,
664                                base_url: None,
665                                headers: HashMap::new(),
666                                session_id_header: None,
667                                attachment: false,
668                                reasoning: false,
669                                tool_call: true,
670                                temperature: true,
671                                release_date: None,
672                                modalities: ModelModalities::default(),
673                                cost: ModelCost::default(),
674                                limit: ModelLimit::default(),
675                            };
676
677                            for (key, value) in &model_block.attributes {
678                                match key.as_str() {
679                                    "name" => {
680                                        if let Some(s) = acl_string(value) {
681                                            model.name = s;
682                                        }
683                                    }
684                                    "family" => {
685                                        if let Some(s) = acl_string(value) {
686                                            model.family = s;
687                                        }
688                                    }
689                                    "apiKey" | "api_key" => {
690                                        if let Some(api_key) = acl_string(value) {
691                                            model.api_key = Some(api_key);
692                                        }
693                                    }
694                                    "baseUrl" | "base_url" => {
695                                        if let Some(base_url) = acl_string(value) {
696                                            model.base_url = Some(base_url);
697                                        }
698                                    }
699                                    "sessionIdHeader" | "session_id_header" => {
700                                        if let Some(header) = acl_string(value) {
701                                            model.session_id_header = Some(header);
702                                        }
703                                    }
704                                    "headers" => {
705                                        model.headers = acl_string_map(value);
706                                    }
707                                    "attachment" => {
708                                        model.attachment =
709                                            acl_bool_attr(model_block, &["attachment"])
710                                                .unwrap_or(model.attachment);
711                                    }
712                                    "reasoning" => {
713                                        model.reasoning =
714                                            acl_bool_attr(model_block, &["reasoning"])
715                                                .unwrap_or(model.reasoning);
716                                    }
717                                    "toolCall" | "tool_call" => {
718                                        model.tool_call =
719                                            acl_bool_attr(model_block, &["toolCall", "tool_call"])
720                                                .unwrap_or(model.tool_call);
721                                    }
722                                    "temperature" => {
723                                        model.temperature =
724                                            acl_bool_attr(model_block, &["temperature"])
725                                                .unwrap_or(model.temperature);
726                                    }
727                                    "releaseDate" | "release_date" => {
728                                        if let Some(release_date) = acl_string(value) {
729                                            model.release_date = Some(release_date);
730                                        }
731                                    }
732                                    "maxTokens" => {
733                                        tracing::warn!(
734                                            provider = %provider.name,
735                                            model = %model.id,
736                                            field = "maxTokens",
737                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
738                                        );
739                                        if let Some(output) = acl_u32(value) {
740                                            model.limit.output = output;
741                                        }
742                                    }
743                                    "contextTokens" => {
744                                        tracing::warn!(
745                                            provider = %provider.name,
746                                            model = %model.id,
747                                            field = "contextTokens",
748                                            "Flat ACL model token limit fields are deprecated; use limit = {{ output = ..., context = ... }}"
749                                        );
750                                        if let Some(context) = acl_u32(value) {
751                                            model.limit.context = context;
752                                        }
753                                    }
754                                    "limit" => {
755                                        if let Some(output) =
756                                            acl_object_u32_attr(value, &["output"])
757                                        {
758                                            model.limit.output = output;
759                                        }
760                                        if let Some(context) =
761                                            acl_object_u32_attr(value, &["context"])
762                                        {
763                                            model.limit.context = context;
764                                        }
765                                    }
766                                    "modalities" => {
767                                        if let Some(input) = acl_object_attr(value, &["input"]) {
768                                            model.modalities.input = acl_string_list(input);
769                                        }
770                                        if let Some(output) = acl_object_attr(value, &["output"]) {
771                                            model.modalities.output = acl_string_list(output);
772                                        }
773                                    }
774                                    "cost" => {
775                                        if let Some(input) = acl_object_f64_attr(value, &["input"])
776                                        {
777                                            model.cost.input = input;
778                                        }
779                                        if let Some(output) =
780                                            acl_object_f64_attr(value, &["output"])
781                                        {
782                                            model.cost.output = output;
783                                        }
784                                        if let Some(cache_read) =
785                                            acl_object_f64_attr(value, &["cache_read", "cacheRead"])
786                                        {
787                                            model.cost.cache_read = cache_read;
788                                        }
789                                        if let Some(cache_write) = acl_object_f64_attr(
790                                            value,
791                                            &["cache_write", "cacheWrite"],
792                                        ) {
793                                            model.cost.cache_write = cache_write;
794                                        }
795                                    }
796                                    _ => {}
797                                }
798                            }
799
800                            provider.models.push(model);
801                        }
802                    }
803
804                    config.providers.push(provider);
805                }
806                _ => {}
807            }
808        }
809
810        if let Some(auto_parallel) = config.auto_parallel {
811            config.auto_delegation.auto_parallel = auto_parallel;
812        }
813
814        Ok(config)
815    }
816
817    /// Find a provider by name
818    pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
819        self.providers.iter().find(|p| p.name == name)
820    }
821
822    /// Get the default provider configuration (parsed from `default_model` "provider/model" format)
823    pub fn default_provider_config(&self) -> Option<&ProviderConfig> {
824        let default = self.default_model.as_ref()?;
825        let (provider_name, _) = default.split_once('/')?;
826        self.find_provider(provider_name)
827    }
828
829    /// Get the default model configuration (parsed from `default_model` "provider/model" format)
830    pub fn default_model_config(&self) -> Option<(&ProviderConfig, &ModelConfig)> {
831        let default = self.default_model.as_ref()?;
832        let (provider_name, model_id) = default.split_once('/')?;
833        let provider = self.find_provider(provider_name)?;
834        let model = provider.find_model(model_id)?;
835        Some((provider, model))
836    }
837
838    /// Get LlmConfig for the default provider and model
839    ///
840    /// Returns None if default provider/model is not configured or API key is missing.
841    pub fn default_llm_config(&self) -> Option<LlmConfig> {
842        let (provider, model) = self.default_model_config()?;
843        let api_key = provider.get_api_key(model)?;
844        let base_url = provider.get_base_url(model);
845        let headers = provider.get_headers(model);
846        let session_id_header = provider.get_session_id_header(model);
847
848        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
849        if let Some(url) = base_url {
850            config = config.with_base_url(url);
851        }
852        if !headers.is_empty() {
853            config = config.with_headers(headers);
854        }
855        if let Some(header_name) = session_id_header {
856            config = config.with_session_id_header(header_name);
857        }
858        if let Some(timeout_ms) = self.llm_api_timeout_ms {
859            config = config.with_api_timeout(timeout_ms);
860        }
861        config = apply_model_caps(config, model, self.thinking_budget);
862        Some(config)
863    }
864
865    /// Get LlmConfig for a specific provider and model
866    ///
867    /// Returns None if provider/model is not found or API key is missing.
868    pub fn llm_config(&self, provider_name: &str, model_id: &str) -> Option<LlmConfig> {
869        let provider = self.find_provider(provider_name)?;
870        let model = provider.find_model(model_id)?;
871        let api_key = provider.get_api_key(model)?;
872        let base_url = provider.get_base_url(model);
873        let headers = provider.get_headers(model);
874        let session_id_header = provider.get_session_id_header(model);
875
876        let mut config = LlmConfig::new(&provider.name, &model.id, api_key);
877        if let Some(url) = base_url {
878            config = config.with_base_url(url);
879        }
880        if !headers.is_empty() {
881            config = config.with_headers(headers);
882        }
883        if let Some(header_name) = session_id_header {
884            config = config.with_session_id_header(header_name);
885        }
886        if let Some(timeout_ms) = self.llm_api_timeout_ms {
887            config = config.with_api_timeout(timeout_ms);
888        }
889        config = apply_model_caps(config, model, self.thinking_budget);
890        Some(config)
891    }
892
893    /// List all available models across all providers
894    pub fn list_models(&self) -> Vec<(&ProviderConfig, &ModelConfig)> {
895        self.providers
896            .iter()
897            .flat_map(|p| p.models.iter().map(move |m| (p, m)))
898            .collect()
899    }
900
901    /// Add a skill directory
902    pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
903        self.skill_dirs.push(dir.into());
904        self
905    }
906
907    /// Add an agent directory
908    pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
909        self.agent_dirs.push(dir.into());
910        self
911    }
912
913    /// Check if any directories are configured
914    pub fn has_directories(&self) -> bool {
915        !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
916    }
917
918    /// Check if provider configuration is available
919    pub fn has_providers(&self) -> bool {
920        !self.providers.is_empty()
921    }
922}