-
+
@@ -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)