Skip to main content

codex_helper_core/
runtime_host.rs

1use std::collections::HashMap;
2use std::net::{IpAddr, SocketAddr};
3use std::sync::{Arc, Mutex};
4
5use anyhow::Result;
6use axum::Router;
7use reqwest::Client;
8use tokio::sync::watch;
9use tokio::task::JoinHandle;
10
11use crate::config::{
12    LoadedProxyConfig, ProxyConfig, ServiceKind, load_or_bootstrap_for_service_with_v4_source,
13    model_routing_warnings,
14};
15use crate::lb::LbState;
16use crate::proxy::{
17    ProxyService, admin_listener_router, admin_loopback_addr_for_proxy_port, local_proxy_base_url,
18    proxy_only_router_with_admin_base_url,
19};
20use crate::state::ProxyState;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ProxyLifetimeMode {
24    Ephemeral,
25    Resident,
26}
27
28impl ProxyLifetimeMode {
29    pub fn is_resident(self) -> bool {
30        matches!(self, Self::Resident)
31    }
32}
33
34pub struct ProxyRuntime {
35    pub service_name: &'static str,
36    pub host: IpAddr,
37    pub port: u16,
38    pub admin_addr: SocketAddr,
39    pub config: Arc<ProxyConfig>,
40    pub proxy: ProxyService,
41    pub state: Arc<ProxyState>,
42    pub shutdown_tx: watch::Sender<bool>,
43    shutdown_rx: watch::Receiver<bool>,
44    listener: Option<tokio::net::TcpListener>,
45    admin_listener: Option<tokio::net::TcpListener>,
46    app: Option<Router>,
47    admin_app: Option<Router>,
48}
49
50pub struct RunningProxyRuntime {
51    pub service_name: &'static str,
52    pub host: IpAddr,
53    pub port: u16,
54    pub admin_addr: SocketAddr,
55    pub config: Arc<ProxyConfig>,
56    pub proxy: ProxyService,
57    pub state: Arc<ProxyState>,
58    pub shutdown_tx: watch::Sender<bool>,
59    pub server_handle: JoinHandle<Result<()>>,
60}
61
62impl ProxyRuntime {
63    pub fn shutdown_receiver(&self) -> watch::Receiver<bool> {
64        self.shutdown_rx.clone()
65    }
66
67    pub fn start(mut self) -> RunningProxyRuntime {
68        let listener = self
69            .listener
70            .take()
71            .expect("proxy runtime listener should exist before start");
72        let admin_listener = self
73            .admin_listener
74            .take()
75            .expect("proxy runtime admin listener should exist before start");
76        let app = self
77            .app
78            .take()
79            .expect("proxy runtime app should exist before start");
80        let admin_app = self
81            .admin_app
82            .take()
83            .expect("proxy runtime admin app should exist before start");
84        let shutdown_rx = self.shutdown_rx.clone();
85        let server_handle =
86            spawn_proxy_runtime_servers(listener, admin_listener, app, admin_app, shutdown_rx);
87
88        RunningProxyRuntime {
89            service_name: self.service_name,
90            host: self.host,
91            port: self.port,
92            admin_addr: self.admin_addr,
93            config: self.config,
94            proxy: self.proxy,
95            state: self.state,
96            shutdown_tx: self.shutdown_tx,
97            server_handle,
98        }
99    }
100}
101
102impl RunningProxyRuntime {
103    pub async fn wait(self) -> Result<()> {
104        self.server_handle
105            .await
106            .map_err(|error| anyhow::anyhow!("server task join error: {error}"))?
107    }
108}
109
110pub async fn build_proxy_runtime(
111    service_kind: ServiceKind,
112    host: IpAddr,
113    port: u16,
114) -> Result<ProxyRuntime> {
115    let service_name = service_name_for_kind(service_kind);
116    let loaded = load_or_bootstrap_for_service_with_v4_source(service_kind).await?;
117    build_proxy_runtime_from_loaded(service_name, host, port, loaded).await
118}
119
120pub async fn build_proxy_runtime_from_loaded(
121    service_name: &'static str,
122    host: IpAddr,
123    port: u16,
124    loaded: LoadedProxyConfig,
125) -> Result<ProxyRuntime> {
126    let addr: SocketAddr = SocketAddr::from((host, port));
127    let admin_addr = admin_loopback_addr_for_proxy_port(port);
128    let listener = tokio::net::TcpListener::bind(addr).await?;
129    let admin_listener = tokio::net::TcpListener::bind(admin_addr).await?;
130    build_proxy_runtime_from_bound_listeners(
131        service_name,
132        host,
133        port,
134        loaded,
135        listener,
136        admin_listener,
137    )
138    .await
139}
140
141pub async fn build_proxy_runtime_from_bound_listeners(
142    service_name: &'static str,
143    host: IpAddr,
144    port: u16,
145    loaded: LoadedProxyConfig,
146    listener: tokio::net::TcpListener,
147    admin_listener: tokio::net::TcpListener,
148) -> Result<ProxyRuntime> {
149    let cfg = loaded.runtime;
150    validate_service_has_upstream(service_name, &cfg)?;
151    let v4_source = loaded.v4.map(Arc::new);
152
153    let warnings = model_routing_warnings(&cfg, service_name);
154    if !warnings.is_empty() {
155        tracing::warn!("======== Model routing config warnings ========");
156        for warning in warnings {
157            tracing::warn!("{}", warning);
158        }
159        tracing::warn!("==============================================");
160    }
161
162    let client = Client::builder()
163        .connect_timeout(std::time::Duration::from_secs(10))
164        .tcp_keepalive(std::time::Duration::from_secs(30))
165        .pool_idle_timeout(std::time::Duration::from_secs(30))
166        .build()?;
167
168    let lb_states = Arc::new(Mutex::new(HashMap::<String, LbState>::new()));
169    let admin_addr = admin_loopback_addr_for_proxy_port(port);
170    let cfg = Arc::new(cfg);
171    let (shutdown_tx, shutdown_rx) = watch::channel(false);
172
173    let proxy = ProxyService::new_with_v4_source_and_shutdown(
174        client,
175        cfg.clone(),
176        v4_source,
177        service_name,
178        lb_states,
179        Some(shutdown_tx.clone()),
180    );
181    let state = proxy.state_handle();
182    let app = proxy_only_router_with_admin_base_url(
183        proxy.clone(),
184        Some(local_proxy_base_url(admin_addr.port())),
185    );
186    let admin_app = admin_listener_router(proxy.clone());
187
188    Ok(ProxyRuntime {
189        service_name,
190        host,
191        port,
192        admin_addr,
193        config: cfg,
194        proxy,
195        state,
196        shutdown_tx,
197        shutdown_rx,
198        listener: Some(listener),
199        admin_listener: Some(admin_listener),
200        app: Some(app),
201        admin_app: Some(admin_app),
202    })
203}
204
205pub fn service_name_for_kind(service_kind: ServiceKind) -> &'static str {
206    match service_kind {
207        ServiceKind::Codex => "codex",
208        ServiceKind::Claude => "claude",
209    }
210}
211
212pub fn service_kind_for_name(service_name: &str) -> ServiceKind {
213    match service_name {
214        "claude" => ServiceKind::Claude,
215        _ => ServiceKind::Codex,
216    }
217}
218
219pub fn validate_service_has_upstream(service_name: &str, cfg: &ProxyConfig) -> Result<()> {
220    match service_name {
221        "claude" => {
222            if !cfg.claude.has_stations() || cfg.claude.active_station().is_none() {
223                anyhow::bail!(
224                    "未找到任何可用的 Claude 上游配置,请先确保 ~/.claude/settings.json 配置完整,\
225或在 ~/.codex-helper/config.toml(或 config.json)的 `claude` 段下手动添加上游配置"
226                );
227            }
228        }
229        _ => {
230            if !cfg.codex.has_stations() || cfg.codex.active_station().is_none() {
231                anyhow::bail!(
232                    "未找到任何可用的 Codex 上游配置,请先确保 ~/.codex/config.toml 与 ~/.codex/auth.json 配置完整,或手动编辑 ~/.codex-helper/config.toml(或 config.json)添加配置"
233                );
234            }
235        }
236    }
237    Ok(())
238}
239
240fn spawn_proxy_runtime_servers(
241    listener: tokio::net::TcpListener,
242    admin_listener: tokio::net::TcpListener,
243    app: Router,
244    admin_app: Router,
245    shutdown_rx: watch::Receiver<bool>,
246) -> JoinHandle<Result<()>> {
247    let proxy_server_shutdown = {
248        let mut rx = shutdown_rx.clone();
249        async move {
250            let _ = rx.changed().await;
251        }
252    };
253    let admin_server_shutdown = {
254        let mut rx = shutdown_rx;
255        async move {
256            let _ = rx.changed().await;
257        }
258    };
259
260    tokio::spawn(async move {
261        tokio::try_join!(
262            axum::serve(
263                listener,
264                app.into_make_service_with_connect_info::<SocketAddr>(),
265            )
266            .with_graceful_shutdown(proxy_server_shutdown),
267            axum::serve(
268                admin_listener,
269                admin_app.into_make_service_with_connect_info::<SocketAddr>(),
270            )
271            .with_graceful_shutdown(admin_server_shutdown),
272        )?;
273        Ok(())
274    })
275}