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