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 predictor = config
119 .routing
120 .as_ref()
121 .and_then(|r| r.escalation.predictor.as_ref())
122 .map(|pc| {
123 let mut p = firstpass_core::PassPredictor::new(pc.lr, pc.l2);
124 if let Ok(traces_history) = store::load_all_traces(&config.db_path) {
125 let mut n = 0usize;
126 for trace in &traces_history {
127 for attempt in &trace.attempts {
128 match attempt.verdict {
129 firstpass_core::Verdict::Pass => {
130 p.update(&trace.request.features, attempt.rung, true);
131 n += 1;
132 }
133 firstpass_core::Verdict::Fail => {
134 p.update(&trace.request.features, attempt.rung, false);
135 n += 1;
136 }
137 firstpass_core::Verdict::Abstain => {}
138 }
139 }
140 }
141 tracing::info!(examples = n, "predictor warm-started from trace store");
142 }
143 Arc::new(std::sync::Mutex::new(p))
144 });
145
146 let state = AppState {
147 config: Arc::new(config),
148 http: reqwest::Client::builder()
152 .connect_timeout(std::time::Duration::from_secs(10))
153 .build()?,
154 providers,
155 gate_health: Arc::new(gate_health),
156 shadow_ledger: Arc::new(crate::shadow::ShadowLedger::new()),
157 guardrails: Arc::new(crate::guard::GuardrailRegistry::new()),
158 traces,
159 adaptive,
160 bandit,
161 predictor,
162 tenant_rate_limiter,
163 spill,
164 };
165
166 let listener = tokio::net::TcpListener::bind(&bind).await?;
167 tracing::info!(%bind, "firstpass listening");
168 tracing::info!("offboard: unset ANTHROPIC_BASE_URL");
169
170 axum::serve(listener, app(state)?)
171 .with_graceful_shutdown(shutdown_signal())
172 .await?;
173
174 drop(writer);
177 Ok(())
178}
179
180async fn shutdown_signal() {
181 if let Err(err) = tokio::signal::ctrl_c().await {
182 tracing::warn!(%err, "failed to install Ctrl-C handler; shutting down anyway");
183 }
184}