mod body_logger;
mod proxy_config;
mod proxy_connector;
mod proxy_stream;
use self::{body_logger::BodyLogger, proxy_config::ProxyConfig, proxy_connector::ProxyConnector};
use crate::{BoxError, StandardOptions};
use axum::{
Router,
body::{Body, Bytes},
extract::{Request, State},
http::{self, HeaderMap, HeaderValue, StatusCode, Version},
response::Response,
routing::any,
};
use clientele::crates::clap::Args;
use http_body_util::{BodyExt, Full};
use hyper_rustls::{ConfigBuilderExt as _, HttpsConnector};
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use std::{
net::{IpAddr, SocketAddr},
sync::Arc,
};
use tokio::net::TcpListener;
const UPSTREAM_BASE_URL: &str = "https://openrouter.ai/api";
const UPSTREAM_HOST: &str = "openrouter.ai";
type UpstreamClient = Client<HttpsConnector<ProxyConnector>, Full<Bytes>>;
#[derive(Args, Clone, Debug, Default)]
pub struct ProxyServeArgs {
#[clap(long)]
pub bind: Option<IpAddr>,
#[clap(long)]
pub port: Option<u16>,
}
#[derive(Clone)]
struct ProxyState {
client: UpstreamClient,
logger: Option<BodyLogger>,
}
pub async fn serve(args: ProxyServeArgs, flags: &StandardOptions) -> Result<(), BoxError> {
let _openrouter_api_key =
std::env::var("OPENROUTER_API_KEY").expect("OPENROUTER_API_KEY should be set");
let tls_config = Arc::new(
rustls::ClientConfig::builder()
.with_native_roots()?
.with_no_client_auth(),
);
let proxy_config = ProxyConfig::from_env(UPSTREAM_HOST).map_err(|err| -> BoxError { err })?;
if flags.verbose > 0 && !matches!(proxy_config, ProxyConfig::Direct) {
eprintln!("Using upstream proxy: {:?}", proxy_config);
}
let proxy_connector = ProxyConnector::new(proxy_config, Arc::clone(&tls_config));
let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config((*tls_config).clone())
.https_only()
.enable_http1()
.wrap_connector(proxy_connector);
let client: UpstreamClient = Client::builder(TokioExecutor::new()).build(https_connector);
let state = ProxyState {
client,
logger: BodyLogger::from_env()?, };
let router = Router::new()
.route("/{*path}", any(proxy_handler))
.with_state(state);
let bind: IpAddr = args.bind.unwrap_or_else(|| {
std::env::var("ASIMOV_PROXY_BIND")
.ok()
.and_then(|input| input.parse::<IpAddr>().ok())
.unwrap_or(IpAddr::from([127, 0, 0, 1]))
});
let port = args.port.unwrap_or_else(|| {
std::env::var("ASIMOV_PROXY_PORT")
.ok()
.and_then(|input| input.parse::<u16>().ok())
.unwrap_or(1920)
});
let addr = SocketAddr::from((bind, port));
let listener = TcpListener::bind(addr).await.unwrap();
if flags.verbose > 0 {
let addr = listener.local_addr()?;
eprintln!("Listening on {}...", addr);
}
axum::serve(listener, router).await.unwrap();
Ok(())
}
async fn proxy_handler(
State(state): State<ProxyState>,
req: Request,
) -> Result<Response, StatusCode> {
let openrouter_api_key =
std::env::var("OPENROUTER_API_KEY").expect("OPENROUTER_API_KEY should be set");
let request_path = req.uri().path();
let request_query = req
.uri()
.query()
.map(|q| format!("?{q}"))
.unwrap_or_default();
if true {
eprintln!("Proxying request: {} {}", request_path, request_query);
}
let target_url = format!("{}{}{}", UPSTREAM_BASE_URL, request_path, request_query);
let (mut head, body) = req.into_parts();
let body_bytes = body
.collect()
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
.to_bytes();
let upstream_request_body = patch_request_body(body_bytes)?;
if let Some(logger) = &state.logger {
logger.log_request_body(&upstream_request_body);
}
head.uri = target_url
.parse()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
head.version = Version::HTTP_11;
head.headers.remove("host"); head.headers.remove("content-length"); head.headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {}", openrouter_api_key)).unwrap(),
);
insert_attribution_headers(&mut head.headers);
let upstream_request = http::Request::from_parts(head, Full::new(upstream_request_body));
let upstream_response = state
.client
.request(upstream_request)
.await
.map_err(|err| {
eprintln!("Upstream request failed: {}", err);
StatusCode::BAD_GATEWAY
})?;
let (head, upstream_response_body) = upstream_response.into_parts();
let logger = state.logger.clone();
let upstream_response_body = upstream_response_body.map_frame(move |frame| {
if let (Some(logger), Some(data)) = (&logger, frame.data_ref()) {
logger.log_response_chunk(data);
}
frame
});
let response = Response::from_parts(head, Body::new(upstream_response_body));
Ok(response)
}
fn patch_request_body(body: Bytes) -> Result<Bytes, StatusCode> {
Ok(body)
}
fn insert_attribution_headers(headers: &mut HeaderMap<HeaderValue>) {
headers.insert(
"HTTP-Referer",
HeaderValue::from_static("https://asimov.sh"),
);
headers.insert("X-OpenRouter-Title", HeaderValue::from_static("ASIMOV"));
headers.insert(
"X-OpenRouter-Categories",
HeaderValue::from_static("cli-agent,personal-agent"),
);
}