Skip to main content

bitrouter_runtime/
server.rs

1use std::sync::Arc;
2
3use bitrouter_api::router::{anthropic, openai};
4use bitrouter_config::BitrouterConfig;
5use bitrouter_core::routers::{model_router::LanguageModelRouter, routing_table::RoutingTable};
6use warp::Filter;
7
8use crate::error::Result;
9
10/// A stub model router that rejects all requests with a descriptive error.
11///
12/// Used when the server starts without a real provider-backed router. Health
13/// checks and other non-model endpoints still work; only model API requests
14/// will return an error.
15pub struct StubModelRouter;
16
17impl LanguageModelRouter for StubModelRouter {
18    async fn route_model(
19        &self,
20        _target: bitrouter_core::routers::routing_table::RoutingTarget,
21    ) -> bitrouter_core::errors::Result<
22        Box<bitrouter_core::models::language::language_model::DynLanguageModel<'static>>,
23    > {
24        Err(bitrouter_core::errors::BitrouterError::unsupported(
25            "runtime",
26            "model routing",
27            Some("no model router configured — configure providers to enable API endpoints".into()),
28        ))
29    }
30}
31
32pub struct ServerPlan<T, R> {
33    config: BitrouterConfig,
34    table: Arc<T>,
35    router: Arc<R>,
36}
37
38impl<T, R> ServerPlan<T, R>
39where
40    T: RoutingTable + Send + Sync + 'static,
41    R: LanguageModelRouter + Send + Sync + 'static,
42{
43    pub fn new(config: BitrouterConfig, table: Arc<T>, router: Arc<R>) -> Self {
44        Self {
45            config,
46            table,
47            router,
48        }
49    }
50
51    pub async fn serve(self) -> Result<()> {
52        let addr = self.config.server.listen;
53
54        let health = warp::path("health")
55            .and(warp::get())
56            .map(|| warp::reply::json(&serde_json::json!({ "status": "ok" })));
57
58        let chat =
59            openai::chat::filters::chat_completions_filter(self.table.clone(), self.router.clone());
60        let messages =
61            anthropic::messages::filters::messages_filter(self.table.clone(), self.router.clone());
62        let responses =
63            openai::responses::filters::responses_filter(self.table.clone(), self.router.clone());
64
65        let routes = health
66            .or(chat)
67            .or(messages)
68            .or(responses)
69            .recover(openai::chat::filters::rejection_handler)
70            .with(warp::trace::request());
71
72        let server = warp::serve(routes)
73            .bind(addr)
74            .await
75            .graceful(shutdown_signal());
76
77        tracing::info!(%addr, "server listening");
78        server.run().await;
79        tracing::info!("server stopped");
80
81        Ok(())
82    }
83}
84
85async fn shutdown_signal() {
86    let ctrl_c = tokio::signal::ctrl_c();
87
88    #[cfg(unix)]
89    {
90        let mut term =
91            tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap();
92        tokio::select! {
93            _ = ctrl_c => {}
94            _ = term.recv() => {}
95        }
96    }
97
98    #[cfg(not(unix))]
99    {
100        ctrl_c.await.ok();
101    }
102
103    tracing::info!("shutdown signal received");
104}