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) -> &str {
77 model_id
78 .split_once('/')
79 .map(|(p, _)| p)
80 .unwrap_or("anthropic")
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_provider_from_model() {
90 assert_eq!(
91 CredentialStore::provider_from_model("anthropic/claude-sonnet-4-20250514"),
92 "anthropic"
93 );
94 assert_eq!(
95 CredentialStore::provider_from_model("openai/gpt-4o"),
96 "openai"
97 );
98 assert_eq!(
99 CredentialStore::provider_from_model("bare-model"),
100 "anthropic"
101 );
102 }
103
104 #[test]
105 fn test_config_key_takes_priority() {
106 let result = CredentialStore::resolve("anthropic", Some("sk-test-config-key"));
108 assert!(result.is_some());
109 let (key, source) = result.unwrap();
110 assert_eq!(key, "sk-test-config-key");
111 assert!(matches!(source, CredentialSource::Config));
112 }
113
114 #[test]
115 fn test_empty_config_key_skipped() {
116 let result = CredentialStore::resolve("anthropic", Some(""));
117 let _ = result;
121 }
122
123 #[test]
124 fn test_none_config_key_skipped() {
125 let result = CredentialStore::resolve("anthropic", None);
126 let _ = result; }
128}