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