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
15fn 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
457impl CodeConfig {
462 pub fn new() -> Self {
464 Self::default()
465 }
466
467 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 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 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 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 pub fn find_provider(&self, name: &str) -> Option<&ProviderConfig> {
819 self.providers.iter().find(|p| p.name == name)
820 }
821
822 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 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 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 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 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 pub fn add_skill_dir(mut self, dir: impl Into<PathBuf>) -> Self {
903 self.skill_dirs.push(dir.into());
904 self
905 }
906
907 pub fn add_agent_dir(mut self, dir: impl Into<PathBuf>) -> Self {
909 self.agent_dirs.push(dir.into());
910 self
911 }
912
913 pub fn has_directories(&self) -> bool {
915 !self.skill_dirs.is_empty() || !self.agent_dirs.is_empty()
916 }
917
918 pub fn has_providers(&self) -> bool {
920 !self.providers.is_empty()
921 }
922}