mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## What changed - Prefetch ChatGPT account eligibility in the background and cache it across TUI widget replacements. - Show an application link when Daybreak is available but not enabled, an Astra-specific explanation for unsupported models, and neutral copy when access is already present or eligibility cannot be determined. - Refresh eligibility after account changes and reconnects without changing the selected model or delaying refusal handling. ## Testing - Cover eligibility responses, account changes, model-specific notices, fallback behavior, and rendered notice snapshots. GitOrigin-RevId: 7ae8613a946cb46b8f0b46bf47e5e15ae258ed86
154 lines
4.7 KiB
Rust
154 lines
4.7 KiB
Rust
//! Read-only Daybreak eligibility used to choose cyber refusal copy.
|
||
//! Account-scoped discovery runs in the background; pending/failed reads use neutral copy.
|
||
//! Astra takes precedence; the TUI does not configure the app’s Daybreak access program.
|
||
|
||
use crate::app_server_session::AppServerSession;
|
||
use crate::legacy_core::config::Config;
|
||
use codex_app_server_client::AppServerRequestHandle;
|
||
use codex_app_server_protocol::AuthMode;
|
||
use codex_app_server_protocol::ClientRequest;
|
||
use codex_app_server_protocol::GetAuthStatusParams;
|
||
use codex_app_server_protocol::GetAuthStatusResponse;
|
||
use codex_app_server_protocol::RequestId;
|
||
use codex_http_client::ClientRouteClass;
|
||
use codex_http_client::RouteAwareClientPool;
|
||
use codex_login::CodexAuth;
|
||
use serde::Deserialize;
|
||
use std::sync::Arc;
|
||
use std::time::Duration;
|
||
use tokio::sync::OnceCell;
|
||
|
||
pub(crate) type NoticeCache = Arc<OnceCell<Notice>>;
|
||
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub(crate) enum Notice {
|
||
Apply,
|
||
Astra,
|
||
#[default]
|
||
Limited,
|
||
}
|
||
|
||
pub(crate) fn prefetch_notice(config: &Config, server: &AppServerSession, cache: NoticeCache) {
|
||
if config.model_provider_id != "openai"
|
||
|| server.uses_remote_workspace()
|
||
|| cache.get().is_some()
|
||
{
|
||
return;
|
||
}
|
||
let config = config.clone();
|
||
let request_handle = server.request_handle();
|
||
tokio::spawn(async move {
|
||
cache
|
||
.get_or_init(|| read_notice(&config, &request_handle))
|
||
.await;
|
||
});
|
||
}
|
||
|
||
async fn read_notice(config: &Config, request_handle: &AppServerRequestHandle) -> Notice {
|
||
tokio::time::timeout(Duration::from_secs(3), async {
|
||
// Allow the server to refresh its token before reading local credentials.
|
||
let status: GetAuthStatusResponse = request_handle
|
||
.request_typed(ClientRequest::GetAuthStatus {
|
||
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
|
||
params: GetAuthStatusParams {
|
||
include_token: Some(true),
|
||
refresh_token: Some(false),
|
||
},
|
||
})
|
||
.await
|
||
.ok()?;
|
||
let auth = config
|
||
.auth_config()
|
||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||
.await
|
||
.ok()
|
||
.flatten()?;
|
||
let CodexAuth::Chatgpt(_) = &auth else {
|
||
return None;
|
||
};
|
||
if status.auth_method != Some(AuthMode::Chatgpt)
|
||
|| status.auth_token.as_deref() != Some(auth.get_token().ok()?.as_str())
|
||
{
|
||
return None;
|
||
}
|
||
let client = RouteAwareClientPool::new_without_redirects(
|
||
config.http_client_factory(),
|
||
ClientRouteClass::Api,
|
||
);
|
||
let url = format!(
|
||
"{}/accounts/verified_access",
|
||
config.chatgpt_base_url.trim_end_matches('/')
|
||
);
|
||
let response = client
|
||
.get(url)
|
||
.headers(codex_model_provider::auth_provider_from_auth(&auth).to_auth_headers())
|
||
.send()
|
||
.await
|
||
.ok()?;
|
||
if !response.status().is_success() {
|
||
return None;
|
||
}
|
||
let access = response.json::<VerifiedAccess>().await.ok()?;
|
||
let current_auth = config
|
||
.auth_config()
|
||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||
.await
|
||
.ok()
|
||
.flatten()?;
|
||
if current_auth.get_token().ok()? != auth.get_token().ok()?
|
||
|| current_auth.get_account_id() != auth.get_account_id()
|
||
{
|
||
return None;
|
||
}
|
||
Some(access.notice())
|
||
})
|
||
.await
|
||
.ok()
|
||
.flatten()
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
struct VerifiedAccess {
|
||
programs: Vec<Program>,
|
||
}
|
||
#[derive(Deserialize)]
|
||
#[serde(tag = "program", rename_all = "snake_case")]
|
||
enum Program {
|
||
Cyber {
|
||
state: String,
|
||
grants: Vec<serde::de::IgnoredAny>,
|
||
},
|
||
#[serde(other)]
|
||
Other,
|
||
}
|
||
impl VerifiedAccess {
|
||
fn notice(&self) -> Notice {
|
||
let absent = self.programs.iter().all(|program| match program {
|
||
Program::Cyber { state, grants } => state == "inactive" && grants.is_empty(),
|
||
Program::Other => true,
|
||
});
|
||
if absent {
|
||
Notice::Apply
|
||
} else {
|
||
Notice::Limited
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Notice {
|
||
pub(crate) fn for_model(self, model: &str) -> Self {
|
||
match model {
|
||
// Same identifiers as the app's isDaybreakUnavailableModel.
|
||
"gpt-6-astra" | "gpt-6-astra-wm" => Self::Astra,
|
||
"gpt-5.6-sol" => self,
|
||
// Other model/access-program mappings are not established for the TUI.
|
||
_ => Self::Limited,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "daybreak_tests.rs"]
|
||
mod tests;
|