feat(stats): make ST4TS model price table operator-tunable

The hive-wide cost estimate on the dashboard's ST4TS tab used a
hard-coded model->price table in hive_stats.rs. Anthropic list pricing
drifts, so move the table to a nix option operators can keep current
without a code change.

- New `services.hyperhive.modelPrices` option: attrset of model-family
  short name -> { input, output, cache_read, cache_write } USD per
  million tokens. Passed to `hive-c0re serve --model-prices <json>`.
- hive_stats: `Prices` is now public + Deserialize; add `PriceTable`
  type and `resolve_prices` (longest case-insensitive substring key
  wins) with the old hard-coded table preserved as `builtin_prices`
  fallback for any model not covered.
- Coordinator holds the parsed table (hive-c0re-local, not injected
  into containers, so not part of HiveEnv); `/api/stats-hive` reads it.
- Docs: dashboard.md ST4TS cost note updated; option self-documents
  via nixosOptionsDoc.

Closes #1434
This commit is contained in:
iris 2026-06-05 23:52:16 +02:00
commit cc8f58fb24
6 changed files with 172 additions and 23 deletions

View file

@ -423,6 +423,75 @@ in
'';
};
modelPrices = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
input = lib.mkOption {
type = lib.types.float;
description = "USD per million input tokens.";
};
output = lib.mkOption {
type = lib.types.float;
description = "USD per million output tokens.";
};
cache_read = lib.mkOption {
type = lib.types.float;
description = "USD per million cache-read tokens.";
};
cache_write = lib.mkOption {
type = lib.types.float;
description = "USD per million cache-creation (write) tokens.";
};
};
}
);
default = {
opus = {
input = 15.0;
output = 75.0;
cache_read = 1.5;
cache_write = 18.75;
};
sonnet = {
input = 3.0;
output = 15.0;
cache_read = 0.3;
cache_write = 3.75;
};
haiku = {
input = 0.8;
output = 4.0;
cache_read = 0.08;
cache_write = 1.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%";
@ -662,7 +731,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}";
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)}";
# 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