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