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:
parent
6c57b2b4e0
commit
1f54a07195
2 changed files with 57 additions and 12 deletions
|
|
@ -273,33 +273,37 @@ window.marked = marked;
|
|||
menu.append(logoutBtn);
|
||||
|
||||
// ─── model quick-picker ────────────────────────────────────────
|
||||
// Three one-click shortcuts for the most common model aliases.
|
||||
// One-click shortcuts for each model in `availableModels` (seeded
|
||||
// from `state.available_models` / `HIVE_AVAILABLE_MODELS` nix option).
|
||||
// The active model is highlighted via the `active` class; see
|
||||
// `renderModelChip` which updates `modelPickerBtns` live.
|
||||
const modelSep = el('div', { class: 'overflow-sep', 'aria-hidden': 'true' });
|
||||
const modelLabel = el('div', { class: 'overflow-section-label' }, 'model');
|
||||
menu.append(modelSep, modelLabel);
|
||||
const MODEL_ALIASES = [
|
||||
{ name: 'haiku', label: 'haiku (fast)' },
|
||||
{ name: 'sonnet', label: 'sonnet (balanced)' },
|
||||
{ name: 'opus', label: 'opus (powerful)' },
|
||||
];
|
||||
// Well-known aliases get a parenthetical description; unknown aliases
|
||||
// (operator-declared custom models) show just the name.
|
||||
const MODEL_DESCRIPTIONS = {
|
||||
haiku: 'haiku (fast)',
|
||||
sonnet: 'sonnet (balanced)',
|
||||
opus: 'opus (powerful)',
|
||||
};
|
||||
modelPickerBtns = [];
|
||||
for (const m of MODEL_ALIASES) {
|
||||
for (const name of availableModels) {
|
||||
const label = MODEL_DESCRIPTIONS[name] ?? name;
|
||||
const btn = el('button', {
|
||||
type: 'button',
|
||||
class: 'overflow-item overflow-item-model',
|
||||
role: 'menuitem',
|
||||
title: `/model ${m.name}`,
|
||||
'data-model': m.name,
|
||||
title: `/model ${name}`,
|
||||
'data-model': name,
|
||||
},
|
||||
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '⊞'),
|
||||
m.label,
|
||||
label,
|
||||
);
|
||||
btn.addEventListener('click', () => {
|
||||
if (currentModel === m.name) { closeOverflowMenu(); return; }
|
||||
if (currentModel === name) { closeOverflowMenu(); return; }
|
||||
closeOverflowMenu();
|
||||
postModel(m.name);
|
||||
postModel(name);
|
||||
});
|
||||
modelPickerBtns.push(btn);
|
||||
menu.append(btn);
|
||||
|
|
@ -471,6 +475,10 @@ window.marked = marked;
|
|||
// menu so renderModelChip can update their `active` state without
|
||||
// rebuilding the whole menu.
|
||||
let modelPickerBtns = [];
|
||||
// Ordered list of model short-names available on this hive. Seeded from
|
||||
// `state.available_models` (injected by the nix option); falls back to
|
||||
// the built-in default until the first /api/state cold-load completes.
|
||||
let availableModels = ['haiku', 'sonnet', 'opus'];
|
||||
|
||||
const SLASH_COMMANDS = [
|
||||
{ name: '/help', desc: 'list slash commands' },
|
||||
|
|
@ -1113,6 +1121,12 @@ window.marked = marked;
|
|||
const resp = await fetch('api/state');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const s = await resp.json();
|
||||
// Seed available_models before populateOverflowMenu (called from
|
||||
// setHeader on the first load) so the picker uses the operator-declared
|
||||
// list rather than the JS fallback default.
|
||||
if (Array.isArray(s.available_models) && s.available_models.length > 0) {
|
||||
availableModels = s.available_models;
|
||||
}
|
||||
if (!headerSet) { setHeader(s.label, s.qualified_label, s.dashboard_port, s.hive_name, s.swarm_name); headerSet = true; }
|
||||
currentLabel = s.label;
|
||||
// Render server-supplied navigation links — see
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue