Skip to main content

alex_proxy/
lib.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use alex_auth::{now_ms, Account, Vault};
6use alex_core::{
7    compute_cost, conversation_root, parse_since, parse_sse_usage, parse_trace_tags, route_model,
8    usage_from_json, ClientFormat, Provider, TraceRecord,
9};
10use alex_store::{Store, TraceFilter};
11use axum::body::{Body, Bytes};
12use axum::extract::{ConnectInfo, Path, Query, State};
13use axum::http::{HeaderMap, HeaderValue, StatusCode};
14use axum::response::{IntoResponse, Response};
15use axum::routing::{get, post};
16use axum::Router;
17use futures_util::StreamExt;
18use serde_json::{json, Value};
19use tokio_stream::wrappers::ReceiverStream;
20
21const ANTHROPIC_BASE: &str = "https://api.anthropic.com";
22const OPENAI_BASE: &str = "https://api.openai.com";
23const CODEX_BASE: &str = "https://chatgpt.com/backend-api/codex";
24const XAI_BASE: &str = "https://cli-chat-proxy.grok.com/v1";
25const GROK_CLIENT_VERSION: &str = "0.2.77";
26const ANTHROPIC_OAUTH_BETA: &str = "oauth-2025-04-20";
27
28#[derive(Debug, Clone)]
29pub struct DarioActive {
30    pub generation_id: String,
31    pub base_url: String,
32    pub api_key: String,
33}
34
35pub trait DarioRouter: Send + Sync {
36    fn active(&self) -> Option<DarioActive>;
37    fn begin(&self, generation_id: &str) -> Option<Box<dyn std::any::Any + Send>>;
38    fn status(&self) -> Value;
39    fn suspect(&self, generation_id: &str);
40}
41
42fn suspect_dario(state: &AppState, account: &Account) {
43    if account.kind != "dario" {
44        return;
45    }
46    if let (Some(dario), Some(gen)) = (&state.dario, account.id.strip_prefix("dario:")) {
47        dario.suspect(gen);
48    }
49}
50
51pub struct AppState {
52    pub local_key: String,
53    pub vault: Arc<Vault>,
54    pub store: Arc<Store>,
55    pub http: reqwest::Client,
56    pub dario: Option<Arc<dyn DarioRouter>>,
57    pub in_flight: std::sync::atomic::AtomicI64,
58    pub started_ms: i64,
59    pub base_url: String,
60    pub anthropic_usage: std::sync::Mutex<UsageCache>,
61    pub logins: alex_auth::sessions::LoginManager,
62    pub run_keys: std::sync::RwLock<HashMap<String, CachedRunKey>>,
63}
64
65#[derive(Debug, Clone)]
66pub struct CachedRunKey {
67    pub run_id: Option<String>,
68    pub tags_json: Option<String>,
69    pub expires_ms: Option<i64>,
70}
71
72struct InFlight(Arc<AppState>);
73
74impl InFlight {
75    fn new(state: &Arc<AppState>) -> Self {
76        state
77            .in_flight
78            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
79        Self(state.clone())
80    }
81}
82
83impl Drop for InFlight {
84    fn drop(&mut self) {
85        self.0
86            .in_flight
87            .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
88    }
89}
90
91pub fn build_state(
92    local_key: String,
93    vault: Arc<Vault>,
94    store: Arc<Store>,
95    dario: Option<Arc<dyn DarioRouter>>,
96    base_url: String,
97) -> Arc<AppState> {
98    let http = reqwest::Client::builder()
99        .connect_timeout(Duration::from_secs(15))
100        .build()
101        .expect("reqwest client");
102    Arc::new(AppState {
103        local_key,
104        vault,
105        store,
106        http,
107        dario,
108        in_flight: std::sync::atomic::AtomicI64::new(0),
109        started_ms: now_ms(),
110        base_url,
111        anthropic_usage: std::sync::Mutex::new(UsageCache::default()),
112        logins: alex_auth::sessions::LoginManager::default(),
113        run_keys: std::sync::RwLock::new(HashMap::new()),
114    })
115}
116
117pub fn router(state: Arc<AppState>) -> Router {
118    Router::new()
119        .route("/health", get(health))
120        .route("/connect", get(connect_info))
121        .route("/v1/models", get(models))
122        .route("/v1/messages", post(anthropic_messages))
123        .route("/v1/chat/completions", post(openai_chat))
124        .route("/v1/responses", post(openai_responses))
125        .route("/chat/completions", post(openai_chat))
126        .route("/responses", post(openai_responses))
127        .route("/traces/search", get(traces_search))
128        .route("/traces/export.ndjson", get(traces_export))
129        .route("/traces/sessions", get(traces_sessions))
130        .route(
131            "/traces/sessions/{session_id}/transcript",
132            get(traces_session_transcript),
133        )
134        .route("/traces/{id}", get(trace_get).delete(trace_delete))
135        .route("/traces/{id}/reply.md", get(trace_reply_md))
136        .route("/traces/runs/{run_id}", get(traces_run_summary))
137        .route("/traces/runs/{run_id}/events", get(traces_run_events))
138        .route("/traces/runs/{run_id}/export.ndjson", get(traces_run_export))
139        .route("/traces/runs/{run_id}/artifacts", get(traces_run_artifacts))
140        .route(
141            "/admin/run-keys",
142            get(admin_run_keys_list).post(admin_run_keys_create),
143        )
144        .route(
145            "/admin/run-keys/{id}",
146            axum::routing::delete(admin_run_keys_revoke),
147        )
148        .route("/admin/traces", get(admin_traces))
149        .route("/admin/accounts", get(admin_accounts))
150        .route("/admin/health", get(admin_health))
151        .route("/admin/analytics", get(admin_analytics))
152        .route("/admin/limits", get(admin_limits))
153        .route("/admin/dario", get(admin_dario))
154        .route("/admin/auth/import", post(admin_auth_import))
155        .route("/admin/auth/login/start", post(admin_auth_login_start))
156        .route("/admin/auth/login/complete", post(admin_auth_login_complete))
157        .route("/admin/auth/login/{id}", get(admin_auth_login_status))
158        .layer(axum::extract::DefaultBodyLimit::max(64 * 1024 * 1024))
159        .with_state(state)
160}
161
162async fn admin_auth_import(
163    State(state): State<Arc<AppState>>,
164    body: Option<axum::Json<Value>>,
165) -> Response {
166    let source = body
167        .as_ref()
168        .and_then(|b| b.0["source"].as_str().or(b.0["provider"].as_str()))
169        .unwrap_or("all")
170        .to_string();
171    match alex_auth::import_all(&state.vault, &source).await {
172        Ok(outcomes) => {
173            let items: Vec<Value> = outcomes
174                .iter()
175                .map(|o| {
176                    json!({
177                        "source": o.source,
178                        "imported": o.imported,
179                        "note": o.note,
180                    })
181                })
182                .collect();
183            axum::Json(json!({"outcomes": items})).into_response()
184        }
185        Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
186    }
187}
188
189async fn admin_auth_login_start(
190    State(state): State<Arc<AppState>>,
191    body: axum::Json<Value>,
192) -> Response {
193    let Some(provider) = body.0["provider"].as_str() else {
194        return error_response(StatusCode::BAD_REQUEST, "missing 'provider'");
195    };
196    match state.logins.start(state.vault.clone(), provider).await {
197        Ok(snapshot) => axum::Json(snapshot).into_response(),
198        Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
199    }
200}
201
202async fn admin_auth_login_complete(
203    State(state): State<Arc<AppState>>,
204    body: axum::Json<Value>,
205) -> Response {
206    let (Some(id), Some(input)) = (body.0["login_id"].as_str(), body.0["input"].as_str()) else {
207        return error_response(StatusCode::BAD_REQUEST, "missing 'login_id' or 'input'");
208    };
209    match state.logins.complete(state.vault.clone(), id, input).await {
210        Ok(snapshot) => axum::Json(snapshot).into_response(),
211        Err(e) => error_response(StatusCode::BAD_REQUEST, &e.to_string()),
212    }
213}
214
215async fn admin_auth_login_status(
216    State(state): State<Arc<AppState>>,
217    axum::extract::Path(id): axum::extract::Path<String>,
218) -> Response {
219    match state.logins.status(&id).await {
220        Some(snapshot) => axum::Json(snapshot).into_response(),
221        None => error_response(StatusCode::NOT_FOUND, "unknown or expired login session"),
222    }
223}
224
225async fn health(State(state): State<Arc<AppState>>) -> impl IntoResponse {
226    axum::Json(json!({
227        "status": "ok",
228        "service": "alexandria",
229        "version": env!("CARGO_PKG_VERSION"),
230        "in_flight": state.in_flight.load(std::sync::atomic::Ordering::SeqCst),
231        "uptime_s": (now_ms() - state.started_ms) / 1000,
232        "dario": state.dario.is_some(),
233    }))
234}
235
236fn rewrite_host(base_url: &str, host: &str) -> String {
237    let port = base_url.rsplit(':').next().unwrap_or("4100");
238    format!("http://{host}:{port}")
239}
240
241pub fn connect_payload(base_url: &str, local_key: &str) -> (Value, String) {
242    let base = base_url.trim_end_matches('/').to_string();
243    let v1 = format!("{base}/v1");
244    let exports = format!(
245        "export ANTHROPIC_BASE_URL={base}\nexport ANTHROPIC_API_KEY={local_key}\nexport OPENAI_BASE_URL={v1}\nexport OPENAI_API_KEY={local_key}\nexport XAI_API_KEY={local_key}\nexport GROK_MODELS_BASE_URL={v1}\n"
246    );
247    let payload = json!({
248        "service": "alexandria",
249        "base_url": base,
250        "api_key": local_key,
251        "anthropic": {"base_url": base, "env": {"ANTHROPIC_BASE_URL": base, "ANTHROPIC_API_KEY": local_key}},
252        "openai": {"base_url": v1, "env": {"OPENAI_BASE_URL": v1, "OPENAI_API_KEY": local_key}},
253        "xai": {"base_url": v1, "env": {"XAI_API_KEY": local_key, "GROK_MODELS_BASE_URL": v1}},
254        "exports": exports,
255    });
256    (payload, exports)
257}
258
259async fn connect_info(
260    State(state): State<Arc<AppState>>,
261    axum::extract::ConnectInfo(peer): axum::extract::ConnectInfo<std::net::SocketAddr>,
262    Query(q): Query<HashMap<String, String>>,
263) -> Response {
264    if !peer.ip().is_loopback() {
265        return error_response(
266            StatusCode::FORBIDDEN,
267            "connection info is only served to loopback clients",
268        );
269    }
270    let base = match q.get("host").map(String::as_str) {
271        Some("docker") => rewrite_host(&state.base_url, "host.docker.internal"),
272        Some(host) if !host.is_empty() => rewrite_host(&state.base_url, host),
273        _ => state.base_url.clone(),
274    };
275    let (payload, exports) = connect_payload(&base, &state.local_key);
276    if q.get("format").map(|f| f == "env").unwrap_or(false) {
277        return Response::builder()
278            .status(StatusCode::OK)
279            .header("content-type", "text/plain")
280            .body(Body::from(exports))
281            .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
282    }
283    axum::Json(payload).into_response()
284}
285
286async fn models(State(state): State<Arc<AppState>>) -> impl IntoResponse {
287    let mut ids = state.store.pricing_models();
288    for (alias, _) in alex_core::model_aliases() {
289        ids.push((*alias).to_string());
290    }
291    for id in ids.clone() {
292        ids.push(format!("alexandria/{id}"));
293    }
294    let data: Vec<Value> = ids
295        .into_iter()
296        .map(|m| json!({"id": m, "object": "model", "owned_by": "alexandria"}))
297        .collect();
298    axum::Json(json!({"object": "list", "data": data}))
299}
300
301async fn admin_analytics(
302    State(state): State<Arc<AppState>>,
303    Query(q): Query<HashMap<String, String>>,
304) -> Response {
305    let minutes: i64 = q
306        .get("since_minutes")
307        .and_then(|s| s.parse().ok())
308        .unwrap_or(60);
309    match state.store.analytics(now_ms() - minutes * 60_000) {
310        Ok(v) => axum::Json(v).into_response(),
311        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
312    }
313}
314
315const USAGE_CACHE_TTL_MS: i64 = 300_000;
316const USAGE_BACKOFF_BASE_MS: i64 = 60_000;
317const USAGE_BACKOFF_MAX_MS: i64 = 3_600_000;
318
319#[derive(Default)]
320pub struct UsageCache {
321    fetched_at_ms: i64,
322    entry: Option<Value>,
323    cooldown_until_ms: i64,
324    failures: u32,
325}
326
327fn usage_backoff_ms(failures: u32, retry_after_ms: Option<i64>) -> i64 {
328    let exp = USAGE_BACKOFF_BASE_MS
329        .saturating_mul(1i64 << failures.saturating_sub(1).min(6))
330        .min(USAGE_BACKOFF_MAX_MS);
331    exp.max(retry_after_ms.unwrap_or(0))
332}
333
334async fn anthropic_usage_entry(state: &Arc<AppState>) -> Option<Value> {
335    let account = state.vault.account_for(Provider::Anthropic, true).await.ok()?;
336    if account.kind != "oauth" {
337        return None;
338    }
339    let token = account.access_token.as_deref()?.to_string();
340    {
341        let cache = state.anthropic_usage.lock().unwrap();
342        if cache.entry.is_some() && now_ms() < cache.fetched_at_ms + USAGE_CACHE_TTL_MS {
343            return cache.entry.clone();
344        }
345        if now_ms() < cache.cooldown_until_ms {
346            return cache.entry.clone();
347        }
348    }
349    let result = state
350        .http
351        .get(format!("{ANTHROPIC_BASE}/api/oauth/usage"))
352        .header("authorization", format!("Bearer {token}"))
353        .header("anthropic-beta", ANTHROPIC_OAUTH_BETA)
354        .header("accept", "application/json")
355        .header("user-agent", "claude-cli/2.1.202 (external, cli)")
356        .send()
357        .await;
358    match result {
359        Ok(resp) if resp.status().is_success() => {
360            let raw: Value = resp.json().await.unwrap_or(Value::Null);
361            let mut windows = Vec::new();
362            for (name, key) in [
363                ("5h", "five_hour"),
364                ("7d", "seven_day"),
365                ("7d opus", "seven_day_opus"),
366                ("7d sonnet", "seven_day_sonnet"),
367            ] {
368                let w = &raw[key];
369                if w.is_object() {
370                    windows.push(json!({
371                        "window": name,
372                        "used_pct": w["utilization"],
373                        "resets_at": w["resets_at"],
374                    }));
375                }
376            }
377            let entry = json!({
378                "provider": "anthropic",
379                "source": "oauth usage endpoint",
380                "plan": account.label,
381                "windows": windows,
382                "extra_usage": raw["extra_usage"],
383            });
384            let mut cache = state.anthropic_usage.lock().unwrap();
385            cache.fetched_at_ms = now_ms();
386            cache.cooldown_until_ms = 0;
387            cache.failures = 0;
388            cache.entry = Some(entry.clone());
389            Some(entry)
390        }
391        Ok(resp) => {
392            let retry_after = resp
393                .headers()
394                .get("retry-after")
395                .and_then(|v| v.to_str().ok())
396                .and_then(|v| v.parse::<i64>().ok())
397                .map(|s| s.clamp(30, 3600) * 1000);
398            let mut cache = state.anthropic_usage.lock().unwrap();
399            cache.failures += 1;
400            let cooldown = usage_backoff_ms(cache.failures, retry_after);
401            cache.cooldown_until_ms = now_ms() + cooldown;
402            tracing::debug!(
403                status = resp.status().as_u16(),
404                failures = cache.failures,
405                cooldown_ms = cooldown,
406                "anthropic usage endpoint unavailable; backing off"
407            );
408            cache.entry.clone()
409        }
410        Err(_) => {
411            let mut cache = state.anthropic_usage.lock().unwrap();
412            cache.failures += 1;
413            let cooldown = usage_backoff_ms(cache.failures, None);
414            cache.cooldown_until_ms = now_ms() + cooldown;
415            cache.entry.clone()
416        }
417    }
418}
419
420pub async fn limits_snapshot(state: &Arc<AppState>) -> Value {
421    let mut providers: Vec<Value> = Vec::new();
422    if let Some(entry) = anthropic_usage_entry(state).await {
423        providers.push(entry);
424    }
425    for (provider_str, ts_ms, headers_json) in
426        state.store.latest_provider_headers().unwrap_or_default()
427    {
428        if providers
429            .iter()
430            .any(|p| p["provider"].as_str() == Some(&provider_str))
431        {
432            continue;
433        }
434        let Some(provider) = Provider::from_str_loose(&provider_str) else {
435            continue;
436        };
437        let headers: Value = serde_json::from_str(&headers_json).unwrap_or(Value::Null);
438        let mut parsed = alex_core::parse_limit_headers(provider, &headers);
439        if let Some(o) = parsed.as_object_mut() {
440            o.insert("provider".into(), json!(provider_str));
441            o.insert("source".into(), json!("captured response headers"));
442            o.insert("observed_at_ms".into(), json!(ts_ms));
443            providers.push(parsed);
444        }
445    }
446    providers.sort_by_key(|p| p["provider"].as_str().unwrap_or("").to_string());
447    json!({"providers": providers})
448}
449
450async fn admin_limits(State(state): State<Arc<AppState>>) -> Response {
451    axum::Json(limits_snapshot(&state).await).into_response()
452}
453
454async fn admin_dario(State(state): State<Arc<AppState>>) -> Response {
455    match &state.dario {
456        Some(d) => axum::Json(d.status()).into_response(),
457        None => error_response(StatusCode::NOT_FOUND, "dario mode is not enabled"),
458    }
459}
460
461async fn admin_traces(
462    State(state): State<Arc<AppState>>,
463    Query(q): Query<HashMap<String, String>>,
464) -> Response {
465    let limit = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(50);
466    match state.store.list_traces(
467        limit,
468        q.get("session").map(|s| s.as_str()),
469        q.get("model").map(|s| s.as_str()),
470    ) {
471        Ok(rows) => axum::Json(json!({"traces": rows})).into_response(),
472        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
473    }
474}
475
476fn filter_from_query(q: &HashMap<String, String>) -> TraceFilter {
477    let now = now_ms();
478    TraceFilter {
479        since_ms: q.get("since").and_then(|s| parse_since(s, now)),
480        until_ms: q.get("until").and_then(|s| parse_since(s, now)),
481        run_id: q.get("run_id").cloned(),
482        session: q.get("session").cloned(),
483        model: q.get("model").cloned(),
484        provider: q.get("provider").cloned(),
485        path: q.get("path").cloned(),
486        harness: q.get("harness").cloned(),
487        status: q.get("status").and_then(|s| s.parse().ok()),
488        errors_only: q
489            .get("errors")
490            .map(|v| v == "1" || v == "true")
491            .unwrap_or(false),
492        key_fingerprint: q.get("key_fingerprint").cloned(),
493        limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(200),
494    }
495}
496
497fn wants_bodies(q: &HashMap<String, String>) -> bool {
498    q.get("bodies").map(|v| v == "1" || v == "true").unwrap_or(false)
499}
500
501fn read_gz_file(path: &str) -> Option<Vec<u8>> {
502    let file = std::fs::File::open(path).ok()?;
503    let mut decoder = flate2::read::GzDecoder::new(file);
504    let mut buf = Vec::new();
505    std::io::Read::read_to_end(&mut decoder, &mut buf).ok()?;
506    Some(buf)
507}
508
509fn inline_row_bodies(row: &mut Value) {
510    use base64::Engine;
511    for (path_key, out_key) in [
512        ("req_body_path", "req_body_b64"),
513        ("upstream_req_body_path", "upstream_req_body_b64"),
514        ("resp_body_path", "resp_body_b64"),
515    ] {
516        let Some(buf) = row[path_key].as_str().and_then(read_gz_file) else {
517            continue;
518        };
519        row[out_key] = json!(base64::engine::general_purpose::STANDARD.encode(&buf));
520    }
521}
522
523fn ndjson_response(mut rows: Vec<Value>, inline_bodies: bool) -> Response {
524    rows.sort_by_key(|r| r["ts_request_ms"].as_i64().unwrap_or(0));
525    let mut out = String::new();
526    for mut row in rows {
527        if inline_bodies {
528            inline_row_bodies(&mut row);
529        }
530        out.push_str(&serde_json::to_string(&row).unwrap_or_default());
531        out.push('\n');
532    }
533    Response::builder()
534        .status(StatusCode::OK)
535        .header("content-type", "application/x-ndjson")
536        .body(Body::from(out))
537        .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))
538}
539
540const TEXT_SCAN_CAP: usize = 300;
541
542fn trace_matches_text(row: &Value, needle: &str) -> bool {
543    for key in ["req_body_path", "resp_body_path"] {
544        if let Some(path) = row.get(key).and_then(|v| v.as_str()) {
545            if let Some(bytes) = read_gz_file(path) {
546                if String::from_utf8_lossy(&bytes)
547                    .to_lowercase()
548                    .contains(needle)
549                {
550                    return true;
551                }
552            }
553        }
554    }
555    false
556}
557
558async fn traces_search(
559    State(state): State<Arc<AppState>>,
560    Query(q): Query<HashMap<String, String>>,
561) -> Response {
562    let mut filter = filter_from_query(&q);
563    let text = q
564        .get("text")
565        .or_else(|| q.get("q"))
566        .map(|t| t.trim().to_lowercase())
567        .filter(|t| !t.is_empty());
568    if text.is_some() {
569        filter.limit = TEXT_SCAN_CAP;
570    }
571    match state.store.search_traces(&filter) {
572        Ok(rows) => match text {
573            Some(needle) => {
574                let scanned = rows.len();
575                let rows = tokio::task::spawn_blocking(move || {
576                    rows.into_iter()
577                        .filter(|r| trace_matches_text(r, &needle))
578                        .collect::<Vec<_>>()
579                })
580                .await
581                .unwrap_or_default();
582                axum::Json(json!({"traces": rows, "scanned": scanned, "scan_cap": TEXT_SCAN_CAP}))
583                    .into_response()
584            }
585            None => axum::Json(json!({"traces": rows})).into_response(),
586        },
587        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
588    }
589}
590
591async fn traces_export(
592    State(state): State<Arc<AppState>>,
593    Query(q): Query<HashMap<String, String>>,
594) -> Response {
595    match state.store.search_traces(&filter_from_query(&q)) {
596        Ok(rows) => ndjson_response(rows, wants_bodies(&q)),
597        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
598    }
599}
600
601fn truncate_chars(s: String, max: usize) -> String {
602    if s.chars().count() <= max {
603        s
604    } else {
605        s.chars().take(max).collect()
606    }
607}
608
609fn read_gz_json(path: Option<&str>) -> Option<Value> {
610    let buf = path.and_then(read_gz_file)?;
611    serde_json::from_slice(&buf).ok()
612}
613
614fn read_gz_text(path: Option<&str>) -> Option<String> {
615    let buf = path.and_then(read_gz_file)?;
616    Some(String::from_utf8_lossy(&buf).to_string())
617}
618
619async fn traces_sessions(
620    State(state): State<Arc<AppState>>,
621    Query(q): Query<HashMap<String, String>>,
622) -> Response {
623    let since = q.get("since").and_then(|s| parse_since(s, now_ms()));
624    let limit = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(0);
625    match state.store.sessions(since, limit) {
626        Ok(rows) => axum::Json(json!({"sessions": rows})).into_response(),
627        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
628    }
629}
630
631fn transcript_turn(row: &Value) -> Value {
632    use alex_core::translate;
633    let user = read_gz_json(row["req_body_path"].as_str())
634        .and_then(|req| {
635            translate::last_user_text(row["client_format"].as_str().unwrap_or(""), &req)
636        })
637        .map(|s| truncate_chars(s, 8000));
638    let assistant = read_gz_text(row["resp_body_path"].as_str())
639        .and_then(|text| {
640            let fmt = row["upstream_format"]
641                .as_str()
642                .or(row["client_format"].as_str())
643                .unwrap_or("");
644            translate::assistant_reply_text(fmt, &text)
645        })
646        .map(|s| truncate_chars(s, 8000));
647    json!({
648        "trace_id": row["id"],
649        "ts_request_ms": row["ts_request_ms"],
650        "ts_response_ms": row["ts_response_ms"],
651        "model": row["routed_model"],
652        "status": row["status"],
653        "input_tokens": row["input_tokens"],
654        "output_tokens": row["output_tokens"],
655        "cost_usd": row["cost_usd"],
656        "error": row["error"],
657        "user": user,
658        "assistant": assistant,
659    })
660}
661
662async fn traces_session_transcript(
663    State(state): State<Arc<AppState>>,
664    Path(session_id): Path<String>,
665    Query(q): Query<HashMap<String, String>>,
666) -> Response {
667    let since = q
668        .get("since_ms")
669        .and_then(|s| s.parse::<i64>().ok())
670        .or_else(|| q.get("since").and_then(|s| parse_since(s, now_ms())));
671    let limit: usize = q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(500);
672    let rows = match state.store.session_traces(&session_id, since) {
673        Ok(rows) => rows,
674        Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
675    };
676    let turns: Vec<Value> = rows.iter().take(limit).map(transcript_turn).collect();
677    axum::Json(json!({"session_id": session_id, "turns": turns})).into_response()
678}
679
680fn trace_extras(req: &Value) -> Value {
681    let system_chars = match &req["system"] {
682        Value::String(s) => Some(s.chars().count()),
683        Value::Array(parts) => Some(
684            parts
685                .iter()
686                .filter_map(|p| p["text"].as_str())
687                .map(|s| s.chars().count())
688                .sum(),
689        ),
690        _ => req["instructions"].as_str().map(|s| s.chars().count()),
691    };
692    json!({
693        "reasoning_effort": req["reasoning"]["effort"],
694        "thinking_budget": req["thinking"]["budget_tokens"],
695        "max_tokens": req["max_tokens"].as_i64().or(req["max_output_tokens"].as_i64()),
696        "temperature": req["temperature"],
697        "message_count": req["messages"].as_array().or(req["input"].as_array()).map(|a| a.len()),
698        "system_chars": system_chars,
699    })
700}
701
702async fn trace_get(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
703    match state.store.get_trace(&id) {
704        Ok(Some(row)) => {
705            let extras = read_gz_json(row["req_body_path"].as_str())
706                .map(|req| trace_extras(&req))
707                .unwrap_or_else(|| json!({}));
708            axum::Json(json!({"trace": row, "extras": extras})).into_response()
709        }
710        Ok(None) => error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'")),
711        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
712    }
713}
714
715async fn trace_reply_md(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
716    use alex_core::translate;
717    let row = match state.store.get_trace(&id) {
718        Ok(Some(row)) => row,
719        Ok(None) => {
720            return error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'"))
721        }
722        Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
723    };
724    let fmt = row["upstream_format"]
725        .as_str()
726        .or(row["client_format"].as_str())
727        .unwrap_or("");
728    let reply = read_gz_text(row["resp_body_path"].as_str())
729        .and_then(|text| translate::assistant_reply_text(fmt, &text));
730    match reply {
731        Some(md) => Response::builder()
732            .status(StatusCode::OK)
733            .header("content-type", "text/markdown; charset=utf-8")
734            .body(Body::from(md))
735            .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())),
736        None => error_response(
737            StatusCode::NOT_FOUND,
738            &format!("no assistant reply available for trace '{id}'"),
739        ),
740    }
741}
742
743async fn trace_delete(State(state): State<Arc<AppState>>, Path(id): Path<String>) -> Response {
744    match state.store.get_trace(&id) {
745        Ok(Some(_)) => {}
746        Ok(None) => {
747            return error_response(StatusCode::NOT_FOUND, &format!("unknown trace '{id}'"))
748        }
749        Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
750    }
751    match state.store.delete_trace(&id) {
752        Ok(paths) => {
753            let removed = paths
754                .iter()
755                .filter(|p| std::fs::remove_file(p).is_ok())
756                .count();
757            axum::Json(json!({"deleted": true, "files_removed": removed})).into_response()
758        }
759        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
760    }
761}
762
763async fn traces_run_summary(
764    State(state): State<Arc<AppState>>,
765    Path(run_id): Path<String>,
766) -> Response {
767    match state.store.run_summary(&run_id) {
768        Ok(v) if v["trace_count"].as_i64().unwrap_or(0) == 0 => error_response(
769            StatusCode::NOT_FOUND,
770            &format!("no traces for run '{run_id}'"),
771        ),
772        Ok(v) => axum::Json(v).into_response(),
773        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
774    }
775}
776
777async fn traces_run_events(
778    State(state): State<Arc<AppState>>,
779    Path(run_id): Path<String>,
780    Query(q): Query<HashMap<String, String>>,
781) -> Response {
782    let filter = TraceFilter {
783        run_id: Some(run_id),
784        limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(1000),
785        ..Default::default()
786    };
787    match state.store.search_traces(&filter) {
788        Ok(rows) => axum::Json(json!({"traces": rows})).into_response(),
789        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
790    }
791}
792
793async fn traces_run_export(
794    State(state): State<Arc<AppState>>,
795    Path(run_id): Path<String>,
796    Query(q): Query<HashMap<String, String>>,
797) -> Response {
798    let filter = TraceFilter {
799        run_id: Some(run_id),
800        limit: q.get("limit").and_then(|s| s.parse().ok()).unwrap_or(5000),
801        ..Default::default()
802    };
803    match state.store.search_traces(&filter) {
804        Ok(rows) => ndjson_response(rows, wants_bodies(&q)),
805        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
806    }
807}
808
809async fn traces_run_artifacts(
810    State(state): State<Arc<AppState>>,
811    Path(run_id): Path<String>,
812) -> Response {
813    match state.store.run_artifacts(&run_id) {
814        Ok(artifacts) => axum::Json(json!({
815            "run_id": run_id,
816            "data_dir": state.store.data_dir.to_string_lossy(),
817            "artifacts": artifacts,
818        }))
819        .into_response(),
820        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
821    }
822}
823
824async fn admin_accounts(State(state): State<Arc<AppState>>) -> impl IntoResponse {
825    let accounts: Vec<Value> = state
826        .vault
827        .list()
828        .await
829        .into_iter()
830        .map(|a| {
831            json!({
832                "id": a.id,
833                "provider": a.provider.as_str(),
834                "kind": a.kind,
835                "label": a.label,
836                "status": a.status,
837                "expires_at_ms": a.expires_at_ms,
838                "expires_in_s": a.expires_at_ms.map(|e| (e - now_ms()) / 1000),
839            })
840        })
841        .collect();
842    axum::Json(json!({"accounts": accounts}))
843}
844
845async fn admin_health(State(state): State<Arc<AppState>>) -> Response {
846    let heartbeats = state.store.last_heartbeats().unwrap_or_default();
847    let accounts: Vec<Value> = state
848        .vault
849        .list()
850        .await
851        .into_iter()
852        .map(|a| {
853            let last = heartbeats
854                .iter()
855                .find(|h| h["provider"].as_str() == Some(a.provider.as_str()))
856                .cloned();
857            json!({
858                "id": a.id,
859                "provider": a.provider.as_str(),
860                "kind": a.kind,
861                "status": a.status,
862                "token_expires_in_s": a.expires_at_ms.map(|e| (e - now_ms()) / 1000),
863                "last_heartbeat": last,
864            })
865        })
866        .collect();
867    axum::Json(json!({"accounts": accounts})).into_response()
868}
869
870#[derive(Debug, Clone, serde::Serialize)]
871pub struct PingResult {
872    pub provider: &'static str,
873    pub account_id: Option<String>,
874    pub ok: bool,
875    pub status: Option<u16>,
876    pub latency_ms: i64,
877    pub message: String,
878}
879
880#[derive(Debug, Clone)]
881pub struct PingModels {
882    pub anthropic: String,
883    pub openai: String,
884    pub xai: String,
885}
886
887pub async fn ping_provider(
888    state: &Arc<AppState>,
889    provider: Provider,
890    models: &PingModels,
891) -> PingResult {
892    let start = now_ms();
893    let prompt = "Health check: what time is it? If you cannot know, just reply: creds ok";
894    let (format, path, body) = match provider {
895        Provider::Anthropic => (
896            ClientFormat::AnthropicMessages,
897            "/v1/messages",
898            json!({
899                "model": models.anthropic,
900                "max_tokens": 64,
901                "system": "You are Claude Code, Anthropic's official CLI for Claude.",
902                "messages": [{"role": "user", "content": prompt}],
903            }),
904        ),
905        Provider::Openai => (
906            ClientFormat::OpenaiResponses,
907            "/v1/responses",
908            json!({
909                "model": models.openai,
910                "stream": true,
911                "store": false,
912                "instructions": "You are a helpful assistant.",
913                "input": [{"type": "message", "role": "user",
914                           "content": [{"type": "input_text", "text": prompt}]}],
915            }),
916        ),
917        Provider::Xai => (
918            ClientFormat::OpenaiChat,
919            "/v1/chat/completions",
920            json!({
921                "model": models.xai,
922                "stream": false,
923                "messages": [{"role": "user", "content": prompt}],
924            }),
925        ),
926        other => {
927            return PingResult {
928                provider: other.as_str(),
929                account_id: None,
930                ok: false,
931                status: None,
932                latency_ms: 0,
933                message: "upstream not supported yet".into(),
934            }
935        }
936    };
937    let account_id = state
938        .vault
939        .account_for(provider, true)
940        .await
941        .ok()
942        .map(|a| a.id);
943    let mut headers = HeaderMap::new();
944    headers.insert(
945        "x-api-key",
946        HeaderValue::from_str(&state.local_key).expect("key header"),
947    );
948    headers.insert("user-agent", HeaderValue::from_static("alexandria-ping"));
949    let resp = proxy(
950        state.clone(),
951        format,
952        path,
953        headers,
954        Bytes::from(serde_json::to_vec(&body).expect("ping body")),
955        None,
956    )
957    .await;
958    let status = resp.status().as_u16();
959    let bytes = axum::body::to_bytes(resp.into_body(), 8 * 1024 * 1024)
960        .await
961        .unwrap_or_default();
962    let text = String::from_utf8_lossy(&bytes);
963    let message = extract_reply(&text).unwrap_or_else(|| snippet(&text));
964    PingResult {
965        provider: provider.as_str(),
966        account_id,
967        ok: (200..300).contains(&status),
968        status: Some(status),
969        latency_ms: now_ms() - start,
970        message,
971    }
972}
973
974fn extract_reply(text: &str) -> Option<String> {
975    if let Ok(v) = serde_json::from_str::<Value>(text) {
976        if let Some(t) = v["content"][0]["text"].as_str() {
977            return Some(t.to_string());
978        }
979        if let Some(t) = v["choices"][0]["message"]["content"].as_str() {
980            return Some(t.to_string());
981        }
982        if let Some(t) = v["error"]["message"].as_str() {
983            return Some(t.to_string());
984        }
985        if let Some(t) = v["detail"].as_str() {
986            return Some(t.to_string());
987        }
988    }
989    for line in text.lines() {
990        let Some(data) = line.strip_prefix("data:") else {
991            continue;
992        };
993        let Ok(v) = serde_json::from_str::<Value>(data.trim()) else {
994            continue;
995        };
996        if v["type"].as_str() == Some("response.output_text.done") {
997            if let Some(t) = v["text"].as_str() {
998                return Some(t.to_string());
999            }
1000        }
1001    }
1002    None
1003}
1004
1005fn snippet(text: &str) -> String {
1006    let t: String = text.chars().take(200).collect();
1007    t.replace('\n', " ")
1008}
1009
1010pub async fn heartbeat_once(state: &Arc<AppState>, models: &PingModels) -> Vec<PingResult> {
1011    let providers: Vec<Provider> = {
1012        let mut seen = Vec::new();
1013        for a in state.vault.list().await {
1014            if a.status == "active"
1015                && matches!(
1016                    a.provider,
1017                    Provider::Anthropic | Provider::Openai | Provider::Xai
1018                )
1019                && !seen.contains(&a.provider)
1020            {
1021                seen.push(a.provider);
1022            }
1023        }
1024        seen
1025    };
1026    let mut results = Vec::new();
1027    for provider in providers {
1028        let r = ping_provider(state, provider, models).await;
1029        if let Err(e) = state.store.insert_heartbeat(
1030            now_ms(),
1031            r.provider,
1032            r.account_id.as_deref(),
1033            r.ok,
1034            r.status.map(|s| s as i64),
1035            r.latency_ms,
1036            &r.message,
1037        ) {
1038            tracing::warn!("failed to record heartbeat: {e}");
1039        }
1040        tracing::info!(
1041            provider = r.provider,
1042            ok = r.ok,
1043            status = r.status,
1044            latency_ms = r.latency_ms,
1045            reply = %r.message,
1046            "heartbeat"
1047        );
1048        results.push(r);
1049    }
1050    results
1051}
1052
1053async fn anthropic_messages(
1054    State(state): State<Arc<AppState>>,
1055    ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
1056    headers: HeaderMap,
1057    body: Bytes,
1058) -> Response {
1059    proxy(
1060        state,
1061        ClientFormat::AnthropicMessages,
1062        "/v1/messages",
1063        headers,
1064        body,
1065        Some(peer),
1066    )
1067    .await
1068}
1069
1070async fn openai_chat(
1071    State(state): State<Arc<AppState>>,
1072    ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
1073    headers: HeaderMap,
1074    body: Bytes,
1075) -> Response {
1076    proxy(
1077        state,
1078        ClientFormat::OpenaiChat,
1079        "/v1/chat/completions",
1080        headers,
1081        body,
1082        Some(peer),
1083    )
1084    .await
1085}
1086
1087async fn openai_responses(
1088    State(state): State<Arc<AppState>>,
1089    ConnectInfo(peer): ConnectInfo<std::net::SocketAddr>,
1090    headers: HeaderMap,
1091    body: Bytes,
1092) -> Response {
1093    proxy(
1094        state,
1095        ClientFormat::OpenaiResponses,
1096        "/v1/responses",
1097        headers,
1098        body,
1099        Some(peer),
1100    )
1101    .await
1102}
1103
1104fn error_response(status: StatusCode, message: &str) -> Response {
1105    (
1106        status,
1107        axum::Json(json!({"error": {"type": "alexandria", "message": message}})),
1108    )
1109        .into_response()
1110}
1111
1112fn client_key(headers: &HeaderMap) -> Option<String> {
1113    if let Some(v) = headers.get("x-api-key").and_then(|v| v.to_str().ok()) {
1114        return Some(v.to_string());
1115    }
1116    headers
1117        .get("authorization")
1118        .and_then(|v| v.to_str().ok())
1119        .and_then(|v| v.strip_prefix("Bearer "))
1120        .map(String::from)
1121}
1122
1123fn redacted_headers(headers: &HeaderMap) -> String {
1124    let map: HashMap<String, String> = headers
1125        .iter()
1126        .map(|(k, v)| {
1127            let key = k.as_str().to_lowercase();
1128            let val = if ["authorization", "x-api-key", "cookie", "chatgpt-account-id"]
1129                .contains(&key.as_str())
1130            {
1131                "<redacted>".to_string()
1132            } else {
1133                v.to_str().unwrap_or("<binary>").to_string()
1134            };
1135            (key, val)
1136        })
1137        .collect();
1138    serde_json::to_string(&map).unwrap_or_default()
1139}
1140
1141#[derive(Debug, Clone, Copy, PartialEq)]
1142enum RespondAs {
1143    Anthropic,
1144    OpenaiChat,
1145    OpenaiResponses,
1146}
1147
1148struct UpstreamPlan {
1149    url: String,
1150    account: Account,
1151    body: Vec<u8>,
1152    upstream_format: &'static str,
1153    destream: bool,
1154    respond_as: Option<RespondAs>,
1155    client_stream: bool,
1156    extra_headers: Vec<(String, String)>,
1157    dario_guard: Option<Box<dyn std::any::Any + Send>>,
1158}
1159
1160fn dario_account(active: &DarioActive) -> Account {
1161    Account {
1162        id: format!("dario:{}", active.generation_id),
1163        provider: Provider::Anthropic,
1164        kind: "dario".into(),
1165        label: Some("dario generational proxy".into()),
1166        access_token: None,
1167        refresh_token: None,
1168        id_token: None,
1169        api_key: Some(active.api_key.clone()),
1170        expires_at_ms: None,
1171        last_refresh_ms: None,
1172        account_meta: Value::Null,
1173        cooldown_until_ms: None,
1174        status: "active".into(),
1175    }
1176}
1177
1178async fn plan_upstream(
1179    state: &AppState,
1180    format: ClientFormat,
1181    provider: Provider,
1182    routed_model: &str,
1183    body_json: &mut Value,
1184    original_body: &[u8],
1185    trace_id: &str,
1186) -> Result<UpstreamPlan, (StatusCode, String)> {
1187    use alex_core::translate;
1188    let client_stream = body_json["stream"].as_bool().unwrap_or(false);
1189    match provider {
1190        Provider::Anthropic => {
1191            let converted = match format {
1192                ClientFormat::AnthropicMessages => None,
1193                ClientFormat::OpenaiChat => Some((
1194                    translate::openai_chat_to_anthropic(body_json),
1195                    RespondAs::OpenaiChat,
1196                )),
1197                ClientFormat::OpenaiResponses => Some((
1198                    translate::openai_responses_to_anthropic(body_json),
1199                    RespondAs::OpenaiResponses,
1200                )),
1201            };
1202            let dario_active = state.dario.as_ref().and_then(|d| d.active());
1203            let (base, account, dario_guard) = match (&state.dario, dario_active) {
1204                (Some(dario), Some(active)) => (
1205                    active.base_url.trim_end_matches('/').to_string(),
1206                    dario_account(&active),
1207                    dario.begin(&active.generation_id),
1208                ),
1209                _ => {
1210                    let account = state
1211                        .vault
1212                        .account_for(provider, true)
1213                        .await
1214                        .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
1215                    (ANTHROPIC_BASE.to_string(), account, None)
1216                }
1217            };
1218            let (body, respond_as) = match converted {
1219                None => {
1220                    body_json["model"] = json!(routed_model);
1221                    (
1222                        serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec()),
1223                        None,
1224                    )
1225                }
1226                Some((mut converted, respond_as)) => {
1227                    converted["model"] = json!(routed_model);
1228                    converted["stream"] = json!(false);
1229                    (
1230                        serde_json::to_vec(&converted).unwrap_or_else(|_| original_body.to_vec()),
1231                        Some(respond_as),
1232                    )
1233                }
1234            };
1235            Ok(UpstreamPlan {
1236                url: format!("{base}/v1/messages"),
1237                account,
1238                body,
1239                upstream_format: "anthropic",
1240                destream: false,
1241                respond_as,
1242                client_stream,
1243                extra_headers: vec![],
1244                dario_guard,
1245            })
1246        }
1247        Provider::Openai => {
1248            let prefer_oauth = format != ClientFormat::OpenaiChat;
1249            let account = state
1250                .vault
1251                .account_for(provider, prefer_oauth)
1252                .await
1253                .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
1254            let oauth = account.kind == "oauth";
1255            match format {
1256                ClientFormat::OpenaiChat if !oauth => {
1257                    body_json["model"] = json!(routed_model);
1258                    let body =
1259                        serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
1260                    Ok(UpstreamPlan {
1261                        url: format!("{OPENAI_BASE}/v1/chat/completions"),
1262                        account,
1263                        body,
1264                        upstream_format: "openai-chat",
1265                        destream: false,
1266                        respond_as: None,
1267                        client_stream,
1268                        extra_headers: vec![],
1269                        dario_guard: None,
1270                    })
1271                }
1272                ClientFormat::OpenaiChat => {
1273                    let pivot = translate::openai_chat_to_anthropic(body_json);
1274                    let mut converted = translate::anthropic_to_openai_responses(&pivot);
1275                    converted["model"] = json!(routed_model);
1276                    translate::normalize_codex_request(&mut converted);
1277                    let body = serde_json::to_vec(&converted)
1278                        .unwrap_or_else(|_| original_body.to_vec());
1279                    Ok(UpstreamPlan {
1280                        url: format!("{CODEX_BASE}/responses"),
1281                        account,
1282                        body,
1283                        upstream_format: "openai-responses",
1284                        destream: false,
1285                        respond_as: Some(RespondAs::OpenaiChat),
1286                        client_stream,
1287                        extra_headers: vec![],
1288                        dario_guard: None,
1289                    })
1290                }
1291                ClientFormat::OpenaiResponses => {
1292                    body_json["model"] = json!(routed_model);
1293                    let mut destream = false;
1294                    let url = if oauth {
1295                        if body_json["stream"].as_bool() != Some(true) {
1296                            destream = true;
1297                        }
1298                        translate::normalize_codex_request(body_json);
1299                        format!("{CODEX_BASE}/responses")
1300                    } else {
1301                        format!("{OPENAI_BASE}/v1/responses")
1302                    };
1303                    let body =
1304                        serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
1305                    Ok(UpstreamPlan {
1306                        url,
1307                        account,
1308                        body,
1309                        upstream_format: "openai-responses",
1310                        destream,
1311                        respond_as: None,
1312                        client_stream,
1313                        extra_headers: vec![],
1314                        dario_guard: None,
1315                    })
1316                }
1317                ClientFormat::AnthropicMessages => {
1318                    let mut converted = translate::anthropic_to_openai_responses(body_json);
1319                    converted["model"] = json!(routed_model);
1320                    let url = if oauth {
1321                        translate::normalize_codex_request(&mut converted);
1322                        format!("{CODEX_BASE}/responses")
1323                    } else {
1324                        converted["stream"] = json!(false);
1325                        format!("{OPENAI_BASE}/v1/responses")
1326                    };
1327                    let body = serde_json::to_vec(&converted)
1328                        .unwrap_or_else(|_| original_body.to_vec());
1329                    Ok(UpstreamPlan {
1330                        url,
1331                        account,
1332                        body,
1333                        upstream_format: "openai-responses",
1334                        destream: false,
1335                        respond_as: Some(RespondAs::Anthropic),
1336                        client_stream,
1337                        extra_headers: vec![],
1338                        dario_guard: None,
1339                    })
1340                }
1341            }
1342        }
1343        Provider::Xai => {
1344            if format != ClientFormat::OpenaiChat {
1345                return Err((
1346                    StatusCode::NOT_IMPLEMENTED,
1347                    "the xai/grok upstream speaks OpenAI chat completions; POST to /v1/chat/completions".to_string(),
1348                ));
1349            }
1350            let account = state
1351                .vault
1352                .account_for(provider, true)
1353                .await
1354                .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
1355            body_json["model"] = json!(routed_model);
1356            let body = serde_json::to_vec(body_json).unwrap_or_else(|_| original_body.to_vec());
1357            Ok(UpstreamPlan {
1358                url: format!("{XAI_BASE}/chat/completions"),
1359                account,
1360                body,
1361                upstream_format: "openai-chat",
1362                destream: false,
1363                respond_as: None,
1364                client_stream,
1365                extra_headers: vec![
1366                    ("x-grok-model-override".into(), routed_model.to_string()),
1367                    ("x-grok-conv-id".into(), trace_id.to_string()),
1368                ],
1369                dario_guard: None,
1370            })
1371        }
1372        other => Err((
1373            StatusCode::NOT_IMPLEMENTED,
1374            format!("upstream provider {} not supported yet", other.as_str()),
1375        )),
1376    }
1377}
1378
1379#[cfg(test)]
1380mod trace_api_tests {
1381    use super::{session_from_metadata, trace_extras, transcript_turn, truncate_chars};
1382    use serde_json::json;
1383
1384    #[test]
1385    fn metadata_session_variants() {
1386        let claude = json!({"metadata": {"user_id":
1387            "{\"device_id\":\"d1\",\"session_id\":\"ses_inner\"}"}});
1388        assert_eq!(session_from_metadata(&claude), Some("ses_inner".into()));
1389        let plain = json!({"metadata": {"user_id": "user-123"}});
1390        assert_eq!(session_from_metadata(&plain), Some("user-123".into()));
1391        let json_no_session = json!({"metadata": {"user_id": "{\"device_id\":\"d1\"}"}});
1392        assert_eq!(
1393            session_from_metadata(&json_no_session),
1394            Some("{\"device_id\":\"d1\"}".into())
1395        );
1396        assert_eq!(session_from_metadata(&json!({})), None);
1397    }
1398
1399    #[test]
1400    fn extras_per_format() {
1401        let anthropic = json!({
1402            "system": [{"type": "text", "text": "abcd"}],
1403            "messages": [{"role": "user", "content": "hi"}],
1404            "max_tokens": 100,
1405            "temperature": 0.5,
1406            "thinking": {"type": "enabled", "budget_tokens": 4096},
1407        });
1408        let e = trace_extras(&anthropic);
1409        assert_eq!(e["thinking_budget"], 4096);
1410        assert_eq!(e["max_tokens"], 100);
1411        assert_eq!(e["temperature"], 0.5);
1412        assert_eq!(e["message_count"], 1);
1413        assert_eq!(e["system_chars"], 4);
1414        assert_eq!(e["reasoning_effort"], serde_json::Value::Null);
1415        let responses = json!({
1416            "instructions": "abc",
1417            "input": [{"type": "message"}, {"type": "message"}],
1418            "max_output_tokens": 200,
1419            "reasoning": {"effort": "high"},
1420        });
1421        let e = trace_extras(&responses);
1422        assert_eq!(e["reasoning_effort"], "high");
1423        assert_eq!(e["max_tokens"], 200);
1424        assert_eq!(e["message_count"], 2);
1425        assert_eq!(e["system_chars"], 3);
1426        assert_eq!(e["thinking_budget"], serde_json::Value::Null);
1427    }
1428
1429    #[test]
1430    fn truncates_on_char_boundaries() {
1431        assert_eq!(truncate_chars("abc".into(), 8000), "abc");
1432        assert_eq!(truncate_chars("héllo".repeat(2000), 8000).chars().count(), 8000);
1433    }
1434
1435    #[test]
1436    fn transcript_turn_missing_bodies_are_null() {
1437        let row = json!({
1438            "id": "t1", "ts_request_ms": 1, "ts_response_ms": 2,
1439            "routed_model": "m", "status": 200,
1440            "input_tokens": 10, "output_tokens": 5, "cost_usd": 0.01, "error": null,
1441            "req_body_path": "/nonexistent/x.gz", "resp_body_path": null,
1442            "client_format": "anthropic", "upstream_format": "anthropic",
1443        });
1444        let turn = transcript_turn(&row);
1445        assert_eq!(turn["trace_id"], "t1");
1446        assert_eq!(turn["user"], serde_json::Value::Null);
1447        assert_eq!(turn["assistant"], serde_json::Value::Null);
1448        assert_eq!(turn["model"], "m");
1449    }
1450}
1451
1452#[cfg(test)]
1453mod run_key_tests {
1454    use super::{generate_run_key, key_fingerprint, key_hash_hex, merge_run_key_tags};
1455
1456    #[test]
1457    fn merge_header_tags_win_per_key() {
1458        let merged = merge_run_key_tags(
1459            Some(r#"{"task":"demo","suite":"swebench"}"#),
1460            Some(r#"{"task":"override","case":"x1"}"#),
1461        )
1462        .unwrap();
1463        let v: serde_json::Value = serde_json::from_str(&merged).unwrap();
1464        assert_eq!(v["task"], "override");
1465        assert_eq!(v["suite"], "swebench");
1466        assert_eq!(v["case"], "x1");
1467    }
1468
1469    #[test]
1470    fn merge_handles_missing_sides() {
1471        assert_eq!(merge_run_key_tags(None, None), None);
1472        assert_eq!(
1473            merge_run_key_tags(Some(r#"{"a":"1"}"#), None).unwrap(),
1474            r#"{"a":"1"}"#
1475        );
1476        assert_eq!(
1477            merge_run_key_tags(None, Some(r#"{"b":"2"}"#)).unwrap(),
1478            r#"{"b":"2"}"#
1479        );
1480        assert_eq!(merge_run_key_tags(Some("not json"), None), None);
1481    }
1482
1483    #[test]
1484    fn run_key_shape_and_fingerprint() {
1485        let key = generate_run_key();
1486        assert!(key.starts_with("alxk-"));
1487        assert_eq!(key.len(), 5 + 64);
1488        assert_ne!(key, generate_run_key());
1489        let hash = key_hash_hex(&key);
1490        assert_eq!(hash.len(), 64);
1491        assert_eq!(hash[..16], key_fingerprint(&key));
1492    }
1493}
1494
1495#[cfg(test)]
1496mod usage_tests {
1497    use super::{key_fingerprint, usage_backoff_ms};
1498
1499    #[test]
1500    fn fingerprint_is_first_8_sha256_bytes_hex() {
1501        assert_eq!(key_fingerprint("abc"), "ba7816bf8f01cfea");
1502        assert_eq!(key_fingerprint("abc").len(), 16);
1503        assert_ne!(key_fingerprint("abc"), key_fingerprint("abd"));
1504    }
1505
1506    #[test]
1507    fn backoff_grows_and_caps() {
1508        assert_eq!(usage_backoff_ms(1, None), 60_000);
1509        assert_eq!(usage_backoff_ms(2, None), 120_000);
1510        assert_eq!(usage_backoff_ms(3, None), 240_000);
1511        assert_eq!(usage_backoff_ms(10, None), 3_600_000);
1512    }
1513
1514    #[test]
1515    fn retry_after_wins_when_larger() {
1516        assert_eq!(usage_backoff_ms(1, Some(600_000)), 600_000);
1517        assert_eq!(usage_backoff_ms(5, Some(1_000)), 960_000);
1518    }
1519}
1520
1521fn upstream_headers(
1522    account: &Account,
1523    client_headers: &HeaderMap,
1524) -> Result<reqwest::header::HeaderMap, (StatusCode, String)> {
1525    let mut h = reqwest::header::HeaderMap::new();
1526    h.insert("content-type", HeaderValue::from_static("application/json"));
1527    h.insert("accept", HeaderValue::from_static("*/*"));
1528    h.insert("accept-encoding", HeaderValue::from_static("identity"));
1529    if let Some(ua) = client_headers.get("user-agent") {
1530        h.insert("user-agent", ua.clone());
1531    }
1532    match (account.provider, account.kind.as_str()) {
1533        (Provider::Anthropic, "oauth") => {
1534            let token = account.access_token.as_deref().ok_or((
1535                StatusCode::BAD_GATEWAY,
1536                "anthropic oauth account has no access token".to_string(),
1537            ))?;
1538            h.insert(
1539                "authorization",
1540                HeaderValue::from_str(&format!("Bearer {token}"))
1541                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1542            );
1543            let mut beta = ANTHROPIC_OAUTH_BETA.to_string();
1544            if let Some(client_beta) = client_headers
1545                .get("anthropic-beta")
1546                .and_then(|v| v.to_str().ok())
1547            {
1548                if !client_beta.contains(ANTHROPIC_OAUTH_BETA) {
1549                    beta = format!("{ANTHROPIC_OAUTH_BETA},{client_beta}");
1550                } else {
1551                    beta = client_beta.to_string();
1552                }
1553            }
1554            h.insert(
1555                "anthropic-beta",
1556                HeaderValue::from_str(&beta)
1557                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1558            );
1559            insert_anthropic_version(&mut h, client_headers);
1560        }
1561        (Provider::Anthropic, _) => {
1562            let key = account
1563                .api_key
1564                .as_deref()
1565                .or(account.access_token.as_deref())
1566                .ok_or((
1567                    StatusCode::BAD_GATEWAY,
1568                    "anthropic account has no api key".to_string(),
1569                ))?;
1570            h.insert(
1571                "x-api-key",
1572                HeaderValue::from_str(key).map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1573            );
1574            if let Some(client_beta) = client_headers.get("anthropic-beta") {
1575                h.insert("anthropic-beta", client_beta.clone());
1576            }
1577            insert_anthropic_version(&mut h, client_headers);
1578        }
1579        (Provider::Openai, "oauth") => {
1580            let token = account.access_token.as_deref().ok_or((
1581                StatusCode::BAD_GATEWAY,
1582                "openai oauth account has no access token".to_string(),
1583            ))?;
1584            h.insert(
1585                "authorization",
1586                HeaderValue::from_str(&format!("Bearer {token}"))
1587                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1588            );
1589            if let Some(acct) = account.chatgpt_account_id() {
1590                h.insert(
1591                    "chatgpt-account-id",
1592                    HeaderValue::from_str(&acct)
1593                        .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1594                );
1595            }
1596            h.insert(
1597                "openai-beta",
1598                HeaderValue::from_static("responses=experimental"),
1599            );
1600            h.insert("originator", HeaderValue::from_static("codex_cli_rs"));
1601            let session = client_headers
1602                .get("session_id")
1603                .and_then(|v| v.to_str().ok())
1604                .map(String::from)
1605                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
1606            h.insert(
1607                "session_id",
1608                HeaderValue::from_str(&session)
1609                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1610            );
1611        }
1612        (Provider::Openai, _) => {
1613            let key = account.api_key.as_deref().ok_or((
1614                StatusCode::BAD_GATEWAY,
1615                "openai account has no api key".to_string(),
1616            ))?;
1617            h.insert(
1618                "authorization",
1619                HeaderValue::from_str(&format!("Bearer {key}"))
1620                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1621            );
1622        }
1623        (Provider::Xai, _) => {
1624            let token = account
1625                .access_token
1626                .as_deref()
1627                .or(account.api_key.as_deref())
1628                .ok_or((
1629                    StatusCode::BAD_GATEWAY,
1630                    "xai account has no access token".to_string(),
1631                ))?;
1632            h.insert(
1633                "authorization",
1634                HeaderValue::from_str(&format!("Bearer {token}"))
1635                    .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?,
1636            );
1637            h.insert("x-xai-token-auth", HeaderValue::from_static("xai-grok-cli"));
1638            h.insert(
1639                "x-grok-client-version",
1640                HeaderValue::from_static(GROK_CLIENT_VERSION),
1641            );
1642            h.insert("user-agent", HeaderValue::from_static("xai-grok-workspace"));
1643            h.insert("accept", HeaderValue::from_static("text/event-stream"));
1644        }
1645        _ => {}
1646    }
1647    Ok(h)
1648}
1649
1650fn insert_anthropic_version(h: &mut reqwest::header::HeaderMap, client_headers: &HeaderMap) {
1651    match client_headers.get("anthropic-version") {
1652        Some(v) => {
1653            h.insert("anthropic-version", v.clone());
1654        }
1655        None => {
1656            h.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
1657        }
1658    }
1659}
1660
1661fn key_fingerprint(key: &str) -> String {
1662    use sha2::{Digest, Sha256};
1663    let digest = Sha256::digest(key.as_bytes());
1664    digest[..8].iter().map(|b| format!("{b:02x}")).collect()
1665}
1666
1667const RUN_KEY_PREFIX: &str = "alxk-";
1668const RUN_KEY_DEFAULT_TTL_S: i64 = 86_400;
1669const RUN_KEY_MAX_TTL_S: i64 = 604_800;
1670
1671pub fn generate_run_key() -> String {
1672    use rand::RngCore;
1673    let mut bytes = [0u8; 32];
1674    rand::thread_rng().fill_bytes(&mut bytes);
1675    format!(
1676        "{RUN_KEY_PREFIX}{}",
1677        bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()
1678    )
1679}
1680
1681fn key_hash_hex(key: &str) -> String {
1682    use sha2::{Digest, Sha256};
1683    let digest = Sha256::digest(key.as_bytes());
1684    digest.iter().map(|b| format!("{b:02x}")).collect()
1685}
1686
1687fn merge_run_key_tags(key_tags: Option<&str>, header_tags: Option<&str>) -> Option<String> {
1688    let parse = |s: Option<&str>| {
1689        s.and_then(|s| serde_json::from_str::<Value>(s).ok())
1690            .and_then(|v| v.as_object().cloned())
1691            .unwrap_or_default()
1692    };
1693    let mut merged = parse(key_tags);
1694    merged.extend(parse(header_tags));
1695    if merged.is_empty() {
1696        None
1697    } else {
1698        serde_json::to_string(&Value::Object(merged)).ok()
1699    }
1700}
1701
1702fn run_key_entry(state: &AppState, key_hash: &str) -> Option<CachedRunKey> {
1703    let now = now_ms();
1704    let cached = state.run_keys.read().unwrap().get(key_hash).cloned();
1705    if let Some(entry) = cached {
1706        if entry.expires_ms.map(|e| e > now).unwrap_or(true) {
1707            return Some(entry);
1708        }
1709        state.run_keys.write().unwrap().remove(key_hash);
1710        return None;
1711    }
1712    let row = state.store.lookup_run_key(key_hash, now).ok().flatten()?;
1713    let entry = CachedRunKey {
1714        run_id: row["run_id"].as_str().map(String::from),
1715        tags_json: row["tags"]
1716            .as_object()
1717            .filter(|o| !o.is_empty())
1718            .and_then(|o| serde_json::to_string(o).ok()),
1719        expires_ms: row["expires_ms"].as_i64(),
1720    };
1721    state
1722        .run_keys
1723        .write()
1724        .unwrap()
1725        .insert(key_hash.to_string(), entry.clone());
1726    Some(entry)
1727}
1728
1729async fn admin_run_keys_create(
1730    State(state): State<Arc<AppState>>,
1731    body: Option<axum::Json<Value>>,
1732) -> Response {
1733    let body = body.map(|b| b.0).unwrap_or_else(|| json!({}));
1734    let tags = match &body["tags"] {
1735        Value::Null => None,
1736        Value::Object(o) => Some(o.clone()),
1737        _ => return error_response(StatusCode::BAD_REQUEST, "'tags' must be an object"),
1738    };
1739    let ttl_seconds = body["ttl_seconds"].as_i64().unwrap_or(RUN_KEY_DEFAULT_TTL_S);
1740    if ttl_seconds <= 0 {
1741        return error_response(StatusCode::BAD_REQUEST, "'ttl_seconds' must be positive");
1742    }
1743    let ttl_seconds = ttl_seconds.min(RUN_KEY_MAX_TTL_S);
1744    let run_id = body["run_id"].as_str().map(String::from);
1745    let label = body["label"].as_str().map(String::from);
1746    let key = generate_run_key();
1747    let key_hash = key_hash_hex(&key);
1748    let id = format!("rk-{}", &key_hash[..8]);
1749    let created_ms = now_ms();
1750    let expires_ms = created_ms + ttl_seconds * 1000;
1751    let tags_json = tags
1752        .as_ref()
1753        .filter(|o| !o.is_empty())
1754        .and_then(|o| serde_json::to_string(o).ok());
1755    match state.store.insert_run_key(
1756        &id,
1757        &key_hash,
1758        run_id.as_deref(),
1759        tags_json.as_deref(),
1760        label.as_deref(),
1761        created_ms,
1762        Some(expires_ms),
1763    ) {
1764        Ok(()) => (
1765            StatusCode::CREATED,
1766            axum::Json(json!({
1767                "id": id,
1768                "key": key,
1769                "run_id": run_id,
1770                "tags": tags.map(Value::Object).unwrap_or_else(|| json!({})),
1771                "expires_ms": expires_ms,
1772            })),
1773        )
1774            .into_response(),
1775        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1776    }
1777}
1778
1779async fn admin_run_keys_list(
1780    State(state): State<Arc<AppState>>,
1781    Query(q): Query<HashMap<String, String>>,
1782) -> Response {
1783    let all = q.get("all").map(|v| v == "1" || v == "true").unwrap_or(false);
1784    match state.store.list_run_keys(all) {
1785        Ok(rows) => axum::Json(json!({"run_keys": rows})).into_response(),
1786        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1787    }
1788}
1789
1790async fn admin_run_keys_revoke(
1791    State(state): State<Arc<AppState>>,
1792    Path(id): Path<String>,
1793) -> Response {
1794    match state.store.revoke_run_key(&id) {
1795        Ok(true) => {
1796            state.run_keys.write().unwrap().clear();
1797            axum::Json(json!({"revoked": true})).into_response()
1798        }
1799        Ok(false) => error_response(StatusCode::NOT_FOUND, &format!("unknown run key '{id}'")),
1800        Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
1801    }
1802}
1803
1804fn session_from_metadata(body_json: &Value) -> Option<String> {
1805    let raw = body_json["metadata"]["user_id"].as_str()?;
1806    if let Ok(v) = serde_json::from_str::<Value>(raw) {
1807        if let Some(inner) = v["session_id"].as_str() {
1808            return Some(inner.to_string());
1809        }
1810    }
1811    Some(raw.to_string())
1812}
1813
1814const METADATA_HEADERS: &[(&str, &str)] = &[
1815    ("x-alexandria-harness", "harness"),
1816    ("x-alexandria-task", "task"),
1817    ("x-alexandria-model", "model"),
1818    ("x-alexandria-job", "job"),
1819    ("x-alexandria-phase", "phase"),
1820    ("x-alexandria-kind", "kind"),
1821];
1822
1823fn trace_tags_json(headers: &HeaderMap) -> Option<String> {
1824    let values: Vec<&str> = headers
1825        .get_all("x-alexandria-trace-tag")
1826        .iter()
1827        .filter_map(|v| v.to_str().ok())
1828        .collect();
1829    let mut tags = parse_trace_tags(&values);
1830    if let Some(o) = tags.as_object_mut() {
1831        for (header, key) in METADATA_HEADERS {
1832            if o.contains_key(*key) {
1833                continue;
1834            }
1835            if let Some(v) = headers.get(*header).and_then(|v| v.to_str().ok()) {
1836                let v = v.trim();
1837                if !v.is_empty() {
1838                    o.insert((*key).to_string(), json!(v));
1839                }
1840            }
1841        }
1842    }
1843    if tags.as_object().map(|o| o.is_empty()).unwrap_or(true) {
1844        return None;
1845    }
1846    serde_json::to_string(&tags).ok()
1847}
1848
1849async fn proxy(
1850    state: Arc<AppState>,
1851    format: ClientFormat,
1852    path: &'static str,
1853    headers: HeaderMap,
1854    body: Bytes,
1855    peer: Option<std::net::SocketAddr>,
1856) -> Response {
1857    let mut run_key: Option<CachedRunKey> = None;
1858    let client_fingerprint = match client_key(&headers) {
1859        Some(k) if k == state.local_key => key_fingerprint(&k),
1860        Some(k) => {
1861            let key_hash = key_hash_hex(&k);
1862            match run_key_entry(&state, &key_hash) {
1863                Some(entry) => {
1864                    if let Err(e) = state.store.touch_run_key(&key_hash, now_ms()) {
1865                        tracing::warn!("failed to touch run key: {e}");
1866                    }
1867                    run_key = Some(entry);
1868                    key_hash.chars().take(16).collect()
1869                }
1870                None if k.starts_with(RUN_KEY_PREFIX) => {
1871                    return error_response(
1872                        StatusCode::UNAUTHORIZED,
1873                        "run key expired or revoked",
1874                    )
1875                }
1876                None => {
1877                    return error_response(
1878                        StatusCode::UNAUTHORIZED,
1879                        "bad or missing local key (x-api-key / Authorization: Bearer)",
1880                    )
1881                }
1882            }
1883        }
1884        None => {
1885            return error_response(
1886                StatusCode::UNAUTHORIZED,
1887                "bad or missing local key (x-api-key / Authorization: Bearer)",
1888            )
1889        }
1890    };
1891
1892    let in_flight = InFlight::new(&state);
1893    let trace_id = uuid::Uuid::new_v4().to_string();
1894    let mut trace = TraceRecord {
1895        id: trace_id.clone(),
1896        ts_request_ms: now_ms(),
1897        method: Some("POST".into()),
1898        path: Some(path.into()),
1899        client_format: Some(format.as_str().into()),
1900        harness: headers
1901            .get("user-agent")
1902            .and_then(|v| v.to_str().ok())
1903            .map(String::from),
1904        req_headers_json: Some(redacted_headers(&headers)),
1905        run_id: headers
1906            .get("x-alexandria-run-id")
1907            .and_then(|v| v.to_str().ok())
1908            .map(String::from)
1909            .or_else(|| run_key.as_ref().and_then(|k| k.run_id.clone())),
1910        tags: merge_run_key_tags(
1911            run_key.as_ref().and_then(|k| k.tags_json.as_deref()),
1912            trace_tags_json(&headers).as_deref(),
1913        ),
1914        client_ip: peer.map(|p| p.ip().to_string()),
1915        key_fingerprint: Some(client_fingerprint),
1916        ..Default::default()
1917    };
1918
1919    let mut body_json: Value = match serde_json::from_slice(&body) {
1920        Ok(v) => v,
1921        Err(e) => {
1922            return error_response(StatusCode::BAD_REQUEST, &format!("body is not JSON: {e}"))
1923        }
1924    };
1925
1926    trace.session_id = headers
1927        .get("x-session-id")
1928        .and_then(|v| v.to_str().ok())
1929        .map(String::from)
1930        .or_else(|| {
1931            headers
1932                .get("session_id")
1933                .and_then(|v| v.to_str().ok())
1934                .map(String::from)
1935        })
1936        .or_else(|| session_from_metadata(&body_json))
1937        .or_else(|| {
1938            conversation_root(format, &body_json).map(|root| {
1939                let ua = headers
1940                    .get("user-agent")
1941                    .and_then(|v| v.to_str().ok())
1942                    .unwrap_or("");
1943                let ip = peer.map(|p| p.ip().to_string()).unwrap_or_default();
1944                format!("auto-{}", key_fingerprint(&format!("{root}{ua}{ip}")))
1945            })
1946        });
1947
1948    let requested_model = body_json["model"].as_str().unwrap_or("").to_string();
1949    if requested_model.is_empty() {
1950        return error_response(StatusCode::BAD_REQUEST, "missing 'model' in request body");
1951    }
1952    let (routed_provider, routed_model) = route_model(&requested_model);
1953    let provider = routed_provider.unwrap_or_else(|| format.default_provider());
1954    trace.requested_model = Some(requested_model.clone());
1955    trace.routed_model = Some(routed_model.clone());
1956    trace.upstream_provider = Some(provider.as_str().into());
1957    trace.streamed = Some(body_json["stream"].as_bool().unwrap_or(false));
1958
1959    let mut plan = match plan_upstream(
1960        &state,
1961        format,
1962        provider,
1963        &routed_model,
1964        &mut body_json,
1965        &body,
1966        &trace_id,
1967    )
1968    .await
1969    {
1970        Ok(p) => p,
1971        Err((status, msg)) => {
1972            trace.status = Some(status.as_u16() as i64);
1973            trace.error = Some(msg.clone());
1974            finalize_trace(&state, trace, &body, None, None);
1975            return error_response(status, &msg);
1976        }
1977    };
1978    trace.upstream_format = Some(plan.upstream_format.into());
1979    trace.account_id = Some(plan.account.id.clone());
1980    trace.billing_bucket = Some(
1981        if plan.account.kind == "oauth" || plan.account.kind == "dario" {
1982            "subscription"
1983        } else {
1984            "api"
1985        }
1986        .into(),
1987    );
1988
1989    tracing::info!(
1990        trace_id,
1991        model = %routed_model,
1992        provider = provider.as_str(),
1993        account = %plan.account.id,
1994        url = %plan.url,
1995        "proxying request"
1996    );
1997
1998    let mut account = plan.account.clone();
1999    let mut upstream_resp = None;
2000    for attempt in 0..2 {
2001        let mut up_headers = match upstream_headers(&account, &headers) {
2002            Ok(h) => h,
2003            Err((status, msg)) => {
2004                trace.status = Some(status.as_u16() as i64);
2005                trace.error = Some(msg.clone());
2006                finalize_trace(&state, trace, &body, Some(&plan.body), None);
2007                return error_response(status, &msg);
2008            }
2009        };
2010        for (k, v) in &plan.extra_headers {
2011            if let (Ok(name), Ok(value)) = (
2012                reqwest::header::HeaderName::from_bytes(k.as_bytes()),
2013                HeaderValue::from_str(v),
2014            ) {
2015                up_headers.insert(name, value);
2016            }
2017        }
2018        let resp = match state
2019            .http
2020            .post(&plan.url)
2021            .headers(up_headers)
2022            .body(plan.body.clone())
2023            .send()
2024            .await
2025        {
2026            Ok(r) => r,
2027            Err(e) => {
2028                let msg = format!("upstream request failed: {e}");
2029                suspect_dario(&state, &account);
2030                trace.status = Some(502);
2031                trace.error = Some(msg.clone());
2032                finalize_trace(&state, trace, &body, Some(&plan.body), None);
2033                return error_response(StatusCode::BAD_GATEWAY, &msg);
2034            }
2035        };
2036        if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS && account.kind != "dario" {
2037            let retry_after_s = resp
2038                .headers()
2039                .get("retry-after")
2040                .and_then(|v| v.to_str().ok())
2041                .and_then(|v| v.parse::<i64>().ok())
2042                .unwrap_or(60)
2043                .clamp(1, 3600);
2044            tracing::warn!(
2045                account = %account.id,
2046                retry_after_s,
2047                "upstream returned 429; cooling account down"
2048            );
2049            if let Err(e) = state
2050                .vault
2051                .mark_cooldown(&account.id, now_ms() + retry_after_s * 1000)
2052                .await
2053            {
2054                tracing::warn!("failed to mark cooldown: {e}");
2055            }
2056        }
2057        if resp.status() == reqwest::StatusCode::UNAUTHORIZED
2058            && account.kind == "oauth"
2059            && attempt == 0
2060        {
2061            tracing::warn!(
2062                account = %account.id,
2063                "upstream returned 401 for oauth account; forcing token refresh and retrying"
2064            );
2065            match state.vault.refresh(&account.id, true).await {
2066                Ok(fresh) => {
2067                    account = fresh;
2068                    continue;
2069                }
2070                Err(e) => {
2071                    tracing::warn!("forced refresh failed: {e}");
2072                }
2073            }
2074        }
2075        upstream_resp = Some(resp);
2076        break;
2077    }
2078    let upstream_resp = upstream_resp.expect("upstream response after retry loop");
2079
2080    let status = upstream_resp.status();
2081    trace.status = Some(status.as_u16() as i64);
2082    let resp_headers = upstream_resp.headers().clone();
2083    let content_type = resp_headers
2084        .get("content-type")
2085        .and_then(|v| v.to_str().ok())
2086        .unwrap_or("application/octet-stream")
2087        .to_string();
2088    trace.resp_headers_json = Some(redacted_headers(&resp_headers));
2089    let is_sse = content_type.starts_with("text/event-stream");
2090
2091    if let Some(target) = plan.respond_as {
2092        use alex_core::translate;
2093        let buf = match upstream_resp.bytes().await {
2094            Ok(b) => b.to_vec(),
2095            Err(e) => {
2096                let msg = format!("upstream body read failed: {e}");
2097                suspect_dario(&state, &account);
2098                trace.status = Some(502);
2099                trace.error = Some(msg.clone());
2100                finalize_trace(&state, trace, &body, Some(&plan.body), None);
2101                return error_response(StatusCode::BAD_GATEWAY, &msg);
2102            }
2103        };
2104        drop(plan.dario_guard.take());
2105        trace.ts_response_ms = Some(now_ms());
2106        fill_usage_and_cost(&state, &mut trace, &buf, is_sse);
2107        let text = String::from_utf8_lossy(&buf).to_string();
2108        if !status.is_success() {
2109            finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
2110            return Response::builder()
2111                .status(status)
2112                .header("content-type", "application/json")
2113                .header("x-alexandria-trace-id", &trace_id)
2114                .body(Body::from(buf))
2115                .unwrap_or_else(|e| {
2116                    error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
2117                });
2118        }
2119        let trimmed = text.trim_start();
2120        let looks_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
2121        let upstream_final = if plan.upstream_format == "anthropic" {
2122            if is_sse || looks_sse {
2123                translate::parse_anthropic_sse_to_message(&text)
2124            } else {
2125                serde_json::from_str(&text).ok()
2126            }
2127        } else if is_sse || looks_sse {
2128            translate::parse_responses_sse_final(&text)
2129        } else {
2130            serde_json::from_str(&text).ok()
2131        };
2132        let Some(upstream_final) = upstream_final else {
2133            let msg = "could not reassemble upstream response for translation";
2134            trace.error = Some(msg.to_string());
2135            finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
2136            return error_response(StatusCode::BAD_GATEWAY, msg);
2137        };
2138        let out = match (target, plan.upstream_format) {
2139            (RespondAs::OpenaiChat, "anthropic") => {
2140                translate::anthropic_response_to_openai_chat(&upstream_final, &requested_model)
2141            }
2142            (RespondAs::OpenaiResponses, "anthropic") => translate::anthropic_response_to_openai_responses(
2143                &upstream_final,
2144                &requested_model,
2145            ),
2146            (RespondAs::Anthropic, "anthropic") | (RespondAs::OpenaiResponses, _) => {
2147                upstream_final.clone()
2148            }
2149            (RespondAs::Anthropic, _) => {
2150                translate::responses_final_to_anthropic(&upstream_final, &requested_model)
2151            }
2152            (RespondAs::OpenaiChat, _) => {
2153                translate::responses_final_to_openai_chat(&upstream_final, &requested_model)
2154            }
2155        };
2156        let (out_ct, out_body) = if plan.client_stream {
2157            let sse = match target {
2158                RespondAs::Anthropic => translate::synth_anthropic_sse(&out),
2159                RespondAs::OpenaiChat => translate::synth_openai_chat_sse(&out),
2160                RespondAs::OpenaiResponses => translate::synth_openai_responses_sse(&out),
2161            };
2162            ("text/event-stream", sse.into_bytes())
2163        } else {
2164            (
2165                "application/json",
2166                serde_json::to_vec(&out).unwrap_or_default(),
2167            )
2168        };
2169        finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
2170        return Response::builder()
2171            .status(StatusCode::OK)
2172            .header("content-type", out_ct)
2173            .header("x-alexandria-trace-id", &trace_id)
2174            .body(Body::from(out_body))
2175            .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
2176    }
2177
2178    if plan.destream {
2179        let buf = match upstream_resp.bytes().await {
2180            Ok(b) => b.to_vec(),
2181            Err(e) => {
2182                let msg = format!("upstream body read failed: {e}");
2183                trace.status = Some(502);
2184                trace.error = Some(msg.clone());
2185                finalize_trace(&state, trace, &body, Some(&plan.body), None);
2186                return error_response(StatusCode::BAD_GATEWAY, &msg);
2187            }
2188        };
2189        trace.ts_response_ms = Some(now_ms());
2190        fill_usage_and_cost(&state, &mut trace, &buf, is_sse);
2191        let text = String::from_utf8_lossy(&buf).to_string();
2192        let (out_status, out_body) = if status.is_success() {
2193            match extract_final_response(&text) {
2194                Some(v) => (StatusCode::OK, serde_json::to_vec(&v).unwrap_or_default()),
2195                None => {
2196                    let msg = "upstream stream ended without a response.completed event";
2197                    trace.error = Some(msg.to_string());
2198                    (
2199                        StatusCode::BAD_GATEWAY,
2200                        serde_json::to_vec(
2201                            &json!({"error": {"type": "alexandria", "message": msg}}),
2202                        )
2203                        .unwrap_or_default(),
2204                    )
2205                }
2206            }
2207        } else {
2208            (status, buf.clone())
2209        };
2210        finalize_trace(&state, trace, &body, Some(&plan.body), Some(&buf));
2211        return Response::builder()
2212            .status(out_status)
2213            .header("content-type", "application/json")
2214            .header("x-alexandria-trace-id", &trace_id)
2215            .body(Body::from(out_body))
2216            .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()));
2217    }
2218
2219    let (tx, rx) = tokio::sync::mpsc::channel::<Result<Bytes, std::io::Error>>(64);
2220    let mut upstream_stream = upstream_resp.bytes_stream();
2221    let state2 = state.clone();
2222    let client_body = body.clone();
2223    let upstream_body = plan.body.clone();
2224    let dario_guard = plan.dario_guard.take();
2225    tokio::spawn(async move {
2226        let _dario_guard = dario_guard;
2227        let _in_flight = in_flight;
2228        let mut buf: Vec<u8> = Vec::new();
2229        let mut stream_error: Option<String> = None;
2230        while let Some(chunk) = upstream_stream.next().await {
2231            match chunk {
2232                Ok(b) => {
2233                    buf.extend_from_slice(&b);
2234                    let _ = tx.send(Ok(b)).await;
2235                }
2236                Err(e) => {
2237                    stream_error = Some(format!("upstream stream error: {e}"));
2238                    let _ = tx
2239                        .send(Err(std::io::Error::new(
2240                            std::io::ErrorKind::Other,
2241                            e.to_string(),
2242                        )))
2243                        .await;
2244                    break;
2245                }
2246            }
2247        }
2248        drop(tx);
2249        trace.ts_response_ms = Some(now_ms());
2250        if trace.error.is_none() {
2251            trace.error = stream_error;
2252        }
2253        if trace.error.is_some() {
2254            if let (Some(dario), Some(gen)) = (
2255                &state2.dario,
2256                trace
2257                    .account_id
2258                    .as_deref()
2259                    .and_then(|id| id.strip_prefix("dario:")),
2260            ) {
2261                dario.suspect(gen);
2262            }
2263        }
2264
2265        fill_usage_and_cost(&state2, &mut trace, &buf, is_sse);
2266        finalize_trace(
2267            &state2,
2268            trace,
2269            &client_body,
2270            Some(&upstream_body),
2271            Some(&buf),
2272        );
2273    });
2274
2275    let mut response = Response::builder().status(status);
2276    for (k, v) in resp_headers.iter() {
2277        let key = k.as_str().to_lowercase();
2278        if [
2279            "transfer-encoding",
2280            "connection",
2281            "content-encoding",
2282            "content-length",
2283        ]
2284        .contains(&key.as_str())
2285        {
2286            continue;
2287        }
2288        response = response.header(k, v);
2289    }
2290    response = response.header("x-alexandria-trace-id", &trace_id);
2291    response
2292        .body(Body::from_stream(ReceiverStream::new(rx)))
2293        .unwrap_or_else(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))
2294}
2295
2296fn fill_usage_and_cost(state: &AppState, trace: &mut TraceRecord, buf: &[u8], is_sse: bool) {
2297    let text = String::from_utf8_lossy(buf);
2298    let trimmed = text.trim_start();
2299    let looks_sse = trimmed.starts_with("event:") || trimmed.starts_with("data:");
2300    trace.usage = if is_sse || looks_sse {
2301        parse_sse_usage(&text)
2302    } else {
2303        serde_json::from_str::<Value>(&text)
2304            .map(|v| usage_from_json(&v))
2305            .unwrap_or_default()
2306    };
2307    if !trace.usage.is_empty() {
2308        if let Some(pricing) = trace
2309            .routed_model
2310            .as_deref()
2311            .and_then(|m| state.store.pricing_for(m))
2312        {
2313            let input_includes_cached = trace
2314                .upstream_format
2315                .as_deref()
2316                .map(|f| f.starts_with("openai"))
2317                .unwrap_or(false);
2318            trace.cost_usd = Some(compute_cost(&trace.usage, &pricing, input_includes_cached));
2319        }
2320    }
2321}
2322
2323fn extract_final_response(text: &str) -> Option<Value> {
2324    let mut last: Option<Value> = None;
2325    for line in text.lines() {
2326        let Some(data) = line.strip_prefix("data:") else {
2327            continue;
2328        };
2329        let Ok(v) = serde_json::from_str::<Value>(data.trim()) else {
2330            continue;
2331        };
2332        if matches!(
2333            v["type"].as_str(),
2334            Some("response.completed") | Some("response.incomplete") | Some("response.failed")
2335        ) && v["response"].is_object()
2336        {
2337            last = Some(v["response"].clone());
2338        }
2339    }
2340    last
2341}
2342
2343fn finalize_trace(
2344    state: &AppState,
2345    mut trace: TraceRecord,
2346    client_body: &[u8],
2347    upstream_body: Option<&[u8]>,
2348    resp_body: Option<&[u8]>,
2349) {
2350    let store = &state.store;
2351    match store.write_body(&trace.id, "request.json", client_body) {
2352        Ok(p) => trace.req_body_path = Some(p),
2353        Err(e) => tracing::warn!("failed to write request body: {e}"),
2354    }
2355    if let Some(up) = upstream_body {
2356        if up != client_body {
2357            match store.write_body(&trace.id, "upstream-request.json", up) {
2358                Ok(p) => trace.upstream_req_body_path = Some(p),
2359                Err(e) => tracing::warn!("failed to write upstream request body: {e}"),
2360            }
2361        }
2362    }
2363    if let Some(resp) = resp_body {
2364        match store.write_body(&trace.id, "response.body", resp) {
2365            Ok(p) => trace.resp_body_path = Some(p),
2366            Err(e) => tracing::warn!("failed to write response body: {e}"),
2367        }
2368    }
2369    if let Err(e) = store.insert_trace(&trace) {
2370        tracing::error!("failed to insert trace {}: {e}", trace.id);
2371    } else {
2372        tracing::info!(
2373            trace_id = %trace.id,
2374            status = trace.status,
2375            input = trace.usage.input_tokens,
2376            output = trace.usage.output_tokens,
2377            cost = trace.cost_usd,
2378            "trace recorded"
2379        );
2380    }
2381}