Skip to main content

shift_proxy/routes/
anthropic.rs

1//! Anthropic route handler — POST /v1/messages
2//!
3//! Intercepts Anthropic API requests, runs the SHIFT optimization pipeline
4//! on the payload (extracting and transforming images), records stats with
5//! full per-image token savings, and forwards to the real Anthropic API.
6//!
7//! The CPU-intensive optimization runs on a blocking thread to avoid
8//! starving the tokio event loop.
9
10use crate::body::extract_body;
11use crate::forward::forward_request;
12use crate::optimize::optimize_payload;
13use crate::ProxyState;
14use axum::body::Bytes;
15use axum::extract::State;
16use axum::http::{HeaderMap, StatusCode, Uri};
17use axum::response::{IntoResponse, Response};
18
19/// POST /v1/messages — optimize and forward to Anthropic.
20pub async fn anthropic_handler(
21    State(state): State<ProxyState>,
22    uri: Uri,
23    headers: HeaderMap,
24    body: Bytes,
25) -> Response {
26    let body = match extract_body(&headers, body) {
27        Ok(s) => s,
28        Err(e) => {
29            return (
30                StatusCode::BAD_REQUEST,
31                axum::Json(serde_json::json!({"error": e})),
32            )
33                .into_response();
34        }
35    };
36    let config = state.config.shift_config("anthropic");
37    let base_url = &state.config.providers.anthropic;
38    let query = uri.query().map(|q| format!("?{}", q)).unwrap_or_default();
39    let target_url = format!("{}{}{}", base_url, uri.path(), query);
40
41    // Run SHIFT optimization pipeline on a blocking thread to avoid
42    // starving the async runtime during CPU-intensive image operations.
43    let start = std::time::Instant::now();
44    let body_clone = body.clone();
45    let optimization_result =
46        tokio::task::spawn_blocking(move || optimize_payload(&body_clone, &config)).await;
47
48    let (final_body, optimized) = match optimization_result {
49        Ok(Some((transformed_json, report))) => {
50            let duration_ms = start.elapsed().as_millis() as u64;
51
52            // Record session stats (in-memory)
53            state.session.record(&report);
54
55            // Record persistent stats with FULL token savings
56            let record =
57                shift_preflight::stats::record_from_report(&report, "anthropic", duration_ms);
58            if let Err(e) = shift_preflight::stats::record_run(&record, None) {
59                tracing::warn!("failed to save stats: {}", e);
60            }
61
62            if state.config.verbose {
63                let saved = report.original_size.saturating_sub(report.transformed_size);
64                if saved > 0 {
65                    tracing::info!(
66                        "Anthropic: saved {:.1}KB ({} tokens)",
67                        saved as f64 / 1024.0,
68                        report.token_savings.anthropic_saved(),
69                    );
70                }
71            }
72
73            (transformed_json, true)
74        }
75        Ok(None) | Err(_) => (body, false),
76    };
77
78    if state.config.verbose && !optimized {
79        tracing::debug!("Anthropic: no optimization applied (passthrough)");
80    }
81
82    forward_request(
83        &state.http_client,
84        "POST",
85        &target_url,
86        &headers,
87        Some(final_body),
88    )
89    .await
90}