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
125 lines
4.1 KiB
Go
125 lines
4.1 KiB
Go
package loki
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestShardInfo(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
idx, total int
|
|
ok bool
|
|
}{
|
|
{"Laguna-S-2.1-UD-IQ4_XS-00001-of-00003.gguf", 1, 3, true},
|
|
{"Laguna-S-2.1-UD-IQ4_XS-00002-of-00003.gguf", 2, 3, true},
|
|
{"m-01-of-03.gguf", 1, 3, true},
|
|
{"Qwen2.5-7B-Instruct-Q4_K_M.gguf", 0, 0, false},
|
|
{"bidon-00004-of-00003.gguf", 0, 0, false}, // numérotation incohérente
|
|
{"pas-un-gguf-00001-of-00002.bin", 0, 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
idx, total, ok := shardInfo(c.name)
|
|
if idx != c.idx || total != c.total || ok != c.ok {
|
|
t.Errorf("shardInfo(%q) = %d,%d,%v ; attendu %d,%d,%v", c.name, idx, total, ok, c.idx, c.total, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShardFamilyGardeLaLargeur(t *testing.T) {
|
|
got := shardFamily("Laguna-00002-of-00003.gguf")
|
|
want := []string{
|
|
"Laguna-00001-of-00003.gguf",
|
|
"Laguna-00002-of-00003.gguf",
|
|
"Laguna-00003-of-00003.gguf",
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("famille = %v ; attendu %v", got, want)
|
|
}
|
|
if solo := shardFamily("simple.gguf"); !reflect.DeepEqual(solo, []string{"simple.gguf"}) {
|
|
t.Fatalf("fichier simple = %v", solo)
|
|
}
|
|
}
|
|
|
|
// Le lien collé peut désigner n'importe quelle tranche : on remonte toujours à la
|
|
// première, et on ne réécrit QUE le nom de fichier final (le même libellé peut
|
|
// apparaître plus haut dans le chemin du dépôt).
|
|
func TestShardURLSet(t *testing.T) {
|
|
base := "https://huggingface.co/unsloth/Laguna-S-2.1-GGUF/resolve/main/UD-IQ4_XS/"
|
|
in := base + "Laguna-UD-IQ4_XS-00002-of-00003.gguf"
|
|
urls, names := shardURLSet(in, "Laguna-UD-IQ4_XS-00002-of-00003.gguf")
|
|
if len(urls) != 3 || len(names) != 3 {
|
|
t.Fatalf("attendu 3 tranches, got %d/%d", len(urls), len(names))
|
|
}
|
|
if names[0] != "Laguna-UD-IQ4_XS-00001-of-00003.gguf" {
|
|
t.Fatalf("première tranche = %q", names[0])
|
|
}
|
|
if urls[0] != base+"Laguna-UD-IQ4_XS-00001-of-00003.gguf" {
|
|
t.Fatalf("URL de la première tranche = %q", urls[0])
|
|
}
|
|
if urls[2] != base+"Laguna-UD-IQ4_XS-00003-of-00003.gguf" {
|
|
t.Fatalf("URL de la dernière tranche = %q", urls[2])
|
|
}
|
|
// Fichier simple : une seule URL, inchangée.
|
|
u, n := shardURLSet(base+"m.gguf", "m.gguf")
|
|
if len(u) != 1 || u[0] != base+"m.gguf" || n[0] != "m.gguf" {
|
|
t.Fatalf("fichier simple mal traité : %v %v", u, n)
|
|
}
|
|
}
|
|
|
|
func TestShardFamilyTailleEtManquantes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write := func(n string, size int) {
|
|
if err := os.WriteFile(filepath.Join(dir, n), make([]byte, size), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("m-00001-of-00003.gguf", 100)
|
|
write("m-00003-of-00003.gguf", 300)
|
|
|
|
if got := shardFamilySize(dir, "m-00001-of-00003.gguf"); got != 400 {
|
|
t.Fatalf("taille de la famille = %d ; attendu 400", got)
|
|
}
|
|
missing := shardFamilyMissing(dir, "m-00001-of-00003.gguf")
|
|
if !reflect.DeepEqual(missing, []string{"m-00002-of-00003.gguf"}) {
|
|
t.Fatalf("tranches manquantes = %v", missing)
|
|
}
|
|
write("m-00002-of-00003.gguf", 200)
|
|
if m := shardFamilyMissing(dir, "m-00001-of-00003.gguf"); len(m) != 0 {
|
|
t.Fatalf("famille complète signalée incomplète : %v", m)
|
|
}
|
|
}
|
|
|
|
// Supprimer un modèle découpé doit emporter TOUTES ses tranches, sinon des
|
|
// dizaines de Go restent sur le disque sans que rien ne sache plus les effacer.
|
|
func TestDeleteModelFileEmporteLesTranches(t *testing.T) {
|
|
testHome(t)
|
|
dir := modelsDir()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := []string{"m-00001-of-00002.gguf", "m-00002-of-00002.gguf", "autre.gguf"}
|
|
for _, n := range names {
|
|
if err := os.WriteFile(filepath.Join(dir, n), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
freed, err := deleteModelFile("m-00001-of-00002.gguf")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if freed != 2 {
|
|
t.Fatalf("octets libérés = %d, attendu 2 (les deux tranches d'un octet)", freed)
|
|
}
|
|
for _, n := range names[:2] {
|
|
if _, err := os.Stat(filepath.Join(dir, n)); !os.IsNotExist(err) {
|
|
t.Fatalf("tranche %s non supprimée", n)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "autre.gguf")); err != nil {
|
|
t.Fatal("un modèle sans rapport a été supprimé")
|
|
}
|
|
}
|