use anyhow::{Result, anyhow};
use std::sync::Arc;
use crate::AuthCredentialsStoreMode;
use crate::codex_auth_import::{codex_auth_json_refresher, is_session_expired, try_load_codex_chatgpt_session};
use crate::config::{OpenAIAuthConfig, OpenAIPreferredMethod};
use crate::openai_chatgpt_oauth::{
OpenAIChatGptAuthHandle, OpenAIChatGptSession, OpenAIChatGptSessionProvenance, OpenAIChatGptSessionRefresher,
OpenAICredentialOverview, OpenAIResolvedAuth, OpenAIResolvedAuthSource, load_openai_chatgpt_session_with_mode,
};
#[derive(Debug, Clone)]
pub struct OpenAIAccountAuthService {
auth_config: OpenAIAuthConfig,
storage_mode: AuthCredentialsStoreMode,
}
impl OpenAIAccountAuthService {
#[must_use]
pub(crate) fn new(auth_config: OpenAIAuthConfig, storage_mode: AuthCredentialsStoreMode) -> Self {
Self { auth_config, storage_mode }
}
pub(crate) fn resolve_runtime_auth(&self, api_key: Option<String>) -> Result<OpenAIResolvedAuth> {
let session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
match self.auth_config.preferred_method {
OpenAIPreferredMethod::Chatgpt => match session {
Some(native) => {
let handle = self.handle_from_session(native);
let api_key = handle.current_api_key()?;
Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
}
None => match self.try_codex_fallback()? {
Some(codex_session) => {
if is_session_expired(&codex_session) {
Err(anyhow!(
"Codex's ChatGPT session is expired. \
Run `codex login` to refresh it, or `vtcode login openai` for a VT Code session."
))
} else {
let handle = OpenAIChatGptAuthHandle::new_external(
codex_session,
self.auth_config.auto_refresh,
codex_auth_json_refresher(),
);
let api_key = handle.current_api_key()?;
Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
}
}
None => Err(anyhow!("Run vtcode login openai")),
},
},
OpenAIPreferredMethod::ApiKey => {
let api_key = require_api_key(api_key)?;
Ok(OpenAIResolvedAuth::ApiKey { api_key })
}
OpenAIPreferredMethod::Auto => {
if let Some(session) = session {
let handle = self.handle_from_session(session);
let api_key = handle.current_api_key()?;
Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
} else if let Some(codex_session) = self.try_codex_fallback()? {
if is_session_expired(&codex_session) {
tracing::info!("codex auth.json session is expired; falling back to API key");
let api_key = require_api_key(api_key)?;
Ok(OpenAIResolvedAuth::ApiKey { api_key })
} else {
let handle = OpenAIChatGptAuthHandle::new_external(
codex_session,
self.auth_config.auto_refresh,
codex_auth_json_refresher(),
);
let api_key = handle.current_api_key()?;
Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
}
} else {
let api_key = require_api_key(api_key)?;
Ok(OpenAIResolvedAuth::ApiKey { api_key })
}
}
}
}
fn handle_from_session(&self, session: OpenAIChatGptSession) -> OpenAIChatGptAuthHandle {
OpenAIChatGptAuthHandle::new(session, self.auth_config.clone(), self.storage_mode)
}
fn try_codex_fallback(&self) -> Result<Option<OpenAIChatGptSession>> {
match try_load_codex_chatgpt_session() {
Ok(session) => Ok(session),
Err(err) => {
tracing::warn!("failed to load codex auth.json fallback: {err}");
Ok(None)
}
}
}
pub fn resolve_external_session_auth(
&self,
session: OpenAIChatGptSession,
refresher: Arc<dyn OpenAIChatGptSessionRefresher>,
) -> Result<OpenAIResolvedAuth> {
let handle = OpenAIChatGptAuthHandle::new_external(session, self.auth_config.auto_refresh, refresher);
let api_key = handle.current_api_key()?;
Ok(OpenAIResolvedAuth::ChatGpt { api_key, handle })
}
pub(crate) fn summarize_credentials(&self, api_key: Option<String>) -> Result<OpenAICredentialOverview> {
let vtcode_session = load_openai_chatgpt_session_with_mode(self.storage_mode)?;
let codex_session = self.try_codex_fallback()?.filter(|session| !is_session_expired(session));
let codex_fallback_available = codex_session.is_some();
let (chatgpt_session, chatgpt_session_provenance) = match vtcode_session {
Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::Native)),
None => match codex_session {
Some(session) => (Some(session), Some(OpenAIChatGptSessionProvenance::CodexFallback)),
None => (None, None),
},
};
let (chatgpt_email, chatgpt_plan, chatgpt_session_present) = match &chatgpt_session {
Some(session) => (session.email.clone(), session.plan.clone(), true),
None => (None, None, false),
};
let api_key_available = api_key.as_ref().is_some_and(|value| !value.trim().is_empty());
let active_source = match self.auth_config.preferred_method {
OpenAIPreferredMethod::Chatgpt => chatgpt_session_present.then_some(OpenAIResolvedAuthSource::ChatGpt),
OpenAIPreferredMethod::ApiKey => api_key_available.then_some(OpenAIResolvedAuthSource::ApiKey),
OpenAIPreferredMethod::Auto => {
if chatgpt_session_present {
Some(OpenAIResolvedAuthSource::ChatGpt)
} else if api_key_available {
Some(OpenAIResolvedAuthSource::ApiKey)
} else {
None
}
}
};
let (notice, recommendation) = if api_key_available && chatgpt_session_present {
let active_label = match active_source {
Some(OpenAIResolvedAuthSource::ChatGpt) => "ChatGPT subscription",
Some(OpenAIResolvedAuthSource::ApiKey) => "OPENAI_API_KEY",
None => "neither credential",
};
let recommendation = match active_source {
Some(OpenAIResolvedAuthSource::ChatGpt) => {
"Next step: keep the current priority, run /logout openai to rely on API-key auth only, or set [auth.openai].preferred_method = \"api_key\"."
}
Some(OpenAIResolvedAuthSource::ApiKey) => {
"Next step: keep the current priority, remove OPENAI_API_KEY if ChatGPT should win, or set [auth.openai].preferred_method = \"chatgpt\"."
}
None => "Next step: choose a single preferred source or set [auth.openai].preferred_method explicitly.",
};
(
Some(format!(
"Both ChatGPT subscription auth and OPENAI_API_KEY are available. VT Code is using {active_label} because auth.openai.preferred_method = {}.",
self.auth_config.preferred_method.as_str()
)),
Some(recommendation.to_string()),
)
} else {
(None, None)
};
Ok(OpenAICredentialOverview {
api_key_available,
chatgpt_email,
chatgpt_plan,
chatgpt_session_present,
chatgpt_session_provenance,
codex_fallback_available,
active_source,
preferred_method: self.auth_config.preferred_method,
notice,
recommendation,
})
}
}
fn require_api_key(api_key: Option<String>) -> Result<String> {
api_key
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow!("OpenAI API key not found"))
}