vtcode_auth/
auth_service.rs1use anyhow::{Result, anyhow};
4use std::sync::Arc;
5
6use crate::AuthCredentialsStoreMode;
7use crate::codex_auth_import::{codex_auth_json_refresher, is_session_expired, try_load_codex_chatgpt_session};
8use crate::config::{OpenAIAuthConfig, OpenAIPreferredMethod};
9use crate::openai_chatgpt_oauth::{
10 OpenAIChatGptAuthHandle, OpenAIChatGptSession, OpenAIChatGptSessionProvenance, OpenAIChatGptSessionRefresher,
11 OpenAICredentialOverview, OpenAIResolvedAuth, OpenAIResolvedAuthSource, load_openai_chatgpt_session_with_mode,
12};
13
14#[derive(Debug, Clone)]
16pub struct OpenAIAccountAuthService {
17 auth_config: OpenAIAuthConfig,
18 storage_mode: AuthCredentialsStoreMode,
19}
20
21impl OpenAIAccountAuthService {
22 #[must_use]
23 pub(crate) fn new(auth_config: OpenAIAuthConfig, storage_mode: AuthCredentialsStoreMode) -> Self {
24 Self { auth_config, storage_mode }
25 }
26
27 pub(crate) 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 => match session {
32 Some(native) => {
33 let handle = self.handle_from_session(native);
34 let api_key = handle.current_api_key()?;
35 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
36 }
37 None => match self.try_codex_fallback()? {
38 Some(codex_session) => {
39 if is_session_expired(&codex_session) {
45 Err(anyhow!(
46 "Codex's ChatGPT session is expired. \
47 Run `codex login` to refresh it, or `vtcode login openai` for a VT Code session."
48 ))
49 } else {
50 let handle = OpenAIChatGptAuthHandle::new_external(
56 codex_session,
57 self.auth_config.auto_refresh,
58 codex_auth_json_refresher(),
59 );
60 let api_key = handle.current_api_key()?;
61 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
62 }
63 }
64 None => Err(anyhow!("Run vtcode login openai")),
65 },
66 },
67 OpenAIPreferredMethod::ApiKey => {
68 let api_key = require_api_key(api_key)?;
69 Ok(OpenAIResolvedAuth::ApiKey { api_key })
70 }
71 OpenAIPreferredMethod::Auto => {
72 if let Some(session) = session {
73 let handle = self.handle_from_session(session);
74 let api_key = handle.current_api_key()?;
75 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
76 } else if let Some(codex_session) = self.try_codex_fallback()? {
77 if is_session_expired(&codex_session) {
80 tracing::info!("codex auth.json session is expired; falling back to API key");
81 let api_key = require_api_key(api_key)?;
82 Ok(OpenAIResolvedAuth::ApiKey { api_key })
83 } else {
84 let handle = OpenAIChatGptAuthHandle::new_external(
85 codex_session,
86 self.auth_config.auto_refresh,
87 codex_auth_json_refresher(),
88 );
89 let api_key = handle.current_api_key()?;
90 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
91 }
92 } else {
93 let api_key = require_api_key(api_key)?;
94 Ok(OpenAIResolvedAuth::ApiKey { api_key })
95 }
96 }
97 }
98 }
99
100 fn handle_from_session(&self, session: OpenAIChatGptSession) -> OpenAIChatGptAuthHandle {
102 OpenAIChatGptAuthHandle::new(session, self.auth_config.clone(), self.storage_mode)
103 }
104
105 fn try_codex_fallback(&self) -> Result<Option<OpenAIChatGptSession>> {
111 match try_load_codex_chatgpt_session() {
112 Ok(session) => Ok(session),
113 Err(err) => {
114 tracing::warn!("failed to load codex auth.json fallback: {err}");
115 Ok(None)
116 }
117 }
118 }
119
120 pub fn resolve_external_session_auth(
122 &self,
123 session: OpenAIChatGptSession,
124 refresher: Arc<dyn OpenAIChatGptSessionRefresher>,
125 ) -> Result<OpenAIResolvedAuth> {
126 let handle = OpenAIChatGptAuthHandle::new_external(session, self.auth_config.auto_refresh, refresher);
127 let api_key = handle.current_api_key()?;
128 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
129 }
130
131 pub(crate) fn summarize_credentials(&self, api_key: Option<String>) -> Result<OpenAICredentialOverview> {
133 let vtcode_session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
134 let codex_session = self.try_codex_fallback()?.filter(|session| !is_session_expired(session));
139 let codex_fallback_available = codex_session.is_some();
140
141 let (chatgpt_session, chatgpt_session_provenance) = match vtcode_session {
143 Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::Native)),
144 None => match codex_session {
145 Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::CodexFallback)),
146 None => (None, None),
147 },
148 };
149
150 let (chatgpt_email, chatgpt_plan, chatgpt_session_present) = match &chatgpt_session {
152 Some(session) => (session.email.clone(), session.plan.clone(), true),
153 None => (None, None, false),
154 };
155
156 let api_key_available = api_key.as_ref().is_some_and(|value| !value.trim().is_empty());
157 let active_source = match self.auth_config.preferred_method {
158 OpenAIPreferredMethod::Chatgpt => chatgpt_session_present.then_some(OpenAIResolvedAuthSource::ChatGpt),
159 OpenAIPreferredMethod::ApiKey => api_key_available.then_some(OpenAIResolvedAuthSource::ApiKey),
160 OpenAIPreferredMethod::Auto => {
161 if chatgpt_session_present {
162 Some(OpenAIResolvedAuthSource::ChatGpt)
163 } else if api_key_available {
164 Some(OpenAIResolvedAuthSource::ApiKey)
165 } else {
166 None
167 }
168 }
169 };
170
171 let (notice, recommendation) = if api_key_available && chatgpt_session_present {
172 let active_label = match active_source {
173 Some(OpenAIResolvedAuthSource::ChatGpt) => "ChatGPT subscription",
174 Some(OpenAIResolvedAuthSource::ApiKey) => "OPENAI_API_KEY",
175 None => "neither credential",
176 };
177 let recommendation = match active_source {
178 Some(OpenAIResolvedAuthSource::ChatGpt) => {
179 "Next step: keep the current priority, run /logout openai to rely on API-key auth only, or set [auth.openai].preferred_method = \"api_key\"."
180 }
181 Some(OpenAIResolvedAuthSource::ApiKey) => {
182 "Next step: keep the current priority, remove OPENAI_API_KEY if ChatGPT should win, or set [auth.openai].preferred_method = \"chatgpt\"."
183 }
184 None => "Next step: choose a single preferred source or set [auth.openai].preferred_method explicitly.",
185 };
186 (
187 Some(format!(
188 "Both ChatGPT subscription auth and OPENAI_API_KEY are available. VT Code is using {active_label} because auth.openai.preferred_method = {}.",
189 self.auth_config.preferred_method.as_str()
190 )),
191 Some(recommendation.to_string()),
192 )
193 } else {
194 (None, None)
195 };
196
197 Ok(OpenAICredentialOverview {
198 api_key_available,
199 chatgpt_email,
200 chatgpt_plan,
201 chatgpt_session_present,
202 chatgpt_session_provenance,
203 codex_fallback_available,
204 active_source,
205 preferred_method: self.auth_config.preferred_method,
206 notice,
207 recommendation,
208 })
209 }
210}
211
212fn require_api_key(api_key: Option<String>) -> Result<String> {
213 api_key
214 .map(|value| value.trim().to_string())
215 .filter(|value| !value.is_empty())
216 .ok_or_else(|| anyhow!("OpenAI API key not found"))
217}