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:
nathaninline committed 2026-07-25 12:21:02 +02:00
1 parent 075a05ebaa
commit 43da4b9197
10 files changed
+109 -6

No files matched your search

+17 -2
View File
@@ -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,
+4
View File
@@ -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) }
+11
View File
@@ -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)
}
+4
View File
@@ -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) }
+29 -2
View File
@@ -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…';
+10 -1
View File
@@ -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>
+18
View File
@@ -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…';
+1 -1
View File
@@ -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)}
+14
View File
@@ -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",
+1
View File
@@ -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)