diff --git a/Plugin/unraid/include/ai.php b/Plugin/unraid/include/ai.php index 3e4a4ce..a357539 100644 --- a/Plugin/unraid/include/ai.php +++ b/Plugin/unraid/include/ai.php @@ -236,8 +236,11 @@ function vv_ai_models_loaded(): ?array { function vv_ai_index_meta(): array { $cfg = vv_ai_config(); if (!file_exists($cfg['db'])) return []; + // Columns are k/v, not key/value. Named wrongly this returns nothing and the embedder-match + // check reports "unrecorded" — a silent pass on the one mismatch that returns confident + // nonsense rather than an error. $raw = trim((string)@shell_exec( - 'sqlite3 ' . escapeshellarg($cfg['db']) . ' ' . escapeshellarg('SELECT key, value FROM vv_meta;') . ' 2>/dev/null' + 'sqlite3 ' . escapeshellarg($cfg['db']) . ' ' . escapeshellarg('SELECT k, v FROM vv_meta;') . ' 2>/dev/null' )); $out = []; foreach (explode("\n", $raw) as $line) { @@ -288,14 +291,14 @@ function vv_ai_health(): array { if ($cfg['model'] === '') { $add('gen', 'Generation model', 'bad', 'not configured', 'Set ' . strtoupper(vv_detect_host()) . '_OLLAMA_MODEL in the host conf'); - } elseif (!in_array($cfg['model'], $names, true)) { + } elseif (!vv_ai_model_installed($cfg['model'], $names)) { $add('gen', 'Generation model', 'bad', $cfg['model'] . ' is not installed', 'Either `ollama pull` it, or point _OLLAMA_MODEL at one of: ' . implode(', ', array_slice($names, 0, 4))); } else { $add('gen', 'Generation model', 'ok', $cfg['model']); } - if ($cfg['embed_model'] === '' || !in_array($cfg['embed_model'], $names, true)) { + if ($cfg['embed_model'] === '' || !vv_ai_model_installed($cfg['embed_model'], $names)) { $add('embed', 'Embedding model', 'bad', ($cfg['embed_model'] ?: 'not configured') . ' is not installed', 'Retrieval cannot embed a query without it — `ollama pull ' . ($cfg['embed_model'] ?: 'nomic-embed-text') . '`'); @@ -315,7 +318,8 @@ function vv_ai_health(): array { // model without reindexing does not error — it silently returns nonsense, scored // confidently, which is the hardest failure here to notice from the answers alone. $builtWith = $meta['embed_model'] ?? ''; - if ($builtWith !== '' && $cfg['embed_model'] !== '' && $builtWith !== $cfg['embed_model']) { + if ($builtWith !== '' && $cfg['embed_model'] !== '' + && vv_ai_model_norm($builtWith) !== vv_ai_model_norm($cfg['embed_model'])) { $add('embedmatch', 'Index / embedder match', 'bad', 'index built with ' . $builtWith . ', conf says ' . $cfg['embed_model'], 'Vectors from different models are not comparable — rerun AI/ai_index.sh --force'); @@ -348,6 +352,25 @@ function vv_ai_health(): array { return $checks; } +// Ollama reports tags fully qualified — nomic-embed-text:latest — while conf commonly carries +// the bare name, and both are valid references. Comparing literally reports an installed model +// as missing, which is exactly the false alarm that makes a health panel worth ignoring. +function vv_ai_model_norm(string $name): string { + $name = trim($name); + if ($name === '') return ''; + // A colon in the final path segment is a tag; anything else is a registry path. + $last = substr($name, strrpos($name, '/') === false ? 0 : strrpos($name, '/') + 1); + return str_contains($last, ':') ? $name : $name . ':latest'; +} + +function vv_ai_model_installed(string $name, array $available): bool { + $n = vv_ai_model_norm($name); + foreach ($available as $m) { + if (vv_ai_model_norm($m) === $n) return true; + } + return false; +} + function command_exists_node(): bool { static $has = null; if ($has !== null) return $has;