mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Modèles : un .gguf gardé se réutilise et s'efface
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
This commit is contained in:
9 files changed
+1061
-71
No files matched your search
@@ -201,38 +201,70 @@ func modelFromPresetContent(content string) string {
|
||||
// n'effacer que la première laissait des dizaines de Go de fichiers que plus
|
||||
// rien ne référence, et que rien ne sait plus supprimer depuis l'interface (les
|
||||
// tranches suivantes n'y apparaissent pas).
|
||||
func deleteModelFile(name string) error {
|
||||
//
|
||||
// Renvoie le nombre d'octets réellement libérés — l'interface le dit, et
|
||||
// « 41,2 Go libérés » est la seule confirmation qui vaille quelque chose après
|
||||
// un ménage de disque.
|
||||
//
|
||||
// Un modèle EN SERVICE n'est jamais effacé : le fichier reste mappé par
|
||||
// llama-server (la place n'est donc même pas rendue) et le prochain démarrage
|
||||
// meurt sur un .gguf absent, sans que rien n'ait prévenu.
|
||||
func deleteModelFile(name string) (int64, error) {
|
||||
p, err := resolveModelPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if _, active := modelUsers(name); active {
|
||||
return 0, fmt.Errorf("%s", modelInServiceMsg)
|
||||
}
|
||||
freed := modelFamilySize(p)
|
||||
if err := os.Remove(p); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("modèle introuvable: %s", filepath.Base(p))
|
||||
return 0, fmt.Errorf("modèle introuvable: %s", filepath.Base(p))
|
||||
}
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
dir := filepath.Dir(p)
|
||||
for _, n := range shardFamily(filepath.Base(p)) {
|
||||
_ = os.Remove(filepath.Join(dir, n)) // déjà supprimée ou absente = rien à faire
|
||||
}
|
||||
return nil
|
||||
return freed, nil
|
||||
}
|
||||
|
||||
// handleModelDelete deletes a single .gguf from LOKI_HOME.
|
||||
// handleModelDelete efface un .gguf d'un des dossiers de modèles déclarés.
|
||||
//
|
||||
// Deux garde-fous, et un seul est un refus définitif :
|
||||
//
|
||||
// - modèle en service → refus net (deleteModelFile), il faut basculer d'abord ;
|
||||
// - modèle nommé par un ou plusieurs presets → 409 avec la LISTE des presets
|
||||
// concernés. L'interface la montre, l'utilisateur tranche, et renvoie la
|
||||
// même requête avec force:true. Refuser tout court transformerait le
|
||||
// nettoyage en impasse — c'est exactement le travers qu'on corrige ici.
|
||||
func handleModelDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := deleteModelFile(req.Name); err != nil {
|
||||
users, active := modelUsers(req.Name)
|
||||
if active {
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "error": modelInServiceMsg, "active": true})
|
||||
return
|
||||
}
|
||||
if len(users) > 0 && !req.Force {
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "used": users,
|
||||
"error": "modèle utilisé par : " + strings.Join(users, ", ")})
|
||||
return
|
||||
}
|
||||
freed, err := deleteModelFile(req.Name)
|
||||
if err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "freed": freed, "used": users})
|
||||
}
|
||||
|
||||
// ---- Hugging Face downloads -------------------------------------------------
|
||||
@@ -382,7 +414,16 @@ func handleModelDownload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if len(todoURLs) == 0 {
|
||||
dlMu.Unlock()
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "error": "le modèle existe déjà: " + name})
|
||||
// Rien à faire : toutes les tranches sont là. Ce n'était pas une erreur,
|
||||
// c'était le résultat voulu — le modèle est installé. On répond ok avec la
|
||||
// valeur à écrire dans MODEL=, l'interface le sélectionne et enchaîne la
|
||||
// file (le projecteur vision, par exemple) au lieu de s'arrêter en rouge.
|
||||
value, _, _ := modelPresence(name, req.Dir)
|
||||
if value == "" {
|
||||
value = modelPickerValue(filepath.Dir(dests[0]), name)
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "filename": name, "exists": true,
|
||||
"value": value, "parts": 0})
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -462,6 +503,21 @@ func handleModelDownloadProbe(w http.ResponseWriter, r *http.Request) {
|
||||
// puis échouer au deux tiers du transfert.
|
||||
urls, names := shardURLSet(dlURL, name)
|
||||
name = names[0]
|
||||
// Déjà sur le disque : on ne sonde même pas Hugging Face. C'est le cas qui
|
||||
// mettait l'interface dans une impasse — un modèle gardé après la suppression
|
||||
// de son preset ne pouvait plus être re-choisi qu'en le retéléchargeant, et
|
||||
// le téléchargement refusait d'écraser. Il n'y a rien à télécharger : il y a
|
||||
// un modèle à SÉLECTIONNER, et l'UI a besoin de sa valeur MODEL= pour ça.
|
||||
// Cherché dans TOUS les dossiers déclarés, pas seulement la destination
|
||||
// choisie : re-télécharger 40 Go sur le SSD parce que le fichier est sur le
|
||||
// disque externe déjà déclaré n'aide personne — le sélecteur, lui, le trouve
|
||||
// où qu'il soit.
|
||||
if value, path, ok := modelPresence(name, ""); ok {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "filename": name, "dir": filepath.Dir(path),
|
||||
"exists": true, "value": value, "size": modelFamilySize(path),
|
||||
"free": diskFree(dir), "free_exact": diskFreeReliable(dir), "enough": true, "parts": len(urls)})
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
var total int64
|
||||
|
||||
Reference in new issue
Block a user