1use serde::{Deserialize, Serialize};
2
3pub const AGENT_DIR: &str = ".agent";
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
8#[serde(rename_all = "snake_case")]
9pub enum LlmProvider {
10 #[default]
11 Claude,
12 OpenAi,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct LlmConfig {
17 #[serde(default)]
18 pub provider: LlmProvider,
19 pub model: String,
20 pub max_tokens: usize,
21 pub requests_per_minute: u32,
22 pub tokens_per_minute: u32,
23 #[serde(default)]
24 pub api_key: Option<String>,
25 #[serde(default)]
26 pub api_base_url: Option<String>,
27
28 #[serde(default = "default_http_timeout_secs")]
30 pub http_timeout_secs: u64,
31 #[serde(default = "default_max_retries")]
33 pub max_retries: u32,
34 #[serde(default = "default_retry_base_delay_ms")]
36 pub retry_base_delay_ms: u64,
37}
38
39fn default_http_timeout_secs() -> u64 {
40 120
41}
42
43fn default_max_retries() -> u32 {
44 3
45}
46
47fn default_retry_base_delay_ms() -> u64 {
48 1000
49}
50
51impl LlmConfig {
52 pub fn resolve_api_key(&self) -> Option<String> {
53 if let Some(ref key) = self.api_key {
54 if !key.is_empty() {
55 return Some(key.clone());
56 }
57 }
58 let env_var = match self.provider {
59 LlmProvider::Claude => "ANTHROPIC_API_KEY",
60 LlmProvider::OpenAi => "OPENAI_API_KEY",
61 };
62 std::env::var(env_var).ok()
63 }
64
65 pub fn resolve_base_url(&self) -> String {
66 if let Some(ref url) = self.api_base_url {
67 if !url.is_empty() {
68 return url.clone();
69 }
70 }
71 let env_var = match self.provider {
72 LlmProvider::Claude => "ANTHROPIC_API_BASE_URL",
73 LlmProvider::OpenAi => "OPENAI_API_BASE_URL",
74 };
75 if let Ok(url) = std::env::var(env_var) {
76 if !url.is_empty() {
77 return url;
78 }
79 }
80 match self.provider {
81 LlmProvider::Claude => "https://api.anthropic.com".to_string(),
82 LlmProvider::OpenAi => "https://api.openai.com".to_string(),
83 }
84 }
85}
86
87impl Default for LlmConfig {
88 fn default() -> Self {
89 Self {
90 provider: LlmProvider::Claude,
91 model: "claude-sonnet-4-20250514".to_string(),
92 max_tokens: 4096,
93 requests_per_minute: 50,
94 tokens_per_minute: 80_000,
95 api_key: None,
96 api_base_url: None,
97 http_timeout_secs: default_http_timeout_secs(),
98 max_retries: default_max_retries(),
99 retry_base_delay_ms: default_retry_base_delay_ms(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AgentConfig {
106 pub max_parallel_agents: usize,
107 pub poll_interval_ms: u64,
108 pub max_task_retries: u32,
109 #[serde(default = "default_max_loop_iterations")]
110 pub max_loop_iterations: usize,
111 #[serde(default = "default_max_context_tokens")]
112 pub max_context_tokens: usize,
113 #[serde(default = "default_max_idle_cycles")]
115 pub max_idle_cycles: u32,
116 #[serde(default = "default_plan_approval_timeout_secs")]
118 pub plan_approval_timeout_secs: u64,
119 #[serde(default = "default_command_timeout_secs")]
121 pub command_timeout_secs: u64,
122}
123
124fn default_max_loop_iterations() -> usize {
125 50
126}
127
128fn default_max_context_tokens() -> usize {
129 200_000
130}
131
132fn default_max_idle_cycles() -> u32 {
133 50
134}
135
136fn default_plan_approval_timeout_secs() -> u64 {
137 300
138}
139
140fn default_command_timeout_secs() -> u64 {
141 30
142}
143
144impl Default for AgentConfig {
145 fn default() -> Self {
146 Self {
147 max_parallel_agents: 4,
148 poll_interval_ms: 200,
149 max_task_retries: 3,
150 max_loop_iterations: default_max_loop_iterations(),
151 max_context_tokens: default_max_context_tokens(),
152 max_idle_cycles: default_max_idle_cycles(),
153 plan_approval_timeout_secs: default_plan_approval_timeout_secs(),
154 command_timeout_secs: default_command_timeout_secs(),
155 }
156 }
157}