1use std::sync::Arc;
6
7use crate::bandit::{ContextBucket, StartRungBandit};
8use crate::gate::GateHealthRegistry;
9use crate::provider::ProviderRegistry;
10use crate::{AppState, ProxyConfig, app, store};
11
12pub fn init_tracing() {
15 tracing_subscriber::fmt()
16 .with_env_filter(
17 tracing_subscriber::EnvFilter::try_from_default_env()
18 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
19 )
20 .init();
21}
22
23#[must_use]
26pub fn build_gate_health(config: &ProxyConfig) -> GateHealthRegistry {
27 let mut gate_health = GateHealthRegistry::new();
28 if let Some(routing) = config.routing.as_ref() {
29 let mut seen = std::collections::HashSet::new();
30 for route in &routing.routes {
31 for gate in route.gates.iter().chain(&route.deferred_gates) {
32 if seen.insert(gate.clone()) {
33 gate_health = gate_health.with_budget(gate.clone(), 50, 0.25);
34 }
35 }
36 }
37 }
38 gate_health
39}
40
41pub async fn serve(config: ProxyConfig) -> Result<(), Box<dyn std::error::Error>> {
47 let (traces, spill, writer) = store::open_with_receipts(&config.db_path, config.receipts_mode)?;
48 let bind = config.bind.clone();
49 let provider_defs = config
52 .routing
53 .as_ref()
54 .map(|r| r.providers.as_slice())
55 .unwrap_or_default();
56 let providers = ProviderRegistry::from_config(
57 provider_defs,
58 &config.upstream_anthropic,
59 &config.upstream_openai,
60 );
61 let gate_health = build_gate_health(&config);
62 let adaptive = config
65 .routing
66 .as_ref()
67 .and_then(|r| r.escalation.adaptive.as_ref())
68 .map(|a| {
69 let init = config
70 .routing
71 .as_ref()
72 .and_then(|r| r.escalation.serve_threshold)
73 .unwrap_or(0.5);
74 Arc::new(std::sync::Mutex::new(
75 firstpass_core::conformal::AdaptiveConformal::new(a.alpha, a.gamma, init),
76 ))
77 });
78 let tenant_rate_limiter = crate::proxy::build_tenant_rate_limiter(&config);
79
80 let bandit = config
85 .routing
86 .as_ref()
87 .and_then(|r| r.escalation.bandit.as_ref())
88 .map(|bc| {
89 let algorithm = match bc.algorithm {
90 firstpass_core::BanditAlgorithm::Ucb1 => crate::bandit::Algorithm::Ucb1,
91 firstpass_core::BanditAlgorithm::Thompson => crate::bandit::Algorithm::Thompson,
92 };
93 let seed = uuid::Uuid::now_v7().as_u128() as u64;
95 let mut b = StartRungBandit::with_algorithm(
96 bc.min_observations,
97 bc.exploration,
98 algorithm,
99 bc.discount,
100 seed,
101 );
102 if let Ok(traces_history) = store::load_all_traces(&config.db_path) {
103 for trace in &traces_history {
104 let ctx = ContextBucket::from_features(&trace.request.features);
105 b.feed_trace_attempts(&ctx, &trace.attempts);
106 }
107 tracing::info!(
108 n = traces_history.len(),
109 "bandit warm-started from trace store"
110 );
111 }
112 Arc::new(std::sync::Mutex::new(b))
113 });
114
115 let state = AppState {
116 config: Arc::new(config),
117 http: reqwest::Client::builder()
121 .connect_timeout(std::time::Duration::from_secs(10))
122 .build()?,
123 providers,
124 gate_health: Arc::new(gate_health),
125 traces,
126 adaptive,
127 bandit,
128 tenant_rate_limiter,
129 spill,
130 };
131
132 let listener = tokio::net::TcpListener::bind(&bind).await?;
133 tracing::info!(%bind, "firstpass listening");
134 tracing::info!("offboard: unset ANTHROPIC_BASE_URL");
135
136 axum::serve(listener, app(state)?)
137 .with_graceful_shutdown(shutdown_signal())
138 .await?;
139
140 drop(writer);
143 Ok(())
144}
145
146async fn shutdown_signal() {
147 if let Err(err) = tokio::signal::ctrl_c().await {
148 tracing::warn!(%err, "failed to install Ctrl-C handler; shutting down anyway");
149 }
150}