mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
macOS : DYLD_LIBRARY_PATH pour llama-server + journal du moteur dans l'UI
- dyld IGNORE LD_LIBRARY_PATH : sur macOS les .dylib livrees a cote de llama-server n'etaient pas trouvees, le moteur mourait sans un mot - nouvelle route GET /api/service/log (journalctl sous Linux, fichier de log ailleurs) + panneau repliable ouvert en cliquant la pastille d'etat : on voit enfin POURQUOI un modele ne charge pas
This commit is contained in:
1 parent
075a05ebaa
commit
43da4b9197
10 files changed
+109
-6
No files matched your search
@@ -106,13 +106,28 @@ func totalRAMGB() float64 {
|
||||
// live under /usr/local/cuda*/lib64 and are often absent from the global ld
|
||||
// cache — without them llama-server fails to load the GPU backend (or runs
|
||||
// degraded), costing a large chunk of throughput.
|
||||
// Sur macOS, dyld IGNORE LD_LIBRARY_PATH : la variable équivalente est
|
||||
// DYLD_LIBRARY_PATH. Les binaires macOS des releases llama.cpp sont livrés avec
|
||||
// leurs libllama/libggml*.dylib À CÔTÉ de l'exécutable — sans cette variable,
|
||||
// llama-server meurt instantanément sur « Library not loaded », donc sans le
|
||||
// moindre message dans l'UI.
|
||||
func setLibraryPath(dir string) {
|
||||
parts := []string{dir}
|
||||
parts = append(parts, cudaLibDirs()...)
|
||||
if existing := os.Getenv("LD_LIBRARY_PATH"); existing != "" {
|
||||
envVar := "LD_LIBRARY_PATH"
|
||||
if runtime.GOOS == "darwin" {
|
||||
envVar = "DYLD_LIBRARY_PATH"
|
||||
}
|
||||
if existing := os.Getenv(envVar); existing != "" {
|
||||
parts = append(parts, existing)
|
||||
}
|
||||
_ = os.Setenv("LD_LIBRARY_PATH", strings.Join(parts, ":"))
|
||||
joined := strings.Join(parts, ":")
|
||||
_ = os.Setenv(envVar, joined)
|
||||
if runtime.GOOS == "darwin" {
|
||||
// Filet de sécurité : DYLD_FALLBACK_LIBRARY_PATH est consulté en dernier
|
||||
// recours et survit à certains contextes où DYLD_LIBRARY_PATH est purgé.
|
||||
_ = os.Setenv("DYLD_FALLBACK_LIBRARY_PATH", joined+":/usr/local/lib:/usr/lib")
|
||||
}
|
||||
}
|
||||
|
||||
// cudaLibDirs returns the CUDA runtime lib directories present on the machine,
|
||||
|
||||
@@ -268,3 +268,7 @@ func tailFile(path string, n int) string {
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
// serviceLogTail renvoie les n dernières lignes du journal du service (pour
|
||||
// l'UI web). Le fichier est le même en mode launchd et en mode utilisateur.
|
||||
func serviceLogTail(n int) string { return tailFile(logFilePath(), n) }
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -70,3 +71,13 @@ func serviceIsActive() bool {
|
||||
out, _ := exec.Command("systemctl", "is-active", serviceName()).Output()
|
||||
return strings.TrimSpace(string(out)) == "active"
|
||||
}
|
||||
|
||||
// serviceLogTail renvoie les n dernières lignes du journal du service (pour
|
||||
// l'UI web). Linux : journalctl.
|
||||
func serviceLogTail(n int) string {
|
||||
out, err := exec.Command("journalctl", "-u", serviceName(), "-n", strconv.Itoa(n), "--no-pager").CombinedOutput()
|
||||
if err != nil && len(out) == 0 {
|
||||
return "journalctl indisponible : " + err.Error()
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -221,3 +221,7 @@ func tailFile(path string, n int) string {
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
// serviceLogTail renvoie les n dernières lignes du journal du service (pour
|
||||
// l'UI web).
|
||||
func serviceLogTail(n int) string { return tailFile(logFilePath(), n) }
|
||||
@@ -68,7 +68,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:5px;letter-spacing:0}
|
||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
||||
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
||||
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
|
||||
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
|
||||
@@ -399,7 +399,16 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div class="side" id="side">
|
||||
<div class="sidehead">
|
||||
<h1 id="brand"><span id="brand-a">a</span>jean<span id="ver"></span></h1>
|
||||
<span id="status-svc" class="statuspill">…</span>
|
||||
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
|
||||
</div>
|
||||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état.
|
||||
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
||||
<div id="svc-log-box" style="display:none;margin:0 0 10px">
|
||||
<div class="row" style="justify-content:space-between;align-items:center">
|
||||
<span class="io-cap">Journal du moteur</span>
|
||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||
</div>
|
||||
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
|
||||
</div>
|
||||
<details open><summary>GPU</summary>
|
||||
<div id="vram" class="muted">…</div>
|
||||
@@ -913,6 +922,24 @@ async function loadStatus(){
|
||||
document.getElementById('ver').textContent='v'+s.version;
|
||||
}
|
||||
}
|
||||
// Journal du moteur — replié par défaut, on l'ouvre en cliquant la pastille.
|
||||
function toggleSvcLog(){
|
||||
const box=document.getElementById('svc-log-box');
|
||||
if(!box) return;
|
||||
const show = box.style.display==='none';
|
||||
box.style.display = show ? '' : 'none';
|
||||
if(show) loadSvcLog();
|
||||
}
|
||||
async function loadSvcLog(){
|
||||
const el=document.getElementById('svc-log');
|
||||
if(!el) return;
|
||||
el.textContent='chargement du journal…';
|
||||
try{
|
||||
const r=await jget('/api/service/log?n=120');
|
||||
el.textContent = (r && r.log && r.log.trim()) ? r.log : 'journal vide — le moteur n\'a encore rien écrit.';
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}catch(e){ el.textContent='journal indisponible : '+e; }
|
||||
}
|
||||
async function checkUpdate(){
|
||||
const b=document.getElementById('upd-check'), msg=document.getElementById('upd-msg');
|
||||
b.disabled=true; msg.textContent='Vérification…';
|
||||
|
||||
@@ -24,7 +24,16 @@
|
||||
<div class="side" id="side">
|
||||
<div class="sidehead">
|
||||
<h1 id="brand"><span id="brand-a">a</span>jean<span id="ver"></span></h1>
|
||||
<span id="status-svc" class="statuspill">…</span>
|
||||
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
|
||||
</div>
|
||||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état.
|
||||
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
||||
<div id="svc-log-box" style="display:none;margin:0 0 10px">
|
||||
<div class="row" style="justify-content:space-between;align-items:center">
|
||||
<span class="io-cap">Journal du moteur</span>
|
||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||
</div>
|
||||
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
|
||||
</div>
|
||||
<details open><summary>GPU</summary>
|
||||
<div id="vram" class="muted">…</div>
|
||||
|
||||
@@ -14,6 +14,24 @@ async function loadStatus(){
|
||||
document.getElementById('ver').textContent='v'+s.version;
|
||||
}
|
||||
}
|
||||
// Journal du moteur — replié par défaut, on l'ouvre en cliquant la pastille.
|
||||
function toggleSvcLog(){
|
||||
const box=document.getElementById('svc-log-box');
|
||||
if(!box) return;
|
||||
const show = box.style.display==='none';
|
||||
box.style.display = show ? '' : 'none';
|
||||
if(show) loadSvcLog();
|
||||
}
|
||||
async function loadSvcLog(){
|
||||
const el=document.getElementById('svc-log');
|
||||
if(!el) return;
|
||||
el.textContent='chargement du journal…';
|
||||
try{
|
||||
const r=await jget('/api/service/log?n=120');
|
||||
el.textContent = (r && r.log && r.log.trim()) ? r.log : 'journal vide — le moteur n\'a encore rien écrit.';
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}catch(e){ el.textContent='journal indisponible : '+e; }
|
||||
}
|
||||
async function checkUpdate(){
|
||||
const b=document.getElementById('upd-check'), msg=document.getElementById('upd-msg');
|
||||
b.disabled=true; msg.textContent='Vérification…';
|
||||
|
||||
@@ -49,7 +49,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:5px;letter-spacing:0}
|
||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
||||
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
||||
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
|
||||
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
|
||||
|
||||
@@ -44,6 +44,20 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleServiceLog (GET /api/service/log) : dernières lignes du journal du
|
||||
// service, pour que l'UI puisse montrer POURQUOI le modèle ne se charge pas
|
||||
// (fichier .gguf illisible, VRAM insuffisante, bibliothèque manquante…) au lieu
|
||||
// d'un « chargement… » perpétuel sans explication.
|
||||
func handleServiceLog(w http.ResponseWriter, r *http.Request) {
|
||||
n := 80
|
||||
if v := r.URL.Query().Get("n"); v != "" {
|
||||
if k, err := strconv.Atoi(v); err == nil && k > 0 && k <= 500 {
|
||||
n = k
|
||||
}
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"log": serviceLogTail(n)})
|
||||
}
|
||||
|
||||
func handleVram(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
||||
|
||||
@@ -78,6 +78,7 @@ func newWebMux() *http.ServeMux {
|
||||
api := func(path string, h http.HandlerFunc) { mux.HandleFunc(path, requireWebAuth(h)) }
|
||||
api("/api/ping", handlePing)
|
||||
api("/api/status", handleStatus)
|
||||
api("/api/service/log", handleServiceLog) // journal du service pour diagnostiquer un modèle qui ne charge pas
|
||||
api("/api/vram", handleVram)
|
||||
api("/api/ram", handleRam)
|
||||
api("/api/config", handleConfigEnv)
|
||||
|
||||
Reference in new issue
Block a user