use std::cell::RefCell;
use std::sync::{OnceLock, RwLock};
use super::*;
static CONFIG: OnceLock<ProvidersConfig> = OnceLock::new();
static CONFIG_PATH: OnceLock<String> = OnceLock::new();
static RUNTIME_CATALOG_OVERLAY: OnceLock<RwLock<Option<ProvidersConfig>>> = OnceLock::new();
thread_local! {
static USER_OVERRIDES: RefCell<Option<ProvidersConfig>> = const { RefCell::new(None) };
}
pub fn load_config() -> &'static ProvidersConfig {
CONFIG.get_or_init(|| {
let mut config = default_config();
let verbose_config_logging = matches!(
std::env::var("HARN_VERBOSE_CONFIG").ok().as_deref(),
Some("1" | "true" | "TRUE" | "yes" | "YES")
) || matches!(
std::env::var("HARN_ACP_VERBOSE").ok().as_deref(),
Some("1" | "true" | "TRUE" | "yes" | "YES")
);
if let Ok(path) = std::env::var("HARN_PROVIDERS_CONFIG") {
if let Some(overlay) = read_external_config(&path, verbose_config_logging) {
config.merge_from(&overlay);
let _ = CONFIG_PATH.set(path);
return config;
}
}
if should_load_home_config() {
if let Some(home) = dirs_or_home() {
let path = format!("{home}/.config/harn/providers.toml");
if let Some(overlay) = read_external_config(&path, false) {
config.merge_from(&overlay);
let _ = CONFIG_PATH.set(path);
return config;
}
}
}
config
})
}
fn read_external_config(path: &str, verbose: bool) -> Option<ProvidersConfig> {
match std::fs::read_to_string(path) {
Ok(content) => match parse_config_toml(&content) {
Ok(config) => {
if verbose {
eprintln!(
"[llm_config] Loaded {} providers, {} aliases from {}",
config.providers.len(),
config.aliases.len(),
path
);
}
Some(config)
}
Err(error) => {
eprintln!("[llm_config] TOML parse error in {path}: {error}");
None
}
},
Err(error) => {
if verbose {
eprintln!("[llm_config] Cannot read {path}: {error}");
}
None
}
}
}
fn should_load_home_config() -> bool {
!cfg!(test)
}
pub fn parse_config_toml(src: &str) -> Result<ProvidersConfig, toml::de::Error> {
toml::from_str::<ProvidersConfig>(src)
}
pub fn loaded_config_path() -> Option<std::path::PathBuf> {
let _ = load_config();
CONFIG_PATH.get().map(std::path::PathBuf::from)
}
pub fn set_user_overrides(config: Option<ProvidersConfig>) {
USER_OVERRIDES.with(|cell| *cell.borrow_mut() = config);
}
pub fn clear_user_overrides() {
set_user_overrides(None);
}
pub fn set_runtime_catalog_overlay(config: Option<ProvidersConfig>) {
*runtime_catalog_overlay()
.write()
.expect("runtime catalog overlay poisoned") = config;
}
pub fn clear_runtime_catalog_overlay() {
set_runtime_catalog_overlay(None);
}
pub(crate) fn effective_config() -> ProvidersConfig {
let user_overrides = USER_OVERRIDES.with(|cell| cell.borrow().clone());
effective_config_with_user_overrides(user_overrides.as_ref())
}
pub fn embedded_config(explicit_overlay: Option<&ProvidersConfig>) -> ProvidersConfig {
let mut config = default_config();
if let Some(overlay) = explicit_overlay {
config.merge_from(overlay);
}
config
}
pub(crate) fn effective_config_with_user_overrides(
user_overrides: Option<&ProvidersConfig>,
) -> ProvidersConfig {
let mut merged = load_config().clone();
if let Some(overlay) = runtime_catalog_overlay()
.read()
.expect("runtime catalog overlay poisoned")
.as_ref()
{
merged.merge_from(overlay);
}
if let Some(overlay) = user_overrides {
merged.merge_from(overlay);
}
merged
}
fn runtime_catalog_overlay() -> &'static RwLock<Option<ProvidersConfig>> {
RUNTIME_CATALOG_OVERLAY.get_or_init(|| RwLock::new(None))
}
fn dirs_or_home() -> Option<String> {
crate::user_dirs::home_dir().map(|home| home.to_string_lossy().into_owned())
}
const EMBEDDED_PROVIDERS_TOML: &str = include_str!("../llm/providers.toml");
pub(crate) fn default_config() -> ProvidersConfig {
parse_config_toml(EMBEDDED_PROVIDERS_TOML)
.expect("embedded providers.toml must parse — invariant checked by harn-vm tests")
}
#[cfg(test)]
pub(crate) fn merge_global_config(overlay: ProvidersConfig) -> ProvidersConfig {
let mut config = default_config();
config.merge_from(&overlay);
config
}