use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use aither_core::llm::model::{Ability, Profile as ModelProfile};
use aither_core::llm::{Event, LLMRequest, LanguageModel};
use aither_openai::OpenAI;
use async_lock::Mutex;
use futures_lite::StreamExt as _;
use crate::auth::{CodexCredentials, refresh_tokens};
use crate::constant::{ACCOUNT_ID_HEADER, CODEX_BASE_URL, ORIGINATOR, RESIDENCY_HEADER};
use crate::error::CodexError;
const EXPIRY_SKEW: Duration = Duration::from_secs(60);
const NO_RESIDENCY_CONSTRAINT: &str = "no_constraint";
pub type OnRefresh = Arc<dyn Fn(&CodexCredentials) + Send + Sync>;
#[derive(Clone)]
pub struct Codex {
inner: Arc<Inner>,
}
struct Inner {
model: String,
base_url: String,
originator: String,
session_id: String,
cell: Mutex<Cell>,
on_refresh: Option<OnRefresh>,
}
struct Cell {
credentials: CodexCredentials,
}
impl Codex {
#[must_use]
pub fn new(credentials: CodexCredentials, model: impl Into<String>) -> Self {
Self {
inner: Arc::new(Inner {
model: model.into(),
base_url: CODEX_BASE_URL.into(),
originator: ORIGINATOR.into(),
session_id: session_id(),
cell: Mutex::new(Cell { credentials }),
on_refresh: None,
}),
}
}
#[must_use]
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_base_url must be called before the client is shared")
.base_url = base_url.into();
self
}
#[must_use]
pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
Arc::get_mut(&mut self.inner)
.expect("with_originator must be called before the client is shared")
.originator = originator.into();
self
}
#[must_use]
pub fn on_refresh(
mut self,
callback: impl Fn(&CodexCredentials) + Send + Sync + 'static,
) -> Self {
Arc::get_mut(&mut self.inner)
.expect("on_refresh must be called before the client is shared")
.on_refresh = Some(Arc::new(callback));
self
}
pub async fn credentials(&self) -> CodexCredentials {
self.inner.cell.lock().await.credentials.clone()
}
}
impl Inner {
fn openai_client(&self, credentials: &CodexCredentials) -> OpenAI {
let mut builder = OpenAI::builder(&credentials.access_token)
.base_url(&self.base_url)
.model(&self.model)
.store(false)
.extra_header("originator", &self.originator)
.extra_header("session-id", &self.session_id);
if let Some(account_id) = &credentials.account_id {
builder = builder.extra_header(ACCOUNT_ID_HEADER, account_id);
}
if let Some(residency) = credentials
.residency
.as_deref()
.filter(|r| *r != NO_RESIDENCY_CONSTRAINT)
{
builder = builder.extra_header(RESIDENCY_HEADER, residency);
}
builder.build()
}
async fn valid_credentials(&self, force: bool) -> Result<CodexCredentials, CodexError> {
let mut cell = self.cell.lock().await;
if !force && !cell.credentials.is_expired(EXPIRY_SKEW) {
return Ok(cell.credentials.clone());
}
let fresh = refresh_tokens(&cell.credentials.refresh_token).await?;
tracing::debug!("refreshed ChatGPT access token");
if let Some(callback) = &self.on_refresh {
callback(&fresh);
}
cell.credentials = fresh.clone();
drop(cell);
Ok(fresh)
}
async fn request_client(&self, force_refresh: bool) -> Result<OpenAI, CodexError> {
let credentials = self.valid_credentials(force_refresh).await?;
Ok(self.openai_client(&credentials))
}
}
impl fmt::Debug for Codex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Codex")
.field("model", &self.inner.model)
.field("base_url", &self.inner.base_url)
.field("session_id", &self.inner.session_id)
.finish_non_exhaustive()
}
}
impl LanguageModel for Codex {
type Error = CodexError;
fn respond(
&self,
request: LLMRequest,
) -> impl futures_core::Stream<Item = Result<Event, Self::Error>> + Send {
let inner = Arc::clone(&self.inner);
async_stream::stream! {
let mut retried = false;
loop {
let client = match inner.request_client(retried).await {
Ok(client) => client,
Err(err) => {
yield Err(err);
return;
}
};
let mut stream = std::pin::pin!(client.respond(request.clone()));
let mut unauthorized = false;
while let Some(event) = stream.next().await {
match event {
Err(err) if is_unauthorized(&err) && !retried => {
unauthorized = true;
break;
}
event => yield event.map_err(CodexError::OpenAI),
}
}
if !unauthorized {
return;
}
tracing::warn!("Codex request unauthorized; refreshing token and retrying");
retried = true;
}
}
}
fn profile(&self) -> impl core::future::Future<Output = ModelProfile> + Send {
let model = self.inner.model.clone();
async move {
let entry = aither_models::lookup(&model);
let mut profile = ModelProfile::new(
model.clone(),
"openai",
model,
"OpenAI model through a ChatGPT subscription",
entry
.and_then(aither_models::ModelEntry::max_input_tokens)
.unwrap_or(0),
)
.with_ability(Ability::ToolUse);
if let Some(entry) = entry {
for ability in entry.abilities() {
if !profile.abilities.contains(ability) {
profile.abilities.push(*ability);
}
}
}
profile
}
}
}
const fn is_unauthorized(err: &aither_openai::OpenAIError) -> bool {
matches!(
err,
aither_openai::OpenAIError::Http(zenwave::Error::Http { status, .. })
if status.as_u16() == 401
)
}
fn session_id() -> String {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("secure random source unavailable");
bytes.iter().fold(String::with_capacity(32), |mut s, b| {
use std::fmt::Write as _;
let _ = write!(s, "{b:02x}");
s
})
}