mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Un modèle téléchargé depuis l'éditeur d'un preset survit à la suppression de ce preset (c'est voulu : on le réutilise ailleurs). Il devenait pourtant inatteignable — impossible de refaire un preset dessus, impossible de l'effacer. Trois causes, trois correctifs. 1. « Le modèle existe déjà » n'est plus une erreur. La recherche Hugging Face proposait le quant, le clic lançait la sonde, et le téléchargement répondait en rouge « le modèle existe déjà » — fin du parcours. La sonde constate maintenant la présence du fichier (sans même sortir sur le réseau) et renvoie la valeur à écrire dans MODEL= : l'interface le SÉLECTIONNE, et la file continue (le projecteur vision, par exemple). Les quants déjà présents portent une pastille « déjà installé » dans la liste du dépôt, avant le clic. 2. Le sélecteur de modèle reconnaissait mal ce qu'il avait sous les yeux. /api/models comparait le dossier de chaque .gguf à LokiHome() alors que les téléchargements atterrissent dans $LOKI_HOME/models : la comparaison ne pouvait jamais être vraie. Conséquences : l'étiquette « dossier loki » ne s'affichait nulle part, et un preset écrit MODEL=modele.gguf s'affichait « introuvable ; ajoute son dossier ci-dessous » — le fichier étant juste à côté. Un nom simple est désormais résolu comme le fait le moteur : dossier de loki d'abord, puis les autres. 3. Une liste « Modèles installés », dans le groupe Modèle de l'éditeur. La route /api/models/delete existait depuis longtemps ; aucun bouton ne l'appelait. Le seul moment où un .gguf pouvait disparaître, c'était en cochant « supprimer aussi le fichier » à la suppression de son preset. La liste montre taille, dossier, tranches manquantes, et qui s'en sert : le modèle en service ne s'efface pas (le moteur l'a ouvert, la place ne serait même pas rendue), celui que des presets nomment prévient en les nommant puis obéit. La suppression dit ce qu'elle a libéré. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129sffVC43rezAXUMQuzUog
165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
package loki
|
|
|
|
// backend_models_refs.go — QUI référence un .gguf, et peut-on l'effacer ?
|
|
//
|
|
// Vécu qui a produit ce fichier : un modèle téléchargé depuis l'éditeur d'un
|
|
// preset, le preset supprimé (sans cocher « supprimer aussi le .gguf »), et
|
|
// 40 Go de fichier plus rien pour les reprendre — la route /api/models/delete
|
|
// existait mais AUCUN bouton ne l'appelait. Supprimer un modèle depuis
|
|
// l'interface demande deux choses : savoir qui s'en sert (le moteur en service,
|
|
// d'autres presets), et le dire AVANT d'effacer.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// modelRefIndex — l'index « ce fichier est référencé par… », construit en une
|
|
// seule passe. Demander « qui utilise ce modèle ? » fichier par fichier
|
|
// relisait tous les presets à chaque ligne de la liste des modèles.
|
|
//
|
|
// Les clés sont des chemins normalisés (normDir) : un preset peut nommer son
|
|
// modèle en chemin absolu et un autre en simple nom de fichier, c'est le même
|
|
// fichier sur le disque.
|
|
type modelRefIndex struct {
|
|
active map[string]bool // référencé par config.env — donc par le moteur qui tourne
|
|
presets map[string][]string // chemin -> noms d'affichage des presets
|
|
}
|
|
|
|
// modelRefKey normalise une valeur MODEL=/MMPROJ= en clé d'index, ou "" si elle
|
|
// ne désigne pas un .gguf. resolveModelPath accepte le nom simple comme le
|
|
// chemin absolu et les résout dans les dossiers déclarés — c'est exactement la
|
|
// règle qu'appliquera le moteur.
|
|
func modelRefKey(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return ""
|
|
}
|
|
p, err := resolveServeModelPath(v)
|
|
if err != nil {
|
|
// Introuvable : on garde quand même une clé comparable, sinon deux
|
|
// presets qui nomment le même fichier absent paraîtraient distincts.
|
|
p = filepath.Join(modelsDir(), baseName(v))
|
|
}
|
|
return normDir(p)
|
|
}
|
|
|
|
// modelKeysOf liste les .gguf référencés par une configuration : le modèle, le
|
|
// projecteur vision, et un --mmproj posé à la main dans EXTRA_ARGS (les presets
|
|
// d'avant la clé MMPROJ l'écrivaient là).
|
|
func modelKeysOf(env map[string]string) []string {
|
|
vals := []string{env["MODEL"], env["MMPROJ"]}
|
|
args := splitArgs(env["EXTRA_ARGS"])
|
|
for i, a := range args {
|
|
if a == "--mmproj" && i+1 < len(args) {
|
|
vals = append(vals, args[i+1])
|
|
}
|
|
}
|
|
out := []string{}
|
|
for _, v := range vals {
|
|
if k := modelRefKey(v); k != "" {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// buildModelRefIndex lit la configuration active et tous les presets.
|
|
func buildModelRefIndex() modelRefIndex {
|
|
ix := modelRefIndex{active: map[string]bool{}, presets: map[string][]string{}}
|
|
for _, k := range modelKeysOf(ReadConfig()) {
|
|
ix.active[k] = true
|
|
}
|
|
list, err := ListPresets()
|
|
if err != nil {
|
|
return ix
|
|
}
|
|
for _, p := range list {
|
|
content, err := ReadPreset(p.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, k := range modelKeysOf(parseEnv(content)) {
|
|
if seen[k] {
|
|
continue // MODEL et MMPROJ identiques : un seul « utilisé par »
|
|
}
|
|
seen[k] = true
|
|
ix.presets[k] = append(ix.presets[k], p.Name)
|
|
}
|
|
}
|
|
return ix
|
|
}
|
|
|
|
// lookup renvoie les presets qui référencent path et s'il est en service.
|
|
func (ix modelRefIndex) lookup(path string) (presets []string, active bool) {
|
|
k := normDir(path)
|
|
return ix.presets[k], ix.active[k]
|
|
}
|
|
|
|
// modelUsers répond pour UN modèle (nom de fichier ou chemin).
|
|
func modelUsers(name string) (presets []string, active bool) {
|
|
k := modelRefKey(name)
|
|
if k == "" {
|
|
return nil, false
|
|
}
|
|
return buildModelRefIndex().lookup(k)
|
|
}
|
|
|
|
// modelInServiceErr est le refus opposé à la suppression d'un modèle que le
|
|
// moteur a ouvert. Effacer le .gguf sous un llama-server qui tourne ne libère
|
|
// même pas la place (le fichier reste mappé) et rend le prochain démarrage
|
|
// impossible sans rien dire.
|
|
const modelInServiceMsg = "modèle en service — bascule sur un autre preset avant de le supprimer"
|
|
|
|
// modelFamilySize additionne ce que la suppression libérera réellement
|
|
// (toutes les tranches présentes).
|
|
func modelFamilySize(p string) int64 {
|
|
return shardFamilySize(filepath.Dir(p), filepath.Base(p))
|
|
}
|
|
|
|
// modelPresence dit si un modèle est DÉJÀ sur le disque, et sous quelle valeur
|
|
// l'écrire dans MODEL=. dir restreint la recherche à ce dossier (celui visé par
|
|
// un téléchargement) ; vide = tous les dossiers déclarés.
|
|
//
|
|
// C'est ce qui transforme « le modèle existe déjà » — un cul-de-sac rouge au
|
|
// bout d'un clic sur un quant — en « déjà là, je le sélectionne ».
|
|
func modelPresence(name, dir string) (value, path string, ok bool) {
|
|
base := baseName(strings.TrimSpace(name))
|
|
if base == "" || !strings.HasSuffix(strings.ToLower(base), ".gguf") {
|
|
return "", "", false
|
|
}
|
|
dirs := modelDirs()
|
|
if strings.TrimSpace(dir) != "" {
|
|
if d, err := resolveDownloadDir(dir); err == nil {
|
|
dirs = []string{d}
|
|
}
|
|
}
|
|
for _, d := range dirs {
|
|
p := filepath.Join(d, base)
|
|
if st, err := os.Stat(p); err != nil || st.IsDir() {
|
|
continue
|
|
}
|
|
// Famille incomplète = modèle inutilisable : on ne prétend pas qu'il est
|
|
// là, l'appelant doit pouvoir relancer le téléchargement des tranches
|
|
// manquantes (handleModelDownload les reprend une par une).
|
|
if len(shardFamilyMissing(d, base)) > 0 {
|
|
continue
|
|
}
|
|
return modelPickerValue(d, base), p, true
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// modelPickerValue est la valeur à écrire dans MODEL= pour un fichier donné :
|
|
// le simple nom quand il vit dans le dossier de téléchargement de Loki (le
|
|
// preset reste alors portable d'une machine à l'autre), le chemin complet
|
|
// ailleurs. Même règle que la liste /api/models.
|
|
func modelPickerValue(dir, base string) string {
|
|
if normDir(dir) == normDir(modelsDir()) {
|
|
return base
|
|
}
|
|
return filepath.Join(dir, base)
|
|
}
|