diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 5a383d81d..c17c34fa2 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -40,6 +40,9 @@ pub struct AppState { /// Avoids blocking the `/api/providers` endpoint on TCP timeouts to /// unreachable local services. 60-second TTL. pub provider_probe_cache: openfang_runtime::provider_health::ProbeCache, + /// Thread-safe mutable budget config. Updated via PUT /api/budget. + /// Initialized from `kernel.config.budget` at startup. + pub budget_config: Arc>, } /// POST /api/agents — Spawn a new agent. @@ -5266,10 +5269,8 @@ pub async fn usage_daily(State(state): State>) -> impl IntoRespons /// GET /api/budget — Current budget status (limits, spend, % used). pub async fn budget_status(State(state): State>) -> impl IntoResponse { - let status = state - .kernel - .metering - .budget_status(&state.kernel.config.budget); + let budget = state.budget_config.read().await; + let status = state.kernel.metering.budget_status(&budget); Json(serde_json::to_value(&status).unwrap_or_default()) } @@ -5278,34 +5279,26 @@ pub async fn update_budget( State(state): State>, Json(body): Json, ) -> impl IntoResponse { - // SAFETY: Budget config is updated in-place. Since KernelConfig is behind - // an Arc and we only have &self, we use ptr mutation (same pattern as OFP). - let config_ptr = &state.kernel.config as *const openfang_types::config::KernelConfig - as *mut openfang_types::config::KernelConfig; - - // Apply updates - unsafe { + { + let mut budget = state.budget_config.write().await; if let Some(v) = body["max_hourly_usd"].as_f64() { - (*config_ptr).budget.max_hourly_usd = v; + budget.max_hourly_usd = v; } if let Some(v) = body["max_daily_usd"].as_f64() { - (*config_ptr).budget.max_daily_usd = v; + budget.max_daily_usd = v; } if let Some(v) = body["max_monthly_usd"].as_f64() { - (*config_ptr).budget.max_monthly_usd = v; + budget.max_monthly_usd = v; } if let Some(v) = body["alert_threshold"].as_f64() { - (*config_ptr).budget.alert_threshold = v.clamp(0.0, 1.0); + budget.alert_threshold = v.clamp(0.0, 1.0); } if let Some(v) = body["default_max_llm_tokens_per_hour"].as_u64() { - (*config_ptr).budget.default_max_llm_tokens_per_hour = v; + budget.default_max_llm_tokens_per_hour = v; } } - - let status = state - .kernel - .metering - .budget_status(&state.kernel.config.budget); + let budget = state.budget_config.read().await; + let status = state.kernel.metering.budget_status(&budget); Json(serde_json::to_value(&status).unwrap_or_default()) } diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index b6980928c..21a51d4d5 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -51,6 +51,7 @@ pub async fn build_router( shutdown_notify: Arc::new(tokio::sync::Notify::new()), clawhub_cache: dashmap::DashMap::new(), provider_probe_cache: openfang_runtime::provider_health::ProbeCache::new(), + budget_config: Arc::new(tokio::sync::RwLock::new(kernel.config.budget.clone())), }); // CORS: allow localhost origins by default. If API key is set, the API