1use std::{
19 collections::{HashMap, VecDeque},
20 num::NonZeroUsize,
21 sync::{
22 Arc, Mutex,
23 atomic::{AtomicU64, AtomicUsize, Ordering},
24 },
25 time::Duration,
26};
27
28use axum::{
29 body::Body,
30 extract::State,
31 http::{Method, StatusCode},
32 response::{IntoResponse, Json, Response},
33};
34use bytes::Bytes;
35use reqwest::Client as HttpClient;
36use serde::{Deserialize, Serialize};
37use headroom_core::ccr::{
38 CcrStore,
39 backends::{in_memory::InMemoryCcrStore, sqlite::SqliteCcrStore},
40 compute_key,
41};
42use tokio_util::task::TaskTracker;
43use futures::StreamExt;
44
45#[derive(Clone)]
47pub struct Secret(pub(crate) String);
48
49impl std::fmt::Debug for Secret {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 write!(f, "[REDACTED]")
52 }
53}
54
55impl std::fmt::Display for Secret {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "[REDACTED]")
58 }
59}
60
61impl Secret {
62 pub fn expose(&self) -> &str {
64 &self.0
65 }
66}
67
68impl From<&str> for Secret {
69 fn from(s: &str) -> Self {
70 Secret(s.to_string())
71 }
72}
73
74impl From<String> for Secret {
75 fn from(s: String) -> Self {
76 Secret(s)
77 }
78}
79
80use crate::config::{Cli, CompressionConfig, ProxyMode, env_parse_warn};
81
82const CACHE_COMPRESS_THRESHOLD: usize = 8192;
86const TOKEN_COMPRESS_THRESHOLD: usize = 1024;
88const INLINE_CCR_THRESHOLD: usize = 256;
92const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions";
94const RESPONSE_CACHE_MAX_BODY_BYTES: usize = 1024 * 1024;
100
101const RESPONSE_MAX_BODY_BYTES: usize = 64 * 1024 * 1024; pub struct ResolvedThresholds {
114 pub cache: usize,
115 pub token: usize,
116 pub inline: usize,
117 pub code_multiplier: f64,
118}
119
120pub fn resolve_thresholds(compression: Option<&CompressionConfig>) -> ResolvedThresholds {
131 ResolvedThresholds {
132 cache: env_parse_warn::<usize>("APHRODITE_TOOL_THRESHOLD_CACHE")
133 .or_else(|| compression.and_then(|c| c.tool_threshold_cache).map(|v| v as usize))
134 .unwrap_or(CACHE_COMPRESS_THRESHOLD),
135 token: env_parse_warn::<usize>("APHRODITE_TOOL_THRESHOLD_TOKEN")
136 .or_else(|| compression.and_then(|c| c.tool_threshold_token).map(|v| v as usize))
137 .unwrap_or(TOKEN_COMPRESS_THRESHOLD),
138 inline: env_parse_warn::<usize>("APHRODITE_INLINE_THRESHOLD")
139 .or_else(|| compression.and_then(|c| c.inline_threshold).map(|v| v as usize))
140 .unwrap_or(INLINE_CCR_THRESHOLD),
141 code_multiplier: env_parse_warn::<f64>("APHRODITE_CODE_MULTIPLIER")
142 .or_else(|| compression.and_then(|c| c.code_multiplier))
143 .unwrap_or(3.0),
144 }
145}
146
147pub(crate) async fn ccr_get(ccr: &Arc<dyn CcrStore>, hash: &str) -> Option<String> {
151 let ccr = ccr.clone();
152 let hash = hash.to_owned();
153 tokio::task::spawn_blocking(move || ccr.get(&hash)).await.unwrap_or(None)
154}
155
156async fn ccr_put(ccr: &Arc<dyn CcrStore>, hash: &str, content: &str) -> bool {
163 let ccr = ccr.clone();
164 let hash = hash.to_owned();
165 let content = content.to_owned();
166 tokio::task::spawn_blocking(move || ccr.put(&hash, &content))
167 .await
168 .unwrap_or(false)
169}
170
171async fn ccr_del(ccr: &Arc<dyn CcrStore>, hash: &str) -> bool {
173 let ccr = ccr.clone();
174 let hash = hash.to_owned();
175 tokio::task::spawn_blocking(move || ccr.del(&hash)).await.unwrap_or(false)
176}
177
178async fn ccr_len(ccr: &Arc<dyn CcrStore>) -> usize {
180 let ccr = ccr.clone();
181 tokio::task::spawn_blocking(move || ccr.len()).await.unwrap_or(0)
182}
183
184pub struct AppState {
190 pub client: HttpClient,
191 pub stream_client: HttpClient,
201 pub api_url: String,
202 pub model: String,
203 pub api_key: Secret,
204 pub ccr: Option<Arc<dyn CcrStore>>,
205 pub add_markers: bool,
206 pub mode: ProxyMode,
207 pub tool_relay: bool,
208 pub notify_url: Option<String>,
209 pub notify_key: Option<String>,
210 pub dev: bool,
212
213 pub request_history: std::sync::Mutex<VecDeque<serde_json::Value>>,
219 pub inline_ccr: std::sync::Mutex<lru::LruCache<String, String>>,
223
224 pub latency_buckets: [AtomicU64; 5],
227 pub total_latency_micros: AtomicU64,
229 pub last_errors: std::sync::Mutex<VecDeque<String>>,
234 pub compressions_by_type: std::sync::Mutex<std::collections::HashMap<String, u64>>,
237
238 pub requests_total: AtomicU64,
240 pub requests_compressed: AtomicU64,
241 pub tokens_saved: AtomicU64,
252 pub ccr_hits: AtomicU64,
253 pub ccr_misses: AtomicU64,
254 pub ccr_created: AtomicU64,
255 pub tool_relay_calls: AtomicU64,
256 pub compression_ratio_ema: AtomicU64, pub response_cache: std::sync::Mutex<lru::LruCache<u64, (std::time::Instant, Vec<u8>)>>,
265 pub response_cache_ttl: std::time::Duration,
269 pub cache_hits: AtomicU64,
270 pub cache_misses: AtomicU64,
271
272 pub task_tracker: TaskTracker,
275
276 pub fill_pct: AtomicU64,
281
282 pub inline_ccr_hits: AtomicU64,
284 pub inline_ccr_misses: AtomicU64,
285 pub tool_relay_success: AtomicU64,
286 pub tool_relay_failure: AtomicU64,
287 pub notify_success: AtomicU64,
288 pub notify_failure: AtomicU64,
289 pub upstream_errors_4xx: AtomicU64,
290 pub upstream_errors_5xx: AtomicU64,
291 pub upstream_timeouts: AtomicU64,
292 pub upstream_connect_errors: AtomicU64,
296 pub sse_stream_errors: AtomicU64,
305 pub ccr_store_entries: AtomicU64,
306 pub ccr_store_bytes: AtomicU64,
307 pub request_body_bytes: AtomicU64,
308 pub response_body_bytes: AtomicU64,
309 pub upstream_latency_micros: AtomicU64,
310
311 pub upstream_health_cache: std::sync::Mutex<Option<(bool, std::time::Instant)>>,
319
320 pub cache_compress_threshold: AtomicUsize,
329 pub token_compress_threshold: AtomicUsize,
330 pub inline_ccr_threshold: AtomicUsize,
331 pub code_multiplier_x100: AtomicU64,
334}
335
336fn estimate_compressed_size(content: &str) -> usize {
348 use std::collections::HashSet;
349
350 let bytes = content.as_bytes();
351 let sample = if bytes.len() <= 4096 { bytes } else { &bytes[..4096] };
352 if sample.len() < 3 {
353 return sample.len().max(40);
356 }
357
358 let mut trigrams: HashSet<[u8; 3]> = HashSet::with_capacity(sample.len().min(4096));
360 for window in sample.windows(3) {
361 trigrams.insert([window[0], window[1], window[2]]);
362 }
363
364 let unique = trigrams.len().max(1);
365 let total = sample.len().saturating_sub(2).max(1);
366
367 let uniqueness = unique as f64 / total as f64;
370 let compressibility = 1.0 - uniqueness;
371
372 let overhead: f64 = 40.0;
375 let raw = bytes.len() as f64 * (1.0 - compressibility * 0.97) + overhead;
376 let est = raw.min(bytes.len() as f64).max(40.0);
377 est as usize
378}
379
380impl AppState {
381 pub fn stats_json(&self) -> serde_json::Value {
382 serde_json::json!({
383 "mode": match self.mode {
384 ProxyMode::Cache => "cache",
385 ProxyMode::Token => "token",
386 },
387 "proxy": "aphrodite",
388 "ccr_backend": if self.ccr.is_some() { "enabled" } else { "none" },
389 "tool_relay_enabled": self.tool_relay,
396 "requests": {
397 "total": self.requests_total.load(Ordering::Relaxed),
398 "compressed": self.requests_compressed.load(Ordering::Relaxed),
399 },
400 "tokens_saved": self.tokens_saved.load(Ordering::Relaxed),
401 "ccr": {
402 "hits": self.ccr_hits.load(Ordering::Relaxed),
403 "misses": self.ccr_misses.load(Ordering::Relaxed),
404 "created": self.ccr_created.load(Ordering::Relaxed),
405 },
406 "tool_relay_calls": self.tool_relay_calls.load(Ordering::Relaxed),
407 "cache": {
408 "hits": self.cache_hits.load(Ordering::Relaxed),
409 "misses": self.cache_misses.load(Ordering::Relaxed),
410 },
411 "latency_buckets_us": [
412 self.latency_buckets[0].load(Ordering::Relaxed),
413 self.latency_buckets[1].load(Ordering::Relaxed),
414 self.latency_buckets[2].load(Ordering::Relaxed),
415 self.latency_buckets[3].load(Ordering::Relaxed),
416 self.latency_buckets[4].load(Ordering::Relaxed),
417 ],
418 "total_latency_micros": self.total_latency_micros.load(Ordering::Relaxed),
419 "compressions_by_type": self.compressions_by_type.lock().map(|m| m.clone()).unwrap_or_default(),
420 "compression_ratio_ema": self.compression_ratio_ema.load(Ordering::Relaxed) as f64 / 100.0,
421 "last_errors": self.last_errors.lock().map(|v| v.iter().rev().take(5).cloned().collect::<Vec<_>>()).unwrap_or_default(),
422 "request_history": self.request_history.lock().map(|v| v.clone()).unwrap_or_default(),
423 "inline_ccr": {
424 "hits": self.inline_ccr_hits.load(Ordering::Relaxed),
425 "misses": self.inline_ccr_misses.load(Ordering::Relaxed),
426 },
427 "tool_relay": {
428 "total": self.tool_relay_calls.load(Ordering::Relaxed),
429 "success": self.tool_relay_success.load(Ordering::Relaxed),
430 "failure": self.tool_relay_failure.load(Ordering::Relaxed),
431 },
432 "notify": {
433 "success": self.notify_success.load(Ordering::Relaxed),
434 "failure": self.notify_failure.load(Ordering::Relaxed),
435 },
436 "upstream_errors": {
437 "4xx": self.upstream_errors_4xx.load(Ordering::Relaxed),
438 "5xx": self.upstream_errors_5xx.load(Ordering::Relaxed),
439 "timeouts": self.upstream_timeouts.load(Ordering::Relaxed),
440 "connect_errors": self.upstream_connect_errors.load(Ordering::Relaxed),
443 "sse_stream_errors": self.sse_stream_errors.load(Ordering::Relaxed),
445 },
446 "ccr_store": {
447 "entries": self.ccr_store_entries.load(Ordering::Relaxed),
448 "bytes_approx": self.ccr_store_bytes.load(Ordering::Relaxed),
449 },
450 "body_bytes": {
451 "request": self.request_body_bytes.load(Ordering::Relaxed),
452 "response": self.response_body_bytes.load(Ordering::Relaxed),
453 },
454 "upstream_latency_micros": self.upstream_latency_micros.load(Ordering::Relaxed),
455 })
456 }
457
458 fn compress_threshold(&self) -> usize {
459 match self.mode {
460 ProxyMode::Cache => self.cache_compress_threshold.load(Ordering::Relaxed),
461 ProxyMode::Token => self.token_compress_threshold.load(Ordering::Relaxed),
462 }
463 }
464
465 fn inline_ccr_threshold(&self) -> usize {
469 self.inline_ccr_threshold.load(Ordering::Relaxed)
470 }
471
472 fn code_multiplier(&self) -> f64 {
478 self.code_multiplier_x100.load(Ordering::Relaxed) as f64 / 100.0
479 }
480
481 fn threshold_for(&self, ct: &str) -> usize {
484 let base = self.compress_threshold();
485 match ct {
488 "linter" | "build_output" | "log" => return base,
489 _ => {},
490 }
491 let ratio = self.compression_ratio_ema.load(Ordering::Relaxed) as f64 / 100.0;
493 let tune = if ratio > 20.0 {
494 2.0
496 } else if ratio < 3.0 && ratio > 0.0 {
497 0.5
499 } else {
500 1.0
501 };
502 let base = (base as f64 * tune) as usize;
503 match ct {
504 "error" => base * 8,
505 "code_rust" | "code_python" | "code_go" | "code_js" | "code" => {
506 (base as f64 * self.code_multiplier()) as usize
507 },
508 "diff" | "git" => base * 2,
509 "text" => base * 2,
510 "tool_output" => base,
511 "json" => base,
512 _ => base,
513 }
514 }
515
516 fn update_compression_ratio(&self, original_len: usize, compressed_len: usize) {
517 if original_len == 0 || compressed_len == 0 {
518 return;
519 }
520 let ratio = (original_len as f64 / compressed_len as f64 * 100.0) as u64;
521 let old = self.compression_ratio_ema.load(Ordering::Relaxed);
523 let new = ((ratio as f64 * 0.2) + (old as f64 * 0.8)) as u64;
524 self.compression_ratio_ema.store(new, Ordering::Relaxed);
525 self.compute_fill_pct();
528 }
529
530 fn compute_fill_pct(&self) {
534 let ratio_ema = self.compression_ratio_ema.load(Ordering::Relaxed);
535 let pct = if ratio_ema == 0 {
536 99u64
537 } else {
538 let raw = 100u64.saturating_sub(ratio_ema / 20);
539 raw.clamp(1, 99)
540 };
541 self.fill_pct.store(pct * 100, Ordering::Relaxed); }
543
544 fn record_latency(&self, d: std::time::Duration) {
545 let us = d.as_micros() as u64;
546 let bucket = if us < 1_000 {
547 0
548 } else if us < 10_000 {
549 1
550 } else if us < 100_000 {
551 2
552 } else if us < 1_000_000 {
553 3
554 } else {
555 4
556 };
557 self.latency_buckets[bucket].fetch_add(1, Ordering::Relaxed);
558 self.total_latency_micros.fetch_add(us, Ordering::Relaxed);
559 }
560
561 fn record_error(&self, msg: String) {
562 if let Ok(mut v) = self.last_errors.lock() {
563 v.push_back(msg);
564 if v.len() > 100 {
565 v.pop_front();
566 }
567 }
568 }
569
570 fn record_compression(&self, ct: &str) {
571 if let Ok(mut m) = self.compressions_by_type.lock() {
572 *m.entry(ct.to_string()).or_insert(0) += 1;
573 }
574 }
575
576 fn record_request(&self, id: &str, method: &str, path: &str, status: u16, compressed: bool, elapsed_ms: u128) {
577 if let Ok(mut hist) = self.request_history.lock() {
578 hist.push_back(serde_json::json!({
579 "id": id,
580 "method": method,
581 "path": path,
582 "status": status,
583 "compressed": compressed,
584 "elapsed_ms": elapsed_ms,
585 }));
586 if hist.len() > 50 {
587 hist.pop_front();
588 }
589 }
590 }
591}
592
593#[derive(Debug, Deserialize)]
598pub struct ToolRelayRequest {
599 pub tool: String,
600 pub params: serde_json::Value,
601 pub callback_url: Option<String>,
602}
603
604#[derive(Debug, Serialize)]
609pub struct ToolRelayResponse {
610 pub success: bool,
611 pub result: Option<serde_json::Value>,
612 pub error: Option<String>,
613 pub async_call: bool,
614}
615
616#[derive(Debug, Deserialize)]
621pub struct CcrCreateRequest {
622 pub content: String,
623 pub key: Option<String>,
624 pub ttl_seconds: Option<u64>,
625 pub tags: Option<Vec<String>>,
626}
627
628#[derive(Debug, Serialize)]
631pub struct CcrCreateResponse {
632 pub hash: String,
633 pub token_savings_ratio: f64,
634 pub original_size: usize,
635 pub compressed_size: usize,
636 pub marker_size: usize,
637}
638
639#[derive(Debug, Serialize)]
643pub struct CcrNotification {
644 pub event: String,
645 pub hash: String,
646 pub created_at: u64,
647 pub ttl: u64,
648 pub tags: Vec<String>,
649}
650
651pub async fn build_state(cli: &Cli, compression: Option<&CompressionConfig>) -> anyhow::Result<AppState> {
657 let client = HttpClient::builder()
660 .timeout(std::time::Duration::from_secs(cli.timeout))
661 .connect_timeout(std::time::Duration::from_secs(10))
662 .pool_max_idle_per_host(100)
663 .pool_idle_timeout(std::time::Duration::from_secs(90))
664 .tcp_keepalive(std::time::Duration::from_secs(60))
665 .build()?;
666 let stream_client = HttpClient::builder()
669 .connect_timeout(std::time::Duration::from_secs(10))
670 .pool_max_idle_per_host(100)
671 .pool_idle_timeout(std::time::Duration::from_secs(90))
672 .tcp_keepalive(std::time::Duration::from_secs(60))
673 .build()?;
674
675 let ccr: Option<Arc<dyn CcrStore>> = match cli.mode {
676 ProxyMode::Token if !cli.no_ccr_marker => {
677 let db_path = cli.ccr_db_path.as_ref().map_or_else(
678 || {
679 dirs::home_dir()
680 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
681 .join(".hermes")
682 .join("aphrodite")
683 .join("ccr.db")
684 },
685 |p| p.clone(),
686 );
687 if let Some(parent) = db_path.parent() {
693 std::fs::create_dir_all(parent)
694 .map_err(|e| anyhow::anyhow!("SQLite CCR: cannot create directory {}: {}", parent.display(), e))?;
695 }
696 let store = SqliteCcrStore::open(&db_path, cli.ccr_ttl_seconds)
697 .map_err(|e| anyhow::anyhow!("SQLite CCR: {}", e))?;
698 Some(Arc::new(store))
699 },
700 ProxyMode::Cache => {
701 let store =
702 InMemoryCcrStore::with_capacity_and_ttl(10_000, std::time::Duration::from_secs(cli.ccr_ttl_seconds));
703 Some(Arc::new(store))
704 },
705 _ => None,
706 };
707
708 let thresholds = resolve_thresholds(compression);
709
710 Ok(AppState {
711 client,
712 stream_client,
713 api_url: cli.api_url.clone(),
714 model: cli.model.clone(),
715 api_key: cli.api_key.clone().into(),
716 ccr,
717 add_markers: !cli.no_ccr_marker,
718 mode: cli.mode,
719 tool_relay: cli.tool_relay,
720 notify_url: cli.notify_url.clone(),
721 notify_key: cli.notify_key.clone(),
722 dev: cli.dev,
723 latency_buckets: [
724 AtomicU64::new(0),
725 AtomicU64::new(0),
726 AtomicU64::new(0),
727 AtomicU64::new(0),
728 AtomicU64::new(0),
729 ],
730 total_latency_micros: AtomicU64::new(0),
731 last_errors: Mutex::new(VecDeque::new()),
732 compressions_by_type: Mutex::new(HashMap::new()),
733 request_history: Mutex::new(VecDeque::new()),
734 inline_ccr: Mutex::new(lru::LruCache::new(NonZeroUsize::new(1024).unwrap())),
735 requests_total: AtomicU64::new(0),
736 requests_compressed: AtomicU64::new(0),
737 tokens_saved: AtomicU64::new(0),
738 ccr_hits: AtomicU64::new(0),
739 ccr_misses: AtomicU64::new(0),
740 ccr_created: AtomicU64::new(0),
741 tool_relay_calls: AtomicU64::new(0),
742 compression_ratio_ema: AtomicU64::new(200), response_cache: Mutex::new(lru::LruCache::new(NonZeroUsize::new(128).unwrap())),
744 response_cache_ttl: std::time::Duration::from_secs(cli.ccr_ttl_seconds),
745 cache_hits: AtomicU64::new(0),
746 cache_misses: AtomicU64::new(0),
747 fill_pct: AtomicU64::new(9000), task_tracker: TaskTracker::new(),
749
750 inline_ccr_hits: AtomicU64::new(0),
751 inline_ccr_misses: AtomicU64::new(0),
752 tool_relay_success: AtomicU64::new(0),
753 tool_relay_failure: AtomicU64::new(0),
754 notify_success: AtomicU64::new(0),
755 notify_failure: AtomicU64::new(0),
756 upstream_errors_4xx: AtomicU64::new(0),
757 upstream_errors_5xx: AtomicU64::new(0),
758 upstream_timeouts: AtomicU64::new(0),
759 upstream_connect_errors: AtomicU64::new(0),
760 sse_stream_errors: AtomicU64::new(0),
761 ccr_store_entries: AtomicU64::new(0),
762 ccr_store_bytes: AtomicU64::new(0),
763 request_body_bytes: AtomicU64::new(0),
764 response_body_bytes: AtomicU64::new(0),
765 upstream_latency_micros: AtomicU64::new(0),
766 upstream_health_cache: std::sync::Mutex::new(None),
767 cache_compress_threshold: AtomicUsize::new(thresholds.cache),
768 token_compress_threshold: AtomicUsize::new(thresholds.token),
769 inline_ccr_threshold: AtomicUsize::new(thresholds.inline),
770 code_multiplier_x100: AtomicU64::new((thresholds.code_multiplier * 100.0) as u64),
771 })
772}
773
774fn body_wants_stream(body: &[u8]) -> bool {
782 serde_json::from_slice::<serde_json::Value>(body)
783 .ok()
784 .and_then(|v| v.get("stream").and_then(|s| s.as_bool()))
785 .unwrap_or(false)
786}
787
788fn cache_key_from_body(body: &[u8], api_key: &str) -> Option<u64> {
793 let v: serde_json::Value = serde_json::from_slice(body).ok()?;
794 if v.get("stream").and_then(|s| s.as_bool()).unwrap_or(false) {
798 return None;
799 }
800 v.get("model")?.as_str()?;
803 v.get("messages")?;
804 let mut parts: Vec<u8> = Vec::new();
811 parts.extend_from_slice(api_key.as_bytes());
812 for (label, val) in [
813 ("model", v.get("model")),
814 ("messages", v.get("messages")),
815 ("tools", v.get("tools")),
816 ("tool_choice", v.get("tool_choice")),
817 ("temperature", v.get("temperature")),
818 ("top_p", v.get("top_p")),
819 ("n", v.get("n")),
820 ("response_format", v.get("response_format")),
821 ] {
822 parts.push(b':');
823 parts.extend_from_slice(label.as_bytes());
824 parts.push(b'=');
825 if let Some(val) = val {
826 parts.extend_from_slice(serde_json::to_string(val).ok()?.as_bytes());
827 }
828 }
829 Some(fnv1a_64(&parts))
831}
832
833fn response_cache_get(state: &AppState, ck: u64) -> Option<Vec<u8>> {
839 state.response_cache.lock().ok().and_then(|mut cache| {
840 let expired = cache
841 .peek(&ck)
842 .map(|(inserted_at, _)| inserted_at.elapsed() >= state.response_cache_ttl)
843 .unwrap_or(false);
844 if expired {
845 cache.pop(&ck);
846 None
847 } else {
848 cache.get(&ck).map(|(_, body)| body.clone())
849 }
850 })
851}
852
853fn copy_upstream_headers(
860 mut builder: axum::http::response::Builder,
861 upstream_headers: &reqwest::header::HeaderMap,
862) -> axum::http::response::Builder {
863 const SKIP: &[&str] = &[
864 "content-length",
865 "content-type",
866 "transfer-encoding",
867 "connection",
868 "keep-alive",
869 ];
870 for (name, value) in upstream_headers.iter() {
871 if SKIP.contains(&name.as_str()) {
872 continue;
873 }
874 if let Ok(v) = axum::http::HeaderValue::from_bytes(value.as_bytes()) {
875 builder = builder.header(name.as_str(), v);
876 }
877 }
878 builder
879}
880
881async fn accumulate_body(response: reqwest::Response, max_bytes: usize) -> Result<bytes::Bytes, String> {
886 let mut buf = Vec::new();
887 let mut stream = response.bytes_stream();
888 while let Some(chunk) = stream.next().await {
889 match chunk {
890 Ok(b) => {
891 if buf.len() + b.len() > max_bytes {
892 return Err(format!("response body exceeded {} MB limit", max_bytes / (1024 * 1024)));
893 }
894 buf.extend_from_slice(&b);
895 },
896 Err(e) => return Err(format!("body read: {}", e)),
897 }
898 }
899 Ok(bytes::Bytes::from(buf))
900}
901
902fn fnv1a_64(bytes: &[u8]) -> u64 {
904 const FNV_OFFSET: u64 = 14695981039346656037;
905 const FNV_PRIME: u64 = 1099511628211;
906 let mut hash = FNV_OFFSET;
907 for &b in bytes {
908 hash ^= b as u64;
909 hash = hash.wrapping_mul(FNV_PRIME);
910 }
911 hash
912}
913
914fn is_sse(content_type: Option<&axum::http::HeaderValue>) -> bool {
919 content_type
920 .map(|ct| ct.as_bytes().starts_with(b"text/event-stream"))
921 .unwrap_or(false)
922}
923
924pub async fn proxy_handler(
927 State(state): State<Arc<AppState>>,
928 method: Method,
929 path: axum::extract::OriginalUri,
930 headers: axum::http::HeaderMap,
931 body: Bytes,
932) -> impl IntoResponse {
933 state.requests_total.fetch_add(1, Ordering::Relaxed);
934 state.request_body_bytes.fetch_add(body.len() as u64, Ordering::Relaxed);
935 let t0 = std::time::Instant::now();
936 let req_id = uuid::Uuid::new_v4().to_string();
937 let req_id_short = &req_id[..8];
938
939 if state.dev {
940 let mut hdr_log = String::new();
942 for (k, v) in headers.iter() {
943 let val = v.to_str().unwrap_or("?");
944 if k.as_str().to_lowercase() != "authorization" {
945 hdr_log.push_str(&format!(" {}: {}", k.as_str(), if val.len() > 80 { &val[..80] } else { val }));
946 } else {
947 hdr_log.push_str(" authorization: [REDACTED]");
948 }
949 hdr_log.push('\n');
950 }
951 tracing::info!(
952 id = %req_id_short,
953 method = %method,
954 path = %path.path(),
955 body_len = body.len(),
956 headers = %hdr_log,
957 ">>> REQ"
958 );
959 }
960
961 let deepseek_path_and_query = path
965 .0
966 .path_and_query()
967 .map(|pq| pq.as_str())
968 .unwrap_or_else(|| path.path())
969 .trim_start_matches('/');
970 let url = format!("{}/{}", state.api_url.trim_end_matches('/'), deepseek_path_and_query);
971
972 let is_chat_completion = path.path().trim_start_matches('/') == CHAT_COMPLETIONS_PATH.trim_start_matches('/');
973
974 let body_vec = body.to_vec();
975 let cache_key = if is_chat_completion {
976 cache_key_from_body(&body_vec, state.api_key.expose())
977 } else {
978 None
979 };
980 if let Some(ck) = cache_key {
982 let cached_body = response_cache_get(&state, ck);
983 if let Some(cached_body) = cached_body {
984 state.cache_hits.fetch_add(1, Ordering::Relaxed);
985 state.tokens_saved.fetch_add(cached_body.len() as u64, Ordering::Relaxed);
991 if state.dev {
992 tracing::info!(
993 id = %req_id_short,
994 cached_len = cached_body.len(),
995 "<<< CACHE HIT"
996 );
997 }
998 state.record_latency(t0.elapsed());
1003 state.record_request(req_id_short, method.as_str(), path.path(), 200, false, t0.elapsed().as_millis());
1004 return Response::builder()
1005 .status(StatusCode::OK)
1006 .header("Content-Type", "application/json; charset=utf-8")
1007 .header("X-Aphrodite-Cache", "HIT")
1008 .header("X-Aphrodite-Fill-Pct", {
1009 let v = state.fill_pct.load(Ordering::Relaxed) as f64 / 100.0;
1010 if v.is_finite() { format!("{:.1}", v) } else { "0.0".to_string() }
1011 })
1012 .body(Body::from(cached_body))
1013 .unwrap();
1014 } else {
1015 state.cache_misses.fetch_add(1, Ordering::Relaxed);
1016 if state.dev {
1017 tracing::info!(
1018 id = %req_id_short,
1019 "<<< CACHE MISS"
1020 );
1021 }
1022 }
1023 }
1024 let mut upstream_result = Err("unreachable".to_string());
1025 let mut final_error_was_timeout = false;
1030 let http_client = if body_wants_stream(&body_vec) { &state.stream_client } else { &state.client };
1033 let body_bytes = bytes::Bytes::from(body_vec);
1040 for attempt in 1..=3u32 {
1041 let req = http_client
1042 .request(method.clone(), &url)
1043 .header("Content-Type", "application/json; charset=utf-8")
1044 .header("Accept", "application/json")
1045 .header("Authorization", format!("Bearer {}", state.api_key.expose()));
1046 let mut req = req;
1047 for (key, val) in headers.iter() {
1048 let k = key.as_str().to_lowercase();
1049 if k != "host"
1061 && k != "authorization"
1062 && k != "content-length"
1063 && k != "content-type"
1064 && k != "accept"
1065 && k != "accept-encoding"
1066 && !k.starts_with("x-aphrodite-")
1067 {
1068 req = req.header(key, val);
1069 }
1070 }
1071 match req.body(body_bytes.clone()).send().await {
1074 Ok(r) => {
1075 upstream_result = Ok(r);
1076 break;
1077 },
1078 Err(e) => {
1079 if attempt < 3 && e.is_connect() {
1089 let base_ms = 100 * 2u64.pow(attempt - 1);
1090 let jitter = rand::random::<f64>() * 0.5 + 0.75; let ms = (base_ms as f64 * jitter) as u64;
1092 tracing::warn!(attempt, backoff_ms = ms, "upstream retry after connect error: {}", e);
1093 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
1094 } else {
1095 final_error_was_timeout = e.is_timeout();
1096 upstream_result = Err(format!("{}", e));
1097 break;
1098 }
1099 },
1100 }
1101 }
1102 match upstream_result {
1103 Ok(response) => {
1104 let status = response.status();
1105 let status_code = status.as_u16();
1107 if status_code >= 500 {
1108 state.upstream_errors_5xx.fetch_add(1, Ordering::Relaxed);
1109 } else if status_code >= 400 {
1110 state.upstream_errors_4xx.fetch_add(1, Ordering::Relaxed);
1111 }
1112 let upstream_headers = response.headers().clone();
1118 let content_type = upstream_headers.get("content-type").cloned();
1119
1120 if is_sse(content_type.as_ref()) {
1125 let state_for_stream = state.clone();
1130 let stream = response.bytes_stream().inspect(move |chunk| match chunk {
1131 Ok(bytes) => {
1132 state_for_stream
1133 .response_body_bytes
1134 .fetch_add(bytes.len() as u64, Ordering::Relaxed);
1135 },
1136 Err(_) => {
1137 state_for_stream.sse_stream_errors.fetch_add(1, Ordering::Relaxed);
1138 },
1139 });
1140 if state.dev {
1141 tracing::info!(id = %req_id_short, status = %status, "<<< STREAM (SSE)");
1142 }
1143 state.record_latency(t0.elapsed());
1144 state.record_request(
1145 req_id_short,
1146 method.as_str(),
1147 path.path(),
1148 status.as_u16(),
1149 false,
1150 t0.elapsed().as_millis(),
1151 );
1152 let mut builder = Response::builder().status(status);
1153 builder = copy_upstream_headers(builder, &upstream_headers);
1154 if let Some(ct) = content_type {
1155 builder = builder.header("Content-Type", ct);
1156 }
1157 builder = builder.header("X-Aphrodite-Streamed", "true");
1158 return builder.body(Body::from_stream(stream)).unwrap();
1159 }
1160
1161 let resp_body = match accumulate_body(response, RESPONSE_MAX_BODY_BYTES).await {
1165 Ok(b) => b,
1166 Err(e) => {
1167 state.record_error(format!("body read: {}", e));
1173 return (
1174 StatusCode::BAD_GATEWAY,
1175 Json(serde_json::json!({"error": "upstream request failed"})),
1176 )
1177 .into_response();
1178 },
1179 };
1180
1181 let upstream_elapsed = t0.elapsed().as_micros() as u64;
1183 state.upstream_latency_micros.fetch_add(upstream_elapsed, Ordering::Relaxed);
1184 state.response_body_bytes.fetch_add(resp_body.len() as u64, Ordering::Relaxed);
1186
1187 let elapsed = t0.elapsed();
1189 if is_chat_completion && state.ccr.is_some() {
1190 let headroom_budget = headers.get("x-headroom-budget").and_then(|v| v.to_str().ok());
1196 if state.dev && headroom_budget.is_some() {
1197 tracing::info!(
1198 id = %req_id_short,
1199 budget = %headroom_budget.unwrap_or(""),
1200 "headroom budget applied to compression threshold"
1201 );
1202 }
1203 if let Some(compressed) = compress_chat_completion(&state, &resp_body, headroom_budget).await {
1204 state.requests_compressed.fetch_add(1, Ordering::Relaxed);
1205 state.record_latency(elapsed);
1206 state.record_request(
1207 req_id_short,
1208 method.as_str(),
1209 path.path(),
1210 status.as_u16(),
1211 true,
1212 elapsed.as_millis(),
1213 );
1214 if state.dev {
1215 let elapsed = t0.elapsed();
1216 let comp_len = serde_json::to_vec(&compressed).map(|v| v.len()).unwrap_or(0);
1217 tracing::info!(
1218 id = %req_id_short,
1219 status = %status,
1220 original_len = resp_body.len(),
1221 compressed_len = comp_len,
1222 ratio = format!("{:.1}x", resp_body.len() as f64 / comp_len.max(1) as f64),
1223 elapsed_ms = elapsed.as_millis(),
1224 "<<< COMPRESSED"
1225 );
1226 }
1227 let body = serde_json::to_vec(&compressed).unwrap_or_else(|_| resp_body.to_vec());
1228 if let Some(ck) = cache_key {
1235 if status.is_success() && body.len() <= RESPONSE_CACHE_MAX_BODY_BYTES {
1236 if let Ok(mut cache) = state.response_cache.lock() {
1237 cache.put(ck, (std::time::Instant::now(), body.clone()));
1238 }
1239 }
1240 }
1241 let mut builder = Response::builder().status(status);
1242 builder = copy_upstream_headers(builder, &upstream_headers);
1243 return builder
1244 .header("Content-Type", "application/json; charset=utf-8")
1245 .header("X-Aphrodite-Compressed", "true")
1246 .header("X-Aphrodite-Cache", "MISS")
1247 .header("X-Aphrodite-Fill-Pct", {
1248 let v = state.fill_pct.load(Ordering::Relaxed) as f64 / 100.0;
1249 if v.is_finite() { format!("{:.1}", v) } else { "0.0".to_string() }
1250 })
1251 .body(Body::from(body))
1252 .unwrap();
1253 }
1254 }
1255
1256 if state.dev {
1257 let elapsed = t0.elapsed();
1258 let body_preview = if resp_body.len() > 500 {
1259 let s = std::str::from_utf8(&resp_body).unwrap_or("?");
1260 let preview: String = s.char_indices().take_while(|(i, _)| *i < 200).map(|(_, c)| c).collect();
1261 format!("{}... ({} total)", preview, resp_body.len())
1262 } else {
1263 std::str::from_utf8(&resp_body).unwrap_or("?").to_string()
1264 };
1265 tracing::info!(
1266 id = %req_id_short,
1267 status = %status,
1268 resp_len = resp_body.len(),
1269 elapsed_ms = elapsed.as_millis(),
1270 body = %body_preview,
1271 "<<< RES"
1272 );
1273 }
1274 state.record_latency(t0.elapsed());
1276 state.record_request(
1277 req_id_short,
1278 method.as_str(),
1279 path.path(),
1280 status.as_u16(),
1281 false,
1282 t0.elapsed().as_millis(),
1283 );
1284 if let Some(ck) = cache_key {
1287 if status.is_success() && resp_body.len() <= RESPONSE_CACHE_MAX_BODY_BYTES {
1288 if let Ok(mut cache) = state.response_cache.lock() {
1289 cache.put(ck, (std::time::Instant::now(), resp_body.to_vec()));
1290 }
1291 }
1292 }
1293 let mut builder = Response::builder().status(status);
1294 builder = copy_upstream_headers(builder, &upstream_headers);
1295 builder = builder.header("X-Aphrodite-Cache", "MISS");
1296 builder = builder.header("X-Aphrodite-Fill-Pct", {
1297 let v = state.fill_pct.load(Ordering::Relaxed) as f64 / 100.0;
1298 if v.is_finite() { format!("{:.1}", v) } else { "0.0".to_string() }
1299 });
1300 if let Some(ct) = content_type {
1301 builder = builder.header("Content-Type", ct);
1302 }
1303 builder.body(Body::from(resp_body)).unwrap()
1304 },
1305 Err(e) => {
1306 if final_error_was_timeout {
1307 state.upstream_timeouts.fetch_add(1, Ordering::Relaxed);
1308 } else {
1309 state.upstream_connect_errors.fetch_add(1, Ordering::Relaxed);
1310 }
1311 state.record_latency(t0.elapsed());
1312 state.record_request(req_id_short, method.as_str(), path.path(), 502, false, t0.elapsed().as_millis());
1313 state.record_error(format!("upstream: {}", e));
1314 if state.dev {
1315 tracing::error!(
1316 id = %req_id_short,
1317 error = %e,
1318 elapsed_ms = t0.elapsed().as_millis(),
1319 "<<< ERR"
1320 );
1321 }
1322 (
1327 StatusCode::BAD_GATEWAY,
1328 Json(serde_json::json!({"error": "upstream request failed"})),
1329 )
1330 .into_response()
1331 },
1332 }
1333}
1334
1335fn proxy_detect_content_type(content: &str) -> &'static str {
1337 let first_line = content.lines().next().unwrap_or("");
1338
1339 if content.starts_with('{') || content.starts_with('[') {
1341 if serde_json::from_str::<serde_json::Value>(content).is_err() {
1343 return "text";
1345 }
1346 if content.contains("exit_code") || content.contains("\"status\"") {
1347 return "tool_output";
1348 }
1349 return "json";
1350 }
1351
1352 if content.lines().count() > 3 {
1354 if content.lines().any(|l| {
1357 let t = l.trim_start();
1358 t.starts_with("fn ")
1359 || t.starts_with("pub fn ")
1360 || t.starts_with("async fn ")
1361 || t.starts_with("pub async fn ")
1362 || t.starts_with("impl ")
1363 || t.starts_with("struct ")
1364 || t.starts_with("pub struct ")
1365 || t.starts_with("enum ")
1366 || t.starts_with("pub enum ")
1367 }) && (content.contains("-> ") || content.contains("&") || content.contains("use "))
1368 {
1369 return "code_rust";
1370 }
1371 if content.contains("def ")
1373 && (content.contains("import ")
1374 || content.contains("class ")
1375 || content.contains("from ")
1376 || content.contains("self."))
1377 {
1378 return "code_python";
1379 }
1380 if (content.contains("func ") || content.contains("package ")) && content.contains("import (") {
1382 return "code_go";
1383 }
1384 if (content.contains("function ") || content.contains("const ") || content.contains("=> "))
1386 && (content.contains("import ") || content.contains("export "))
1387 {
1388 return "code_js";
1389 }
1390 if content.contains("fn ")
1392 || content.contains("def ")
1393 || content.contains("class ")
1394 || content.contains("import ")
1395 || content.contains("pub fn")
1396 {
1397 return "code";
1398 }
1399 }
1400
1401 if let Some(t) = crate::preview::detect_semantic_type(content) {
1409 return t;
1410 }
1411
1412 if first_line.contains("error")
1414 || first_line.contains("Error")
1415 || first_line.contains("ERROR")
1416 || first_line.contains("Traceback")
1417 || first_line.contains("panic")
1418 || first_line.starts_with("thread '")
1419 {
1420 return "error";
1421 }
1422
1423 if first_line.starts_with("Compiling ")
1425 || first_line.starts_with(" Compiling ")
1426 || first_line.contains("Finished")
1427 || first_line.starts_with("running ")
1428 || first_line.starts_with("test ")
1429 {
1430 return "build_output";
1431 }
1432
1433 if first_line.starts_with("error[E")
1435 || first_line.starts_with("error: ")
1436 || first_line.starts_with("warning[")
1437 || first_line.starts_with("warning: ")
1438 || first_line.contains("|") && (first_line.contains("error") || first_line.contains("warning"))
1439 || first_line.contains("mypy")
1440 || first_line.contains("clippy")
1441 || first_line.contains("eslint")
1442 || first_line.contains("tsc ")
1443 {
1444 return "linter";
1445 }
1446
1447 if first_line.starts_with("diff --git ")
1449 || first_line.starts_with("@@ -")
1450 || first_line.starts_with("+++ ")
1451 || first_line.starts_with("--- ")
1452 {
1453 return "diff";
1454 }
1455
1456 if first_line.starts_with("commit ") || first_line.starts_with("On branch ") {
1458 return "git";
1459 }
1460
1461 if content.lines().any(|l| {
1463 let t = l.trim();
1464 t.starts_with('[')
1465 && (t.contains("INFO")
1466 || t.contains("WARN")
1467 || t.contains("ERROR")
1468 || t.contains("DEBUG")
1469 || t.contains("TRACE")
1470 || t.contains("FATAL")
1471 || t.contains("PANIC"))
1472 }) || content.lines().any(|l| {
1473 let t = l.trim();
1474 t.starts_with(|c: char| c.is_ascii_digit()) && t.len() > 10 && (t.contains(':') || t.contains('-'))
1476 }) {
1477 return "log";
1478 }
1479 "text"
1480}
1481
1482fn generate_metadata(content: &str, ct: &str) -> String {
1485 let line_count = content.lines().count();
1486 let mut parts: Vec<String> = Vec::new();
1487
1488 match ct {
1489 "code_rust" => {
1490 parts.push("lang=rs".to_string());
1491 let fns: Vec<&str> = content
1492 .lines()
1493 .filter(|l| {
1494 let t = l.trim_start();
1495 t.starts_with("fn ") || t.starts_with("pub fn ") || t.starts_with("async fn ")
1496 })
1497 .filter_map(|l| {
1498 let t = l.trim_start();
1499 let after_fn = t
1500 .strip_prefix("pub async fn ")
1501 .or_else(|| t.strip_prefix("pub fn "))
1502 .or_else(|| t.strip_prefix("async fn "))
1503 .or_else(|| t.strip_prefix("fn "))?;
1504 after_fn.split(['(', ' ', '<']).next().filter(|s| !s.is_empty())
1505 })
1506 .collect();
1507 if !fns.is_empty() {
1508 parts.push(format!("fns={}", fns.join(",")));
1509 }
1510
1511 let structs: Vec<&str> = content
1512 .lines()
1513 .filter(|l| {
1514 let t = l.trim_start();
1515 t.starts_with("struct ") || t.starts_with("pub struct ")
1516 })
1517 .filter_map(|l| {
1518 let t = l.trim_start();
1519 let after = t
1520 .strip_prefix("pub struct ")
1521 .unwrap_or_else(|| t.strip_prefix("struct ").unwrap_or(t));
1522 after.split(['(', ' ', '<', '{']).next().filter(|s| !s.is_empty())
1523 })
1524 .collect();
1525 if !structs.is_empty() {
1526 parts.push(format!("structs={}", structs.join(",")));
1527 }
1528
1529 let impls: Vec<&str> = content
1531 .lines()
1532 .filter(|l| {
1533 let t = l.trim_start();
1534 t.starts_with("impl ") || t.starts_with("pub impl ")
1535 })
1536 .filter_map(|l| {
1537 let t = l.trim_start();
1538 let after = t
1539 .strip_prefix("pub impl ")
1540 .unwrap_or_else(|| t.strip_prefix("impl ").unwrap_or(t));
1541 after.split_whitespace().next().map(|w| w.trim_end_matches('<'))
1542 })
1543 .collect();
1544 if !impls.is_empty() {
1545 parts.push(format!("impls={}", impls.join(",")));
1546 }
1547
1548 let traits: Vec<&str> = content
1549 .lines()
1550 .filter(|l| {
1551 let t = l.trim_start();
1552 t.starts_with("trait ") || t.starts_with("pub trait ")
1553 })
1554 .filter_map(|l| {
1555 let t = l.trim_start();
1556 let after = t
1557 .strip_prefix("pub trait ")
1558 .unwrap_or_else(|| t.strip_prefix("trait ").unwrap_or(t));
1559 after.split([' ', '<', '{']).next().filter(|s| !s.is_empty())
1560 })
1561 .collect();
1562 if !traits.is_empty() {
1563 parts.push(format!("traits={}", traits.join(",")));
1564 }
1565
1566 parts.push(format!("ln={}", line_count));
1567 },
1568 "code_python" => {
1569 parts.push("lang=py".to_string());
1570 let fns: Vec<&str> = content
1571 .lines()
1572 .filter(|l| {
1573 let t = l.trim_start();
1574 t.starts_with("def ") || t.starts_with("async def ")
1575 })
1576 .filter_map(|l| {
1577 let t = l.trim_start();
1578 let after = t
1579 .strip_prefix("async def ")
1580 .unwrap_or_else(|| t.strip_prefix("def ").unwrap_or(t));
1581 after.split(['(', ' ', ':']).next().filter(|s| !s.is_empty())
1582 })
1583 .collect();
1584 if !fns.is_empty() {
1585 parts.push(format!("fns={}", fns.join(",")));
1586 }
1587 let classes: Vec<&str> = content
1588 .lines()
1589 .filter(|l| {
1590 let t = l.trim_start();
1591 t.starts_with("class ")
1592 })
1593 .filter_map(|l| {
1594 let t = l.trim_start();
1595 let after = t.strip_prefix("class ")?;
1596 after.split(['(', ' ', ':']).next().filter(|s| !s.is_empty())
1597 })
1598 .collect();
1599 if !classes.is_empty() {
1600 parts.push(format!("classes={}", classes.join(",")));
1601 }
1602 let imports: Vec<&str> = content
1603 .lines()
1604 .filter(|l| {
1605 let t = l.trim_start();
1606 t.starts_with("import ") || t.starts_with("from ")
1607 })
1608 .filter_map(|l| {
1609 let t = l.trim_start();
1610 if let Some(rest) = t.strip_prefix("import ") {
1611 rest.split([' ', ',', ';']).next().filter(|s| !s.is_empty())
1612 } else {
1613 t.strip_prefix("from ")?.split(' ').next().filter(|s| !s.is_empty())
1614 }
1615 })
1616 .collect();
1617 if !imports.is_empty() {
1618 parts.push(format!("imports={}", imports.join(",")));
1619 }
1620 let decorators: Vec<&str> = content
1622 .lines()
1623 .filter(|l| {
1624 let t = l.trim_start();
1625 t.starts_with('@')
1626 })
1627 .filter_map(|l| {
1628 let t = l.trim_start();
1629 let name = &t[1..];
1630 name.split(['(', ' ']).next().filter(|s| !s.is_empty())
1631 })
1632 .collect();
1633 if !decorators.is_empty() {
1634 parts.push(format!("decorators={}", decorators.join(",")));
1635 }
1636 parts.push(format!("ln={}", line_count));
1637 },
1638 "code_go" => {
1639 parts.push("lang=go".to_string());
1640 let fns: Vec<&str> = content
1641 .lines()
1642 .filter(|l| {
1643 let t = l.trim_start();
1644 t.starts_with("func ")
1645 })
1646 .filter_map(|l| {
1647 let t = l.trim_start();
1648 let after = t.strip_prefix("func ")?;
1649 after.split(['(', ' ']).next().filter(|s| !s.is_empty())
1650 })
1651 .collect();
1652 if !fns.is_empty() {
1653 parts.push(format!("fns={}", fns.join(",")));
1654 }
1655 parts.push(format!("ln={}", line_count));
1656 },
1657 "code_js" => {
1658 parts.push("lang=js".to_string());
1659 let fns: Vec<&str> = content
1660 .lines()
1661 .filter(|l| {
1662 let t = l.trim_start();
1663 t.starts_with("function ") || t.starts_with("const ")
1664 })
1665 .filter_map(|l| {
1666 let t = l.trim_start();
1667 if let Some(rest) = t.strip_prefix("function ") {
1668 rest.split(['(', ' ']).next().filter(|s| !s.is_empty())
1669 } else {
1670 t.strip_prefix("const ")?
1671 .split([' ', '=', ':'])
1672 .next()
1673 .filter(|s| !s.is_empty())
1674 }
1675 })
1676 .collect();
1677 if !fns.is_empty() {
1678 parts.push(format!("fns={}", fns.join(",")));
1679 }
1680 parts.push(format!("ln={}", line_count));
1681 },
1682 "code" => {
1683 parts.push("lang=gen".to_string());
1684 let sigs: Vec<&str> = content
1686 .lines()
1687 .filter(|l| {
1688 let t = l.trim_start();
1689 t.starts_with("fn ")
1690 || t.starts_with("def ")
1691 || t.starts_with("func ")
1692 || t.starts_with("function ")
1693 || t.starts_with("class ")
1694 || t.starts_with("struct ")
1695 })
1696 .filter_map(|l| {
1697 let t = l.trim_start();
1698 let after = t
1699 .strip_prefix("fn ")
1700 .or_else(|| t.strip_prefix("def "))
1701 .or_else(|| t.strip_prefix("func "))
1702 .or_else(|| t.strip_prefix("function "))
1703 .or_else(|| t.strip_prefix("class "))
1704 .or_else(|| t.strip_prefix("struct "))?;
1705 after.split(['(', ' ']).next()
1706 })
1707 .collect();
1708 if !sigs.is_empty() {
1709 parts.push(format!("sigs={}", sigs.join(",")));
1710 }
1711 parts.push(format!("ln={}", line_count));
1712 },
1713 "error" => {
1714 let mut trace = String::new();
1715 for l in content.lines() {
1716 let t = l.trim();
1717 let ext_pos = t.find(".rs:").or_else(|| t.find(".py:")).or_else(|| t.find(".go:"));
1718 if let Some(pos) = ext_pos {
1719 let mut start = pos.saturating_sub(12);
1725 while start > 0 && !t.is_char_boundary(start) {
1726 start -= 1;
1727 }
1728 let mut end = (pos + 40).min(t.len());
1729 while end < t.len() && !t.is_char_boundary(end) {
1730 end += 1;
1731 }
1732 trace = t[start..end].to_string();
1733 break;
1734 }
1735 }
1736 if !trace.is_empty() {
1737 parts.push(format!("trace={}", trace.replace('|', "/")));
1738 }
1739 let msg = content.lines().find(|l| l.contains("Error:") || l.contains("error[")).map(|l| {
1740 let t = l.trim();
1741 let idx = t.find("Error:").or_else(|| t.find("error[")).unwrap_or(0);
1742 t[idx..].chars().take(80).collect::<String>().replace('|', "/")
1743 });
1744 if let Some(m) = msg {
1745 parts.push(format!("msg={}", m));
1746 } else {
1747 let fl = content.lines().next().unwrap_or("").trim();
1748 if !fl.is_empty() {
1749 parts.push(format!("msg={}", fl.chars().take(80).collect::<String>().replace('|', "/")));
1750 }
1751 }
1752 let err_count = content
1753 .lines()
1754 .filter(|l| l.contains("error") || l.starts_with("thread '"))
1755 .count();
1756 if err_count > 0 {
1757 parts.push(format!("N_errors={}", err_count));
1758 }
1759 },
1760 "diff" => {
1761 let files = content.lines().filter(|l| l.starts_with("diff --git ")).count();
1762 if files > 0 {
1763 parts.push(format!("files={}", files));
1764 }
1765 let adds = content.lines().filter(|l| l.starts_with('+') && !l.starts_with("+++")).count();
1766 let dels = content.lines().filter(|l| l.starts_with('-') && !l.starts_with("---")).count();
1767 if adds > 0 {
1768 parts.push(format!("adds={}", adds));
1769 }
1770 if dels > 0 {
1771 parts.push(format!("dels={}", dels));
1772 }
1773 },
1774 "git" => {
1775 for l in content.lines() {
1776 let t = l.trim();
1777 if let Some(rest) = t.strip_prefix("On branch ") {
1778 parts.push(format!("branch={}", rest.trim().replace('|', "/")));
1779 break;
1780 }
1781 }
1782 if !parts.iter().any(|p| p.starts_with("branch=")) {
1783 for l in content.lines() {
1784 let t = l.trim();
1785 if !t.is_empty() && !t.starts_with("* ") && !t.starts_with(" ") {
1786 parts.push(format!("branch={}", t.chars().take(40).collect::<String>().replace('|', "/")));
1787 break;
1788 }
1789 }
1790 }
1791 let commits = content
1792 .lines()
1793 .filter(|l| l.starts_with("commit ") || l.trim().starts_with("* ") || l.contains("commit"))
1794 .count();
1795 if commits > 0 {
1796 parts.push(format!("commits={}", commits));
1797 }
1798 },
1799 "build_output" => {
1800 if content.contains("error") || content.contains("aborting") {
1801 parts.push("status=FAIL".to_string());
1802 } else {
1803 parts.push("status=OK".to_string());
1804 }
1805 let files = content
1806 .lines()
1807 .filter(|l| l.starts_with("Compiling ") || l.contains(" Compiling "))
1808 .count();
1809 if files > 0 {
1810 parts.push(format!("files={}", files));
1811 }
1812 let first_err = content
1813 .lines()
1814 .find(|l| l.contains("error[") || l.contains("Error:"))
1815 .map(|l| l.trim().chars().take(80).collect::<String>().replace('|', "/"));
1816 if let Some(e) = first_err {
1817 parts.push(format!("first_err={}", e));
1818 }
1819 },
1820 "log" => {
1821 for l in content.lines() {
1822 let t = l.trim();
1823 for level in &["ERROR", "WARN", "WARNING", "INFO", "DEBUG", "TRACE", "FATAL", "PANIC"] {
1824 if t.contains(level) {
1825 parts.push(format!("level={}", level.to_lowercase()));
1826 break;
1827 }
1828 }
1829 if parts.iter().any(|p| p.starts_with("level=")) {
1830 break;
1831 }
1832 }
1833 let last_line = content.lines().last().unwrap_or("").trim().chars().take(60).collect::<String>();
1834 if !last_line.is_empty() {
1835 parts.push(format!("last={}", last_line.replace('|', "/")));
1836 }
1837 parts.push(format!("ln={}", line_count));
1838 },
1839 "linter" => {
1840 let files_linted = content
1841 .lines()
1842 .filter(|l| {
1843 (l.contains(".rs:") || l.contains(".py:") || l.contains(".go:") || l.contains(".ts:"))
1844 && (l.contains("error") || l.contains("warning"))
1845 })
1846 .count();
1847 if files_linted > 0 {
1848 parts.push(format!("files={}", files_linted));
1849 }
1850 let first_err = content
1851 .lines()
1852 .find(|l| l.contains("error[") || l.contains("Error:") || l.starts_with("error: "))
1853 .map(|l| l.trim().chars().take(80).collect::<String>().replace('|', "/"));
1854 if let Some(e) = first_err {
1855 parts.push(format!("first_err={}", e));
1856 }
1857 parts.push(format!("ln={}", line_count));
1858 },
1859 "json" | "tool_output" => {
1860 let mut keys: Vec<String> = Vec::new();
1863 for l in content.lines() {
1864 let bytes = l.as_bytes();
1866 let mut i = 0;
1867 while i + 3 < bytes.len() {
1868 if bytes[i] == b'"' {
1869 let start = i + 1;
1870 let mut end = start;
1871 while end < bytes.len() && bytes[end] != b'"' {
1872 end += 1;
1873 }
1874 if end < bytes.len() && end + 2 < bytes.len() && bytes[end + 1] == b':' {
1875 let key = &l[start..end];
1876 if !key.starts_with('_') && !keys.contains(&key.to_string()) {
1877 keys.push(key.to_string());
1878 if keys.len() >= 10 {
1879 break;
1880 }
1881 }
1882 }
1883 i = end + 1;
1884 } else {
1885 i += 1;
1886 }
1887 }
1888 if keys.len() >= 10 {
1889 break;
1890 }
1891 }
1892 if !keys.is_empty() {
1893 parts.push(format!("keys={}", keys.join(",")));
1894 }
1895 let entries = content
1897 .lines()
1898 .filter(|l| {
1899 let t = l.trim();
1900 t.starts_with('{') || t.starts_with('"') || t.starts_with('[')
1901 })
1902 .count();
1903 if entries > 1 {
1904 parts.push(format!("entries={}", entries));
1905 }
1906 },
1907 "text" => {
1908 parts.push(format!("ln={}", line_count));
1909 },
1910 _ => {
1911 parts.push(format!("ln={}", line_count));
1912 },
1913 }
1914
1915 let result = parts.join(";").replace('\n', " ").replace('\r', "");
1919 let truncated: String = result.chars().take(400).collect();
1920 truncated.trim_end_matches([';', ' ', ',']).to_string()
1921}
1922
1923fn proxy_format_ccr_output(
1927 preview: &str,
1928 ct: &str,
1929 metadata: &str,
1930 center: Option<&str>,
1931 hash: &str,
1932 size: usize,
1933) -> String {
1934 let center_seg = center.map(|c| format!(";center={c}")).unwrap_or_default();
1935 format!("{preview}\n[{ct}: {metadata}{center_seg}]\n<<<CCR:{hash}|{ct}|{size}>>>")
1936}
1937
1938fn proxy_build_preview(content: &str, ct: &str) -> String {
1947 if matches!(
1955 ct,
1956 "git" | "git_status" | "gitlog" | "git_log" | "ls" | "dir" | "test" | "test_output" | "grep" | "log"
1957 ) || (matches!(ct, "text" | "terminal") && crate::preview::detect_semantic_type(content).is_some())
1958 {
1959 return crate::preview::build_preview(ct, content);
1960 }
1961 match ct {
1962 "code_rust" | "code_python" | "code_go" | "code_js" | "code_ts" | "code_sh" | "code" => {
1963 let mut fns: Vec<&str> = Vec::new();
1965 let mut structs: Vec<&str> = Vec::new();
1966 let mut impls: Vec<&str> = Vec::new();
1967 let mut classes: Vec<&str> = Vec::new();
1968 let mut budget: usize = 280;
1969
1970 for line in content.lines() {
1971 if budget == 0 {
1972 break;
1973 }
1974 let trimmed = line.trim();
1975 if trimmed.is_empty() {
1976 continue;
1977 }
1978
1979 if ct == "code_rust" || ct == "code" {
1981 if trimmed.strip_prefix("fn ").is_some() {
1982 let sig: String = trimmed.chars().take(58).collect();
1983 fns.push(trimmed); budget = budget.saturating_sub(sig.len() + 2);
1985 } else if trimmed.strip_prefix("pub fn ").is_some() {
1986 let sig: String = trimmed.chars().take(58).collect();
1987 fns.push(trimmed);
1988 budget = budget.saturating_sub(sig.len() + 2);
1989 } else if trimmed.starts_with("struct ") || trimmed.starts_with("pub struct ") {
1990 let s: String = trimmed.chars().take(50).collect();
1991 structs.push(trimmed);
1992 budget = budget.saturating_sub(s.len() + 2);
1993 } else if trimmed.starts_with("impl ") {
1994 let s: String = trimmed.chars().take(50).collect();
1995 impls.push(trimmed);
1996 budget = budget.saturating_sub(s.len() + 2);
1997 }
1998 }
1999 if ct == "code_python" || ct == "code" {
2001 if (trimmed.starts_with("def ") || trimmed.starts_with("async def ")) && trimmed.ends_with(':') {
2002 let s: String = trimmed.chars().take(58).collect();
2003 fns.push(trimmed);
2004 budget = budget.saturating_sub(s.len() + 2);
2005 } else if trimmed.starts_with("class ") && trimmed.ends_with(':') {
2006 let s: String = trimmed.chars().take(50).collect();
2007 classes.push(trimmed);
2008 budget = budget.saturating_sub(s.len() + 2);
2009 }
2010 }
2011 if ct == "code_go" && trimmed.starts_with("func ") {
2013 let s: String = trimmed.chars().take(58).collect();
2014 fns.push(trimmed);
2015 budget = budget.saturating_sub(s.len() + 2);
2016 }
2017 }
2018
2019 let mut parts: Vec<String> = Vec::new();
2021 if !fns.is_empty() {
2022 parts.push(format!("{}fns", fns.len()));
2023 }
2024 if !structs.is_empty() {
2025 parts.push(format!("{}structs", structs.len()));
2026 }
2027 if !impls.is_empty() {
2028 parts.push(format!("{}impls", impls.len()));
2029 }
2030 if !classes.is_empty() {
2031 parts.push(format!("{}classes", classes.len()));
2032 }
2033 let summary = if parts.is_empty() { "?".to_string() } else { parts.join("|") };
2034
2035 let sig_previews: Vec<String> =
2037 fns.iter().take(2).map(|s| s.chars().take(56).collect::<String>()).collect();
2038 let sig_str = sig_previews.join("; ");
2039
2040 let lines = content.lines().count();
2041 format!("[{ct}:{summary} {sig_str} {lines}L]").chars().take(300).collect()
2042 },
2043 "error" => {
2044 let err_line = content
2046 .lines()
2047 .find(|l| l.contains("Error:") || l.contains("error[") || l.contains("panicked"))
2048 .unwrap_or_else(|| content.lines().next().unwrap_or(""));
2049 err_line.chars().take(300).collect()
2050 },
2051 "diff" => {
2052 let files: Vec<&str> = content.lines().filter(|l| l.starts_with("diff --git ")).take(2).collect();
2054 if files.is_empty() {
2055 content.lines().next().unwrap_or("").chars().take(200).collect()
2056 } else {
2057 files.join("\n").chars().take(300).collect()
2058 }
2059 },
2060 "json" | "tool_output" => {
2061 let first = content.lines().next().unwrap_or("");
2063 let key_count = content.matches("\":").count();
2064 format!("{} … {} keys", first.chars().take(150).collect::<String>(), key_count)
2065 },
2066 "build_output" => {
2067 content
2069 .lines()
2070 .find(|l| l.contains("Compiling") || l.contains("Finished") || l.contains("error"))
2071 .unwrap_or_else(|| content.lines().next().unwrap_or(""))
2072 .chars()
2073 .take(250)
2074 .collect()
2075 },
2076 _ => {
2077 content.lines().next().unwrap_or("").chars().take(250).collect()
2079 },
2080 }
2081}
2082
2083fn smart_marker(hash: &str, content: &str, ct: &str, center: Option<&str>) -> String {
2089 let size = content.len();
2090 let metadata = generate_metadata(content, ct);
2091 let preview = proxy_build_preview(content, ct);
2092 proxy_format_ccr_output(&preview, ct, &metadata, center, hash, size)
2093}
2094
2095fn cache_marker(hash: &str, content: &str, ct: &str, center: Option<&str>) -> String {
2097 let size = content.len();
2098 let preview: String = content.chars().take(512).collect();
2099 proxy_format_ccr_output(&preview, ct, "", center, hash, size)
2100}
2101
2102async fn compress_chat_completion(
2104 state: &AppState,
2105 resp_body: &[u8],
2106 headroom_budget: Option<&str>,
2107) -> Option<serde_json::Value> {
2108 let mut response: serde_json::Value = serde_json::from_slice(resp_body).ok()?;
2109 let choices = response.get_mut("choices")?.as_array_mut()?;
2110 let base_threshold = state.compress_threshold(); let budget_mult = headroom_budget
2116 .and_then(|b| {
2117 let val: f64 = b.parse().ok()?;
2118 Some((0.50 + (val / 100.0) * 0.50).clamp(0.50, 1.0))
2120 })
2121 .unwrap_or(1.0);
2122 let mut did_compress = false;
2123
2124 for choice in choices {
2125 let message = choice.get_mut("message")?;
2126
2127 if let Some(content_val) = message.get_mut("content") {
2129 if let Some(content) = content_val.as_str() {
2130 let ct = proxy_detect_content_type(content);
2131 let threshold = (state.threshold_for(ct).max(base_threshold) as f64 * budget_mult) as usize;
2132 if content.len() > threshold {
2133 if let Some(ccr) = &state.ccr {
2134 let hash = compute_key(content.as_bytes());
2135 let stored = if ccr_get(ccr, &hash).await.is_some() {
2143 state.ccr_hits.fetch_add(1, Ordering::Relaxed);
2144 true
2145 } else {
2146 state.ccr_misses.fetch_add(1, Ordering::Relaxed);
2147 let ok = ccr_put(ccr, &hash, content).await;
2148 if ok {
2149 state.ccr_created.fetch_add(1, Ordering::Relaxed);
2150 } else {
2151 tracing::error!(hash = %hash, "ccr_put failed - leaving content uncompressed to avoid data loss");
2152 }
2153 ok
2154 };
2155 if stored {
2156 let (compressed, orig_len) = {
2157 let compressed = match state.mode {
2158 ProxyMode::Cache => cache_marker(&hash, content, ct, None),
2159 ProxyMode::Token => smart_marker(&hash, content, ct, None),
2160 };
2161 let len = content.len();
2162 state.record_compression(ct);
2163 (compressed, len)
2164 };
2165 let marker_len = compressed.len();
2166 state
2174 .tokens_saved
2175 .fetch_add(orig_len.saturating_sub(marker_len) as u64, Ordering::Relaxed);
2176 *content_val = serde_json::Value::String(compressed);
2177 did_compress = true;
2178 state.update_compression_ratio(orig_len, marker_len);
2179 }
2180 }
2181 } else if content.len() > state.inline_ccr_threshold() {
2182 let hash = compute_key(content.as_bytes());
2185 if let Ok(mut map) = state.inline_ccr.lock() {
2186 if map.contains(&hash) {
2187 state.inline_ccr_hits.fetch_add(1, Ordering::Relaxed);
2188 } else {
2189 state.inline_ccr_misses.fetch_add(1, Ordering::Relaxed);
2190 map.put(hash, content.to_string());
2191 }
2192 }
2193 }
2194 }
2195 }
2196
2197 }
2207
2208 if did_compress { Some(response) } else { None }
2209}
2210
2211pub async fn handle_tool_relay(
2219 State(state): State<Arc<AppState>>,
2220 Json(req): Json<ToolRelayRequest>,
2221) -> impl IntoResponse {
2222 state.tool_relay_calls.fetch_add(1, Ordering::Relaxed);
2223 tracing::info!(tool = %req.tool, "tool_relay");
2224
2225 if req.tool == "aphrodite_retrieve" && req.params.get("hash").and_then(|v| v.as_str()).is_none() {
2228 return (
2229 StatusCode::BAD_REQUEST,
2230 Json(ToolRelayResponse {
2231 success: false,
2232 result: None,
2233 error: Some(
2234 "`hash` is required for 💋/aphrodite_retrieve. Requests with only `query` and no `hash` are \
2235 invalid."
2236 .into(),
2237 ),
2238 async_call: false,
2239 }),
2240 )
2241 .into_response();
2242 }
2243
2244 if let Some(cb) = &req.callback_url {
2245 let parsed_url = match url::Url::parse(cb) {
2247 Ok(u) if u.scheme() == "https" => u,
2248 _ => {
2249 tracing::warn!(callback_url = %cb, "tool_relay callback rejected: only https scheme allowed");
2254 return (
2255 StatusCode::BAD_REQUEST,
2256 Json(ToolRelayResponse {
2257 success: false,
2258 result: None,
2259 error: Some("callback_url must use the https scheme".into()),
2260 async_call: false,
2261 }),
2262 )
2263 .into_response();
2264 },
2265 };
2266 let tracker = state.task_tracker.clone();
2267 let state = state.clone();
2268 let tool = req.tool.clone();
2269 let params = req.params.clone();
2270 let cb = parsed_url.to_string();
2271 tracker.spawn(async move {
2272 let result = execute_tool_relay(&state, &tool, ¶ms).await;
2273 if result.is_ok() {
2279 state.tool_relay_success.fetch_add(1, Ordering::Relaxed);
2280 } else {
2281 state.tool_relay_failure.fetch_add(1, Ordering::Relaxed);
2282 }
2283 let _ = state
2284 .client
2285 .post(&cb)
2286 .json(&result)
2287 .timeout(Duration::from_secs(5))
2288 .send()
2289 .await;
2290 });
2291 return Json(ToolRelayResponse { success: true, result: None, error: None, async_call: true }).into_response();
2292 }
2293
2294 match execute_tool_relay(&state, &req.tool, &req.params).await {
2295 Ok(val) => {
2296 state.tool_relay_success.fetch_add(1, Ordering::Relaxed);
2297 Json(ToolRelayResponse { success: true, result: Some(val), error: None, async_call: false }).into_response()
2298 },
2299 Err(e) => {
2300 state.tool_relay_failure.fetch_add(1, Ordering::Relaxed);
2301 Json(ToolRelayResponse { success: false, result: None, error: Some(e), async_call: false }).into_response()
2302 },
2303 }
2304}
2305
2306async fn execute_tool_relay(
2310 state: &AppState,
2311 tool: &str,
2312 params: &serde_json::Value,
2313) -> Result<serde_json::Value, String> {
2314 match tool {
2315 "aphrodite_retrieve" => {
2316 let hash_raw = params.get("hash").and_then(|v| v.as_str()).ok_or("missing hash")?;
2317 let hash = crate::marker::normalize_hash(hash_raw);
2323 if let Ok(mut map) = state.inline_ccr.lock() {
2325 if let Some(content) = map.get(hash) {
2326 state.inline_ccr_hits.fetch_add(1, Ordering::Relaxed);
2327 return Ok(serde_json::json!({"found": true, "content": content.clone()}));
2328 }
2329 }
2330 state.inline_ccr_misses.fetch_add(1, Ordering::Relaxed);
2331 if let Some(ccr) = &state.ccr {
2333 match ccr_get(ccr, hash).await {
2334 Some(content) => Ok(serde_json::json!({"found": true, "content": content})),
2335 None => Ok(serde_json::json!({"found": false})),
2336 }
2337 } else {
2338 Err("CCR not enabled".into())
2339 }
2340 },
2341 "aphrodite_compress" => {
2342 let content = params.get("content").and_then(|v| v.as_str()).ok_or("missing content")?;
2343 let center = params.get("_ccr_center").and_then(|v| v.as_str());
2344 let hash = compute_key(content.as_bytes());
2345 let size = content.len();
2346 if size < state.inline_ccr_threshold() {
2347 if let Ok(mut map) = state.inline_ccr.lock() {
2359 if map.contains(&hash) {
2360 state.inline_ccr_hits.fetch_add(1, Ordering::Relaxed);
2361 } else {
2362 state.inline_ccr_misses.fetch_add(1, Ordering::Relaxed);
2363 map.put(hash.clone(), content.to_string());
2364 }
2365 }
2366 if let Some(ccr) = &state.ccr {
2367 ccr_put(ccr, &hash, content).await;
2368 }
2369 Ok(serde_json::json!({
2370 "compressed": smart_marker(&hash, content, "compress", center),
2371 "hash": hash,
2372 "original_size": size
2373 }))
2374 } else if let Some(ccr) = &state.ccr {
2375 if !ccr_put(ccr, &hash, content).await {
2377 return Err("failed to store content in CCR backend".into());
2378 }
2379 let compressed = smart_marker(&hash, content, "compress", center);
2380 state
2383 .tokens_saved
2384 .fetch_add(size.saturating_sub(compressed.len()) as u64, Ordering::Relaxed);
2385 Ok(serde_json::json!({
2386 "compressed": compressed,
2387 "hash": hash,
2388 "original_size": size
2389 }))
2390 } else {
2391 Err("CCR not enabled".into())
2392 }
2393 },
2394 "aphrodite_list" => {
2395 let entries = match &state.ccr {
2396 Some(ccr) => ccr_len(ccr).await,
2397 None => 0,
2398 };
2399 Ok(serde_json::json!({
2400 "entries": entries,
2401 "backend": match state.mode {
2402 ProxyMode::Cache => "in_memory",
2403 ProxyMode::Token => "sqlite",
2404 },
2405 }))
2406 },
2407 _ => Err(format!("Unknown tool: {}", tool)),
2408 }
2409}
2410
2411pub async fn handle_ccr_create(
2419 State(state): State<Arc<AppState>>,
2420 headers: axum::http::HeaderMap,
2421 body: Bytes,
2422) -> impl IntoResponse {
2423 let content_type = headers.get("content-type").and_then(|v| v.to_str().ok()).unwrap_or("");
2424
2425 if content_type.contains("json") {
2427 match serde_json::from_slice::<CcrCreateRequest>(&body) {
2429 Ok(req) => {
2430 let original_size = req.content.len();
2431 let hash = req.key.unwrap_or_else(|| compute_key(req.content.as_bytes()));
2432
2433 let ccr = match &state.ccr {
2440 Some(ccr) => ccr,
2441 None => {
2442 return (
2443 StatusCode::SERVICE_UNAVAILABLE,
2444 Json(serde_json::json!({"error": "CCR not enabled"})),
2445 )
2446 .into_response();
2447 },
2448 };
2449 if !ccr_put(ccr, &hash, &req.content).await {
2450 return (
2451 StatusCode::INTERNAL_SERVER_ERROR,
2452 Json(serde_json::json!({"error": "failed to store content in CCR backend"})),
2453 )
2454 .into_response();
2455 }
2456 state.ccr_created.fetch_add(1, Ordering::Relaxed);
2457 state
2464 .tokens_saved
2465 .fetch_add(original_size.saturating_sub(hash.len()) as u64, Ordering::Relaxed);
2466 state.requests_compressed.fetch_add(1, Ordering::Relaxed);
2467
2468 let estimate = estimate_compressed_size(&req.content);
2483 state.update_compression_ratio(original_size, estimate);
2484
2485 if let Some(notify_url) = &state.notify_url {
2486 let notification = CcrNotification {
2487 event: "ccr_created".into(),
2488 hash: hash.clone(),
2489 created_at: std::time::SystemTime::now()
2490 .duration_since(std::time::UNIX_EPOCH)
2491 .unwrap_or_default()
2492 .as_secs(),
2493 ttl: req.ttl_seconds.unwrap_or(3600),
2494 tags: req.tags.unwrap_or_default(),
2495 };
2496 let tracker = state.task_tracker.clone();
2497 let client = state.client.clone();
2498 let url = notify_url.clone();
2499 let key = state.notify_key.clone();
2500 let state_clone = state.clone();
2501 tracker.spawn(async move {
2502 let mut req = client.post(&url).json(¬ification);
2503 if let Some(k) = &key {
2504 req = req.header("Authorization", format!("Bearer {k}"));
2505 }
2506 match req.timeout(Duration::from_secs(5)).send().await {
2507 Ok(r) if r.status().is_success() => {
2508 state_clone.notify_success.fetch_add(1, Ordering::Relaxed);
2509 },
2510 _ => {
2511 state_clone.notify_failure.fetch_add(1, Ordering::Relaxed);
2512 },
2513 }
2514 });
2515 }
2516
2517 let compressed_size = hash.len();
2518 Json(CcrCreateResponse {
2519 hash,
2520 token_savings_ratio: if original_size > 0 {
2521 original_size as f64 / compressed_size.max(1) as f64
2522 } else {
2523 1.0
2524 },
2525 original_size,
2526 compressed_size,
2527 marker_size: compressed_size,
2528 })
2529 .into_response()
2530 },
2531 Err(e) => (
2532 StatusCode::BAD_REQUEST,
2533 Json(serde_json::json!({"error": format!("invalid JSON: {}", e)})),
2534 )
2535 .into_response(),
2536 }
2537 } else {
2538 let content = match String::from_utf8(body.to_vec()) {
2540 Ok(c) => c,
2541 Err(_) => {
2542 return (
2543 StatusCode::BAD_REQUEST,
2544 Json(serde_json::json!({"error": "invalid UTF-8 in body"})),
2545 )
2546 .into_response();
2547 },
2548 };
2549 let original_size = content.len();
2550 let hash = compute_key(content.as_bytes());
2551
2552 let ccr = match &state.ccr {
2556 Some(ccr) => ccr,
2557 None => {
2558 return (
2559 StatusCode::SERVICE_UNAVAILABLE,
2560 Json(serde_json::json!({"error": "CCR not enabled"})),
2561 )
2562 .into_response();
2563 },
2564 };
2565 if !ccr_put(ccr, &hash, &content).await {
2566 return (
2567 StatusCode::INTERNAL_SERVER_ERROR,
2568 Json(serde_json::json!({"error": "failed to store content in CCR backend"})),
2569 )
2570 .into_response();
2571 }
2572 state.ccr_created.fetch_add(1, Ordering::Relaxed);
2573 state.requests_compressed.fetch_add(1, Ordering::Relaxed);
2574 state
2577 .tokens_saved
2578 .fetch_add(original_size.saturating_sub(hash.len()) as u64, Ordering::Relaxed);
2579
2580 let compressed_size = hash.len();
2581 Json(CcrCreateResponse {
2582 hash,
2583 token_savings_ratio: if original_size > 0 {
2584 original_size as f64 / compressed_size.max(1) as f64
2585 } else {
2586 1.0
2587 },
2588 original_size,
2589 compressed_size,
2590 marker_size: compressed_size,
2591 })
2592 .into_response()
2593 }
2594}
2595
2596pub async fn handle_ccr_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
2599 match &state.ccr {
2600 Some(ccr) => {
2601 let entries = ccr_len(ccr).await;
2602 Json(serde_json::json!({
2603 "entries": entries,
2604 "backend": match state.mode {
2605 ProxyMode::Cache => "in_memory",
2606 ProxyMode::Token => "sqlite",
2607 },
2608 "mode": match state.mode {
2609 ProxyMode::Cache => "cache",
2610 ProxyMode::Token => "token",
2611 },
2612 }))
2613 },
2614 None => Json(serde_json::json!({"entries": 0, "message": "CCR not enabled"})),
2615 }
2616}
2617
2618pub async fn handle_ccr_delete(
2621 State(state): State<Arc<AppState>>,
2622 axum::extract::Path(hash): axum::extract::Path<String>,
2623) -> impl IntoResponse {
2624 match &state.ccr {
2625 Some(ccr) => {
2626 let existed = ccr_del(ccr, &hash).await;
2627 if existed {
2628 (StatusCode::OK, Json(serde_json::json!({"deleted": true, "hash": hash})))
2629 } else {
2630 (
2631 StatusCode::NOT_FOUND,
2632 Json(serde_json::json!({"deleted": false, "hash": hash, "error": "not found"})),
2633 )
2634 }
2635 },
2636 None => (
2637 StatusCode::SERVICE_UNAVAILABLE,
2638 Json(serde_json::json!({"error": "CCR not enabled"})),
2639 ),
2640 }
2641}
2642
2643pub async fn handle_ccr_reload(State(state): State<Arc<AppState>>) -> impl IntoResponse {
2656 let config_path = std::env::var("APHRODITE_CONFIG_PATH").unwrap_or_else(|_| "aphrodite.toml".to_string());
2657 match crate::config::MultiConfig::load(&config_path) {
2658 Ok(config) => {
2659 let comp = config.compression.as_ref();
2660 let thresholds = resolve_thresholds(comp);
2661 state.cache_compress_threshold.store(thresholds.cache, Ordering::Relaxed);
2662 state.token_compress_threshold.store(thresholds.token, Ordering::Relaxed);
2663 state.inline_ccr_threshold.store(thresholds.inline, Ordering::Relaxed);
2664 state
2665 .code_multiplier_x100
2666 .store((thresholds.code_multiplier * 100.0) as u64, Ordering::Relaxed);
2667 let body = serde_json::json!({
2668 "reloaded": true,
2669 "applied": true,
2670 "config": config_path,
2671 "compression": {
2672 "tool_threshold_cache": thresholds.cache,
2673 "tool_threshold_token": thresholds.token,
2674 "inline_threshold": thresholds.inline,
2675 "code_multiplier": thresholds.code_multiplier,
2676 },
2677 "parsed_only": {
2680 "auto_expand": comp.and_then(|c| c.auto_expand),
2681 "auto_expand_limit": comp.and_then(|c| c.auto_expand_limit),
2682 "terminal_threshold": comp.and_then(|c| c.terminal_threshold),
2683 "engine_threshold_pct": comp.and_then(|c| c.engine_threshold_pct),
2684 "catalog_mode": comp.and_then(|c| c.catalog_mode.clone()),
2685 }
2686 });
2687 tracing::info!(
2688 %config_path,
2689 cache_threshold = thresholds.cache,
2690 token_threshold = thresholds.token,
2691 inline_threshold = thresholds.inline,
2692 code_multiplier = thresholds.code_multiplier,
2693 "config reloaded - compression thresholds applied"
2694 );
2695 (StatusCode::OK, Json(body)).into_response()
2696 },
2697 Err(e) => (
2698 StatusCode::INTERNAL_SERVER_ERROR,
2699 Json(serde_json::json!({"error": format!("failed to reload: {e}")})),
2700 )
2701 .into_response(),
2702 }
2703}
2704
2705pub async fn health_check(State(state): State<Arc<AppState>>) -> impl IntoResponse {
2712 let ccr_ok = state.ccr.is_some();
2713
2714 (
2715 StatusCode::OK,
2716 Json(serde_json::json!({
2717 "status": "healthy",
2718 "ccr": ccr_ok,
2719 "mode": match state.mode {
2720 ProxyMode::Cache => "cache",
2721 ProxyMode::Token => "token",
2722 },
2723 "version": env!("CARGO_PKG_VERSION"),
2724 "fill_pct": state.fill_pct.load(Ordering::Relaxed) as f64 / 100.0,
2725 })),
2726 )
2727 .into_response()
2728}
2729
2730#[cfg(test)]
2736pub(crate) mod tests {
2737 use super::*;
2738
2739 #[test]
2740 fn test_compress_threshold_cache() {
2741 use std::{collections::HashMap, sync::Mutex};
2742 let state = AppState {
2743 client: HttpClient::new(),
2744 stream_client: HttpClient::new(),
2745 api_url: "https://upstream-openai.com".into(),
2746 model: "test".into(),
2747 api_key: "test".into(),
2748 ccr: None,
2749 add_markers: false,
2750 mode: ProxyMode::Cache,
2751 tool_relay: false,
2752 notify_url: None,
2753 notify_key: None,
2754 dev: false,
2755 requests_total: AtomicU64::new(0),
2756 requests_compressed: AtomicU64::new(0),
2757 tokens_saved: AtomicU64::new(0),
2758 ccr_hits: AtomicU64::new(0),
2759 ccr_misses: AtomicU64::new(0),
2760 ccr_created: AtomicU64::new(0),
2761 tool_relay_calls: AtomicU64::new(0),
2762 compression_ratio_ema: AtomicU64::new(200), request_history: Mutex::new(VecDeque::new()),
2764 inline_ccr: Mutex::new(lru::LruCache::new(NonZeroUsize::new(1024).unwrap())),
2765 latency_buckets: [
2766 AtomicU64::new(0),
2767 AtomicU64::new(0),
2768 AtomicU64::new(0),
2769 AtomicU64::new(0),
2770 AtomicU64::new(0),
2771 ],
2772 total_latency_micros: AtomicU64::new(0),
2773 last_errors: Mutex::new(VecDeque::new()),
2774 compressions_by_type: Mutex::new(HashMap::new()),
2775 response_cache: Mutex::new(lru::LruCache::new(NonZeroUsize::new(128).unwrap())),
2776 response_cache_ttl: std::time::Duration::from_secs(3600),
2777 cache_hits: AtomicU64::new(0),
2778 cache_misses: AtomicU64::new(0),
2779 fill_pct: AtomicU64::new(9000),
2780 task_tracker: TaskTracker::new(),
2781 inline_ccr_hits: AtomicU64::new(0),
2782 inline_ccr_misses: AtomicU64::new(0),
2783 tool_relay_success: AtomicU64::new(0),
2784 tool_relay_failure: AtomicU64::new(0),
2785 notify_success: AtomicU64::new(0),
2786 notify_failure: AtomicU64::new(0),
2787 upstream_errors_4xx: AtomicU64::new(0),
2788 upstream_errors_5xx: AtomicU64::new(0),
2789 upstream_timeouts: AtomicU64::new(0),
2790 upstream_connect_errors: AtomicU64::new(0),
2791 sse_stream_errors: AtomicU64::new(0),
2792 ccr_store_entries: AtomicU64::new(0),
2793 ccr_store_bytes: AtomicU64::new(0),
2794 request_body_bytes: AtomicU64::new(0),
2795 response_body_bytes: AtomicU64::new(0),
2796 upstream_latency_micros: AtomicU64::new(0),
2797 upstream_health_cache: std::sync::Mutex::new(None),
2798 cache_compress_threshold: AtomicUsize::new(CACHE_COMPRESS_THRESHOLD),
2799 token_compress_threshold: AtomicUsize::new(TOKEN_COMPRESS_THRESHOLD),
2800 inline_ccr_threshold: AtomicUsize::new(INLINE_CCR_THRESHOLD),
2801 code_multiplier_x100: AtomicU64::new(300),
2802 };
2803 assert_eq!(state.compress_threshold(), CACHE_COMPRESS_THRESHOLD);
2804 }
2805
2806 #[test]
2807 fn test_compress_threshold_aphrodite() {
2808 let state = AppState { mode: ProxyMode::Token, ..test_state() };
2809 assert_eq!(state.compress_threshold(), TOKEN_COMPRESS_THRESHOLD);
2810 }
2811
2812 #[test]
2815 fn test_resolve_thresholds_toml_overrides_defaults() {
2816 let comp = CompressionConfig {
2817 engine_threshold_pct: None,
2818 engine_protect_first: None,
2819 engine_protect_last: None,
2820 engine_min_msgs: None,
2821 tool_threshold_token: Some(512),
2822 tool_threshold_cache: Some(4096),
2823 terminal_threshold: None,
2824 inline_threshold: Some(2048),
2825 auto_expand: None,
2826 auto_expand_limit: None,
2827 catalog_mode: None,
2828 classifier_poll: None,
2829 code_multiplier: Some(5.0),
2830 };
2831 let t = resolve_thresholds(Some(&comp));
2832 assert_eq!(t.cache, 4096);
2833 assert_eq!(t.token, 512);
2834 assert_eq!(t.inline, 2048);
2835 assert_eq!(t.code_multiplier, 5.0);
2836 }
2837
2838 #[test]
2839 fn test_resolve_thresholds_defaults_when_no_toml() {
2840 let t = resolve_thresholds(None);
2841 assert_eq!(t.cache, CACHE_COMPRESS_THRESHOLD);
2842 assert_eq!(t.token, TOKEN_COMPRESS_THRESHOLD);
2843 assert_eq!(t.inline, INLINE_CCR_THRESHOLD);
2844 assert_eq!(t.code_multiplier, 3.0);
2847 }
2848
2849 #[test]
2852 fn test_handle_ccr_reload_applies_thresholds_to_state() {
2853 let dir = std::env::temp_dir();
2854 let path = dir.join(format!(
2855 "aphrodite_reload_test_{}_{}.toml",
2856 std::process::id(),
2857 fnv1a_64(b"reload-test-salt")
2858 ));
2859 std::fs::write(
2860 &path,
2861 r#"
2862[[proxies]]
2863name = "token"
2864mode = "token"
2865
2866[compression]
2867tool_threshold_token = 999
2868tool_threshold_cache = 1234
2869inline_threshold = 77
2870code_multiplier = 6.5
2871"#,
2872 )
2873 .unwrap();
2874
2875 std::env::set_var("APHRODITE_CONFIG_PATH", &path);
2877 let state = std::sync::Arc::new(test_state());
2878 let rt = tokio::runtime::Runtime::new().unwrap();
2879 let resp = rt.block_on(handle_ccr_reload(State(state.clone()))).into_response();
2880 std::env::remove_var("APHRODITE_CONFIG_PATH");
2881 let _ = std::fs::remove_file(&path);
2882
2883 assert_eq!(resp.status(), axum::http::StatusCode::OK);
2884 assert_eq!(state.token_compress_threshold.load(Ordering::Relaxed), 999);
2885 assert_eq!(state.cache_compress_threshold.load(Ordering::Relaxed), 1234);
2886 assert_eq!(state.inline_ccr_threshold.load(Ordering::Relaxed), 77);
2887 assert_eq!(state.code_multiplier_x100.load(Ordering::Relaxed), 650);
2888 }
2889
2890 #[test]
2891 fn test_stats_json_modes() {
2892 let cache = test_state();
2893 let stats = cache.stats_json();
2894 assert_eq!(stats["mode"], "cache");
2895 assert_eq!(stats["proxy"], "aphrodite");
2896
2897 let mut aph = test_state();
2898 aph.mode = ProxyMode::Token;
2899 let stats = aph.stats_json();
2900 assert_eq!(stats["mode"], "token");
2901 }
2902
2903 #[test]
2907 fn test_stats_json_tool_relay_enabled_flag_not_shadowed() {
2908 let mut state = test_state();
2909 state.tool_relay = true;
2910 let stats = state.stats_json();
2911 assert_eq!(stats["tool_relay_enabled"], true);
2912 assert!(
2913 stats["tool_relay"].is_object(),
2914 "the calls-stats object must still be present under its own key"
2915 );
2916 assert!(stats["tool_relay"]["total"].is_u64());
2917 }
2918
2919 #[test]
2920 fn test_ccr_create_response_serde_shape() {
2921 let resp = CcrCreateResponse {
2924 hash: "abc123".into(),
2925 token_savings_ratio: 2.5,
2926 original_size: 100,
2927 compressed_size: 40,
2928 marker_size: 40,
2929 };
2930 let v = serde_json::to_value(&resp).unwrap();
2931 assert_eq!(v["hash"], "abc123");
2932 assert_eq!(v["original_size"], 100);
2933 assert_eq!(v["compressed_size"], 40);
2934 assert_eq!(v["marker_size"], 40);
2935 assert!((v["token_savings_ratio"].as_f64().unwrap() - 2.5).abs() < 0.01);
2936 assert!(v.get("compression_ratio").is_none());
2939 }
2940
2941 #[test]
2942 fn test_tool_relay_response_sync_serde_shape() {
2943 let resp = ToolRelayResponse {
2944 success: true,
2945 result: Some(serde_json::json!({"found": true})),
2946 error: None,
2947 async_call: false,
2948 };
2949 let v = serde_json::to_value(&resp).unwrap();
2950 assert_eq!(v["success"], true);
2951 assert_eq!(v["async_call"], false);
2952 assert_eq!(v["result"]["found"], true);
2953 assert!(v["error"].is_null());
2954 }
2955
2956 #[test]
2957 fn test_tool_relay_response_async_serde_shape() {
2958 let resp = ToolRelayResponse { success: true, result: None, error: None, async_call: true };
2959 let v = serde_json::to_value(&resp).unwrap();
2960 assert_eq!(v["async_call"], true);
2961 assert!(v["result"].is_null());
2962 }
2963
2964 #[test]
2966 fn test_detect_content_type_json_tool_output() {
2967 assert_eq!(proxy_detect_content_type(r#"{"exit_code": 0, "output": "ok"}"#), "tool_output");
2968 }
2969
2970 #[test]
2971 fn test_detect_content_type_invalid_json_is_text() {
2972 assert_eq!(proxy_detect_content_type("{ not json at all"), "text");
2974 }
2975
2976 #[test]
2977 fn test_detect_content_type_json_array() {
2978 assert_eq!(proxy_detect_content_type(r#"[{"a":1},{"a":2}]"#), "json");
2979 }
2980
2981 #[test]
2982 fn test_detect_content_type_rust_code() {
2983 let src = "use std::fmt;\nfn add(a:i32, b:i32) -> i32 {\n a + b\n}\n";
2984 assert_eq!(proxy_detect_content_type(src), "code_rust");
2985 }
2986
2987 #[test]
2988 fn test_detect_content_type_python_code() {
2989 let src = "import os\nclass Foo:\n def bar(self):\n pass\n";
2990 assert_eq!(proxy_detect_content_type(src), "code_python");
2991 }
2992
2993 #[test]
2994 fn test_detect_content_type_go_code() {
2995 let src = "package main\nimport (\n\t\"fmt\"\n)\nfunc main() {\n\tfmt.Println(\"hi\")\n}\n";
2996 assert_eq!(proxy_detect_content_type(src), "code_go");
2997 }
2998
2999 #[test]
3000 fn test_detect_content_type_js_code() {
3001 let src = "import { foo } from 'bar';\nexport const add = (a, b) => a + b;\nconst x = 1;\nconst y = 2;\n";
3002 assert_eq!(proxy_detect_content_type(src), "code_js");
3003 }
3004
3005 #[test]
3006 fn test_detect_content_type_error_first_line() {
3007 assert_eq!(
3008 proxy_detect_content_type("Traceback (most recent call last):\n File \"x.py\", line 1\nValueError: bad\n"),
3009 "error"
3010 );
3011 }
3012
3013 #[test]
3014 fn test_detect_content_type_diff() {
3015 let d = "diff --git a/src/lib.rs b/src/lib.rs\n--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1,3 +1,4 @@\n+added a \
3016 line\n";
3017 assert_eq!(proxy_detect_content_type(d), "diff");
3018 }
3019
3020 #[test]
3021 fn test_detect_content_type_log_lines() {
3022 let log = "starting up\n[INFO] service ready\n[WARN] disk low\n[ERROR] connection lost\n";
3024 assert_eq!(proxy_detect_content_type(log), "log");
3025 }
3026
3027 #[test]
3028 fn test_detect_content_type_empty_is_text() {
3029 assert_eq!(proxy_detect_content_type(""), "text");
3030 }
3031
3032 #[test]
3033 fn test_detect_content_type_plain_text() {
3034 assert_eq!(proxy_detect_content_type("just some plain text\nnothing special\n"), "text");
3035 }
3036
3037 #[test]
3039 fn test_generate_metadata_rust_has_lang_and_fns() {
3040 let src = "fn add(a:i32, b:i32) -> i32 {\n a + b\n}\n";
3041 let meta = generate_metadata(src, "code_rust");
3042 assert!(meta.contains("lang=rs"));
3043 assert!(meta.contains("fns=add"));
3044 }
3045
3046 #[test]
3047 fn test_generate_metadata_escapes_pipes() {
3048 let src = "On branch feature|weird\n";
3051 let meta = generate_metadata(src, "git");
3052 assert!(!meta.contains('|'), "metadata must not contain a raw pipe: {meta}");
3053 }
3054
3055 #[test]
3056 fn test_generate_metadata_max_400_chars() {
3057 let src = (0..100).map(|i| format!("fn f{i}() {{}}")).collect::<Vec<_>>().join("\n");
3058 let meta = generate_metadata(&src, "code_rust");
3059 assert!(meta.chars().count() <= 400, "metadata too long: {} chars", meta.chars().count());
3060 }
3061
3062 #[test]
3063 fn test_generate_metadata_error_branch_no_panic_on_multibyte_utf8() {
3064 let src = "日本語エラー at src/日本.rs:10:5 something\n";
3067 let meta = generate_metadata(src, "error");
3068 assert!(meta.is_empty() || meta.contains("trace=") || meta.contains("msg="));
3070 }
3071
3072 #[test]
3073 fn test_generate_metadata_text_has_line_count() {
3074 let meta = generate_metadata("a\nb\nc\n", "text");
3075 assert_eq!(meta, "ln=3");
3076 }
3077
3078 #[test]
3080 fn test_build_preview_code_has_ct_prefix() {
3081 let src = "fn add(a:i32, b:i32) -> i32 {\n a + b\n}\n";
3082 let preview = proxy_build_preview(src, "code_rust");
3083 assert!(preview.starts_with("[code_rust:"));
3084 }
3085
3086 #[test]
3091 fn test_proxy_and_hook_previews_are_identical_for_semantic_shapes() {
3092 let git_status = " M src/preview.rs\nA src/new.rs\nD src/old.rs\n?? tmp/x\n?? tmp/y";
3093 let cargo_test = "test result: ok. 220 passed; 0 failed; 1 ignored; finished in 0.31s";
3094 let ripgrep = "src/a.rs:12:hit one\nsrc/a.rs:20:hit two\nsrc/b.rs:5:hit three";
3095 let ls = "-rw-r--r-- 1 u g 10 x a.rs\n-rw-r--r-- 1 u g 10 x b.rs\ndrwxr-xr-x 2 u g 64 x sub";
3096 for content in [git_status, cargo_test, ripgrep, ls] {
3097 let ct = proxy_detect_content_type(content);
3099 let proxy_preview = proxy_build_preview(content, ct);
3100 let hook_preview = crate::preview::build_preview(ct, content);
3101 assert_eq!(proxy_preview, hook_preview, "preview drift for ct={ct} content={content:?}");
3102 assert!(proxy_preview.starts_with('['), "expected enriched preview, got {proxy_preview}");
3103 }
3104 }
3105
3106 #[test]
3107 fn test_build_preview_error_has_ct_prefix_via_error_line() {
3108 let src = "some noise\nerror[E0308]: mismatched types\nmore noise\n";
3109 let preview = proxy_build_preview(src, "error");
3110 assert!(preview.contains("error[E0308]"));
3111 }
3112
3113 #[test]
3114 fn test_build_preview_diff_has_ct_prefix() {
3115 let src = "diff --git a/x b/x\n--- a/x\n+++ a/x\n";
3116 let preview = proxy_build_preview(src, "diff");
3117 assert!(preview.starts_with("diff --git"));
3118 }
3119
3120 #[test]
3121 fn test_build_preview_json_has_ct_prefix() {
3122 let src = "{\"a\":1,\"b\":2}\n";
3123 let preview = proxy_build_preview(src, "json");
3124 assert!(preview.contains("keys"));
3125 }
3126
3127 #[test]
3129 fn test_cache_key_from_body_deterministic() {
3130 let body = br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}"#;
3131 let k1 = cache_key_from_body(body, "key-a");
3132 let k2 = cache_key_from_body(body, "key-a");
3133 assert!(k1.is_some());
3134 assert_eq!(k1, k2);
3135 }
3136
3137 #[test]
3138 fn test_cache_key_from_body_differs_by_api_key() {
3139 let body = br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}"#;
3140 let k1 = cache_key_from_body(body, "key-a");
3141 let k2 = cache_key_from_body(body, "key-b");
3142 assert_ne!(k1, k2);
3143 }
3144
3145 #[test]
3146 fn test_cache_key_from_body_none_on_junk() {
3147 assert_eq!(cache_key_from_body(b"not json", "key"), None);
3148 assert_eq!(cache_key_from_body(b"{}", "key"), None); }
3150
3151 #[test]
3155 fn test_cache_key_from_body_differs_by_tools_and_temperature_and_stream() {
3156 let base = br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}"#;
3157 let with_tools =
3158 br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"tools":[{"type":"function"}]}"#;
3159 let with_temp = br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"temperature":0.7}"#;
3160 let k_base = cache_key_from_body(base, "key");
3161 let k_tools = cache_key_from_body(with_tools, "key");
3162 let k_temp = cache_key_from_body(with_temp, "key");
3163 assert!(k_base.is_some());
3164 assert_ne!(k_base, k_tools, "differing `tools` must produce a different cache key");
3165 assert_ne!(k_base, k_temp, "differing `temperature` must produce a different cache key");
3166 assert_ne!(k_tools, k_temp);
3167 }
3168
3169 #[test]
3170 fn test_cache_key_from_body_none_when_streaming() {
3171 let streamed = br#"{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"stream":true}"#;
3172 assert_eq!(cache_key_from_body(streamed, "key"), None);
3173 }
3174
3175 #[test]
3178 fn test_response_cache_get_expires_past_ttl() {
3179 let mut state = test_state();
3180 state.response_cache_ttl = std::time::Duration::from_millis(1);
3181 state
3182 .response_cache
3183 .lock()
3184 .unwrap()
3185 .put(42, (std::time::Instant::now(), b"cached".to_vec()));
3186 std::thread::sleep(std::time::Duration::from_millis(20));
3187 assert_eq!(response_cache_get(&state, 42), None, "expired entry must not be returned");
3188 assert!(
3189 state.response_cache.lock().unwrap().peek(&42).is_none(),
3190 "expired entry must be evicted, not just skipped"
3191 );
3192 }
3193
3194 #[test]
3195 fn test_response_cache_get_hits_within_ttl() {
3196 let mut state = test_state();
3197 state.response_cache_ttl = std::time::Duration::from_secs(3600);
3198 state
3199 .response_cache
3200 .lock()
3201 .unwrap()
3202 .put(7, (std::time::Instant::now(), b"cached".to_vec()));
3203 assert_eq!(response_cache_get(&state, 7), Some(b"cached".to_vec()));
3204 }
3205
3206 #[test]
3207 fn test_fnv1a_64_known_vectors() {
3208 assert_eq!(fnv1a_64(b""), 14695981039346656037);
3210 assert_ne!(fnv1a_64(b"a"), fnv1a_64(b"b"));
3212 }
3213
3214 #[test]
3220 fn test_body_wants_stream_true_when_set() {
3221 assert!(body_wants_stream(br#"{"model":"gpt-4o","stream":true}"#));
3222 }
3223
3224 #[test]
3225 fn test_body_wants_stream_false_when_absent_or_false() {
3226 assert!(!body_wants_stream(br#"{"model":"gpt-4o"}"#));
3227 assert!(!body_wants_stream(br#"{"model":"gpt-4o","stream":false}"#));
3228 }
3229
3230 #[test]
3231 fn test_body_wants_stream_false_on_invalid_json() {
3232 assert!(!body_wants_stream(b"not json"));
3233 }
3234
3235 fn test_state() -> AppState {
3236 use std::{collections::HashMap, sync::Mutex};
3237 AppState {
3238 client: HttpClient::new(),
3239 stream_client: HttpClient::new(),
3240 api_url: "https://upstream-openai.com".into(),
3241 model: "default-model".into(),
3242 api_key: "test".into(),
3243 ccr: None,
3244 add_markers: false,
3245 mode: ProxyMode::Cache,
3246 tool_relay: false,
3247 notify_url: None,
3248 notify_key: None,
3249 dev: false,
3250 requests_total: AtomicU64::new(0),
3251 requests_compressed: AtomicU64::new(0),
3252 tokens_saved: AtomicU64::new(0),
3253 ccr_hits: AtomicU64::new(0),
3254 ccr_misses: AtomicU64::new(0),
3255 ccr_created: AtomicU64::new(0),
3256 tool_relay_calls: AtomicU64::new(0),
3257 compression_ratio_ema: AtomicU64::new(200), request_history: Mutex::new(VecDeque::new()),
3259 inline_ccr: Mutex::new(lru::LruCache::new(NonZeroUsize::new(1024).unwrap())),
3260 latency_buckets: [
3261 AtomicU64::new(0),
3262 AtomicU64::new(0),
3263 AtomicU64::new(0),
3264 AtomicU64::new(0),
3265 AtomicU64::new(0),
3266 ],
3267 total_latency_micros: AtomicU64::new(0),
3268 last_errors: Mutex::new(VecDeque::new()),
3269 compressions_by_type: Mutex::new(HashMap::new()),
3270 response_cache: Mutex::new(lru::LruCache::new(NonZeroUsize::new(128).unwrap())),
3271 response_cache_ttl: std::time::Duration::from_secs(3600),
3272 cache_hits: AtomicU64::new(0),
3273 cache_misses: AtomicU64::new(0),
3274 fill_pct: AtomicU64::new(9000),
3275 task_tracker: TaskTracker::new(),
3276 inline_ccr_hits: AtomicU64::new(0),
3277 inline_ccr_misses: AtomicU64::new(0),
3278 tool_relay_success: AtomicU64::new(0),
3279 tool_relay_failure: AtomicU64::new(0),
3280 notify_success: AtomicU64::new(0),
3281 notify_failure: AtomicU64::new(0),
3282 upstream_errors_4xx: AtomicU64::new(0),
3283 upstream_errors_5xx: AtomicU64::new(0),
3284 upstream_timeouts: AtomicU64::new(0),
3285 upstream_connect_errors: AtomicU64::new(0),
3286 sse_stream_errors: AtomicU64::new(0),
3287 ccr_store_entries: AtomicU64::new(0),
3288 ccr_store_bytes: AtomicU64::new(0),
3289 request_body_bytes: AtomicU64::new(0),
3290 response_body_bytes: AtomicU64::new(0),
3291 upstream_latency_micros: AtomicU64::new(0),
3292 upstream_health_cache: std::sync::Mutex::new(None),
3293 cache_compress_threshold: AtomicUsize::new(CACHE_COMPRESS_THRESHOLD),
3294 token_compress_threshold: AtomicUsize::new(TOKEN_COMPRESS_THRESHOLD),
3295 inline_ccr_threshold: AtomicUsize::new(INLINE_CCR_THRESHOLD),
3296 code_multiplier_x100: AtomicU64::new(300),
3297 }
3298 }
3299
3300 pub(crate) fn test_state_with_ccr() -> AppState {
3307 AppState {
3308 ccr: Some(std::sync::Arc::new(InMemoryCcrStore::with_capacity_and_ttl(
3309 1000,
3310 std::time::Duration::from_secs(300),
3311 ))),
3312 mode: ProxyMode::Token,
3313 ..test_state()
3314 }
3315 }
3316
3317 struct FailingCcrStore;
3321 impl headroom_core::ccr::CcrStore for FailingCcrStore {
3322 fn put(&self, _hash: &str, _payload: &str) -> bool {
3323 false
3324 }
3325 fn get(&self, _hash: &str) -> Option<String> {
3326 None
3327 }
3328 fn len(&self) -> usize {
3329 0
3330 }
3331 fn del(&self, _hash: &str) -> bool {
3332 false
3333 }
3334 }
3335
3336 fn test_state_with_failing_ccr() -> AppState {
3337 AppState {
3338 ccr: Some(std::sync::Arc::new(FailingCcrStore)),
3339 mode: ProxyMode::Token,
3340 ..test_state()
3341 }
3342 }
3343
3344 #[test]
3347 fn test_compress_chat_completion_ccr_put_failure_leaves_content_uncompressed() {
3348 let state = test_state_with_failing_ccr();
3349 let content = "fn answer() -> i32 { 42 }\n".repeat(200); let body = chat_completion_body(&content);
3351 let rt = tokio::runtime::Runtime::new().unwrap();
3352 let result = rt.block_on(compress_chat_completion(&state, &body, None));
3353 assert!(result.is_none(), "a failed ccr_put must not produce a compressed response");
3357 }
3358
3359 #[test]
3362 fn test_handle_ccr_create_503_when_ccr_disabled() {
3363 let mut state = test_state();
3364 state.ccr = None;
3365 let state = std::sync::Arc::new(state);
3366 let rt = tokio::runtime::Runtime::new().unwrap();
3367 let resp = rt
3368 .block_on(handle_ccr_create(
3369 State(state),
3370 axum::http::HeaderMap::new(),
3371 Bytes::from_static(b"hello world"),
3372 ))
3373 .into_response();
3374 assert_eq!(resp.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
3375 }
3376
3377 #[test]
3378 fn test_handle_ccr_create_500_when_put_fails() {
3379 let state = std::sync::Arc::new(test_state_with_failing_ccr());
3380 let rt = tokio::runtime::Runtime::new().unwrap();
3381 let resp = rt
3382 .block_on(handle_ccr_create(
3383 State(state),
3384 axum::http::HeaderMap::new(),
3385 Bytes::from_static(b"hello world"),
3386 ))
3387 .into_response();
3388 assert_eq!(resp.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
3389 }
3390
3391 fn chat_completion_body(content: &str) -> Vec<u8> {
3392 serde_json::json!({
3393 "choices": [{
3394 "message": {"role": "assistant", "content": content}
3395 }]
3396 })
3397 .to_string()
3398 .into_bytes()
3399 }
3400
3401 #[test]
3403 fn test_compress_chat_completion_above_threshold_produces_marker() {
3404 let content = "the quick brown fox jumps over the lazy dog. ".repeat(200);
3407 let body = chat_completion_body(&content);
3408 let state = test_state_with_ccr();
3409
3410 let result = tokio::runtime::Runtime::new()
3411 .unwrap()
3412 .block_on(compress_chat_completion(&state, &body, None));
3413
3414 let response = result.expect("content above threshold must be compressed");
3415 let new_content = response["choices"][0]["message"]["content"].as_str().unwrap();
3416 assert!(new_content.contains("<<<CCR:"), "expected a CCR marker, got: {new_content}");
3417
3418 let ccr = state.ccr.as_ref().unwrap().clone();
3420 let hash = compute_key(content.as_bytes());
3421 let stored = tokio::runtime::Runtime::new().unwrap().block_on(ccr_get(&ccr, &hash));
3422 assert_eq!(stored.as_deref(), Some(content.as_str()));
3423 }
3424
3425 #[test]
3426 fn test_compress_chat_completion_below_threshold_is_none() {
3427 let content = "short reply";
3428 let body = chat_completion_body(content);
3429 let state = test_state_with_ccr();
3430
3431 let result = tokio::runtime::Runtime::new()
3432 .unwrap()
3433 .block_on(compress_chat_completion(&state, &body, None));
3434
3435 assert!(result.is_none(), "short content must not be compressed: {result:?}");
3436 }
3437
3438 #[test]
3439 fn test_compress_chat_completion_tool_call_arguments_pass_through_untouched() {
3440 let big_args = serde_json::json!({"data": "x".repeat(4000)}).to_string();
3448 let big_content = "line of moderate length text content here.\n".repeat(200);
3449 let body = serde_json::json!({
3450 "choices": [{
3451 "message": {
3452 "role": "assistant",
3453 "content": big_content,
3454 "tool_calls": [{
3455 "function": {"name": "f", "arguments": big_args}
3456 }]
3457 }
3458 }]
3459 })
3460 .to_string()
3461 .into_bytes();
3462 let state = test_state_with_ccr();
3463
3464 let result = tokio::runtime::Runtime::new()
3465 .unwrap()
3466 .block_on(compress_chat_completion(&state, &body, None));
3467
3468 let response = result.expect("large message content must still be compressed");
3469 let new_content = response["choices"][0]["message"]["content"].as_str().unwrap();
3470 assert!(
3471 new_content.contains("<<<CCR:"),
3472 "expected message content to be compressed: {new_content}"
3473 );
3474 let new_args = response["choices"][0]["message"]["tool_calls"][0]["function"]["arguments"]
3475 .as_str()
3476 .unwrap();
3477 assert_eq!(
3478 new_args, big_args,
3479 "tool-call arguments must pass through untouched, never marker-replaced"
3480 );
3481 }
3482
3483 #[test]
3484 fn test_compress_chat_completion_budget_header_lowers_effective_threshold() {
3485 let content = "line of moderate length text content here.\n".repeat(40); let body = chat_completion_body(&content);
3491
3492 let state_no_budget = test_state_with_ccr();
3493 let result_no_budget =
3494 tokio::runtime::Runtime::new()
3495 .unwrap()
3496 .block_on(compress_chat_completion(&state_no_budget, &body, None));
3497
3498 let state_low_budget = test_state_with_ccr();
3499 let result_low_budget = tokio::runtime::Runtime::new().unwrap().block_on(compress_chat_completion(
3500 &state_low_budget,
3501 &body,
3502 Some("0"),
3503 ));
3504
3505 if result_no_budget.is_some() {
3507 assert!(
3508 result_low_budget.is_some(),
3509 "lower budget must compress at least as much as no budget"
3510 );
3511 }
3512 }
3513
3514 #[test]
3516 fn test_execute_tool_relay_retrieve_missing_hash_param() {
3517 let state = test_state_with_ccr();
3518 let result = tokio::runtime::Runtime::new().unwrap().block_on(execute_tool_relay(
3519 &state,
3520 "aphrodite_retrieve",
3521 &serde_json::json!({}),
3522 ));
3523 assert_eq!(result, Err("missing hash".to_string()));
3524 }
3525
3526 #[test]
3527 fn test_execute_tool_relay_unknown_tool_is_err() {
3528 let state = test_state_with_ccr();
3529 let result = tokio::runtime::Runtime::new().unwrap().block_on(execute_tool_relay(
3530 &state,
3531 "not_a_real_tool",
3532 &serde_json::json!({}),
3533 ));
3534 assert!(result.is_err());
3535 }
3536
3537 #[test]
3538 fn test_execute_tool_relay_compress_small_content_stores_inline_and_returns_marker() {
3539 let state = test_state_with_ccr();
3540 let content = "tiny"; let result = tokio::runtime::Runtime::new().unwrap().block_on(execute_tool_relay(
3542 &state,
3543 "aphrodite_compress",
3544 &serde_json::json!({"content": content}),
3545 ));
3546 let v = result.expect("compress must succeed");
3547 assert!(v["compressed"].as_str().unwrap().contains("<<<CCR:"));
3548 assert_eq!(v["original_size"], content.len());
3549 }
3550
3551 #[test]
3557 fn test_execute_tool_relay_compress_small_content_also_stores_durably() {
3558 let state = test_state_with_ccr();
3559 let content = "tiny"; let rt = tokio::runtime::Runtime::new().unwrap();
3561 let result = rt.block_on(execute_tool_relay(
3562 &state,
3563 "aphrodite_compress",
3564 &serde_json::json!({"content": content}),
3565 ));
3566 let v = result.expect("compress must succeed");
3567 let hash = v["hash"].as_str().unwrap().to_string();
3568
3569 let ccr = state.ccr.as_ref().unwrap();
3572 let durable = rt.block_on(ccr_get(ccr, &hash));
3573 assert_eq!(
3574 durable.as_deref(),
3575 Some(content),
3576 "tiny content must also be durable, not inline-only"
3577 );
3578 }
3579
3580 #[test]
3581 fn test_execute_tool_relay_retrieve_finds_inline_entry() {
3582 let state = test_state_with_ccr();
3583 let content = "tiny";
3584 let compressed = tokio::runtime::Runtime::new()
3585 .unwrap()
3586 .block_on(execute_tool_relay(
3587 &state,
3588 "aphrodite_compress",
3589 &serde_json::json!({"content": content}),
3590 ))
3591 .unwrap();
3592 let hash = compressed["hash"].as_str().unwrap().to_string();
3593
3594 let retrieved = tokio::runtime::Runtime::new()
3595 .unwrap()
3596 .block_on(execute_tool_relay(
3597 &state,
3598 "aphrodite_retrieve",
3599 &serde_json::json!({"hash": hash}),
3600 ))
3601 .unwrap();
3602 assert_eq!(retrieved["found"], true);
3603 assert_eq!(retrieved["content"], content);
3604 }
3605
3606 #[test]
3611 fn test_execute_tool_relay_retrieve_normalizes_pipe_suffixed_and_whitespace_hash() {
3612 let state = test_state_with_ccr();
3613 let content = "tiny";
3614 let rt = tokio::runtime::Runtime::new().unwrap();
3615 let compressed = rt
3616 .block_on(execute_tool_relay(
3617 &state,
3618 "aphrodite_compress",
3619 &serde_json::json!({"content": content}),
3620 ))
3621 .unwrap();
3622 let hash = compressed["hash"].as_str().unwrap().to_string();
3623
3624 for hash_arg in [hash.clone(), format!("{hash}|tool|1024"), format!(" {hash} ")] {
3625 let retrieved = rt
3626 .block_on(execute_tool_relay(
3627 &state,
3628 "aphrodite_retrieve",
3629 &serde_json::json!({"hash": hash_arg}),
3630 ))
3631 .unwrap();
3632 assert_eq!(retrieved["found"], true, "hash arg {hash_arg:?} must resolve: {retrieved:?}");
3633 assert_eq!(retrieved["content"], content);
3634 }
3635 }
3636
3637 #[test]
3645 fn regression_07_tokens_saved_increments_on_compress() {
3646 let content = "the quick brown fox jumps over the lazy dog. ".repeat(200);
3647 let body = chat_completion_body(&content);
3648 let state = test_state_with_ccr();
3649
3650 assert_eq!(state.tokens_saved.load(Ordering::Relaxed), 0);
3651 let result = tokio::runtime::Runtime::new()
3652 .unwrap()
3653 .block_on(compress_chat_completion(&state, &body, None));
3654 assert!(result.is_some(), "content above threshold must compress");
3655 assert!(
3656 state.tokens_saved.load(Ordering::Relaxed) > 0,
3657 "tokens_saved must be incremented by the real compression path"
3658 );
3659 }
3660
3661 #[test]
3664 fn regression_11_below_threshold_skips_compression_and_counter() {
3665 let content = "short reply below any threshold";
3666 let body = chat_completion_body(content);
3667 let state = test_state_with_ccr();
3668
3669 let result = tokio::runtime::Runtime::new()
3670 .unwrap()
3671 .block_on(compress_chat_completion(&state, &body, None));
3672 assert!(result.is_none(), "below-threshold content must not compress");
3673 assert_eq!(
3674 state.tokens_saved.load(Ordering::Relaxed),
3675 0,
3676 "no savings should be recorded when nothing was compressed"
3677 );
3678 }
3679
3680 #[test]
3685 fn regression_13_marker_terminator_never_truncated() {
3686 let hash = "abc123def456abc123def456abc123def456";
3687 let huge_preview = "x".repeat(10_000);
3688 let huge_metadata = "y".repeat(10_000);
3689 let out = proxy_format_ccr_output(&huge_preview, "text", &huge_metadata, None, hash, 123456);
3690 let expected_terminator = format!("<<<CCR:{hash}|text|123456>>>");
3691 assert!(
3692 out.contains(&expected_terminator),
3693 "marker terminator must always be complete and unsliced, regardless of preview/metadata length"
3694 );
3695 }
3696
3697 use proptest::{prop_assert, proptest};
3699
3700 proptest! {
3701 #[test]
3706 fn prop_classifier_and_metadata_never_panic(s in ".*") {
3707 let ct = proxy_detect_content_type(&s);
3708 let meta = generate_metadata(&s, ct);
3709 prop_assert!(!meta.contains('|'));
3710 prop_assert!(!meta.contains('\n'));
3711 prop_assert!(meta.chars().count() <= 400);
3712 }
3713 }
3714
3715 fn header_value(s: &str) -> axum::http::HeaderValue {
3718 axum::http::HeaderValue::from_str(s).unwrap()
3719 }
3720
3721 #[test]
3722 fn test_is_sse_exact_match() {
3723 assert!(is_sse(Some(&header_value("text/event-stream"))));
3724 }
3725
3726 #[test]
3727 fn test_is_sse_prefix_match_with_charset() {
3728 assert!(is_sse(Some(&header_value("text/event-stream; charset=utf-8"))));
3729 }
3730
3731 #[test]
3732 fn test_is_sse_rejects_non_sse_content_types() {
3733 assert!(!is_sse(Some(&header_value("application/json"))));
3734 assert!(!is_sse(Some(&header_value("text/plain"))));
3735 assert!(!is_sse(Some(&header_value("text/x-event-stream"))));
3737 }
3738
3739 #[test]
3740 fn test_is_sse_missing_header_is_false() {
3741 assert!(!is_sse(None));
3742 }
3743
3744 async fn spawn_mock_upstream(response: &'static str) -> std::net::SocketAddr {
3749 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3750 let addr = listener.local_addr().unwrap();
3751 tokio::spawn(async move {
3752 use tokio::io::{AsyncReadExt, AsyncWriteExt};
3753 let (mut socket, _) = listener.accept().await.unwrap();
3754 let mut buf = [0u8; 4096];
3755 loop {
3757 let n = socket.read(&mut buf).await.unwrap_or(0);
3758 if n == 0 || buf[..n].windows(4).any(|w| w == b"\r\n\r\n") {
3759 break;
3760 }
3761 }
3762 let _ = socket.write_all(response.as_bytes()).await;
3763 let _ = socket.shutdown().await;
3764 });
3765 addr
3766 }
3767
3768 fn test_request(
3769 state: Arc<AppState>,
3770 path: &str,
3771 body: &'static str,
3772 ) -> (
3773 axum::extract::State<Arc<AppState>>,
3774 Method,
3775 axum::extract::OriginalUri,
3776 axum::http::HeaderMap,
3777 Bytes,
3778 ) {
3779 (
3780 axum::extract::State(state),
3781 Method::POST,
3782 axum::extract::OriginalUri(format!("http://x{path}").parse().unwrap()),
3783 axum::http::HeaderMap::new(),
3784 Bytes::from_static(body.as_bytes()),
3785 )
3786 }
3787
3788 #[tokio::test]
3789 async fn test_sse_response_is_streamed_with_header_and_not_cached() {
3790 let addr = spawn_mock_upstream(
3791 "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\nConnection: close\r\n\r\ndata: \
3792 {\"delta\":\"hi\"}\n\ndata: [DONE]\n\n",
3793 )
3794 .await;
3795
3796 let mut state = test_state();
3797 state.api_url = format!("http://{addr}");
3798 let state = Arc::new(state);
3799
3800 let (s, m, p, h, b) = test_request(state.clone(), CHAT_COMPLETIONS_PATH, r#"{"model":"x","messages":[]}"#);
3803 let response = proxy_handler(s, m, p, h, b).await.into_response();
3804
3805 assert_eq!(
3806 response.headers().get("X-Aphrodite-Streamed").map(|v| v.to_str().unwrap()),
3807 Some("true"),
3808 "SSE responses must be marked with X-Aphrodite-Streamed"
3809 );
3810 assert_eq!(
3811 response.headers().get("content-type").map(|v| v.to_str().unwrap()),
3812 Some("text/event-stream")
3813 );
3814
3815 let cache_key = cache_key_from_body(r#"{"model":"x","messages":[]}"#.as_bytes(), state.api_key.expose());
3819 if let Some(ck) = cache_key {
3820 assert!(response_cache_get(&state, ck).is_none(), "SSE response must not be cached");
3821 }
3822 }
3823
3824 #[tokio::test]
3825 async fn test_non_sse_response_is_not_streamed() {
3826 let addr = spawn_mock_upstream(
3827 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\nConnection: close\r\n\r\n{\"ok\":true}",
3828 )
3829 .await;
3830
3831 let mut state = test_state();
3832 state.api_url = format!("http://{addr}");
3833 let (s, m, p, h, b) = test_request(Arc::new(state), "/v1/models", "");
3834 let response = proxy_handler(s, m, p, h, b).await.into_response();
3835
3836 assert!(
3837 response.headers().get("X-Aphrodite-Streamed").is_none(),
3838 "a plain JSON upstream response must not be marked as streamed"
3839 );
3840 }
3841}