mod client;
mod resolve;
mod types;
pub use client::{build_http_client, client_for_config, create_client};
pub(crate) use types::default_timeout_secs;
pub use types::{
AgentConfig, AppConfig, McpServerConfig, ModelsInfo, ProviderConfig, ProviderModels,
};
use std::path::PathBuf;
use crate::error::{Error, Result};
const DEFAULT_CONFIG: &str = include_str!("config.toml.default");
const BUILTIN_PROVIDER_DEFAULTS: &[(&str, &str, &str)] = &[
("openai", "https://api.openai.com/v1", "gpt-4o"),
(
"anthropic",
"https://api.anthropic.com/v1",
"claude-sonnet-4-6",
),
(
"gemini",
"https://generativelanguage.googleapis.com/v1beta",
"gemini-2.0-flash",
),
];
pub(crate) fn builtin_provider_defaults(provider: &str) -> (&'static str, &'static str) {
BUILTIN_PROVIDER_DEFAULTS
.iter()
.find(|(name, ..)| *name == provider)
.map(|(_, api_base, model)| (*api_base, *model))
.unwrap_or_else(|| {
let (_, api_base, model) = BUILTIN_PROVIDER_DEFAULTS[0];
(api_base, model)
})
}
pub fn config_dir() -> Result<PathBuf> {
let home =
dirs::home_dir().ok_or_else(|| Error::config("Could not determine home directory"))?;
Ok(home.join(".openheim"))
}
pub fn config_path() -> Result<PathBuf> {
Ok(config_dir()?.join("config.toml"))
}
const DEFAULT_SYSTEM_MD: &str = "You are Openheim, a multipurpose, multiprovider LLM agent.";
pub fn init_config() -> Result<PathBuf> {
let dir = config_dir()?;
std::fs::create_dir_all(&dir)?;
let system_path = dir.join("system.md");
let system_written = !system_path.exists();
if system_written {
std::fs::write(&system_path, DEFAULT_SYSTEM_MD)?;
}
let config_path = dir.join("config.toml");
if config_path.exists() {
let system_note = if system_written {
format!("system.md has been created at {}.", system_path.display())
} else {
format!("system.md is available at {}.", system_path.display())
};
return Err(Error::config(format!(
"Config file already exists at {}. {}",
config_path.display(),
system_note
)));
}
std::fs::write(&config_path, DEFAULT_CONFIG)?;
Ok(config_path)
}
pub fn load_config_from(path: impl AsRef<std::path::Path>) -> Result<AppConfig> {
let path = path.as_ref();
if !path.exists() {
return Err(Error::config(format!(
"Config file not found at {}",
path.display()
)));
}
let contents = std::fs::read_to_string(path)?;
let config: AppConfig = toml::from_str(&contents)?;
Ok(config)
}
pub fn load_config() -> Result<AppConfig> {
let path = config_path()?;
if !path.exists() {
return Err(Error::config(format!(
"Config file not found at {}. Run `openheim init` to create one.",
path.display()
)));
}
let contents = std::fs::read_to_string(&path)?;
let config: AppConfig = toml::from_str(&contents)?;
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_provider_defaults_known_providers() {
assert_eq!(
builtin_provider_defaults("openai"),
("https://api.openai.com/v1", "gpt-4o")
);
assert_eq!(
builtin_provider_defaults("anthropic"),
("https://api.anthropic.com/v1", "claude-sonnet-4-6")
);
assert_eq!(
builtin_provider_defaults("gemini"),
(
"https://generativelanguage.googleapis.com/v1beta",
"gemini-2.0-flash"
)
);
}
#[test]
fn builtin_provider_defaults_unknown_provider_falls_back_to_openai() {
assert_eq!(
builtin_provider_defaults("some-custom-endpoint"),
builtin_provider_defaults("openai")
);
}
#[test]
fn config_toml_default_matches_builtin_provider_defaults() {
let config: AppConfig = toml::from_str(DEFAULT_CONFIG).unwrap();
for provider in ["openai", "anthropic"] {
let (expected_api_base, expected_model) = builtin_provider_defaults(provider);
let shipped = &config.providers[provider];
assert_eq!(shipped.api_base, expected_api_base, "provider: {provider}");
assert_eq!(
shipped.default_model, expected_model,
"provider: {provider}"
);
assert!(
shipped.models.contains(&expected_model.to_string()),
"provider {provider}'s default_model must be in its own models list"
);
}
let (gemini_api_base, gemini_model) = builtin_provider_defaults("gemini");
assert!(DEFAULT_CONFIG.contains(gemini_api_base));
assert!(DEFAULT_CONFIG.contains(gemini_model));
}
}