oxios_kernel/
credential.rs1use anyhow::Result;
9
10#[derive(Debug, Clone)]
12pub enum CredentialSource {
13 Config,
15 OxiAuthStore,
17 EnvVar,
19}
20
21pub struct CredentialStore;
23
24impl CredentialStore {
25 pub fn resolve(provider: &str, config_key: Option<&str>) -> Option<(String, CredentialSource)> {
30 let env_var = format!("OXIOS_{}_API_KEY", provider.to_uppercase());
32 if let Ok(key) = std::env::var(&env_var) {
33 if !key.is_empty() {
34 return Some((key, CredentialSource::EnvVar));
35 }
36 }
37
38 if let Some(key) = config_key {
40 if !key.is_empty() {
41 return Some((key.to_string(), CredentialSource::Config));
42 }
43 }
44
45 if let Ok(Some(token)) = oxi_sdk::load_token(provider) {
47 if !token.access_token.is_empty() {
48 return Some((token.access_token, CredentialSource::OxiAuthStore));
49 }
50 }
51
52 if let Some(key) = oxi_sdk::get_env_api_key(provider) {
54 return Some((key, CredentialSource::EnvVar));
55 }
56
57 None
58 }
59
60 pub fn has_credential(provider: &str, config_key: Option<&str>) -> bool {
62 Self::resolve(provider, config_key).is_some()
63 }
64
65 pub fn store(provider: &str, api_key: &str) -> Result<()> {
70 let token = oxi_sdk::TokenBundle {
71 access_token: api_key.to_string(),
72 refresh_token: None,
73 token_type: "Bearer".to_string(),
74 obtained_at: chrono::Utc::now(),
75 expires_in: 0,
76 scope: None,
77 };
78 oxi_sdk::save_token(provider, &token)?;
79 tracing::info!(provider = %provider, "API key stored to oxi auth store");
80 Ok(())
81 }
82
83 pub fn provider_from_model(model_id: &str) -> Option<&str> {
87 if model_id.is_empty() {
88 return None;
89 }
90 model_id.split_once('/').map(|(p, _)| p)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_provider_from_model() {
100 assert_eq!(
101 CredentialStore::provider_from_model("anthropic/claude-sonnet-4-20250514"),
102 Some("anthropic")
103 );
104 assert_eq!(
105 CredentialStore::provider_from_model("openai/gpt-4o"),
106 Some("openai")
107 );
108 assert_eq!(CredentialStore::provider_from_model("bare-model"), None);
109 assert_eq!(CredentialStore::provider_from_model(""), None);
110 }
111
112 #[test]
113 fn test_config_key_takes_priority() {
114 let result = CredentialStore::resolve("anthropic", Some("sk-test-config-key"));
116 assert!(result.is_some());
117 let (key, source) = result.unwrap();
118 assert_eq!(key, "sk-test-config-key");
119 assert!(matches!(source, CredentialSource::Config));
120 }
121
122 #[test]
123 fn test_empty_config_key_skipped() {
124 let result = CredentialStore::resolve("anthropic", Some(""));
125 let _ = result;
129 }
130
131 #[test]
132 fn test_none_config_key_skipped() {
133 let result = CredentialStore::resolve("anthropic", None);
134 let _ = result; }
136}