lean_ctx/core/config/
proxy.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7#[serde(default)]
8pub struct ProxyConfig {
9 pub anthropic_upstream: Option<String>,
10 pub openai_upstream: Option<String>,
11 pub gemini_upstream: Option<String>,
12}
13
14impl ProxyConfig {
15 pub fn resolve_upstream(&self, provider: ProxyProvider) -> String {
16 let (env_var, config_val, default) = match provider {
17 ProxyProvider::Anthropic => (
18 "LEAN_CTX_ANTHROPIC_UPSTREAM",
19 self.anthropic_upstream.as_deref(),
20 "https://api.anthropic.com",
21 ),
22 ProxyProvider::OpenAi => (
23 "LEAN_CTX_OPENAI_UPSTREAM",
24 self.openai_upstream.as_deref(),
25 "https://api.openai.com",
26 ),
27 ProxyProvider::Gemini => (
28 "LEAN_CTX_GEMINI_UPSTREAM",
29 self.gemini_upstream.as_deref(),
30 "https://generativelanguage.googleapis.com",
31 ),
32 };
33 let resolved = std::env::var(env_var)
34 .ok()
35 .and_then(|v| normalize_url_opt(&v))
36 .or_else(|| config_val.and_then(normalize_url_opt))
37 .unwrap_or_else(|| normalize_url(default));
38 match validate_upstream_url(&resolved) {
39 Ok(url) => url,
40 Err(e) => {
41 tracing::warn!("upstream validation failed, using default: {e}");
42 normalize_url(default)
43 }
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy)]
49pub enum ProxyProvider {
50 Anthropic,
51 OpenAi,
52 Gemini,
53}
54
55pub fn normalize_url(value: &str) -> String {
56 value.trim().trim_end_matches('/').to_string()
57}
58
59pub fn normalize_url_opt(value: &str) -> Option<String> {
60 let trimmed = normalize_url(value);
61 if trimmed.is_empty() {
62 None
63 } else {
64 Some(trimmed)
65 }
66}
67
68const ALLOWED_UPSTREAM_HOSTS: &[&str] = &[
69 "api.anthropic.com",
70 "api.openai.com",
71 "generativelanguage.googleapis.com",
72];
73
74pub fn validate_upstream_url(url: &str) -> Result<String, String> {
75 let normalized = normalize_url(url);
76 if is_local_proxy_url(&normalized) {
77 return Ok(normalized);
78 }
79 if !normalized.starts_with("https://") {
80 return Err(format!(
81 "upstream URL must use HTTPS: {normalized} (set LEAN_CTX_ALLOW_CUSTOM_UPSTREAM=1 to override)"
82 ));
83 }
84 let host = normalized
85 .strip_prefix("https://")
86 .unwrap_or(&normalized)
87 .split('/')
88 .next()
89 .unwrap_or("");
90 let host_no_port = host.split(':').next().unwrap_or(host);
91 if ALLOWED_UPSTREAM_HOSTS.contains(&host_no_port)
92 || std::env::var("LEAN_CTX_ALLOW_CUSTOM_UPSTREAM").is_ok()
93 {
94 Ok(normalized)
95 } else {
96 Err(format!(
97 "upstream host '{host_no_port}' not in allowlist {ALLOWED_UPSTREAM_HOSTS:?} (set LEAN_CTX_ALLOW_CUSTOM_UPSTREAM=1 to override)"
98 ))
99 }
100}
101
102pub fn is_local_proxy_url(value: &str) -> bool {
103 let n = normalize_url(value);
104 n.starts_with("http://127.0.0.1:")
105 || n.starts_with("http://localhost:")
106 || n.starts_with("http://[::1]:")
107}