1use std::fmt;
12use std::sync::Arc;
13use std::time::Duration;
14
15use aither_core::llm::model::{Ability, Profile as ModelProfile};
16use aither_core::llm::{Event, LLMRequest, LanguageModel};
17use aither_openai::OpenAI;
18use async_lock::Mutex;
19use futures_lite::StreamExt as _;
20
21use crate::auth::{CodexCredentials, refresh_tokens};
22use crate::constant::{ACCOUNT_ID_HEADER, CODEX_BASE_URL, ORIGINATOR, RESIDENCY_HEADER};
23use crate::error::CodexError;
24
25const EXPIRY_SKEW: Duration = Duration::from_secs(60);
27
28const NO_RESIDENCY_CONSTRAINT: &str = "no_constraint";
30
31pub type OnRefresh = Arc<dyn Fn(&CodexCredentials) + Send + Sync>;
37
38#[derive(Clone)]
40pub struct Codex {
41 inner: Arc<Inner>,
42}
43
44struct Inner {
45 model: String,
46 base_url: String,
47 originator: String,
48 session_id: String,
49 cell: Mutex<Cell>,
50 on_refresh: Option<OnRefresh>,
51}
52
53struct Cell {
54 credentials: CodexCredentials,
55}
56
57impl Codex {
58 #[must_use]
60 pub fn new(credentials: CodexCredentials, model: impl Into<String>) -> Self {
61 Self {
62 inner: Arc::new(Inner {
63 model: model.into(),
64 base_url: CODEX_BASE_URL.into(),
65 originator: ORIGINATOR.into(),
66 session_id: session_id(),
67 cell: Mutex::new(Cell { credentials }),
68 on_refresh: None,
69 }),
70 }
71 }
72
73 #[must_use]
80 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
81 Arc::get_mut(&mut self.inner)
82 .expect("with_base_url must be called before the client is shared")
83 .base_url = base_url.into();
84 self
85 }
86
87 #[must_use]
93 pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
94 Arc::get_mut(&mut self.inner)
95 .expect("with_originator must be called before the client is shared")
96 .originator = originator.into();
97 self
98 }
99
100 #[must_use]
106 pub fn on_refresh(
107 mut self,
108 callback: impl Fn(&CodexCredentials) + Send + Sync + 'static,
109 ) -> Self {
110 Arc::get_mut(&mut self.inner)
111 .expect("on_refresh must be called before the client is shared")
112 .on_refresh = Some(Arc::new(callback));
113 self
114 }
115
116 pub async fn credentials(&self) -> CodexCredentials {
118 self.inner.cell.lock().await.credentials.clone()
119 }
120}
121
122impl Inner {
123 fn openai_client(&self, credentials: &CodexCredentials) -> OpenAI {
125 let mut builder = OpenAI::builder(&credentials.access_token)
126 .base_url(&self.base_url)
127 .model(&self.model)
128 .store(false)
129 .extra_header("originator", &self.originator)
130 .extra_header("session-id", &self.session_id);
131 if let Some(account_id) = &credentials.account_id {
132 builder = builder.extra_header(ACCOUNT_ID_HEADER, account_id);
133 }
134 if let Some(residency) = credentials
135 .residency
136 .as_deref()
137 .filter(|r| *r != NO_RESIDENCY_CONSTRAINT)
138 {
139 builder = builder.extra_header(RESIDENCY_HEADER, residency);
140 }
141 builder.build()
142 }
143
144 async fn valid_credentials(&self, force: bool) -> Result<CodexCredentials, CodexError> {
150 let mut cell = self.cell.lock().await;
151 if !force && !cell.credentials.is_expired(EXPIRY_SKEW) {
152 return Ok(cell.credentials.clone());
153 }
154 let fresh = refresh_tokens(&cell.credentials.refresh_token).await?;
155 tracing::debug!("refreshed ChatGPT access token");
156 if let Some(callback) = &self.on_refresh {
157 callback(&fresh);
158 }
159 cell.credentials = fresh.clone();
160 drop(cell);
161 Ok(fresh)
162 }
163
164 async fn request_client(&self, force_refresh: bool) -> Result<OpenAI, CodexError> {
166 let credentials = self.valid_credentials(force_refresh).await?;
167 Ok(self.openai_client(&credentials))
168 }
169}
170
171impl fmt::Debug for Codex {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("Codex")
174 .field("model", &self.inner.model)
175 .field("base_url", &self.inner.base_url)
176 .field("session_id", &self.inner.session_id)
177 .finish_non_exhaustive()
178 }
179}
180
181impl LanguageModel for Codex {
182 type Error = CodexError;
183
184 fn respond(
185 &self,
186 request: LLMRequest,
187 ) -> impl futures_core::Stream<Item = Result<Event, Self::Error>> + Send {
188 let inner = Arc::clone(&self.inner);
189 async_stream::stream! {
190 let mut retried = false;
191 loop {
192 let client = match inner.request_client(retried).await {
193 Ok(client) => client,
194 Err(err) => {
195 yield Err(err);
196 return;
197 }
198 };
199 let mut stream = std::pin::pin!(client.respond(request.clone()));
200 let mut unauthorized = false;
201 while let Some(event) = stream.next().await {
202 match event {
203 Err(err) if is_unauthorized(&err) && !retried => {
204 unauthorized = true;
205 break;
206 }
207 event => yield event.map_err(CodexError::OpenAI),
208 }
209 }
210 if !unauthorized {
211 return;
212 }
213 tracing::warn!("Codex request unauthorized; refreshing token and retrying");
214 retried = true;
215 }
216 }
217 }
218
219 fn profile(&self) -> impl core::future::Future<Output = ModelProfile> + Send {
220 let model = self.inner.model.clone();
221 async move {
222 let entry = aither_models::lookup(&model);
223 let mut profile = ModelProfile::new(
224 model.clone(),
225 "openai",
226 model,
227 "OpenAI model through a ChatGPT subscription",
228 entry
229 .and_then(aither_models::ModelEntry::max_input_tokens)
230 .unwrap_or(0),
231 )
232 .with_ability(Ability::ToolUse);
233 if let Some(entry) = entry {
234 for ability in entry.abilities() {
235 if !profile.abilities.contains(ability) {
236 profile.abilities.push(*ability);
237 }
238 }
239 }
240 profile
241 }
242 }
243}
244
245const fn is_unauthorized(err: &aither_openai::OpenAIError) -> bool {
247 matches!(
248 err,
249 aither_openai::OpenAIError::Http(zenwave::Error::Http { status, .. })
250 if status.as_u16() == 401
251 )
252}
253
254fn session_id() -> String {
256 let mut bytes = [0u8; 16];
257 getrandom::fill(&mut bytes).expect("secure random source unavailable");
258 bytes.iter().fold(String::with_capacity(32), |mut s, b| {
259 use std::fmt::Write as _;
260 let _ = write!(s, "{b:02x}");
261 s
262 })
263}