Skip to main content

bitrouter_config/
detect.rs

1use std::collections::HashMap;
2
3use crate::registry::builtin_provider_defs;
4
5/// A provider detected from environment variables.
6#[derive(Debug, Clone)]
7pub struct DetectedProvider {
8    /// Provider name (e.g. "openai").
9    pub name: String,
10    /// Environment variable prefix (e.g. "OPENAI").
11    pub env_prefix: String,
12    /// The API key variable name (e.g. "OPENAI_API_KEY").
13    pub api_key_var: String,
14    /// Whether `{PREFIX}_BASE_URL` is also set.
15    pub has_base_url: bool,
16}
17
18/// Scan an environment map for builtin providers that have API keys set.
19///
20/// Accepts an explicit env map for testability.
21pub fn detect_providers(env: &HashMap<String, String>) -> Vec<DetectedProvider> {
22    let defs = builtin_provider_defs();
23    let mut detected: Vec<DetectedProvider> = defs
24        .iter()
25        .filter_map(|(name, bp)| {
26            let prefix = bp.config.env_prefix.as_deref()?;
27            let key_var = format!("{prefix}_API_KEY");
28            let key_value = env.get(&key_var).map(|v| v.as_str()).unwrap_or("");
29            if key_value.is_empty() {
30                return None;
31            }
32            let base_var = format!("{prefix}_BASE_URL");
33            let has_base_url = env.get(&base_var).map(|v| !v.is_empty()).unwrap_or(false);
34            Some(DetectedProvider {
35                name: name.clone(),
36                env_prefix: prefix.to_owned(),
37                api_key_var: key_var,
38                has_base_url,
39            })
40        })
41        .collect();
42    detected.sort_by(|a, b| a.name.cmp(&b.name));
43    detected
44}
45
46/// Convenience wrapper using the process environment.
47pub fn detect_providers_from_env() -> Vec<DetectedProvider> {
48    let env: HashMap<String, String> = std::env::vars().collect();
49    detect_providers(&env)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn detects_provider_with_key() {
58        let env = HashMap::from([("OPENAI_API_KEY".into(), "sk-test".into())]);
59        let detected = detect_providers(&env);
60        assert_eq!(detected.len(), 1);
61        assert_eq!(detected[0].name, "openai");
62        assert_eq!(detected[0].api_key_var, "OPENAI_API_KEY");
63        assert!(!detected[0].has_base_url);
64    }
65
66    #[test]
67    fn detects_base_url() {
68        let env = HashMap::from([
69            ("OPENAI_API_KEY".into(), "sk-test".into()),
70            ("OPENAI_BASE_URL".into(), "https://proxy.com/v1".into()),
71        ]);
72        let detected = detect_providers(&env);
73        assert!(detected[0].has_base_url);
74    }
75
76    #[test]
77    fn empty_key_not_detected() {
78        let env = HashMap::from([("OPENAI_API_KEY".into(), "".into())]);
79        let detected = detect_providers(&env);
80        assert!(detected.is_empty());
81    }
82
83    #[test]
84    fn no_keys_returns_empty() {
85        let env = HashMap::new();
86        let detected = detect_providers(&env);
87        assert!(detected.is_empty());
88    }
89
90    #[test]
91    fn multiple_providers() {
92        let env = HashMap::from([
93            ("OPENAI_API_KEY".into(), "sk-test".into()),
94            ("ANTHROPIC_API_KEY".into(), "sk-ant-test".into()),
95            ("GOOGLE_API_KEY".into(), "goog-test".into()),
96        ]);
97        let detected = detect_providers(&env);
98        assert_eq!(detected.len(), 3);
99        // sorted alphabetically
100        assert_eq!(detected[0].name, "anthropic");
101        assert_eq!(detected[1].name, "google");
102        assert_eq!(detected[2].name, "openai");
103    }
104}