use crate::config;
use std::sync::OnceLock;
#[derive(Debug, Clone, Default)]
pub struct RuntimeOverrides {
pub embedding_dim: Option<u32>,
pub llm_model: Option<String>,
pub llm_fallback: Option<String>,
pub skip_embedding_on_failure: bool,
pub llm_max_host_concurrency: Option<usize>,
pub llm_slot_wait_secs: Option<u64>,
pub llm_slot_no_wait: bool,
pub openrouter_timeout: Option<u64>,
pub log_level: Option<String>,
pub log_format: Option<String>,
pub lang: Option<String>,
pub display_tz: Option<String>,
pub db_path: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct PathOverrides {
pub config_dir: Option<String>,
pub cache_dir: Option<String>,
}
static PATHS: OnceLock<PathOverrides> = OnceLock::new();
pub fn init_paths(overrides: PathOverrides) {
let _ = PATHS.set(overrides);
}
fn paths() -> PathOverrides {
PATHS.get().cloned().unwrap_or_default()
}
static RUNTIME: OnceLock<RuntimeOverrides> = OnceLock::new();
pub fn init(overrides: RuntimeOverrides) {
let _ = RUNTIME.set(overrides);
}
pub fn get() -> RuntimeOverrides {
RUNTIME.get().cloned().unwrap_or_default()
}
pub fn config_dir_override() -> Option<String> {
paths()
.config_dir
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn cache_dir_override() -> Option<String> {
if let Some(v) = paths().cache_dir {
if !v.trim().is_empty() {
return Some(v.trim().to_string());
}
}
config::get_setting("cache.dir")
.ok()
.flatten()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn resolve_string(flag: Option<&str>, xdg_key: &str, default: &str) -> String {
if let Some(v) = flag {
if !v.is_empty() {
return v.to_string();
}
}
if let Ok(Some(v)) = config::get_setting(xdg_key) {
if !v.is_empty() {
return v;
}
}
default.to_string()
}
pub fn resolve_optional_string(flag: Option<&str>, xdg_key: &str) -> Option<String> {
if let Some(v) = flag {
if !v.is_empty() {
return Some(v.to_string());
}
}
config::get_setting(xdg_key)
.ok()
.flatten()
.filter(|s| !s.is_empty())
}
pub fn resolve_usize(flag: Option<usize>, xdg_key: &str, default: usize) -> usize {
if let Some(v) = flag {
return v;
}
if let Ok(Some(v)) = config::get_setting(xdg_key) {
if let Ok(n) = v.parse::<usize>() {
return n;
}
}
default
}
pub fn resolve_u64(flag: Option<u64>, xdg_key: &str, default: u64) -> u64 {
if let Some(v) = flag {
return v;
}
if let Ok(Some(v)) = config::get_setting(xdg_key) {
if let Ok(n) = v.parse::<u64>() {
return n;
}
}
default
}
pub fn resolve_f64(flag: Option<f64>, xdg_key: &str, default: f64) -> f64 {
if let Some(v) = flag {
return v;
}
if let Ok(Some(v)) = config::get_setting(xdg_key) {
if let Ok(n) = v.parse::<f64>() {
return n;
}
}
default
}
pub fn resolve_bool(flag_set: bool, xdg_key: &str, default: bool) -> bool {
if flag_set {
return true;
}
if let Ok(Some(v)) = config::get_setting(xdg_key) {
let t = v.trim().to_ascii_lowercase();
return matches!(t.as_str(), "1" | "true" | "yes" | "on");
}
default
}
pub fn embedding_dim_override() -> Option<u32> {
let rt = get();
if let Some(d) = rt.embedding_dim {
return Some(d);
}
if let Ok(Some(v)) = config::get_setting("embedding.dim") {
if let Ok(n) = v.parse::<u32>() {
if crate::constants::EMBEDDING_DIM_RANGE.contains(&(n as usize)) {
return Some(n);
}
}
}
None
}
pub fn embedding_model(cli: Option<&str>) -> Option<String> {
if let Some(m) = cli.map(str::trim).filter(|m| !m.is_empty()) {
return Some(m.to_string());
}
match config::get_setting("embedding.model") {
Ok(Some(v)) if !v.trim().is_empty() => Some(v.trim().to_string()),
_ => None,
}
}
pub fn embedding_backend(
cli: Option<crate::backend_choice::EmbeddingBackendChoice>,
) -> crate::backend_choice::EmbeddingBackendChoice {
use crate::backend_choice::EmbeddingBackendChoice as B;
if let Some(v) = cli {
return v;
}
match config::get_setting("embedding.backend") {
Ok(Some(v)) => match v.trim().to_ascii_lowercase().as_str() {
"openrouter" | "open-router" => B::Openrouter,
"auto" => B::Auto,
_ => B::Auto,
},
_ => B::Auto,
}
}
pub fn llm_backend(
cli: Option<crate::backend_choice::LlmBackendChoice>,
) -> crate::backend_choice::LlmBackendChoice {
use crate::backend_choice::LlmBackendChoice as B;
if let Some(v) = cli {
return v;
}
match config::get_setting("llm.backend") {
Ok(Some(v)) => match v.trim().to_ascii_lowercase().as_str() {
"none" => B::None,
"openrouter" | "open-router" => B::OpenRouter,
_ => B::OpenRouter,
},
_ => B::OpenRouter,
}
}
pub fn skip_embedding_on_failure() -> bool {
let rt = get();
resolve_bool(
rt.skip_embedding_on_failure,
"llm.skip_embedding_on_failure",
false,
)
}
pub fn llm_max_host_concurrency(default: usize) -> usize {
let rt = get();
resolve_usize(
rt.llm_max_host_concurrency,
"llm.max_host_concurrency",
default,
)
}
pub fn llm_slot_wait_secs(default: u64) -> u64 {
let rt = get();
if rt.llm_slot_no_wait {
return 0;
}
resolve_u64(rt.llm_slot_wait_secs, "llm.slot_wait_secs", default)
}
pub fn llm_slot_no_wait() -> bool {
let rt = get();
resolve_bool(rt.llm_slot_no_wait, "llm.slot_no_wait", false)
}
pub fn llm_model() -> Option<String> {
let rt = get();
resolve_optional_string(rt.llm_model.as_deref(), "llm.model")
}
pub fn llm_fallback(default: &str) -> String {
let rt = get();
resolve_string(rt.llm_fallback.as_deref(), "llm.fallback", default)
}
pub fn openrouter_chat_timeout_secs(default: u64) -> u64 {
let rt = get();
resolve_u64(
rt.openrouter_timeout,
"llm.openrouter_timeout_secs",
default,
)
}
pub fn openrouter_timeout_override() -> Option<u64> {
get().openrouter_timeout
}
pub fn log_level(default: &str) -> String {
let rt = get();
resolve_string(rt.log_level.as_deref(), "log.level", default)
}
pub fn log_format(default: &str) -> String {
let rt = get();
resolve_string(rt.log_format.as_deref(), "log.format", default)
}
pub fn max_entities_per_memory(default: usize) -> usize {
resolve_usize(None, "limits.max_entities_per_memory", default)
}
pub fn max_relations_per_memory(default: usize) -> usize {
resolve_usize(None, "limits.max_relations_per_memory", default)
}
pub fn openrouter_chat_url(default: &str) -> String {
resolve_string_with_aliases(
None,
&["network.openrouter.chat_url", "network.chat_url"],
default,
)
}
pub fn openrouter_embeddings_url(default: &str) -> String {
resolve_string_with_aliases(
None,
&["network.openrouter.embeddings_url", "network.embed_url"],
default,
)
}
pub fn llm_probe_timeout_ms(default: u64) -> u64 {
resolve_u64(None, "llm.probe_timeout_ms", default)
}
pub fn rayon_threads(default: usize) -> usize {
let n = resolve_usize(None, "parallelism.rayon_threads", default);
if n == 0 {
default
} else {
n
}
}
pub fn embed_runtime_worker_threads(default: usize) -> usize {
let n = resolve_usize(None, "parallelism.embed_runtime_threads", default);
if n == 0 {
default
} else {
n
}
}
pub fn agent_surface_max_items(flag: Option<usize>) -> usize {
resolve_usize(
flag,
"agent_surface.max_items",
crate::constants::DEFAULT_AGENT_SURFACE_MAX_ITEMS,
)
}
pub fn agent_surface_truncate_content(flag: Option<usize>) -> usize {
resolve_usize(
flag,
"agent_surface.truncate_content",
crate::constants::DEFAULT_AGENT_SURFACE_TRUNCATE_CONTENT,
)
}
pub fn agent_surface_max_output_bytes(flag: Option<usize>) -> usize {
resolve_usize(
flag,
"agent_surface.max_output_bytes",
crate::constants::DEFAULT_AGENT_SURFACE_MAX_OUTPUT_BYTES,
)
}
pub fn db_busy_retries(default: u32) -> u32 {
resolve_u64(None, "db.busy_retries", u64::from(default)) as u32
}
pub fn db_busy_base_delay_ms(default: u64) -> u64 {
resolve_u64(None, "db.busy_base_delay_ms", default)
}
pub fn db_query_timeout_ms(default: u64) -> u64 {
resolve_u64(None, "db.query_timeout_ms", default)
}
pub fn embedding_batch_size(default: usize) -> usize {
resolve_usize(None, "embedding.batch_size", default).max(1)
}
pub fn reembed_claim_batch() -> usize {
let range = crate::constants::REEMBED_CLAIM_BATCH_RANGE;
resolve_usize(
None,
"enrich.reembed_claim_batch",
crate::constants::DEFAULT_REEMBED_CLAIM_BATCH,
)
.clamp(*range.start(), *range.end())
}
pub fn enrich_scan_page_size(cli: Option<usize>) -> usize {
let range = crate::constants::ENRICH_SCAN_PAGE_SIZE_RANGE;
resolve_usize(
cli,
"enrich.scan_page_size",
crate::constants::DEFAULT_ENRICH_SCAN_PAGE_SIZE,
)
.clamp(*range.start(), *range.end())
}
pub fn rate_limit_deadline_secs() -> std::time::Duration {
let secs = resolve_u64(
None,
"enrich.rate_limit_deadline_secs",
crate::constants::DEFAULT_RATE_LIMIT_DEADLINE_SECS,
);
let secs = if secs == 0 {
crate::constants::DEFAULT_RATE_LIMIT_DEADLINE_SECS
} else {
secs
};
std::time::Duration::from_secs(secs)
}
pub fn enrich_circuit_breaker_reset_secs() -> std::time::Duration {
let secs = resolve_u64(
None,
"enrich.circuit_breaker_reset_secs",
crate::constants::DEFAULT_ENRICH_CIRCUIT_BREAKER_RESET_SECS,
);
let secs = if secs == 0 {
crate::constants::DEFAULT_ENRICH_CIRCUIT_BREAKER_RESET_SECS
} else {
secs
};
std::time::Duration::from_secs(secs)
}
pub fn stdin_timeout_secs() -> u64 {
let secs = resolve_u64(
None,
"cli.stdin_timeout_secs",
crate::constants::DEFAULT_STDIN_READ_TIMEOUT_SECS,
);
if secs == 0 {
crate::constants::DEFAULT_STDIN_READ_TIMEOUT_SECS
} else {
secs
}
}
pub fn no_input(flag: bool) -> bool {
resolve_bool(flag, "cli.no_input", false)
}
fn resolve_string_with_aliases(flag: Option<&str>, keys: &[&str], default: &str) -> String {
if let Some(v) = flag {
if !v.is_empty() {
return v.to_string();
}
}
for key in keys {
if let Ok(Some(v)) = config::get_setting(key) {
if !v.is_empty() {
return v;
}
}
}
default.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_string_prefers_flag() {
assert_eq!(
resolve_string(Some("from-flag"), "nonexistent.key.xyz", "def"),
"from-flag"
);
}
#[test]
fn resolve_string_falls_to_default() {
assert_eq!(
resolve_string(None, "nonexistent.key.xyz.zzz", "def"),
"def"
);
}
}