Skip to main content

vtcode_auth/
auth_service.rs

1//! Internal auth service contracts used by VT Code.
2
3use anyhow::{Result, anyhow};
4use std::sync::Arc;
5
6use crate::AuthCredentialsStoreMode;
7use crate::config::{OpenAIAuthConfig, OpenAIPreferredMethod};
8use crate::openai_chatgpt_oauth::{
9    OpenAIChatGptAuthHandle, OpenAIChatGptSession, OpenAIChatGptSessionRefresher, OpenAICredentialOverview,
10    OpenAIResolvedAuth, OpenAIResolvedAuthSource, load_openai_chatgpt_session_with_mode,
11};
12
13/// Service contract for resolving VT Code's OpenAI account auth state.
14#[derive(Debug, Clone)]
15pub struct OpenAIAccountAuthService {
16    auth_config: OpenAIAuthConfig,
17    storage_mode: AuthCredentialsStoreMode,
18}
19
20impl OpenAIAccountAuthService {
21    #[must_use]
22    pub fn new(auth_config: OpenAIAuthConfig, storage_mode: AuthCredentialsStoreMode) -> Self {
23        Self { auth_config, storage_mode }
24    }
25
26    /// Resolve the active OpenAI auth source for the current configuration.
27    pub fn resolve_runtime_auth(&self, api_key: Option<String>) -> Result<OpenAIResolvedAuth> {
28        let session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
29        match self.auth_config.preferred_method {
30            OpenAIPreferredMethod::Chatgpt => {
31                let session = session.ok_or_else(|| anyhow!("Run vtcode login openai"))?;
32                let handle = OpenAIChatGptAuthHandle::new(session, self.auth_config.clone(), self.storage_mode);
33                let api_key = handle.current_api_key()?;
34                Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
35            }
36            OpenAIPreferredMethod::ApiKey => {
37                let api_key = require_api_key(api_key)?;
38                Ok(OpenAIResolvedAuth::ApiKey { api_key })
39            }
40            OpenAIPreferredMethod::Auto => {
41                if let Some(session) = session {
42                    let handle = OpenAIChatGptAuthHandle::new(session, self.auth_config.clone(), self.storage_mode);
43                    let api_key = handle.current_api_key()?;
44                    Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
45                } else {
46                    let api_key = require_api_key(api_key)?;
47                    Ok(OpenAIResolvedAuth::ApiKey { api_key })
48                }
49            }
50        }
51    }
52
53    /// Resolve a non-persistent OpenAI auth session backed by externally managed tokens.
54    pub fn resolve_external_session_auth(
55        &self,
56        session: OpenAIChatGptSession,
57        refresher: Arc<dyn OpenAIChatGptSessionRefresher>,
58    ) -> Result<OpenAIResolvedAuth> {
59        let handle = OpenAIChatGptAuthHandle::new_external(session, self.auth_config.auto_refresh, refresher);
60        let api_key = handle.current_api_key()?;
61        Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
62    }
63
64    /// Summarize the available OpenAI credentials without mutating storage.
65    pub fn summarize_credentials(&self, api_key: Option<String>) -> Result<OpenAICredentialOverview> {
66        let chatgpt_session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
67        let api_key_available = api_key.as_ref().is_some_and(|value| !value.trim().is_empty());
68        let active_source = match self.auth_config.preferred_method {
69            OpenAIPreferredMethod::Chatgpt => chatgpt_session.as_ref().map(|_| OpenAIResolvedAuthSource::ChatGpt),
70            OpenAIPreferredMethod::ApiKey => api_key_available.then_some(OpenAIResolvedAuthSource::ApiKey),
71            OpenAIPreferredMethod::Auto => {
72                if chatgpt_session.is_some() {
73                    Some(OpenAIResolvedAuthSource::ChatGpt)
74                } else if api_key_available {
75                    Some(OpenAIResolvedAuthSource::ApiKey)
76                } else {
77                    None
78                }
79            }
80        };
81
82        let (notice, recommendation) = if api_key_available && chatgpt_session.is_some() {
83            let active_label = match active_source {
84                Some(OpenAIResolvedAuthSource::ChatGpt) => "ChatGPT subscription",
85                Some(OpenAIResolvedAuthSource::ApiKey) => "OPENAI_API_KEY",
86                None => "neither credential",
87            };
88            let recommendation = match active_source {
89                Some(OpenAIResolvedAuthSource::ChatGpt) => {
90                    "Next step: keep the current priority, run /logout openai to rely on API-key auth only, or set [auth.openai].preferred_method = \"api_key\"."
91                }
92                Some(OpenAIResolvedAuthSource::ApiKey) => {
93                    "Next step: keep the current priority, remove OPENAI_API_KEY if ChatGPT should win, or set [auth.openai].preferred_method = \"chatgpt\"."
94                }
95                None => "Next step: choose a single preferred source or set [auth.openai].preferred_method explicitly.",
96            };
97            (
98                Some(format!(
99                    "Both ChatGPT subscription auth and OPENAI_API_KEY are available. VT Code is using {active_label} because auth.openai.preferred_method = {}.",
100                    self.auth_config.preferred_method.as_str()
101                )),
102                Some(recommendation.to_string()),
103            )
104        } else {
105            (None, None)
106        };
107
108        Ok(OpenAICredentialOverview {
109            api_key_available,
110            chatgpt_session,
111            active_source,
112            preferred_method: self.auth_config.preferred_method,
113            notice,
114            recommendation,
115        })
116    }
117}
118
119fn require_api_key(api_key: Option<String>) -> Result<String> {
120    api_key
121        .map(|value| value.trim().to_string())
122        .filter(|value| !value.is_empty())
123        .ok_or_else(|| anyhow!("OpenAI API key not found"))
124}