Watch
0
0
Fork
You've already forked hyperhive
0

events: HIVE_DEFAULT_MODEL takes priority over persisted model (closes #319)

This commit is contained in:
damocles 2026-05-23 00:38:17 +02:00
commit 77b249076f
2 changed files with 68 additions and 17 deletions

View file

@ -310,22 +310,27 @@ pub enum TurnState {
Compacting,
}
/// Default claude model when nothing's been set at runtime. Overridable
/// via the `HIVE_DEFAULT_MODEL` env var (set from `hyperhive.model` in
/// the container's `agent.nix`). The operator can also switch at runtime
/// via `/model <name>` in the web terminal; the chosen model is persisted
/// to the state dir so it survives restarts.
/// Compiled-in fallback model used when neither `HIVE_DEFAULT_MODEL` nor a
/// persisted runtime override is present.
pub const DEFAULT_MODEL: &str = "haiku";
/// Return the initial default model name: `HIVE_DEFAULT_MODEL` env var if
/// set to a non-empty string, otherwise `DEFAULT_MODEL`.
/// Return the model declared in `HIVE_DEFAULT_MODEL` (set from
/// `hyperhive.model` in `agent.nix`), or `None` if the env var is absent /
/// empty. When `Some`, this takes precedence over any persisted runtime
/// override so that nix config changes always take effect on rebuild.
#[must_use]
pub fn default_model() -> &'static str {
pub fn configured_model() -> Option<&'static str> {
// Leak once at startup — acceptable for a single config value.
std::env::var("HIVE_DEFAULT_MODEL")
.ok()
.filter(|s| !s.trim().is_empty())
.map_or(DEFAULT_MODEL, |s| Box::leak(s.into_boxed_str()))
.map(|s| &*Box::leak(s.into_boxed_str()))
}
/// Return the model to use when no config and no persisted override exist.
#[must_use]
pub fn default_model() -> &'static str {
configured_model().unwrap_or(DEFAULT_MODEL)
}
/// Context-window size in tokens for a given model name.
@ -441,7 +446,13 @@ impl Bus {
}
};
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
let initial_model = load_model().unwrap_or_else(|| default_model().to_owned());
// Priority: HIVE_DEFAULT_MODEL (from hyperhive.model in agent.nix) >
// persisted runtime override > compiled-in DEFAULT_MODEL.
// The nix config always wins on rebuild; the persisted file is kept
// for within-session tracking only (see persist_model / set_model).
let initial_model = configured_model()
.map(str::to_owned)
.unwrap_or_else(|| load_model().unwrap_or_else(|| DEFAULT_MODEL.to_owned()));
// Restore rate_limited from the sentinel file — if the harness
// crashed while parked, we should still show the right status on
// cold load until the next turn clears it.

View file

@ -2,6 +2,11 @@
pkgs,
lib,
config,
# Flake inputs routed through _module.args by the agent flake.nix.
# Default to {} so the module evaluates cleanly even when the agent
# flake doesn't set up the routing pattern (e.g. during standalone
# nixos-rebuild without a flake wrapper).
flakeInputs ? { },
...
}:
{
@ -20,13 +25,12 @@
default = "haiku";
example = "sonnet";
description = ''
Default claude model for this agent. Sets the `HIVE_DEFAULT_MODEL`
environment variable consumed by the harness at boot; if no
persisted model choice exists in the agent's state dir the harness
falls back to this value. The operator can still switch the model at
runtime via the per-agent web UI — that choice is persisted to the
state dir and takes precedence over this default until the agent is
purged.
Claude model for this agent. Sets the `HIVE_DEFAULT_MODEL`
environment variable; the harness applies it at boot and it takes
priority over any persisted runtime override. The operator can still
switch the model at runtime via the per-agent web UI — that choice
is tracked in the state dir for the current session but is reset by
any rebuild that changes this option.
Valid values are the short model names that `claude --model` accepts:
`"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). Context
@ -320,6 +324,42 @@
};
config = {
assertions = [
# Guard the inputs-routed-as-output pattern: the agent flake.nix is
# expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`.
# If `self` leaks into flakeInputs the agent gets a spurious attrset
# entry that can shadow real inputs and is almost certainly a bug.
{
assertion = !(builtins.hasAttr "self" flakeInputs);
message = ''
hyperhive: `flakeInputs` must not contain "self".
In your agent flake.nix, use:
_module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ];
'';
}
# hyperhive.model must be a non-empty string — an empty value causes
# the harness to pass an invalid model flag to claude.
{
assertion = config.hyperhive.model != "";
message = "hyperhive.model must not be empty (set it to e.g. \"haiku\" or \"sonnet\")";
}
# hyperhive.forge.url must look like an HTTP URL when non-default.
{
assertion =
config.hyperhive.forge.url == ""
|| lib.hasPrefix "http://" config.hyperhive.forge.url
|| lib.hasPrefix "https://" config.hyperhive.forge.url;
message = "hyperhive.forge.url must be an http:// or https:// URL (got: \"${config.hyperhive.forge.url}\")";
}
# hyperhive.icon must reference an SVG file when set.
{
assertion =
config.hyperhive.icon == null
|| lib.hasSuffix ".svg" (toString config.hyperhive.icon);
message = "hyperhive.icon must point to an .svg file";
}
];
environment.etc."hyperhive/extra-mcp.json".text = builtins.toJSON config.hyperhive.extraMcpServers;
# Operator-set per-agent icon (hyperhive.icon). When configured, the