use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize)]
pub struct ProxyConfig {
pub port: u16,
pub upstream_url: String,
pub upstream_routes: Vec<UpstreamRoute>,
pub global_token_limit: u32,
pub cache_ttl_secs: u64,
pub cache_max_capacity: u64,
pub max_payload_bytes: usize,
pub jwt_secret: String,
pub database_url: Option<String>,
#[serde(default)]
pub require_auth: bool,
#[serde(default)]
pub admin_api_key: Option<String>,
}
impl std::fmt::Debug for ProxyConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProxyConfig")
.field("port", &self.port)
.field("upstream_url", &self.upstream_url)
.field("upstream_routes", &self.upstream_routes)
.field("global_token_limit", &self.global_token_limit)
.field("cache_ttl_secs", &self.cache_ttl_secs)
.field("cache_max_capacity", &self.cache_max_capacity)
.field("max_payload_bytes", &self.max_payload_bytes)
.field("jwt_secret", &"***REDACTED***")
.field(
"database_url",
&self.database_url.as_ref().map(|_| "***REDACTED***"),
)
.field("require_auth", &self.require_auth)
.field(
"admin_api_key",
&self.admin_api_key.as_ref().map(|_| "***REDACTED***"),
)
.finish()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpstreamRoute {
pub path_prefix: String,
pub target: String,
pub name: String,
}
impl ProxyConfig {
pub fn from_env() -> Self {
Self {
port: env_or("PORT", "8080").parse().unwrap_or(8080),
upstream_url: env_or("UPSTREAM_URL", DEFAULT_UPSTREAM),
upstream_routes: vec![
UpstreamRoute {
path_prefix: "/v1/".into(),
target: env_or("OPENAI_UPSTREAM", "https://api.openai.com"),
name: "OpenAI".into(),
},
UpstreamRoute {
path_prefix: "/anthropic/".into(),
target: env_or("ANTHROPIC_UPSTREAM", "https://api.anthropic.com"),
name: "Anthropic".into(),
},
UpstreamRoute {
path_prefix: "/mcp/".into(),
target: env_or("MCP_UPSTREAM", "http://localhost:3001"),
name: "MCP Server".into(),
},
],
global_token_limit: env_or("GLOBAL_TOKEN_LIMIT", "100000")
.parse()
.unwrap_or(100_000),
cache_ttl_secs: env_or("CACHE_TTL_SECS", "900").parse().unwrap_or(900),
cache_max_capacity: env_or("CACHE_MAX_CAPACITY", "10000")
.parse()
.unwrap_or(10_000),
max_payload_bytes: env_or("MAX_PAYLOAD_BYTES", "1048576")
.parse()
.unwrap_or(1_048_576),
jwt_secret: resolve_jwt_secret(),
database_url: std::env::var("DATABASE_URL").ok(),
require_auth: env_or("REQUIRE_AUTH", "false").parse().unwrap_or(false),
admin_api_key: std::env::var("ADMIN_API_KEY")
.ok()
.filter(|s| !s.trim().is_empty()),
}
}
pub fn from_env_oss() -> Self {
Self {
jwt_secret: String::new(),
database_url: None,
require_auth: false,
admin_api_key: None,
..Self::from_env_common()
}
}
fn from_env_common() -> Self {
Self {
port: env_or("PORT", "8080").parse().unwrap_or(8080),
upstream_url: env_or("UPSTREAM_URL", DEFAULT_UPSTREAM),
upstream_routes: vec![
UpstreamRoute {
path_prefix: "/v1/".into(),
target: env_or("OPENAI_UPSTREAM", "https://api.openai.com"),
name: "OpenAI".into(),
},
UpstreamRoute {
path_prefix: "/anthropic/".into(),
target: env_or("ANTHROPIC_UPSTREAM", "https://api.anthropic.com"),
name: "Anthropic".into(),
},
UpstreamRoute {
path_prefix: "/mcp/".into(),
target: env_or("MCP_UPSTREAM", "http://localhost:3001"),
name: "MCP Server".into(),
},
],
global_token_limit: env_or("GLOBAL_TOKEN_LIMIT", "100000")
.parse()
.unwrap_or(100_000),
cache_ttl_secs: env_or("CACHE_TTL_SECS", "900").parse().unwrap_or(900),
cache_max_capacity: env_or("CACHE_MAX_CAPACITY", "10000")
.parse()
.unwrap_or(10_000),
max_payload_bytes: env_or("MAX_PAYLOAD_BYTES", "1048576")
.parse()
.unwrap_or(1_048_576),
jwt_secret: String::new(),
database_url: None,
require_auth: false,
admin_api_key: None,
}
}
pub fn resolve_upstream(&self, path: &str) -> &str {
for route in &self.upstream_routes {
if path.starts_with(&route.path_prefix) {
return &route.target;
}
}
&self.upstream_url
}
}
const DEFAULT_UPSTREAM: &str = "https://api.openai.com";
pub fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
pub fn data_path(name: &str) -> String {
let dir = std::env::var("DATA_DIR").unwrap_or_else(|_| "data".to_string());
format!("{}/{}", dir.trim_end_matches('/'), name)
}
fn resolve_jwt_secret() -> String {
match std::env::var("JWT_SECRET") {
Ok(s) if !s.trim().is_empty() => s,
_ => {
let ephemeral = format!(
"{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
);
tracing::warn!(
"JWT_SECRET is not set — generated an ephemeral random secret. \
Issued tokens will be invalidated on restart. Set JWT_SECRET for production."
);
ephemeral
}
}
}