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