mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Modèles : les supprimer depuis l'interface, et ne charger que des presets
Un .gguf ne pouvait s'effacer qu'en cochant « supprimer aussi le fichier »
au moment de supprimer son preset. Tout ce qui avait été téléchargé sans
preset — une quantization essayée puis abandonnée, un projecteur du mauvais
dépôt — restait donc sur le disque pour des dizaines de gigaoctets, sans
aucun moyen de partir depuis Loki.
L'éditeur de preset porte désormais une ligne « Modèles installés » (repliée,
sous le sélecteur de fichier) : chaque .gguf des dossiers déclarés avec sa
taille, ses tranches, qui le réclame, et un bouton « supprimer ».
Rien n'est réversible ici, d'où deux garde-fous côté serveur, partagés par
/api/models/delete ET par la case de l'éditeur de preset :
- le modèle CHARGÉ est intouchable — l'effacer sous le moteur ne rend pas
l'espace tant qu'il tient le descripteur, et le démarrage suivant meurt
sur un fichier introuvable ;
- un modèle réclamé par des presets ne part qu'avec `force`, le refus 409
nommant les presets pour que la seconde confirmation dise ce qu'on casse.
Supprimer un preset ne peut donc plus emporter le modèle de son voisin.
La suppression emporte toute la famille de tranches et annonce la place
réellement libérée. /api/models marque au passage le modèle chargé et les
presets qui référencent chaque fichier.
Le sélecteur de l'en-tête ne liste plus que des presets. Il proposait aussi
les .gguf du disque, chargés en n'écrivant que MODEL= : le contexte, le NGL
et l'échantillonnage du modèle PRÉCÉDENT restaient en place, et un 27B chargé
par-dessus les réglages d'un 4B partait sur 100k de contexte pour mourir 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. /api/models/use
disparaît avec le groupe qu'il servait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoKfYRvVHTf8GyKhgY5WxT
This commit is contained in:
10 files changed
+637
-152
No files matched your search
+44
-37
@@ -307,6 +307,15 @@ func handleModels(w http.ResponseWriter, r *http.Request) {
|
||||
out := []map[string]any{}
|
||||
seen := map[string]bool{}
|
||||
home := LokiHome()
|
||||
// Ce que le moteur a ouvert, et ce que les presets réclament : l'interface en
|
||||
// a besoin pour dire lesquels sont supprimables sans rien casser.
|
||||
refs := modelPresetRefs()
|
||||
loaded := ""
|
||||
if cur := strings.TrimSpace(ReadConfig()["MODEL"]); cur != "" {
|
||||
if q, err := resolveServeModelPath(cur); err == nil {
|
||||
loaded = normDir(q)
|
||||
}
|
||||
}
|
||||
for _, dir := range modelDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
@@ -341,6 +350,13 @@ func handleModels(w http.ResponseWriter, r *http.Request) {
|
||||
"name": e.Name(), "size": size, "value": value,
|
||||
"path": full, "dir": dir, "home": isHome,
|
||||
}
|
||||
key := normDir(full)
|
||||
if key == loaded {
|
||||
m["loaded"] = true
|
||||
}
|
||||
if len(refs[key]) > 0 {
|
||||
m["presets"] = refs[key]
|
||||
}
|
||||
// Taille annoncée = la famille entière, et on signale les tranches
|
||||
// manquantes : un modèle incomplet démarre puis meurt sur un tenseur
|
||||
// introuvable, autant le voir avant de le sélectionner.
|
||||
@@ -463,15 +479,38 @@ func handlePresetDelete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
modelDeleted, modelErr := "", ""
|
||||
if req.DeleteModel && model != "" {
|
||||
if err := deleteModelFile(model); err != nil {
|
||||
modelErr = err.Error()
|
||||
} else {
|
||||
modelDeleted = model
|
||||
}
|
||||
modelDeleted, modelErr = deletePresetModel(model)
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "modelDeleted": modelDeleted, "modelError": modelErr})
|
||||
}
|
||||
|
||||
// deletePresetModel efface le .gguf d'un preset qu'on vient de supprimer, avec
|
||||
// les MÊMES garde-fous que la suppression directe (/api/models/delete) : la case
|
||||
// « supprimer aussi le fichier » ne doit pas être un chemin détourné pour
|
||||
// effacer un modèle qu'on protège ailleurs.
|
||||
//
|
||||
// Le modèle CHARGÉ est conservé (le preset supprimé n'était pas forcément celui
|
||||
// en service : deux presets peuvent désigner le même .gguf), et un modèle encore
|
||||
// réclamé par un AUTRE preset aussi — sinon supprimer un preset laissait son
|
||||
// voisin avec un moteur qui meurt au démarrage. Le refus est renvoyé à
|
||||
// l'interface, qui le dit ; la suppression du preset, elle, a déjà eu lieu.
|
||||
func deletePresetModel(model string) (deleted, errMsg string) {
|
||||
p, err := resolveModelPath(model)
|
||||
if err != nil {
|
||||
return "", err.Error()
|
||||
}
|
||||
if modelIsLoaded(p) {
|
||||
return "", "modèle chargé par le moteur — fichier conservé"
|
||||
}
|
||||
if used := presetsUsingModel(p); len(used) > 0 {
|
||||
return "", "encore utilisé par : " + strings.Join(used, ", ") + " — fichier conservé"
|
||||
}
|
||||
if err := deleteModelFile(p); err != nil {
|
||||
return "", err.Error()
|
||||
}
|
||||
return model, ""
|
||||
}
|
||||
|
||||
// handleAgent renvoie l'état du mode agent ET la liste des pages mémoire (que
|
||||
// l'IA gère via les outils mem_*) — un seul aller-retour pour l'UI. La clé
|
||||
// "skills" est conservée en miroir de "pages" pour l'ancien portail ajean.link.
|
||||
@@ -938,38 +977,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) {
|
||||
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) {
|
||||
var req struct {
|
||||
N int `json:"n"`
|
||||
|
||||
Reference in new issue
Block a user