riglr_config/
providers.rs

1//! External API provider configuration
2
3use crate::{ConfigError, ConfigResult};
4use serde::{Deserialize, Serialize};
5
6/// External API providers configuration
7#[derive(Debug, Clone, Default, Deserialize, Serialize)]
8pub struct ProvidersConfig {
9    // AI Providers
10    /// API key for Anthropic Claude
11    #[serde(default)]
12    pub anthropic_api_key: Option<String>,
13
14    /// API key for OpenAI
15    #[serde(default)]
16    pub openai_api_key: Option<String>,
17
18    /// API key for Groq
19    #[serde(default)]
20    pub groq_api_key: Option<String>,
21
22    /// API key for Perplexity AI
23    #[serde(default)]
24    pub perplexity_api_key: Option<String>,
25
26    // Blockchain Data Providers
27    /// API key for Alchemy
28    #[serde(default)]
29    pub alchemy_api_key: Option<String>,
30
31    /// API key for Infura
32    #[serde(default)]
33    pub infura_api_key: Option<String>,
34
35    /// API key for QuickNode
36    #[serde(default)]
37    pub quicknode_api_key: Option<String>,
38
39    /// API key for Moralis
40    #[serde(default)]
41    pub moralis_api_key: Option<String>,
42
43    // Cross-chain and DeFi
44    /// API key for LI.FI
45    #[serde(default)]
46    pub lifi_api_key: Option<String>,
47
48    /// API key for 1inch
49    #[serde(default)]
50    pub one_inch_api_key: Option<String>,
51
52    /// API key for 0x Protocol
53    #[serde(default)]
54    pub zerox_api_key: Option<String>,
55
56    // Market Data
57    /// API key for DexScreener
58    #[serde(default)]
59    pub dexscreener_api_key: Option<String>,
60
61    /// API key for CoinGecko
62    #[serde(default)]
63    pub coingecko_api_key: Option<String>,
64
65    /// API key for CoinMarketCap
66    #[serde(default)]
67    pub coinmarketcap_api_key: Option<String>,
68
69    /// API key for Pump.fun
70    #[serde(default)]
71    pub pump_api_key: Option<String>,
72
73    /// API URL for Pump.fun
74    #[serde(default)]
75    pub pump_api_url: Option<String>,
76
77    /// API URL for Jupiter aggregator
78    #[serde(default)]
79    pub jupiter_api_url: Option<String>,
80
81    // Social and Web Data
82    /// Bearer token for Twitter API
83    #[serde(default)]
84    pub twitter_bearer_token: Option<String>,
85
86    /// API key for Exa
87    #[serde(default)]
88    pub exa_api_key: Option<String>,
89
90    /// API key for Serper
91    #[serde(default)]
92    pub serper_api_key: Option<String>,
93
94    // News and Analytics
95    /// API key for LunarCrush
96    #[serde(default)]
97    pub lunarcrush_api_key: Option<String>,
98
99    /// API key for NewsAPI
100    #[serde(default)]
101    pub newsapi_key: Option<String>,
102
103    /// API key for CryptoPanic
104    #[serde(default)]
105    pub cryptopanic_key: Option<String>,
106
107    // Security and Risk Analysis Tools
108    /// API key for PocketUniverse
109    #[serde(default)]
110    pub pocket_universe_api_key: Option<String>,
111
112    /// API key for TrenchBot
113    #[serde(default)]
114    pub trenchbot_api_key: Option<String>,
115
116    /// API key for RugCheck
117    #[serde(default)]
118    pub rugcheck_api_key: Option<String>,
119
120    /// API key for TweetScout
121    #[serde(default)]
122    pub tweetscout_api_key: Option<String>,
123
124    /// API key for Faster100x
125    #[serde(default)]
126    pub faster100x_api_key: Option<String>,
127}
128
129impl ProvidersConfig {
130    /// Check if a specific AI provider is configured
131    pub fn has_ai_provider(&self, provider: AiProvider) -> bool {
132        match provider {
133            AiProvider::Anthropic => self.anthropic_api_key.is_some(),
134            AiProvider::OpenAI => self.openai_api_key.is_some(),
135            AiProvider::Groq => self.groq_api_key.is_some(),
136            AiProvider::Perplexity => self.perplexity_api_key.is_some(),
137        }
138    }
139
140    /// Get the API key for an AI provider
141    pub fn get_ai_key(&self, provider: AiProvider) -> Option<&str> {
142        match provider {
143            AiProvider::Anthropic => self.anthropic_api_key.as_deref(),
144            AiProvider::OpenAI => self.openai_api_key.as_deref(),
145            AiProvider::Groq => self.groq_api_key.as_deref(),
146            AiProvider::Perplexity => self.perplexity_api_key.as_deref(),
147        }
148    }
149
150    /// Check if a blockchain provider is configured
151    pub fn has_blockchain_provider(&self, provider: BlockchainProvider) -> bool {
152        match provider {
153            BlockchainProvider::Alchemy => self.alchemy_api_key.is_some(),
154            BlockchainProvider::Infura => self.infura_api_key.is_some(),
155            BlockchainProvider::QuickNode => self.quicknode_api_key.is_some(),
156            BlockchainProvider::Moralis => self.moralis_api_key.is_some(),
157        }
158    }
159
160    /// Get the API key for a blockchain provider
161    pub fn get_blockchain_key(&self, provider: BlockchainProvider) -> Option<&str> {
162        match provider {
163            BlockchainProvider::Alchemy => self.alchemy_api_key.as_deref(),
164            BlockchainProvider::Infura => self.infura_api_key.as_deref(),
165            BlockchainProvider::QuickNode => self.quicknode_api_key.as_deref(),
166            BlockchainProvider::Moralis => self.moralis_api_key.as_deref(),
167        }
168    }
169
170    /// Check if a data provider is configured
171    pub fn has_data_provider(&self, provider: DataProvider) -> bool {
172        match provider {
173            DataProvider::DexScreener => self.dexscreener_api_key.is_some(),
174            DataProvider::CoinGecko => self.coingecko_api_key.is_some(),
175            DataProvider::CoinMarketCap => self.coinmarketcap_api_key.is_some(),
176            DataProvider::Twitter => self.twitter_bearer_token.is_some(),
177            DataProvider::LunarCrush => self.lunarcrush_api_key.is_some(),
178        }
179    }
180
181    /// Validate API key formats and configurations
182    pub fn validate_config(&self) -> ConfigResult<()> {
183        // Validate API key formats
184        if let Some(ref key) = self.anthropic_api_key {
185            if key.is_empty() {
186                return Err(ConfigError::validation("ANTHROPIC_API_KEY cannot be empty"));
187            }
188        }
189
190        if let Some(ref token) = self.twitter_bearer_token {
191            if !token.is_empty() && !token.starts_with("Bearer ") {
192                return Err(ConfigError::validation(
193                    "TWITTER_BEARER_TOKEN must start with 'Bearer ' if it is set",
194                ));
195            }
196        }
197
198        // More validations can be added as needed
199        Ok(())
200    }
201}
202
203/// AI provider enumeration
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum AiProvider {
206    /// Anthropic Claude AI provider
207    Anthropic,
208    /// OpenAI provider
209    OpenAI,
210    /// Groq provider
211    Groq,
212    /// Perplexity AI provider
213    Perplexity,
214}
215
216/// Blockchain data provider enumeration
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum BlockchainProvider {
219    /// Alchemy blockchain data provider
220    Alchemy,
221    /// Infura blockchain infrastructure provider
222    Infura,
223    /// QuickNode blockchain infrastructure provider
224    QuickNode,
225    /// Moralis Web3 development platform
226    Moralis,
227}
228
229/// Data provider enumeration
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum DataProvider {
232    /// DexScreener DEX analytics provider
233    DexScreener,
234    /// CoinGecko cryptocurrency data provider
235    CoinGecko,
236    /// CoinMarketCap cryptocurrency market data provider
237    CoinMarketCap,
238    /// Twitter social media data provider
239    Twitter,
240    /// LunarCrush social analytics provider
241    LunarCrush,
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_providers_config_default() {
250        let config = ProvidersConfig::default();
251
252        // Test all fields are None by default
253        assert!(config.anthropic_api_key.is_none());
254        assert!(config.openai_api_key.is_none());
255        assert!(config.groq_api_key.is_none());
256        assert!(config.perplexity_api_key.is_none());
257        assert!(config.alchemy_api_key.is_none());
258        assert!(config.infura_api_key.is_none());
259        assert!(config.quicknode_api_key.is_none());
260        assert!(config.moralis_api_key.is_none());
261        assert!(config.lifi_api_key.is_none());
262        assert!(config.one_inch_api_key.is_none());
263        assert!(config.zerox_api_key.is_none());
264        assert!(config.dexscreener_api_key.is_none());
265        assert!(config.coingecko_api_key.is_none());
266        assert!(config.coinmarketcap_api_key.is_none());
267        assert!(config.pump_api_key.is_none());
268        assert!(config.twitter_bearer_token.is_none());
269        assert!(config.exa_api_key.is_none());
270        assert!(config.serper_api_key.is_none());
271        assert!(config.lunarcrush_api_key.is_none());
272        assert!(config.newsapi_key.is_none());
273        assert!(config.pocket_universe_api_key.is_none());
274        assert!(config.trenchbot_api_key.is_none());
275        assert!(config.rugcheck_api_key.is_none());
276        assert!(config.tweetscout_api_key.is_none());
277        assert!(config.faster100x_api_key.is_none());
278    }
279
280    #[test]
281    fn test_has_ai_provider_when_keys_are_none_should_return_false() {
282        let config = ProvidersConfig::default();
283
284        assert!(!config.has_ai_provider(AiProvider::Anthropic));
285        assert!(!config.has_ai_provider(AiProvider::OpenAI));
286        assert!(!config.has_ai_provider(AiProvider::Groq));
287        assert!(!config.has_ai_provider(AiProvider::Perplexity));
288    }
289
290    #[test]
291    fn test_has_ai_provider_when_keys_are_some_should_return_true() {
292        let config = ProvidersConfig {
293            anthropic_api_key: Some("test_anthropic_key".to_string()),
294            openai_api_key: Some("test_openai_key".to_string()),
295            groq_api_key: Some("test_groq_key".to_string()),
296            perplexity_api_key: Some("test_perplexity_key".to_string()),
297            ..Default::default()
298        };
299
300        assert!(config.has_ai_provider(AiProvider::Anthropic));
301        assert!(config.has_ai_provider(AiProvider::OpenAI));
302        assert!(config.has_ai_provider(AiProvider::Groq));
303        assert!(config.has_ai_provider(AiProvider::Perplexity));
304    }
305
306    #[test]
307    fn test_get_ai_key_when_keys_are_none_should_return_none() {
308        let config = ProvidersConfig::default();
309
310        assert!(config.get_ai_key(AiProvider::Anthropic).is_none());
311        assert!(config.get_ai_key(AiProvider::OpenAI).is_none());
312        assert!(config.get_ai_key(AiProvider::Groq).is_none());
313        assert!(config.get_ai_key(AiProvider::Perplexity).is_none());
314    }
315
316    #[test]
317    fn test_get_ai_key_when_keys_are_some_should_return_key() {
318        let config = ProvidersConfig {
319            anthropic_api_key: Some("test_anthropic_key".to_string()),
320            openai_api_key: Some("test_openai_key".to_string()),
321            groq_api_key: Some("test_groq_key".to_string()),
322            perplexity_api_key: Some("test_perplexity_key".to_string()),
323            ..Default::default()
324        };
325
326        assert_eq!(
327            config.get_ai_key(AiProvider::Anthropic),
328            Some("test_anthropic_key")
329        );
330        assert_eq!(
331            config.get_ai_key(AiProvider::OpenAI),
332            Some("test_openai_key")
333        );
334        assert_eq!(config.get_ai_key(AiProvider::Groq), Some("test_groq_key"));
335        assert_eq!(
336            config.get_ai_key(AiProvider::Perplexity),
337            Some("test_perplexity_key")
338        );
339    }
340
341    #[test]
342    fn test_has_blockchain_provider_when_keys_are_none_should_return_false() {
343        let config = ProvidersConfig::default();
344
345        assert!(!config.has_blockchain_provider(BlockchainProvider::Alchemy));
346        assert!(!config.has_blockchain_provider(BlockchainProvider::Infura));
347        assert!(!config.has_blockchain_provider(BlockchainProvider::QuickNode));
348        assert!(!config.has_blockchain_provider(BlockchainProvider::Moralis));
349    }
350
351    #[test]
352    fn test_has_blockchain_provider_when_keys_are_some_should_return_true() {
353        let config = ProvidersConfig {
354            alchemy_api_key: Some("test_alchemy_key".to_string()),
355            infura_api_key: Some("test_infura_key".to_string()),
356            quicknode_api_key: Some("test_quicknode_key".to_string()),
357            moralis_api_key: Some("test_moralis_key".to_string()),
358            ..Default::default()
359        };
360
361        assert!(config.has_blockchain_provider(BlockchainProvider::Alchemy));
362        assert!(config.has_blockchain_provider(BlockchainProvider::Infura));
363        assert!(config.has_blockchain_provider(BlockchainProvider::QuickNode));
364        assert!(config.has_blockchain_provider(BlockchainProvider::Moralis));
365    }
366
367    #[test]
368    fn test_get_blockchain_key_when_keys_are_none_should_return_none() {
369        let config = ProvidersConfig::default();
370
371        assert!(config
372            .get_blockchain_key(BlockchainProvider::Alchemy)
373            .is_none());
374        assert!(config
375            .get_blockchain_key(BlockchainProvider::Infura)
376            .is_none());
377        assert!(config
378            .get_blockchain_key(BlockchainProvider::QuickNode)
379            .is_none());
380        assert!(config
381            .get_blockchain_key(BlockchainProvider::Moralis)
382            .is_none());
383    }
384
385    #[test]
386    fn test_get_blockchain_key_when_keys_are_some_should_return_key() {
387        let config = ProvidersConfig {
388            alchemy_api_key: Some("test_alchemy_key".to_string()),
389            infura_api_key: Some("test_infura_key".to_string()),
390            quicknode_api_key: Some("test_quicknode_key".to_string()),
391            moralis_api_key: Some("test_moralis_key".to_string()),
392            ..Default::default()
393        };
394
395        assert_eq!(
396            config.get_blockchain_key(BlockchainProvider::Alchemy),
397            Some("test_alchemy_key")
398        );
399        assert_eq!(
400            config.get_blockchain_key(BlockchainProvider::Infura),
401            Some("test_infura_key")
402        );
403        assert_eq!(
404            config.get_blockchain_key(BlockchainProvider::QuickNode),
405            Some("test_quicknode_key")
406        );
407        assert_eq!(
408            config.get_blockchain_key(BlockchainProvider::Moralis),
409            Some("test_moralis_key")
410        );
411    }
412
413    #[test]
414    fn test_has_data_provider_when_keys_are_none_should_return_false() {
415        let config = ProvidersConfig::default();
416
417        assert!(!config.has_data_provider(DataProvider::DexScreener));
418        assert!(!config.has_data_provider(DataProvider::CoinGecko));
419        assert!(!config.has_data_provider(DataProvider::CoinMarketCap));
420        assert!(!config.has_data_provider(DataProvider::Twitter));
421        assert!(!config.has_data_provider(DataProvider::LunarCrush));
422    }
423
424    #[test]
425    fn test_has_data_provider_when_keys_are_some_should_return_true() {
426        let config = ProvidersConfig {
427            dexscreener_api_key: Some("test_dexscreener_key".to_string()),
428            coingecko_api_key: Some("test_coingecko_key".to_string()),
429            coinmarketcap_api_key: Some("test_coinmarketcap_key".to_string()),
430            twitter_bearer_token: Some("test_twitter_token".to_string()),
431            lunarcrush_api_key: Some("test_lunarcrush_key".to_string()),
432            ..Default::default()
433        };
434
435        assert!(config.has_data_provider(DataProvider::DexScreener));
436        assert!(config.has_data_provider(DataProvider::CoinGecko));
437        assert!(config.has_data_provider(DataProvider::CoinMarketCap));
438        assert!(config.has_data_provider(DataProvider::Twitter));
439        assert!(config.has_data_provider(DataProvider::LunarCrush));
440    }
441
442    #[test]
443    fn test_validate_when_all_keys_are_none_should_return_ok() {
444        let config = ProvidersConfig::default();
445        assert!(config.validate_config().is_ok());
446    }
447
448    #[test]
449    fn test_validate_when_anthropic_key_is_empty_should_return_err() {
450        let config = ProvidersConfig {
451            anthropic_api_key: Some("".to_string()),
452            ..Default::default()
453        };
454
455        let result = config.validate_config();
456        assert!(result.is_err());
457        if let Err(error) = result {
458            let error_message = format!("{}", error);
459            assert!(error_message.contains("ANTHROPIC_API_KEY cannot be empty"));
460        }
461    }
462
463    #[test]
464    fn test_validate_when_anthropic_key_is_valid_should_return_ok() {
465        let config = ProvidersConfig {
466            anthropic_api_key: Some("valid_key".to_string()),
467            ..Default::default()
468        };
469        assert!(config.validate_config().is_ok());
470    }
471
472    #[test]
473    fn test_validate_when_twitter_token_has_bearer_prefix_should_return_ok() {
474        let config = ProvidersConfig {
475            twitter_bearer_token: Some("Bearer valid_token".to_string()),
476            ..Default::default()
477        };
478        assert!(config.validate_config().is_ok());
479    }
480
481    #[test]
482    fn test_validate_when_twitter_token_missing_bearer_prefix_should_return_err() {
483        let config = ProvidersConfig {
484            twitter_bearer_token: Some("valid_token_without_bearer".to_string()),
485            ..Default::default()
486        };
487        // This should now return an error
488        let result = config.validate_config();
489        assert!(result.is_err());
490        if let Err(error) = result {
491            let error_message = format!("{}", error);
492            assert!(error_message.contains("TWITTER_BEARER_TOKEN must start with 'Bearer '"));
493        }
494    }
495
496    #[test]
497    fn test_validate_when_twitter_token_is_empty_should_return_ok() {
498        let config = ProvidersConfig {
499            twitter_bearer_token: Some("".to_string()),
500            ..Default::default()
501        };
502        assert!(config.validate_config().is_ok());
503    }
504
505    #[test]
506    fn test_ai_provider_debug_display() {
507        // Test Debug implementation
508        assert_eq!(format!("{:?}", AiProvider::Anthropic), "Anthropic");
509        assert_eq!(format!("{:?}", AiProvider::OpenAI), "OpenAI");
510        assert_eq!(format!("{:?}", AiProvider::Groq), "Groq");
511        assert_eq!(format!("{:?}", AiProvider::Perplexity), "Perplexity");
512    }
513
514    #[test]
515    fn test_ai_provider_clone_and_copy() {
516        let provider = AiProvider::Anthropic;
517        let cloned = provider.clone();
518        let copied = provider;
519
520        assert_eq!(provider, cloned);
521        assert_eq!(provider, copied);
522    }
523
524    #[test]
525    fn test_ai_provider_equality() {
526        assert_eq!(AiProvider::Anthropic, AiProvider::Anthropic);
527        assert_ne!(AiProvider::Anthropic, AiProvider::OpenAI);
528        assert_ne!(AiProvider::OpenAI, AiProvider::Groq);
529        assert_ne!(AiProvider::Groq, AiProvider::Perplexity);
530    }
531
532    #[test]
533    fn test_blockchain_provider_debug_display() {
534        assert_eq!(format!("{:?}", BlockchainProvider::Alchemy), "Alchemy");
535        assert_eq!(format!("{:?}", BlockchainProvider::Infura), "Infura");
536        assert_eq!(format!("{:?}", BlockchainProvider::QuickNode), "QuickNode");
537        assert_eq!(format!("{:?}", BlockchainProvider::Moralis), "Moralis");
538    }
539
540    #[test]
541    fn test_blockchain_provider_clone_and_copy() {
542        let provider = BlockchainProvider::Alchemy;
543        let cloned = provider.clone();
544        let copied = provider;
545
546        assert_eq!(provider, cloned);
547        assert_eq!(provider, copied);
548    }
549
550    #[test]
551    fn test_blockchain_provider_equality() {
552        assert_eq!(BlockchainProvider::Alchemy, BlockchainProvider::Alchemy);
553        assert_ne!(BlockchainProvider::Alchemy, BlockchainProvider::Infura);
554        assert_ne!(BlockchainProvider::Infura, BlockchainProvider::QuickNode);
555        assert_ne!(BlockchainProvider::QuickNode, BlockchainProvider::Moralis);
556    }
557
558    #[test]
559    fn test_data_provider_debug_display() {
560        assert_eq!(format!("{:?}", DataProvider::DexScreener), "DexScreener");
561        assert_eq!(format!("{:?}", DataProvider::CoinGecko), "CoinGecko");
562        assert_eq!(
563            format!("{:?}", DataProvider::CoinMarketCap),
564            "CoinMarketCap"
565        );
566        assert_eq!(format!("{:?}", DataProvider::Twitter), "Twitter");
567        assert_eq!(format!("{:?}", DataProvider::LunarCrush), "LunarCrush");
568    }
569
570    #[test]
571    fn test_data_provider_clone_and_copy() {
572        let provider = DataProvider::DexScreener;
573        let cloned = provider.clone();
574        let copied = provider;
575
576        assert_eq!(provider, cloned);
577        assert_eq!(provider, copied);
578    }
579
580    #[test]
581    fn test_data_provider_equality() {
582        assert_eq!(DataProvider::DexScreener, DataProvider::DexScreener);
583        assert_ne!(DataProvider::DexScreener, DataProvider::CoinGecko);
584        assert_ne!(DataProvider::CoinGecko, DataProvider::CoinMarketCap);
585        assert_ne!(DataProvider::Twitter, DataProvider::LunarCrush);
586    }
587
588    #[test]
589    fn test_providers_config_debug_display() {
590        let config = ProvidersConfig {
591            anthropic_api_key: Some("test_key".to_string()),
592            ..Default::default()
593        };
594        let debug_str = format!("{:?}", config);
595        assert!(debug_str.contains("ProvidersConfig"));
596        assert!(debug_str.contains("anthropic_api_key"));
597    }
598
599    #[test]
600    fn test_providers_config_clone() {
601        let config = ProvidersConfig {
602            anthropic_api_key: Some("test_key".to_string()),
603            openai_api_key: Some("openai_key".to_string()),
604            ..Default::default()
605        };
606        let cloned = config.clone();
607
608        assert_eq!(config.anthropic_api_key, cloned.anthropic_api_key);
609        assert_eq!(config.openai_api_key, cloned.openai_api_key);
610        assert_eq!(config.groq_api_key, cloned.groq_api_key);
611    }
612
613    #[test]
614    fn test_serde_serialization_deserialization() {
615        let config = ProvidersConfig {
616            anthropic_api_key: Some("anthropic_test".to_string()),
617            openai_api_key: Some("openai_test".to_string()),
618            twitter_bearer_token: Some("Bearer twitter_test".to_string()),
619            ..Default::default()
620        };
621
622        // Test serialization
623        let serialized = serde_json::to_string(&config).unwrap();
624        assert!(serialized.contains("anthropic_test"));
625        assert!(serialized.contains("openai_test"));
626        assert!(serialized.contains("Bearer twitter_test"));
627
628        // Test deserialization
629        let deserialized: ProvidersConfig = serde_json::from_str(&serialized).unwrap();
630        assert_eq!(config.anthropic_api_key, deserialized.anthropic_api_key);
631        assert_eq!(config.openai_api_key, deserialized.openai_api_key);
632        assert_eq!(
633            config.twitter_bearer_token,
634            deserialized.twitter_bearer_token
635        );
636    }
637
638    #[test]
639    fn test_serde_default_fields() {
640        // Test that fields are properly defaulted when not present in JSON
641        let json = "{}";
642        let config: ProvidersConfig = serde_json::from_str(json).unwrap();
643
644        assert!(config.anthropic_api_key.is_none());
645        assert!(config.openai_api_key.is_none());
646        assert!(config.groq_api_key.is_none());
647        assert!(config.perplexity_api_key.is_none());
648    }
649
650    #[test]
651    fn test_mixed_provider_configuration() {
652        let config = ProvidersConfig {
653            anthropic_api_key: Some("anthropic_key".to_string()),
654            alchemy_api_key: Some("alchemy_key".to_string()),
655            dexscreener_api_key: Some("dex_key".to_string()),
656            // Leave others as None
657            ..Default::default()
658        };
659
660        // Test AI providers
661        assert!(config.has_ai_provider(AiProvider::Anthropic));
662        assert!(!config.has_ai_provider(AiProvider::OpenAI));
663
664        // Test blockchain providers
665        assert!(config.has_blockchain_provider(BlockchainProvider::Alchemy));
666        assert!(!config.has_blockchain_provider(BlockchainProvider::Infura));
667
668        // Test data providers
669        assert!(config.has_data_provider(DataProvider::DexScreener));
670        assert!(!config.has_data_provider(DataProvider::CoinGecko));
671    }
672}