mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
* refactor: simplify config resolution and consolidate main.rs init into AppBuilder - Add parse_bool_env() and parse_string_env() helpers to eliminate repetitive 5-line optional_env/parse/map_err/unwrap_or boilerplate across 12 config files - Add EmbeddingsConfig::create_provider() to centralize embeddings construction (fixes hardcoded 1536 dimensions and missing Ollama provider in app.rs) - Extract init_cli_tracing(), setup_wasm_channels(), start_tunnel(), run_memory_command(), run_worker(), run_claude_bridge() from main.rs - Replace ~600 lines of inline init in main.rs with AppBuilder::build_all() - Expose catalog_entries from AppComponents for gateway registry entries - Net reduction: ~738 lines across 15 files Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: propagate dev_loaded_tool_names from AppBuilder and add parse_option_env helper Address PR review feedback: - Capture dev_loaded_tool_names from WASM loading in init_extensions() and expose via AppComponents so bootstrap_hooks receives the actual dev tool names instead of an empty slice (fixes silent hook skip) - Add parse_option_env<T>() helper for Option<T> config fields, simplifying max_cost_per_day_cents and max_actions_per_hour in agent.rs Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: fetch real NEAR AI pricing and unify cost calculation path CostGuard was independently looking up pricing via costs::model_cost(), falling back to GPT-4o default rates when NEAR AI model names didn't match the static table — causing ~3x cost overestimates in logs. - Add pricing map to NearAiChatProvider that fetches real rates from /v1/model/list at startup (background, non-blocking) - Update cost_per_token() to check fetched pricing first, then static table, then default - Add cost_per_token parameter to CostGuard::record_llm_call() so the dispatcher passes provider-sourced rates directly Co-Authored-By: Claude Opus 4.6 <[email protected]> * chore: update default NEAR AI model to GLM-latest Replace fireworks llama4-maverick-instruct-basic with zai-org/GLM-latest as the default model in config and setup wizard. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: align wizard default model name with config Change "zai/GLM-latest" to "zai-org/GLM-latest" in wizard.rs to match the default in config/llm.rs. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
92 lines
2.8 KiB
Rust
92 lines
2.8 KiB
Rust
use crate::error::ConfigError;
|
|
|
|
use super::INJECTED_VARS;
|
|
|
|
/// Crate-wide mutex for tests that mutate process environment variables.
|
|
///
|
|
/// The process environment is global state shared across all threads.
|
|
/// Per-module mutexes do NOT prevent races between modules running in
|
|
/// parallel. Every `unsafe { set_var / remove_var }` call in tests
|
|
/// MUST hold this single lock.
|
|
#[cfg(test)]
|
|
pub(crate) static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
pub(crate) fn optional_env(key: &str) -> Result<Option<String>, ConfigError> {
|
|
// Check real env vars first (always win over injected secrets)
|
|
match std::env::var(key) {
|
|
Ok(val) if val.is_empty() => {}
|
|
Ok(val) => return Ok(Some(val)),
|
|
Err(std::env::VarError::NotPresent) => {}
|
|
Err(e) => {
|
|
return Err(ConfigError::ParseError(format!(
|
|
"failed to read {key}: {e}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Fall back to thread-safe overlay (secrets injected from DB)
|
|
if let Some(val) = INJECTED_VARS.get().and_then(|map| map.get(key)) {
|
|
return Ok(Some(val.clone()));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub(crate) fn parse_optional_env<T>(key: &str, default: T) -> Result<T, ConfigError>
|
|
where
|
|
T: std::str::FromStr,
|
|
T::Err: std::fmt::Display,
|
|
{
|
|
optional_env(key)?
|
|
.map(|s| {
|
|
s.parse().map_err(|e| ConfigError::InvalidValue {
|
|
key: key.to_string(),
|
|
message: format!("{e}"),
|
|
})
|
|
})
|
|
.transpose()
|
|
.map(|opt| opt.unwrap_or(default))
|
|
}
|
|
|
|
/// Parse a boolean from an env var with a default.
|
|
///
|
|
/// Accepts "true"/"1" as true, "false"/"0" as false.
|
|
pub(crate) fn parse_bool_env(key: &str, default: bool) -> Result<bool, ConfigError> {
|
|
match optional_env(key)? {
|
|
Some(s) => match s.to_lowercase().as_str() {
|
|
"true" | "1" => Ok(true),
|
|
"false" | "0" => Ok(false),
|
|
_ => Err(ConfigError::InvalidValue {
|
|
key: key.to_string(),
|
|
message: format!("must be 'true' or 'false', got '{s}'"),
|
|
}),
|
|
},
|
|
None => Ok(default),
|
|
}
|
|
}
|
|
|
|
/// Parse an env var into `Option<T>` — returns `None` when unset,
|
|
/// `Some(parsed)` when set to a valid value.
|
|
pub(crate) fn parse_option_env<T>(key: &str) -> Result<Option<T>, ConfigError>
|
|
where
|
|
T: std::str::FromStr,
|
|
T::Err: std::fmt::Display,
|
|
{
|
|
optional_env(key)?
|
|
.map(|s| {
|
|
s.parse().map_err(|e| ConfigError::InvalidValue {
|
|
key: key.to_string(),
|
|
message: format!("{e}"),
|
|
})
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
/// Parse a string from an env var with a default.
|
|
pub(crate) fn parse_string_env(
|
|
key: &str,
|
|
default: impl Into<String>,
|
|
) -> Result<String, ConfigError> {
|
|
Ok(optional_env(key)?.unwrap_or_else(|| default.into()))
|
|
}
|