From 57629f72544ede825939a4e2e1899a516d65de5b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 12:35:50 +0000 Subject: [PATCH] =?UTF-8?q?Le=20s=C3=A9lecteur=20de=20l'en-t=C3=AAte=20ne?= =?UTF-8?q?=20charge=20que=20des=20presets=20(repris=20de=20la=20PR=20#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le sélecteur de l'en-tête proposait, sous les presets, un groupe « Modèles (.gguf) » listant les fichiers du disque. En choisir un écrivait MODEL= et rien d'autre : le contexte, le NGL et l'échantillonnage du modèle précédent restaient en place — un 27B chargé par-dessus les réglages d'un 4B partait sur 100k de contexte et mourait en OOM, sans que rien ne dise pourquoi. Charger un modèle, c'est appliquer tous ses réglages : ça s'appelle un preset, et c'est désormais le seul chemin. /api/models/use disparaît avec le groupe qu'elle servait, ainsi que switchModelFile, loadModelSwitchFiles et MS_FILES côté interface. Le title du sélecteur le dit : « preset actif — en changer recharge tous ses réglages et relance le moteur ». Repris tel quel de la PR #25, dont l'historique (antérieur à la réécriture de main) ne peut plus y être fusionné. index.html régénéré par tools/assemble-ui. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv --- internal/loki/ui/index.html | 65 ++++++-------------------- internal/loki/ui/src/index.tmpl.html | 2 +- internal/loki/ui/src/js/06-settings.js | 2 +- internal/loki/ui/src/js/18-shell.js | 61 +++++------------------- internal/loki/web_api.go | 35 -------------- internal/loki/web_server.go | 1 - 6 files changed, 26 insertions(+), 140 deletions(-) diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index f874c44..67427e3 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -2407,7 +2407,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
Nouvelle discussion
- + @@ -5246,7 +5246,7 @@ async function loadAll(){ // MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute // suppression de module doit donc retirer son appel de cette ligne, et le test // navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard. - await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]); + await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]); releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } @@ -10863,17 +10863,18 @@ function openDetails(el){ } } -// Sélecteur de modèle de l'en-tête. Il liste les presets ; en choisir un revient -// à cliquer la ligne correspondante dans les réglages (switchTo demande +// Sélecteur de modèle de l'en-tête. Il ne liste QUE des presets ; en choisir un +// revient à cliquer la ligne correspondante dans les réglages (switchTo demande // confirmation puis relance le moteur). +// +// Il a listé un temps les .gguf du disque, qu'on chargeait en n'écrivant que +// MODEL= : le contexte, le NGL et l'échantillonnage du modèle PRÉCÉDENT +// restaient en place. Un 27B chargé par-dessus les réglages d'un 4B partait sur +// 100k de contexte et mourait en OOM, sans que rien ne dise pourquoi. Charger un +// modèle, c'est appliquer TOUS ses réglages : ça s'appelle un preset, et c'est +// le seul chemin. function onModelSwitch(sel){ const v = sel.value; - // Entrée du groupe « Modèles » : charger CE .gguf, sans preset — la config - // active garde son contexte et son échantillonnage, seul MODEL change. - if(v && v.startsWith('m:')){ - switchModelFile(v.slice(2), sel.options[sel.selectedIndex].textContent); - return; - } const n = parseInt(v, 10); if(!n) return; const name = sel.options[sel.selectedIndex].textContent; @@ -10883,22 +10884,6 @@ function onModelSwitch(sel){ switchTo(n, name, sel.options[sel.selectedIndex].dataset.id); } -// Bascule directe vers un fichier de modèle (POST /api/models/use) : confirme, -// écrit MODEL, le service redémarre derrière. Annulé ou en échec, la sélection -// revient sur le modèle réellement chargé. -async function switchModelFile(value, name){ - if(!await askConfirm('Charger « ' + name + ' » et redémarrer le moteur ?', {title:'Changer de modèle', okText:'Charger'})){ - paintModelSwitch(); - return; - } - toast('changement de modèle…'); - let r = null; - try{ r = await jpost('/api/models/use', {value}); }catch(_){} - if(!r || !r.ok){ toast('erreur : ' + ((r && r.error) || 'réseau')); paintModelSwitch(); return; } - toast('modèle en cours de chargement…'); - loadAll(); -} - // Le sélecteur affiche LE MODÈLE CHARGÉ, pas seulement le preset actif. // // Les deux ne coïncident pas toujours : un preset n'est « actif » que si son @@ -10911,23 +10896,12 @@ async function switchModelFile(value, name){ // Deux sources, deux moments : loadPresets() donne la liste, loadCfg() donne le // modèle. Chacune dépose ce qu'elle sait et redemande le dessin — elles partent // en parallèle (loadAll), aucun ordre n'est garanti. -let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '', MS_FILES = []; +let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = ''; function renderModelSwitch(presets, active){ MS_PRESETS = presets || []; MS_ACTIVE = active || null; paintModelSwitch(); } -// Les fichiers .gguf découverts sur le disque : le sélecteur doit permettre de -// CHARGER un modèle, pas seulement de rappeler un preset — sans preset créé, -// il n'offrait aucun choix. Chargé par loadAll(), rafraîchi avec le reste. -async function loadModelSwitchFiles(){ - try{ - const r = await jget('/api/models'); - // Projecteurs vision exclus : choisis comme modèle, le moteur ne démarre pas. - MS_FILES = (Array.isArray(r) ? r : []).filter(m => !/mmproj/i.test(String(m.name||''))); - }catch(_){ return; } - paintModelSwitch(); -} // Appelé par loadCfg avec la valeur MODEL de la configuration active. function setLoadedModel(name){ MS_MODEL = (name || '').trim(); @@ -10966,20 +10940,7 @@ function paintModelSwitch(){ }); sel.appendChild(g); } - // Modèles du disque : en choisir un le CHARGE (voir switchModelFile). Un - // preset règle tout (contexte, sampling…) ; un modèle ne change que MODEL. - if(MS_FILES.length){ - const g = document.createElement('optgroup'); g.label = 'Modèles (.gguf)'; - for(const m of MS_FILES){ - const o = document.createElement('option'); - o.value = 'm:' + m.value; - o.textContent = modelLabel(m.name); - if(m.missing && m.missing.length) o.disabled = true; // tranches manquantes - g.appendChild(o); - } - sel.appendChild(g); - } - sel.disabled = !MS_PRESETS.length && !MS_FILES.length; + sel.disabled = !MS_PRESETS.length; sel.title = MS_MODEL ? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur' : 'aucun modèle configuré'; diff --git a/internal/loki/ui/src/index.tmpl.html b/internal/loki/ui/src/index.tmpl.html index bc78e6d..e405561 100644 --- a/internal/loki/ui/src/index.tmpl.html +++ b/internal/loki/ui/src/index.tmpl.html @@ -122,7 +122,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
Nouvelle discussion
- + diff --git a/internal/loki/ui/src/js/06-settings.js b/internal/loki/ui/src/js/06-settings.js index 273789d..5990abb 100644 --- a/internal/loki/ui/src/js/06-settings.js +++ b/internal/loki/ui/src/js/06-settings.js @@ -673,7 +673,7 @@ async function loadAll(){ // MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute // suppression de module doit donc retirer son appel de cette ligne, et le test // navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard. - await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]); + await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]); releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } diff --git a/internal/loki/ui/src/js/18-shell.js b/internal/loki/ui/src/js/18-shell.js index 5c931ce..b60fdb2 100644 --- a/internal/loki/ui/src/js/18-shell.js +++ b/internal/loki/ui/src/js/18-shell.js @@ -48,17 +48,18 @@ function openDetails(el){ } } -// Sélecteur de modèle de l'en-tête. Il liste les presets ; en choisir un revient -// à cliquer la ligne correspondante dans les réglages (switchTo demande +// Sélecteur de modèle de l'en-tête. Il ne liste QUE des presets ; en choisir un +// revient à cliquer la ligne correspondante dans les réglages (switchTo demande // confirmation puis relance le moteur). +// +// Il a listé un temps les .gguf du disque, qu'on chargeait en n'écrivant que +// MODEL= : le contexte, le NGL et l'échantillonnage du modèle PRÉCÉDENT +// restaient en place. Un 27B chargé par-dessus les réglages d'un 4B partait sur +// 100k de contexte et mourait en OOM, sans que rien ne dise pourquoi. Charger un +// modèle, c'est appliquer TOUS ses réglages : ça s'appelle un preset, et c'est +// le seul chemin. function onModelSwitch(sel){ const v = sel.value; - // Entrée du groupe « Modèles » : charger CE .gguf, sans preset — la config - // active garde son contexte et son échantillonnage, seul MODEL change. - if(v && v.startsWith('m:')){ - switchModelFile(v.slice(2), sel.options[sel.selectedIndex].textContent); - return; - } const n = parseInt(v, 10); if(!n) return; const name = sel.options[sel.selectedIndex].textContent; @@ -68,22 +69,6 @@ function onModelSwitch(sel){ switchTo(n, name, sel.options[sel.selectedIndex].dataset.id); } -// Bascule directe vers un fichier de modèle (POST /api/models/use) : confirme, -// écrit MODEL, le service redémarre derrière. Annulé ou en échec, la sélection -// revient sur le modèle réellement chargé. -async function switchModelFile(value, name){ - if(!await askConfirm('Charger « ' + name + ' » et redémarrer le moteur ?', {title:'Changer de modèle', okText:'Charger'})){ - paintModelSwitch(); - return; - } - toast('changement de modèle…'); - let r = null; - try{ r = await jpost('/api/models/use', {value}); }catch(_){} - if(!r || !r.ok){ toast('erreur : ' + ((r && r.error) || 'réseau')); paintModelSwitch(); return; } - toast('modèle en cours de chargement…'); - loadAll(); -} - // Le sélecteur affiche LE MODÈLE CHARGÉ, pas seulement le preset actif. // // Les deux ne coïncident pas toujours : un preset n'est « actif » que si son @@ -96,23 +81,12 @@ async function switchModelFile(value, name){ // Deux sources, deux moments : loadPresets() donne la liste, loadCfg() donne le // modèle. Chacune dépose ce qu'elle sait et redemande le dessin — elles partent // en parallèle (loadAll), aucun ordre n'est garanti. -let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '', MS_FILES = []; +let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = ''; function renderModelSwitch(presets, active){ MS_PRESETS = presets || []; MS_ACTIVE = active || null; paintModelSwitch(); } -// Les fichiers .gguf découverts sur le disque : le sélecteur doit permettre de -// CHARGER un modèle, pas seulement de rappeler un preset — sans preset créé, -// il n'offrait aucun choix. Chargé par loadAll(), rafraîchi avec le reste. -async function loadModelSwitchFiles(){ - try{ - const r = await jget('/api/models'); - // Projecteurs vision exclus : choisis comme modèle, le moteur ne démarre pas. - MS_FILES = (Array.isArray(r) ? r : []).filter(m => !/mmproj/i.test(String(m.name||''))); - }catch(_){ return; } - paintModelSwitch(); -} // Appelé par loadCfg avec la valeur MODEL de la configuration active. function setLoadedModel(name){ MS_MODEL = (name || '').trim(); @@ -151,20 +125,7 @@ function paintModelSwitch(){ }); sel.appendChild(g); } - // Modèles du disque : en choisir un le CHARGE (voir switchModelFile). Un - // preset règle tout (contexte, sampling…) ; un modèle ne change que MODEL. - if(MS_FILES.length){ - const g = document.createElement('optgroup'); g.label = 'Modèles (.gguf)'; - for(const m of MS_FILES){ - const o = document.createElement('option'); - o.value = 'm:' + m.value; - o.textContent = modelLabel(m.name); - if(m.missing && m.missing.length) o.disabled = true; // tranches manquantes - g.appendChild(o); - } - sel.appendChild(g); - } - sel.disabled = !MS_PRESETS.length && !MS_FILES.length; + sel.disabled = !MS_PRESETS.length; sel.title = MS_MODEL ? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur' : 'aucun modèle configuré'; diff --git a/internal/loki/web_api.go b/internal/loki/web_api.go index 7ec0545..f08b0ac 100644 --- a/internal/loki/web_api.go +++ b/internal/loki/web_api.go @@ -1377,41 +1377,6 @@ func handleMemDelete(w http.ResponseWriter, r *http.Request) { sendJSON(w, 200, map[string]any{"ok": true}) } -// handleModelUse charge un MODÈLE précis (fichier .gguf) sans passer par un -// preset : seul MODEL change dans la configuration active — contexte, NGL et -// échantillonnage sont conservés — et le service redémarre en arrière-plan, -// même contrat que handleSwitch. C'est ce qui rend le sélecteur de l'en-tête -// utilisable même sans avoir créé de preset. -func handleModelUse(w http.ResponseWriter, r *http.Request) { - if tuneDenyHTTP(w) { - return - } - var req struct { - Value string `json:"value"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Value) == "" { - sendJSON(w, 400, map[string]any{"ok": false, "error": "modèle manquant"}) - return - } - // Le fichier doit exister AVANT d'écrire la config : une faute de frappe ne - // doit pas laisser un moteur qui boucle sur un modèle introuvable. - if _, err := resolveServeModelPath(req.Value); err != nil { - sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()}) - return - } - if err := SetConfigKey("MODEL", req.Value); err != nil { - sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()}) - return - } - fmt.Printf("%s MODEL <- %s\n", green("[ok]"), req.Value) - go func() { - if err := serviceAction("restart"); err != nil { - fmt.Println(red("[err] redémarrage : " + err.Error())) - } - }() - sendJSON(w, 200, map[string]any{"ok": true}) -} - func handleSwitch(w http.ResponseWriter, r *http.Request) { // Pendant une optimisation, la configuration active est celle qu'elle mesure // et le moteur ne doit pas redémarrer (backend_tune_lock.go). diff --git a/internal/loki/web_server.go b/internal/loki/web_server.go index 37640a3..4609ff9 100644 --- a/internal/loki/web_server.go +++ b/internal/loki/web_server.go @@ -243,7 +243,6 @@ func newWebMux() *http.ServeMux { api("/api/models", handleModels) api("/api/models/delete", handleModelDelete) api("/api/models/dirs", handleModelDirs) // dossiers de modèles (disque externe…) - api("/api/models/use", handleModelUse) // charger CE .gguf (sélecteur de l'en-tête) api("/api/models/download", handleModelDownload) api("/api/models/download/probe", handleModelDownloadProbe) // taille + espace libre avant de lancer api("/api/models/download/status", handleModelDownloadStatus)