use anyhow::Result;
use crate::jev::error::JevError;
use crate::jev::protocol::DEFAULT_MODEL;
use crate::utils::env::{non_empty_var, EnvSource};
use crate::utils::secret::Secret;
use crate::utils::secret_env::secret_var_any;
pub const TYPESAFE_API_KEY: &str = "TYPESAFE_API_KEY";
pub const OMNI_DEV_JEV_API_KEY: &str = "OMNI_DEV_JEV_API_KEY";
pub const OMNI_DEV_JEV_BASE_URL: &str = "OMNI_DEV_JEV_BASE_URL";
pub const TYPESAFE_MODEL: &str = "TYPESAFE_MODEL";
pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai";
#[derive(Clone)]
pub struct JevConfig {
pub api_key: Secret,
pub base_url: String,
pub model: String,
}
impl std::fmt::Debug for JevConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JevConfig")
.field("api_key", &self.api_key)
.field("base_url", &self.base_url)
.field("model", &self.model)
.finish()
}
}
impl JevConfig {
pub fn from_env() -> Result<Self> {
Self::from_env_with(&crate::utils::settings::SettingsEnv::load())
}
pub(crate) fn from_env_with(env: &impl EnvSource) -> Result<Self> {
let api_key = secret_var_any(env, &[TYPESAFE_API_KEY, OMNI_DEV_JEV_API_KEY])?
.ok_or(JevError::CredentialsNotFound)?;
let base_url = non_empty_var(env, OMNI_DEV_JEV_BASE_URL)
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
.trim_end_matches('/')
.to_string();
let model = non_empty_var(env, TYPESAFE_MODEL).unwrap_or_else(|| DEFAULT_MODEL.to_string());
Ok(Self {
api_key,
base_url,
model,
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::test_support::env::MapEnv;
#[test]
fn typesafe_api_key_wins_over_omni_dev_override() {
let env = MapEnv::new()
.with(TYPESAFE_API_KEY, "typesafe-key")
.with(OMNI_DEV_JEV_API_KEY, "omni-dev-key");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.api_key.expose_secret(), "typesafe-key");
}
#[test]
fn omni_dev_override_used_when_typesafe_key_unset() {
let env = MapEnv::new().with(OMNI_DEV_JEV_API_KEY, "omni-dev-key");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.api_key.expose_secret(), "omni-dev-key");
}
#[test]
fn empty_typesafe_key_counts_as_unset() {
let env = MapEnv::new()
.with(TYPESAFE_API_KEY, "")
.with(OMNI_DEV_JEV_API_KEY, "omni-dev-key");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.api_key.expose_secret(), "omni-dev-key");
}
#[test]
fn missing_api_key_is_credentials_not_found() {
let env = MapEnv::new();
let err = JevConfig::from_env_with(&env).unwrap_err();
assert!(matches!(
err.downcast_ref::<JevError>(),
Some(JevError::CredentialsNotFound)
));
}
#[test]
fn base_url_defaults_when_unset() {
let env = MapEnv::new().with(TYPESAFE_API_KEY, "k");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.base_url, DEFAULT_BASE_URL);
}
#[test]
fn base_url_override_trims_trailing_slash() {
let env = MapEnv::new()
.with(TYPESAFE_API_KEY, "k")
.with(OMNI_DEV_JEV_BASE_URL, "http://proxy.example:8080/");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.base_url, "http://proxy.example:8080");
}
#[test]
fn model_defaults_when_unset() {
let env = MapEnv::new().with(TYPESAFE_API_KEY, "k");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.model, DEFAULT_MODEL);
}
#[test]
fn model_override_resolves() {
let env = MapEnv::new()
.with(TYPESAFE_API_KEY, "k")
.with(TYPESAFE_MODEL, "jev-1.13.0");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.model, "jev-1.13.0");
}
#[test]
fn omni_dev_model_is_ignored() {
let env = MapEnv::new()
.with(TYPESAFE_API_KEY, "k")
.with("OMNI_DEV_MODEL", "claude-opus-5");
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.model, DEFAULT_MODEL);
}
#[test]
fn debug_redacts_api_key() {
let env = MapEnv::new().with(TYPESAFE_API_KEY, "sekret-value");
let config = JevConfig::from_env_with(&env).unwrap();
let debug = format!("{config:?}");
assert!(!debug.contains("sekret-value"));
assert!(debug.contains("api_key: <redacted>"));
}
#[test]
fn api_key_can_come_from_a_file() {
let (_dir, path) = crate::test_support::env::secret_file("from-file\n");
let env = MapEnv::new().with("OMNI_DEV_JEV_API_KEY_FILE", &path);
let config = JevConfig::from_env_with(&env).unwrap();
assert_eq!(config.api_key.expose_secret(), "from-file");
}
}