asimov_cli/commands/proxy/
serve.rs1mod body_logger;
4mod proxy_config;
5mod proxy_connector;
6mod proxy_stream;
7
8use self::{body_logger::BodyLogger, proxy_config::ProxyConfig, proxy_connector::ProxyConnector};
9use crate::{BoxError, StandardOptions};
10use axum::{
11 Router,
12 body::{Body, Bytes},
13 extract::{Request, State},
14 http::{self, HeaderMap, HeaderValue, StatusCode, Version},
15 response::Response,
16 routing::any,
17};
18use clientele::crates::clap::Args;
19use http_body_util::{BodyExt, Full};
20use hyper_rustls::{ConfigBuilderExt as _, HttpsConnector};
21use hyper_util::{client::legacy::Client, rt::TokioExecutor};
22use std::{
23 net::{IpAddr, SocketAddr},
24 sync::Arc,
25};
26use tokio::net::TcpListener;
27
28const UPSTREAM_BASE_URL: &str = "https://openrouter.ai/api";
29const UPSTREAM_HOST: &str = "openrouter.ai";
30
31type UpstreamClient = Client<HttpsConnector<ProxyConnector>, Full<Bytes>>;
35
36#[derive(Args, Clone, Debug, Default)]
37pub struct ProxyServeArgs {
38 #[clap(long)]
40 pub bind: Option<IpAddr>,
41
42 #[clap(long)]
44 pub port: Option<u16>,
45}
46
47#[derive(Clone)]
48struct ProxyState {
49 client: UpstreamClient,
50 logger: Option<BodyLogger>,
51}
52
53pub async fn serve(args: ProxyServeArgs, flags: &StandardOptions) -> Result<(), BoxError> {
54 let _openrouter_api_key =
55 std::env::var("OPENROUTER_API_KEY").expect("OPENROUTER_API_KEY should be set");
56
57 let tls_config = Arc::new(
60 rustls::ClientConfig::builder()
61 .with_native_roots()?
62 .with_no_client_auth(),
63 );
64
65 let proxy_config = ProxyConfig::from_env(UPSTREAM_HOST).map_err(|err| -> BoxError { err })?;
69 if flags.verbose > 0 && !matches!(proxy_config, ProxyConfig::Direct) {
70 eprintln!("Using upstream proxy: {:?}", proxy_config);
71 }
72
73 let proxy_connector = ProxyConnector::new(proxy_config, Arc::clone(&tls_config));
74 let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
75 .with_tls_config((*tls_config).clone())
76 .https_only()
77 .enable_http1()
78 .wrap_connector(proxy_connector);
79 let client: UpstreamClient = Client::builder(TokioExecutor::new()).build(https_connector);
80
81 let state = ProxyState {
82 client,
83 logger: BodyLogger::from_env()?, };
85
86 let router = Router::new()
87 .route("/{*path}", any(proxy_handler))
88 .with_state(state);
89
90 let bind: IpAddr = args.bind.unwrap_or_else(|| {
91 std::env::var("ASIMOV_PROXY_BIND")
92 .ok()
93 .and_then(|input| input.parse::<IpAddr>().ok())
94 .unwrap_or(IpAddr::from([127, 0, 0, 1]))
95 });
96 let port = args.port.unwrap_or_else(|| {
97 std::env::var("ASIMOV_PROXY_PORT")
98 .ok()
99 .and_then(|input| input.parse::<u16>().ok())
100 .unwrap_or(1920)
101 });
102 let addr = SocketAddr::from((bind, port));
103 let listener = TcpListener::bind(addr).await.unwrap();
104
105 if flags.verbose > 0 {
106 let addr = listener.local_addr()?;
107 eprintln!("Listening on {}...", addr);
108 }
109
110 axum::serve(listener, router).await.unwrap();
111 Ok(())
112}
113
114async fn proxy_handler(
115 State(state): State<ProxyState>,
116 req: Request,
117) -> Result<Response, StatusCode> {
118 let openrouter_api_key =
119 std::env::var("OPENROUTER_API_KEY").expect("OPENROUTER_API_KEY should be set");
120
121 let request_path = req.uri().path();
122 let request_query = req
123 .uri()
124 .query()
125 .map(|q| format!("?{q}"))
126 .unwrap_or_default();
127
128 if true {
129 eprintln!("Proxying request: {} {}", request_path, request_query);
131 }
132
133 let target_url = format!("{}{}{}", UPSTREAM_BASE_URL, request_path, request_query);
135
136 let (mut head, body) = req.into_parts();
137
138 let body_bytes = body
139 .collect()
140 .await
141 .map_err(|_| StatusCode::BAD_REQUEST)?
142 .to_bytes();
143
144 let upstream_request_body = patch_request_body(body_bytes)?;
146
147 if let Some(logger) = &state.logger {
148 logger.log_request_body(&upstream_request_body);
149 }
150
151 head.uri = target_url
153 .parse()
154 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
155 head.version = Version::HTTP_11; head.headers.remove("host"); head.headers.remove("content-length"); head.headers.insert(
161 "Authorization",
162 HeaderValue::from_str(&format!("Bearer {}", openrouter_api_key)).unwrap(),
163 );
164
165 insert_attribution_headers(&mut head.headers);
167
168 let upstream_request = http::Request::from_parts(head, Full::new(upstream_request_body));
169
170 let upstream_response = state
171 .client
172 .request(upstream_request)
173 .await
174 .map_err(|err| {
175 eprintln!("Upstream request failed: {}", err);
176 StatusCode::BAD_GATEWAY
177 })?;
178
179 let (head, upstream_response_body) = upstream_response.into_parts();
182 let logger = state.logger.clone();
183 let upstream_response_body = upstream_response_body.map_frame(move |frame| {
184 if let (Some(logger), Some(data)) = (&logger, frame.data_ref()) {
185 logger.log_response_chunk(data);
186 }
187 frame
188 });
189
190 let response = Response::from_parts(head, Body::new(upstream_response_body));
191 Ok(response)
192}
193
194fn patch_request_body(body: Bytes) -> Result<Bytes, StatusCode> {
208 Ok(body)
210}
211
212fn insert_attribution_headers(headers: &mut HeaderMap<HeaderValue>) {
213 headers.insert(
215 "HTTP-Referer",
216 HeaderValue::from_static("https://asimov.sh"),
217 );
218 headers.insert("X-OpenRouter-Title", HeaderValue::from_static("ASIMOV"));
219 headers.insert(
220 "X-OpenRouter-Categories",
221 HeaderValue::from_static("cli-agent,personal-agent"),
222 );
223}