Declare the AI owner, and let every node borrow its model over the mesh
vv_is_ai_host() was `=== 'host1'`, which made a physical fact — that is where the GPU is — look like a rule. AI_OWNER_HOST declares it, so the card can move to a rebuilt host3 or a friend's spare. The gate was also doing two jobs. Assistant docks and findings strips now ask whether a model is reachable, so a node without a GPU gets them by borrowing; the AI tab asks whether this is the owner, because that page carries the bug reports, the index and the model configuration — the surface where the vocabulary assumes you built the mesh. Resolution is local, then owner, then anyone else declaring a model, pinned once it answers. Pinned rather than re-derived per call: a mesh that re-decides every request eventually decides differently mid-conversation, and a chat whose second turn lands on another machine has no history there. Cleared only on a transport failure, and only when there is somewhere else to go — a single-node mesh whose model is down should say so, not report AI as unconfigured.
This commit is contained in:
@@ -413,8 +413,94 @@ function vv_is_owner(): bool {
|
||||
// either moves, conflating them would move the other by accident.
|
||||
//
|
||||
// Returns false for 'unknown', so a host that cannot identify itself never shows the tab.
|
||||
// Which node runs the model, the index and the bug store. Declared, not assumed: it was
|
||||
// `=== 'host1'` for as long as host1 was the only box with a GPU, which made a physical fact look
|
||||
// like a rule. A mesh can put the card anywhere — a spare GPU on someone else's node, a rebuilt
|
||||
// host3 — and the owner has to be able to move without editing PHP.
|
||||
//
|
||||
// Defaults to host1 because whoever builds the mesh is host1, and is the person who would be
|
||||
// changing this if it were ever wrong.
|
||||
function vv_ai_owner_host(): string {
|
||||
$h = strtolower(trim((string)(vv_conf_vars()['AI_OWNER_HOST'] ?? '')));
|
||||
return preg_match('/^host\d+$/', $h) ? $h : 'host1';
|
||||
}
|
||||
|
||||
function vv_ai_is_owner(): bool {
|
||||
return vv_detect_host() === vv_ai_owner_host();
|
||||
}
|
||||
|
||||
// Whose Ollama this node should talk to: its own if it has one, otherwise the owner's.
|
||||
//
|
||||
// "Scans local, then defaults to the mesh" — a node with a GPU uses it, and a node without one
|
||||
// borrows rather than going without. The check is whether the conf declares a URL for this host,
|
||||
// not whether Ollama answers: a reachability probe on every call would spend a network round trip
|
||||
// to decide where to send a network round trip, and a box whose own model is down wants that said
|
||||
// plainly rather than papered over by silently using someone else's.
|
||||
// Every node that declares a model, in the order this one should try them: itself first, then the
|
||||
// owner, then anyone else. Local before mesh because a GPU you already have costs nothing to
|
||||
// reach; owner before the rest because that is where the index and the curated model live.
|
||||
function vv_ai_model_candidates(): array {
|
||||
$vars = vv_conf_vars();
|
||||
$me = vv_detect_host();
|
||||
$owner = vv_ai_owner_host();
|
||||
$order = array_unique(array_merge([$me, $owner], array_keys(vv_known_hosts())));
|
||||
|
||||
$out = [];
|
||||
foreach ($order as $h) {
|
||||
if (trim((string)($vars[strtoupper($h) . '_OLLAMA_URL'] ?? '')) !== '') $out[] = $h;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// Where the resolved choice is remembered. VV_CACHE_ROOT is tmpfs, which is the right lifetime:
|
||||
// a reboot re-resolves, and nothing about which node answered belongs on flash.
|
||||
function vv_ai_model_pin_path(): string {
|
||||
return rtrim(VV_CACHE_DIR, '/') . '/ai_model_host.json';
|
||||
}
|
||||
|
||||
// The node whose model this one uses. Resolved once, then pinned until something fails.
|
||||
//
|
||||
// Pinned rather than re-derived because resolution is only interesting when it changes: a mesh
|
||||
// where every request re-decides which node to ask is a mesh that will eventually decide
|
||||
// differently mid-conversation, and a chat whose second turn goes to another machine has no
|
||||
// history there. The pin is what makes "borrow the owner's model" a stable answer.
|
||||
//
|
||||
// Cleared by vv_ai_model_failed(), never by a timer. A working endpoint does not need rechecking,
|
||||
// and an expiry would reintroduce exactly the mid-conversation switch the pin exists to stop.
|
||||
function vv_ai_model_host(): string {
|
||||
$cands = vv_ai_model_candidates();
|
||||
if (!$cands) return vv_ai_owner_host();
|
||||
|
||||
$pin = @json_decode((string)@file_get_contents(vv_ai_model_pin_path()), true);
|
||||
if (is_array($pin) && in_array($pin['host'] ?? '', $cands, true)) return $pin['host'];
|
||||
|
||||
$chosen = $cands[0];
|
||||
@file_put_contents(vv_ai_model_pin_path(),
|
||||
json_encode(['host' => $chosen, 'at' => time()]), LOCK_EX);
|
||||
return $chosen;
|
||||
}
|
||||
|
||||
// Called when a model call fails. Drops the pin and, if there is somewhere else to go, records the
|
||||
// failed node so the next resolution steps past it rather than pinning it again.
|
||||
//
|
||||
// Deliberately forgets the failure as soon as the alternatives run out: a single-model mesh whose
|
||||
// only node is briefly down should keep pointing at it and report that it is down, not resolve to
|
||||
// nothing and report that AI is unconfigured. Those are different problems and only one of them is
|
||||
// the operator's to fix.
|
||||
function vv_ai_model_failed(string $host = ''): void {
|
||||
$host = $host !== '' ? $host : vv_ai_model_host();
|
||||
$cands = vv_ai_model_candidates();
|
||||
$rest = array_values(array_filter($cands, fn($h) => $h !== $host));
|
||||
|
||||
if (!$rest) { @unlink(vv_ai_model_pin_path()); return; }
|
||||
@file_put_contents(vv_ai_model_pin_path(),
|
||||
json_encode(['host' => $rest[0], 'at' => time(), 'after_failure' => $host]), LOCK_EX);
|
||||
}
|
||||
|
||||
// Retained as the owner test it always was, so nothing that meant "is this the AI node" changes
|
||||
// meaning underneath. Callers that meant "may this node use AI at all" want vv_ai_ui_on().
|
||||
function vv_is_ai_host(): bool {
|
||||
return vv_detect_host() === 'host1';
|
||||
return vv_ai_is_owner();
|
||||
}
|
||||
|
||||
// Whether the UI may offer anything AI at all: the right host, with the master switch on. Every
|
||||
@@ -425,8 +511,25 @@ function vv_is_ai_host(): bool {
|
||||
// that file; the Scheduler loads only config.php, and a gate that silently answers false where
|
||||
// its definition is missing is worse than no gate. Reads AI_ENABLED directly for the same
|
||||
// reason. Fail-closed on anything but the literal "true", matching the conf's own contract.
|
||||
// May this node show AI features — the assistant docks, the findings strips, the AI rows on the
|
||||
// Monitor card. No longer "am I host1": a node without a GPU borrows the owner's model over the
|
||||
// mesh, so every node in the mesh gets the assistant. What it does not get is the AI tab; see
|
||||
// vv_ai_owner_ui_on().
|
||||
//
|
||||
// Still fails closed. A node with no local URL and no owner URL resolves to nothing, and an
|
||||
// assistant that cannot reach a model is worse than an absent one.
|
||||
function vv_ai_ui_on(): bool {
|
||||
return vv_is_ai_host()
|
||||
if (strtolower(trim(vv_conf_vars()['AI_ENABLED'] ?? 'false')) !== 'true') return false;
|
||||
$host = strtoupper(vv_ai_model_host());
|
||||
return trim((string)(vv_conf_vars()[$host . '_OLLAMA_URL'] ?? '')) !== '';
|
||||
}
|
||||
|
||||
// May this node show the AI tab. Owner only, and deliberately so: that page carries the bug
|
||||
// reports, the retrieval index and the model configuration — the surface where a wrong answer is
|
||||
// expensive and the vocabulary assumes you built the thing. Someone running two containers on a
|
||||
// node they were handed should have the assistant, not the machinery behind it.
|
||||
function vv_ai_owner_ui_on(): bool {
|
||||
return vv_ai_is_owner()
|
||||
&& strtolower(trim(vv_conf_vars()['AI_ENABLED'] ?? 'false')) === 'true';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user