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)> {
29 if let Some(key) = config_key {
31 if !key.is_empty() {
32 return Some((key.to_string(), CredentialSource::Config));
33 }
34 }
35
36 if let Ok(Some(token)) = oxi_sdk::load_token(provider) {
38 if !token.access_token.is_empty() {
39 return Some((token.access_token, CredentialSource::OxiAuthStore));
40 }
41 }
42
43 if let Some(key) = oxi_sdk::get_env_api_key(provider) {
45 return Some((key, CredentialSource::EnvVar));
46 }
47
48 None
49 }
50
51 pub fn has_credential(provider: &str, config_key: Option<&str>) -> bool {
53 Self::resolve(provider, config_key).is_some()
54 }
55
56 pub fn store(provider: &str, api_key: &str) -> Result<()> {
61 let token = oxi_sdk::TokenBundle {
62 access_token: api_key.to_string(),
63 refresh_token: None,
64 token_type: "Bearer".to_string(),
65 obtained_at: chrono::Utc::now(),
66 expires_in: 0,
67 scope: None,
68 };
69 oxi_sdk::save_token(provider, &token)?;
70 tracing::info!(provider = %provider, "API key stored to oxi auth store");
71 Ok(())
72 }
73
74 pub fn provider_from_model(model_id: &str) -> Option<&str> {
78 if model_id.is_empty() {
79 return None;
80 }
81 model_id.split_once('/').map(|(p, _)| p)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_provider_from_model() {
91 assert_eq!(
92 CredentialStore::provider_from_model("anthropic/claude-sonnet-4-20250514"),
93 Some("anthropic")
94 );
95 assert_eq!(
96 CredentialStore::provider_from_model("openai/gpt-4o"),
97 Some("openai")
98 );
99 assert_eq!(
100 CredentialStore::provider_from_model("bare-model"),
101 None
102 );
103 assert_eq!(
104 CredentialStore::provider_from_model(""),
105 None
106 );
107 }
108
109 #[test]
110 fn test_config_key_takes_priority() {
111 let result = CredentialStore::resolve("anthropic", Some("sk-test-config-key"));
113 assert!(result.is_some());
114 let (key, source) = result.unwrap();
115 assert_eq!(key, "sk-test-config-key");
116 assert!(matches!(source, CredentialSource::Config));
117 }
118
119 #[test]
120 fn test_empty_config_key_skipped() {
121 let result = CredentialStore::resolve("anthropic", Some(""));
122 let _ = result;
126 }
127
128 #[test]
129 fn test_none_config_key_skipped() {
130 let result = CredentialStore::resolve("anthropic", None);
131 let _ = result; }
133}