elph_ai/auth/oauth/
registry.rs1use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use crate::auth::helpers::lazy_oauth;
7use crate::auth::oauth::{anthropic_oauth_loader, github_copilot_oauth_loader, openai_codex_oauth_loader};
8use crate::auth::types::{AuthLoginCallbacks, ModelAuth, OAuthAuth, OAuthCredential};
9use crate::models::catalog::GITHUB_COPILOT_MODELS;
10use crate::types::Model;
11
12pub type OAuthProviderId = String;
13
14pub type OAuthModifyModelsFn = Arc<dyn Fn(Vec<Model>, &OAuthCredential) -> Vec<Model> + Send + Sync>;
15
16#[derive(Clone)]
17pub struct OAuthProviderInterface {
18 pub id: OAuthProviderId,
19 pub name: String,
20 pub auth: OAuthAuth,
21 pub get_api_key: Arc<dyn Fn(&OAuthCredential) -> String + Send + Sync>,
22 pub modify_models: Option<OAuthModifyModelsFn>,
23}
24
25fn anthropic_provider() -> OAuthProviderInterface {
26 OAuthProviderInterface {
27 id: "anthropic".to_string(),
28 name: "Anthropic (Claude Pro/Max)".to_string(),
29 auth: lazy_oauth("Anthropic (Claude Pro/Max)", anthropic_oauth_loader()),
30 get_api_key: Arc::new(|c| c.access.clone()),
31 modify_models: None,
32 }
33}
34
35fn github_copilot_provider() -> OAuthProviderInterface {
36 OAuthProviderInterface {
37 id: "github-copilot".to_string(),
38 name: "GitHub Copilot".to_string(),
39 auth: lazy_oauth("GitHub Copilot", github_copilot_oauth_loader()),
40 get_api_key: Arc::new(|c| c.access.clone()),
41 modify_models: Some(Arc::new(modify_github_copilot_models)),
42 }
43}
44
45fn openai_codex_provider() -> OAuthProviderInterface {
46 OAuthProviderInterface {
47 id: "openai-codex".to_string(),
48 name: "OpenAI (ChatGPT Plus/Pro)".to_string(),
49 auth: lazy_oauth("OpenAI (ChatGPT Plus/Pro)", openai_codex_oauth_loader()),
50 get_api_key: Arc::new(|c| c.access.clone()),
51 modify_models: None,
52 }
53}
54
55fn built_in_providers() -> Vec<OAuthProviderInterface> {
56 vec![anthropic_provider(), github_copilot_provider(), openai_codex_provider()]
57}
58
59fn modify_github_copilot_models(models: Vec<Model>, credential: &OAuthCredential) -> Vec<Model> {
60 let enterprise_domain = credential
61 .enterprise_url
62 .as_deref()
63 .and_then(crate::auth::oauth::normalize_domain);
64 let base_url =
65 crate::auth::oauth::get_github_copilot_base_url(Some(&credential.access), enterprise_domain.as_deref());
66 models
67 .into_iter()
68 .map(|mut model| {
69 model.base_url = base_url.clone();
70 model
71 })
72 .collect()
73}
74
75static REGISTRY: once_cell::sync::Lazy<RwLock<HashMap<String, OAuthProviderInterface>>> =
76 once_cell::sync::Lazy::new(|| {
77 let mut map = HashMap::new();
78 for provider in built_in_providers() {
79 map.insert(provider.id.clone(), provider);
80 }
81 RwLock::new(map)
82 });
83
84pub fn get_oauth_provider(id: &str) -> Option<OAuthProviderInterface> {
85 REGISTRY.read().ok()?.get(id).cloned()
86}
87
88pub fn register_oauth_provider(provider: OAuthProviderInterface) {
89 if let Ok(mut registry) = REGISTRY.write() {
90 registry.insert(provider.id.clone(), provider);
91 }
92}
93
94pub fn unregister_oauth_provider(id: &str) {
95 let Ok(mut registry) = REGISTRY.write() else {
96 return;
97 };
98 if let Some(built_in) = built_in_providers().into_iter().find(|p| p.id == id) {
99 registry.insert(id.to_string(), built_in);
100 return;
101 }
102 registry.remove(id);
103}
104
105pub fn reset_oauth_providers() {
106 if let Ok(mut registry) = REGISTRY.write() {
107 registry.clear();
108 for provider in built_in_providers() {
109 registry.insert(provider.id.clone(), provider);
110 }
111 }
112}
113
114pub fn get_oauth_providers() -> Vec<OAuthProviderInterface> {
115 REGISTRY
116 .read()
117 .map(|registry| registry.values().cloned().collect())
118 .unwrap_or_default()
119}
120
121pub async fn refresh_oauth_token(provider_id: &str, credential: OAuthCredential) -> anyhow::Result<OAuthCredential> {
122 let provider =
123 get_oauth_provider(provider_id).ok_or_else(|| anyhow::anyhow!("Unknown OAuth provider: {provider_id}"))?;
124 (provider.auth.refresh)(credential).await
125}
126
127pub struct OAuthApiKeyResult {
128 pub new_credentials: OAuthCredential,
129 pub api_key: String,
130}
131
132pub async fn get_oauth_api_key(
133 provider_id: &str,
134 mut credential: OAuthCredential,
135) -> anyhow::Result<OAuthApiKeyResult> {
136 let provider =
137 get_oauth_provider(provider_id).ok_or_else(|| anyhow::anyhow!("Unknown OAuth provider: {provider_id}"))?;
138
139 if chrono::Utc::now().timestamp_millis() >= credential.expires {
140 credential = (provider.auth.refresh)(credential).await?;
141 }
142
143 let api_key = (provider.get_api_key)(&credential);
144 Ok(OAuthApiKeyResult {
145 new_credentials: credential,
146 api_key,
147 })
148}
149
150pub async fn oauth_provider_login(
151 provider_id: &str,
152 callbacks: Arc<dyn AuthLoginCallbacks>,
153) -> anyhow::Result<OAuthCredential> {
154 let provider =
155 get_oauth_provider(provider_id).ok_or_else(|| anyhow::anyhow!("Unknown OAuth provider: {provider_id}"))?;
156 (provider.auth.login)(callbacks).await
157}
158
159pub async fn oauth_provider_to_auth(provider_id: &str, credential: OAuthCredential) -> anyhow::Result<ModelAuth> {
160 let provider =
161 get_oauth_provider(provider_id).ok_or_else(|| anyhow::anyhow!("Unknown OAuth provider: {provider_id}"))?;
162 (provider.auth.to_auth)(credential).await
163}
164
165pub fn oauth_provider_modify_models(provider_id: &str, models: Vec<Model>, credential: &OAuthCredential) -> Vec<Model> {
166 let Some(provider) = get_oauth_provider(provider_id) else {
167 return models;
168 };
169 provider
170 .modify_models
171 .as_ref()
172 .map(|modify| modify(models.clone(), credential))
173 .unwrap_or(models)
174}
175
176pub fn builtin_oauth_provider_ids() -> Vec<&'static str> {
177 vec!["anthropic", "github-copilot", "openai-codex"]
178}
179
180pub fn github_copilot_catalog_models() -> Vec<Model> {
181 GITHUB_COPILOT_MODELS.iter().cloned().collect()
182}