feat(agent): read available models from HIVE_AVAILABLE_MODELS env var

web_ui.rs: add available_models() helper that reads HIVE_AVAILABLE_MODELS
(comma-separated, injected by services.hyperhive.availableModels nix option).
Falls back to ["haiku", "sonnet", "opus"] when absent or empty. Field added
to StateSnapshot so the frontend receives the list on cold-load.

app.js: replace hardcoded MODEL_ALIASES array with availableModels module var.
Seeded from state.available_models before setHeader/populateOverflowMenu on
first /api/state load. Well-known aliases (haiku/sonnet/opus) still get their
descriptive labels; operator-declared custom model names show the name itself.

Implements the frontend + web_ui.rs side of issue #1359 (nix option shipped
separately in PR #1360 by atlas).
This commit is contained in:
iris 2026-06-05 13:27:51 +02:00 committed by mara
commit 1f54a07195
2 changed files with 57 additions and 12 deletions

View file

@ -436,6 +436,14 @@ struct StateSnapshot {
/// Human name of the swarm (e.g. `"constellat1on"`). Sourced from
/// `HYPERHIVE_SWARM_NAME`; `None` when unset.
swarm_name: Option<String>,
/// Ordered list of model short-names the operator has declared as
/// available on this hive. Sourced from `HIVE_AVAILABLE_MODELS`
/// (comma-separated, set by `services.hyperhive.availableModels`).
/// Falls back to `["haiku", "sonnet", "opus"]` when the env var is
/// absent or empty. The frontend model quick-picker renders one button
/// per entry in this list, so operators can add new models or drop
/// ones they don't want without touching the frontend code.
available_models: Vec<String>,
}
/// One navigation link in the agent page header row. The same JSON
@ -561,6 +569,7 @@ async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
.filter(|s| !s.is_empty()),
hive_name: crate::identity::hive_name(),
swarm_name: crate::identity::swarm_name(),
available_models: available_models(),
})
}
@ -1076,3 +1085,25 @@ fn error_response(message: &str) -> Response {
// be noise.
(StatusCode::INTERNAL_SERVER_ERROR, message.to_owned()).into_response()
}
/// Read `HIVE_AVAILABLE_MODELS` (comma-separated short names injected by
/// `services.hyperhive.availableModels`) and return the parsed list.
/// Falls back to `["haiku", "sonnet", "opus"]` when the env var is absent
/// or resolves to an empty list after trimming.
fn available_models() -> Vec<String> {
const DEFAULT: &[&str] = &["haiku", "sonnet", "opus"];
let raw = match std::env::var("HIVE_AVAILABLE_MODELS") {
Ok(v) if !v.trim().is_empty() => v,
_ => return DEFAULT.iter().map(|s| s.to_string()).collect(),
};
let models: Vec<String> = raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if models.is_empty() {
DEFAULT.iter().map(|s| s.to_string()).collect()
} else {
models
}
}