use std::sync::Arc;
use crate::bandit::{ContextBucket, StartRungBandit};
use crate::gate::GateHealthRegistry;
use crate::provider::ProviderRegistry;
use crate::{AppState, ProxyConfig, app, store};
pub fn init_tracing() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
}
#[must_use]
pub fn build_gate_health(config: &ProxyConfig) -> GateHealthRegistry {
let mut gate_health = GateHealthRegistry::new();
if let Some(routing) = config.routing.as_ref() {
let mut seen = std::collections::HashSet::new();
for route in &routing.routes {
for gate in route.gates.iter().chain(&route.deferred_gates) {
if seen.insert(gate.clone()) {
gate_health = gate_health.with_budget(gate.clone(), 50, 0.25);
}
}
}
}
gate_health
}
pub async fn serve(config: ProxyConfig) -> Result<(), Box<dyn std::error::Error>> {
let (traces, spill, writer) = store::open_with_receipts(&config.db_path, config.receipts_mode)?;
let bind = config.bind.clone();
let provider_defs = config
.routing
.as_ref()
.map(|r| r.providers.as_slice())
.unwrap_or_default();
let providers = ProviderRegistry::from_config(
provider_defs,
&config.upstream_anthropic,
&config.upstream_openai,
);
let gate_health = build_gate_health(&config);
let adaptive = config
.routing
.as_ref()
.and_then(|r| r.escalation.adaptive.as_ref())
.map(|a| {
let init = config
.routing
.as_ref()
.and_then(|r| r.escalation.serve_threshold)
.unwrap_or(0.5);
Arc::new(std::sync::Mutex::new(
firstpass_core::conformal::AdaptiveConformal::new(a.alpha, a.gamma, init),
))
});
let tenant_rate_limiter = crate::proxy::build_tenant_rate_limiter(&config);
let bandit = config
.routing
.as_ref()
.and_then(|r| r.escalation.bandit.as_ref())
.map(|bc| {
let algorithm = match bc.algorithm {
firstpass_core::BanditAlgorithm::Ucb1 => crate::bandit::Algorithm::Ucb1,
firstpass_core::BanditAlgorithm::Thompson => crate::bandit::Algorithm::Thompson,
};
let seed = uuid::Uuid::now_v7().as_u128() as u64;
let mut b = StartRungBandit::with_algorithm(
bc.min_observations,
bc.exploration,
algorithm,
bc.discount,
seed,
);
if let Ok(traces_history) = store::load_all_traces(&config.db_path) {
for trace in &traces_history {
let ctx = ContextBucket::from_features(&trace.request.features);
b.feed_trace_attempts(&ctx, &trace.attempts);
}
tracing::info!(
n = traces_history.len(),
"bandit warm-started from trace store"
);
}
Arc::new(std::sync::Mutex::new(b))
});
let state = AppState {
config: Arc::new(config),
http: reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(10))
.build()?,
providers,
gate_health: Arc::new(gate_health),
traces,
adaptive,
bandit,
tenant_rate_limiter,
spill,
};
let listener = tokio::net::TcpListener::bind(&bind).await?;
tracing::info!(%bind, "firstpass listening");
tracing::info!("offboard: unset ANTHROPIC_BASE_URL");
axum::serve(listener, app(state)?)
.with_graceful_shutdown(shutdown_signal())
.await?;
drop(writer);
Ok(())
}
async fn shutdown_signal() {
if let Err(err) = tokio::signal::ctrl_c().await {
tracing::warn!(%err, "failed to install Ctrl-C handler; shutting down anyway");
}
}