Skip to main content

alex_proxy/
lib.rs

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        // Sliding expiry keeps an active thread sticky without retaining
167        // abandoned session IDs forever.
168        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        // An API-key fallback is not a Codex subscription affinity. Forget a
236        // stale subscription binding so the failed account is not restored as
237        // soon as its cooldown ends.
238        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    // Control-plane + report routes: gated by the local key (sent as
317    // `x-api-key: <key>` or `Authorization: Bearer <key>`) so a LAN/0.0.0.0
318    // bind doesn't expose them. Run keys are NOT accepted here — a worker's
319    // run key must not mint/revoke run keys or read the trace store.
320    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
751/// Fetch SuperGrok weekly credits from grok.com gRPC-web billing RPC.
752/// Uses the vault's xAI OAuth access token. Degrades gracefully on any failure.
753async 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            // Serialize only the account-planning step for the same thread.
2574            // This closes the first-request race without blocking unrelated
2575            // sessions or holding a lock during the upstream model request.
2576            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                    // pivot (anthropic) → chat completions for xAI/Grok upstream
2742                    let mut converted = translate::anthropic_to_openai_chat(body_json);
2743                    converted["model"] = json!(routed_model);
2744                    // buffer full upstream body then re-synth client dialect
2745                    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            // Prefer an AI Studio API key over the OAuth/Code-Assist path.
2769            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                // AI Studio: plain request, model in the path, x-goog-api-key header.
2807                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                // Code Assist (OAuth): wrapped envelope + project.
2818                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    // Capture this before planning binds a brand-new session. The policy only
3548    // suppresses failover for a thread that arrived with an existing affinity;
3549    // an unaffined first request may still fail over and then pin its winner.
3550    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        // session-a's lookup extends its expiry, so session-b is oldest.
4540        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}