use std::env;
use std::num::NonZeroU32;
use firstpass_core::{Config as RoutingConfig, Mode, PriceTable};
const DEFAULT_PROMPT_SALT: &str = "firstpass-dev-salt";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReceiptsMode {
#[default]
BestEffort,
Durable,
}
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub bind: String,
pub upstream_anthropic: String,
pub upstream_openai: String,
pub db_path: String,
pub tenant_id: String,
pub require_auth: bool,
pub tenant_keys: crate::tenant_auth::TenantKeys,
pub prompt_salt: String,
pub mode: Mode,
pub routing: Option<RoutingConfig>,
pub prices: PriceTable,
pub max_concurrency: usize,
pub tenant_rate_per_sec: Option<NonZeroU32>,
pub receipts_mode: ReceiptsMode,
}
const DEFAULT_MAX_CONCURRENCY: usize = 512;
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error(
"FIRSTPASS_MODE={0:?} is not a known mode; set `observe` or `enforce`, or leave it unset"
)]
UnsupportedMode(String),
#[error("FIRSTPASS_RECEIPTS={0:?} is not valid; set `best_effort` (default) or `durable`")]
UnsupportedReceiptsMode(String),
#[error("routing config error: {0}")]
Config(String),
#[error("tenant auth config error: {0}")]
Auth(#[from] crate::tenant_auth::AuthConfigError),
}
impl ProxyConfig {
pub fn from_env() -> Result<Self, ConfigError> {
let routing_toml = match env::var("FIRSTPASS_CONFIG").ok() {
Some(path) => Some(
std::fs::read_to_string(&path)
.map_err(|e| ConfigError::Config(format!("reading {path}: {e}")))?,
),
None => None,
};
let tenant_keys_json = match env::var("FIRSTPASS_TENANT_KEYS").ok() {
Some(path) => Some(
std::fs::read_to_string(&path)
.map_err(|e| ConfigError::Config(format!("reading {path}: {e}")))?,
),
None => None,
};
Self::from_lookup(|key| match key {
"FIRSTPASS_CONFIG_TOML" => routing_toml.clone(),
"FIRSTPASS_TENANT_KEYS_JSON" => env::var(key).ok().or_else(|| tenant_keys_json.clone()),
other => env::var(other).ok(),
})
}
pub fn from_lookup(lookup: impl Fn(&str) -> Option<String>) -> Result<Self, ConfigError> {
let bind = lookup("FIRSTPASS_BIND").unwrap_or_else(|| "127.0.0.1:8080".to_owned());
let upstream_anthropic = lookup("FIRSTPASS_UPSTREAM_ANTHROPIC")
.unwrap_or_else(|| "https://api.anthropic.com".to_owned());
let upstream_openai = lookup("FIRSTPASS_UPSTREAM_OPENAI")
.unwrap_or_else(|| "https://api.openai.com".to_owned());
let db_path = lookup("FIRSTPASS_DB").unwrap_or_else(|| "firstpass.db".to_owned());
let tenant_id = lookup("FIRSTPASS_TENANT").unwrap_or_else(|| "default".to_owned());
let prompt_salt = lookup("FIRSTPASS_PROMPT_SALT").unwrap_or_else(|| {
tracing::warn!(
"FIRSTPASS_PROMPT_SALT is unset — using the built-in dev default; \
set a real secret before handling production traffic"
);
DEFAULT_PROMPT_SALT.to_owned()
});
let mode_str = lookup("FIRSTPASS_MODE").unwrap_or_else(|| "observe".to_owned());
let mode = match mode_str.as_str() {
"observe" => Mode::Observe,
"enforce" => Mode::Enforce,
other => return Err(ConfigError::UnsupportedMode(other.to_owned())),
};
let routing = match lookup("FIRSTPASS_CONFIG_TOML") {
Some(toml) => {
Some(RoutingConfig::parse(&toml).map_err(|e| ConfigError::Config(e.to_string()))?)
}
None => None,
};
let max_concurrency = match lookup("FIRSTPASS_MAX_CONCURRENCY") {
Some(s) => s.parse().map_err(|e| {
ConfigError::Config(format!("FIRSTPASS_MAX_CONCURRENCY={s:?}: {e}"))
})?,
None => DEFAULT_MAX_CONCURRENCY,
};
let require_auth = lookup("FIRSTPASS_REQUIRE_AUTH")
.map(|s| {
matches!(
s.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
.unwrap_or(false);
let tenant_keys = match lookup("FIRSTPASS_TENANT_KEYS_JSON") {
Some(json) => crate::tenant_auth::TenantKeys::from_json(&json)?,
None => crate::tenant_auth::TenantKeys::default(),
};
if require_auth && tenant_keys.is_empty() {
tracing::warn!(
"FIRSTPASS_REQUIRE_AUTH is on but no tenant keys are configured — every request \
will be rejected with 401 until FIRSTPASS_TENANT_KEYS is set"
);
}
let tenant_rate_per_sec = match lookup("FIRSTPASS_TENANT_RATE_PER_SEC") {
Some(s) => Some(s.trim().parse::<NonZeroU32>().map_err(|e| {
ConfigError::Config(format!("FIRSTPASS_TENANT_RATE_PER_SEC={s:?}: {e}"))
})?),
None => None,
};
let receipts_mode = match lookup("FIRSTPASS_RECEIPTS").as_deref() {
None | Some("best_effort") => ReceiptsMode::BestEffort,
Some("durable") => ReceiptsMode::Durable,
Some(other) => {
return Err(ConfigError::UnsupportedReceiptsMode(other.to_owned()));
}
};
Ok(Self {
bind,
upstream_anthropic,
upstream_openai,
db_path,
tenant_id,
require_auth,
tenant_keys,
prompt_salt,
mode,
prices: {
let mut prices = PriceTable::defaults();
if let Some(cfg) = routing.as_ref() {
for p in &cfg.price_defs {
prices = prices.with_override(
p.model.clone(),
firstpass_core::ModelPrice {
input_per_mtok: p.input_per_mtok,
output_per_mtok: p.output_per_mtok,
},
);
}
}
prices
},
routing,
max_concurrency,
tenant_rate_per_sec,
receipts_mode,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sane_when_unset() {
let cfg = ProxyConfig::from_lookup(|_| None).unwrap();
assert_eq!(cfg.bind, "127.0.0.1:8080");
assert_eq!(cfg.upstream_anthropic, "https://api.anthropic.com");
assert_eq!(cfg.db_path, "firstpass.db");
assert_eq!(cfg.tenant_id, "default");
assert_eq!(cfg.prompt_salt, DEFAULT_PROMPT_SALT);
assert_eq!(cfg.mode, Mode::Observe);
assert_eq!(cfg.max_concurrency, DEFAULT_MAX_CONCURRENCY);
}
#[test]
fn max_concurrency_is_parsed_from_env() {
let cfg = ProxyConfig::from_lookup(|key| {
(key == "FIRSTPASS_MAX_CONCURRENCY").then(|| "64".to_owned())
})
.unwrap();
assert_eq!(cfg.max_concurrency, 64);
}
#[test]
fn bad_max_concurrency_is_an_error() {
let result = ProxyConfig::from_lookup(|key| {
(key == "FIRSTPASS_MAX_CONCURRENCY").then(|| "not-a-number".to_owned())
});
assert!(matches!(result, Err(ConfigError::Config(_))));
}
#[test]
fn overrides_are_applied() {
let cfg = ProxyConfig::from_lookup(|key| match key {
"FIRSTPASS_BIND" => Some("0.0.0.0:9090".to_owned()),
"FIRSTPASS_TENANT" => Some("acme".to_owned()),
_ => None,
})
.unwrap();
assert_eq!(cfg.bind, "0.0.0.0:9090");
assert_eq!(cfg.tenant_id, "acme");
}
#[test]
fn enforce_mode_is_accepted() {
let cfg =
ProxyConfig::from_lookup(|key| (key == "FIRSTPASS_MODE").then(|| "enforce".to_owned()))
.unwrap();
assert_eq!(cfg.mode, Mode::Enforce);
}
#[test]
fn unknown_mode_is_rejected() {
let result =
ProxyConfig::from_lookup(|key| (key == "FIRSTPASS_MODE").then(|| "banana".to_owned()));
assert!(matches!(result, Err(ConfigError::UnsupportedMode(m)) if m == "banana"));
}
#[test]
fn routing_config_parses_inline() {
let toml = r#"
[[route]]
match = { task_kind = "code_edit" }
mode = "enforce"
ladder = ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"]
gates = ["non-empty"]
"#;
let cfg = ProxyConfig::from_lookup(|key| match key {
"FIRSTPASS_CONFIG_TOML" => Some(toml.to_owned()),
_ => None,
})
.unwrap();
let routing = cfg.routing.expect("routing config present");
assert_eq!(routing.routes.len(), 1);
assert_eq!(routing.routes[0].mode, Mode::Enforce);
}
#[test]
fn bad_routing_config_is_an_error() {
let result = ProxyConfig::from_lookup(|key| match key {
"FIRSTPASS_CONFIG_TOML" => Some("this is not valid = = toml".to_owned()),
_ => None,
});
assert!(matches!(result, Err(ConfigError::Config(_))));
}
#[test]
fn receipts_mode_defaults_to_best_effort() {
let cfg = ProxyConfig::from_lookup(|_| None).unwrap();
assert_eq!(cfg.receipts_mode, ReceiptsMode::BestEffort);
}
#[test]
fn receipts_mode_durable_is_accepted() {
let cfg =
ProxyConfig::from_lookup(|k| (k == "FIRSTPASS_RECEIPTS").then(|| "durable".to_owned()))
.unwrap();
assert_eq!(cfg.receipts_mode, ReceiptsMode::Durable);
}
#[test]
fn receipts_mode_best_effort_explicit_is_accepted() {
let cfg = ProxyConfig::from_lookup(|k| {
(k == "FIRSTPASS_RECEIPTS").then(|| "best_effort".to_owned())
})
.unwrap();
assert_eq!(cfg.receipts_mode, ReceiptsMode::BestEffort);
}
#[test]
fn receipts_mode_unknown_is_rejected() {
let result = ProxyConfig::from_lookup(|k| {
(k == "FIRSTPASS_RECEIPTS").then(|| "never_drop".to_owned())
});
assert!(
matches!(result, Err(ConfigError::UnsupportedReceiptsMode(m)) if m == "never_drop")
);
}
#[test]
fn price_overrides_reach_the_price_table() {
let toml = "[[route]]\nmatch = {}\nmode = \"observe\"\nladder = [\"anthropic/claude-haiku-4-5\"]\n\n[[price]]\nmodel = \"anthropic/claude-haiku-4-5\"\ninput_per_mtok = 2.0\noutput_per_mtok = 10.0\n";
let cfg = ProxyConfig::from_lookup(|k| match k {
"FIRSTPASS_CONFIG_TOML" => Some(toml.to_owned()),
_ => None,
})
.unwrap();
let cost = cfg
.prices
.cost_usd("anthropic/claude-haiku-4-5", 1000, 1000)
.unwrap();
assert!((cost - 0.012).abs() < 1e-9, "override must win: {cost}");
}
}