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