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