1use std::collections::{HashMap, HashSet};
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::time::Duration;
7
8use alex_auth::{
9 codex_reserve_blocked, codex_reserve_pct, codex_reset_selection, now_ms, Account,
10 AccountPolicy, AccountPolicyMode, Vault,
11};
12use alex_core::{
13 compute_cost, conversation_root, parse_grpc_web_response, parse_since, parse_sse_usage,
14 parse_trace_tags, route_model, usage_from_json, validate_grpc_status_headers, window_label,
15 ClientFormat, Provider, TraceRecord, GROK_CREDITS_ENDPOINT, GROK_CREDITS_REQUEST_BODY,
16};
17use alex_store::{Store, TraceFilter};
18use axum::body::{Body, Bytes};
19use axum::extract::{ConnectInfo, Path, Query, State};
20use axum::http::{HeaderMap, HeaderValue, StatusCode};
21use axum::response::{IntoResponse, Response};
22use axum::routing::{get, post};
23use axum::Router;
24use futures_util::StreamExt;
25use serde_json::{json, Value};
26use tokio_stream::wrappers::ReceiverStream;
27
28const ANTHROPIC_BASE: &str = "https://api.anthropic.com";
29const OPENAI_BASE: &str = "https://api.openai.com";
30const CODEX_BASE: &str = "https://chatgpt.com/backend-api/codex";
31const XAI_BASE: &str = "https://cli-chat-proxy.grok.com/v1";
32const GROK_CLIENT_VERSION: &str = "0.2.77";
33const ANTHROPIC_OAUTH_BETA: &str = "oauth-2025-04-20";
34const GEMINI_CODE_ASSIST_BASE: &str = "https://cloudcode-pa.googleapis.com";
35const GEMINI_CODE_ASSIST_VERSION: &str = "v1internal";
36const GEMINI_API_BASE: &str = "https://generativelanguage.googleapis.com";
37const CODEX_AFFINITY_TTL_MS: i64 = 30 * 24 * 60 * 60 * 1000;
38const CODEX_AFFINITY_MAX_ENTRIES: usize = 10_000;
39
40#[derive(Debug, Clone)]
41pub struct DarioActive {
42 pub generation_id: String,
43 pub base_url: String,
44 pub api_key: String,
45}
46
47pub trait DarioRouter: Send + Sync {
48 fn active(&self) -> Option<DarioActive>;
49 fn begin(&self, generation_id: &str) -> Option<Box<dyn std::any::Any + Send>>;
50 fn status(&self) -> Value;
51 fn suspect(&self, generation_id: &str);
52}
53
54pub type UpdateApplyFuture =
55 Pin<Box<dyn Future<Output = Result<Value, UpdateApplyError>> + Send + 'static>>;
56
57#[derive(Debug)]
58pub enum UpdateApplyError {
59 Conflict(Value),
60 Failed(String),
61}
62
63pub trait DaemonUpdater: Send + Sync {
64 fn apply(&self) -> UpdateApplyFuture;
65}
66
67fn suspect_dario(state: &AppState, account: &Account) {
68 if account.kind != "dario" {
69 return;
70 }
71 if let (Some(dario), Some(gen)) = (&state.dario, account.id.strip_prefix("dario:")) {
72 dario.suspect(gen);
73 }
74}
75
76fn codex_limits_from_headers(headers: &reqwest::header::HeaderMap) -> Option<Value> {
77 const LIMIT_HEADERS: &[&str] = &[
78 "x-codex-primary-used-percent",
79 "x-codex-primary-window-minutes",
80 "x-codex-primary-reset-at",
81 "x-codex-secondary-used-percent",
82 "x-codex-secondary-window-minutes",
83 "x-codex-secondary-reset-at",
84 "x-codex-plan-type",
85 "x-codex-active-limit",
86 "x-codex-credits-balance",
87 "x-codex-credits-has-credits",
88 "x-codex-credits-unlimited",
89 ];
90 let mut safe = serde_json::Map::new();
91 for name in LIMIT_HEADERS {
92 if let Some(value) = headers.get(*name).and_then(|value| value.to_str().ok()) {
93 safe.insert((*name).to_string(), json!(value));
94 }
95 }
96 if safe.is_empty() {
97 return None;
98 }
99 let parsed = alex_core::parse_limit_headers(Provider::Openai, &Value::Object(safe));
100 let has_windows = parsed
101 .get("windows")
102 .and_then(Value::as_array)
103 .map(|windows| !windows.is_empty())
104 .unwrap_or(false);
105 let has_plan = parsed.get("plan").is_some_and(|value| !value.is_null());
106 (has_windows || has_plan).then_some(parsed)
107}
108
109pub struct AppState {
110 pub local_key: String,
111 pub vault: Arc<Vault>,
112 pub store: Arc<Store>,
113 pub http: reqwest::Client,
114 pub dario: Option<Arc<dyn DarioRouter>>,
115 pub in_flight: std::sync::atomic::AtomicI64,
116 pub started_ms: i64,
117 pub base_url: String,
118 pub anthropic_usage: std::sync::Mutex<UsageCache>,
119 pub xai_usage: std::sync::Mutex<UsageCache>,
120 pub logins: alex_auth::sessions::LoginManager,
121 pub run_keys: std::sync::RwLock<HashMap<String, CachedRunKey>>,
122 pub update_status: Arc<tokio::sync::RwLock<Option<Value>>>,
123 pub daemon_updater: std::sync::RwLock<Option<Arc<dyn DaemonUpdater>>>,
124 codex_affinity: std::sync::Mutex<CodexAffinityCache>,
125 codex_affinity_locks:
126 std::sync::Mutex<HashMap<String, std::sync::Weak<tokio::sync::Mutex<()>>>>,
127}
128
129#[derive(Debug, Clone)]
130pub struct CachedRunKey {
131 pub run_id: Option<String>,
132 pub tags_json: Option<String>,
133 pub expires_ms: Option<i64>,
134}
135
136#[derive(Debug, Clone)]
137struct CodexAffinityEntry {
138 account_id: String,
139 expires_at_ms: i64,
140}
141
142#[derive(Debug)]
143struct CodexAffinityCache {
144 entries: HashMap<String, CodexAffinityEntry>,
145 ttl_ms: i64,
146 max_entries: usize,
147}
148
149impl Default for CodexAffinityCache {
150 fn default() -> Self {
151 Self {
152 entries: HashMap::new(),
153 ttl_ms: CODEX_AFFINITY_TTL_MS,
154 max_entries: CODEX_AFFINITY_MAX_ENTRIES,
155 }
156 }
157}
158
159impl CodexAffinityCache {
160 fn preferred(&mut self, session_id: &str, now: i64) -> Option<String> {
161 let entry = self.entries.get_mut(session_id)?;
162 if entry.expires_at_ms <= now {
163 self.entries.remove(session_id);
164 return None;
165 }
166 entry.expires_at_ms = now.saturating_add(self.ttl_ms);
169 Some(entry.account_id.clone())
170 }
171
172 fn bind(&mut self, session_id: &str, account_id: &str, now: i64) {
173 self.entries.retain(|_, entry| entry.expires_at_ms > now);
174 if !self.entries.contains_key(session_id) && self.entries.len() >= self.max_entries {
175 if let Some(oldest) = self
176 .entries
177 .iter()
178 .min_by_key(|(_, entry)| entry.expires_at_ms)
179 .map(|(session, _)| session.clone())
180 {
181 self.entries.remove(&oldest);
182 }
183 }
184 self.entries.insert(
185 session_id.to_string(),
186 CodexAffinityEntry {
187 account_id: account_id.to_string(),
188 expires_at_ms: now.saturating_add(self.ttl_ms),
189 },
190 );
191 }
192
193 fn unbind(&mut self, session_id: &str) {
194 self.entries.remove(session_id);
195 }
196}
197
198fn preferred_codex_account(state: &AppState, session_id: Option<&str>) -> Option<String> {
199 let session_id = session_id.filter(|value| !value.is_empty())?;
200 state
201 .codex_affinity
202 .lock()
203 .unwrap_or_else(|poisoned| poisoned.into_inner())
204 .preferred(session_id, now_ms())
205}
206
207fn codex_affinity_lock(state: &AppState, session_id: &str) -> Arc<tokio::sync::Mutex<()>> {
208 let mut locks = state
209 .codex_affinity_locks
210 .lock()
211 .unwrap_or_else(|poisoned| poisoned.into_inner());
212 locks.retain(|_, lock| lock.strong_count() > 0);
213 if let Some(lock) = locks.get(session_id).and_then(std::sync::Weak::upgrade) {
214 return lock;
215 }
216 let lock = Arc::new(tokio::sync::Mutex::new(()));
217 locks.insert(session_id.to_string(), Arc::downgrade(&lock));
218 lock
219}
220
221fn bind_codex_account(state: &AppState, session_id: Option<&str>, account: &Account) {
222 let Some(session_id) = session_id.filter(|value| !value.is_empty()) else {
223 return;
224 };
225 if account.provider != Provider::Openai {
226 return;
227 }
228 let mut affinity = state
229 .codex_affinity
230 .lock()
231 .unwrap_or_else(|poisoned| poisoned.into_inner());
232 if account.kind == "oauth" {
233 affinity.bind(session_id, &account.id, now_ms());
234 } else {
235 affinity.unbind(session_id);
239 }
240}
241
242struct InFlight(Arc<AppState>);
243
244impl InFlight {
245 fn new(state: &Arc<AppState>) -> Self {
246 state
247 .in_flight
248 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
249 Self(state.clone())
250 }
251}
252
253impl Drop for InFlight {
254 fn drop(&mut self) {
255 self.0
256 .in_flight
257 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
258 }
259}
260
261pub fn build_state(
262 local_key: String,
263 vault: Arc<Vault>,
264 store: Arc<Store>,
265 dario: Option<Arc<dyn DarioRouter>>,
266 base_url: String,
267) -> Arc<AppState> {
268 let http = reqwest::Client::builder()
269 .connect_timeout(Duration::from_secs(15))
270 .build()
271 .expect("reqwest client");
272 Arc::new(AppState {
273 local_key,
274 vault,
275 store,
276 http,
277 dario,
278 in_flight: std::sync::atomic::AtomicI64::new(0),
279 started_ms: now_ms(),
280 base_url,
281 anthropic_usage: std::sync::Mutex::new(UsageCache::default()),
282 xai_usage: std::sync::Mutex::new(UsageCache::default()),
283 logins: alex_auth::sessions::LoginManager::default(),
284 run_keys: std::sync::RwLock::new(HashMap::new()),
285 update_status: Arc::new(tokio::sync::RwLock::new(None)),
286 daemon_updater: std::sync::RwLock::new(None),
287 codex_affinity: std::sync::Mutex::new(CodexAffinityCache::default()),
288 codex_affinity_locks: std::sync::Mutex::new(HashMap::new()),
289 })
290}
291
292pub fn set_daemon_updater(state: &Arc<AppState>, updater: Arc<dyn DaemonUpdater>) {
293 if let Ok(mut slot) = state.daemon_updater.write() {
294 *slot = Some(updater);
295 }
296}
297
298async fn require_local_key(
299 State(state): State<Arc<AppState>>,
300 req: axum::extract::Request,
301 next: axum::middleware::Next,
302) -> Response {
303 let ok = client_key(req.headers())
304 .map(|k| k == state.local_key)
305 .unwrap_or(false);
306 if !ok {
307 return error_response(
308 StatusCode::UNAUTHORIZED,
309 "admin routes require x-api-key: <local_key>",
310 );
311 }
312 next.run(req).await
313}
314
315pub fn router(state: Arc<AppState>) -> Router {
316 let gated = Router::new()
321 .route(
322 "/admin/run-keys",
323 get(admin_run_keys_list).post(admin_run_keys_create),
324 )
325 .route(
326 "/admin/run-keys/{id}",
327 axum::routing::delete(admin_run_keys_revoke),
328 )
329 .route("/admin/storage", get(admin_storage))
330 .route("/admin/storage/prune", post(admin_storage_prune))
331 .route("/admin/traces", get(admin_traces))
332 .route("/admin/accounts", get(admin_accounts))
333 .route("/admin/accounts/analytics", get(admin_account_analytics))
334 .route(
335 "/admin/accounts/routing/openai",
336 get(admin_codex_routing).put(admin_codex_routing_update),
337 )
338 .route(
339 "/admin/accounts/{id}",
340 axum::routing::delete(admin_account_remove).put(admin_account_update),
341 )
342 .route("/admin/auth/gemini-key", post(admin_auth_gemini_key))
343 .route("/admin/health", get(admin_health))
344 .route("/admin/analytics", get(admin_analytics))
345 .route("/admin/limits", get(admin_limits))
346 .route("/admin/update", get(admin_update).post(admin_update_apply))
347 .route("/admin/dario", get(admin_dario))
348 .route("/admin/dario/prompt-caches", get(admin_dario_prompt_caches))
349 .route(
350 "/admin/dario/prompt-caches/{key}",
351 axum::routing::delete(admin_dario_prompt_cache_delete),
352 )
353 .route("/admin/auth/import", post(admin_auth_import))
354 .route("/admin/auth/login/start", post(admin_auth_login_start))
355 .route("/admin/auth/login/complete", post(admin_auth_login_complete))
356 .route("/admin/auth/login/{id}", get(admin_auth_login_status))
357 .route("/traces/search", get(traces_search))
358 .route("/traces/export.ndjson", get(traces_export))
359 .route("/traces/sessions", get(traces_sessions))
360 .route(
361 "/traces/sessions/{session_id}/transcript",
362 get(traces_session_transcript),
363 )
364 .route("/traces/{id}", get(trace_get).delete(trace_delete))
365 .route("/traces/{id}/reply.md", get(trace_reply_md))
366 .route("/traces/{id}/body/{kind}", get(trace_body))
367 .route("/traces/runs/{run_id}", get(traces_run_summary))
368 .route("/traces/runs/{run_id}/events", get(traces_run_events))
369 .route("/traces/runs/{run_id}/export.ndjson", get(traces_run_export))
370 .route("/traces/runs/{run_id}/artifacts", get(traces_run_artifacts))
371 .route_layer(axum::middleware::from_fn_with_state(
372 state.clone(),
373 require_local_key,
374 ));
375
376 Router::new()
377 .route("/health", get(health))
378 .route("/connect", get(connect_info))
379 .route("/v1/models", get(models))
380 .route("/v1/messages", post(anthropic_messages))
381 .route("/v1/chat/completions", post(openai_chat))
382 .route("/v1/responses", post(openai_responses))
383 .route("/chat/completions", post(openai_chat))
384 .route("/responses", post(openai_responses))
385 .route("/v1beta/models/{model_action}", post(gemini_generate))
386 .merge(gated)
387 .layer(axum::extract::DefaultBodyLimit::max(64 * 1024 * 1024))
388 .with_state(state)
389}
390
391async fn admin_auth_import(
392 State(state): State<Arc<AppState>>,
393 body: Option<axum::Json<Value>>,
394) -> Response {
395 let source = body
396 .as_ref()
397 .and_then(|b| b.0["source"].as_str().or(b.0["provider"].as_str()))
398 .unwrap_or("all")
399 .to_string();
400 match alex_auth::import_all(&state.vault, &source).await {
401 Ok(outcomes) => {
402 let items: Vec<Value> = outcomes
403 .iter()
404 .map(|o| {
405 json!({
406 "source": o.source,
407 "imported": o.imported,
408 "note": o.note,
409 })
410 })
411 .collect();
412 axum::Json(json!({"outcomes": items})).into_response()
413 }
414 Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
415 }
416}
417
418async fn admin_account_remove(
419 State(state): State<Arc<AppState>>,
420 Path(id): Path<String>,
421) -> Response {
422 match state.vault.remove(&id).await {
423 Ok(true) => axum::Json(json!({"removed": id})).into_response(),
424 Ok(false) => error_response(StatusCode::NOT_FOUND, &format!("unknown account '{id}'")),
425 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
426 }
427}
428
429async fn admin_account_update(
430 State(state): State<Arc<AppState>>,
431 Path(id): Path<String>,
432 body: axum::Json<Value>,
433) -> Response {
434 let Some(paused) = body.0["paused"].as_bool() else {
435 return error_response(StatusCode::BAD_REQUEST, "missing boolean 'paused'");
436 };
437 match state.vault.set_paused(&id, paused).await {
438 Ok(()) => axum::Json(json!({"id": id, "paused": paused})).into_response(),
439 Err(e) if e.to_string().starts_with("unknown account") => {
440 error_response(StatusCode::NOT_FOUND, &e.to_string())
441 }
442 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
443 }
444}
445
446async fn admin_auth_gemini_key(
447 State(state): State<Arc<AppState>>,
448 body: axum::Json<Value>,
449) -> Response {
450 let Some(key) = body.0["key"].as_str().map(str::trim).filter(|k| !k.is_empty()) else {
451 return error_response(StatusCode::BAD_REQUEST, "missing 'key'");
452 };
453 let account = Account {
454 id: alex_auth::named_account_id(Provider::Gemini, "api_key", "default"),
455 provider: Provider::Gemini,
456 kind: "api_key".into(),
457 name: "default".into(),
458 description: None,
459 paused: false,
460 label: Some("gemini (AI Studio key)".into()),
461 access_token: None,
462 refresh_token: None,
463 id_token: None,
464 api_key: Some(key.to_string()),
465 expires_at_ms: None,
466 last_refresh_ms: None,
467 account_meta: Value::Null,
468 cooldown_until_ms: None,
469 status: "active".into(),
470 path: None,
471 };
472 match state.vault.upsert(account).await {
473 Ok(()) => axum::Json(json!({"saved": "gemini-api-key"})).into_response(),
474 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
475 }
476}
477
478async fn admin_auth_login_start(
479 State(state): State<Arc<AppState>>,
480 body: axum::Json<Value>,
481) -> Response {
482 let Some(provider) = body.0["provider"].as_str() else {
483 return error_response(StatusCode::BAD_REQUEST, "missing 'provider'");
484 };
485 if body.0["auto_identity"].as_bool() == Some(true) {
486 return match state.logins.start_auto(state.vault.clone(), provider).await {
487 Ok(snapshot) => axum::Json(snapshot).into_response(),
488 Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
489 };
490 }
491 let name = body.0["name"].as_str().unwrap_or("default");
492 match state.logins.start(state.vault.clone(), provider, name).await {
493 Ok(snapshot) => axum::Json(snapshot).into_response(),
494 Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
495 }
496}
497
498async fn admin_auth_login_complete(
499 State(state): State<Arc<AppState>>,
500 body: axum::Json<Value>,
501) -> Response {
502 let (Some(id), Some(input)) = (body.0["login_id"].as_str(), body.0["input"].as_str()) else {
503 return error_response(StatusCode::BAD_REQUEST, "missing 'login_id' or 'input'");
504 };
505 match state.logins.complete(state.vault.clone(), id, input).await {
506 Ok(snapshot) => axum::Json(snapshot).into_response(),
507 Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
508 }
509}
510
511async fn admin_auth_login_status(
512 State(state): State<Arc<AppState>>,
513 axum::extract::Path(id): axum::extract::Path<String>,
514) -> Response {
515 match state.logins.status(&id).await {
516 Some(snapshot) => axum::Json(snapshot).into_response(),
517 None => error_response(StatusCode::NOT_FOUND, "unknown or expired login session"),
518 }
519}
520
521async fn health(State(state): State<Arc<AppState>>) -> impl IntoResponse {
522 axum::Json(json!({
523 "status": "ok",
524 "service": "alexandria",
525 "version": env!("CARGO_PKG_VERSION"),
526 "in_flight": state.in_flight.load(std::sync::atomic::Ordering::SeqCst),
527 "uptime_s": (now_ms() - state.started_ms) / 1000,
528 "dario": state.dario.is_some(),
529 }))
530}
531
532fn rewrite_host(base_url: &str, host: &str) -> String {
533 let port = base_url.rsplit(':').next().unwrap_or("4100");
534 format!("http://{host}:{port}")
535}
536
537pub fn connect_payload(base_url: &str, local_key: &str) -> (Value, String) {
538 let base = base_url.trim_end_matches('/').to_string();
539 let v1 = format!("{base}/v1");
540 let exports = format!(
541 "export ANTHROPIC_BASE_URL={base}\nexport ANTHROPIC_API_KEY={local_key}\n\
542 export OPENAI_BASE_URL={v1}\nexport OPENAI_API_KEY={local_key}\n\
543 export XAI_API_KEY={local_key}\nexport GROK_MODELS_BASE_URL={v1}\n\
544 export GOOGLE_GEMINI_BASE_URL={base}\nexport GOOGLE_GENAI_API_VERSION=v1beta\n\
545 export GEMINI_API_KEY={local_key}\nexport GEMINI_API_KEY_AUTH_MECHANISM=bearer\n\
546 export GOOGLE_GENAI_USE_GCA=false\n"
547 );
548 let payload = json!({
549 "service": "alexandria",
550 "base_url": base,
551 "api_key": local_key,
552 "anthropic": {"base_url": base, "env": {"ANTHROPIC_BASE_URL": base, "ANTHROPIC_API_KEY": local_key}},
553 "openai": {"base_url": v1, "env": {"OPENAI_BASE_URL": v1, "OPENAI_API_KEY": local_key}},
554 "xai": {"base_url": v1, "env": {"XAI_API_KEY": local_key, "GROK_MODELS_BASE_URL": v1}},
555 "gemini": {"base_url": base, "env": {
556 "GOOGLE_GEMINI_BASE_URL": base,
557 "GOOGLE_GENAI_API_VERSION": "v1beta",
558 "GEMINI_API_KEY": local_key,
559 "GEMINI_API_KEY_AUTH_MECHANISM": "bearer",
560 "GOOGLE_GENAI_USE_GCA": "false",
561 }},
562 "exports": exports,
563 });
564 (payload, exports)
565}
566
567async fn connect_info(
568 State(state): State<Arc<AppState>>,
569 axum::extract::ConnectInfo(peer): axum::extract::ConnectInfo<std::net::SocketAddr>,
570 Query(q): Query<HashMap<String, String>>,
571) -> Response {
572 if !peer.ip().is_loopback() {
573 return error_response(
574 StatusCode::FORBIDDEN,
575 "connection info is only served to loopback clients",
576 );
577 }
578 let base = match q.get("host").map(String::as_str) {
579 Some("docker") => rewrite_host(&state.base_url, "host.docker.internal"),
580 Some(host) if !host.is_empty() => rewrite_host(&state.base_url, host),
581 _ => state.base_url.clone(),
582 };
583 let (payload, exports) = connect_payload(&base, &state.local_key);
584 if q.get("format").map(|f| f == "env").unwrap_or(false) {
585 return Response::builder()
586 .status(StatusCode::OK)
587 .header("content-type", "text/plain")
588 .body(Body::from(exports))
589 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
590 }
591 axum::Json(payload).into_response()
592}
593
594async fn models(State(state): State<Arc<AppState>>) -> impl IntoResponse {
595 let mut ids = state.store.pricing_models();
596 for (alias, _) in alex_core::model_aliases() {
597 ids.push((*alias).to_string());
598 }
599 for id in ids.clone() {
600 ids.push(format!("alexandria/{id}"));
601 }
602 let data: Vec<Value> = ids
603 .into_iter()
604 .map(|m| json!({"id": m, "object": "model", "owned_by": "alexandria"}))
605 .collect();
606 axum::Json(json!({"object": "list", "data": data}))
607}
608
609async fn admin_analytics(
610 State(state): State<Arc<AppState>>,
611 Query(q): Query<HashMap<String, String>>,
612) -> Response {
613 let minutes: i64 = q
614 .get("since_minutes")
615 .and_then(|s| s.parse().ok())
616 .unwrap_or(60);
617 match state.store.analytics(now_ms() - minutes * 60_000) {
618 Ok(v) => axum::Json(v).into_response(),
619 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
620 }
621}
622
623async fn admin_account_analytics(
624 State(state): State<Arc<AppState>>,
625 Query(q): Query<HashMap<String, String>>,
626) -> Response {
627 let minutes: i64 = q
628 .get("since_minutes")
629 .and_then(|s| s.parse().ok())
630 .unwrap_or(24 * 60)
631 .clamp(1, 30 * 24 * 60);
632 let bucket_minutes: i64 = q
633 .get("bucket_minutes")
634 .and_then(|s| s.parse().ok())
635 .unwrap_or(60)
636 .clamp(1, 24 * 60);
637 match state
638 .store
639 .account_analytics(now_ms() - minutes * 60_000, bucket_minutes * 60_000)
640 {
641 Ok(v) => axum::Json(v).into_response(),
642 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
643 }
644}
645
646const USAGE_CACHE_TTL_MS: i64 = 300_000;
647const USAGE_BACKOFF_BASE_MS: i64 = 60_000;
648const USAGE_BACKOFF_MAX_MS: i64 = 3_600_000;
649
650#[derive(Default)]
651pub struct UsageCache {
652 fetched_at_ms: i64,
653 entry: Option<Value>,
654 cooldown_until_ms: i64,
655 failures: u32,
656}
657
658fn usage_backoff_ms(failures: u32, retry_after_ms: Option<i64>) -> i64 {
659 let exp = USAGE_BACKOFF_BASE_MS
660 .saturating_mul(1i64 << failures.saturating_sub(1).min(6))
661 .min(USAGE_BACKOFF_MAX_MS);
662 exp.max(retry_after_ms.unwrap_or(0))
663}
664
665async fn anthropic_usage_entry(state: &Arc<AppState>) -> Option<Value> {
666 let account = state.vault.account_for(Provider::Anthropic, true).await.ok()?;
667 if account.kind != "oauth" {
668 return None;
669 }
670 let token = account.access_token.as_deref()?.to_string();
671 {
672 let cache = state.anthropic_usage.lock().unwrap();
673 if cache.entry.is_some() && now_ms() < cache.fetched_at_ms + USAGE_CACHE_TTL_MS {
674 return cache.entry.clone();
675 }
676 if now_ms() < cache.cooldown_until_ms {
677 return cache.entry.clone();
678 }
679 }
680 let result = state
681 .http
682 .get(format!("{ANTHROPIC_BASE}/api/oauth/usage"))
683 .header("authorization", format!("Bearer {token}"))
684 .header("anthropic-beta", ANTHROPIC_OAUTH_BETA)
685 .header("accept", "application/json")
686 .header("user-agent", "claude-cli/2.1.202 (external, cli)")
687 .send()
688 .await;
689 match result {
690 Ok(resp) if resp.status().is_success() => {
691 let raw: Value = resp.json().await.unwrap_or(Value::Null);
692 let mut windows = Vec::new();
693 for (name, key) in [
694 ("5h", "five_hour"),
695 ("7d", "seven_day"),
696 ("7d opus", "seven_day_opus"),
697 ("7d sonnet", "seven_day_sonnet"),
698 ] {
699 let w = &raw[key];
700 if w.is_object() {
701 windows.push(json!({
702 "window": name,
703 "used_pct": w["utilization"],
704 "resets_at": w["resets_at"],
705 }));
706 }
707 }
708 let entry = json!({
709 "provider": "anthropic",
710 "source": "oauth usage endpoint",
711 "plan": account.label,
712 "windows": windows,
713 "extra_usage": raw["extra_usage"],
714 });
715 let mut cache = state.anthropic_usage.lock().unwrap();
716 cache.fetched_at_ms = now_ms();
717 cache.cooldown_until_ms = 0;
718 cache.failures = 0;
719 cache.entry = Some(entry.clone());
720 Some(entry)
721 }
722 Ok(resp) => {
723 let retry_after = resp
724 .headers()
725 .get("retry-after")
726 .and_then(|v| v.to_str().ok())
727 .and_then(|v| v.parse::<i64>().ok())
728 .map(|s| s.clamp(30, 3600) * 1000);
729 let mut cache = state.anthropic_usage.lock().unwrap();
730 cache.failures += 1;
731 let cooldown = usage_backoff_ms(cache.failures, retry_after);
732 cache.cooldown_until_ms = now_ms() + cooldown;
733 tracing::debug!(
734 status = resp.status().as_u16(),
735 failures = cache.failures,
736 cooldown_ms = cooldown,
737 "anthropic usage endpoint unavailable; backing off"
738 );
739 cache.entry.clone()
740 }
741 Err(_) => {
742 let mut cache = state.anthropic_usage.lock().unwrap();
743 cache.failures += 1;
744 let cooldown = usage_backoff_ms(cache.failures, None);
745 cache.cooldown_until_ms = now_ms() + cooldown;
746 cache.entry.clone()
747 }
748 }
749}
750
751async fn xai_usage_entry(state: &Arc<AppState>) -> Option<Value> {
754 let account = state.vault.account_for(Provider::Xai, true).await.ok()?;
755 if account.kind != "oauth" {
756 return None;
757 }
758 let token = account.access_token.as_deref()?.to_string();
759 {
760 let cache = state.xai_usage.lock().unwrap();
761 if cache.entry.is_some() && now_ms() < cache.fetched_at_ms + USAGE_CACHE_TTL_MS {
762 return cache.entry.clone();
763 }
764 if now_ms() < cache.cooldown_until_ms {
765 return cache.entry.clone();
766 }
767 }
768
769 let result = state
770 .http
771 .post(GROK_CREDITS_ENDPOINT)
772 .header("authorization", format!("Bearer {token}"))
773 .header("origin", "https://grok.com")
774 .header("referer", "https://grok.com/?_s=usage")
775 .header("accept", "*/*")
776 .header("content-type", "application/grpc-web+proto")
777 .header("x-grpc-web", "1")
778 .header("x-user-agent", "connect-es/2.1.1")
779 .header("user-agent", "Alexandria")
780 .body(GROK_CREDITS_REQUEST_BODY.to_vec())
781 .timeout(Duration::from_secs(15))
782 .send()
783 .await;
784
785 match result {
786 Ok(resp) if resp.status().is_success() => {
787 let headers_for_grpc: Vec<(String, String)> = resp
788 .headers()
789 .iter()
790 .filter_map(|(k, v)| {
791 let key = k.as_str();
792 if key.starts_with("grpc-") {
793 v.to_str().ok().map(|val| (key.to_string(), val.to_string()))
794 } else {
795 None
796 }
797 })
798 .collect();
799 if let Err(e) = validate_grpc_status_headers(headers_for_grpc) {
800 tracing::debug!(error = %e, "xai grok credits grpc header status failed");
801 let mut cache = state.xai_usage.lock().unwrap();
802 cache.failures += 1;
803 let cooldown = usage_backoff_ms(cache.failures, None);
804 cache.cooldown_until_ms = now_ms() + cooldown;
805 return cache.entry.clone();
806 }
807 let body = match resp.bytes().await {
808 Ok(b) => b,
809 Err(e) => {
810 tracing::debug!(error = %e, "xai grok credits body read failed");
811 let mut cache = state.xai_usage.lock().unwrap();
812 cache.failures += 1;
813 let cooldown = usage_backoff_ms(cache.failures, None);
814 cache.cooldown_until_ms = now_ms() + cooldown;
815 return cache.entry.clone();
816 }
817 };
818 let now_s = now_ms() / 1000;
819 match parse_grpc_web_response(&body, now_s) {
820 Ok(snap) => {
821 let label = window_label(snap.resets_at_s, now_s);
822 let mut window = json!({
823 "window": label,
824 "used_pct": snap.used_percent,
825 });
826 if let Some(ts) = snap.resets_at_s {
827 window["resets_at_s"] = json!(ts);
828 }
829 let entry = json!({
830 "provider": "xai",
831 "source": "grok web billing",
832 "plan": account.label,
833 "windows": [window],
834 });
835 let mut cache = state.xai_usage.lock().unwrap();
836 cache.fetched_at_ms = now_ms();
837 cache.cooldown_until_ms = 0;
838 cache.failures = 0;
839 cache.entry = Some(entry.clone());
840 Some(entry)
841 }
842 Err(e) => {
843 tracing::debug!(error = %e, "xai grok credits parse failed");
844 let mut cache = state.xai_usage.lock().unwrap();
845 cache.failures += 1;
846 let cooldown = usage_backoff_ms(cache.failures, None);
847 cache.cooldown_until_ms = now_ms() + cooldown;
848 cache.entry.clone()
849 }
850 }
851 }
852 Ok(resp) => {
853 let status = resp.status().as_u16();
854 let retry_after = resp
855 .headers()
856 .get("retry-after")
857 .and_then(|v| v.to_str().ok())
858 .and_then(|v| v.parse::<i64>().ok())
859 .map(|s| s.clamp(30, 3600) * 1000);
860 let mut cache = state.xai_usage.lock().unwrap();
861 cache.failures += 1;
862 let cooldown = usage_backoff_ms(cache.failures, retry_after);
863 cache.cooldown_until_ms = now_ms() + cooldown;
864 tracing::debug!(
865 status,
866 failures = cache.failures,
867 cooldown_ms = cooldown,
868 "xai grok web billing unavailable; backing off"
869 );
870 cache.entry.clone()
871 }
872 Err(e) => {
873 tracing::debug!(error = %e, "xai grok web billing request failed");
874 let mut cache = state.xai_usage.lock().unwrap();
875 cache.failures += 1;
876 let cooldown = usage_backoff_ms(cache.failures, None);
877 cache.cooldown_until_ms = now_ms() + cooldown;
878 cache.entry.clone()
879 }
880 }
881}
882
883pub async fn limits_snapshot(state: &Arc<AppState>) -> Value {
884 let mut providers: Vec<Value> = Vec::new();
885 if let Some(entry) = anthropic_usage_entry(state).await {
886 providers.push(entry);
887 }
888 if let Some(entry) = xai_usage_entry(state).await {
889 providers.push(entry);
890 }
891 for (provider_str, ts_ms, headers_json) in
892 state.store.latest_provider_headers().unwrap_or_default()
893 {
894 if providers
895 .iter()
896 .any(|p| p["provider"].as_str() == Some(&provider_str))
897 {
898 continue;
899 }
900 let Some(provider) = Provider::from_str_loose(&provider_str) else {
901 continue;
902 };
903 let headers: Value = serde_json::from_str(&headers_json).unwrap_or(Value::Null);
904 let mut parsed = alex_core::parse_limit_headers(provider, &headers);
905 if let Some(o) = parsed.as_object_mut() {
906 o.insert("provider".into(), json!(provider_str));
907 o.insert("source".into(), json!("captured response headers"));
908 o.insert("observed_at_ms".into(), json!(ts_ms));
909 providers.push(parsed);
910 }
911 }
912 providers.sort_by_key(|p| p["provider"].as_str().unwrap_or("").to_string());
913 json!({"providers": providers})
914}
915
916async fn admin_limits(State(state): State<Arc<AppState>>) -> Response {
917 axum::Json(limits_snapshot(&state).await).into_response()
918}
919
920async fn admin_update(State(state): State<Arc<AppState>>) -> Response {
921 let stored = state.update_status.read().await.clone();
922 let mut body = json!({
923 "current": env!("CARGO_PKG_VERSION"),
924 "latest": null,
925 "update_available": false,
926 "checked_at_ms": null,
927 });
928 if let Some(Value::Object(fields)) = stored {
929 if let Some(obj) = body.as_object_mut() {
930 obj.extend(fields);
931 obj.insert("current".into(), json!(env!("CARGO_PKG_VERSION")));
932 }
933 }
934 axum::Json(body).into_response()
935}
936
937async fn admin_update_apply(State(state): State<Arc<AppState>>) -> Response {
938 let updater = state
939 .daemon_updater
940 .read()
941 .ok()
942 .and_then(|slot| slot.clone());
943 let Some(updater) = updater else {
944 return error_response(StatusCode::SERVICE_UNAVAILABLE, "daemon updater is not configured");
945 };
946 match updater.apply().await {
947 Ok(body) => {
948 let status = if body["applying"].as_bool() == Some(true) {
949 StatusCode::ACCEPTED
950 } else {
951 StatusCode::OK
952 };
953 (status, axum::Json(body)).into_response()
954 }
955 Err(UpdateApplyError::Conflict(body)) => {
956 (StatusCode::CONFLICT, axum::Json(body)).into_response()
957 }
958 Err(UpdateApplyError::Failed(message)) => {
959 error_response(StatusCode::INTERNAL_SERVER_ERROR, &message)
960 }
961 }
962}
963
964async fn admin_dario(State(state): State<Arc<AppState>>) -> Response {
965 match &state.dario {
966 Some(d) => {
967 let mut status = d.status();
968 if let Some(obj) = status.as_object_mut() {
969 obj.insert("prompt_caches".into(), json!(dario_prompt_caches(&state)));
970 }
971 axum::Json(status).into_response()
972 }
973 None => error_response(StatusCode::NOT_FOUND, "dario mode is not enabled"),
974 }
975}
976
977fn dario_prompt_cache_dir(state: &AppState) -> PathBuf {
978 state.store.data_dir.join("dario-prompt-cache")
979}
980
981fn dario_prompt_cache_key_valid(key: &str) -> bool {
982 !key.is_empty()
983 && key.len() <= 128
984 && key
985 .bytes()
986 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
987}
988
989fn dario_prompt_cache_summary(path: PathBuf) -> Option<Value> {
990 let raw = std::fs::read_to_string(&path).ok()?;
991 let value: Value = serde_json::from_str(&raw).ok()?;
992 let key = path.file_stem()?.to_string_lossy().to_string();
993 let runs: Vec<Value> = value["runs"]
994 .as_array()
995 .map(|runs| runs.iter().rev().take(12).cloned().collect())
996 .unwrap_or_default();
997 Some(json!({
998 "key": value["key"].as_str().unwrap_or(&key),
999 "model": value["model"],
1000 "source": value["source"],
1001 "captured_at": value["captured_at"],
1002 "last_used_at": value["last_used_at"],
1003 "trace_id": value["trace_id"],
1004 "claude_bin": value["claude_bin"],
1005 "claude_version": value["claude_version"],
1006 "system_prompt_chars": value["system_prompt_chars"],
1007 "agent_identity_chars": value["agent_identity_chars"],
1008 "path": path.to_string_lossy(),
1009 "runs": runs,
1010 }))
1011}
1012
1013fn dario_prompt_caches(state: &AppState) -> Vec<Value> {
1014 let dir = dario_prompt_cache_dir(state);
1015 let mut caches: Vec<Value> = std::fs::read_dir(dir)
1016 .ok()
1017 .into_iter()
1018 .flatten()
1019 .flatten()
1020 .filter_map(|entry| {
1021 let path = entry.path();
1022 (path.extension().and_then(|s| s.to_str()) == Some("json"))
1023 .then(|| dario_prompt_cache_summary(path))
1024 .flatten()
1025 })
1026 .collect();
1027 caches.sort_by(|a, b| {
1028 b["last_used_at"]
1029 .as_str()
1030 .unwrap_or("")
1031 .cmp(a["last_used_at"].as_str().unwrap_or(""))
1032 });
1033 caches
1034}
1035
1036async fn admin_dario_prompt_caches(State(state): State<Arc<AppState>>) -> Response {
1037 axum::Json(json!({"prompt_caches": dario_prompt_caches(&state)})).into_response()
1038}
1039
1040async fn admin_dario_prompt_cache_delete(
1041 State(state): State<Arc<AppState>>,
1042 Path(key): Path<String>,
1043) -> Response {
1044 if !dario_prompt_cache_key_valid(&key) {
1045 return error_response(StatusCode::BAD_REQUEST, "invalid prompt cache key");
1046 }
1047 let path = dario_prompt_cache_dir(&state).join(format!("{key}.json"));
1048 match std::fs::remove_file(&path) {
1049 Ok(()) => axum::Json(json!({"deleted": true, "key": key})).into_response(),
1050 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1051 error_response(StatusCode::NOT_FOUND, "unknown prompt cache")
1052 }
1053 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1054 }
1055}
1056
1057async fn admin_storage(State(state): State<Arc<AppState>>) -> Response {
1058 match state.store.disk_usage() {
1059 Ok(v) => axum::Json(v).into_response(),
1060 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1061 }
1062}
1063
1064async fn admin_storage_prune(
1065 State(state): State<Arc<AppState>>,
1066 body: axum::Json<Value>,
1067) -> Response {
1068 let now = now_ms();
1069 let cutoff = match &body["older_than_ms"] {
1070 Value::Null => match body["older_than"].as_str() {
1071 Some(s) => match parse_since(s, now) {
1072 Some(ms) => ms,
1073 None => {
1074 return error_response(
1075 StatusCode::BAD_REQUEST,
1076 &format!("invalid 'older_than' '{s}' (use 45s, 30m, 24h, 7d, or RFC3339)"),
1077 )
1078 }
1079 },
1080 None => {
1081 return error_response(
1082 StatusCode::BAD_REQUEST,
1083 "missing 'older_than_ms' or 'older_than'",
1084 )
1085 }
1086 },
1087 v => match v.as_i64() {
1088 Some(ms) => ms,
1089 None => {
1090 return error_response(StatusCode::BAD_REQUEST, "'older_than_ms' must be an integer")
1091 }
1092 },
1093 };
1094 if cutoff > now {
1095 return error_response(StatusCode::BAD_REQUEST, "cutoff is in the future");
1096 }
1097 let bodies_only = body["bodies_only"].as_bool().unwrap_or(true);
1098 let dry_run = body["dry_run"].as_bool().unwrap_or(false);
1099 let store = state.store.clone();
1100 let report =
1101 tokio::task::spawn_blocking(move || store.prune(cutoff, bodies_only, dry_run)).await;
1102 match report {
1103 Ok(Ok(r)) => axum::Json(serde_json::to_value(r).unwrap_or_default()).into_response(),
1104 Ok(Err(e)) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1105 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1106 }
1107}
1108
1109async fn admin_traces(
1110 State(state): State<Arc<AppState>>,
1111 Query(q): Query<HashMap<String, String>>,
1112) -> Response {
1113 let limit = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(50);
1114 match state.store.list_traces(
1115 limit,
1116 q.get("session").map(|s| s.as_str()),
1117 q.get("model").map(|s| s.as_str()),
1118 ) {
1119 Ok(rows) => axum::Json(json!({"traces": rows})).into_response(),
1120 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1121 }
1122}
1123
1124fn filter_from_query(q: &HashMap<String, String>) -> TraceFilter {
1125 let now = now_ms();
1126 TraceFilter {
1127 since_ms: q.get("since").and_then(|s| parse_since(s, now)),
1128 until_ms: q.get("until").and_then(|s| parse_since(s, now)),
1129 run_id: q.get("run_id").cloned(),
1130 session: q.get("session").cloned(),
1131 model: q.get("model").cloned(),
1132 provider: q.get("provider").cloned(),
1133 path: q.get("path").cloned(),
1134 harness: q.get("harness").cloned(),
1135 status: q.get("status").and_then(|s| s.parse().ok()),
1136 errors_only: q
1137 .get("errors")
1138 .map(|v| v == "1" || v == "true")
1139 .unwrap_or(false),
1140 key_fingerprint: q.get("key_fingerprint").cloned(),
1141 reasoning_effort: q.get("effort").cloned(),
1142 limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(200),
1143 }
1144}
1145
1146fn wants_bodies(q: &HashMap<String, String>) -> bool {
1147 q.get("bodies").map(|v| v == "1" || v == "true").unwrap_or(false)
1148}
1149
1150fn read_gz_file(path: &str) -> Option<Vec<u8>> {
1151 let file = std::fs::File::open(path).ok()?;
1152 let mut decoder = flate2::read::GzDecoder::new(file);
1153 let mut buf = Vec::new();
1154 std::io::Read::read_to_end(&mut decoder, &mut buf).ok()?;
1155 Some(buf)
1156}
1157
1158fn inline_row_bodies(row: &mut Value) {
1159 use base64::Engine;
1160 for (path_key, out_key) in [
1161 ("req_body_path", "req_body_b64"),
1162 ("upstream_req_body_path", "upstream_req_body_b64"),
1163 ("resp_body_path", "resp_body_b64"),
1164 ] {
1165 let Some(buf) = row[path_key].as_str().and_then(read_gz_file) else {
1166 continue;
1167 };
1168 row[out_key] = json!(base64::engine::general_purpose::STANDARD.encode(&buf));
1169 }
1170}
1171
1172fn ndjson_response(mut rows: Vec<Value>, inline_bodies: bool) -> Response {
1173 rows.sort_by_key(|r| r["ts_request_ms"].as_i64().unwrap_or(0));
1174 let mut out = String::new();
1175 for mut row in rows {
1176 if inline_bodies {
1177 inline_row_bodies(&mut row);
1178 }
1179 out.push_str(&serde_json::to_string(&row).unwrap_or_default());
1180 out.push('\n');
1181 }
1182 Response::builder()
1183 .status(StatusCode::OK)
1184 .header("content-type", "application/x-ndjson")
1185 .body(Body::from(out))
1186 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))
1187}
1188
1189const TEXT_SCAN_CAP: usize = 300;
1190
1191fn trace_matches_text(row: &Value, needle: &str) -> bool {
1192 for key in ["req_body_path", "resp_body_path"] {
1193 if let Some(path) = row.get(key).and_then(|v| v.as_str()) {
1194 if let Some(bytes) = read_gz_file(path) {
1195 if String::from_utf8_lossy(&bytes)
1196 .to_lowercase()
1197 .contains(needle)
1198 {
1199 return true;
1200 }
1201 }
1202 }
1203 }
1204 false
1205}
1206
1207async fn traces_search(
1208 State(state): State<Arc<AppState>>,
1209 Query(q): Query<HashMap<String, String>>,
1210) -> Response {
1211 let mut filter = filter_from_query(&q);
1212 let text = q
1213 .get("text")
1214 .or_else(|| q.get("q"))
1215 .map(|t| t.trim().to_lowercase())
1216 .filter(|t| !t.is_empty());
1217 if text.is_some() {
1218 filter.limit = TEXT_SCAN_CAP;
1219 }
1220 match state.store.search_traces(&filter) {
1221 Ok(rows) => match text {
1222 Some(needle) => {
1223 let scanned = rows.len();
1224 let rows = tokio::task::spawn_blocking(move || {
1225 rows.into_iter()
1226 .filter(|r| trace_matches_text(r, &needle))
1227 .collect::<Vec<_>>()
1228 })
1229 .await
1230 .unwrap_or_default();
1231 axum::Json(json!({"traces": rows, "scanned": scanned, "scan_cap": TEXT_SCAN_CAP}))
1232 .into_response()
1233 }
1234 None => axum::Json(json!({"traces": rows})).into_response(),
1235 },
1236 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1237 }
1238}
1239
1240async fn traces_export(
1241 State(state): State<Arc<AppState>>,
1242 Query(q): Query<HashMap<String, String>>,
1243) -> Response {
1244 match state.store.search_traces(&filter_from_query(&q)) {
1245 Ok(rows) => ndjson_response(rows, wants_bodies(&q)),
1246 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1247 }
1248}
1249
1250fn truncate_chars(s: String, max: usize) -> String {
1251 if s.chars().count() <= max {
1252 s
1253 } else {
1254 s.chars().take(max).collect()
1255 }
1256}
1257
1258fn read_gz_json(path: Option<&str>) -> Option<Value> {
1259 let buf = path.and_then(read_gz_file)?;
1260 serde_json::from_slice(&buf).ok()
1261}
1262
1263fn read_gz_text(path: Option<&str>) -> Option<String> {
1264 let buf = path.and_then(read_gz_file)?;
1265 Some(String::from_utf8_lossy(&buf).to_string())
1266}
1267
1268fn body_date_dir_name(name: &str) -> bool {
1269 let bytes = name.as_bytes();
1270 bytes.len() == 10
1271 && bytes[4] == b'-'
1272 && bytes[7] == b'-'
1273 && bytes
1274 .iter()
1275 .enumerate()
1276 .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
1277}
1278
1279fn dario_capture_suffix(kind: &str) -> Option<&'static str> {
1280 match kind {
1281 "dario-upstream-request" => Some("dario-upstream-request.json.gz"),
1282 "dario-upstream-response" => Some("dario-upstream-response.json.gz"),
1283 _ => None,
1284 }
1285}
1286
1287fn is_dario_trace(row: &Value) -> bool {
1288 row["account_id"]
1289 .as_str()
1290 .map(|id| id.starts_with("dario:"))
1291 .unwrap_or(false)
1292}
1293
1294fn find_dario_capture_path(state: &AppState, row: &Value, kind: &str) -> Option<String> {
1295 if !is_dario_trace(row) {
1296 return None;
1297 }
1298 let trace_id = row["id"].as_str()?;
1299 let suffix = dario_capture_suffix(kind)?;
1300 let filename = format!("{trace_id}.{suffix}");
1301 let bodies = state.store.data_dir.join("bodies");
1302 let mut days: Vec<PathBuf> = std::fs::read_dir(&bodies)
1303 .ok()?
1304 .flatten()
1305 .filter_map(|entry| {
1306 let name = entry.file_name().to_string_lossy().to_string();
1307 (entry.path().is_dir() && body_date_dir_name(&name)).then(|| entry.path())
1308 })
1309 .collect();
1310 days.sort_by(|a, b| b.file_name().cmp(&a.file_name()));
1311 for day in days {
1312 let path = day.join(&filename);
1313 if path.is_file() {
1314 return Some(path.to_string_lossy().to_string());
1315 }
1316 }
1317 None
1318}
1319
1320fn dario_capture_summary(state: &AppState, row: &Value) -> Option<Value> {
1321 let request_path = find_dario_capture_path(state, row, "dario-upstream-request");
1322 let response_path = find_dario_capture_path(state, row, "dario-upstream-response");
1323 if request_path.is_none() && response_path.is_none() {
1324 return None;
1325 }
1326 let prompt_cache = request_path
1327 .as_deref()
1328 .and_then(|path| read_gz_json(Some(path)))
1329 .and_then(|body| {
1330 body["prompt_cache"]
1331 .as_object()
1332 .map(|_| body["prompt_cache"].clone())
1333 });
1334 Some(json!({
1335 "request_available": request_path.is_some(),
1336 "response_available": response_path.is_some(),
1337 "request_path": request_path,
1338 "response_path": response_path,
1339 "prompt_cache": prompt_cache,
1340 }))
1341}
1342
1343async fn traces_sessions(
1344 State(state): State<Arc<AppState>>,
1345 Query(q): Query<HashMap<String, String>>,
1346) -> Response {
1347 let since = q.get("since").and_then(|s| parse_since(s, now_ms()));
1348 let limit = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(0);
1349 match state.store.sessions(since, limit) {
1350 Ok(rows) => axum::Json(json!({"sessions": rows})).into_response(),
1351 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1352 }
1353}
1354
1355fn transcript_turn(row: &Value) -> Value {
1356 use alex_core::translate;
1357 let user = read_gz_json(row["req_body_path"].as_str())
1358 .and_then(|req| {
1359 translate::last_user_text(row["client_format"].as_str().unwrap_or(""), &req)
1360 })
1361 .map(|s| truncate_chars(s, 8000));
1362 let resp_text = read_gz_text(row["resp_body_path"].as_str());
1363 let fmt = row["upstream_format"]
1364 .as_str()
1365 .or(row["client_format"].as_str())
1366 .unwrap_or("")
1367 .to_string();
1368 let assistant = resp_text
1369 .as_deref()
1370 .and_then(|text| translate::assistant_reply_text(&fmt, text))
1371 .map(|s| truncate_chars(s, 8000));
1372 let tool_calls: Vec<Value> = resp_text
1373 .as_deref()
1374 .map(|text| translate::assistant_tool_calls(&fmt, text))
1375 .unwrap_or_default()
1376 .into_iter()
1377 .take(24)
1378 .map(|mut c| {
1379 if let Some(a) = c["arguments"].as_str() {
1380 let t = truncate_chars(a.to_string(), 600);
1381 c["arguments"] = json!(t);
1382 }
1383 c
1384 })
1385 .collect();
1386 json!({
1387 "trace_id": row["id"],
1388 "ts_request_ms": row["ts_request_ms"],
1389 "ts_response_ms": row["ts_response_ms"],
1390 "model": row["routed_model"],
1391 "status": row["status"],
1392 "input_tokens": row["input_tokens"],
1393 "output_tokens": row["output_tokens"],
1394 "reasoning_effort": row["reasoning_effort"],
1395 "thinking_budget": row["thinking_budget"],
1396 "cost_usd": row["cost_usd"],
1397 "account_id": row["account_id"],
1398 "error": row["error"],
1399 "user": user,
1400 "assistant": assistant,
1401 "tool_calls": tool_calls,
1402 })
1403}
1404
1405async fn traces_session_transcript(
1406 State(state): State<Arc<AppState>>,
1407 Path(session_id): Path<String>,
1408 Query(q): Query<HashMap<String, String>>,
1409) -> Response {
1410 let since = q
1411 .get("since_ms")
1412 .and_then(|s| s.parse::<i64>().ok())
1413 .or_else(|| q.get("since").and_then(|s| parse_since(s, now_ms())));
1414 let limit: usize = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(500);
1415 let rows = match state.store.session_traces(&session_id, since) {
1416 Ok(rows) => rows,
1417 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1418 };
1419 let turns: Vec<Value> = rows.iter().take(limit).map(transcript_turn).collect();
1420 axum::Json(json!({"session_id": session_id, "turns": turns})).into_response()
1421}
1422
1423fn trace_reasoning_fields(req: &Value) -> (Option<String>, Option<i64>) {
1424 (
1425 req["reasoning"]["effort"].as_str().map(String::from),
1426 req["thinking"]["budget_tokens"].as_i64(),
1427 )
1428}
1429
1430fn trace_extras(req: &Value) -> Value {
1431 let system_text: Option<String> = match &req["system"] {
1432 Value::String(s) => Some(s.clone()),
1433 Value::Array(parts) => {
1434 let joined: Vec<&str> = parts.iter().filter_map(|p| p["text"].as_str()).collect();
1435 (!joined.is_empty()).then(|| joined.join("\n\n"))
1436 }
1437 _ => req["instructions"].as_str().map(String::from).or_else(|| {
1438 let joined: Vec<String> = req["messages"]
1439 .as_array()
1440 .into_iter()
1441 .flatten()
1442 .filter(|m| matches!(m["role"].as_str(), Some("system") | Some("developer")))
1443 .map(|m| match &m["content"] {
1444 Value::String(s) => s.clone(),
1445 Value::Array(parts) => parts
1446 .iter()
1447 .filter_map(|p| p["text"].as_str())
1448 .collect::<Vec<_>>()
1449 .join("\n"),
1450 _ => String::new(),
1451 })
1452 .filter(|s| !s.is_empty())
1453 .collect();
1454 (!joined.is_empty()).then(|| joined.join("\n\n"))
1455 }),
1456 };
1457 let system_chars = system_text.as_ref().map(|s| s.chars().count());
1458 let system_prompt = system_text.map(|s| truncate_chars(s, 64_000));
1459 let (reasoning_effort, thinking_budget) = trace_reasoning_fields(req);
1460 json!({
1461 "reasoning_effort": reasoning_effort,
1462 "thinking_budget": thinking_budget,
1463 "max_tokens": req["max_tokens"].as_i64().or(req["max_output_tokens"].as_i64()),
1464 "temperature": req["temperature"],
1465 "message_count": req["messages"].as_array().or(req["input"].as_array()).map(|a| a.len()),
1466 "system_chars": system_chars,
1467 "system_prompt": system_prompt,
1468 })
1469}
1470
1471async fn trace_get(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
1472 match state.store.get_trace(&id) {
1473 Ok(Some(row)) => {
1474 let mut extras = read_gz_json(row["req_body_path"].as_str())
1475 .map(|req| trace_extras(&req))
1476 .unwrap_or_else(|| json!({}));
1477 if let Some(summary) = dario_capture_summary(&state, &row) {
1478 if !extras.is_object() {
1479 extras = json!({});
1480 }
1481 if let Some(obj) = extras.as_object_mut() {
1482 obj.insert("dario_capture".into(), summary);
1483 }
1484 }
1485 axum::Json(json!({"trace": row, "extras": extras})).into_response()
1486 }
1487 Ok(None) => error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'")),
1488 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1489 }
1490}
1491
1492async fn trace_body(
1493 State(state): State<Arc<AppState>>,
1494 Path((id, kind)): Path<(String, String)>,
1495) -> Response {
1496 let row = match state.store.get_trace(&id) {
1497 Ok(Some(row)) => row,
1498 Ok(None) => {
1499 return error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'"))
1500 }
1501 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1502 };
1503 let path = match kind.as_str() {
1504 "request" => row["req_body_path"].as_str().map(String::from),
1505 "upstream-request" => row["upstream_req_body_path"].as_str().map(String::from),
1506 "response" => row["resp_body_path"].as_str().map(String::from),
1507 "dario-upstream-request" | "dario-upstream-response" => {
1508 find_dario_capture_path(&state, &row, &kind)
1509 }
1510 _ => {
1511 return error_response(
1512 StatusCode::BAD_REQUEST,
1513 "kind must be request|upstream-request|response|dario-upstream-request|dario-upstream-response",
1514 )
1515 }
1516 };
1517 match read_gz_text(path.as_deref()) {
1518 Some(text) => {
1519 let ct = if text.trim_start().starts_with('{') || text.trim_start().starts_with('[') {
1520 "application/json; charset=utf-8"
1521 } else {
1522 "text/plain; charset=utf-8"
1523 };
1524 Response::builder()
1525 .status(StatusCode::OK)
1526 .header("content-type", ct)
1527 .header("x-alexandria-body-path", path.as_deref().unwrap_or(""))
1528 .body(Body::from(text))
1529 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))
1530 }
1531 None => error_response(
1532 StatusCode::NOT_FOUND,
1533 &format!("no {kind} body stored for trace '{id}'"),
1534 ),
1535 }
1536}
1537
1538async fn trace_reply_md(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
1539 use alex_core::translate;
1540 let row = match state.store.get_trace(&id) {
1541 Ok(Some(row)) => row,
1542 Ok(None) => {
1543 return error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'"))
1544 }
1545 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1546 };
1547 let fmt = row["upstream_format"]
1548 .as_str()
1549 .or(row["client_format"].as_str())
1550 .unwrap_or("");
1551 let reply = read_gz_text(row["resp_body_path"].as_str())
1552 .and_then(|text| translate::assistant_reply_text(fmt, &text));
1553 match reply {
1554 Some(md) => Response::builder()
1555 .status(StatusCode::OK)
1556 .header("content-type", "text/markdown; charset=utf-8")
1557 .body(Body::from(md))
1558 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())),
1559 None => error_response(
1560 StatusCode::NOT_FOUND,
1561 &format!("no assistant reply available for trace '{id}'"),
1562 ),
1563 }
1564}
1565
1566async fn trace_delete(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
1567 match state.store.get_trace(&id) {
1568 Ok(Some(_)) => {}
1569 Ok(None) => {
1570 return error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'"))
1571 }
1572 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1573 }
1574 match state.store.delete_trace(&id) {
1575 Ok(paths) => {
1576 let removed = paths
1577 .iter()
1578 .filter(|p| std::fs::remove_file(p).is_ok())
1579 .count();
1580 axum::Json(json!({"deleted": true, "files_removed": removed})).into_response()
1581 }
1582 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1583 }
1584}
1585
1586async fn traces_run_summary(
1587 State(state): State<Arc<AppState>>,
1588 Path(run_id): Path<String>,
1589) -> Response {
1590 match state.store.run_summary(&run_id) {
1591 Ok(v) if v["trace_count"].as_i64().unwrap_or(0) == 0 => error_response(
1592 StatusCode::NOT_FOUND,
1593 &format!("no traces for run '{run_id}'"),
1594 ),
1595 Ok(v) => axum::Json(v).into_response(),
1596 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1597 }
1598}
1599
1600async fn traces_run_events(
1601 State(state): State<Arc<AppState>>,
1602 Path(run_id): Path<String>,
1603 Query(q): Query<HashMap<String, String>>,
1604) -> Response {
1605 let filter = TraceFilter {
1606 run_id: Some(run_id),
1607 limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(1000),
1608 ..Default::default()
1609 };
1610 match state.store.search_traces(&filter) {
1611 Ok(rows) => axum::Json(json!({"traces": rows})).into_response(),
1612 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1613 }
1614}
1615
1616async fn traces_run_export(
1617 State(state): State<Arc<AppState>>,
1618 Path(run_id): Path<String>,
1619 Query(q): Query<HashMap<String, String>>,
1620) -> Response {
1621 let filter = TraceFilter {
1622 run_id: Some(run_id),
1623 limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(5000),
1624 ..Default::default()
1625 };
1626 match state.store.search_traces(&filter) {
1627 Ok(rows) => ndjson_response(rows, wants_bodies(&q)),
1628 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1629 }
1630}
1631
1632async fn traces_run_artifacts(
1633 State(state): State<Arc<AppState>>,
1634 Path(run_id): Path<String>,
1635) -> Response {
1636 match state.store.run_artifacts(&run_id) {
1637 Ok(artifacts) => axum::Json(json!({
1638 "run_id": run_id,
1639 "data_dir": state.store.data_dir.to_string_lossy(),
1640 "artifacts": artifacts,
1641 }))
1642 .into_response(),
1643 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1644 }
1645}
1646
1647async fn admin_accounts(State(state): State<Arc<AppState>>) -> impl IntoResponse {
1648 let openai_policy = state.vault.policy(Provider::Openai);
1649 let accounts: Vec<Value> = state
1650 .vault
1651 .list()
1652 .await
1653 .into_iter()
1654 .map(|a| {
1655 let email = a.email();
1656 let routing = if a.provider == Provider::Openai {
1657 let reserve_pct = codex_reserve_pct(&a, &openai_policy);
1658 json!({
1659 "eligible": !openai_policy.disabled.iter().any(|name| name == &a.name || name == &a.id),
1660 "priority": openai_policy.order.iter().position(|name| name == &a.name),
1661 "reserve_pct": reserve_pct,
1662 "reserve_blocked": codex_reserve_blocked(&a, reserve_pct, now_ms() / 1000),
1663 "reset_selection": codex_reset_selection(&a, now_ms() / 1000),
1664 })
1665 } else {
1666 Value::Null
1667 };
1668 let limits = if a.provider == Provider::Openai {
1669 a.account_meta
1670 .get("codex_limits")
1671 .cloned()
1672 .unwrap_or(Value::Null)
1673 } else {
1674 Value::Null
1675 };
1676 json!({
1677 "id": a.id,
1678 "provider": a.provider.as_str(),
1679 "name": a.name,
1680 "kind": a.kind,
1681 "label": a.label,
1682 "description": a.description,
1683 "email": email,
1684 "paused": a.paused,
1685 "path": a.path.map(|p| p.display().to_string()),
1686 "status": a.status,
1687 "expires_at_ms": a.expires_at_ms,
1688 "expires_in_s": a.expires_at_ms.map(|e| (e - now_ms()) / 1000),
1689 "routing": routing,
1690 "limits": limits,
1691 })
1692 })
1693 .collect();
1694 axum::Json(json!({"accounts": accounts}))
1695}
1696
1697fn codex_strategy_name(mode: &AccountPolicyMode) -> &'static str {
1698 match mode {
1699 AccountPolicyMode::ResetFirst => "reset_first",
1700 AccountPolicyMode::RoundRobin => "round_robin",
1701 AccountPolicyMode::Priority | AccountPolicyMode::Threshold => "priority",
1702 }
1703}
1704
1705async fn codex_routing_snapshot(state: &Arc<AppState>) -> Value {
1706 let policy = state.vault.policy(Provider::Openai);
1707 let mut accounts: Vec<Account> = state
1708 .vault
1709 .list()
1710 .await
1711 .into_iter()
1712 .filter(|account| account.provider == Provider::Openai)
1713 .collect();
1714 accounts.sort_by_key(|account| {
1715 (
1716 policy
1717 .order
1718 .iter()
1719 .position(|name| name == &account.name)
1720 .unwrap_or(usize::MAX / 2),
1721 account.name.clone(),
1722 account.id.clone(),
1723 )
1724 });
1725 let accounts: Vec<Value> = accounts
1726 .into_iter()
1727 .enumerate()
1728 .map(|(fallback_priority, account)| {
1729 let eligible = !policy
1730 .disabled
1731 .iter()
1732 .any(|name| name == &account.name || name == &account.id);
1733 let priority = policy
1734 .order
1735 .iter()
1736 .position(|name| name == &account.name)
1737 .unwrap_or(fallback_priority);
1738 let limits = account
1739 .account_meta
1740 .get("codex_limits")
1741 .cloned()
1742 .unwrap_or_else(|| json!({}));
1743 let reserve_pct = codex_reserve_pct(&account, &policy);
1744 let reset_selection = codex_reset_selection(&account, now_ms() / 1000);
1745 json!({
1746 "account_id": account.id,
1747 "eligible": eligible,
1748 "priority": priority,
1749 "reserve_pct": reserve_pct,
1750 "reserve_blocked": codex_reserve_blocked(&account, reserve_pct, now_ms() / 1000),
1751 "reset_selection": reset_selection,
1752 "observed_at_ms": limits.get("observed_at_ms"),
1753 "plan": limits.get("plan"),
1754 "active_limit": limits.get("active_limit"),
1755 "windows": limits.get("windows").cloned().unwrap_or_else(|| json!([])),
1756 "credits": limits.get("credits").cloned().unwrap_or(Value::Null),
1757 })
1758 })
1759 .collect();
1760 json!({
1761 "provider": "openai",
1762 "strategy": codex_strategy_name(&policy.mode),
1763 "reserve_pct": policy.reserve_pct.unwrap_or(10).min(100),
1764 "allow_mid_thread_failover": policy.allow_mid_thread_failover,
1765 "reset_selection_rule": "highest_used_pct_then_earliest_reset",
1766 "accounts": accounts,
1767 })
1768}
1769
1770async fn admin_codex_routing(State(state): State<Arc<AppState>>) -> Response {
1771 axum::Json(codex_routing_snapshot(&state).await).into_response()
1772}
1773
1774async fn admin_codex_routing_update(
1775 State(state): State<Arc<AppState>>,
1776 body: axum::Json<Value>,
1777) -> Response {
1778 let current_policy = state.vault.policy(Provider::Openai);
1779 let mode = match body.0.get("strategy").and_then(Value::as_str) {
1780 Some("reset_first") => AccountPolicyMode::ResetFirst,
1781 Some("priority") => AccountPolicyMode::Priority,
1782 Some("round_robin") => AccountPolicyMode::RoundRobin,
1783 _ => {
1784 return error_response(
1785 StatusCode::BAD_REQUEST,
1786 "strategy must be reset_first, priority, or round_robin",
1787 )
1788 }
1789 };
1790 let reserve_pct = match body.0.get("reserve_pct") {
1791 Some(value) => match value.as_u64() {
1792 Some(value) => value,
1793 None => return error_response(StatusCode::BAD_REQUEST, "reserve_pct must be an integer"),
1794 },
1795 None => current_policy.reserve_pct.unwrap_or(10) as u64,
1796 };
1797 if reserve_pct > 100 {
1798 return error_response(StatusCode::BAD_REQUEST, "reserve_pct must be between 0 and 100");
1799 }
1800 let allow_mid_thread_failover = match body.0.get("allow_mid_thread_failover") {
1801 Some(value) => match value.as_bool() {
1802 Some(value) => value,
1803 None => return error_response(
1804 StatusCode::BAD_REQUEST,
1805 "allow_mid_thread_failover must be boolean",
1806 ),
1807 },
1808 None => current_policy.allow_mid_thread_failover,
1809 };
1810 let Some(requested) = body.0.get("accounts").and_then(Value::as_array) else {
1811 return error_response(StatusCode::BAD_REQUEST, "accounts must be an array");
1812 };
1813 let openai_accounts: Vec<Account> = state
1814 .vault
1815 .list()
1816 .await
1817 .into_iter()
1818 .filter(|account| account.provider == Provider::Openai)
1819 .collect();
1820 let by_id: HashMap<String, String> = openai_accounts
1821 .iter()
1822 .map(|account| (account.id.clone(), account.name.clone()))
1823 .collect();
1824 let mut seen = std::collections::HashSet::new();
1825 let mut ordered = Vec::new();
1826 for item in requested {
1827 let Some(account_id) = item.get("account_id").and_then(Value::as_str) else {
1828 return error_response(StatusCode::BAD_REQUEST, "each account needs account_id");
1829 };
1830 let Some(eligible) = item.get("eligible").and_then(Value::as_bool) else {
1831 return error_response(StatusCode::BAD_REQUEST, "each account needs boolean eligible");
1832 };
1833 let Some(priority) = item.get("priority").and_then(Value::as_u64) else {
1834 return error_response(StatusCode::BAD_REQUEST, "each account needs integer priority");
1835 };
1836 let account_reserve_pct = match item.get("reserve_pct") {
1837 Some(value) => match value.as_u64() {
1838 Some(value) if value <= 100 => value as u8,
1839 Some(_) => return error_response(
1840 StatusCode::BAD_REQUEST,
1841 "account reserve_pct must be between 0 and 100",
1842 ),
1843 None => return error_response(
1844 StatusCode::BAD_REQUEST,
1845 "account reserve_pct must be an integer",
1846 ),
1847 },
1848 None => reserve_pct as u8,
1849 };
1850 let Some(name) = by_id.get(account_id) else {
1851 return error_response(
1852 StatusCode::BAD_REQUEST,
1853 &format!("unknown OpenAI account '{account_id}'"),
1854 );
1855 };
1856 if !seen.insert(account_id.to_string()) {
1857 return error_response(
1858 StatusCode::BAD_REQUEST,
1859 &format!("duplicate OpenAI account '{account_id}'"),
1860 );
1861 }
1862 ordered.push((priority, name.clone(), eligible, account_reserve_pct));
1863 }
1864 if seen.len() != by_id.len() {
1865 return error_response(
1866 StatusCode::CONFLICT,
1867 "account list changed; refresh Settings and save again",
1868 );
1869 }
1870 ordered.sort_by_key(|(priority, name, _, _)| (*priority, name.clone()));
1871 let policy = AccountPolicy {
1872 order: ordered.iter().map(|(_, name, _, _)| name.clone()).collect(),
1873 mode,
1874 threshold_pct: None,
1875 reserve_pct: Some(reserve_pct as u8),
1876 account_reserve_pct: ordered
1877 .iter()
1878 .map(|(_, name, _, reserve_pct)| (name.clone(), *reserve_pct))
1879 .collect(),
1880 allow_mid_thread_failover,
1881 disabled: ordered
1882 .iter()
1883 .filter(|(_, _, eligible, _)| !*eligible)
1884 .map(|(_, name, _, _)| name.clone())
1885 .collect(),
1886 };
1887 match state
1888 .vault
1889 .set_policy_persisted(Provider::Openai, policy)
1890 .await
1891 {
1892 Ok(()) => axum::Json(codex_routing_snapshot(&state).await).into_response(),
1893 Err(error) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
1894 }
1895}
1896
1897async fn admin_health(State(state): State<Arc<AppState>>) -> Response {
1898 let heartbeats = state.store.last_heartbeats().unwrap_or_default();
1899 let accounts: Vec<Value> = state
1900 .vault
1901 .list()
1902 .await
1903 .into_iter()
1904 .map(|a| {
1905 let last = heartbeats
1906 .iter()
1907 .find(|h| h["provider"].as_str() == Some(a.provider.as_str()))
1908 .cloned();
1909 json!({
1910 "id": a.id,
1911 "provider": a.provider.as_str(),
1912 "name": a.name,
1913 "kind": a.kind,
1914 "status": a.status,
1915 "paused": a.paused,
1916 "path": a.path.map(|p| p.display().to_string()),
1917 "token_expires_in_s": a.expires_at_ms.map(|e| (e - now_ms()) / 1000),
1918 "last_heartbeat": last,
1919 })
1920 })
1921 .collect();
1922 axum::Json(json!({"accounts": accounts})).into_response()
1923}
1924
1925#[derive(Debug, Clone, serde::Serialize)]
1926pub struct PingResult {
1927 pub provider: &'static str,
1928 pub account_id: Option<String>,
1929 pub ok: bool,
1930 pub status: Option<u16>,
1931 pub latency_ms: i64,
1932 pub message: String,
1933}
1934
1935#[derive(Debug, Clone)]
1936pub struct PingModels {
1937 pub anthropic: String,
1938 pub openai: String,
1939 pub xai: String,
1940 pub gemini: String,
1941}
1942
1943pub async fn ping_provider(
1944 state: &Arc<AppState>,
1945 provider: Provider,
1946 models: &PingModels,
1947) -> PingResult {
1948 let start = now_ms();
1949 let prompt = "Health check: what time is it? If you cannot know, just reply: creds ok";
1950 let (format, path, body) = match provider {
1951 Provider::Anthropic => (
1952 ClientFormat::AnthropicMessages,
1953 "/v1/messages",
1954 json!({
1955 "model": models.anthropic,
1956 "max_tokens": 64,
1957 "system": "You are Claude Code, Anthropic's official CLI for Claude.",
1958 "messages": [{"role": "user", "content": prompt}],
1959 }),
1960 ),
1961 Provider::Openai => (
1962 ClientFormat::OpenaiResponses,
1963 "/v1/responses",
1964 json!({
1965 "model": models.openai,
1966 "stream": true,
1967 "store": false,
1968 "instructions": "You are a helpful assistant.",
1969 "input": [{"type": "message", "role": "user",
1970 "content": [{"type": "input_text", "text": prompt}]}],
1971 }),
1972 ),
1973 Provider::Xai => (
1974 ClientFormat::OpenaiChat,
1975 "/v1/chat/completions",
1976 json!({
1977 "model": models.xai,
1978 "stream": false,
1979 "messages": [{"role": "user", "content": prompt}],
1980 }),
1981 ),
1982 Provider::Gemini => (
1983 ClientFormat::GeminiGenerate,
1984 "/v1beta/models/:generateContent",
1985 json!({
1986 "model": models.gemini,
1987 "contents": [{"role": "user", "parts": [{"text": prompt}]}],
1988 "generationConfig": {"maxOutputTokens": 64},
1989 }),
1990 ),
1991 };
1992 let account_id = state
1993 .vault
1994 .account_for(provider, true)
1995 .await
1996 .ok()
1997 .map(|a| a.id);
1998 let mut headers = HeaderMap::new();
1999 headers.insert(
2000 "x-api-key",
2001 HeaderValue::from_str(&state.local_key).expect("key header"),
2002 );
2003 headers.insert("user-agent", HeaderValue::from_static("alexandria-ping"));
2004 let resp = proxy(
2005 state.clone(),
2006 format,
2007 path,
2008 headers,
2009 Bytes::from(serde_json::to_vec(&body).expect("ping body")),
2010 None,
2011 )
2012 .await;
2013 let status = resp.status().as_u16();
2014 let bytes = axum::body::to_bytes(resp.into_body(), 8 * 1024 * 1024)
2015 .await
2016 .unwrap_or_default();
2017 let text = String::from_utf8_lossy(&bytes);
2018 let message = extract_reply(&text).unwrap_or_else(|| snippet(&text));
2019 PingResult {
2020 provider: provider.as_str(),
2021 account_id,
2022 ok: (200..300).contains(&status),
2023 status: Some(status),
2024 latency_ms: now_ms() - start,
2025 message,
2026 }
2027}
2028
2029fn extract_reply(text: &str) -> Option<String> {
2030 if let Ok(v) = serde_json::from_str::<Value>(text) {
2031 if let Some(t) = v["content"][0]["text"].as_str() {
2032 return Some(t.to_string());
2033 }
2034 if let Some(t) = v["choices"][0]["message"]["content"].as_str() {
2035 return Some(t.to_string());
2036 }
2037 if let Some(t) = v["error"]["message"].as_str() {
2038 return Some(t.to_string());
2039 }
2040 if let Some(t) = v["detail"].as_str() {
2041 return Some(t.to_string());
2042 }
2043 }
2044 for line in text.lines() {
2045 let Some(data) = line.strip_prefix("data:") else {
2046 continue;
2047 };
2048 let Ok(v) = serde_json::from_str::<Value>(data.trim()) else {
2049 continue;
2050 };
2051 if v["type"].as_str() == Some("response.output_text.done") {
2052 if let Some(t) = v["text"].as_str() {
2053 return Some(t.to_string());
2054 }
2055 }
2056 }
2057 None
2058}
2059
2060fn snippet(text: &str) -> String {
2061 let t: String = text.chars().take(200).collect();
2062 t.replace('\n', " ")
2063}
2064
2065pub async fn heartbeat_once(state: &Arc<AppState>, models: &PingModels) -> Vec<PingResult> {
2066 let providers: Vec<Provider> = {
2067 let mut seen = Vec::new();
2068 for a in state.vault.list().await {
2069 if a.status == "active"
2070 && matches!(
2071 a.provider,
2072 Provider::Anthropic | Provider::Openai | Provider::Xai | Provider::Gemini
2073 )
2074 && !seen.contains(&a.provider)
2075 {
2076 seen.push(a.provider);
2077 }
2078 }
2079 seen
2080 };
2081 let mut results = Vec::new();
2082 for provider in providers {
2083 let r = ping_provider(state, provider, models).await;
2084 if let Err(e) = state.store.insert_heartbeat(
2085 now_ms(),
2086 r.provider,
2087 r.account_id.as_deref(),
2088 r.ok,
2089 r.status.map(|s| s as i64),
2090 r.latency_ms,
2091 &r.message,
2092 ) {
2093 tracing::warn!("failed to record heartbeat: {e}");
2094 }
2095 tracing::info!(
2096 provider = r.provider,
2097 ok = r.ok,
2098 status = r.status,
2099 latency_ms = r.latency_ms,
2100 reply = %r.message,
2101 "heartbeat"
2102 );
2103 results.push(r);
2104 }
2105 results
2106}
2107
2108async fn anthropic_messages(
2109 State(state): State<Arc<AppState>>,
2110 ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
2111 headers: HeaderMap,
2112 body: Bytes,
2113) -> Response {
2114 proxy(
2115 state,
2116 ClientFormat::AnthropicMessages,
2117 "/v1/messages",
2118 headers,
2119 body,
2120 Some(peer),
2121 )
2122 .await
2123}
2124
2125async fn openai_chat(
2126 State(state): State<Arc<AppState>>,
2127 ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
2128 headers: HeaderMap,
2129 body: Bytes,
2130) -> Response {
2131 proxy(
2132 state,
2133 ClientFormat::OpenaiChat,
2134 "/v1/chat/completions",
2135 headers,
2136 body,
2137 Some(peer),
2138 )
2139 .await
2140}
2141
2142async fn openai_responses(
2143 State(state): State<Arc<AppState>>,
2144 ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
2145 headers: HeaderMap,
2146 body: Bytes,
2147) -> Response {
2148 proxy(
2149 state,
2150 ClientFormat::OpenaiResponses,
2151 "/v1/responses",
2152 headers,
2153 body,
2154 Some(peer),
2155 )
2156 .await
2157}
2158
2159async fn gemini_generate(
2160 State(state): State<Arc<AppState>>,
2161 ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
2162 axum::extract::Path(model_action): axum::extract::Path<String>,
2163 headers: HeaderMap,
2164 body: Bytes,
2165) -> Response {
2166 let (model, method) = model_action
2167 .split_once(':')
2168 .unwrap_or((model_action.as_str(), "generateContent"));
2169 let stream = method == "streamGenerateContent";
2170 if method != "generateContent" && !stream {
2171 return error_response(
2172 StatusCode::NOT_FOUND,
2173 &format!("unsupported gemini method '{method}' (expected generateContent or streamGenerateContent)"),
2174 );
2175 }
2176 if model.is_empty() {
2177 return error_response(StatusCode::BAD_REQUEST, "missing model in path");
2178 }
2179 let mut v: Value = match serde_json::from_slice(&body) {
2180 Ok(v) => v,
2181 Err(e) => {
2182 return error_response(StatusCode::BAD_REQUEST, &format!("invalid JSON body: {e}"))
2183 }
2184 };
2185 v["model"] = json!(model);
2186 if stream {
2187 v["stream"] = json!(true);
2188 }
2189 let body = Bytes::from(serde_json::to_vec(&v).unwrap_or_default());
2190 let path = if stream {
2191 "/v1beta/models/:streamGenerateContent"
2192 } else {
2193 "/v1beta/models/:generateContent"
2194 };
2195 proxy(
2196 state,
2197 ClientFormat::GeminiGenerate,
2198 path,
2199 headers,
2200 body,
2201 Some(peer),
2202 )
2203 .await
2204}
2205
2206fn error_response(status: StatusCode, message: &str) -> Response {
2207 (
2208 status,
2209 axum::Json(json!({"error": {"type": "alexandria", "message": message}})),
2210 )
2211 .into_response()
2212}
2213
2214fn client_key(headers: &HeaderMap) -> Option<String> {
2215 if let Some(v) = headers.get("x-api-key").and_then(|v| v.to_str().ok()) {
2216 return Some(v.to_string());
2217 }
2218 if let Some(v) = headers.get("x-goog-api-key").and_then(|v| v.to_str().ok()) {
2219 return Some(v.to_string());
2220 }
2221 headers
2222 .get("authorization")
2223 .and_then(|v| v.to_str().ok())
2224 .and_then(|v| v.strip_prefix("Bearer "))
2225 .map(String::from)
2226}
2227
2228const CLAUDE_CODE_PASSTHROUGH_HEADERS: &[&str] = &[
2229 "accept",
2230 "x-app",
2231 "x-claude-code-session-id",
2232 "x-stainless-arch",
2233 "x-stainless-lang",
2234 "x-stainless-os",
2235 "x-stainless-package-version",
2236 "x-stainless-retry-count",
2237 "x-stainless-runtime",
2238 "x-stainless-runtime-version",
2239 "x-stainless-timeout",
2240 "anthropic-dangerous-direct-browser-access",
2241];
2242
2243fn is_genuine_claude_code_request(
2244 format: ClientFormat,
2245 headers: &HeaderMap,
2246 body: &Value,
2247) -> bool {
2248 if format != ClientFormat::AnthropicMessages {
2249 return false;
2250 }
2251
2252 for harness in headers.get_all("x-alexandria-harness").iter() {
2253 let Ok(harness) = harness.to_str() else {
2254 return false;
2255 };
2256 let harness = harness.trim().to_ascii_lowercase();
2257 if !matches!(harness.as_str(), "claude" | "claude-code") {
2258 return false;
2259 }
2260 }
2261
2262 let claude_user_agent = headers
2263 .get("user-agent")
2264 .and_then(|value| value.to_str().ok())
2265 .is_some_and(|value| value.to_ascii_lowercase().starts_with("claude-cli/"));
2266 let cli_app = headers
2267 .get("x-app")
2268 .and_then(|value| value.to_str().ok())
2269 .is_some_and(|value| value.eq_ignore_ascii_case("cli"));
2270 let has_session = headers
2271 .get("x-claude-code-session-id")
2272 .and_then(|value| value.to_str().ok())
2273 .is_some_and(|value| !value.trim().is_empty());
2274 let has_billing_header = body["system"]
2275 .as_array()
2276 .and_then(|blocks| blocks.first())
2277 .and_then(|block| block["text"].as_str())
2278 .is_some_and(|text| text.starts_with("x-anthropic-billing-header:"));
2279
2280 claude_user_agent && cli_app && has_session && has_billing_header
2281}
2282
2283fn redacted_headers(headers: &HeaderMap) -> String {
2284 let map: HashMap<String, String> = headers
2285 .iter()
2286 .map(|(k, v)| {
2287 let key = k.as_str().to_lowercase();
2288 let val = if ["authorization", "x-api-key", "cookie", "chatgpt-account-id"]
2289 .contains(&key.as_str())
2290 {
2291 "<redacted>".to_string()
2292 } else {
2293 v.to_str().unwrap_or("<binary>").to_string()
2294 };
2295 (key, val)
2296 })
2297 .collect();
2298 serde_json::to_string(&map).unwrap_or_default()
2299}
2300
2301#[derive(Debug, Clone, Copy, PartialEq)]
2302enum RespondAs {
2303 Anthropic,
2304 OpenaiChat,
2305 OpenaiResponses,
2306 Gemini,
2307}
2308
2309struct UpstreamPlan {
2310 url: String,
2311 account: Account,
2312 body: Vec<u8>,
2313 upstream_format: &'static str,
2314 destream: bool,
2315 respond_as: Option<RespondAs>,
2316 client_stream: bool,
2317 extra_headers: Vec<(String, String)>,
2318 dario_guard: Option<Box<dyn std::any::Any + Send>>,
2319}
2320
2321fn dario_account(active: &DarioActive) -> Account {
2322 Account {
2323 id: format!("dario:{}", active.generation_id),
2324 provider: Provider::Anthropic,
2325 kind: "dario".into(),
2326 name: active.generation_id.clone(),
2327 description: None,
2328 paused: false,
2329 label: Some("dario generational proxy".into()),
2330 access_token: None,
2331 refresh_token: None,
2332 id_token: None,
2333 api_key: Some(active.api_key.clone()),
2334 expires_at_ms: None,
2335 last_refresh_ms: None,
2336 account_meta: Value::Null,
2337 cooldown_until_ms: None,
2338 status: "active".into(),
2339 path: None,
2340 }
2341}
2342
2343async fn ensure_gemini_project(
2344 state: &AppState,
2345 account: &Account,
2346) -> Result<String, (StatusCode, String)> {
2347 if let Some(p) = account.account_meta.get("project_id").and_then(|v| v.as_str()) {
2348 if !p.is_empty() {
2349 return Ok(p.to_string());
2350 }
2351 }
2352 if let Ok(env_p) = std::env::var("GOOGLE_CLOUD_PROJECT") {
2353 if !env_p.is_empty() {
2354 let _ = state
2355 .vault
2356 .set_account_meta(&account.id, "project_id", json!(env_p))
2357 .await;
2358 return Ok(env_p);
2359 }
2360 }
2361 let token = account
2362 .access_token
2363 .as_deref()
2364 .ok_or_else(|| (StatusCode::BAD_GATEWAY, "gemini account has no access token".into()))?;
2365 let load_url = format!("{GEMINI_CODE_ASSIST_BASE}/{GEMINI_CODE_ASSIST_VERSION}:loadCodeAssist");
2366 let load_body = json!({
2367 "cloudaicompanionProject": null,
2368 "metadata": {"ideType": "IDE_UNSPECIFIED", "platform": "PLATFORM_UNSPECIFIED", "pluginType": "GEMINI"},
2369 });
2370 let resp = state
2371 .http
2372 .post(&load_url)
2373 .header("authorization", format!("Bearer {token}"))
2374 .json(&load_body)
2375 .send()
2376 .await
2377 .map_err(|e| (StatusCode::BAD_GATEWAY, format!("loadCodeAssist failed: {e}")))?;
2378 let load: Value = resp.json().await.unwrap_or(Value::Null);
2379 let extract = |v: &Value| -> Option<String> {
2380 for key in ["cloudaicompanionProject", "projectId", "project"] {
2381 match &v[key] {
2382 Value::String(s) if !s.is_empty() => return Some(s.clone()),
2383 obj if obj["id"].is_string() => {
2384 return obj["id"].as_str().map(String::from)
2385 }
2386 _ => {}
2387 }
2388 }
2389 None
2390 };
2391 if let Some(p) = extract(&load) {
2392 let _ = state
2393 .vault
2394 .set_account_meta(&account.id, "project_id", json!(p))
2395 .await;
2396 return Ok(p);
2397 }
2398 if let Some(free) = load["ineligibleTiers"]
2399 .as_array()
2400 .and_then(|t| t.iter().find(|t| t["tierId"] == json!("free-tier")))
2401 {
2402 let reason = free["reasonCode"].as_str().unwrap_or("");
2403 let msg = free["reasonMessage"].as_str().unwrap_or("");
2404 let hint = if let Some(url) = free["validationUrl"].as_str() {
2405 format!(
2406 "gemini account needs verification before the free Code Assist tier works — \
2407 open {url} to verify, then retry (or sign in with another personal Google \
2408 account, or set gemini_project to a GCP project)"
2409 )
2410 } else if reason == "DASHER_USER" {
2411 "this is a Google Workspace account, which cannot use the free Code Assist tier — \
2412 set gemini_project to a GCP project (Code Assist API enabled), or sign in with a \
2413 personal Google account"
2414 .to_string()
2415 } else {
2416 format!("gemini account is not eligible for the free Code Assist tier: {msg}")
2417 };
2418 return Err((StatusCode::BAD_GATEWAY, hint));
2419 }
2420 let tier = load["allowedTiers"]
2421 .as_array()
2422 .and_then(|tiers| tiers.iter().find(|t| t["isDefault"] == json!(true)))
2423 .and_then(|t| t["id"].as_str())
2424 .unwrap_or("free-tier")
2425 .to_string();
2426 let onboard_url =
2427 format!("{GEMINI_CODE_ASSIST_BASE}/{GEMINI_CODE_ASSIST_VERSION}:onboardUser");
2428 let onboard_body = json!({
2429 "tierId": tier,
2430 "cloudaicompanionProject": load["cloudaicompanionProject"],
2431 "metadata": {"ideType": "IDE_UNSPECIFIED", "platform": "PLATFORM_UNSPECIFIED", "pluginType": "GEMINI"},
2432 });
2433 for _ in 0..5 {
2434 let resp = state
2435 .http
2436 .post(&onboard_url)
2437 .header("authorization", format!("Bearer {token}"))
2438 .json(&onboard_body)
2439 .send()
2440 .await
2441 .map_err(|e| (StatusCode::BAD_GATEWAY, format!("onboardUser failed: {e}")))?;
2442 let lro: Value = resp.json().await.unwrap_or(Value::Null);
2443 if lro["done"] == json!(true) {
2444 if let Some(p) = extract(&lro["response"]) {
2445 let _ = state
2446 .vault
2447 .set_account_meta(&account.id, "project_id", json!(p))
2448 .await;
2449 return Ok(p);
2450 }
2451 }
2452 tokio::time::sleep(Duration::from_secs(2)).await;
2453 }
2454 Err((
2455 StatusCode::BAD_GATEWAY,
2456 "gemini Code Assist onboarding did not return a project id (set GOOGLE_CLOUD_PROJECT to override)".into(),
2457 ))
2458}
2459
2460async fn plan_upstream(
2461 state: &AppState,
2462 format: ClientFormat,
2463 provider: Provider,
2464 routed_model: &str,
2465 body_json: &mut Value,
2466 original_body: &[u8],
2467 trace_id: &str,
2468 excluded_accounts: &HashSet<String>,
2469 affinity_session: Option<&str>,
2470 client_headers: &HeaderMap,
2471) -> Result<UpstreamPlan, (StatusCode, String)> {
2472 use alex_core::translate;
2473 let client_stream = body_json["stream"].as_bool().unwrap_or(false);
2474 let genuine_claude_code =
2475 is_genuine_claude_code_request(format, client_headers, body_json);
2476 match provider {
2477 Provider::Anthropic => {
2478 let converted = match format {
2479 ClientFormat::AnthropicMessages => None,
2480 ClientFormat::OpenaiChat => Some((
2481 translate::openai_chat_to_anthropic(body_json),
2482 RespondAs::OpenaiChat,
2483 )),
2484 ClientFormat::OpenaiResponses => Some((
2485 translate::openai_responses_to_anthropic(body_json),
2486 RespondAs::OpenaiResponses,
2487 )),
2488 ClientFormat::GeminiGenerate => Some((
2489 translate::gemini_to_anthropic(body_json),
2490 RespondAs::Gemini,
2491 )),
2492 };
2493 let (base, account, dario_guard, dario_capture) = if genuine_claude_code {
2494 let account = state
2495 .vault
2496 .account_for_excluding(provider, true, excluded_accounts)
2497 .await
2498 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
2499 (ANTHROPIC_BASE.to_string(), account, None, false)
2500 } else {
2501 let dario_active = state.dario.as_ref().and_then(|dario| dario.active());
2502 match (&state.dario, dario_active) {
2503 (Some(dario), Some(active)) => {
2504 let guard = dario.begin(&active.generation_id).ok_or_else(|| {
2505 (
2506 StatusCode::SERVICE_UNAVAILABLE,
2507 "Dario became unavailable while routing the Anthropic request"
2508 .to_string(),
2509 )
2510 })?;
2511 (
2512 active.base_url.trim_end_matches('/').to_string(),
2513 dario_account(&active),
2514 Some(guard),
2515 true,
2516 )
2517 }
2518 (Some(_), None) => {
2519 return Err((
2520 StatusCode::SERVICE_UNAVAILABLE,
2521 "Dario is configured but no healthy generation is available"
2522 .to_string(),
2523 ));
2524 }
2525 (None, None) => {
2526 let account = state
2527 .vault
2528 .account_for_excluding(provider, true, excluded_accounts)
2529 .await
2530 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
2531 (ANTHROPIC_BASE.to_string(), account, None, false)
2532 }
2533 (None, Some(_)) => unreachable!("Dario cannot be active when it is disabled"),
2534 }
2535 };
2536 let (body, respond_as) = match converted {
2537 None => {
2538 body_json["model"] = json!(routed_model);
2539 (
2540 serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec()),
2541 None,
2542 )
2543 }
2544 Some((mut converted, respond_as)) => {
2545 converted["model"] = json!(routed_model);
2546 converted["stream"] = json!(false);
2547 (
2548 serde_json::to_vec(&converted).unwrap_or_else(|_| original_body.to_vec()),
2549 Some(respond_as),
2550 )
2551 }
2552 };
2553 Ok(UpstreamPlan {
2554 url: format!("{base}/v1/messages"),
2555 account,
2556 body,
2557 upstream_format: "anthropic",
2558 destream: false,
2559 respond_as,
2560 client_stream,
2561 extra_headers: if dario_capture {
2562 vec![
2563 ("x-dario-capture-id".into(), trace_id.into()),
2564 ("x-dario-capture-model".into(), routed_model.into()),
2565 ]
2566 } else {
2567 vec![]
2568 },
2569 dario_guard,
2570 })
2571 }
2572 Provider::Openai => {
2573 let _affinity_guard = match affinity_session.filter(|value| !value.is_empty()) {
2577 Some(session_id) => Some(
2578 codex_affinity_lock(state, session_id)
2579 .lock_owned()
2580 .await,
2581 ),
2582 None => None,
2583 };
2584 let prefer_oauth = format != ClientFormat::OpenaiChat;
2585 let policy = state.vault.policy(Provider::Openai);
2586 let preferred = preferred_codex_account(state, affinity_session);
2587 let account = state
2588 .vault
2589 .account_for_excluding_preferred_mode(
2590 provider,
2591 prefer_oauth,
2592 excluded_accounts,
2593 preferred.as_deref(),
2594 preferred.is_some() && !policy.allow_mid_thread_failover,
2595 )
2596 .await
2597 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
2598 let oauth = account.kind == "oauth";
2599 bind_codex_account(state, affinity_session, &account);
2600 match format {
2601 ClientFormat::OpenaiChat if !oauth => {
2602 body_json["model"] = json!(routed_model);
2603 let body =
2604 serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
2605 Ok(UpstreamPlan {
2606 url: format!("{OPENAI_BASE}/v1/chat/completions"),
2607 account,
2608 body,
2609 upstream_format: "openai-chat",
2610 destream: false,
2611 respond_as: None,
2612 client_stream,
2613 extra_headers: vec![],
2614 dario_guard: None,
2615 })
2616 }
2617 ClientFormat::OpenaiChat => {
2618 let pivot = translate::openai_chat_to_anthropic(body_json);
2619 let mut converted = translate::anthropic_to_openai_responses(&pivot);
2620 converted["model"] = json!(routed_model);
2621 translate::normalize_codex_request(&mut converted);
2622 let body = serde_json::to_vec(&converted)
2623 .unwrap_or_else(|_| original_body.to_vec());
2624 Ok(UpstreamPlan {
2625 url: format!("{CODEX_BASE}/responses"),
2626 account,
2627 body,
2628 upstream_format: "openai-responses",
2629 destream: false,
2630 respond_as: Some(RespondAs::OpenaiChat),
2631 client_stream,
2632 extra_headers: vec![],
2633 dario_guard: None,
2634 })
2635 }
2636 ClientFormat::OpenaiResponses => {
2637 body_json["model"] = json!(routed_model);
2638 let mut destream = false;
2639 let url = if oauth {
2640 if body_json["stream"].as_bool() != Some(true) {
2641 destream = true;
2642 }
2643 translate::normalize_codex_request(body_json);
2644 format!("{CODEX_BASE}/responses")
2645 } else {
2646 format!("{OPENAI_BASE}/v1/responses")
2647 };
2648 let body =
2649 serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
2650 Ok(UpstreamPlan {
2651 url,
2652 account,
2653 body,
2654 upstream_format: "openai-responses",
2655 destream,
2656 respond_as: None,
2657 client_stream,
2658 extra_headers: vec![],
2659 dario_guard: None,
2660 })
2661 }
2662 ClientFormat::AnthropicMessages => {
2663 let mut converted = translate::anthropic_to_openai_responses(body_json);
2664 converted["model"] = json!(routed_model);
2665 let url = if oauth {
2666 translate::normalize_codex_request(&mut converted);
2667 format!("{CODEX_BASE}/responses")
2668 } else {
2669 converted["stream"] = json!(false);
2670 format!("{OPENAI_BASE}/v1/responses")
2671 };
2672 let body = serde_json::to_vec(&converted)
2673 .unwrap_or_else(|_| original_body.to_vec());
2674 Ok(UpstreamPlan {
2675 url,
2676 account,
2677 body,
2678 upstream_format: "openai-responses",
2679 destream: false,
2680 respond_as: Some(RespondAs::Anthropic),
2681 client_stream,
2682 extra_headers: vec![],
2683 dario_guard: None,
2684 })
2685 }
2686 ClientFormat::GeminiGenerate => {
2687 let pivot = translate::gemini_to_anthropic(body_json);
2688 let mut converted = translate::anthropic_to_openai_responses(&pivot);
2689 converted["model"] = json!(routed_model);
2690 let url = if oauth {
2691 translate::normalize_codex_request(&mut converted);
2692 format!("{CODEX_BASE}/responses")
2693 } else {
2694 converted["stream"] = json!(false);
2695 format!("{OPENAI_BASE}/v1/responses")
2696 };
2697 let body = serde_json::to_vec(&converted)
2698 .unwrap_or_else(|_| original_body.to_vec());
2699 Ok(UpstreamPlan {
2700 url,
2701 account,
2702 body,
2703 upstream_format: "openai-responses",
2704 destream: false,
2705 respond_as: Some(RespondAs::Gemini),
2706 client_stream,
2707 extra_headers: vec![],
2708 dario_guard: None,
2709 })
2710 }
2711 }
2712 }
2713 Provider::Xai => {
2714 let account = state
2715 .vault
2716 .account_for_excluding(provider, true, excluded_accounts)
2717 .await
2718 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
2719 let extra_headers = vec![
2720 ("x-grok-model-override".into(), routed_model.to_string()),
2721 ("x-grok-conv-id".into(), trace_id.to_string()),
2722 ];
2723 match format {
2724 ClientFormat::OpenaiChat => {
2725 body_json["model"] = json!(routed_model);
2726 let body =
2727 serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
2728 Ok(UpstreamPlan {
2729 url: format!("{XAI_BASE}/chat/completions"),
2730 account,
2731 body,
2732 upstream_format: "openai-chat",
2733 destream: false,
2734 respond_as: None,
2735 client_stream,
2736 extra_headers,
2737 dario_guard: None,
2738 })
2739 }
2740 ClientFormat::AnthropicMessages => {
2741 let mut converted = translate::anthropic_to_openai_chat(body_json);
2743 converted["model"] = json!(routed_model);
2744 converted["stream"] = json!(false);
2746 let body = serde_json::to_vec(&converted)
2747 .unwrap_or_else(|_| original_body.to_vec());
2748 Ok(UpstreamPlan {
2749 url: format!("{XAI_BASE}/chat/completions"),
2750 account,
2751 body,
2752 upstream_format: "openai-chat",
2753 destream: false,
2754 respond_as: Some(RespondAs::Anthropic),
2755 client_stream,
2756 extra_headers,
2757 dario_guard: None,
2758 })
2759 }
2760 ClientFormat::OpenaiResponses | ClientFormat::GeminiGenerate => Err((
2761 StatusCode::NOT_IMPLEMENTED,
2762 "the xai/grok upstream speaks OpenAI chat completions; POST to /v1/chat/completions or /v1/messages"
2763 .to_string(),
2764 )),
2765 }
2766 }
2767 Provider::Gemini => {
2768 let account = state
2770 .vault
2771 .account_for_excluding(provider, false, excluded_accounts)
2772 .await
2773 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
2774 let (gemini_req, respond_as) = match format {
2775 ClientFormat::GeminiGenerate => {
2776 let mut g = body_json.clone();
2777 if let Some(o) = g.as_object_mut() {
2778 o.remove("model");
2779 o.remove("stream");
2780 }
2781 (g, None)
2782 }
2783 ClientFormat::AnthropicMessages => (
2784 translate::anthropic_to_gemini_request(body_json),
2785 Some(RespondAs::Anthropic),
2786 ),
2787 ClientFormat::OpenaiChat => (
2788 translate::anthropic_to_gemini_request(&translate::openai_chat_to_anthropic(
2789 body_json,
2790 )),
2791 Some(RespondAs::OpenaiChat),
2792 ),
2793 ClientFormat::OpenaiResponses => (
2794 translate::anthropic_to_gemini_request(
2795 &translate::openai_responses_to_anthropic(body_json),
2796 ),
2797 Some(RespondAs::OpenaiResponses),
2798 ),
2799 };
2800 let method = if client_stream {
2801 "streamGenerateContent"
2802 } else {
2803 "generateContent"
2804 };
2805 let (url, body) = if account.kind == "api_key" {
2806 let mut url = format!(
2808 "{GEMINI_API_BASE}/v1beta/models/{routed_model}:{method}"
2809 );
2810 if client_stream {
2811 url.push_str("?alt=sse");
2812 }
2813 let body =
2814 serde_json::to_vec(&gemini_req).unwrap_or_else(|_| original_body.to_vec());
2815 (url, body)
2816 } else {
2817 let project = ensure_gemini_project(state, &account).await?;
2819 let mut url = format!(
2820 "{GEMINI_CODE_ASSIST_BASE}/{GEMINI_CODE_ASSIST_VERSION}:{method}"
2821 );
2822 if client_stream {
2823 url.push_str("?alt=sse");
2824 }
2825 let envelope = json!({
2826 "model": routed_model,
2827 "project": project,
2828 "request": gemini_req,
2829 });
2830 let body =
2831 serde_json::to_vec(&envelope).unwrap_or_else(|_| original_body.to_vec());
2832 (url, body)
2833 };
2834 Ok(UpstreamPlan {
2835 url,
2836 account,
2837 body,
2838 upstream_format: "gemini",
2839 destream: false,
2840 respond_as: respond_as.or(Some(RespondAs::Gemini)),
2841 client_stream,
2842 extra_headers: vec![],
2843 dario_guard: None,
2844 })
2845 }
2846 }
2847}
2848
2849#[cfg(test)]
2850mod trace_api_tests {
2851 use super::{
2852 session_from_metadata, trace_extras, trace_harness, trace_reasoning_fields, transcript_turn,
2853 truncate_chars,
2854 };
2855 use axum::http::{HeaderMap, HeaderValue};
2856 use serde_json::json;
2857
2858 #[test]
2859 fn metadata_session_variants() {
2860 let claude = json!({"metadata": {"user_id":
2861 "{\"device_id\":\"d1\",\"session_id\":\"ses_inner\"}"}});
2862 assert_eq!(session_from_metadata(&claude), Some("ses_inner".into()));
2863 let plain = json!({"metadata": {"user_id": "user-123"}});
2864 assert_eq!(session_from_metadata(&plain), Some("user-123".into()));
2865 let json_no_session = json!({"metadata": {"user_id": "{\"device_id\":\"d1\"}"}});
2866 assert_eq!(
2867 session_from_metadata(&json_no_session),
2868 Some("{\"device_id\":\"d1\"}".into())
2869 );
2870 assert_eq!(session_from_metadata(&json!({})), None);
2871 }
2872
2873 #[test]
2874 fn explicit_harness_header_wins_over_sdk_user_agent() {
2875 let mut headers = HeaderMap::new();
2876 headers.insert(
2877 "user-agent",
2878 HeaderValue::from_static("Anthropic/JS 0.91.1"),
2879 );
2880 assert_eq!(trace_harness(&headers).as_deref(), Some("Anthropic/JS 0.91.1"));
2881 headers.insert(
2882 "x-alexandria-harness",
2883 HeaderValue::from_static("pi"),
2884 );
2885 assert_eq!(trace_harness(&headers).as_deref(), Some("pi"));
2886 }
2887
2888 #[test]
2889 fn extras_per_format() {
2890 let anthropic = json!({
2891 "system": [{"type": "text", "text": "abcd"}],
2892 "messages": [{"role": "user", "content": "hi"}],
2893 "max_tokens": 100,
2894 "temperature": 0.5,
2895 "thinking": {"type": "enabled", "budget_tokens": 4096},
2896 });
2897 let e = trace_extras(&anthropic);
2898 assert_eq!(e["thinking_budget"], 4096);
2899 assert_eq!(trace_reasoning_fields(&anthropic), (None, Some(4096)));
2900 assert_eq!(e["max_tokens"], 100);
2901 assert_eq!(e["temperature"], 0.5);
2902 assert_eq!(e["message_count"], 1);
2903 assert_eq!(e["system_chars"], 4);
2904 assert_eq!(e["reasoning_effort"], serde_json::Value::Null);
2905 let responses = json!({
2906 "instructions": "abc",
2907 "input": [{"type": "message"}, {"type": "message"}],
2908 "max_output_tokens": 200,
2909 "reasoning": {"effort": "high"},
2910 });
2911 let e = trace_extras(&responses);
2912 assert_eq!(e["reasoning_effort"], "high");
2913 assert_eq!(
2914 trace_reasoning_fields(&responses),
2915 (Some("high".into()), None)
2916 );
2917 assert_eq!(e["max_tokens"], 200);
2918 assert_eq!(e["message_count"], 2);
2919 assert_eq!(e["system_chars"], 3);
2920 assert_eq!(e["thinking_budget"], serde_json::Value::Null);
2921 }
2922
2923 #[test]
2924 fn truncates_on_char_boundaries() {
2925 assert_eq!(truncate_chars("abc".into(), 8000), "abc");
2926 assert_eq!(truncate_chars("héllo".repeat(2000), 8000).chars().count(), 8000);
2927 }
2928
2929 #[test]
2930 fn transcript_turn_missing_bodies_are_null() {
2931 let row = json!({
2932 "id": "t1", "ts_request_ms": 1, "ts_response_ms": 2,
2933 "routed_model": "m", "status": 200,
2934 "input_tokens": 10, "output_tokens": 5, "cost_usd": 0.01, "error": null,
2935 "reasoning_effort": "minimal", "thinking_budget": null,
2936 "req_body_path": "/nonexistent/x.gz", "resp_body_path": null,
2937 "client_format": "anthropic", "upstream_format": "anthropic",
2938 });
2939 let turn = transcript_turn(&row);
2940 assert_eq!(turn["trace_id"], "t1");
2941 assert_eq!(turn["user"], serde_json::Value::Null);
2942 assert_eq!(turn["assistant"], serde_json::Value::Null);
2943 assert_eq!(turn["model"], "m");
2944 assert_eq!(turn["reasoning_effort"], "minimal");
2945 assert_eq!(turn["thinking_budget"], serde_json::Value::Null);
2946 }
2947}
2948
2949#[cfg(test)]
2950mod run_key_tests {
2951 use super::{generate_run_key, key_fingerprint, key_hash_hex, merge_run_key_tags};
2952
2953 #[test]
2954 fn merge_header_tags_win_per_key() {
2955 let merged = merge_run_key_tags(
2956 Some(r#"{"task":"demo","suite":"swebench"}"#),
2957 Some(r#"{"task":"override","case":"x1"}"#),
2958 )
2959 .unwrap();
2960 let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
2961 assert_eq!(v["task"], "override");
2962 assert_eq!(v["suite"], "swebench");
2963 assert_eq!(v["case"], "x1");
2964 }
2965
2966 #[test]
2967 fn merge_handles_missing_sides() {
2968 assert_eq!(merge_run_key_tags(None, None), None);
2969 assert_eq!(
2970 merge_run_key_tags(Some(r#"{"a":"1"}"#), None).unwrap(),
2971 r#"{"a":"1"}"#
2972 );
2973 assert_eq!(
2974 merge_run_key_tags(None, Some(r#"{"b":"2"}"#)).unwrap(),
2975 r#"{"b":"2"}"#
2976 );
2977 assert_eq!(merge_run_key_tags(Some("not json"), None), None);
2978 }
2979
2980 #[test]
2981 fn run_key_shape_and_fingerprint() {
2982 let key = generate_run_key();
2983 assert!(key.starts_with("alxk-"));
2984 assert_eq!(key.len(), 5 + 64);
2985 assert_ne!(key, generate_run_key());
2986 let hash = key_hash_hex(&key);
2987 assert_eq!(hash.len(), 64);
2988 assert_eq!(hash[..16], key_fingerprint(&key));
2989 }
2990}
2991
2992#[cfg(test)]
2993mod usage_tests {
2994 use super::{key_fingerprint, usage_backoff_ms};
2995
2996 #[test]
2997 fn fingerprint_is_first_8_sha256_bytes_hex() {
2998 assert_eq!(key_fingerprint("abc"), "ba7816bf8f01cfea");
2999 assert_eq!(key_fingerprint("abc").len(), 16);
3000 assert_ne!(key_fingerprint("abc"), key_fingerprint("abd"));
3001 }
3002
3003 #[test]
3004 fn backoff_grows_and_caps() {
3005 assert_eq!(usage_backoff_ms(1, None), 60_000);
3006 assert_eq!(usage_backoff_ms(2, None), 120_000);
3007 assert_eq!(usage_backoff_ms(3, None), 240_000);
3008 assert_eq!(usage_backoff_ms(10, None), 3_600_000);
3009 }
3010
3011 #[test]
3012 fn retry_after_wins_when_larger() {
3013 assert_eq!(usage_backoff_ms(1, Some(600_000)), 600_000);
3014 assert_eq!(usage_backoff_ms(5, Some(1_000)), 960_000);
3015 }
3016}
3017
3018fn upstream_headers(
3019 account: &Account,
3020 client_headers: &HeaderMap,
3021 genuine_claude_code: bool,
3022) -> Result<reqwest::header::HeaderMap, (StatusCode, String)> {
3023 let mut h = reqwest::header::HeaderMap::new();
3024 h.insert("content-type", HeaderValue::from_static("application/json"));
3025 h.insert("accept", HeaderValue::from_static("*/*"));
3026 h.insert("accept-encoding", HeaderValue::from_static("identity"));
3027 if let Some(ua) = client_headers.get("user-agent") {
3028 h.insert("user-agent", ua.clone());
3029 }
3030 match (account.provider, account.kind.as_str()) {
3031 (Provider::Anthropic, "oauth") => {
3032 let token = account.access_token.as_deref().ok_or((
3033 StatusCode::BAD_GATEWAY,
3034 "anthropic oauth account has no access token".to_string(),
3035 ))?;
3036 h.insert(
3037 "authorization",
3038 HeaderValue::from_str(&format!("Bearer {token}"))
3039 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3040 );
3041 let mut beta = ANTHROPIC_OAUTH_BETA.to_string();
3042 if let Some(client_beta) = client_headers
3043 .get("anthropic-beta")
3044 .and_then(|v| v.to_str().ok())
3045 {
3046 if !client_beta.contains(ANTHROPIC_OAUTH_BETA) {
3047 beta = format!("{ANTHROPIC_OAUTH_BETA},{client_beta}");
3048 } else {
3049 beta = client_beta.to_string();
3050 }
3051 }
3052 h.insert(
3053 "anthropic-beta",
3054 HeaderValue::from_str(&beta)
3055 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3056 );
3057 insert_anthropic_version(&mut h, client_headers);
3058 }
3059 (Provider::Anthropic, _) => {
3060 let key = account
3061 .api_key
3062 .as_deref()
3063 .or(account.access_token.as_deref())
3064 .ok_or((
3065 StatusCode::BAD_GATEWAY,
3066 "anthropic account has no api key".to_string(),
3067 ))?;
3068 h.insert(
3069 "x-api-key",
3070 HeaderValue::from_str(key).map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3071 );
3072 if let Some(client_beta) = client_headers.get("anthropic-beta") {
3073 h.insert("anthropic-beta", client_beta.clone());
3074 }
3075 insert_anthropic_version(&mut h, client_headers);
3076 }
3077 (Provider::Openai, "oauth") => {
3078 let token = account.access_token.as_deref().ok_or((
3079 StatusCode::BAD_GATEWAY,
3080 "openai oauth account has no access token".to_string(),
3081 ))?;
3082 h.insert(
3083 "authorization",
3084 HeaderValue::from_str(&format!("Bearer {token}"))
3085 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3086 );
3087 if let Some(acct) = account.chatgpt_account_id() {
3088 h.insert(
3089 "chatgpt-account-id",
3090 HeaderValue::from_str(&acct)
3091 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3092 );
3093 }
3094 h.insert(
3095 "openai-beta",
3096 HeaderValue::from_static("responses=experimental"),
3097 );
3098 h.insert("originator", HeaderValue::from_static("codex_cli_rs"));
3099 let session = client_headers
3100 .get("session_id")
3101 .and_then(|v| v.to_str().ok())
3102 .map(String::from)
3103 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
3104 h.insert(
3105 "session_id",
3106 HeaderValue::from_str(&session)
3107 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3108 );
3109 }
3110 (Provider::Openai, _) => {
3111 let key = account.api_key.as_deref().ok_or((
3112 StatusCode::BAD_GATEWAY,
3113 "openai account has no api key".to_string(),
3114 ))?;
3115 h.insert(
3116 "authorization",
3117 HeaderValue::from_str(&format!("Bearer {key}"))
3118 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3119 );
3120 }
3121 (Provider::Xai, _) => {
3122 let token = account
3123 .access_token
3124 .as_deref()
3125 .or(account.api_key.as_deref())
3126 .ok_or((
3127 StatusCode::BAD_GATEWAY,
3128 "xai account has no access token".to_string(),
3129 ))?;
3130 h.insert(
3131 "authorization",
3132 HeaderValue::from_str(&format!("Bearer {token}"))
3133 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3134 );
3135 h.insert("x-xai-token-auth", HeaderValue::from_static("xai-grok-cli"));
3136 h.insert(
3137 "x-grok-client-version",
3138 HeaderValue::from_static(GROK_CLIENT_VERSION),
3139 );
3140 h.insert("user-agent", HeaderValue::from_static("xai-grok-workspace"));
3141 h.insert("accept", HeaderValue::from_static("text/event-stream"));
3142 }
3143 (Provider::Gemini, "api_key") => {
3144 let key = account.api_key.as_deref().ok_or((
3145 StatusCode::BAD_GATEWAY,
3146 "gemini api_key account has no key".to_string(),
3147 ))?;
3148 h.insert(
3149 "x-goog-api-key",
3150 HeaderValue::from_str(key)
3151 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3152 );
3153 }
3154 (Provider::Gemini, _) => {
3155 let token = account.access_token.as_deref().ok_or((
3156 StatusCode::BAD_GATEWAY,
3157 "gemini account has no access token".to_string(),
3158 ))?;
3159 h.insert(
3160 "authorization",
3161 HeaderValue::from_str(&format!("Bearer {token}"))
3162 .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
3163 );
3164 }
3165 }
3166 if genuine_claude_code && account.provider == Provider::Anthropic {
3167 for name in CLAUDE_CODE_PASSTHROUGH_HEADERS {
3168 if let Some(value) = client_headers.get(*name) {
3169 h.insert(*name, value.clone());
3170 }
3171 }
3172 }
3173 Ok(h)
3174}
3175
3176fn insert_anthropic_version(h: &mut reqwest::header::HeaderMap, client_headers: &HeaderMap) {
3177 match client_headers.get("anthropic-version") {
3178 Some(v) => {
3179 h.insert("anthropic-version", v.clone());
3180 }
3181 None => {
3182 h.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
3183 }
3184 }
3185}
3186
3187fn key_fingerprint(key: &str) -> String {
3188 use sha2::{Digest, Sha256};
3189 let digest = Sha256::digest(key.as_bytes());
3190 digest[..8].iter().map(|b| format!("{b:02x}")).collect()
3191}
3192
3193const RUN_KEY_PREFIX: &str = "alxk-";
3194const RUN_KEY_DEFAULT_TTL_S: i64 = 86_400;
3195const RUN_KEY_MAX_TTL_S: i64 = 604_800;
3196
3197pub fn generate_run_key() -> String {
3198 use rand::RngCore;
3199 let mut bytes = [0u8; 32];
3200 rand::thread_rng().fill_bytes(&mut bytes);
3201 format!(
3202 "{RUN_KEY_PREFIX}{}",
3203 bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()
3204 )
3205}
3206
3207fn key_hash_hex(key: &str) -> String {
3208 use sha2::{Digest, Sha256};
3209 let digest = Sha256::digest(key.as_bytes());
3210 digest.iter().map(|b| format!("{b:02x}")).collect()
3211}
3212
3213fn merge_run_key_tags(key_tags: Option<&str>, header_tags: Option<&str>) -> Option<String> {
3214 let parse = |s: Option<&str>| {
3215 s.and_then(|s| serde_json::from_str::<Value>(s).ok())
3216 .and_then(|v| v.as_object().cloned())
3217 .unwrap_or_default()
3218 };
3219 let mut merged = parse(key_tags);
3220 merged.extend(parse(header_tags));
3221 if merged.is_empty() {
3222 None
3223 } else {
3224 serde_json::to_string(&Value::Object(merged)).ok()
3225 }
3226}
3227
3228fn run_key_entry(state: &AppState, key_hash: &str) -> Option<CachedRunKey> {
3229 let now = now_ms();
3230 let cached = state.run_keys.read().unwrap().get(key_hash).cloned();
3231 if let Some(entry) = cached {
3232 if entry.expires_ms.map(|e| e > now).unwrap_or(true) {
3233 return Some(entry);
3234 }
3235 state.run_keys.write().unwrap().remove(key_hash);
3236 return None;
3237 }
3238 let row = state.store.lookup_run_key(key_hash, now).ok().flatten()?;
3239 let entry = CachedRunKey {
3240 run_id: row["run_id"].as_str().map(String::from),
3241 tags_json: row["tags"]
3242 .as_object()
3243 .filter(|o| !o.is_empty())
3244 .and_then(|o| serde_json::to_string(o).ok()),
3245 expires_ms: row["expires_ms"].as_i64(),
3246 };
3247 state
3248 .run_keys
3249 .write()
3250 .unwrap()
3251 .insert(key_hash.to_string(), entry.clone());
3252 Some(entry)
3253}
3254
3255async fn admin_run_keys_create(
3256 State(state): State<Arc<AppState>>,
3257 body: Option<axum::Json<Value>>,
3258) -> Response {
3259 let body = body.map(|b| b.0).unwrap_or_else(|| json!({}));
3260 let tags = match &body["tags"] {
3261 Value::Null => None,
3262 Value::Object(o) => Some(o.clone()),
3263 _ => return error_response(StatusCode::BAD_REQUEST, "'tags' must be an object"),
3264 };
3265 let kind = body["kind"].as_str().unwrap_or("run");
3266 if kind != "run" && kind != "harness" {
3267 return error_response(StatusCode::BAD_REQUEST, "'kind' must be 'run' or 'harness'");
3268 }
3269 let run_id = body["run_id"].as_str().map(String::from);
3270 let label = body["label"].as_str().map(String::from);
3271 if kind == "harness" && label.as_deref().unwrap_or("").trim().is_empty() {
3272 return error_response(StatusCode::BAD_REQUEST, "'label' is required for harness keys");
3273 }
3274 let created_ms = now_ms();
3275 let expires_ms = if kind == "harness" {
3276 None
3277 } else {
3278 let ttl_seconds = body["ttl_seconds"].as_i64().unwrap_or(RUN_KEY_DEFAULT_TTL_S);
3279 if ttl_seconds <= 0 {
3280 return error_response(StatusCode::BAD_REQUEST, "'ttl_seconds' must be positive");
3281 }
3282 Some(created_ms + ttl_seconds.min(RUN_KEY_MAX_TTL_S) * 1000)
3283 };
3284 let key = generate_run_key();
3285 let key_hash = key_hash_hex(&key);
3286 let id = format!("rk-{}", &key_hash[..8]);
3287 let tags_json = tags
3288 .as_ref()
3289 .filter(|o| !o.is_empty())
3290 .and_then(|o| serde_json::to_string(o).ok());
3291 match state.store.insert_run_key(
3292 &id,
3293 &key_hash,
3294 kind,
3295 run_id.as_deref(),
3296 tags_json.as_deref(),
3297 label.as_deref(),
3298 created_ms,
3299 expires_ms,
3300 ) {
3301 Ok(()) => {
3302 let (_, exports) = connect_payload(&state.base_url, &key);
3303 (
3304 StatusCode::CREATED,
3305 axum::Json(json!({
3306 "id": id,
3307 "key": key,
3308 "kind": kind,
3309 "run_id": run_id,
3310 "label": label,
3311 "tags": tags.map(Value::Object).unwrap_or_else(|| json!({})),
3312 "expires_ms": expires_ms,
3313 "exports": exports,
3314 })),
3315 )
3316 .into_response()
3317 }
3318 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
3319 }
3320}
3321
3322async fn admin_run_keys_list(
3323 State(state): State<Arc<AppState>>,
3324 Query(q): Query<HashMap<String, String>>,
3325) -> Response {
3326 let all = q.get("all").map(|v| v == "1" || v == "true").unwrap_or(false);
3327 match state.store.list_run_keys(all) {
3328 Ok(rows) => axum::Json(json!({"run_keys": rows})).into_response(),
3329 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
3330 }
3331}
3332
3333async fn admin_run_keys_revoke(
3334 State(state): State<Arc<AppState>>,
3335 Path(id): Path<String>,
3336) -> Response {
3337 match state.store.revoke_run_key(&id) {
3338 Ok(true) => {
3339 state.run_keys.write().unwrap().clear();
3340 axum::Json(json!({"revoked": true})).into_response()
3341 }
3342 Ok(false) => error_response(StatusCode::NOT_FOUND, &format!("unknown run key '{id}'")),
3343 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
3344 }
3345}
3346
3347fn session_from_metadata(body_json: &Value) -> Option<String> {
3348 let raw = body_json["metadata"]["user_id"].as_str()?;
3349 if let Ok(v) = serde_json::from_str::<Value>(raw) {
3350 if let Some(inner) = v["session_id"].as_str() {
3351 return Some(inner.to_string());
3352 }
3353 }
3354 Some(raw.to_string())
3355}
3356
3357fn trace_harness(headers: &HeaderMap) -> Option<String> {
3358 headers
3359 .get("x-alexandria-harness")
3360 .and_then(|value| value.to_str().ok())
3361 .map(str::trim)
3362 .filter(|value| !value.is_empty())
3363 .map(String::from)
3364 .or_else(|| {
3365 headers
3366 .get("user-agent")
3367 .and_then(|value| value.to_str().ok())
3368 .map(String::from)
3369 })
3370}
3371
3372const METADATA_HEADERS: &[(&str, &str)] = &[
3373 ("x-alexandria-harness", "harness"),
3374 ("x-alexandria-harness-version", "harness_version"),
3375 ("x-alexandria-task", "task"),
3376 ("x-alexandria-model", "model"),
3377 ("x-alexandria-job", "job"),
3378 ("x-alexandria-phase", "phase"),
3379 ("x-alexandria-kind", "kind"),
3380];
3381
3382fn trace_tags_json(headers: &HeaderMap) -> Option<String> {
3383 let values: Vec<&str> = headers
3384 .get_all("x-alexandria-trace-tag")
3385 .iter()
3386 .filter_map(|v| v.to_str().ok())
3387 .collect();
3388 let mut tags = parse_trace_tags(&values);
3389 if let Some(o) = tags.as_object_mut() {
3390 for (header, key) in METADATA_HEADERS {
3391 if o.contains_key(*key) {
3392 continue;
3393 }
3394 if let Some(v) = headers.get(*header).and_then(|v| v.to_str().ok()) {
3395 let v = v.trim();
3396 if !v.is_empty() {
3397 o.insert((*key).to_string(), json!(v));
3398 }
3399 }
3400 }
3401 }
3402 if tags.as_object().map(|o| o.is_empty()).unwrap_or(true) {
3403 return None;
3404 }
3405 serde_json::to_string(&tags).ok()
3406}
3407
3408fn retryable_failover_status(status: reqwest::StatusCode) -> bool {
3409 status == reqwest::StatusCode::TOO_MANY_REQUESTS
3410 || status == reqwest::StatusCode::UNAUTHORIZED
3411 || status == reqwest::StatusCode::FORBIDDEN
3412 || status.is_server_error()
3413}
3414
3415fn retry_failover_allowed(
3416 provider: Provider,
3417 thread_was_affined: bool,
3418 allow_mid_thread_failover: bool,
3419) -> bool {
3420 provider != Provider::Openai || !thread_was_affined || allow_mid_thread_failover
3421}
3422
3423async fn proxy(
3424 state: Arc<AppState>,
3425 format: ClientFormat,
3426 path: &'static str,
3427 headers: HeaderMap,
3428 body: Bytes,
3429 peer: Option<std::net::SocketAddr>,
3430) -> Response {
3431 let mut run_key: Option<CachedRunKey> = None;
3432 let client_fingerprint = match client_key(&headers) {
3433 Some(k) if k == state.local_key => key_fingerprint(&k),
3434 Some(k) => {
3435 let key_hash = key_hash_hex(&k);
3436 match run_key_entry(&state, &key_hash) {
3437 Some(entry) => {
3438 if let Err(e) = state.store.touch_run_key(&key_hash, now_ms()) {
3439 tracing::warn!("failed to touch run key: {e}");
3440 }
3441 run_key = Some(entry);
3442 key_hash.chars().take(16).collect()
3443 }
3444 None if k.starts_with(RUN_KEY_PREFIX) => {
3445 return error_response(
3446 StatusCode::UNAUTHORIZED,
3447 "run key expired or revoked",
3448 )
3449 }
3450 None => {
3451 return error_response(
3452 StatusCode::UNAUTHORIZED,
3453 "bad or missing local key (x-api-key / Authorization: Bearer)",
3454 )
3455 }
3456 }
3457 }
3458 None => {
3459 return error_response(
3460 StatusCode::UNAUTHORIZED,
3461 "bad or missing local key (x-api-key / Authorization: Bearer)",
3462 )
3463 }
3464 };
3465
3466 let in_flight = InFlight::new(&state);
3467 let trace_id = uuid::Uuid::new_v4().to_string();
3468 let mut trace = TraceRecord {
3469 id: trace_id.clone(),
3470 ts_request_ms: now_ms(),
3471 method: Some("POST".into()),
3472 path: Some(path.into()),
3473 client_format: Some(format.as_str().into()),
3474 harness: trace_harness(&headers),
3475 req_headers_json: Some(redacted_headers(&headers)),
3476 run_id: headers
3477 .get("x-alexandria-run-id")
3478 .and_then(|v| v.to_str().ok())
3479 .map(String::from)
3480 .or_else(|| run_key.as_ref().and_then(|k| k.run_id.clone())),
3481 tags: merge_run_key_tags(
3482 run_key.as_ref().and_then(|k| k.tags_json.as_deref()),
3483 trace_tags_json(&headers).as_deref(),
3484 ),
3485 client_ip: peer.map(|p| p.ip().to_string()),
3486 key_fingerprint: Some(client_fingerprint),
3487 ..Default::default()
3488 };
3489
3490 let mut body_json: Value = match serde_json::from_slice(&body) {
3491 Ok(v) => v,
3492 Err(e) => {
3493 return error_response(StatusCode::BAD_REQUEST, &format!("body is not JSON: {e}"))
3494 }
3495 };
3496 let (reasoning_effort, thinking_budget) = trace_reasoning_fields(&body_json);
3497 trace.reasoning_effort = reasoning_effort;
3498 trace.thinking_budget = thinking_budget;
3499 let genuine_claude_code = is_genuine_claude_code_request(format, &headers, &body_json);
3500
3501 trace.session_id = headers
3502 .get("x-session-id")
3503 .and_then(|v| v.to_str().ok())
3504 .map(String::from)
3505 .or_else(|| {
3506 headers
3507 .get("session_id")
3508 .and_then(|v| v.to_str().ok())
3509 .map(String::from)
3510 })
3511 .or_else(|| {
3512 headers
3513 .get("x-claude-code-session-id")
3514 .and_then(|v| v.to_str().ok())
3515 .map(String::from)
3516 })
3517 .or_else(|| session_from_metadata(&body_json))
3518 .or_else(|| {
3519 body_json
3520 .get("prompt_cache_key")
3521 .and_then(Value::as_str)
3522 .filter(|value| !value.is_empty())
3523 .map(String::from)
3524 })
3525 .or_else(|| {
3526 conversation_root(format, &body_json).map(|root| {
3527 let ua = headers
3528 .get("user-agent")
3529 .and_then(|v| v.to_str().ok())
3530 .unwrap_or("");
3531 let ip = peer.map(|p| p.ip().to_string()).unwrap_or_default();
3532 format!("auto-{}", key_fingerprint(&format!("{root}{ua}{ip}")))
3533 })
3534 });
3535
3536 let requested_model = body_json["model"].as_str().unwrap_or("").to_string();
3537 if requested_model.is_empty() {
3538 return error_response(StatusCode::BAD_REQUEST, "missing 'model' in request body");
3539 }
3540 let (routed_provider, routed_model) = route_model(&requested_model);
3541 let provider = routed_provider.unwrap_or_else(|| format.default_provider());
3542 trace.requested_model = Some(requested_model.clone());
3543 trace.routed_model = Some(routed_model.clone());
3544 trace.upstream_provider = Some(provider.as_str().into());
3545 trace.streamed = Some(body_json["stream"].as_bool().unwrap_or(false));
3546
3547 let codex_thread_was_affined = provider == Provider::Openai
3551 && preferred_codex_account(&state, trace.session_id.as_deref()).is_some();
3552 let allow_mid_thread_failover = state
3553 .vault
3554 .policy(Provider::Openai)
3555 .allow_mid_thread_failover;
3556
3557 let original_body_json = body_json.clone();
3558 let mut attempted_accounts = HashSet::new();
3559 let mut plan = match plan_upstream(
3560 &state,
3561 format,
3562 provider,
3563 &routed_model,
3564 &mut body_json,
3565 &body,
3566 &trace_id,
3567 &attempted_accounts,
3568 trace.session_id.as_deref(),
3569 &headers,
3570 )
3571 .await
3572 {
3573 Ok(p) => p,
3574 Err((status, msg)) => {
3575 trace.status = Some(status.as_u16() as i64);
3576 trace.error = Some(msg.clone());
3577 finalize_trace(&state, trace, &body, None, None);
3578 return error_response(status, &msg);
3579 }
3580 };
3581 trace.upstream_format = Some(plan.upstream_format.into());
3582 trace.account_id = Some(plan.account.id.clone());
3583 trace.billing_bucket = Some(
3584 if plan.account.kind == "oauth" || plan.account.kind == "dario" {
3585 "subscription"
3586 } else {
3587 "api"
3588 }
3589 .into(),
3590 );
3591
3592 tracing::info!(
3593 trace_id,
3594 model = %routed_model,
3595 provider = provider.as_str(),
3596 account = %plan.account.id,
3597 url = %plan.url,
3598 genuine_claude_code,
3599 "proxying request"
3600 );
3601
3602 let mut account = plan.account.clone();
3603 let mut upstream_resp = None;
3604 'accounts: loop {
3605 if !attempted_accounts.insert(account.id.clone()) {
3606 tracing::error!(account = %account.id, "refusing to retry an already-attempted account");
3607 break;
3608 }
3609
3610 let mut forced_oauth_refresh = false;
3611 loop {
3612 let mut up_headers =
3613 match upstream_headers(&account, &headers, genuine_claude_code) {
3614 Ok(h) => h,
3615 Err((status, msg)) => {
3616 trace.status = Some(status.as_u16() as i64);
3617 trace.error = Some(msg.clone());
3618 finalize_trace(&state, trace, &body, Some(&plan.body), None);
3619 return error_response(status, &msg);
3620 }
3621 };
3622 for (k, v) in &plan.extra_headers {
3623 if let (Ok(name), Ok(value)) = (
3624 reqwest::header::HeaderName::from_bytes(k.as_bytes()),
3625 HeaderValue::from_str(v),
3626 ) {
3627 up_headers.insert(name, value);
3628 }
3629 }
3630 let resp = match state
3631 .http
3632 .post(&plan.url)
3633 .headers(up_headers)
3634 .body(plan.body.clone())
3635 .send()
3636 .await
3637 {
3638 Ok(r) => r,
3639 Err(e) => {
3640 let msg = format!("upstream request failed: {e}");
3641 suspect_dario(&state, &account);
3642 trace.status = Some(502);
3643 trace.error = Some(msg.clone());
3644 finalize_trace(&state, trace, &body, Some(&plan.body), None);
3645 return error_response(StatusCode::BAD_GATEWAY, &msg);
3646 }
3647 };
3648
3649 if account.provider == Provider::Openai && account.kind == "oauth" {
3650 if let Some(snapshot) = codex_limits_from_headers(resp.headers()) {
3651 if let Err(error) = state
3652 .vault
3653 .record_codex_limits(&account.id, snapshot)
3654 .await
3655 {
3656 tracing::warn!(account = %account.id, %error, "could not persist Codex limit snapshot");
3657 }
3658 }
3659 }
3660 if resp.status() == reqwest::StatusCode::UNAUTHORIZED
3661 && account.kind == "oauth"
3662 && !forced_oauth_refresh
3663 {
3664 tracing::warn!(
3665 account = %account.id,
3666 "upstream returned 401 for oauth account; forcing token refresh and retrying"
3667 );
3668 forced_oauth_refresh = true;
3669 match state.vault.refresh(&account.id, true).await {
3670 Ok(fresh) => {
3671 account = fresh;
3672 continue;
3673 }
3674 Err(e) => {
3675 tracing::warn!("forced refresh failed: {e}");
3676 }
3677 }
3678 }
3679
3680 if retryable_failover_status(resp.status()) && account.kind != "dario" {
3681 let retry_after_s = resp
3682 .headers()
3683 .get("retry-after")
3684 .and_then(|v| v.to_str().ok())
3685 .and_then(|v| v.parse::<i64>().ok())
3686 .unwrap_or(60)
3687 .clamp(1, 3600);
3688 tracing::warn!(account = %account.id, retry_after_s, status = %resp.status(), "upstream failed; cooling account down");
3689 if let Err(e) = state
3690 .vault
3691 .mark_cooldown(&account.id, now_ms() + retry_after_s * 1000)
3692 .await
3693 {
3694 tracing::warn!("failed to mark cooldown: {e}");
3695 }
3696
3697 if !retry_failover_allowed(
3698 provider,
3699 codex_thread_was_affined,
3700 allow_mid_thread_failover,
3701 ) {
3702 tracing::warn!(
3703 account = %account.id,
3704 status = %resp.status(),
3705 "returning retryable Codex error without switching an affined thread"
3706 );
3707 upstream_resp = Some(resp);
3708 break 'accounts;
3709 }
3710
3711 let mut retry_body_json = original_body_json.clone();
3712 match plan_upstream(
3713 &state,
3714 format,
3715 provider,
3716 &routed_model,
3717 &mut retry_body_json,
3718 &body,
3719 &trace_id,
3720 &attempted_accounts,
3721 trace.session_id.as_deref(),
3722 &headers,
3723 )
3724 .await
3725 {
3726 Ok(next_plan) if !attempted_accounts.contains(&next_plan.account.id) => {
3727 plan = next_plan;
3728 account = plan.account.clone();
3729 trace.account_id = Some(account.id.clone());
3730 trace.upstream_format = Some(plan.upstream_format.into());
3731 trace.billing_bucket = Some(
3732 if account.kind == "oauth" || account.kind == "dario" {
3733 "subscription"
3734 } else {
3735 "api"
3736 }
3737 .into(),
3738 );
3739 tracing::warn!(
3740 account = %account.id,
3741 attempted = attempted_accounts.len(),
3742 "retrying with failover account"
3743 );
3744 continue 'accounts;
3745 }
3746 Ok(next_plan) => {
3747 tracing::error!(
3748 account = %next_plan.account.id,
3749 "account selector returned an already-attempted failover account"
3750 );
3751 }
3752 Err((status, msg)) => tracing::warn!(
3753 status = %status,
3754 error = %msg,
3755 attempted = attempted_accounts.len(),
3756 "no untried failover account available"
3757 ),
3758 }
3759 }
3760
3761 upstream_resp = Some(resp);
3762 break 'accounts;
3763 }
3764 }
3765 let upstream_resp = upstream_resp.expect("upstream response after retry loop");
3766 trace.account_id = Some(account.id.clone());
3767
3768 let status = upstream_resp.status();
3769 trace.status = Some(status.as_u16() as i64);
3770 let resp_headers = upstream_resp.headers().clone();
3771 let content_type = resp_headers
3772 .get("content-type")
3773 .and_then(|v| v.to_str().ok())
3774 .unwrap_or("application/octet-stream")
3775 .to_string();
3776 trace.resp_headers_json = Some(redacted_headers(&resp_headers));
3777 let is_sse = content_type.starts_with("text/event-stream");
3778
3779 if let Some(target) = plan.respond_as {
3780 use alex_core::translate;
3781 let buf = match upstream_resp.bytes().await {
3782 Ok(b) => b.to_vec(),
3783 Err(e) => {
3784 let msg = format!("upstream body read failed: {e}");
3785 suspect_dario(&state, &account);
3786 trace.status = Some(502);
3787 trace.error = Some(msg.clone());
3788 finalize_trace(&state, trace, &body, Some(&plan.body), None);
3789 return error_response(StatusCode::BAD_GATEWAY, &msg);
3790 }
3791 };
3792 drop(plan.dario_guard.take());
3793 trace.ts_response_ms = Some(now_ms());
3794 fill_usage_and_cost(&state, &mut trace, &buf, is_sse);
3795 let text = String::from_utf8_lossy(&buf).to_string();
3796 if !status.is_success() {
3797 finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
3798 return Response::builder()
3799 .status(status)
3800 .header("content-type", "application/json")
3801 .header("x-alexandria-trace-id", &trace_id)
3802 .body(Body::from(buf))
3803 .unwrap_or_else(|e| {
3804 error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
3805 });
3806 }
3807 let trimmed = text.trim_start();
3808 let looks_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
3809 let upstream_final = if plan.upstream_format == "anthropic" {
3810 if is_sse || looks_sse {
3811 translate::parse_anthropic_sse_to_message(&text)
3812 } else {
3813 serde_json::from_str(&text).ok()
3814 }
3815 } else if plan.upstream_format == "gemini" {
3816 translate::parse_gemini_upstream_final(&text)
3817 } else if plan.upstream_format == "openai-chat" {
3818 if is_sse || looks_sse {
3819 translate::parse_openai_chat_sse_final(&text)
3820 } else {
3821 serde_json::from_str(&text).ok()
3822 }
3823 } else if is_sse || looks_sse {
3824 translate::parse_responses_sse_final(&text)
3825 } else {
3826 serde_json::from_str(&text).ok()
3827 };
3828 let Some(upstream_final) = upstream_final else {
3829 let msg = "could not reassemble upstream response for translation";
3830 trace.error = Some(msg.to_string());
3831 finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
3832 return error_response(StatusCode::BAD_GATEWAY, msg);
3833 };
3834 let out = match (target, plan.upstream_format) {
3835 (RespondAs::Gemini, "gemini") => upstream_final.clone(),
3836 (RespondAs::Anthropic, "gemini") => {
3837 translate::gemini_response_to_anthropic(&upstream_final, &requested_model)
3838 }
3839 (RespondAs::OpenaiChat, "gemini") => translate::anthropic_response_to_openai_chat(
3840 &translate::gemini_response_to_anthropic(&upstream_final, &requested_model),
3841 &requested_model,
3842 ),
3843 (RespondAs::OpenaiResponses, "gemini") => {
3844 translate::anthropic_response_to_openai_responses(
3845 &translate::gemini_response_to_anthropic(&upstream_final, &requested_model),
3846 &requested_model,
3847 )
3848 }
3849 (RespondAs::OpenaiChat, "anthropic") => {
3850 translate::anthropic_response_to_openai_chat(&upstream_final, &requested_model)
3851 }
3852 (RespondAs::OpenaiResponses, "anthropic") => translate::anthropic_response_to_openai_responses(
3853 &upstream_final,
3854 &requested_model,
3855 ),
3856 (RespondAs::Gemini, "anthropic") => {
3857 translate::anthropic_response_to_gemini(&upstream_final, &requested_model)
3858 }
3859 (RespondAs::Anthropic, "openai-chat") => {
3860 translate::openai_chat_response_to_anthropic(&upstream_final, &requested_model)
3861 }
3862 (RespondAs::OpenaiChat, "openai-chat") => upstream_final.clone(),
3863 (RespondAs::OpenaiResponses, "openai-chat") => {
3864 translate::anthropic_response_to_openai_responses(
3865 &translate::openai_chat_response_to_anthropic(
3866 &upstream_final,
3867 &requested_model,
3868 ),
3869 &requested_model,
3870 )
3871 }
3872 (RespondAs::Gemini, "openai-chat") => translate::anthropic_response_to_gemini(
3873 &translate::openai_chat_response_to_anthropic(&upstream_final, &requested_model),
3874 &requested_model,
3875 ),
3876 (RespondAs::Gemini, _) => translate::anthropic_response_to_gemini(
3877 &translate::responses_final_to_anthropic(&upstream_final, &requested_model),
3878 &requested_model,
3879 ),
3880 (RespondAs::Anthropic, "anthropic") | (RespondAs::OpenaiResponses, _) => {
3881 upstream_final.clone()
3882 }
3883 (RespondAs::Anthropic, _) => {
3884 translate::responses_final_to_anthropic(&upstream_final, &requested_model)
3885 }
3886 (RespondAs::OpenaiChat, _) => {
3887 translate::responses_final_to_openai_chat(&upstream_final, &requested_model)
3888 }
3889 };
3890 let (out_ct, out_body) = if plan.client_stream {
3891 let sse = match target {
3892 RespondAs::Anthropic => translate::synth_anthropic_sse(&out),
3893 RespondAs::OpenaiChat => translate::synth_openai_chat_sse(&out),
3894 RespondAs::OpenaiResponses => translate::synth_openai_responses_sse(&out),
3895 RespondAs::Gemini => translate::synth_gemini_sse(&out),
3896 };
3897 ("text/event-stream", sse.into_bytes())
3898 } else {
3899 (
3900 "application/json",
3901 serde_json::to_vec(&out).unwrap_or_default(),
3902 )
3903 };
3904 finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
3905 return Response::builder()
3906 .status(StatusCode::OK)
3907 .header("content-type", out_ct)
3908 .header("x-alexandria-trace-id", &trace_id)
3909 .body(Body::from(out_body))
3910 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
3911 }
3912
3913 if plan.destream {
3914 let buf = match upstream_resp.bytes().await {
3915 Ok(b) => b.to_vec(),
3916 Err(e) => {
3917 let msg = format!("upstream body read failed: {e}");
3918 trace.status = Some(502);
3919 trace.error = Some(msg.clone());
3920 finalize_trace(&state, trace, &body, Some(&plan.body), None);
3921 return error_response(StatusCode::BAD_GATEWAY, &msg);
3922 }
3923 };
3924 trace.ts_response_ms = Some(now_ms());
3925 fill_usage_and_cost(&state, &mut trace, &buf, is_sse);
3926 let text = String::from_utf8_lossy(&buf).to_string();
3927 let (out_status, out_body) = if status.is_success() {
3928 match extract_final_response(&text) {
3929 Some(v) => (StatusCode::OK, serde_json::to_vec(&v).unwrap_or_default()),
3930 None => {
3931 let msg = "upstream stream ended without a response.completed event";
3932 trace.error = Some(msg.to_string());
3933 (
3934 StatusCode::BAD_GATEWAY,
3935 serde_json::to_vec(
3936 &json!({"error": {"type": "alexandria", "message": msg}}),
3937 )
3938 .unwrap_or_default(),
3939 )
3940 }
3941 }
3942 } else {
3943 (status, buf.clone())
3944 };
3945 finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
3946 return Response::builder()
3947 .status(out_status)
3948 .header("content-type", "application/json")
3949 .header("x-alexandria-trace-id", &trace_id)
3950 .body(Body::from(out_body))
3951 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
3952 }
3953
3954 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, std::io::Error>>(64);
3955 let mut upstream_stream = upstream_resp.bytes_stream();
3956 let state2 = state.clone();
3957 let client_body = body.clone();
3958 let upstream_body = plan.body.clone();
3959 let dario_guard = plan.dario_guard.take();
3960 tokio::spawn(async move {
3961 let _dario_guard = dario_guard;
3962 let _in_flight = in_flight;
3963 let mut buf: Vec<u8> = Vec::new();
3964 let mut stream_error: Option<String> = None;
3965 while let Some(chunk) = upstream_stream.next().await {
3966 match chunk {
3967 Ok(b) => {
3968 buf.extend_from_slice(&b);
3969 let _ = tx.send(Ok(b)).await;
3970 }
3971 Err(e) => {
3972 stream_error = Some(format!("upstream stream error: {e}"));
3973 let _ = tx
3974 .send(Err(std::io::Error::new(
3975 std::io::ErrorKind::Other,
3976 e.to_string(),
3977 )))
3978 .await;
3979 break;
3980 }
3981 }
3982 }
3983 drop(tx);
3984 trace.ts_response_ms = Some(now_ms());
3985 if trace.error.is_none() {
3986 trace.error = stream_error;
3987 }
3988 if trace.error.is_some() {
3989 if let (Some(dario), Some(gen)) = (
3990 &state2.dario,
3991 trace
3992 .account_id
3993 .as_deref()
3994 .and_then(|id| id.strip_prefix("dario:")),
3995 ) {
3996 dario.suspect(gen);
3997 }
3998 }
3999
4000 fill_usage_and_cost(&state2, &mut trace, &buf, is_sse);
4001 finalize_trace(
4002 &state2,
4003 trace,
4004 &client_body,
4005 Some(&upstream_body),
4006 Some(&buf),
4007 );
4008 });
4009
4010 let mut response = Response::builder().status(status);
4011 for (k, v) in resp_headers.iter() {
4012 let key = k.as_str().to_lowercase();
4013 if [
4014 "transfer-encoding",
4015 "connection",
4016 "content-encoding",
4017 "content-length",
4018 ]
4019 .contains(&key.as_str())
4020 {
4021 continue;
4022 }
4023 response = response.header(k, v);
4024 }
4025 response = response.header("x-alexandria-trace-id", &trace_id);
4026 response
4027 .body(Body::from_stream(ReceiverStream::new(rx)))
4028 .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))
4029}
4030
4031fn fill_usage_and_cost(state: &AppState, trace: &mut TraceRecord, buf: &[u8], is_sse: bool) {
4032 let text = String::from_utf8_lossy(buf);
4033 let trimmed = text.trim_start();
4034 let looks_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
4035 trace.usage = if is_sse || looks_sse {
4036 parse_sse_usage(&text)
4037 } else {
4038 serde_json::from_str::<Value>(&text)
4039 .map(|v| usage_from_json(&v))
4040 .unwrap_or_default()
4041 };
4042 if !trace.usage.is_empty() {
4043 if let Some(pricing) = trace
4044 .routed_model
4045 .as_deref()
4046 .and_then(|m| state.store.pricing_for(m))
4047 {
4048 let input_includes_cached = trace
4049 .upstream_format
4050 .as_deref()
4051 .map(|f| f.starts_with("openai"))
4052 .unwrap_or(false);
4053 trace.cost_usd = Some(compute_cost(&trace.usage, &pricing, input_includes_cached));
4054 }
4055 }
4056}
4057
4058fn extract_final_response(text: &str) -> Option<Value> {
4059 let mut last: Option<Value> = None;
4060 for line in text.lines() {
4061 let Some(data) = line.strip_prefix("data:") else {
4062 continue;
4063 };
4064 let Ok(v) = serde_json::from_str::<Value>(data.trim()) else {
4065 continue;
4066 };
4067 if matches!(
4068 v["type"].as_str(),
4069 Some("response.completed") | Some("response.incomplete") | Some("response.failed")
4070 ) && v["response"].is_object()
4071 {
4072 last = Some(v["response"].clone());
4073 }
4074 }
4075 last
4076}
4077
4078fn finalize_trace(
4079 state: &AppState,
4080 mut trace: TraceRecord,
4081 client_body: &[u8],
4082 upstream_body: Option<&[u8]>,
4083 resp_body: Option<&[u8]>,
4084) {
4085 let store = &state.store;
4086 match store.write_body(&trace.id, "request.json", client_body) {
4087 Ok(p) => trace.req_body_path = Some(p),
4088 Err(e) => tracing::warn!("failed to write request body: {e}"),
4089 }
4090 if let Some(up) = upstream_body {
4091 if up != client_body {
4092 match store.write_body(&trace.id, "upstream-request.json", up) {
4093 Ok(p) => trace.upstream_req_body_path = Some(p),
4094 Err(e) => tracing::warn!("failed to write upstream request body: {e}"),
4095 }
4096 }
4097 }
4098 if let Some(resp) = resp_body {
4099 match store.write_body(&trace.id, "response.body", resp) {
4100 Ok(p) => trace.resp_body_path = Some(p),
4101 Err(e) => tracing::warn!("failed to write response body: {e}"),
4102 }
4103 }
4104 if let Err(e) = store.insert_trace(&trace) {
4105 tracing::error!("failed to insert trace {}: {e}", trace.id);
4106 } else {
4107 tracing::info!(
4108 trace_id = %trace.id,
4109 status = trace.status,
4110 input = trace.usage.input_tokens,
4111 output = trace.usage.output_tokens,
4112 cost = trace.cost_usd,
4113 "trace recorded"
4114 );
4115 }
4116}
4117
4118#[cfg(test)]
4119mod tests {
4120 use super::*;
4121 use std::path::PathBuf;
4122
4123 fn tmpdir(name: &str) -> PathBuf {
4124 let dir = std::env::temp_dir().join(format!(
4125 "alex-proxy-{name}-{}-{}",
4126 std::process::id(),
4127 std::time::SystemTime::now()
4128 .duration_since(std::time::UNIX_EPOCH)
4129 .unwrap()
4130 .as_nanos()
4131 ));
4132 std::fs::create_dir_all(&dir).unwrap();
4133 dir
4134 }
4135
4136 fn test_state(name: &str) -> Arc<AppState> {
4137 test_state_with_dario(name, None)
4138 }
4139
4140 fn test_state_with_dario(
4141 name: &str,
4142 dario: Option<Arc<dyn DarioRouter>>,
4143 ) -> Arc<AppState> {
4144 let dir = tmpdir(name);
4145 let store = Arc::new(Store::open(dir.join("store")).unwrap());
4146 let vault = Arc::new(Vault::open(dir.join("vault")).unwrap());
4147 build_state(
4148 "alx-local".into(),
4149 vault,
4150 store,
4151 dario,
4152 "http://127.0.0.1:4100".into(),
4153 )
4154 }
4155
4156 fn anthropic_account() -> Account {
4157 Account {
4158 id: "anthropic:test".into(),
4159 provider: Provider::Anthropic,
4160 kind: "oauth".into(),
4161 name: "test".into(),
4162 description: None,
4163 paused: false,
4164 label: None,
4165 access_token: Some("direct-token".into()),
4166 refresh_token: None,
4167 id_token: None,
4168 api_key: None,
4169 expires_at_ms: Some(now_ms() + 3_600_000),
4170 last_refresh_ms: None,
4171 account_meta: Value::Null,
4172 cooldown_until_ms: None,
4173 status: "active".into(),
4174 path: None,
4175 }
4176 }
4177
4178 struct FakeDario {
4179 active: Option<DarioActive>,
4180 begin_succeeds: bool,
4181 }
4182
4183 impl DarioRouter for FakeDario {
4184 fn active(&self) -> Option<DarioActive> {
4185 self.active.clone()
4186 }
4187
4188 fn begin(&self, _generation_id: &str) -> Option<Box<dyn std::any::Any + Send>> {
4189 self.begin_succeeds
4190 .then(|| Box::new(()) as Box<dyn std::any::Any + Send>)
4191 }
4192
4193 fn status(&self) -> Value {
4194 json!({"active": self.active.is_some()})
4195 }
4196
4197 fn suspect(&self, _generation_id: &str) {}
4198 }
4199
4200 fn active_dario() -> Arc<dyn DarioRouter> {
4201 Arc::new(FakeDario {
4202 active: Some(DarioActive {
4203 generation_id: "test-generation".into(),
4204 base_url: "http://127.0.0.1:9191".into(),
4205 api_key: "dario-key".into(),
4206 }),
4207 begin_succeeds: true,
4208 })
4209 }
4210
4211 fn claude_code_request() -> (HeaderMap, Value) {
4212 let mut headers = HeaderMap::new();
4213 headers.insert("user-agent", HeaderValue::from_static("claude-cli/2.1.0"));
4214 headers.insert("x-app", HeaderValue::from_static("cli"));
4215 headers.insert(
4216 "x-claude-code-session-id",
4217 HeaderValue::from_static("session-123"),
4218 );
4219 let body = json!({
4220 "model": "claude-sonnet-4-5",
4221 "system": [{
4222 "type": "text",
4223 "text": "x-anthropic-billing-header: cc_version=2.1.0"
4224 }],
4225 "messages": []
4226 });
4227 (headers, body)
4228 }
4229
4230 #[test]
4231 fn claude_code_detection_requires_the_complete_signature() {
4232 let (headers, body) = claude_code_request();
4233 assert!(is_genuine_claude_code_request(
4234 ClientFormat::AnthropicMessages,
4235 &headers,
4236 &body
4237 ));
4238 assert!(!is_genuine_claude_code_request(
4239 ClientFormat::OpenaiChat,
4240 &headers,
4241 &body
4242 ));
4243
4244 for required in ["user-agent", "x-app", "x-claude-code-session-id"] {
4245 let mut missing = headers.clone();
4246 missing.remove(required);
4247 assert!(
4248 !is_genuine_claude_code_request(
4249 ClientFormat::AnthropicMessages,
4250 &missing,
4251 &body
4252 ),
4253 "request without {required} must not bypass Dario"
4254 );
4255 }
4256
4257 let mut missing_billing = body.clone();
4258 missing_billing["system"] = json!([]);
4259 assert!(!is_genuine_claude_code_request(
4260 ClientFormat::AnthropicMessages,
4261 &headers,
4262 &missing_billing
4263 ));
4264
4265 let mut explicit_other_harness = headers;
4266 explicit_other_harness.insert(
4267 "x-alexandria-harness",
4268 HeaderValue::from_static("pi"),
4269 );
4270 assert!(!is_genuine_claude_code_request(
4271 ClientFormat::AnthropicMessages,
4272 &explicit_other_harness,
4273 &body
4274 ));
4275
4276 let (mut conflicting_harness, body) = claude_code_request();
4277 conflicting_harness.append(
4278 "x-alexandria-harness",
4279 HeaderValue::from_static("claude-code"),
4280 );
4281 conflicting_harness.append(
4282 "x-alexandria-harness",
4283 HeaderValue::from_static("codex"),
4284 );
4285 assert!(!is_genuine_claude_code_request(
4286 ClientFormat::AnthropicMessages,
4287 &conflicting_harness,
4288 &body
4289 ));
4290
4291 let (mut malformed_harness, body) = claude_code_request();
4292 malformed_harness.insert(
4293 "x-alexandria-harness",
4294 HeaderValue::from_bytes(b"\x80").unwrap(),
4295 );
4296 assert!(!is_genuine_claude_code_request(
4297 ClientFormat::AnthropicMessages,
4298 &malformed_harness,
4299 &body
4300 ));
4301 }
4302
4303 #[tokio::test]
4304 async fn anthropic_routing_bypasses_dario_only_for_claude_code() {
4305 let state = test_state_with_dario("dario-routing", Some(active_dario()));
4306 state.vault.upsert(anthropic_account()).await.unwrap();
4307 let (claude_headers, request) = claude_code_request();
4308 let original = serde_json::to_vec(&request).unwrap();
4309 let excluded = HashSet::new();
4310
4311 let mut direct_body = request.clone();
4312 let direct = plan_upstream(
4313 &state,
4314 ClientFormat::AnthropicMessages,
4315 Provider::Anthropic,
4316 "claude-sonnet-4-5",
4317 &mut direct_body,
4318 &original,
4319 "trace-direct",
4320 &excluded,
4321 None,
4322 &claude_headers,
4323 )
4324 .await
4325 .unwrap();
4326 assert_eq!(direct.url, "https://api.anthropic.com/v1/messages");
4327 assert_eq!(direct.account.id, "anthropic:test");
4328 assert!(direct.extra_headers.is_empty());
4329
4330 let mut harness_headers = HeaderMap::new();
4331 harness_headers.insert("x-alexandria-harness", HeaderValue::from_static("pi"));
4332 let mut dario_body = request;
4333 let dario = plan_upstream(
4334 &state,
4335 ClientFormat::AnthropicMessages,
4336 Provider::Anthropic,
4337 "claude-sonnet-4-5",
4338 &mut dario_body,
4339 &original,
4340 "trace-dario",
4341 &excluded,
4342 None,
4343 &harness_headers,
4344 )
4345 .await
4346 .unwrap();
4347 assert_eq!(dario.url, "http://127.0.0.1:9191/v1/messages");
4348 assert_eq!(dario.account.kind, "dario");
4349 assert!(dario
4350 .extra_headers
4351 .contains(&("x-dario-capture-id".into(), "trace-dario".into())));
4352 }
4353
4354 #[tokio::test]
4355 async fn configured_dario_fails_closed_when_unhealthy() {
4356 let state = test_state_with_dario(
4357 "dario-unhealthy",
4358 Some(Arc::new(FakeDario {
4359 active: None,
4360 begin_succeeds: false,
4361 })),
4362 );
4363 state.vault.upsert(anthropic_account()).await.unwrap();
4364 let (_, mut request) = claude_code_request();
4365 let client_headers = HeaderMap::new();
4366 let excluded = HashSet::new();
4367 let original = serde_json::to_vec(&request).unwrap();
4368
4369 let result = plan_upstream(
4370 &state,
4371 ClientFormat::AnthropicMessages,
4372 Provider::Anthropic,
4373 "claude-sonnet-4-5",
4374 &mut request,
4375 &original,
4376 "trace-unhealthy",
4377 &excluded,
4378 None,
4379 &client_headers,
4380 )
4381 .await;
4382 match result {
4383 Err((status, message)) => {
4384 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
4385 assert!(message.contains("no healthy generation"));
4386 }
4387 Ok(_) => panic!("an unhealthy configured Dario must not fall back to direct traffic"),
4388 }
4389 }
4390
4391 #[tokio::test]
4392 async fn dario_generation_race_fails_closed() {
4393 let state = test_state_with_dario(
4394 "dario-generation-race",
4395 Some(Arc::new(FakeDario {
4396 active: Some(DarioActive {
4397 generation_id: "vanished-generation".into(),
4398 base_url: "http://127.0.0.1:9191".into(),
4399 api_key: "dario-key".into(),
4400 }),
4401 begin_succeeds: false,
4402 })),
4403 );
4404 let (_, mut request) = claude_code_request();
4405 let original = serde_json::to_vec(&request).unwrap();
4406 let client_headers = HeaderMap::new();
4407 let excluded = HashSet::new();
4408
4409 let result = plan_upstream(
4410 &state,
4411 ClientFormat::AnthropicMessages,
4412 Provider::Anthropic,
4413 "claude-sonnet-4-5",
4414 &mut request,
4415 &original,
4416 "trace-race",
4417 &excluded,
4418 None,
4419 &client_headers,
4420 )
4421 .await;
4422 match result {
4423 Err((status, message)) => {
4424 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
4425 assert!(message.contains("became unavailable"));
4426 }
4427 Ok(_) => panic!("a vanished Dario generation must not fall back to direct traffic"),
4428 }
4429 }
4430
4431 #[test]
4432 fn direct_claude_code_headers_are_allowlisted() {
4433 let (mut client_headers, _) = claude_code_request();
4434 for name in CLAUDE_CODE_PASSTHROUGH_HEADERS {
4435 client_headers.insert(*name, HeaderValue::from_static("safe-client-value"));
4436 }
4437 client_headers.insert(
4438 "authorization",
4439 HeaderValue::from_static("Bearer client-secret"),
4440 );
4441 client_headers.insert("x-api-key", HeaderValue::from_static("client-secret"));
4442 client_headers.insert("host", HeaderValue::from_static("attacker.invalid"));
4443 client_headers.insert("content-length", HeaderValue::from_static("999"));
4444 client_headers.insert("connection", HeaderValue::from_static("close"));
4445 client_headers.insert("accept-encoding", HeaderValue::from_static("br"));
4446 client_headers.insert(
4447 "x-alexandria-harness",
4448 HeaderValue::from_static("claude"),
4449 );
4450 client_headers.insert(
4451 "x-dario-capture-id",
4452 HeaderValue::from_static("spoofed"),
4453 );
4454
4455 let direct = upstream_headers(&anthropic_account(), &client_headers, true).unwrap();
4456 for name in CLAUDE_CODE_PASSTHROUGH_HEADERS {
4457 assert_eq!(direct[*name], "safe-client-value", "missing {name}");
4458 }
4459 assert_eq!(direct["authorization"], "Bearer direct-token");
4460 assert!(direct.get("x-api-key").is_none());
4461 assert!(direct.get("host").is_none());
4462 assert!(direct.get("content-length").is_none());
4463 assert!(direct.get("connection").is_none());
4464 assert_eq!(direct["accept-encoding"], "identity");
4465 assert!(direct.get("x-alexandria-harness").is_none());
4466 assert!(direct.get("x-dario-capture-id").is_none());
4467
4468 let non_claude = upstream_headers(&anthropic_account(), &client_headers, false).unwrap();
4469 assert!(non_claude.get("x-app").is_none());
4470 assert!(non_claude.get("x-claude-code-session-id").is_none());
4471 assert_eq!(non_claude["authorization"], "Bearer direct-token");
4472 }
4473
4474 async fn response_json(resp: Response) -> (StatusCode, Value) {
4475 let status = resp.status();
4476 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
4477 .await
4478 .unwrap();
4479 let value = serde_json::from_slice(&body).unwrap_or(Value::Null);
4480 (status, value)
4481 }
4482
4483 struct FakeUpdater {
4484 result: Result<Value, UpdateApplyError>,
4485 }
4486
4487 impl DaemonUpdater for FakeUpdater {
4488 fn apply(&self) -> UpdateApplyFuture {
4489 let result = match &self.result {
4490 Ok(body) => Ok(body.clone()),
4491 Err(UpdateApplyError::Conflict(body)) => {
4492 Err(UpdateApplyError::Conflict(body.clone()))
4493 }
4494 Err(UpdateApplyError::Failed(message)) => {
4495 Err(UpdateApplyError::Failed(message.clone()))
4496 }
4497 };
4498 Box::pin(async move { result })
4499 }
4500 }
4501
4502 fn test_openai_account(name: &str) -> Account {
4503 Account {
4504 id: if name == "default" {
4505 "openai-oauth".into()
4506 } else {
4507 format!("openai-oauth-{name}")
4508 },
4509 provider: Provider::Openai,
4510 kind: "oauth".into(),
4511 name: name.into(),
4512 description: None,
4513 paused: false,
4514 label: Some("codex (test)".into()),
4515 access_token: Some(format!("token-{name}")),
4516 refresh_token: Some(format!("refresh-{name}")),
4517 id_token: None,
4518 api_key: None,
4519 expires_at_ms: Some(now_ms() + 3_600_000),
4520 last_refresh_ms: Some(now_ms()),
4521 account_meta: json!({"account_id": format!("chatgpt-{name}")}),
4522 cooldown_until_ms: None,
4523 status: "active".into(),
4524 path: None,
4525 }
4526 }
4527
4528 #[test]
4529 fn codex_affinity_cache_expires_and_evicts_oldest_session() {
4530 let mut cache = CodexAffinityCache {
4531 entries: HashMap::new(),
4532 ttl_ms: 100,
4533 max_entries: 2,
4534 };
4535 cache.bind("session-a", "account-a", 0);
4536 cache.bind("session-b", "account-b", 10);
4537 assert_eq!(cache.preferred("session-a", 20).as_deref(), Some("account-a"));
4538
4539 cache.bind("session-c", "account-c", 30);
4541 assert!(cache.preferred("session-b", 30).is_none());
4542 assert_eq!(cache.preferred("session-a", 30).as_deref(), Some("account-a"));
4543 assert_eq!(cache.preferred("session-c", 30).as_deref(), Some("account-c"));
4544 assert!(cache.preferred("session-a", 131).is_none());
4545 }
4546
4547 #[test]
4548 fn captures_only_safe_codex_limit_headers() {
4549 let mut headers = reqwest::header::HeaderMap::new();
4550 headers.insert("authorization", HeaderValue::from_static("Bearer secret"));
4551 headers.insert("x-codex-plan-type", HeaderValue::from_static("plus"));
4552 headers.insert(
4553 "x-codex-primary-used-percent",
4554 HeaderValue::from_static("42"),
4555 );
4556 headers.insert(
4557 "x-codex-primary-window-minutes",
4558 HeaderValue::from_static("300"),
4559 );
4560 headers.insert(
4561 "x-codex-primary-reset-at",
4562 HeaderValue::from_static("1800000000"),
4563 );
4564 let snapshot = codex_limits_from_headers(&headers).unwrap();
4565 assert_eq!(snapshot["plan"], "plus");
4566 assert_eq!(snapshot["windows"][0]["window"], "5h");
4567 assert_eq!(snapshot["windows"][0]["used_pct"], 42.0);
4568 assert!(!snapshot.to_string().contains("secret"));
4569 }
4570
4571 #[tokio::test]
4572 async fn codex_routing_api_updates_eligibility_order_and_reserve() {
4573 let state = test_state("codex-routing");
4574 state
4575 .vault
4576 .upsert(test_openai_account("default"))
4577 .await
4578 .unwrap();
4579 state
4580 .vault
4581 .upsert(test_openai_account("work"))
4582 .await
4583 .unwrap();
4584 let (status, body) = response_json(
4585 admin_codex_routing_update(
4586 State(state.clone()),
4587 axum::Json(json!({
4588 "strategy": "priority",
4589 "reserve_pct": 15,
4590 "allow_mid_thread_failover": false,
4591 "accounts": [
4592 {"account_id": "openai-oauth-work", "eligible": true, "priority": 0, "reserve_pct": 22},
4593 {"account_id": "openai-oauth", "eligible": false, "priority": 1}
4594 ]
4595 })),
4596 )
4597 .await,
4598 )
4599 .await;
4600 assert_eq!(status, StatusCode::OK);
4601 assert_eq!(body["strategy"], "priority");
4602 assert_eq!(body["reserve_pct"], 15);
4603 assert_eq!(body["allow_mid_thread_failover"], false);
4604 assert_eq!(body["accounts"][0]["account_id"], "openai-oauth-work");
4605 assert_eq!(body["accounts"][0]["eligible"], true);
4606 assert_eq!(body["accounts"][0]["reserve_pct"], 22);
4607 assert_eq!(body["accounts"][1]["eligible"], false);
4608 assert_eq!(body["accounts"][1]["reserve_pct"], 15);
4609 assert_eq!(
4610 state
4611 .vault
4612 .policy(Provider::Openai)
4613 .account_reserve_pct["work"],
4614 22
4615 );
4616 assert_eq!(
4617 state
4618 .vault
4619 .account_for(Provider::Openai, true)
4620 .await
4621 .unwrap()
4622 .name,
4623 "work"
4624 );
4625 }
4626
4627 #[tokio::test]
4628 async fn admin_update_apply_response_shapes() {
4629 let state = test_state("admin-update-apply");
4630 set_daemon_updater(
4631 &state,
4632 Arc::new(FakeUpdater {
4633 result: Ok(json!({
4634 "applying": true,
4635 "current": "0.1.0",
4636 "latest": "0.2.0",
4637 "update_available": true,
4638 })),
4639 }),
4640 );
4641 let (status, body) = response_json(admin_update_apply(State(state.clone())).await).await;
4642 assert_eq!(status, StatusCode::ACCEPTED);
4643 assert_eq!(body["applying"], true);
4644 assert_eq!(body["latest"], "0.2.0");
4645
4646 set_daemon_updater(
4647 &state,
4648 Arc::new(FakeUpdater {
4649 result: Ok(json!({
4650 "applying": false,
4651 "current": "0.2.0",
4652 "latest": "0.2.0",
4653 "update_available": false,
4654 })),
4655 }),
4656 );
4657 let (status, body) = response_json(admin_update_apply(State(state.clone())).await).await;
4658 assert_eq!(status, StatusCode::OK);
4659 assert_eq!(body["update_available"], false);
4660
4661 set_daemon_updater(
4662 &state,
4663 Arc::new(FakeUpdater {
4664 result: Err(UpdateApplyError::Conflict(json!({
4665 "applying": false,
4666 "current": "0.1.0",
4667 "latest": "0.2.0",
4668 "update_available": true,
4669 "reason": "alex is managed by Homebrew - run `brew upgrade alex`",
4670 }))),
4671 }),
4672 );
4673 let (status, body) = response_json(admin_update_apply(State(state)).await).await;
4674 assert_eq!(status, StatusCode::CONFLICT);
4675 assert_eq!(
4676 body["reason"],
4677 "alex is managed by Homebrew - run `brew upgrade alex`"
4678 );
4679 }
4680
4681 #[tokio::test]
4682 async fn harness_run_key_mints_lists_validates_and_revokes() {
4683 let state = test_state("harness-run-key");
4684 let (_status, created) = response_json(
4685 admin_run_keys_create(
4686 State(state.clone()),
4687 Some(axum::Json(json!({
4688 "kind": "harness",
4689 "label": "pi",
4690 "ttl_seconds": 1,
4691 "tags": {"harness": "pi"},
4692 }))),
4693 )
4694 .await,
4695 )
4696 .await;
4697 assert_eq!(_status, StatusCode::CREATED);
4698 assert_eq!(created["kind"], "harness");
4699 assert_eq!(created["label"], "pi");
4700 assert_eq!(created["expires_ms"], Value::Null);
4701 let id = created["id"].as_str().unwrap().to_string();
4702 let key = created["key"].as_str().unwrap();
4703 let hash = key_hash_hex(key);
4704
4705 let entry = run_key_entry(&state, &hash).unwrap();
4706 assert_eq!(entry.expires_ms, None);
4707 assert_eq!(entry.tags_json.as_deref(), Some(r#"{"harness":"pi"}"#));
4708
4709 let (_status, listed) = response_json(
4710 admin_run_keys_list(State(state.clone()), Query(HashMap::new())).await,
4711 )
4712 .await;
4713 assert_eq!(_status, StatusCode::OK);
4714 let rows = listed["run_keys"].as_array().unwrap();
4715 assert_eq!(rows.len(), 1);
4716 assert_eq!(rows[0]["id"], id);
4717 assert_eq!(rows[0]["kind"], "harness");
4718 assert_eq!(rows[0]["label"], "pi");
4719 assert_eq!(rows[0]["expires_ms"], Value::Null);
4720
4721 let (_status, revoked) =
4722 response_json(admin_run_keys_revoke(State(state.clone()), Path(id)).await).await;
4723 assert_eq!(_status, StatusCode::OK);
4724 assert_eq!(revoked["revoked"], true);
4725 assert!(run_key_entry(&state, &hash).is_none());
4726 }
4727
4728 #[tokio::test]
4729 async fn harness_run_key_requires_label() {
4730 let state = test_state("harness-run-key-label");
4731 let (status, body) = response_json(
4732 admin_run_keys_create(
4733 State(state),
4734 Some(axum::Json(json!({
4735 "kind": "harness",
4736 "tags": {"harness": "pi"},
4737 }))),
4738 )
4739 .await,
4740 )
4741 .await;
4742 assert_eq!(status, StatusCode::BAD_REQUEST);
4743 assert!(body["error"]["message"]
4744 .as_str()
4745 .unwrap()
4746 .contains("label"));
4747 }
4748
4749 #[test]
4750 fn failover_statuses_are_limited_to_auth_rate_limit_and_server_errors() {
4751 for status in [
4752 reqwest::StatusCode::UNAUTHORIZED,
4753 reqwest::StatusCode::FORBIDDEN,
4754 reqwest::StatusCode::TOO_MANY_REQUESTS,
4755 reqwest::StatusCode::INTERNAL_SERVER_ERROR,
4756 reqwest::StatusCode::BAD_GATEWAY,
4757 reqwest::StatusCode::SERVICE_UNAVAILABLE,
4758 ] {
4759 assert!(
4760 retryable_failover_status(status),
4761 "{status} should fail over"
4762 );
4763 }
4764 for status in [
4765 reqwest::StatusCode::OK,
4766 reqwest::StatusCode::BAD_REQUEST,
4767 reqwest::StatusCode::NOT_FOUND,
4768 reqwest::StatusCode::UNPROCESSABLE_ENTITY,
4769 ] {
4770 assert!(
4771 !retryable_failover_status(status),
4772 "{status} should be returned without failover"
4773 );
4774 }
4775 }
4776
4777 #[test]
4778 fn disabled_mid_thread_failover_only_pins_existing_codex_threads() {
4779 assert!(!retry_failover_allowed(Provider::Openai, true, false));
4780 assert!(retry_failover_allowed(Provider::Openai, false, false));
4781 assert!(retry_failover_allowed(Provider::Openai, true, true));
4782 assert!(retry_failover_allowed(Provider::Anthropic, true, false));
4783 }
4784
4785 #[tokio::test]
4786 async fn codex_planning_excludes_every_account_already_attempted() {
4787 let state = test_state("codex-plan-exclusions");
4788 for (id, name) in [
4789 ("openai-oauth-a", "a"),
4790 ("openai-oauth-b", "b"),
4791 ("openai-oauth-c", "c"),
4792 ] {
4793 state
4794 .vault
4795 .upsert(Account {
4796 id: id.into(),
4797 provider: Provider::Openai,
4798 kind: "oauth".into(),
4799 name: name.into(),
4800 description: None,
4801 paused: false,
4802 label: None,
4803 access_token: Some(format!("access-{name}")),
4804 refresh_token: Some(format!("refresh-{name}")),
4805 id_token: None,
4806 api_key: None,
4807 expires_at_ms: Some(now_ms() + 3_600_000),
4808 last_refresh_ms: Some(now_ms()),
4809 account_meta: json!({"account_id": format!("chatgpt-{name}")}),
4810 cooldown_until_ms: None,
4811 status: "active".into(),
4812 path: None,
4813 })
4814 .await
4815 .unwrap();
4816 }
4817
4818 let original = json!({"model": "gpt-5.3-codex", "stream": true});
4819 let raw = serde_json::to_vec(&original).unwrap();
4820 let client_headers = HeaderMap::new();
4821 let mut excluded = HashSet::new();
4822 let mut selected = Vec::new();
4823 for _ in 0..3 {
4824 let mut body = original.clone();
4825 let plan = plan_upstream(
4826 &state,
4827 ClientFormat::OpenaiResponses,
4828 Provider::Openai,
4829 "gpt-5.3-codex",
4830 &mut body,
4831 &raw,
4832 "trace-test",
4833 &excluded,
4834 None,
4835 &client_headers,
4836 )
4837 .await
4838 .unwrap();
4839 assert!(excluded.insert(plan.account.id.clone()));
4840 selected.push(plan.account.id);
4841 }
4842
4843 assert_eq!(
4844 selected,
4845 vec!["openai-oauth-a", "openai-oauth-b", "openai-oauth-c"]
4846 );
4847 let mut body = original;
4848 assert!(plan_upstream(
4849 &state,
4850 ClientFormat::OpenaiResponses,
4851 Provider::Openai,
4852 "gpt-5.3-codex",
4853 &mut body,
4854 &raw,
4855 "trace-test",
4856 &excluded,
4857 None,
4858 &client_headers,
4859 )
4860 .await
4861 .is_err());
4862 }
4863
4864 #[tokio::test]
4865 async fn codex_sessions_stay_affined_and_failover_rebinds() {
4866 let state = test_state("codex-session-affinity");
4867 state
4868 .vault
4869 .upsert(test_openai_account("a"))
4870 .await
4871 .unwrap();
4872 state
4873 .vault
4874 .upsert(test_openai_account("b"))
4875 .await
4876 .unwrap();
4877 state
4878 .vault
4879 .set_policies(vec![(
4880 Provider::Openai,
4881 AccountPolicy {
4882 order: vec!["a".into(), "b".into()],
4883 mode: AccountPolicyMode::RoundRobin,
4884 reserve_pct: Some(0),
4885 ..AccountPolicy::default()
4886 },
4887 )])
4888 .await;
4889
4890 let request = json!({"model": "gpt-5.3-codex", "stream": true});
4891 let raw = serde_json::to_vec(&request).unwrap();
4892 let headers = HeaderMap::new();
4893 let none_excluded = HashSet::new();
4894
4895 let mut body = request.clone();
4896 let session_a_first = plan_upstream(
4897 &state,
4898 ClientFormat::OpenaiResponses,
4899 Provider::Openai,
4900 "gpt-5.3-codex",
4901 &mut body,
4902 &raw,
4903 "trace-a-1",
4904 &none_excluded,
4905 Some("session-a"),
4906 &headers,
4907 )
4908 .await
4909 .unwrap()
4910 .account
4911 .id;
4912
4913 let mut body = request.clone();
4914 let session_b_first = plan_upstream(
4915 &state,
4916 ClientFormat::OpenaiResponses,
4917 Provider::Openai,
4918 "gpt-5.3-codex",
4919 &mut body,
4920 &raw,
4921 "trace-b-1",
4922 &none_excluded,
4923 Some("session-b"),
4924 &headers,
4925 )
4926 .await
4927 .unwrap()
4928 .account
4929 .id;
4930 assert_ne!(session_a_first, session_b_first);
4931
4932 let mut body = request.clone();
4933 let session_a_again = plan_upstream(
4934 &state,
4935 ClientFormat::OpenaiResponses,
4936 Provider::Openai,
4937 "gpt-5.3-codex",
4938 &mut body,
4939 &raw,
4940 "trace-a-2",
4941 &none_excluded,
4942 Some("session-a"),
4943 &headers,
4944 )
4945 .await
4946 .unwrap()
4947 .account
4948 .id;
4949 assert_eq!(session_a_again, session_a_first);
4950
4951 let excluded = HashSet::from([session_a_first.clone()]);
4952 let mut body = request.clone();
4953 let failover = plan_upstream(
4954 &state,
4955 ClientFormat::OpenaiResponses,
4956 Provider::Openai,
4957 "gpt-5.3-codex",
4958 &mut body,
4959 &raw,
4960 "trace-a-failover",
4961 &excluded,
4962 Some("session-a"),
4963 &headers,
4964 )
4965 .await
4966 .unwrap()
4967 .account
4968 .id;
4969 assert_eq!(failover, session_b_first);
4970
4971 let mut body = request;
4972 let after_failover = plan_upstream(
4973 &state,
4974 ClientFormat::OpenaiResponses,
4975 Provider::Openai,
4976 "gpt-5.3-codex",
4977 &mut body,
4978 &raw,
4979 "trace-a-3",
4980 &none_excluded,
4981 Some("session-a"),
4982 &headers,
4983 )
4984 .await
4985 .unwrap()
4986 .account
4987 .id;
4988 assert_eq!(after_failover, failover);
4989 }
4990}