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