#[allow(clippy::wildcard_imports)]
use super::*;
#[cfg(feature = "http-server")]
fn parse_proxy_port(rest: &[String]) -> u16 {
rest.iter()
.find_map(|p| p.strip_prefix("--port="))
.and_then(|p| p.parse().ok())
.unwrap_or_else(crate::proxy_setup::default_port)
}
#[cfg(feature = "http-server")]
fn stop_proxy_process(port: u16) -> bool {
let health_url = format!("http://127.0.0.1:{port}/health");
let Ok(resp) = ureq::get(&health_url).call() else {
return false;
};
let pid = resp.into_body().read_to_string().ok().and_then(|body| {
body.split("pid\":")
.nth(1)
.and_then(|s| s.split([',', '}']).next())
.and_then(|s| s.trim().parse::<u32>().ok())
});
match pid {
Some(pid) => {
let _ = crate::ipc::process::terminate_gracefully(pid);
std::thread::sleep(std::time::Duration::from_millis(500));
if crate::ipc::process::is_alive(pid) {
let _ = crate::ipc::process::force_kill(pid);
}
println!("Proxy on port {port} stopped (PID {pid}).");
}
None => {
println!(
"Proxy on port {port} running but could not parse PID. Use `lean-ctx stop` to kill all."
);
}
}
true
}
#[cfg(feature = "http-server")]
fn print_compression_by_upstream(v: &serde_json::Value) {
let Some(per_upstream) = v.get("per_upstream").and_then(|u| u.as_object()) else {
return;
};
println!(" Compression by upstream:");
for (label, key) in [
("Anthropic", "anthropic"),
("OpenAI", "openai"),
("ChatGPT", "chatgpt"),
("Gemini", "gemini"),
] {
let Some(row) = per_upstream.get(key).and_then(|x| x.as_object()) else {
continue;
};
let requests = row
.get("requests_total")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let saved = row
.get("tokens_saved")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let ratio = row
.get("compression_ratio_pct")
.and_then(|x| x.as_str())
.unwrap_or("0.0");
println!(" {label:<10} {ratio:>5}% saved, {saved} tok, {requests} req");
}
}
fn print_verified_savings(v: &serde_json::Value) {
let Some(vs) = v.get("verified_savings").filter(|x| x.is_object()) else {
return;
};
let saved = vs
.get("verified_saved_tokens")
.and_then(serde_json::Value::as_i64)
.unwrap_or(0);
let requests = vs
.get("requests")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let counterfactual = vs
.get("counterfactual_input_tokens")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
#[allow(clippy::cast_precision_loss)]
let pct = if counterfactual > 0 {
saved as f64 * 100.0 / counterfactual as f64
} else {
0.0
};
println!(
" Verified: {saved} tok saved across {requests} probed req ({pct:.1}% — provider-counted, #701)"
);
}
#[cfg(feature = "http-server")]
fn print_live_upstreams_and_drift(v: &serde_json::Value, cfg: &crate::core::config::Config) {
use crate::core::config::{
ProxyProvider, UpstreamDrift, diagnose_drift, env_upstream_override,
};
let Some(up) = v.get("upstreams").and_then(|u| u.as_object()) else {
return;
};
let disk = cfg.proxy.resolve_all_disk();
println!(" Upstreams (live):");
let mut notes = Vec::new();
for (label, key, provider, disk_val) in [
(
"Anthropic",
"anthropic",
ProxyProvider::Anthropic,
&disk.anthropic,
),
("OpenAI", "openai", ProxyProvider::OpenAi, &disk.openai),
("ChatGPT", "chatgpt", ProxyProvider::ChatGpt, &disk.chatgpt),
("Gemini", "gemini", ProxyProvider::Gemini, &disk.gemini),
] {
let live = up.get(key).and_then(|x| x.as_str()).unwrap_or("?");
println!(" {label:<10} {live}");
if live == "?" {
continue;
}
let env = env_upstream_override(provider);
match diagnose_drift(env.as_deref(), disk_val, live) {
Some(UpstreamDrift::EnvNotApplied) => {
let want = env.as_deref().unwrap_or("");
notes.push(format!(
" \x1b[33mâš {label}: LEAN_CTX_{}_UPSTREAM is set in this shell ({want})\x1b[0m\n \
\x1b[33m but the running proxy serves {live}. Environment variables do not reach\x1b[0m\n \
\x1b[33m an MCP/service-spawned proxy (#449). Persist it — applies live:\x1b[0m\n \
\x1b[33m lean-ctx config set proxy.{key}_upstream {want}\x1b[0m",
label.to_uppercase(),
));
}
Some(UpstreamDrift::ConfigNotApplied) => {
notes.push(format!(
" \x1b[33mâš {label}: proxy serves {live} but config.toml resolves to {disk_val}.\x1b[0m\n \
\x1b[33m Apply it: lean-ctx proxy restart\x1b[0m",
));
}
None => {}
}
}
for note in notes {
println!();
println!("{note}");
}
if cfg.proxy.has_custom_host_upstream() && !cfg.proxy.allows_custom_upstream() {
println!();
println!(
" \x1b[33mâš A custom upstream host is configured but not permitted, so the proxy\x1b[0m"
);
println!(
" \x1b[33m serves the provider default. Allow it (reaches the managed proxy):\x1b[0m"
);
println!(" \x1b[33m lean-ctx config set proxy.allow_custom_upstream true\x1b[0m");
println!(" \x1b[33m lean-ctx proxy restart\x1b[0m");
}
}
#[cfg(feature = "http-server")]
fn bridge_custom_upstream_optin() -> bool {
if std::env::var("LEAN_CTX_ALLOW_CUSTOM_UPSTREAM").is_err() {
return false;
}
let cfg = crate::core::config::Config::load();
if cfg.proxy.allow_custom_upstream == Some(true) || !cfg.proxy.has_custom_host_upstream() {
return false;
}
match crate::core::config::Config::update_global(|c| {
c.proxy.allow_custom_upstream = Some(true);
}) {
Ok(_) => {
println!(
" \x1b[32m✓\x1b[0m Custom upstream opt-in persisted: [proxy] allow_custom_upstream = true"
);
println!(
" \x1b[2m (so the managed proxy honors your custom upstream — the env var never reaches it, #590)\x1b[0m"
);
true
}
Err(e) => {
tracing::warn!("could not persist allow_custom_upstream: {e}");
false
}
}
}
#[cfg(feature = "http-server")]
pub(super) fn should_persist_codex_chatgpt_optin(env_present: bool, current: Option<bool>) -> bool {
env_present && current != Some(true)
}
#[cfg(feature = "http-server")]
fn bridge_codex_chatgpt_optin() -> bool {
let env_present = std::env::var("LEAN_CTX_CODEX_CHATGPT_PROXY").is_ok();
let current = crate::core::config::Config::load()
.proxy
.codex_chatgpt_proxy;
if !should_persist_codex_chatgpt_optin(env_present, current) {
return false;
}
match crate::core::config::Config::update_global(|c| {
c.proxy.codex_chatgpt_proxy = Some(true);
}) {
Ok(_) => {
println!(
" \x1b[32m✓\x1b[0m Codex ChatGPT proxy opt-in persisted: [proxy] codex_chatgpt_proxy = true"
);
println!(
" \x1b[2m (so the managed proxy and every env-less setup pass route Codex through it — the shell env never reaches them, #603/#616)\x1b[0m"
);
true
}
Err(e) => {
tracing::warn!("could not persist codex_chatgpt_proxy: {e}");
false
}
}
}
#[cfg(feature = "http-server")]
#[derive(Debug, PartialEq, Eq)]
pub(super) enum CodexChatgptAction {
On,
Off,
Status,
Unknown,
}
#[cfg(feature = "http-server")]
pub(super) fn parse_codex_chatgpt_action(arg: Option<&str>) -> CodexChatgptAction {
match arg {
Some("on" | "enable" | "true") => CodexChatgptAction::On,
Some("off" | "disable" | "false") => CodexChatgptAction::Off,
Some("status") | None => CodexChatgptAction::Status,
Some(_) => CodexChatgptAction::Unknown,
}
}
#[cfg(feature = "http-server")]
fn codex_chatgpt_set(on: bool, port: u16) {
if let Err(e) =
crate::core::config::Config::update_global(|c| c.proxy.codex_chatgpt_proxy = Some(on))
{
println!("\x1b[31m✗\x1b[0m Could not persist [proxy] codex_chatgpt_proxy: {e}");
return;
}
if on {
println!(
"\x1b[32m✓\x1b[0m Codex ChatGPT proxy routing \x1b[1menabled\x1b[0m: [proxy] codex_chatgpt_proxy = true"
);
} else {
println!(
"\x1b[32m✓\x1b[0m Codex ChatGPT proxy routing \x1b[1mdisabled\x1b[0m: [proxy] codex_chatgpt_proxy = false"
);
}
let home = dirs::home_dir().unwrap_or_default();
crate::proxy_setup::install_codex_env(&home, port, false);
if on && !crate::proxy_setup::is_proxy_reachable(port) {
println!();
println!(
" \x1b[33m⚠Proxy not running on port {port}\x1b[0m — Codex can't route until it is up."
);
println!(" Start it: lean-ctx proxy enable (managed autostart service)");
println!(" or: lean-ctx proxy start --port={port}");
println!(
" \x1b[2mThe opt-in is saved, so setup writes Codex's chatgpt_base_url once the proxy is reachable.\x1b[0m"
);
}
}
#[cfg(feature = "http-server")]
fn codex_chatgpt_status(port: u16) {
let cfg = crate::core::config::Config::load();
let effective = cfg.proxy.codex_chatgpt_proxy_enabled();
println!("Codex ChatGPT proxy routing:");
println!(
" Effective: {}",
if effective {
"\x1b[32mon\x1b[0m"
} else {
"off"
}
);
match cfg.proxy.codex_chatgpt_proxy {
Some(true) => println!(" Config: [proxy] codex_chatgpt_proxy = true"),
Some(false) => println!(" Config: [proxy] codex_chatgpt_proxy = false"),
None => println!(" Config: (unset → default off)"),
}
if let Ok(v) = std::env::var("LEAN_CTX_CODEX_CHATGPT_PROXY") {
println!(" Env: LEAN_CTX_CODEX_CHATGPT_PROXY={v} (forces on for this process)");
}
let home = dirs::home_dir().unwrap_or_default();
let codex_cfg = crate::core::home::resolve_codex_dir()
.unwrap_or_else(|| home.join(".codex"))
.join("config.toml");
let routed = std::fs::read_to_string(&codex_cfg)
.is_ok_and(|c| c.contains("leanctx-chatgpt") || c.contains("chatgpt_base_url"));
println!(
" Codex cfg: {}",
if routed {
"model_provider → leanctx-chatgpt (routed)"
} else {
"native (no proxy entry)"
}
);
println!(
" Proxy: {}",
if crate::proxy_setup::is_proxy_reachable(port) {
"running"
} else {
"not running"
}
);
if !effective {
println!();
println!(" Enable: lean-ctx proxy codex-chatgpt on");
}
}
pub(crate) fn cmd_proxy(rest: &[String]) {
#[cfg(feature = "http-server")]
{
if wants_help(rest) {
println!(
"Usage: lean-ctx proxy <start|stop|restart|status|enable|disable|cleanup|token|codex-chatgpt> [--port=4444]"
);
println!();
println!("Commands:");
println!(
" start Run the compression proxy (foreground; -d/--detach for background; --autostart installs a service)"
);
println!(" stop Stop the proxy on the given port");
println!(
" restart Restart the managed proxy (re-reads config.toml; drops env overrides)"
);
println!(" status Show proxy config, process, live upstreams and stats");
println!(" enable Enable the proxy: config flag, autostart service, env wiring");
println!(" disable Disable the proxy and restore the original endpoint");
println!(" cleanup Remove stale proxy URLs from AI tool configs");
println!(" token Print the current proxy Bearer token (for MCP/HTTP clients)");
println!(
" codex-chatgpt <on|off|status> Route a Codex ChatGPT-subscription login through the proxy"
);
return;
}
let sub = rest.first().map_or("help", std::string::String::as_str);
match sub {
"start" => {
let port: u16 = rest
.iter()
.find_map(|p| p.strip_prefix("--port=").or_else(|| p.strip_prefix("-p=")))
.and_then(|p| p.parse().ok())
.unwrap_or_else(crate::proxy_setup::default_port);
let autostart = rest.iter().any(|a| a == "--autostart");
if autostart {
crate::proxy_autostart::install(port, false);
return;
}
let detach = rest.iter().any(|a| a == "--detach" || a == "-d");
if detach {
start_detached(port);
return;
}
if let Err(e) = crate::cli::dispatch::run_async(crate::proxy::start_proxy(port)) {
tracing::error!("Proxy error: {e}");
std::process::exit(1);
}
}
"stop" => {
let port = parse_proxy_port(rest);
if !stop_proxy_process(port) {
println!("No proxy running on port {port}.");
}
}
"restart" => {
let port = parse_proxy_port(rest);
if crate::proxy_autostart::is_installed() {
bridge_custom_upstream_optin();
bridge_codex_chatgpt_optin();
crate::proxy_autostart::stop();
std::thread::sleep(std::time::Duration::from_millis(700));
crate::proxy_autostart::start();
println!("\x1b[32m✓\x1b[0m Proxy restarted (managed service).");
println!(" Verify active upstreams: lean-ctx proxy status");
} else if stop_proxy_process(port) {
println!();
println!(" No autostart service installed — start the proxy again:");
println!(" lean-ctx proxy start --port={port}");
} else {
println!("No proxy running on port {port} and no autostart service installed.");
println!(" Start it now: lean-ctx proxy start --port={port}");
println!(" Or install service: lean-ctx proxy enable");
}
}
"status" => {
let port = parse_proxy_port(rest);
let cfg = crate::core::config::Config::load();
println!("lean-ctx proxy:");
match cfg.proxy_enabled {
Some(true) => println!(" Config: enabled"),
Some(false) => println!(" Config: disabled"),
None => println!(" Config: undecided (not yet configured)"),
}
println!(" Port: {port}");
let alive = ureq::get(&format!("http://127.0.0.1:{port}/health"))
.call()
.is_ok();
if alive {
println!(" Process: running");
let token =
crate::core::session_token::resolve_proxy_token("LEAN_CTX_PROXY_TOKEN");
let status = ureq::get(&format!("http://127.0.0.1:{port}/status"))
.header("Authorization", &format!("Bearer {token}"))
.call();
if let Ok(resp) = status {
let body = resp.into_body().read_to_string().unwrap_or_default();
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
println!(" Requests: {}", v["requests_total"]);
println!(" Compressed: {}", v["requests_compressed"]);
println!(" Tokens saved: {} (estimated)", v["tokens_saved"]);
println!(
" Compression: {}%",
v["compression_ratio_pct"].as_str().unwrap_or("0.0")
);
print_verified_savings(&v);
print_compression_by_upstream(&v);
print_live_upstreams_and_drift(&v, &cfg);
}
} else {
println!(
" \x1b[33mâš Live details unavailable: the running proxy rejects this\x1b[0m"
);
println!(
" \x1b[33m shell's session token. Re-sync it: lean-ctx proxy restart\x1b[0m"
);
}
} else {
println!(" Process: not running");
}
if cfg.proxy_enabled == Some(false) || cfg.proxy_enabled.is_none() {
println!();
println!(" Enable: lean-ctx proxy enable");
let home = dirs::home_dir().unwrap_or_default();
if crate::proxy_setup::has_stale_proxy_url(&home) {
println!();
println!(
" \x1b[33mâš WARNING: Claude Code ANTHROPIC_BASE_URL points to the local proxy,\x1b[0m"
);
println!(
" \x1b[33m but proxy is not enabled. This causes 401 auth failures.\x1b[0m"
);
println!(" Fix: lean-ctx proxy cleanup (remove stale URL)");
println!(" lean-ctx proxy enable (enable the proxy)");
}
}
}
"enable" => {
let force = rest.iter().any(|a| a == "--force");
if let Err(e) =
crate::core::config::Config::update_global(|c| c.proxy_enabled = Some(true))
{
tracing::warn!("could not persist proxy_enabled: {e}");
}
bridge_custom_upstream_optin();
bridge_codex_chatgpt_optin();
let port = crate::proxy_setup::default_port();
crate::proxy_autostart::install(port, false);
std::thread::sleep(std::time::Duration::from_millis(500));
let home = dirs::home_dir().unwrap_or_default();
crate::proxy_setup::install_proxy_env_unchecked(&home, port, false, force);
println!(
"\x1b[32m✓\x1b[0m Proxy enabled on port {port}. LLM requests will be compressed."
);
}
"disable" => {
if let Err(e) =
crate::core::config::Config::update_global(|c| c.proxy_enabled = Some(false))
{
tracing::warn!("could not persist proxy_enabled: {e}");
}
crate::proxy_autostart::uninstall(false);
let home = dirs::home_dir().unwrap_or_default();
crate::proxy_setup::uninstall_proxy_env(&home, false);
println!("\x1b[32m✓\x1b[0m Proxy disabled. Original endpoint restored.");
println!(" Re-enable anytime: lean-ctx proxy enable");
}
"cleanup" => {
let home = dirs::home_dir().unwrap_or_default();
let removed = crate::proxy_setup::cleanup_stale_proxy_env(&home);
if removed > 0 {
println!("\x1b[32m✓\x1b[0m Cleaned up {removed} stale proxy URL(s).");
println!(" Restart your AI tool for changes to take effect.");
} else {
println!(" No stale proxy URLs found. Nothing to clean up.");
}
}
"codex-chatgpt" => {
let port = parse_proxy_port(rest);
let action_arg = rest
.get(1..)
.unwrap_or_default()
.iter()
.find(|a| !a.starts_with("--port=") && !a.starts_with("-p="))
.map(std::string::String::as_str);
match parse_codex_chatgpt_action(action_arg) {
CodexChatgptAction::On => codex_chatgpt_set(true, port),
CodexChatgptAction::Off => codex_chatgpt_set(false, port),
CodexChatgptAction::Status => codex_chatgpt_status(port),
CodexChatgptAction::Unknown => {
println!("Unknown argument '{}'.", action_arg.unwrap_or(""));
println!(
"Usage: lean-ctx proxy codex-chatgpt <on|off|status> [--port=4444]"
);
}
}
}
"token" => {
let token = crate::core::session_token::resolve_proxy_token("LEAN_CTX_PROXY_TOKEN");
let quiet = rest.iter().any(|a| a == "--quiet" || a == "-q");
if quiet {
print!("{token}");
} else {
println!("{token}");
}
}
_ => {
println!(
"Usage: lean-ctx proxy <start|stop|restart|status|enable|disable|cleanup|token|codex-chatgpt> [--port=4444]"
);
}
}
}
#[cfg(feature = "http-server")]
fn start_detached(port: u16) {
let exe = std::env::current_exe().unwrap_or_else(|_| "lean-ctx".into());
let log_dir = crate::core::paths::state_dir()
.unwrap_or_else(|_| std::env::temp_dir().join("lean-ctx"));
let _ = std::fs::create_dir_all(&log_dir);
let log_path = log_dir.join("proxy.log");
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.unwrap_or_else(|e| {
eprintln!("Cannot open {}: {e}", log_path.display());
std::process::exit(1);
});
let stderr_file = log_file.try_clone().unwrap_or_else(|e| {
eprintln!("Cannot clone log fd: {e}");
std::process::exit(1);
});
match std::process::Command::new(&exe)
.args(["proxy", "start", &format!("--port={port}")])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log_file))
.stderr(std::process::Stdio::from(stderr_file))
.spawn()
{
Ok(child) => {
println!(
"\x1b[32m✓\x1b[0m Proxy started in background (PID {}, port {port})",
child.id()
);
println!(" Logs: {}", log_path.display());
println!(" Stop: lean-ctx proxy stop --port={port}");
}
Err(e) => {
eprintln!("Failed to start detached proxy: {e}");
std::process::exit(1);
}
}
}
#[cfg(not(feature = "http-server"))]
{
eprintln!("lean-ctx proxy is not available in this build");
std::process::exit(1);
}
}