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 match self.auth_config.preferred_method {
32 OpenAIPreferredMethod::Chatgpt => {
33 let session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
34 match session {
35 Some(native) => {
36 let handle = self.handle_from_session(native);
37 let api_key = handle.current_api_key()?;
38 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
39 }
40 None => match self.try_codex_fallback()? {
41 Some(codex_session) => {
42 if is_session_expired(&codex_session) {
48 Err(anyhow!(
49 "Codex's ChatGPT session is expired. \
50 Run `codex login` to refresh it, or `vtcode login openai` for a VT Code session."
51 ))
52 } else {
53 let handle = OpenAIChatGptAuthHandle::new_external(
59 codex_session,
60 self.auth_config.auto_refresh,
61 codex_auth_json_refresher(),
62 );
63 let api_key = handle.current_api_key()?;
64 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
65 }
66 }
67 None => Err(anyhow!("Run vtcode login openai")),
68 },
69 }
70 }
71 OpenAIPreferredMethod::ApiKey => {
72 let api_key = require_api_key(api_key)?;
73 Ok(OpenAIResolvedAuth::ApiKey { api_key })
74 }
75 OpenAIPreferredMethod::Auto => {
76 let session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
77 if let Some(session) = session {
78 let handle = self.handle_from_session(session);
79 let api_key = handle.current_api_key()?;
80 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
81 } else if let Some(codex_session) = self.try_codex_fallback()? {
82 if is_session_expired(&codex_session) {
85 tracing::info!("codex auth.json session is expired; falling back to API key");
86 let api_key = require_api_key(api_key)?;
87 Ok(OpenAIResolvedAuth::ApiKey { api_key })
88 } else {
89 let handle = OpenAIChatGptAuthHandle::new_external(
90 codex_session,
91 self.auth_config.auto_refresh,
92 codex_auth_json_refresher(),
93 );
94 let api_key = handle.current_api_key()?;
95 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
96 }
97 } else {
98 let api_key = require_api_key(api_key)?;
99 Ok(OpenAIResolvedAuth::ApiKey { api_key })
100 }
101 }
102 }
103 }
104
105 fn handle_from_session(&self, session: OpenAIChatGptSession) -> OpenAIChatGptAuthHandle {
107 OpenAIChatGptAuthHandle::new(session, self.auth_config.clone(), self.storage_mode)
108 }
109
110 fn try_codex_fallback(&self) -> Result<Option<OpenAIChatGptSession>> {
116 match try_load_codex_chatgpt_session() {
117 Ok(session) => Ok(session),
118 Err(err) => {
119 tracing::warn!("failed to load codex auth.json fallback: {err}");
120 Ok(None)
121 }
122 }
123 }
124
125 pub fn resolve_external_session_auth(
127 &self,
128 session: OpenAIChatGptSession,
129 refresher: Arc<dyn OpenAIChatGptSessionRefresher>,
130 ) -> Result<OpenAIResolvedAuth> {
131 let handle = OpenAIChatGptAuthHandle::new_external(session, self.auth_config.auto_refresh, refresher);
132 let api_key = handle.current_api_key()?;
133 Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
134 }
135
136 pub(crate) fn summarize_credentials(&self, api_key: Option<String>) -> Result<OpenAICredentialOverview> {
138 let vtcode_session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
139 let codex_session = self.try_codex_fallback()?.filter(|session| !is_session_expired(session));
144 let codex_fallback_available = codex_session.is_some();
145
146 let (chatgpt_session, chatgpt_session_provenance) = match vtcode_session {
148 Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::Native)),
149 None => match codex_session {
150 Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::CodexFallback)),
151 None => (None, None),
152 },
153 };
154
155 let (chatgpt_email, chatgpt_plan, chatgpt_session_present) = match &chatgpt_session {
157 Some(session) => (session.email.clone(), session.plan.clone(), true),
158 None => (None, None, false),
159 };
160
161 let api_key_available = api_key.as_ref().is_some_and(|value| !value.trim().is_empty());
162 let active_source = match self.auth_config.preferred_method {
163 OpenAIPreferredMethod::Chatgpt => chatgpt_session_present.then_some(OpenAIResolvedAuthSource::ChatGpt),
164 OpenAIPreferredMethod::ApiKey => api_key_available.then_some(OpenAIResolvedAuthSource::ApiKey),
165 OpenAIPreferredMethod::Auto => {
166 if chatgpt_session_present {
167 Some(OpenAIResolvedAuthSource::ChatGpt)
168 } else if api_key_available {
169 Some(OpenAIResolvedAuthSource::ApiKey)
170 } else {
171 None
172 }
173 }
174 };
175
176 let (notice, recommendation) = if api_key_available && chatgpt_session_present {
177 let active_label = match active_source {
178 Some(OpenAIResolvedAuthSource::ChatGpt) => "ChatGPT subscription",
179 Some(OpenAIResolvedAuthSource::ApiKey) => "OPENAI_API_KEY",
180 None => "neither credential",
181 };
182 let recommendation = match active_source {
183 Some(OpenAIResolvedAuthSource::ChatGpt) => {
184 "Next step: keep the current priority, run /logout openai to rely on API-key auth only, or set [auth.openai].preferred_method = \"api_key\"."
185 }
186 Some(OpenAIResolvedAuthSource::ApiKey) => {
187 "Next step: keep the current priority, remove OPENAI_API_KEY if ChatGPT should win, or set [auth.openai].preferred_method = \"chatgpt\"."
188 }
189 None => "Next step: choose a single preferred source or set [auth.openai].preferred_method explicitly.",
190 };
191 (
192 Some(format!(
193 "Both ChatGPT subscription auth and OPENAI_API_KEY are available. VT Code is using {active_label} because auth.openai.preferred_method = {}.",
194 self.auth_config.preferred_method.as_str()
195 )),
196 Some(recommendation.to_string()),
197 )
198 } else {
199 (None, None)
200 };
201
202 Ok(OpenAICredentialOverview {
203 api_key_available,
204 chatgpt_email,
205 chatgpt_plan,
206 chatgpt_session_present,
207 chatgpt_session_provenance,
208 codex_fallback_available,
209 active_source,
210 preferred_method: self.auth_config.preferred_method,
211 notice,
212 recommendation,
213 })
214 }
215}
216
217fn require_api_key(api_key: Option<String>) -> Result<String> {
218 api_key
219 .map(|value| value.trim().to_string())
220 .filter(|value| !value.is_empty())
221 .ok_or_else(|| anyhow!("OpenAI API key not found"))
222}