diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 5c630084..05a28bc3 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -333,14 +333,10 @@ the data is fetched on tab activation and on window change. Rendered with plain tables + CSS bars — the dashboard bundle ships no chart library. -The cost figure is a deliberately rough estimate from a per-model -price table (`est_cost_usd`); it drifts with list pricing and is -labelled accordingly. The table is operator-tunable via the -`services.hyperhive.modelPrices` nix option — each key is a -model-family short name (matched case-insensitively as a substring of -the model id, longest match wins) mapping to -`{ input, output, cache_read, cache_write }` USD-per-million-token -prices. Models not covered fall back to hive-c0re's built-in estimate. +The cost figure is a deliberately rough estimate from an approximate +per-model price table (`est_cost_usd`); it drifts with list pricing and +is labelled accordingly. (A follow-up can move the table to a nix +option so it's operator-tunable.) ## P33RS tab diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 690bc2d1..c9846c9a 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -86,14 +86,6 @@ pub struct Coordinator { pub agent_cpu_quota: String, /// Per-agent systemd `MemoryMax=` value (e.g. `"4G"`). Same drop-in. pub agent_memory_max: String, - /// Operator-tunable model→price table backing the hive-wide cost - /// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices` - /// and passed to `hive-c0re serve --model-prices `. Models not - /// in the table fall back to the built-in estimate in `hive_stats`. - /// hive-c0re-local (read only by the `/api/stats-hive` handler), so — - /// unlike `context_window_tokens` — it is *not* part of `HiveEnv` and - /// is never injected into containers. - pub model_prices: crate::hive_stats::PriceTable, agents: Mutex>, /// Agents whose lifecycle action (currently just spawn) is in flight. /// Read by the dashboard to render a spinner; cleared when the action @@ -287,7 +279,6 @@ impl Coordinator { context_window_tokens: std::collections::HashMap, agent_cpu_quota: String, agent_memory_max: String, - model_prices: crate::hive_stats::PriceTable, ) -> Result { let broker = Broker::open(db_path).context("open broker")?; let approvals = Approvals::open(db_path).context("open approvals")?; @@ -323,7 +314,6 @@ impl Coordinator { context_window_tokens, agent_cpu_quota, agent_memory_max, - model_prices, agents: Mutex::new(HashMap::new()), transient: Mutex::new(HashMap::new()), recent_transient: Mutex::new(HashMap::new()), diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 9d9e3b6f..d452217e 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -1791,16 +1791,9 @@ struct StatsHiveQuery { /// Hive-wide turn-stats rollup for the dashboard swarm-stats view. /// Aggregates every agent's `hyperhive-turn-stats.sqlite` read-only /// (skips missing/unreadable ones). Window defaults to `24h`. -async fn api_stats_hive( - State(state): State, - axum::extract::Query(q): axum::extract::Query, -) -> Response { +async fn api_stats_hive(axum::extract::Query(q): axum::extract::Query) -> Response { let window = crate::hive_stats::Window::parse(q.window.as_deref().unwrap_or("24h")); - axum::Json(crate::hive_stats::hive_snapshot( - window, - &state.coord.model_prices, - )) - .into_response() + axum::Json(crate::hive_stats::hive_snapshot(window)).into_response() } /// Live per-agent-container CPU + memory load from cgroup v2. Samples diff --git a/hive-c0re/src/hive_stats.rs b/hive-c0re/src/hive_stats.rs index 3951fb40..7f23b7df 100644 --- a/hive-c0re/src/hive_stats.rs +++ b/hive-c0re/src/hive_stats.rs @@ -20,7 +20,7 @@ use std::path::Path; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use rusqlite::{Connection, OpenFlags}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use crate::coordinator::Coordinator; @@ -73,53 +73,33 @@ impl Window { } } -/// Approximate USD price per **million** tokens for one model class. -/// The four fields map to the four token kinds the turn-stats schema -/// records. This is a deliberately rough estimate — Anthropic list -/// pricing drifts, so the dashboard labels the figure as an estimate. -/// -/// Operators keep the table current without a code change via the -/// `services.hyperhive.modelPrices` nix option (passed to -/// `hive-c0re serve --model-prices `, held on the `Coordinator` -/// as a [`PriceTable`]). Any model not covered by the operator table -/// falls back to [`builtin_prices`]. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct Prices { - pub input: f64, - pub output: f64, - pub cache_read: f64, - pub cache_write: f64, +/// Approximate USD price per **million** tokens, per model class. +/// Matched by substring against the model id. This is a deliberately +/// rough estimate — Anthropic list pricing drifts, so the dashboard +/// labels the figure as an estimate. A follow-up can wire it from a +/// nix option (like `contextWindowTokens`) instead of hard-coding. +struct Prices { + input: f64, + output: f64, + cache_read: f64, + cache_write: f64, } -/// Operator-tunable model→price map. Keys are matched -/// case-insensitively as a substring of the model id (e.g. `"sonnet"` -/// matches `"claude-sonnet-4-5"`); the longest matching key wins so a -/// specific entry beats a generic family name. An empty map means -/// every model uses [`builtin_prices`]. -pub type PriceTable = HashMap; - -/// Built-in fallback pricing, per **million** tokens. Used when the -/// operator's [`PriceTable`] has no key matching the model. Figures are -/// current Anthropic list prices for the Claude 4.x family (Opus 4.x, -/// Sonnet 4.x, Haiku 4.5); `cache_write` uses the 1-hour cache-TTL price, -/// which is the default through the Claude subscription the agents run -/// on. Keep in sync with the `services.hyperhive.modelPrices` nix default -/// (`nix/modules/hive-c0re.nix`). -fn builtin_prices(model: &str) -> Prices { +fn model_prices(model: &str) -> Prices { let m = model.to_ascii_lowercase(); if m.contains("opus") { Prices { - input: 5.0, - output: 25.0, - cache_read: 0.5, - cache_write: 10.0, + input: 15.0, + output: 75.0, + cache_read: 1.5, + cache_write: 18.75, } } else if m.contains("haiku") { Prices { - input: 1.0, - output: 5.0, - cache_read: 0.1, - cache_write: 2.0, + input: 0.8, + output: 4.0, + cache_read: 0.08, + cache_write: 1.0, } } else { // sonnet + unknown fallback @@ -127,37 +107,11 @@ fn builtin_prices(model: &str) -> Prices { input: 3.0, output: 15.0, cache_read: 0.3, - cache_write: 6.0, + cache_write: 3.75, } } } -/// Resolve the price for `model`: prefer the operator-provided -/// `table` (longest matching key wins, so a specific -/// `claude-3-5-sonnet` entry beats a generic `sonnet`), else fall back -/// to [`builtin_prices`]. -fn resolve_prices(model: &str, table: &PriceTable) -> Prices { - let m = model.to_ascii_lowercase(); - let mut best: Option<(usize, Prices)> = None; - for (k, v) in table { - let key = k.to_ascii_lowercase(); - if key.is_empty() || !m.contains(&key) { - continue; - } - let better = match best { - Some((blen, _)) => key.len() > blen, - None => true, - }; - if better { - best = Some((key.len(), *v)); - } - } - match best { - Some((_, p)) => p, - None => builtin_prices(&m), - } -} - #[derive(Debug, Serialize)] pub struct KeyCount { pub key: String, @@ -225,7 +179,7 @@ fn u64_from_i64(v: i64) -> u64 { /// Aggregate one agent's turn-stats over `[from, now]`. Errors bubble /// up so the caller can skip a single bad/locked db without failing /// the whole endpoint. -fn read_agent(path: &Path, from: i64, prices: &PriceTable) -> rusqlite::Result { +fn read_agent(path: &Path, from: i64) -> rusqlite::Result { let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?; // turn_stats is rollback-journal (not WAL): a read landing while the // harness is mid-INSERT would get SQLITE_BUSY and drop that active @@ -254,7 +208,7 @@ fn read_agent(path: &Path, from: i64, prices: &PriceTable) -> rusqlite::Result rusqlite::Result HiveStats { +pub fn hive_snapshot(window: Window) -> HiveStats { let now = now_secs(); let from = now - window.span_secs(); @@ -293,7 +247,7 @@ pub fn hive_snapshot(window: Window, prices: &PriceTable) -> HiveStats { if !path.exists() { continue; } - let agg = match read_agent(&path, from, prices) { + let agg = match read_agent(&path, from) { Ok(a) => a, Err(e) => { tracing::warn!(agent = %name, error = ?e, "hive-stats: read failed; skipping"); diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index e37fec13..53a3f452 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -82,19 +82,6 @@ enum Cmd { /// Set via `services.hyperhive.agentMemoryMax`. #[arg(long, default_value = "4G")] agent_memory_max: String, - /// Per-model USD prices (per million tokens) for the hive-wide - /// ST4TS cost estimate, as a JSON object mapping a model-family - /// short name to `{input, output, cache_read, cache_write}`. - /// Keys are matched case-insensitively as a substring of the - /// model id; models not covered fall back to the built-in - /// estimate. Set via the `services.hyperhive.modelPrices` NixOS - /// option (whose default carries the full opus/sonnet/haiku - /// table — kept in sync with the in-code `builtin_prices`). - /// Defaults to `{}` here so a bare `hive-c0re serve` leans - /// entirely on `builtin_prices`; the NixOS default (every real - /// deployment) shadows it with the same operator-tunable table. - #[arg(long, default_value = "{}")] - model_prices: String, }, /// Spawn a new agent container directly (`hive-agent-`). Bypasses /// the approval queue — use only as an operator on the host. For @@ -165,7 +152,6 @@ async fn main() -> Result<()> { context_window_tokens, agent_cpu_quota, agent_memory_max, - model_prices, } => { cmd_serve( hyperhive_flake, @@ -177,7 +163,6 @@ async fn main() -> Result<()> { context_window_tokens, agent_cpu_quota, agent_memory_max, - model_prices, &cli.socket, ) .await @@ -236,7 +221,6 @@ async fn cmd_serve( context_window_tokens: String, agent_cpu_quota: String, agent_memory_max: String, - model_prices: String, socket: &std::path::Path, ) -> Result<()> { // Move any host-side state still at the legacy flat layout into its @@ -246,8 +230,6 @@ async fn cmd_serve( hive_c0re::paths::relocate_legacy_state(); let cwt: std::collections::HashMap = serde_json::from_str(&context_window_tokens) .context("--context-window-tokens: invalid JSON")?; - let prices: hive_c0re::hive_stats::PriceTable = - serde_json::from_str(&model_prices).context("--model-prices: invalid JSON")?; let coord = Arc::new(Coordinator::open( &db, hyperhive_flake, @@ -258,7 +240,6 @@ async fn cmd_serve( cwt, agent_cpu_quota, agent_memory_max, - prices, )?); manager_server::start(coord.clone())?; // Idempotent pre-flight: rewrite pre-meta-layout applied diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 05291b6d..d66bc99b 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -423,80 +423,6 @@ in ''; }; - modelPrices = lib.mkOption { - type = lib.types.attrsOf ( - lib.types.submodule { - options = { - input = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million input tokens."; - }; - output = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million output tokens."; - }; - cache_read = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million cache-read tokens."; - }; - cache_write = lib.mkOption { - type = lib.types.numbers.nonnegative; - description = "USD per million cache-creation (write) tokens."; - }; - }; - } - ); - # Current Anthropic list prices for the Claude 4.x family (Opus - # 4.x, Sonnet 4.x, Haiku 4.5); cache_write is the 1-hour cache-TTL - # price (the default through the Claude subscription the agents run - # on). Keep in sync with `builtin_prices` in - # hive-c0re/src/hive_stats.rs. - default = { - opus = { - input = 5.0; - output = 25.0; - cache_read = 0.5; - cache_write = 10.0; - }; - sonnet = { - input = 3.0; - output = 15.0; - cache_read = 0.3; - cache_write = 6.0; - }; - haiku = { - input = 1.0; - output = 5.0; - cache_read = 0.1; - cache_write = 2.0; - }; - }; - example = { - sonnet = { - input = 3.0; - output = 15.0; - cache_read = 0.3; - cache_write = 3.75; - }; - }; - description = '' - Per-model USD prices (per **million** tokens) used for the - hive-wide cost *estimate* on the dashboard's ST4TS tab. Each key - is a model-family short name matched case-insensitively as a - substring of the active model id at runtime (e.g. `"sonnet"` - matches `"claude-sonnet-4-5"`); the longest matching key wins, so - a specific entry beats a generic family name. Any model not - covered by this table falls back to hive-c0re's built-in - estimate. - - The defaults track Anthropic list pricing at the time of - writing — override them here to keep the estimate current - without a code change. Passed to `hive-c0re serve` as JSON via - `--model-prices`; read only by hive-c0re itself (not injected - into containers). Changes apply on the next host rebuild. - ''; - }; - agentCpuQuota = lib.mkOption { type = lib.types.str; default = "200%"; @@ -736,7 +662,7 @@ in ); }; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --nixpkgs-flake ${cfg.nixpkgsFlake} --nixpkgs-unstable-flake ${cfg.nixpkgsUnstableFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)} --agent-cpu-quota ${lib.escapeShellArg cfg.agentCpuQuota} --agent-memory-max ${lib.escapeShellArg cfg.agentMemoryMax} --model-prices ${lib.escapeShellArg (builtins.toJSON cfg.modelPrices)}"; + ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --nixpkgs-flake ${cfg.nixpkgsFlake} --nixpkgs-unstable-flake ${cfg.nixpkgsUnstableFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)} --agent-cpu-quota ${lib.escapeShellArg cfg.agentCpuQuota} --agent-memory-max ${lib.escapeShellArg cfg.agentMemoryMax}"; # Migrate hive-c0re's *own* state to the service user after an # upgrade from a root-run install (systemd's StateDirectory only # chowns the top-level dir, not pre-existing files inside it). The