Skip to main content

agent_diva_core/config/
schema.rs

1//! Configuration schema definitions
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Root configuration for agent-diva
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Config {
9    /// Agent configuration
10    pub agents: AgentsConfig,
11    /// Channel configuration
12    pub channels: ChannelsConfig,
13    /// Provider configuration
14    pub providers: ProvidersConfig,
15    /// Gateway configuration
16    pub gateway: GatewayConfig,
17    /// Tools configuration
18    pub tools: ToolsConfig,
19    /// Logging configuration
20    #[serde(default)]
21    pub logging: LoggingConfig,
22}
23
24/// Logging configuration
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct LoggingConfig {
27    /// Default log level (trace, debug, info, warn, error)
28    #[serde(default = "default_log_level")]
29    pub level: String,
30    /// Log format (text, json)
31    #[serde(default = "default_log_format")]
32    pub format: String,
33    /// Directory for log files
34    #[serde(default = "default_log_dir")]
35    pub dir: String,
36    /// Module-specific overrides
37    #[serde(default)]
38    pub overrides: HashMap<String, String>,
39}
40
41fn default_log_level() -> String {
42    "info".to_string()
43}
44
45fn default_log_format() -> String {
46    "text".to_string()
47}
48
49fn default_log_dir() -> String {
50    "logs".to_string()
51}
52
53impl Default for LoggingConfig {
54    fn default() -> Self {
55        Self {
56            level: default_log_level(),
57            format: default_log_format(),
58            dir: default_log_dir(),
59            overrides: HashMap::new(),
60        }
61    }
62}
63
64/// Agent configuration
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66pub struct AgentsConfig {
67    /// Default agent settings
68    pub defaults: AgentDefaults,
69    /// Soul and identity behavior
70    #[serde(default)]
71    pub soul: AgentSoulConfig,
72}
73
74/// Default agent settings
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AgentDefaults {
77    /// Workspace directory
78    pub workspace: String,
79    /// Explicit default provider selection
80    #[serde(default)]
81    pub provider: Option<String>,
82    /// Default model
83    pub model: String,
84    /// Maximum tokens
85    pub max_tokens: u32,
86    /// Temperature
87    pub temperature: f32,
88    /// Maximum tool iterations
89    pub max_tool_iterations: u32,
90    /// Optional reasoning effort for thinking-capable models (low/medium/high)
91    #[serde(default)]
92    pub reasoning_effort: Option<String>,
93}
94
95impl Default for AgentDefaults {
96    fn default() -> Self {
97        Self {
98            workspace: "~/.agent-diva/workspace".to_string(),
99            provider: Some("deepseek".to_string()),
100            model: "deepseek-chat".to_string(),
101            max_tokens: 8192,
102            temperature: 0.7,
103            max_tool_iterations: 20,
104            reasoning_effort: None,
105        }
106    }
107}
108
109/// Soul/identity settings
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct AgentSoulConfig {
112    /// Whether soul context injection is enabled.
113    #[serde(default = "default_true")]
114    pub enabled: bool,
115    /// Max characters loaded from each soul markdown file.
116    #[serde(default = "default_soul_max_chars")]
117    pub max_chars: usize,
118    /// Whether to notify user when soul files are updated.
119    #[serde(default = "default_true")]
120    pub notify_on_change: bool,
121    /// If true, BOOTSTRAP.md is only used until bootstrap is completed.
122    #[serde(default = "default_true")]
123    pub bootstrap_once: bool,
124    /// Rolling window in seconds for frequent soul-change hints.
125    #[serde(default = "default_soul_window_secs")]
126    pub frequent_change_window_secs: u64,
127    /// Minimum soul-changing turns in window to trigger hints.
128    #[serde(default = "default_soul_change_threshold")]
129    pub frequent_change_threshold: usize,
130    /// Add boundary confirmation hint when SOUL.md changes.
131    #[serde(default = "default_true")]
132    pub boundary_confirmation_hint: bool,
133}
134
135impl Default for AgentSoulConfig {
136    fn default() -> Self {
137        Self {
138            enabled: true,
139            max_chars: 4000,
140            notify_on_change: true,
141            bootstrap_once: true,
142            frequent_change_window_secs: 600,
143            frequent_change_threshold: 3,
144            boundary_confirmation_hint: true,
145        }
146    }
147}
148
149fn default_soul_max_chars() -> usize {
150    4000
151}
152
153fn default_soul_window_secs() -> u64 {
154    600
155}
156
157fn default_soul_change_threshold() -> usize {
158    3
159}
160
161/// Channel configuration
162#[derive(Debug, Clone, Serialize, Deserialize, Default)]
163pub struct ChannelsConfig {
164    #[serde(default)]
165    pub telegram: TelegramConfig,
166    #[serde(default)]
167    pub discord: DiscordConfig,
168    #[serde(default)]
169    pub whatsapp: WhatsAppConfig,
170    #[serde(default)]
171    pub feishu: FeishuConfig,
172    #[serde(default)]
173    pub dingtalk: DingTalkConfig,
174    #[serde(default)]
175    pub email: EmailConfig,
176    #[serde(default)]
177    pub slack: SlackConfig,
178    #[serde(default)]
179    pub qq: QQConfig,
180    #[serde(default)]
181    pub matrix: MatrixConfig,
182    #[serde(
183        default,
184        rename = "neuro-link",
185        alias = "neuro_link",
186        alias = "generic_pipe"
187    )]
188    pub neuro_link: NeuroLinkConfig,
189    #[serde(default)]
190    pub irc: IrcConfig,
191    #[serde(default)]
192    pub mattermost: MattermostConfig,
193    #[serde(default)]
194    pub nextcloud_talk: NextcloudTalkConfig,
195}
196
197/// Telegram channel configuration
198#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct TelegramConfig {
200    #[serde(default)]
201    pub enabled: bool,
202    #[serde(default)]
203    pub token: String,
204    #[serde(default)]
205    pub allow_from: Vec<String>,
206    #[serde(default)]
207    pub proxy: Option<String>,
208}
209
210/// Discord channel configuration
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct DiscordConfig {
213    #[serde(default)]
214    pub enabled: bool,
215    #[serde(default)]
216    pub token: String,
217    #[serde(default)]
218    pub allow_from: Vec<String>,
219    #[serde(default = "default_discord_gateway")]
220    pub gateway_url: String,
221    #[serde(default = "default_discord_intents")]
222    pub intents: u64,
223}
224
225fn default_discord_gateway() -> String {
226    "wss://gateway.discord.gg/?v=10&encoding=json".to_string()
227}
228
229fn default_discord_intents() -> u64 {
230    37377 // GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT
231}
232
233impl Default for DiscordConfig {
234    fn default() -> Self {
235        Self {
236            enabled: false,
237            token: String::new(),
238            allow_from: Vec::new(),
239            gateway_url: default_discord_gateway(),
240            intents: default_discord_intents(),
241        }
242    }
243}
244
245/// WhatsApp channel configuration
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct WhatsAppConfig {
248    #[serde(default)]
249    pub enabled: bool,
250    #[serde(default = "default_whatsapp_bridge")]
251    pub bridge_url: String,
252    #[serde(default)]
253    pub allow_from: Vec<String>,
254}
255
256fn default_whatsapp_bridge() -> String {
257    "ws://localhost:3001".to_string()
258}
259
260impl Default for WhatsAppConfig {
261    fn default() -> Self {
262        Self {
263            enabled: false,
264            bridge_url: default_whatsapp_bridge(),
265            allow_from: Vec::new(),
266        }
267    }
268}
269
270/// Feishu/Lark channel configuration
271#[derive(Debug, Clone, Serialize, Deserialize, Default)]
272pub struct FeishuConfig {
273    #[serde(default)]
274    pub enabled: bool,
275    #[serde(default)]
276    pub app_id: String,
277    #[serde(default)]
278    pub app_secret: String,
279    #[serde(default)]
280    pub encrypt_key: String,
281    #[serde(default)]
282    pub verification_token: String,
283    #[serde(default)]
284    pub allow_from: Vec<String>,
285}
286
287/// DingTalk channel configuration
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct DingTalkConfig {
290    #[serde(default)]
291    pub enabled: bool,
292    #[serde(default)]
293    pub client_id: String,
294    #[serde(default)]
295    pub client_secret: String,
296    #[serde(default)]
297    pub robot_code: String,
298    #[serde(default = "default_dingtalk_policy")]
299    pub dm_policy: String,
300    #[serde(default = "default_dingtalk_policy")]
301    pub group_policy: String,
302    #[serde(default)]
303    pub allow_from: Vec<String>,
304}
305
306fn default_dingtalk_policy() -> String {
307    "open".to_string()
308}
309
310impl Default for DingTalkConfig {
311    fn default() -> Self {
312        Self {
313            enabled: false,
314            client_id: String::new(),
315            client_secret: String::new(),
316            robot_code: String::new(),
317            dm_policy: default_dingtalk_policy(),
318            group_policy: default_dingtalk_policy(),
319            allow_from: Vec::new(),
320        }
321    }
322}
323
324/// Email channel configuration
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct EmailConfig {
327    #[serde(default)]
328    pub enabled: bool,
329    #[serde(default)]
330    pub consent_granted: bool,
331    // IMAP settings
332    #[serde(default)]
333    pub imap_host: String,
334    #[serde(default = "default_imap_port")]
335    pub imap_port: u16,
336    #[serde(default)]
337    pub imap_username: String,
338    #[serde(default)]
339    pub imap_password: String,
340    #[serde(default = "default_imap_mailbox")]
341    pub imap_mailbox: String,
342    #[serde(default = "default_true")]
343    pub imap_use_ssl: bool,
344    // SMTP settings
345    #[serde(default)]
346    pub smtp_host: String,
347    #[serde(default = "default_smtp_port")]
348    pub smtp_port: u16,
349    #[serde(default)]
350    pub smtp_username: String,
351    #[serde(default)]
352    pub smtp_password: String,
353    #[serde(default = "default_true")]
354    pub smtp_use_tls: bool,
355    #[serde(default)]
356    pub smtp_use_ssl: bool,
357    #[serde(default)]
358    pub from_address: String,
359    // Behavior
360    #[serde(default = "default_true")]
361    pub auto_reply_enabled: bool,
362    #[serde(default = "default_poll_interval")]
363    pub poll_interval_seconds: u64,
364    #[serde(default = "default_true")]
365    pub mark_seen: bool,
366    #[serde(default = "default_max_body")]
367    pub max_body_chars: usize,
368    #[serde(default = "default_subject_prefix")]
369    pub subject_prefix: String,
370    #[serde(default)]
371    pub allow_from: Vec<String>,
372}
373
374fn default_imap_port() -> u16 {
375    993
376}
377fn default_imap_mailbox() -> String {
378    "INBOX".to_string()
379}
380fn default_smtp_port() -> u16 {
381    587
382}
383fn default_poll_interval() -> u64 {
384    30
385}
386fn default_max_body() -> usize {
387    12000
388}
389fn default_subject_prefix() -> String {
390    "Re: ".to_string()
391}
392fn default_true() -> bool {
393    true
394}
395
396impl Default for EmailConfig {
397    fn default() -> Self {
398        Self {
399            enabled: false,
400            consent_granted: false,
401            imap_host: String::new(),
402            imap_port: default_imap_port(),
403            imap_username: String::new(),
404            imap_password: String::new(),
405            imap_mailbox: default_imap_mailbox(),
406            imap_use_ssl: true,
407            smtp_host: String::new(),
408            smtp_port: default_smtp_port(),
409            smtp_username: String::new(),
410            smtp_password: String::new(),
411            smtp_use_tls: true,
412            smtp_use_ssl: false,
413            from_address: String::new(),
414            auto_reply_enabled: true,
415            poll_interval_seconds: default_poll_interval(),
416            mark_seen: true,
417            max_body_chars: default_max_body(),
418            subject_prefix: default_subject_prefix(),
419            allow_from: Vec::new(),
420        }
421    }
422}
423
424/// Slack channel configuration
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct SlackConfig {
427    #[serde(default)]
428    pub enabled: bool,
429    #[serde(default = "default_slack_mode")]
430    pub mode: String,
431    #[serde(default)]
432    pub webhook_path: String,
433    #[serde(default)]
434    pub bot_token: String,
435    #[serde(default)]
436    pub app_token: String,
437    #[serde(default = "default_true")]
438    pub user_token_read_only: bool,
439    #[serde(default = "default_slack_policy")]
440    pub group_policy: String,
441    #[serde(default)]
442    pub group_allow_from: Vec<String>,
443    #[serde(default)]
444    pub dm: SlackDMConfig,
445}
446
447fn default_slack_mode() -> String {
448    "socket".to_string()
449}
450fn default_slack_policy() -> String {
451    "mention".to_string()
452}
453
454impl Default for SlackConfig {
455    fn default() -> Self {
456        Self {
457            enabled: false,
458            mode: default_slack_mode(),
459            webhook_path: "/slack/events".to_string(),
460            bot_token: String::new(),
461            app_token: String::new(),
462            user_token_read_only: true,
463            group_policy: default_slack_policy(),
464            group_allow_from: Vec::new(),
465            dm: SlackDMConfig::default(),
466        }
467    }
468}
469
470/// Slack DM configuration
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct SlackDMConfig {
473    #[serde(default = "default_true")]
474    pub enabled: bool,
475    #[serde(default = "default_slack_dm_policy")]
476    pub policy: String,
477    #[serde(default)]
478    pub allow_from: Vec<String>,
479}
480
481fn default_slack_dm_policy() -> String {
482    "open".to_string()
483}
484
485impl Default for SlackDMConfig {
486    fn default() -> Self {
487        Self {
488            enabled: true,
489            policy: default_slack_dm_policy(),
490            allow_from: Vec::new(),
491        }
492    }
493}
494
495/// QQ channel configuration
496#[derive(Debug, Clone, Serialize, Deserialize, Default)]
497pub struct QQConfig {
498    #[serde(default)]
499    pub enabled: bool,
500    #[serde(default)]
501    pub app_id: String,
502    #[serde(default)]
503    pub secret: String,
504    #[serde(default)]
505    pub allow_from: Vec<String>,
506}
507
508/// Neuro-link (WebSocket server) channel configuration
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct NeuroLinkConfig {
511    #[serde(default)]
512    pub enabled: bool,
513    #[serde(default = "default_pipe_host")]
514    pub host: String,
515    #[serde(default = "default_pipe_port")]
516    pub port: u16,
517    #[serde(default)]
518    pub allow_from: Vec<String>,
519}
520
521fn default_pipe_host() -> String {
522    "0.0.0.0".to_string()
523}
524fn default_pipe_port() -> u16 {
525    9100
526}
527
528impl Default for NeuroLinkConfig {
529    fn default() -> Self {
530        Self {
531            enabled: false,
532            host: default_pipe_host(),
533            port: default_pipe_port(),
534            allow_from: Vec::new(),
535        }
536    }
537}
538
539/// Matrix channel configuration
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct MatrixConfig {
542    #[serde(default)]
543    pub enabled: bool,
544    #[serde(default = "default_matrix_homeserver")]
545    pub homeserver: String,
546    #[serde(default)]
547    pub user_id: String,
548    #[serde(default)]
549    pub access_token: String,
550    #[serde(default)]
551    pub device_id: String,
552    #[serde(default = "default_true")]
553    pub e2ee_enabled: bool,
554    #[serde(default = "default_matrix_media_limit")]
555    pub max_media_bytes: usize,
556    #[serde(default)]
557    pub allow_from: Vec<String>,
558    #[serde(default)]
559    pub group_allow_from: Vec<String>,
560    #[serde(default = "default_matrix_sync_timeout")]
561    pub sync_timeout_ms: u64,
562    #[serde(default = "default_matrix_sync_stop_grace")]
563    pub sync_stop_grace_seconds: u64,
564}
565
566fn default_matrix_homeserver() -> String {
567    "https://matrix.org".to_string()
568}
569fn default_matrix_media_limit() -> usize {
570    20 * 1024 * 1024
571}
572fn default_matrix_sync_timeout() -> u64 {
573    30_000
574}
575fn default_matrix_sync_stop_grace() -> u64 {
576    8
577}
578
579impl Default for MatrixConfig {
580    fn default() -> Self {
581        Self {
582            enabled: false,
583            homeserver: default_matrix_homeserver(),
584            user_id: String::new(),
585            access_token: String::new(),
586            device_id: String::new(),
587            e2ee_enabled: true,
588            max_media_bytes: default_matrix_media_limit(),
589            allow_from: Vec::new(),
590            group_allow_from: Vec::new(),
591            sync_timeout_ms: default_matrix_sync_timeout(),
592            sync_stop_grace_seconds: default_matrix_sync_stop_grace(),
593        }
594    }
595}
596
597/// IRC channel configuration
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct IrcConfig {
600    #[serde(default)]
601    pub enabled: bool,
602    #[serde(default)]
603    pub server: String,
604    #[serde(default = "default_irc_port")]
605    pub port: u16,
606    #[serde(default)]
607    pub nickname: String,
608    #[serde(default)]
609    pub username: String,
610    #[serde(default)]
611    pub channels: Vec<String>,
612    #[serde(default)]
613    pub server_password: Option<String>,
614    #[serde(default)]
615    pub nickserv_password: Option<String>,
616    #[serde(default)]
617    pub sasl_password: Option<String>,
618    #[serde(default = "default_true")]
619    pub use_tls: bool,
620    #[serde(default = "default_true")]
621    pub verify_tls: bool,
622    #[serde(default)]
623    pub allow_from: Vec<String>,
624}
625
626fn default_irc_port() -> u16 {
627    6697
628}
629
630impl Default for IrcConfig {
631    fn default() -> Self {
632        Self {
633            enabled: false,
634            server: String::new(),
635            port: default_irc_port(),
636            nickname: String::new(),
637            username: String::new(),
638            channels: Vec::new(),
639            server_password: None,
640            nickserv_password: None,
641            sasl_password: None,
642            use_tls: true,
643            verify_tls: true,
644            allow_from: Vec::new(),
645        }
646    }
647}
648
649/// Mattermost channel configuration
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct MattermostConfig {
652    #[serde(default)]
653    pub enabled: bool,
654    #[serde(default)]
655    pub base_url: String,
656    #[serde(default)]
657    pub bot_token: String,
658    #[serde(default)]
659    pub channel_id: String,
660    #[serde(default = "default_true")]
661    pub thread_replies: bool,
662    #[serde(default)]
663    pub mention_only: bool,
664    #[serde(default = "default_mm_poll_interval")]
665    pub poll_interval_seconds: u64,
666    #[serde(default)]
667    pub allow_from: Vec<String>,
668}
669
670fn default_mm_poll_interval() -> u64 {
671    3
672}
673
674impl Default for MattermostConfig {
675    fn default() -> Self {
676        Self {
677            enabled: false,
678            base_url: String::new(),
679            bot_token: String::new(),
680            channel_id: String::new(),
681            thread_replies: true,
682            mention_only: false,
683            poll_interval_seconds: default_mm_poll_interval(),
684            allow_from: Vec::new(),
685        }
686    }
687}
688
689/// Nextcloud Talk channel configuration
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct NextcloudTalkConfig {
692    #[serde(default)]
693    pub enabled: bool,
694    #[serde(default)]
695    pub base_url: String,
696    #[serde(default)]
697    pub app_token: String,
698    #[serde(default)]
699    pub room_token: String,
700    #[serde(default = "default_nc_poll_interval")]
701    pub poll_interval_seconds: u64,
702    #[serde(default)]
703    pub allow_from: Vec<String>,
704}
705
706fn default_nc_poll_interval() -> u64 {
707    5
708}
709
710impl Default for NextcloudTalkConfig {
711    fn default() -> Self {
712        Self {
713            enabled: false,
714            base_url: String::new(),
715            app_token: String::new(),
716            room_token: String::new(),
717            poll_interval_seconds: default_nc_poll_interval(),
718            allow_from: Vec::new(),
719        }
720    }
721}
722
723/// Provider configuration
724#[derive(Debug, Clone, Serialize, Deserialize, Default)]
725pub struct ProvidersConfig {
726    #[serde(default)]
727    pub anthropic: ProviderConfig,
728    #[serde(default)]
729    pub openai: ProviderConfig,
730    #[serde(default)]
731    pub openrouter: ProviderConfig,
732    #[serde(default)]
733    pub deepseek: ProviderConfig,
734    #[serde(default)]
735    pub groq: ProviderConfig,
736    #[serde(default)]
737    pub zhipu: ProviderConfig,
738    #[serde(default)]
739    pub dashscope: ProviderConfig,
740    #[serde(default)]
741    pub vllm: ProviderConfig,
742    #[serde(default)]
743    pub gemini: ProviderConfig,
744    #[serde(default)]
745    pub moonshot: ProviderConfig,
746    #[serde(default)]
747    pub minimax: ProviderConfig,
748    #[serde(default)]
749    pub aihubmix: ProviderConfig,
750    #[serde(default)]
751    pub custom: ProviderConfig,
752    #[serde(default)]
753    pub custom_providers: HashMap<String, CustomProviderConfig>,
754}
755
756/// Individual provider configuration
757#[derive(Debug, Clone, Serialize, Deserialize, Default)]
758pub struct ProviderConfig {
759    #[serde(default)]
760    pub api_key: String,
761    #[serde(default)]
762    pub api_base: Option<String>,
763    #[serde(default)]
764    pub extra_headers: Option<HashMap<String, String>>,
765    #[serde(default)]
766    pub custom_models: Vec<String>,
767}
768
769/// User-defined provider configuration.
770#[derive(Debug, Clone, Serialize, Deserialize, Default)]
771pub struct CustomProviderConfig {
772    #[serde(default)]
773    pub display_name: String,
774    #[serde(default = "default_custom_provider_api_type")]
775    pub api_type: String,
776    #[serde(default)]
777    pub api_key: String,
778    #[serde(default)]
779    pub api_base: Option<String>,
780    #[serde(default)]
781    pub default_model: Option<String>,
782    #[serde(default)]
783    pub models: Vec<String>,
784    #[serde(default)]
785    pub extra_headers: Option<HashMap<String, String>>,
786}
787
788fn default_custom_provider_api_type() -> String {
789    "openai".to_string()
790}
791
792impl ProvidersConfig {
793    pub const BUILTIN_PROVIDER_IDS: [&'static str; 13] = [
794        "anthropic",
795        "openai",
796        "openrouter",
797        "deepseek",
798        "groq",
799        "zhipu",
800        "dashscope",
801        "vllm",
802        "gemini",
803        "moonshot",
804        "minimax",
805        "aihubmix",
806        "custom",
807    ];
808
809    pub fn builtin_provider_names() -> &'static [&'static str] {
810        &Self::BUILTIN_PROVIDER_IDS
811    }
812
813    pub fn get(&self, name: &str) -> Option<&ProviderConfig> {
814        match name {
815            "anthropic" => Some(&self.anthropic),
816            "openai" => Some(&self.openai),
817            "openrouter" => Some(&self.openrouter),
818            "deepseek" => Some(&self.deepseek),
819            "groq" => Some(&self.groq),
820            "zhipu" => Some(&self.zhipu),
821            "dashscope" => Some(&self.dashscope),
822            "vllm" => Some(&self.vllm),
823            "gemini" => Some(&self.gemini),
824            "moonshot" => Some(&self.moonshot),
825            "minimax" => Some(&self.minimax),
826            "aihubmix" => Some(&self.aihubmix),
827            "custom" => Some(&self.custom),
828            _ => None,
829        }
830    }
831
832    pub fn get_mut(&mut self, name: &str) -> Option<&mut ProviderConfig> {
833        match name {
834            "anthropic" => Some(&mut self.anthropic),
835            "openai" => Some(&mut self.openai),
836            "openrouter" => Some(&mut self.openrouter),
837            "deepseek" => Some(&mut self.deepseek),
838            "groq" => Some(&mut self.groq),
839            "zhipu" => Some(&mut self.zhipu),
840            "dashscope" => Some(&mut self.dashscope),
841            "vllm" => Some(&mut self.vllm),
842            "gemini" => Some(&mut self.gemini),
843            "moonshot" => Some(&mut self.moonshot),
844            "minimax" => Some(&mut self.minimax),
845            "aihubmix" => Some(&mut self.aihubmix),
846            "custom" => Some(&mut self.custom),
847            _ => None,
848        }
849    }
850
851    pub fn get_custom(&self, name: &str) -> Option<&CustomProviderConfig> {
852        self.custom_providers.get(name)
853    }
854
855    pub fn get_custom_mut(&mut self, name: &str) -> Option<&mut CustomProviderConfig> {
856        self.custom_providers.get_mut(name)
857    }
858
859    pub fn is_builtin_provider(name: &str) -> bool {
860        Self::BUILTIN_PROVIDER_IDS.contains(&name)
861    }
862}
863
864/// Gateway configuration
865#[derive(Debug, Clone, Serialize, Deserialize)]
866pub struct GatewayConfig {
867    #[serde(default = "default_host")]
868    pub host: String,
869    #[serde(default = "default_port")]
870    pub port: u16,
871}
872
873fn default_host() -> String {
874    "0.0.0.0".to_string()
875}
876fn default_port() -> u16 {
877    18790
878}
879
880impl Default for GatewayConfig {
881    fn default() -> Self {
882        Self {
883            host: default_host(),
884            port: default_port(),
885        }
886    }
887}
888
889/// Tools configuration
890#[derive(Debug, Clone, Serialize, Deserialize, Default)]
891pub struct ToolsConfig {
892    #[serde(default)]
893    pub web: WebToolsConfig,
894    #[serde(default)]
895    pub exec: ExecToolConfig,
896    #[serde(default)]
897    pub restrict_to_workspace: bool,
898    #[serde(default, rename = "mcpServers", alias = "mcp_servers")]
899    pub mcp_servers: HashMap<String, MCPServerConfig>,
900    #[serde(default, rename = "mcpManager", alias = "mcp_manager")]
901    pub mcp_manager: MCPManagerConfig,
902}
903
904#[derive(Debug, Clone, Serialize, Deserialize, Default)]
905pub struct MCPManagerConfig {
906    #[serde(default)]
907    pub disabled_servers: Vec<String>,
908}
909
910impl ToolsConfig {
911    pub fn active_mcp_servers(&self) -> HashMap<String, MCPServerConfig> {
912        self.mcp_servers
913            .iter()
914            .filter(|(name, _)| !self.is_mcp_server_disabled(name))
915            .map(|(name, cfg)| (name.clone(), cfg.clone()))
916            .collect()
917    }
918
919    pub fn is_mcp_server_disabled(&self, name: &str) -> bool {
920        self.mcp_manager
921            .disabled_servers
922            .iter()
923            .any(|server| server == name)
924    }
925}
926
927/// MCP server connection configuration (stdio or HTTP)
928#[derive(Debug, Clone, Serialize, Deserialize, Default)]
929pub struct MCPServerConfig {
930    #[serde(default)]
931    pub command: String,
932    #[serde(default)]
933    pub args: Vec<String>,
934    #[serde(default)]
935    pub env: HashMap<String, String>,
936    #[serde(default)]
937    pub url: String,
938    /// Per-tool timeout in seconds (default: 30)
939    #[serde(default = "default_tool_timeout")]
940    pub tool_timeout: u64,
941}
942
943fn default_tool_timeout() -> u64 {
944    30
945}
946
947/// Web tools configuration
948#[derive(Debug, Clone, Serialize, Deserialize, Default)]
949pub struct WebToolsConfig {
950    #[serde(default)]
951    pub search: WebSearchConfig,
952    #[serde(default)]
953    pub fetch: WebFetchConfig,
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct WebFetchConfig {
958    #[serde(default = "default_enabled")]
959    pub enabled: bool,
960}
961
962/// Web search configuration
963#[derive(Debug, Clone, Serialize, Deserialize)]
964pub struct WebSearchConfig {
965    #[serde(default = "default_search_provider")]
966    pub provider: String,
967    #[serde(default = "default_enabled")]
968    pub enabled: bool,
969    #[serde(default)]
970    pub api_key: String,
971    #[serde(default = "default_max_results")]
972    pub max_results: u32,
973}
974
975fn default_search_provider() -> String {
976    "bocha".to_string()
977}
978
979fn default_enabled() -> bool {
980    true
981}
982
983fn default_max_results() -> u32 {
984    5
985}
986
987impl Default for WebSearchConfig {
988    fn default() -> Self {
989        Self {
990            provider: default_search_provider(),
991            enabled: default_enabled(),
992            api_key: String::new(),
993            max_results: default_max_results(),
994        }
995    }
996}
997
998impl Default for WebFetchConfig {
999    fn default() -> Self {
1000        Self {
1001            enabled: default_enabled(),
1002        }
1003    }
1004}
1005
1006/// Exec tool configuration
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1008pub struct ExecToolConfig {
1009    #[serde(default = "default_timeout")]
1010    pub timeout: u64,
1011}
1012
1013fn default_timeout() -> u64 {
1014    60
1015}
1016
1017impl Default for ExecToolConfig {
1018    fn default() -> Self {
1019        Self {
1020            timeout: default_timeout(),
1021        }
1022    }
1023}