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