diff --git a/README.md b/README.md index 326aaeb..d368728 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,7 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur : | `LOKI_UPDATE` | `rust` : la mise à jour de Loki (`/api/update`, `/api/update/apply`) est servie par la façade Rust ; en conteneur, l'image se met à jour par Docker (expérimental, migration en cours) | Go | | `LOKI_MCP` | `rust` : les serveurs MCP — configuration, catalogue, état, pool de sessions stdio et HTTP — sont tenus par la façade Rust ; le Go lui demande les outils MCP de l'agent et leurs appels (expérimental, migration en cours) | Go | | `LOKI_BACKENDS` | `rust` : les backends llama.cpp — statut, vérification git, binaires précompilés, bascule fast/opt, backends custom, GPU vus par un moteur — et le job des moteurs sont tenus par la façade Rust ; les jobs du Go (compilation, moteur de l'image, Strata) lui rapportent leurs étapes (expérimental, migration en cours) | Go | +| `LOKI_ENGINE_IMAGE` | `rust` (avec `LOKI_BACKENDS=rust`) : le panneau « moteur de l'image » — version qui tourne, dernier build publié, version recommandée, mise à jour depuis le registre officiel avec essai à blanc, bascules, retour à la version précédente, vérification du rendu du gabarit — est tenu par la façade Rust (expérimental, migration en cours) | Go | | `LOKI_PROJECTS` | `rust` : le hub des projets (liste et comptes, création, renommage, description, suppression, bascule) est tenu par la façade Rust ; les discussions restent à qui tient la conversation (expérimental, migration en cours) | Go | | `LOKI_SYSTEM` | `rust` : les interrupteurs système — accès internet et Crawl4AI, pilotage du navigateur, clé de complétion, écoute réseau du moteur — sont tenus par la façade Rust ; le navigateur piloté reste au Go (expérimental, migration en cours) | Go | | `LOKI_ENGINE` | `rust` : le pilotage du moteur (démarrage, arrêt, redémarrage, déchargement et rechargement de la VRAM) est tenu par la façade Rust, en conteneur avec son superviseur ; le verrou de l'optimiseur reste au Go (expérimental, migration en cours) | Go | diff --git a/docs/superpowers/specs/2026-10-15-rust-migration-lot24-moteur-image-design.md b/docs/superpowers/specs/2026-10-15-rust-migration-lot24-moteur-image-design.md new file mode 100644 index 0000000..df19f28 --- /dev/null +++ b/docs/superpowers/specs/2026-10-15-rust-migration-lot24-moteur-image-design.md @@ -0,0 +1,130 @@ +# Lot 24 — panneau « moteur de l'image » tenu par la façade + +## Contexte + +Le panneau Moteur permet de mettre à jour llama.cpp sans reconstruire +l'image. Le moteur est pris dans l'image de conteneur officielle +(`ghcr.io/ggml-org/llama.cpp:server-cuda-b#####`), dont on ne télécharge que +les couches du sommet, celles qui portent `/app`. Trois fichiers du Go le +servent : + +- `web_engine.go` : statut, dernier build publié, mise à jour, bascule, + suppression ; +- `web_engine_reco.go` : version recommandée, risque de rendu du + raisonnement (défaut de b10763), retour à la version précédente, + vérification du rendu du gabarit avant/après la bascule ; +- `backend_engine_oci.go` : dialogue avec le registre, extraction des + couches, essai à blanc. + +La mise à jour est un job, le job des moteurs que la façade tient depuis le +lot 22. Le risque de rendu et la vérification du rendu reposent sur la sonde +de gabarit, portée au lot 23. + +## Le lot + +Avec `LOKI_ENGINE_IMAGE=rust` **et** `LOKI_BACKENDS=rust` (expérimental), la +façade (`rust/loki-rs/src/engineimg`) sert les routes suivantes : + +| Route | Rôle | +| --- | --- | +| `/api/engine` | statut : binaire, build, source, variante, versions installées, avis, encart « version recommandée », version précédente, relevé du rendu | +| `/api/engine/check` | dernier build publié pour la variante, comparé au courant | +| `/api/engine/update` | job : recherche, téléchargement des couches du sommet, essai à blanc, REASONING_PRESERVE=off si demandé, bascule, ménage, vérification du rendu | +| `/api/engine/use` | bascule vers une version installée ou vers le moteur de l'image | +| `/api/engine/remove` | suppression d'une version installée (refusée pour celle qui tourne) | +| `/api/engine/plan` | sur clic : version visée (dernière ou recommandée) et risque de rendu | +| `/api/engine/rollback` | retour au moteur d'avant la dernière bascule | + +Sans les backends, loki-rs ne passe pas la variable au Go : le panneau +reste au Go et le dit au démarrage. `/api/engine/cmdline` reste au Go. + +La sonde de gabarit est celle de la conversation quand la façade la tient +(`LOKI_CHAT=rust`) : le relevé du rendu rejoue alors la forme de ses vraies +requêtes. Sinon, la façade a sa propre sonde, qui sonde une requête nue. + +Pour REASONING_PRESERVE=off posée dans le preset actif, la façade réutilise +l'enregistrement du lot 15 (`save_applying`). Le service est arrêté puis +relancé comme au lot 22 : par la façade si elle pilote le moteur, sinon par +le Go (`/internal/v1/service/action`). + +Côté Go, `facade_engineimg.go` relaie à la façade les routes reçues par le +tunnel. + +## Parité + +`internal/loki/parity_engineimg_test.go` (Linux) tire du Go un journal +rejoué par `tests/engineimg.rs`. Il tourne contre : + +- un faux registre OCI : jeton, index multi-architecture, manifeste annoté, + couches .tar.gz ; +- un faux moteur local (le `fakeTpl` du Go, porté) pour la sonde et le rendu. + +Le journal couvre : + +- **le statut** : moteur de l'image, téléchargé ou compilé, avec la version + précédente (absente quand c'est le moteur courant), le relevé du rendu, + sans moteur d'image, sans aucun moteur ; +- **le dernier build** : publié, sans numéro, ou refusé (jeton refusé ou + illisible, manifeste refusé ou illisible, aucune image pour + l'architecture) ; +- **le plan** : + - dernière version (avec ou sans numéro) ou recommandée (build trop + ancien, tag figé absent, sans numéro), et déjà à jour ; + - chaque verdict du risque de rendu : moteur arrêté, gabarit vide, + REASONING_PRESERVE posée, EXTRA_ARGS, gabarit sans les variables, gabarit + qui garde la réflexion, build antérieur au défaut, preset externe ; +- **la mise à jour** : + - couches du sommet vers la base ; une couche du dessus masque celle du + dessous ; liens absolus ramenés dans `/app` ; marqueurs de suppression et + sous-dossiers ignorés ; couche sans fichier du moteur ; couche du runtime + ignorée ; phase du téléchargement ; + - essai à blanc ; + - risque de rendu rappelé dans le journal, établi ou impossible à vérifier + (moteur local arrêté) ; + - REASONING_PRESERVE=off : posée dans le preset actif ou dans la + configuration, déjà posée, ou inconnue du moteur ; + - ménage : versions triées par build, dossier sans moteur épargné, version + précédente gardée ; + - rendu comparé avant/après : changé, identique, ou impossible sur le + nouveau moteur ; + - déjà à jour, corps illisible ; +- **les échecs** : + - optimisation en cours, version inconnue, image sans serveur ou sans + version annoncée, manifeste sans couche, runtime au sommet, couche + introuvable, tag illisible ; + - moteur qui ne démarre pas (12 dernières lignes de sa sortie) ou qui ne + voit aucune carte ; +- **les bascules** : vers une version installée, vers l'image, vers une + version absente, pendant une optimisation ; retour, deux fois, et en GET ; +- **la suppression** : version (`../` neutralisé), version absente, moteur + utilisé ; +- **après chaque appel** : BIN, REASONING_PRESERVE, `engine/previous-bin` et + l'arborescence de `engine/`. + +Autres tests : + +- la façade vérifie le service arrêté puis relancé autour d'une mise à jour + et redémarré par une bascule (`service_arrete_puis_relance`), le relevé + « non vérifié à temps », le 503 sans backends, les variantes et les + vérifications périmées ; +- le Go (`TestFacadeMoteurImage`) vérifie que les sept routes partent à la + façade, et elles seulement. + +## Mutations + +74 mutations du port Rust, toutes tuées sauf trois, équivalentes : + +- la garde « moteur d'image absent » de la source : `same_path` ne + rapproche jamais un chemin vide ; +- l'essai à blanc qui relance l'inventaire des cartes quand l'ancien moteur + est le nouveau : même résultat ; +- une vérification du rendu périmée qui continue d'attendre son moteur : son + relevé est de toute façon écarté au rangement. + +## Écarts assumés + +- Les erreurs réseau (registre injoignable) gardent le texte de `reqwest`. +- Un manifeste dont un champ a le mauvais type ne donne pas le message du + Go : le nom Go du type anonyme n'est pas reproduit. +- La vérification du rendu attend son délai à la cadence choisie (5 s), sans + le contexte Go qui coupe aussi une requête en cours au bout de 20 min. diff --git a/internal/loki/facade_chat.go b/internal/loki/facade_chat.go index e4996b8..da43af6 100644 --- a/internal/loki/facade_chat.go +++ b/internal/loki/facade_chat.go @@ -141,7 +141,8 @@ var facadeChatRoutes = map[string]bool{ // mémoire (facade_mem.go), les presets (facade_presets.go), le pilotage du // moteur (facade_engine.go), les interrupteurs système (facade_system.go), les // projets (facade_projects.go), les serveurs MCP (facade_mcp.go), la mise à -// jour (facade_update.go), les backends llama.cpp (facade_backends.go). +// jour (facade_update.go), les backends llama.cpp (facade_backends.go), le +// moteur de l'image (facade_engineimg.go). // Posé APRÈS requireWebAuth : seule une requête authentifiée ici part, avec le // jeton interne qui en tient lieu là-bas. func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc { @@ -174,6 +175,8 @@ func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc { held, unavailable = facadeUpdate, "mise à jour indisponible : " case facadeBackendsRoutes[path]: held, unavailable = facadeBackends, "moteurs indisponibles : " + case facadeEngineImageRoutes[path]: + held, unavailable = facadeEngineImage, "moteur de l'image indisponible : " default: return h } diff --git a/internal/loki/facade_chat_test.go b/internal/loki/facade_chat_test.go index cd0c1d6..e0f0bac 100644 --- a/internal/loki/facade_chat_test.go +++ b/internal/loki/facade_chat_test.go @@ -1102,3 +1102,66 @@ func TestFacadeSondeDeGabarit(t *testing.T) { t.Fatal("verdict sans façade") } } + +// Le panneau « moteur de l'image » part à la façade avec LOKI_ENGINE_IMAGE=rust +// (lot 24) ; la commande du dernier lancement reste au Go. +func TestFacadeMoteurImage(t *testing.T) { + testHome(t) + var mu sync.Mutex + var calls []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(internalTokenHeader) != jetonTest { + w.WriteHeader(403) + return + } + mu.Lock() + calls = append(calls, r.Method+" "+r.URL.Path) + mu.Unlock() + io.WriteString(w, `{"ok":true,"de":"facade"}`) + })) + defer srv.Close() + t.Setenv(internalTokenEnv, jetonTest) + t.Setenv(facadeAddrEnv, strings.TrimPrefix(srv.URL, "http://")) + local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") } + + t.Setenv(facadeEngineImageEnv, "") + for p := range facadeEngineImageRoutes { + w := httptest.NewRecorder() + withFacadeChat(p, local)(w, httptest.NewRequest("POST", p, strings.NewReader("{}"))) + if w.Body.String() != "go" { + t.Fatalf("sans LOKI_ENGINE_IMAGE, %s : %q", p, w.Body.String()) + } + } + if len(calls) != 0 { + t.Fatalf("appels à la façade sans LOKI_ENGINE_IMAGE : %v", calls) + } + + t.Setenv(facadeEngineImageEnv, "Rust ") + if len(facadeEngineImageRoutes) != 7 { + t.Fatalf("routes : %v", facadeEngineImageRoutes) + } + for p := range facadeEngineImageRoutes { + w := httptest.NewRecorder() + withFacadeChat(p, local)(w, httptest.NewRequest("POST", p, strings.NewReader("{}"))) + if !strings.Contains(w.Body.String(), "facade") { + t.Fatalf("%s non relayé : %q", p, w.Body.String()) + } + } + mu.Lock() + n := len(calls) + mu.Unlock() + if n != len(facadeEngineImageRoutes) { + t.Fatalf("relais : %v", calls) + } + w := httptest.NewRecorder() + withFacadeChat("/api/engine/cmdline", local)(w, httptest.NewRequest("GET", "/api/engine/cmdline", nil)) + if w.Body.String() != "go" { + t.Fatalf("/api/engine/cmdline doit rester au Go : %q", w.Body.String()) + } + + // Sans jeton interne, rien ne part. + t.Setenv(internalTokenEnv, "") + if facadeEngineImage() { + t.Fatal("tenu sans jeton interne") + } +} diff --git a/internal/loki/facade_engineimg.go b/internal/loki/facade_engineimg.go new file mode 100644 index 0000000..da16e39 --- /dev/null +++ b/internal/loki/facade_engineimg.go @@ -0,0 +1,34 @@ +package loki + +import ( + "os" + "strings" +) + +// facade_engineimg.go — panneau « moteur de l'image » tenu par la façade Rust +// (LOKI_ENGINE_IMAGE=rust, lot 24 de la migration ; expérimental, le Go reste +// le défaut) : version qui tourne et versions installées, dernier build publié +// sur le registre, mise à jour (job des moteurs, tenu par la façade avec +// LOKI_BACKENDS=rust), bascules, suppression, version recommandée, retour à la +// version précédente et vérification du rendu du gabarit +// (rust/loki-rs/src/engineimg). loki-rs ne passe la variable qu'avec les +// backends. /api/engine/cmdline reste ici. + +const facadeEngineImageEnv = "LOKI_ENGINE_IMAGE" + +// facadeEngineImage : la façade tient-elle le panneau ? +func facadeEngineImage() bool { + return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeEngineImageEnv)), "rust") && + facadeAddr() != "" && internalToken() != "" +} + +// facadeEngineImageRoutes : les routes que la façade sert alors. +var facadeEngineImageRoutes = map[string]bool{ + "/api/engine": true, + "/api/engine/check": true, + "/api/engine/update": true, + "/api/engine/use": true, + "/api/engine/remove": true, + "/api/engine/plan": true, + "/api/engine/rollback": true, +} diff --git a/internal/loki/parity_engineimg_test.go b/internal/loki/parity_engineimg_test.go new file mode 100644 index 0000000..fa91ac0 --- /dev/null +++ b/internal/loki/parity_engineimg_test.go @@ -0,0 +1,544 @@ +//go:build linux + +package loki + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + "time" +) + +// parity_engineimg_test.go — journal de référence du panneau « moteur de +// l'image » (lot 24 de la migration Rust) : statut (variante, build, avis, +// encart de version recommandée, versions installées, version précédente, +// vérification du rendu), dernier build publié, plan (dernière ou recommandée, +// risque de rendu du raisonnement), mise à jour depuis un faux registre OCI +// (couches du sommet, liens, couche du runtime ignorée, essai à blanc, +// REASONING_PRESERVE=off, ménage des versions, rendu comparé avant/après), +// bascules, suppression, retour à la version précédente, et les échecs. +// Rangé dans rust/loki-rs/tests/parity_engineimg/engineimg.json ; +// tests/engineimg.rs le rejoue par la façade. +// +// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMoteurImage + +const parityEngineImgFile = "../../rust/loki-rs/tests/parity_engineimg/engineimg.json" + +// Ce que sert le faux registre : le jeton, la version annoncée par les tags, +// les tags figés qui existent, et les couches (de la base au sommet). +type parityRegistry struct { + Token int `json:"token,omitempty"` // statut de /token (0 = 200) + Blob int `json:"blob,omitempty"` // statut imposé aux couches (0 = servies) + // Corps de /token servi à la place du jeton ; statut et corps imposés + // aux manifestes ; index sans image pour cette architecture. + TokenBody string `json:"token_body,omitempty"` + Manifest int `json:"manifest,omitempty"` + ManifestBody string `json:"manifest_body,omitempty"` + NoArch bool `json:"no_arch,omitempty"` + Version string `json:"version"` + Fixed []string `json:"fixed,omitempty"` + Layers []parityRegistryLay `json:"layers"` +} + +type parityRegistryLay struct { + Size int64 `json:"size,omitempty"` // taille annoncée (0 = réelle) + Entries []parityArchEntry `json:"entries"` +} + +// Le moteur local (sonde de gabarit) : gabarit, build, ou arrêté. +type parityLocal struct { + Mode string `json:"mode"` + Build string `json:"build"` + Template string `json:"template,omitempty"` + Down bool `json:"down,omitempty"` + Apply int `json:"apply,omitempty"` // statut imposé à /apply-template (0 = rendu) +} + +type parityEngineImgStep struct { + Name string `json:"name"` + Files map[string]string `json:"files,omitempty"` + Config map[string]string `json:"config,omitempty"` + Registry *parityRegistry `json:"registry,omitempty"` + Local *parityLocal `json:"local,omitempty"` + Tune bool `json:"tune,omitempty"` + EngineEnv *string `json:"engine_env,omitempty"` // LOKI_ENGINE_BIN + Method string `json:"method"` + Path string `json:"path"` + Body string `json:"body,omitempty"` + Wait bool `json:"wait,omitempty"` + AfterJob *parityLocal `json:"after_job,omitempty"` // le moteur local une fois le job fini + WaitRender bool `json:"wait_render,omitempty"` + Status int `json:"status"` + Out any `json:"out"` + Job any `json:"job,omitempty"` + Render any `json:"render,omitempty"` + After map[string]string `json:"after"` + Tree []string `json:"tree"` +} + +// Les scripts des moteurs. +func parityEngineScript(version, devices, help string, failList bool) string { + s := "#!/bin/sh\ncase \"$1\" in\n--version) echo '" + version + "';;\n--list-devices)" + if failList { + s += " i=0; while [ $i -lt 13 ]; do echo \"trace $i\"; i=$((i+1)); done; echo 'erreur: libcudart.so.13 introuvable'; exit 1;;\n" + } else { + s += " echo 'Available devices:'" + devices + ";;\n" + } + return s + "--help) echo '" + help + "';;\nesac\n" +} + +func parityEngineLayers(version, script string, withServer bool, runtimeSize int64) []parityRegistryLay { + // Le serveur en 0700 : l'installation le rend exécutable par tous. + server := []parityArchEntry{{Name: "app/llama", Type: "file", Body: "#!/bin/true\n", Mode: 0o755}} + libs := []parityArchEntry{} + if withServer { + server = append(server, parityArchEntry{Name: "app/llama-server", Type: "file", Body: script, Mode: 0o700}) + // Masqué par la couche du dessus : jamais extrait. + libs = append(libs, parityArchEntry{Name: "app/llama-server", Type: "file", Body: "ancien", Mode: 0o755}) + } + return []parityRegistryLay{ + {Size: 2 << 30, Entries: []parityArchEntry{{Name: "usr/local/cuda/lib64/libcudart.so", Type: "file", Body: "runtime", Mode: 0o644}}}, + {Entries: []parityArchEntry{{Name: "etc/hosts", Type: "file", Body: "x", Mode: 0o644}}}, + {Entries: append(libs, []parityArchEntry{ + {Name: "app/", Type: "dir", Mode: 0o755}, + {Name: "app/libggml-base.so.0.20.0", Type: "file", Body: "elf " + version, Mode: 0o644}, + {Name: "app/libggml-base.so", Type: "symlink", Link: "libggml-base.so.0.20.0"}, + {Name: "app/libllama.so.0.1.0", Type: "file", Body: "elf", Mode: 0o644}, + {Name: "app/libllama.so", Type: "symlink", Link: "/app/libllama.so.0.1.0"}, + {Name: "app/libggml-cuda.so", Type: "file", Body: "elf", Mode: 0o644}, + {Name: "app/sous/ignoré", Type: "file", Body: "x", Mode: 0o644}, + {Name: "app/.wh.supprimé", Type: "file", Body: "", Mode: 0o644}, + {Name: "etc/passwd", Type: "file", Body: "x", Mode: 0o644}, + }...)}, + {Size: runtimeSize, Entries: server}, + } +} + +var parityAtRe = regexp.MustCompile(`"at":"[^"]*"`) + +func TestPariteMoteurImage(t *testing.T) { + home := testHome(t) + freshTplProbe(t) + resetBackendsState() + t.Cleanup(resetBackendsState) + oldPoll, oldWait := engineRenderPoll, engineRenderWait + engineRenderPoll, engineRenderWait = time.Millisecond, 10*time.Second + reset := func() { + engineRenderMu.Lock() + engineRenderLast = nil + engineRenderGen++ + engineRenderMu.Unlock() + buildMu.Lock() + buildCache = map[string]int{} + buildMu.Unlock() + } + reset() + t.Cleanup(func() { + engineRenderPoll, engineRenderWait = oldPoll, oldWait + reset() + }) + + // Le faux registre. + var mu sync.Mutex + var reg parityRegistry + type built struct { + layers map[string][]byte + manifest []byte + manDig string + index []byte + } + build := func(r parityRegistry) built { + b := built{layers: map[string][]byte{}} + var layers []map[string]any + for _, l := range r.Layers { + data := parityTarGz(t, l.Entries) + sum := sha256.Sum256(data) + d := "sha256:" + hex.EncodeToString(sum[:]) + b.layers[d] = data + size := int64(len(data)) + if l.Size > 0 { + size = l.Size + } + layers = append(layers, map[string]any{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": d, "size": size}) + } + b.manifest, _ = json.Marshal(map[string]any{"schemaVersion": 2, "layers": layers, + "annotations": map[string]string{"org.opencontainers.image.version": r.Version}}) + sum := sha256.Sum256(b.manifest) + b.manDig = "sha256:" + hex.EncodeToString(sum[:]) + arch := ociArch() + if r.NoArch { + arch = "s390x" + } + b.index, _ = json.Marshal(map[string]any{"schemaVersion": 2, + "manifests": []map[string]any{ + {"digest": "sha256:autre", "platform": map[string]string{"os": "linux", "architecture": "ppc64le"}}, + {"digest": b.manDig, "platform": map[string]string{"os": "linux", "architecture": arch}}, + }}) + return b + } + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + cur := reg + mu.Unlock() + b := build(cur) + switch { + case r.URL.Path == "/token": + if cur.Token != 0 { + w.WriteHeader(cur.Token) + return + } + if cur.TokenBody != "" { + _, _ = w.Write([]byte(cur.TokenBody)) + return + } + _, _ = w.Write([]byte(`{"token":"jeton"}`)) + case strings.Contains(r.URL.Path, "/manifests/"): + if r.Header.Get("Authorization") != "Bearer jeton" { + w.WriteHeader(401) + return + } + if cur.Manifest != 0 { + w.WriteHeader(cur.Manifest) + return + } + if cur.ManifestBody != "" { + _, _ = w.Write([]byte(cur.ManifestBody)) + return + } + ref := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + switch { + case ref == b.manDig: + _, _ = w.Write(b.manifest) + case ref == "server-cuda": + _, _ = w.Write(b.index) + default: + for _, f := range cur.Fixed { + if f == ref { + _, _ = w.Write(b.index) + return + } + } + w.WriteHeader(404) + } + case strings.Contains(r.URL.Path, "/blobs/"): + if cur.Blob != 0 { + w.WriteHeader(cur.Blob) + return + } + data, ok := b.layers[r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]] + if !ok { + w.WriteHeader(404) + return + } + _, _ = w.Write(data) + default: + w.WriteHeader(404) + } + })) + defer srv.Close() + oldBase := ociBase + ociBase = srv.URL + defer func() { ociBase = oldBase }() + regHost := strings.TrimPrefix(srv.URL, "http://") + + // Le moteur local, pour la sonde de gabarit. + local := &fakeTpl{mode: "qwen3", build: "b10450-img"} + down := false + eng := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + local.mu.Lock() + d := down + local.mu.Unlock() + if d { + w.WriteHeader(503) + return + } + local.handler(w, r) + })) + defer eng.Close() + u, _ := url.Parse(eng.URL) + if err := SetConfigKey("PORT", u.Port()); err != nil { + t.Fatal(err) + } + setLocal := func(l *parityLocal) { + local.mu.Lock() + local.mode, local.build, local.tpl = l.Mode, l.Build, l.Template + local.applyStatus, local.applyBody = nil, "" + if l.Apply != 0 { + for range 1000 { + local.applyStatus = append(local.applyStatus, l.Apply) + } + local.applyBody = `{"error":{"code":500,"message":"Jinja Exception: gabarit cassé"}}` + } + down = l.Down + local.mu.Unlock() + } + + img := filepath.Join(home, "image") + t.Setenv("LOKI_ENGINE_BIN", filepath.Join(img, "llama-server")) + dev1 := "; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)'" + mask := func(s string) string { + s = strings.ReplaceAll(s, srv.URL, "{{REG}}") + s = strings.ReplaceAll(s, regHost, "{{REGHOST}}") + s = strings.ReplaceAll(s, home, "{{HOME}}") + s = parityTSRe.ReplaceAllString(s, `"$1":"{{T}}"`) + return parityAtRe.ReplaceAllString(s, `"at":"{{T}}"`) + } + unmask := func(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(s, "{{HOME}}", home), "{{PORT}}", u.Port()) + } + handlers := map[string]http.HandlerFunc{ + "/api/engine": handleEngineStatus, + "/api/engine/check": handleEngineCheck, + "/api/engine/update": handleEngineUpdate, + "/api/engine/use": handleEngineUse, + "/api/engine/remove": handleEngineRemove, + "/api/engine/plan": handleEnginePlan, + "/api/engine/rollback": handleEngineRollback, + } + call := func(method, path, body string) (int, any) { + rec := httptest.NewRecorder() + handlers[path](rec, httptest.NewRequest(method, path, strings.NewReader(body))) + var out any + if err := json.Unmarshal([]byte(mask(rec.Body.String())), &out); err != nil { + t.Fatalf("%s : corps illisible %q", path, rec.Body.String()) + } + return rec.Code, out + } + run := func(st parityEngineImgStep) parityEngineImgStep { + files := map[string]string{} + for k, v := range st.Files { + files[k] = unmask(v) + } + parityApplyFiles(t, home, files) + for k, v := range st.Config { + if err := SetConfigKey(k, unmask(v)); err != nil { + t.Fatal(err) + } + } + if st.Registry != nil { + mu.Lock() + reg = *st.Registry + mu.Unlock() + } + if st.Local != nil { + setLocal(st.Local) + } + if st.EngineEnv != nil { + t.Setenv("LOKI_ENGINE_BIN", unmask(*st.EngineEnv)) + } + var lock *tuneLock + if st.Tune { + l, err := tuneLockAcquire("web", false) + if err != nil { + t.Fatal(err) + } + lock = l + } + st.Status, st.Out = call(st.Method, st.Path, st.Body) + if lock != nil { + lock.release() + } + if st.Wait { + deadline := time.Now().Add(30 * time.Second) + for lcJobSnapshot(0, false)["running"] == true { + if time.Now().After(deadline) { + t.Fatalf("%s : le job ne finit pas", st.Name) + } + time.Sleep(10 * time.Millisecond) + } + b, _ := json.Marshal(lcJobSnapshot(0, true)) + _ = json.Unmarshal([]byte(mask(string(b))), &st.Job) + if st.AfterJob != nil { + setLocal(st.AfterJob) + } + } + if st.WaitRender { + deadline := time.Now().Add(15 * time.Second) + for { + c := engineRenderSnapshot() + if c != nil && c.Status != "pending" { + break + } + if time.Now().After(deadline) { + t.Fatalf("%s : la vérification du rendu ne finit pas", st.Name) + } + time.Sleep(10 * time.Millisecond) + } + _, out := call("GET", "/api/engine", "") + st.Render = out.(map[string]any)["render_check"] + } + st.After = map[string]string{} + for _, k := range []string{"BIN", "REASONING_PRESERVE"} { + if v := ReadConfig()[k]; v != "" { + st.After["config/"+k] = mask(v) + } + } + if b, err := os.ReadFile(filepath.Join(home, "engine", "previous-bin")); err == nil { + st.After["previous-bin"] = mask(string(b)) + } + st.Tree = parityTree(t, home) + for i := range st.Tree { + st.Tree[i] = mask(st.Tree[i]) + } + return st + } + + v10900 := "version: 0.1.0-dev (build 10900, commit def5678)" + good := parityEngineScript(v10900, dev1, "--no-reasoning-preserve", false) + reg10900 := &parityRegistry{Version: "b10900", Fixed: []string{"server-cuda-b10900"}, + Layers: parityEngineLayers("b10900", good, true, 0)} + qwenTpl := "{% if preserve_thinking %}garde{% endif %}" + + noImage := "" + var steps []parityEngineImgStep + for _, st := range []parityEngineImgStep{ + {Name: "statut, moteur de l'image", Files: map[string]string{ + "image/llama-server": parityEngineScript("version: 10450 (abc1234)", dev1, "usage", false), + "image/libggml-cuda.so": "elf", + }, Registry: reg10900, Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: qwenTpl}, + Method: "GET", Path: "/api/engine"}, + {Name: "dernière version publiée", Method: "POST", Path: "/api/engine/check"}, + {Name: "dernière version sans numéro", Registry: &parityRegistry{Layers: parityEngineLayers("x", good, true, 0)}, + Method: "POST", Path: "/api/engine/check"}, + {Name: "jeton illisible", Registry: &parityRegistry{TokenBody: `{"token":""}`, Version: "b10900"}, Method: "POST", Path: "/api/engine/check"}, + {Name: "manifeste refusé", Registry: &parityRegistry{Manifest: 403, Version: "b10900"}, Method: "POST", Path: "/api/engine/check"}, + {Name: "manifeste illisible", Registry: &parityRegistry{ManifestBody: `{"mediaType":`, Version: "b10900"}, Method: "POST", Path: "/api/engine/check"}, + {Name: "aucune image pour cette architecture", Registry: &parityRegistry{NoArch: true, Version: "b10900", Layers: parityEngineLayers("b10900", good, true, 0)}, + Method: "POST", Path: "/api/engine/check"}, + {Name: "jeton refusé", Registry: &parityRegistry{Token: 500, Version: "b10900"}, Method: "POST", Path: "/api/engine/check"}, + {Name: "plan : dernière version", Registry: reg10900, Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan : version recommandée", Method: "POST", Path: "/api/engine/plan", Body: `{"recommended":true}`}, + {Name: "plan recommandé, build trop ancien", Registry: &parityRegistry{Version: "b10800", Fixed: []string{"server-cuda-b10800"}, + Layers: parityEngineLayers("b10800", good, true, 0)}, Method: "POST", Path: "/api/engine/plan", Body: `{"recommended":true}`}, + {Name: "plan recommandé, tag figé absent", Registry: &parityRegistry{Version: "b10900", Layers: parityEngineLayers("b10900", good, true, 0)}, + Method: "POST", Path: "/api/engine/plan", Body: `{"recommended":true}`}, + {Name: "plan recommandé, sans numéro", Registry: &parityRegistry{Version: "", Layers: parityEngineLayers("x", good, true, 0)}, + Method: "POST", Path: "/api/engine/plan", Body: `{"recommended":true}`}, + {Name: "plan, dernière version sans numéro", Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan en GET", Method: "GET", Path: "/api/engine/plan"}, + {Name: "plan, moteur local arrêté", Registry: reg10900, Local: &parityLocal{Down: true}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, REASONING_PRESERVE posée", Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: qwenTpl}, + Config: map[string]string{"REASONING_PRESERVE": "on"}, Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, EXTRA_ARGS", Config: map[string]string{"REASONING_PRESERVE": "", "EXTRA_ARGS": "--reasoning-preserve"}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, gabarit sans les variables", Config: map[string]string{"EXTRA_ARGS": ""}, + Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: "simple"}, Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, gabarit qui garde tout", Local: &parityLocal{Mode: "keep", Build: "b10451-img", Template: qwenTpl}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, gabarit vide", Local: &parityLocal{Build: "b10452-img"}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, build antérieur au défaut", Registry: &parityRegistry{Version: "b10700", Fixed: []string{"server-cuda-b10700"}, + Layers: parityEngineLayers("b10700", good, true, 0)}, Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: qwenTpl}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "plan, preset externe", Registry: reg10900, Config: map[string]string{"EXTERNAL": "1"}, + Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "mise à jour pendant une optimisation", Config: map[string]string{"EXTERNAL": ""}, Tune: true, Method: "POST", Path: "/api/engine/update", Body: `{}`}, + {Name: "mise à jour, moteur local arrêté", Registry: &parityRegistry{Version: "b10920", Fixed: []string{"server-cuda-b10920"}, + Layers: parityEngineLayers("b10920", good, true, 0)}, Local: &parityLocal{Down: true}, + Method: "POST", Path: "/api/engine/update", Body: `{}`, Wait: true}, + {Name: "retour au moteur de l'image", Method: "POST", Path: "/api/engine/use", Body: `{"tag":"image"}`}, + {Name: "suppression de la version d'essai", Method: "POST", Path: "/api/engine/remove", Body: `{"tag":"server-cuda-b10920"}`}, + {Name: "mise à jour", Registry: reg10900, Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: qwenTpl}, + Method: "POST", Path: "/api/engine/update", Body: `{}`, Wait: true, + AfterJob: &parityLocal{Mode: "keep", Build: "b10900-new", Template: qwenTpl}, WaitRender: true}, + {Name: "statut après la mise à jour", Method: "GET", Path: "/api/engine"}, + {Name: "plan, déjà à jour", Method: "POST", Path: "/api/engine/plan", Body: `{}`}, + {Name: "déjà à jour", Method: "POST", Path: "/api/engine/update", Body: `{}`, Wait: true}, + {Name: "retour à la version précédente", Method: "POST", Path: "/api/engine/rollback"}, + {Name: "statut après le retour", Method: "GET", Path: "/api/engine"}, + {Name: "retour en GET", Method: "GET", Path: "/api/engine/rollback"}, + {Name: "retour pendant une optimisation", Tune: true, Method: "POST", Path: "/api/engine/rollback"}, + {Name: "bascule vers la version installée", Method: "POST", Path: "/api/engine/use", Body: `{"tag":" server-cuda-b10900 "}`}, + {Name: "bascule vers l'image", Method: "POST", Path: "/api/engine/use", Body: `{"tag":"image"}`}, + {Name: "bascule vers une version absente", Method: "POST", Path: "/api/engine/use", Body: `{"tag":"server-cuda-b1"}`}, + {Name: "bascule pendant une optimisation", Tune: true, Method: "POST", Path: "/api/engine/use", Body: `{"tag":"image"}`}, + {Name: "suppression d'une version", Method: "POST", Path: "/api/engine/remove", Body: `{"tag":"../server-cuda-b10900"}`}, + {Name: "suppression d'une version absente", Method: "POST", Path: "/api/engine/remove", Body: `{"tag":"server-cuda-b10900"}`}, + {Name: "mise à jour épinglée, REASONING_PRESERVE=off dans le preset actif", + Files: map[string]string{"presets/actif.env": "PORT={{PORT}}\nBIN={{HOME}}/image/llama-server\n"}, Local: &parityLocal{Mode: "qwen3", Build: "b10450-img", Template: qwenTpl}, + Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10900","preserve_off":true}`, Wait: true, + AfterJob: &parityLocal{Mode: "qwen3", Build: "b10900-new", Template: qwenTpl}, WaitRender: true}, + {Name: "suppression du moteur utilisé", Method: "POST", Path: "/api/engine/remove", Body: `{"tag":"server-cuda-b10900"}`}, + {Name: "REASONING_PRESERVE déjà posée", Method: "POST", Path: "/api/engine/update", + Body: `{"tag":"server-cuda-b10900","preserve_off":true}`, Wait: true, + AfterJob: &parityLocal{Mode: "drop", Build: "b10900-bis", Template: qwenTpl}, WaitRender: true}, + {Name: "REASONING_PRESERVE=off posée dans la configuration", Files: map[string]string{"presets/actif.env": "-"}, + Config: map[string]string{"REASONING_PRESERVE": ""}, Method: "POST", Path: "/api/engine/update", + Body: `{"tag":"server-cuda-b10900","preserve_off":true}`, Wait: true, + AfterJob: &parityLocal{Mode: "qwen3", Build: "b10900-ter", Template: qwenTpl, Apply: 500}, WaitRender: true}, + {Name: "nouvelle version, ménage", Files: map[string]string{"engine/server-cuda-b10000/llama-server": "#!/bin/sh\n", + "engine/server-cuda-b9999/llama-server": "#!/bin/sh\n", "engine/server-cuda-b10001": "/"}, + Config: map[string]string{"REASONING_PRESERVE": ""}, + Registry: &parityRegistry{Version: "b10950", Fixed: []string{"server-cuda-b10950"}, Layers: parityEngineLayers("b10950", parityEngineScript("version: 0.1.0-dev (build 10950, commit 0ab1234)", dev1, "", false), true, 10<<20)}, + Local: &parityLocal{Down: true}, Method: "POST", Path: "/api/engine/update", Body: `{}`, Wait: true}, + {Name: "statut, version précédente téléchargée", Method: "GET", Path: "/api/engine"}, + {Name: "REASONING_PRESERVE=off inconnue du moteur", Method: "POST", Path: "/api/engine/update", + Body: `{"tag":"server-cuda-b10950","preserve_off":true}`, Wait: true}, + {Name: "version inconnue du registre", Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b1"}`, Wait: true}, + {Name: "image sans serveur", Registry: &parityRegistry{Version: "b10960", Fixed: []string{"server-cuda-b10960"}, + Layers: parityEngineLayers("b10960", good, false, 0)}, Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10960"}`, Wait: true}, + {Name: "manifeste sans couche", Registry: &parityRegistry{Version: "b10966", Fixed: []string{"server-cuda-b10966"}}, + Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10966"}`, Wait: true}, + {Name: "image sans version annoncée", Registry: &parityRegistry{Fixed: []string{"server-cuda-b10967"}, + Layers: parityEngineLayers("b10967", good, false, 0)}, Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10967"}`, Wait: true}, + {Name: "couche du runtime au sommet", Registry: &parityRegistry{Version: "b10961", Fixed: []string{"server-cuda-b10961"}, + Layers: parityEngineLayers("b10961", good, true, 700<<20)}, Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10961"}`, Wait: true}, + {Name: "moteur qui ne démarre pas", Registry: &parityRegistry{Version: "b10962", Fixed: []string{"server-cuda-b10962"}, + Layers: parityEngineLayers("b10962", parityEngineScript("version: 10962 (aaa)", dev1, "", true), true, 0)}, + Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10962"}`, Wait: true}, + {Name: "moteur qui ne voit aucune carte", Registry: &parityRegistry{Version: "b10963", Fixed: []string{"server-cuda-b10963"}, + Layers: parityEngineLayers("b10963", parityEngineScript("version: 10963 (bbb)", "", "", false), true, 0)}, + Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10963"}`, Wait: true}, + {Name: "jeton refusé pendant la mise à jour", Registry: &parityRegistry{Token: 401, Version: "b10964"}, + Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10964"}`, Wait: true}, + {Name: "couche introuvable", Registry: &parityRegistry{Blob: 404, Version: "b10965", Fixed: []string{"server-cuda-b10965"}, + Layers: parityEngineLayers("b10965", good, true, 0)}, Method: "POST", Path: "/api/engine/update", Body: `{"tag":"server-cuda-b10965"}`, Wait: true}, + {Name: "tag illisible", Method: "POST", Path: "/api/engine/update", Body: `{"tag":"."}`, Wait: true}, + {Name: "corps illisible, preset externe", Registry: reg10900, Config: map[string]string{"EXTERNAL": "1"}, + Local: &parityLocal{Mode: "qwen3", Build: "b10900-ext", Template: qwenTpl}, Method: "POST", Path: "/api/engine/update", Body: `{"tag":1}`, Wait: true}, + {Name: "statut, moteur compilé", Files: map[string]string{ + "custom/llama-server": parityEngineScript("version: 10800 (c0ffee1)", dev1, "", false), + }, Config: map[string]string{"BIN": "{{HOME}}/custom/llama-server"}, Method: "GET", Path: "/api/engine"}, + {Name: "bascule vers l'image, sans moteur d'image", EngineEnv: &noImage, Method: "POST", Path: "/api/engine/use", Body: `{"tag":"image"}`}, + {Name: "statut final", Method: "GET", Path: "/api/engine"}, + {Name: "statut sans aucun moteur", Config: map[string]string{"BIN": ""}, Method: "GET", Path: "/api/engine"}, + {Name: "statut, version précédente = moteur courant", Files: map[string]string{ + "engine/previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n", + }, Config: map[string]string{"BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server"}, Method: "GET", Path: "/api/engine"}, + } { + steps = append(steps, run(st)) + } + + got, err := parityJSON(map[string]any{"goarch": ociArch(), "steps": steps}) + if err != nil { + t.Fatal(err) + } + if os.Getenv("LOKI_PARITY_UPDATE") == "1" { + if err := os.MkdirAll(filepath.Dir(parityEngineImgFile), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(parityEngineImgFile, got, 0o644); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(parityEngineImgFile) + if err != nil { + t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err) + } + if string(got) != string(want) { + t.Errorf("le panneau du moteur de l'image ne produit plus le journal rangé dans %s.\n"+ + "Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMoteurImage,\n"+ + "puis cargo test --test engineimg dit si le port Rust suit.\n--- obtenu ---\n%s", parityEngineImgFile, got) + } +} diff --git a/rust/loki-rs/src/api.rs b/rust/loki-rs/src/api.rs index b887caa..4551dbd 100644 --- a/rust/loki-rs/src/api.rs +++ b/rust/loki-rs/src/api.rs @@ -56,6 +56,9 @@ pub struct Ctx { /// Backends llama.cpp et job des moteurs tenus par la façade /// (`LOKI_BACKENDS=rust`). pub backends: Option>, + /// Panneau « moteur de l'image » tenu par la façade + /// (`LOKI_ENGINE_IMAGE=rust`, avec les backends). + pub engineimg: Option>, /// Jeton de l'API interne : une requête qui le porte vient du Go. pub internal_token: String, } @@ -78,6 +81,7 @@ impl Ctx { || self.mcp.is_some() || self.update || self.backends.is_some() + || self.engineimg.is_some() } /// La requête vient du Go (jeton interne valide) : déjà authentifiée par diff --git a/rust/loki-rs/src/backends/devices.rs b/rust/loki-rs/src/backends/devices.rs index 96541df..7061427 100644 --- a/rust/loki-rs/src/backends/devices.rs +++ b/rust/loki-rs/src/backends/devices.rs @@ -214,6 +214,18 @@ async fn list_devices(bin: &str, cvd: &str) -> (String, Option) { c.env("CUDA_VISIBLE_DEVICES", cvd) .env("CUDA_DEVICE_ORDER", "PCI_BUS_ID"); } + c.stdin(Stdio::null()).kill_on_drop(true); + combined(c, bin, Some(LIST_TIMEOUT)).await +} + +/// `cmd.CombinedOutput()` : sorties standard et d'erreur dans le même tube ; +/// l'erreur au format d'`exec` (`exit status N`, `signal: killed`, +/// `fork/exec …`). `limit` : délai au-delà duquel le processus est tué. +pub async fn combined( + mut c: tokio::process::Command, + bin: &str, + limit: Option, +) -> (String, Option) { // Sortie standard et d'erreur dans le même tube, comme CombinedOutput. let (reader, writer) = match std::io::pipe() { Ok(p) => p, @@ -223,10 +235,7 @@ async fn list_devices(bin: &str, cvd: &str) -> (String, Option) { Ok(w) => w, Err(e) => return (String::new(), Some(e.to_string())), }; - c.stdin(Stdio::null()) - .stdout(Stdio::from(writer)) - .stderr(Stdio::from(writer2)) - .kill_on_drop(true); + c.stdout(Stdio::from(writer)).stderr(Stdio::from(writer2)); let mut child = match c.spawn() { Ok(ch) => ch, Err(e) => { @@ -243,14 +252,16 @@ async fn list_devices(bin: &str, cvd: &str) -> (String, Option) { let _ = std::io::Read::read_to_end(&mut r, &mut buf); buf }); - let status = tokio::time::timeout(LIST_TIMEOUT, child.wait()).await; - let status = match status { - Ok(s) => s, - Err(_) => { - let _ = child.start_kill(); - let _ = child.wait().await; - Err(std::io::Error::other("signal: killed")) - } + let status = match limit { + None => child.wait().await, + Some(d) => match tokio::time::timeout(d, child.wait()).await { + Ok(s) => s, + Err(_) => { + let _ = child.start_kill(); + let _ = child.wait().await; + Err(std::io::Error::other("signal: killed")) + } + }, }; let out = read.await.unwrap_or_default(); let out = crate::system::go_lossy(&out); diff --git a/rust/loki-rs/src/backends/mod.rs b/rust/loki-rs/src/backends/mod.rs index 8cdfb4f..e4fd374 100644 --- a/rust/loki-rs/src/backends/mod.rs +++ b/rust/loki-rs/src/backends/mod.rs @@ -123,7 +123,7 @@ impl Backends { } /// `providedEngineBin`. - fn provided_engine_bin(&self) -> String { + pub(crate) fn provided_engine_bin(&self) -> String { let env = self .engine_bin_env .read() @@ -289,7 +289,7 @@ impl Log<'_> { } /// Le verrou de l'optimiseur (au Go) : le message s'il tient le moteur. -async fn tune_busy(store: &Store) -> Option { +pub(crate) async fn tune_busy(store: &Store) -> Option { let v = store .internal_json(Method::GET, "/internal/v1/tune/guard", None) .await @@ -304,7 +304,7 @@ async fn tune_busy(store: &Store) -> Option { /// Le service moteur : celui de la façade si elle le pilote, sinon celui du /// Go. `if_active` : seulement s'il tourne. Rend (lancé, erreur). -async fn service(ctx: &Ctx, action: &str, if_active: bool) -> (bool, String) { +pub(crate) async fn service(ctx: &Ctx, action: &str, if_active: bool) -> (bool, String) { if let Some(ctl) = &ctx.engine_ctl { if if_active && !crate::enginectl::service_is_active(ctx) { return (false, String::new()); @@ -335,6 +335,18 @@ async fn service(ctx: &Ctx, action: &str, if_active: bool) -> (bool, String) { } } +/// Arrête le moteur s'il tourne : (il tournait, erreur de l'arrêt). +pub(crate) async fn service_stop_if_up(ctx: &Ctx) -> (bool, String) { + if ctx.engine_ctl.is_some() { + if crate::enginectl::service_is_active(ctx) { + return service(ctx, "stop", false).await; + } + return (false, String::new()); + } + // Côté Go, « arrête-le s'il tourne » dit aussi s'il tournait. + service(ctx, "stop", true).await +} + /// Les routes servies par ce module. pub fn handles(path: &str) -> bool { matches!( @@ -635,23 +647,11 @@ async fn run_prebuilt(ctx: &Ctx, b: &Backends) { jobs: &b.jobs, store: &ctx.store, }; - let (svc_was_up, err) = if ctx.engine_ctl.is_some() { - if crate::enginectl::service_is_active(ctx) { - log.phase("arrêt du service le temps de l'installation…") - .await; - service(ctx, "stop", false).await - } else { - (false, String::new()) - } - } else { - // Côté Go, « arrête-le s'il tourne » dit aussi s'il tournait. - let r = service(ctx, "stop", true).await; - if r.0 { - log.phase("arrêt du service le temps de l'installation…") - .await; - } - r - }; + let (svc_was_up, err) = service_stop_if_up(ctx).await; + if svc_was_up { + log.phase("arrêt du service le temps de l'installation…") + .await; + } if svc_was_up && !err.is_empty() { log.line(&format!("[warn] impossible d'arrêter le service : {err}")) .await; @@ -770,20 +770,34 @@ pub async fn install(b: &Backends, log: &Log<'_>) -> Result { Ok(bin) } -/// Le téléchargement d'un asset, sa progression au journal du job au fil de -/// l'eau et dans l'ordre. +/// Le téléchargement d'un asset, sa progression au journal du job. async fn download( b: &Backends, a: &prebuilt::Asset, dest: &Path, log: &Log<'_>, +) -> Result<(), String> { + let rq = b + .client + .get(&a.url) + .header("User-Agent", "Go-http-client/1.1"); + log_download(log, rq, dest, a.size).await +} + +/// Un téléchargement dont la progression part au journal du job au fil de +/// l'eau, dans l'ordre. +pub async fn log_download( + log: &Log<'_>, + rq: reqwest::RequestBuilder, + dest: &Path, + total: i64, ) -> Result<(), String> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); let dl = async move { let mut logf = |s: String| { let _ = tx.send(s); }; - prebuilt::download(&b.client, &a.url, dest, a.size, &mut logf).await + prebuilt::download_request(rq, dest, total, &mut logf).await }; let pump = async { while let Some(s) = rx.recv().await { diff --git a/rust/loki-rs/src/backends/prebuilt.rs b/rust/loki-rs/src/backends/prebuilt.rs index 996cdbd..4786d40 100644 --- a/rust/loki-rs/src/backends/prebuilt.rs +++ b/rust/loki-rs/src/backends/prebuilt.rs @@ -312,12 +312,19 @@ pub async fn download( total: i64, logf: &mut (dyn FnMut(String) + Send), ) -> Result<(), String> { - let resp = client - .get(url) - .header("User-Agent", "Go-http-client/1.1") - .send() - .await - .map_err(|e| e.to_string())?; + let rq = client.get(url).header("User-Agent", "Go-http-client/1.1"); + download_request(rq, dest, total, logf).await +} + +/// `downloadRequest` : la même chose pour une requête déjà construite — un +/// registre OCI exige un en-tête d'autorisation, même en lecture anonyme. +pub async fn download_request( + rq: reqwest::RequestBuilder, + dest: &Path, + total: i64, + logf: &mut (dyn FnMut(String) + Send), +) -> Result<(), String> { + let resp = rq.send().await.map_err(|e| e.to_string())?; if resp.status().as_u16() != 200 { return Err(format!("HTTP {}", resp.status().as_u16())); } @@ -439,7 +446,7 @@ fn mkdir_all(p: &Path) -> Result<(), String> { /// `applyLinks` : liens symboliques, ou copie de la cible quand le système /// les refuse. -fn apply_links(links: &[(PathBuf, String)]) -> Result<(), String> { +pub(crate) fn apply_links(links: &[(PathBuf, String)]) -> Result<(), String> { for (path, target) in links { let _ = std::fs::remove_file(path); if let Some(parent) = path.parent() { diff --git a/rust/loki-rs/src/engineimg/mod.rs b/rust/loki-rs/src/engineimg/mod.rs new file mode 100644 index 0000000..ce7f081 --- /dev/null +++ b/rust/loki-rs/src/engineimg/mod.rs @@ -0,0 +1,1201 @@ +//! Panneau « Moteur de l'image » tenu par la façade (lot 24 de la migration, +//! `LOKI_ENGINE_IMAGE=rust` ; expérimental, le Go reste le défaut) — port de +//! `web_engine.go`, `web_engine_reco.go` et `backend_engine_oci.go` : +//! +//! - la version de llama.cpp qui tourne, les versions installées, la variante ; +//! - le dernier build publié sur le registre, l'installation (job partagé de +//! [`crate::backends`]) avec essai à blanc avant toute bascule ; +//! - la bascule vers une version installée, sa suppression, le retour à la +//! version précédente ; +//! - la version recommandée (choisie sur clic), le risque de rendu du +//! raisonnement (défaut de b10763) et la vérification du rendu du gabarit +//! avant/après la bascule, par la sonde de gabarit ([`crate::chat::tplprobe`]). +//! +//! Tenu seulement avec les backends (le job) — et la sonde de la conversation +//! quand la façade la tient. + +pub mod oci; + +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant, SystemTime}; + +use http_body_util::{BodyExt, Limited}; +use hyper::body::Incoming; +use hyper::{Method, Request, Response, StatusCode}; +use serde_json::{Value, json}; + +use crate::api::Ctx; +use crate::auth::json as send_json; +use crate::backends::{Backends, Log}; +use crate::chat::tplprobe::{self, Shape, TplProbe}; +use crate::chat::wire::TplTri; +use crate::gojson::{self, Kind}; +use crate::serve::run::same_path; +use crate::store::{BK_CONFIG, BK_STATE, Store}; + +/// `engineUseImage`. +pub const USE_IMAGE: &str = "image"; + +/// `enginePreserveDefaultBuild` : preserve_reasoning activé par défaut. +pub const PRESERVE_DEFAULT_BUILD: i64 = 10763; +/// `engineQwen4MTPBuild`. +pub const QWEN4_MTP_BUILD: i64 = 11331; +/// `engineBuildFloor` : en dessous, un build n'est pas cru. +pub const BUILD_FLOOR: i64 = 5000; + +const UPDATE_REQ: &str = + r#"struct { Tag string "json:\"tag\""; PreserveOff bool "json:\"preserve_off\"" }"#; +const TAG_REQ: &str = r#"struct { Tag string "json:\"tag\"" }"#; +const PLAN_REQ: &str = r#"struct { Recommended bool "json:\"recommended\"" }"#; + +/// `engineUnsupportedWhy`. +pub const UNSUPPORTED_WHY: &str = "les images officielles llama.cpp n'existent que pour Linux x86-64 et arm64 ; sur cette machine, utilise « llama.cpp précompilé » ou « llama.cpp compilé »."; + +/// `engineRenderCheck`. +#[derive(Clone, Debug, PartialEq)] +pub struct RenderCheck { + pub status: String, + pub tag: String, + pub detail: String, + pub at: SystemTime, +} + +impl RenderCheck { + fn to_json(&self) -> Value { + let mut v = json!({ "status": self.status, "tag": self.tag }); + if !self.detail.is_empty() { + v["detail"] = json!(self.detail); + } + v["at"] = json!(tplprobe::rfc3339(self.at)); + v + } +} + +#[derive(Default)] +struct RenderState { + generation: u64, + last: Option, +} + +/// Le relevé du rendu sur le moteur qui tourne encore. +#[derive(Clone, Debug, Default)] +pub struct RenderRef { + pub text: String, + pub shape: Shape, + pub build: String, +} + +/// Le panneau. +pub struct EngineImg { + pub home: PathBuf, + pub reg: oci::Registry, + /// `engineOCISupported`. + pub supported: bool, + /// La sonde de gabarit (celle de la conversation quand la façade la tient). + pub probe: Arc, + /// Délai laissé au nouveau moteur pour charger le modèle, et cadence. + pub render_wait: Duration, + pub render_poll: Duration, + /// Variables d'environnement lues (`LLAMA_ARG_*`) ; `None` : le processus. + pub env: Option>, + render: Mutex, + builds: Mutex>, +} + +impl EngineImg { + pub fn new(home: PathBuf, reg: oci::Registry, supported: bool, probe: Arc) -> Self { + EngineImg { + home, + reg, + supported, + probe, + render_wait: Duration::from_secs(20 * 60), + render_poll: Duration::from_secs(5), + env: None, + render: Mutex::new(RenderState::default()), + builds: Mutex::new(HashMap::new()), + } + } + + fn getenv(&self, k: &str) -> String { + match &self.env { + Some(m) => m.get(k).cloned().unwrap_or_default(), + None => std::env::var(k).unwrap_or_default(), + } + } + + // --- état du rendu ---------------------------------------------------- + + fn rs(&self) -> std::sync::MutexGuard<'_, RenderState> { + self.render.lock().unwrap_or_else(|e| e.into_inner()) + } + + /// `engineRenderBegin`. + fn render_begin(&self) -> u64 { + let mut s = self.rs(); + s.generation += 1; + s.generation + } + + /// `engineRenderCancel` : la vérification en cours périmée, son relevé + /// effacé. + pub fn render_cancel(&self) { + let mut s = self.rs(); + s.generation += 1; + s.last = None; + } + + fn render_current(&self, generation: u64) -> bool { + self.rs().generation == generation + } + + /// `engineRenderSet`. + fn render_set(&self, generation: u64, status: &str, tag: &str, detail: &str) { + let c = RenderCheck { + status: status.into(), + tag: tag.into(), + detail: detail.into(), + at: SystemTime::now(), + }; + { + let mut s = self.rs(); + if generation != s.generation { + return; + } + s.last = Some(c); + } + if status != "pending" { + let mut line = format!("[moteur] rendu du gabarit après {tag} : {status}"); + if !detail.is_empty() { + line.push_str(&format!(" — {detail}")); + } + eprintln!("{line}"); + } + } + + /// `engineRenderSnapshot`. + pub fn render_snapshot(&self) -> Option { + self.rs().last.clone() + } + + // --- moteur courant --------------------------------------------------- + + /// `currentEngineBin` : BIN (suivi de version du précompilé), sinon le + /// moteur de l'image. + pub fn current_bin(&self, b: &Backends, cfg: &HashMap) -> String { + let ep = crate::serve::run::EnginePaths { + home: self.home.clone(), + }; + let bin = ep.prebuilt_resolve_bin(cfg.get("BIN").map_or("", String::as_str)); + if !bin.is_empty() && crate::backends::probe::is_file(Path::new(&bin)) { + return bin; + } + b.provided_engine_bin() + } + + /// `engineSource`. + pub fn source(&self, b: &Backends, bin: &str) -> &'static str { + let ep = crate::serve::run::EnginePaths { + home: self.home.clone(), + }; + let img = b.provided_engine_bin(); + if !img.is_empty() && same_path(bin, &img) { + "image" + } else if ep.engine_owns(bin) { + "downloaded" + } else if ep.prebuilt_owns(bin) { + "prebuilt" + } else { + "custom" + } + } + + /// `engineVariant` : d'après les backends ggml posés à côté du moteur. + pub fn variant(&self, bin: &str) -> &'static str { + if bin.is_empty() { + return "server"; + } + let dir = crate::paths::go_dir(bin); + let names: Vec = std::fs::read_dir(&dir) + .map(|rd| { + rd.flatten() + .map(|e| e.file_name().to_string_lossy().into_owned()) + .collect() + }) + .unwrap_or_default(); + let has = |p: &str| names.iter().any(|n| n.starts_with(p)); + if has("libggml-cuda.so") { + "server-cuda" + } else if has("libggml-musa.so") { + "server-musa" + } else if has("libggml-sycl.so") { + "server-intel" + } else if has("libggml-vulkan.so") { + "server-vulkan" + } else { + "server" + } + } + + /// `engineBuildCached` : le build d'un binaire, gardé tant que sa taille + /// et sa date ne changent pas. + pub async fn build_cached(&self, bin: &str) -> i64 { + let Ok(md) = std::fs::metadata(bin) else { + return 0; + }; + let mtime = md + .modified() + .ok() + .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) + .map_or(0, |d| d.as_nanos()); + let key = format!("{bin}|{}|{mtime}", md.len()); + if let Some(b) = self + .builds + .lock() + .unwrap_or_else(|e| e.into_inner()) + .get(&key) + { + return *b; + } + let (b, _) = oci::build_of(bin).await; + self.builds + .lock() + .unwrap_or_else(|e| e.into_inner()) + .insert(key, b); + b + } + + /// `engineBuildTrusted`. + pub async fn build_trusted(&self, b: &Backends, bin: &str) -> i64 { + let src = self.source(b, bin); + if src == "custom" { + return 0; + } + build_trust(src, self.build_cached(bin).await) + } + + // --- version précédente ----------------------------------------------- + + fn prev_file(&self) -> PathBuf { + oci::engine_dir(&self.home).join("previous-bin") + } + + /// `engineRememberPrevious`. + pub fn remember_previous(&self, left: &str, next: &str) { + if left.is_empty() || same_path(left, next) { + return; + } + let _ = std::fs::create_dir_all(oci::engine_dir(&self.home)); + let _ = std::fs::write(self.prev_file(), format!("{left}\n")); + } + + /// `enginePrevious`. + pub async fn previous(&self, b: &Backends, cur: &str) -> Option { + let s = std::fs::read_to_string(self.prev_file()).ok()?; + let bin = s.trim().to_string(); + if bin.is_empty() + || !crate::backends::probe::is_file(Path::new(&bin)) + || same_path(&bin, cur) + { + return None; + } + let mut out = json!({ "bin": bin, "source": self.source(b, &bin) }); + let tag = tag_of(&self.home, &bin); + if !tag.is_empty() { + out["build"] = json!(oci::tag_build(&tag)); + out["tag"] = json!(tag); + } else { + out["build"] = json!(self.build_cached(&bin).await); + } + Some(out) + } + + // --- rendu du raisonnement -------------------------------------------- + + /// La sonde de gabarit vers le moteur local. + async fn probe_env(&self, store: &Store, cfg: &HashMap) -> tplprobe::Env { + tplprobe::Env { + external: crate::engstatus::is_external(cfg), + port: crate::engstatus::llm_port(cfg).into(), + key: store.get_str(BK_STATE, "api_key").await, + } + } + + /// `enginePreserveInputs`. + pub async fn preserve_inputs(&self, b: &Backends, store: &Store, target: i64) -> PreserveIn { + let cfg = store.config().await; + let cur = self.current_bin(b, &cfg); + let mut inp = PreserveIn { + external: crate::engstatus::is_external(&cfg), + current: self.build_trusted(b, &cur).await, + target, + key: cfg + .get("REASONING_PRESERVE") + .map_or("", |s| s.trim()) + .into(), + overridden: preserve_overridden(&cfg, |k| self.getenv(k)), + ..PreserveIn::default() + }; + if inp.external + || !inp.key.is_empty() + || !inp.overridden.is_empty() + || (target > 0 && target < PRESERVE_DEFAULT_BUILD) + { + return inp; + } + let env = self.probe_env(store, &cfg).await; + let p = self.probe.prober(&env); + if !p.healthy().await { + return inp; + } + let Ok(props) = p.props().await else { + return inp; + }; + inp.tpl_known = !props.chat_template.is_empty(); + inp.template = props.chat_template; + inp.preserves = self + .probe + .ensure(&env, &self.probe.last_shape()) + .await + .preserves_history; + inp + } + + /// `engineRenderBefore` : le rendu sur le moteur qui tourne encore. + pub async fn render_before(&self, store: &Store) -> Option { + let cfg = store.config().await; + let mut r = RenderRef { + shape: self.probe.last_shape(), + ..RenderRef::default() + }; + if crate::engstatus::is_external(&cfg) { + return None; + } + let env = self.probe_env(store, &cfg).await; + let p = self.probe.prober(&env); + if !p.healthy().await { + return None; + } + if let Ok(props) = p.props().await { + r.build = props.build_info; + } + r.text = p.render_print(&r.shape).await.ok()?; + Some(r) + } + + /// `engineRenderAfter` : attend le nouveau moteur, compare, range le + /// relevé (en tâche de fond). + pub async fn render_after(&self, store: &Store, r: RenderRef, tag: &str) { + let generation = self.render_begin(); + self.render_set(generation, "pending", tag, ""); + let deadline = Instant::now() + self.render_wait; + let cfg = store.config().await; + let env = self.probe_env(store, &cfg).await; + let p = self.probe.prober(&env); + loop { + let ready = p.healthy().await + && (r.build.is_empty() || p.props().await.is_ok_and(|pr| pr.build_info != r.build)); + if ready { + break; + } + if !self.render_current(generation) { + return; + } + if Instant::now() + self.render_poll > deadline { + self.render_set( + generation, + "unknown", + tag, + "le nouveau moteur n'a pas répondu à temps : rendu non vérifié", + ); + return; + } + tokio::time::sleep(self.render_poll).await; + } + match p.render_print(&r.shape).await { + Err(e) => self.render_set( + generation, + "unknown", + tag, + &format!("rendu impossible sur le nouveau moteur : {e}"), + ), + Ok(after) => match tplprobe::render_diff(&r.text, &after) { + (true, _) => self.render_set(generation, "same", tag, ""), + (false, d) => self.render_set(generation, "changed", tag, &d), + }, + } + } +} + +/// `engineBuildTrust`. +pub fn build_trust(source: &str, build: i64) -> i64 { + if source == "custom" || build < BUILD_FLOOR { + 0 + } else { + build + } +} + +/// `engineTagOf`. +pub fn tag_of(home: &Path, bin: &str) -> String { + let ep = crate::serve::run::EnginePaths { + home: home.to_path_buf(), + }; + if !ep.engine_owns(bin) { + return String::new(); + } + crate::paths::go_base(&crate::paths::go_dir(bin)) +} + +/// `engineRecommendation` : l'encart, ou rien. +pub fn recommendation(trusted: i64, supported: bool) -> Value { + let min = crate::serve::ckpt::ENGINE_MIN_RECOMMENDED; + if trusted <= 0 || trusted >= min || !supported { + return Value::Null; + } + json!({ + "current": trusted, + "min": min, + "gains": [ + format!("points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) gardés : plus de ~8 k jetons recalculés à chaque reprise (b{min})"), + format!("MTP rapide (graphes CUDA) à partir de b{}", crate::serve::spec::SPEC_AUTO_MIN_BUILD), + format!("MTP de qwen4exp à partir de b{QWEN4_MTP_BUILD}"), + ], + }) +} + +/// `preserveIn`. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct PreserveIn { + pub external: bool, + pub current: i64, + pub target: i64, + pub key: String, + pub overridden: String, + pub template: String, + pub tpl_known: bool, + pub preserves: TplTri, +} + +/// `tplPreserveVars`. +const TPL_PRESERVE_VARS: [&str; 4] = [ + "preserve_thinking", + "clear_thinking", + "truncate_history_thinking", + "preserve_reasoning", +]; + +/// `reasoningPreserveRisk` : {risk, why, suggest}. +pub fn preserve_risk(i: &PreserveIn) -> Value { + let no = |why: String| json!({ "risk": "no", "why": why, "suggest": false }); + if i.external { + return no("preset externe : son gabarit n'est pas rendu par ce moteur".into()); + } + if i.target > 0 && i.target < PRESERVE_DEFAULT_BUILD { + return no(format!( + "b{} n'a pas encore preserve_reasoning par défaut (b{PRESERVE_DEFAULT_BUILD})", + i.target + )); + } + if i.current >= PRESERVE_DEFAULT_BUILD { + return no(format!( + "le moteur courant (b{}) a déjà preserve_reasoning par défaut", + i.current + )); + } + if !i.key.is_empty() { + return no(format!( + "REASONING_PRESERVE={} est posée : le rendu ne dépend pas du défaut du moteur", + i.key + )); + } + if !i.overridden.is_empty() { + return no(format!("{} règle déjà preserve_reasoning", i.overridden)); + } + if !i.tpl_known { + return json!({ + "risk": "unknown", + "why": format!("moteur arrêté ou gabarit illisible : impossible de vérifier. À partir de b{PRESERVE_DEFAULT_BUILD}, un gabarit de type Qwen3.6 rend la réflexion des tours passés, vide puisque Loki ne la renvoie pas (sans REASONING_ECHO) ; REASONING_PRESERVE=off garde le rendu actuel. À l'inverse, sur un gabarit qui la garde déjà (Qwen3.8), off changerait le rendu."), + "suggest": false, + }); + } + let tpl = TPL_PRESERVE_VARS.iter().any(|v| i.template.contains(v)); + if !tpl { + return no( + "le gabarit chargé ne lit pas preserve_thinking / clear_thinking : rendu inchangé" + .into(), + ); + } + match i.preserves { + TplTri::Yes => no( + "le gabarit garde déjà la réflexion des tours passés : rendu inchangé (ne pas poser off)" + .into(), + ), + TplTri::No => json!({ + "risk": "yes", + "why": format!("le gabarit chargé retire la réflexion des tours passés ; à partir de b{PRESERVE_DEFAULT_BUILD} le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel."), + "suggest": true, + }), + TplTri::Unknown => json!({ + "risk": "unknown", + "why": "le gabarit lit preserve_thinking mais la sonde n'a pas conclu : REASONING_PRESERVE=off garde le rendu actuel s'il retire la réflexion des tours passés", + "suggest": false, + }), + } +} + +/// `preserveOverridden` : EXTRA_ARGS ou variable qui fixent déjà +/// preserve_reasoning ("" = aucune). +pub fn preserve_overridden( + cfg: &HashMap, + getenv: impl Fn(&str) -> String, +) -> String { + let extra = crate::models::split_args(cfg.get("EXTRA_ARGS").map_or("", String::as_str)); + if crate::serve::has_any_flag(&extra, &["--reasoning-preserve", "--no-reasoning-preserve"]) { + return "EXTRA_ARGS".into(); + } + if crate::serve::flag_values(&extra, &["--chat-template-kwargs"]) + .join(" ") + .contains("preserve_reasoning") + { + return "--chat-template-kwargs".into(); + } + if !getenv("LLAMA_ARG_REASONING_PRESERVE").trim().is_empty() { + return "LLAMA_ARG_REASONING_PRESERVE".into(); + } + if getenv("LLAMA_ARG_CHAT_TEMPLATE_KWARGS").contains("preserve_reasoning") { + return "LLAMA_ARG_CHAT_TEMPLATE_KWARGS".into(); + } + String::new() +} + +/// Les routes servies par ce module. +pub fn handles(path: &str) -> bool { + matches!( + path, + "/api/engine" + | "/api/engine/check" + | "/api/engine/update" + | "/api/engine/use" + | "/api/engine/remove" + | "/api/engine/plan" + | "/api/engine/rollback" + ) +} + +fn fail(code: StatusCode, e: impl Into) -> Response { + send_json(code, &json!({ "ok": false, "error": e.into() })) +} + +use crate::proxy::Body; + +/// `from_go` : remise par le Go (tunnel), déjà authentifiée par lui. +pub async fn serve( + ctx: &Ctx, + e: &Arc, + req: Request, + from_go: bool, +) -> Response { + let Some(b) = ctx.backends.clone() else { + return fail( + StatusCode::SERVICE_UNAVAILABLE, + "backends non tenus par la façade", + ); + }; + let (parts, body) = req.into_parts(); + if !from_go && let Err(refus) = ctx.guard(&parts.headers).await { + return *refus; + } + let raw = Limited::new(body, 1 << 20) + .collect() + .await + .map(|b| b.to_bytes().to_vec()) + .unwrap_or_default(); + let store = &ctx.store; + let post = parts.method == Method::POST; + match parts.uri.path() { + "/api/engine" => send_json(StatusCode::OK, &status(e, &b, store).await), + "/api/engine/check" => { + if !e.supported { + return fail(StatusCode::OK, UNSUPPORTED_WHY); + } + let cfg = store.config().await; + let variant = e.variant(&e.current_bin(&b, &cfg)); + match e.reg.latest(variant).await { + Err(err) => fail(StatusCode::OK, err), + Ok((build, tag)) => { + let (cur, _) = oci::build_of(&e.current_bin(&b, &cfg)).await; + send_json( + StatusCode::OK, + &json!({ + "ok": true, "variant": variant, "latest": build, "tag": tag, + "current": cur, "update": build == 0 || cur == 0 || build > cur, + }), + ) + } + } + } + "/api/engine/update" => { + if !e.supported { + return fail(StatusCode::BAD_REQUEST, UNSUPPORTED_WHY); + } + let d = gojson::decode_struct( + &raw, + UPDATE_REQ, + &[("tag", Kind::Str), ("preserve_off", Kind::Bool)], + ); + let tag = d.str("tag").trim().to_string(); + let preserve_off = d.bool("preserve_off"); + if let Some(err) = crate::backends::tune_busy(store).await { + return fail(StatusCode::CONFLICT, err); + } + if let Err(err) = b.jobs.start(store, "engine").await { + return fail(StatusCode::CONFLICT, err); + } + let (ctx2, e2, b2) = (ctx.clone(), e.clone(), b.clone()); + tokio::spawn(async move { + run_update(&ctx2, &e2, &b2, tag, preserve_off).await; + b2.jobs.end(&ctx2.store).await; + }); + send_json(StatusCode::OK, &json!({ "ok": true })) + } + "/api/engine/use" => { + if let Some(err) = crate::backends::tune_busy(store).await { + return send_json( + StatusCode::CONFLICT, + &json!({ "ok": false, "tuning": true, "error": err }), + ); + } + let d = gojson::decode_struct(&raw, TAG_REQ, &[("tag", Kind::Str)]); + let tag = d.str("tag").trim().to_string(); + let bin = if tag == USE_IMAGE { + let img = b.provided_engine_bin(); + if img.is_empty() { + return fail( + StatusCode::BAD_REQUEST, + "cette installation n'a pas de moteur d'image (LOKI_ENGINE_BIN)", + ); + } + img + } else { + let p = oci::bin_for(&e.home, &tag); + if p.is_empty() { + return fail( + StatusCode::BAD_REQUEST, + format!("version non installée : {tag}"), + ); + } + p + }; + switch_simple(ctx, e, &b, &bin).await + } + "/api/engine/remove" => { + let d = gojson::decode_struct(&raw, TAG_REQ, &[("tag", Kind::Str)]); + let tag = crate::paths::go_base(d.str("tag").trim()); + let bin = oci::bin_for(&e.home, &tag); + if bin.is_empty() { + return fail( + StatusCode::BAD_REQUEST, + format!("version non installée : {tag}"), + ); + } + let cfg = store.config().await; + if same_path(&bin, &e.current_bin(&b, &cfg)) { + return fail( + StatusCode::CONFLICT, + "c'est le moteur utilisé — bascule d'abord sur une autre version", + ); + } + let dir = oci::engine_dir(&e.home).join(&tag); + if let Err(err) = std::fs::remove_dir_all(&dir) { + return fail( + StatusCode::INTERNAL_SERVER_ERROR, + crate::paths::os_err("unlinkat", &dir.to_string_lossy(), &err), + ); + } + send_json(StatusCode::OK, &json!({ "ok": true })) + } + "/api/engine/plan" => { + if !post { + return fail(StatusCode::METHOD_NOT_ALLOWED, "POST requis"); + } + if !e.supported { + return fail(StatusCode::OK, UNSUPPORTED_WHY); + } + let d = gojson::decode_struct(&raw, PLAN_REQ, &[("recommended", Kind::Bool)]); + let cfg = store.config().await; + let cur = e.current_bin(&b, &cfg); + let variant = e.variant(&cur); + let res = if d.bool("recommended") { + recommended_tag(e, variant).await + } else { + e.reg.latest(variant).await + }; + let (build, tag) = match res { + Ok(r) => r, + Err(err) => return fail(StatusCode::OK, err), + }; + let (cur_build, _) = oci::build_of(&cur).await; + let inp = + tokio::time::timeout(Duration::from_secs(20), e.preserve_inputs(&b, store, build)) + .await + .unwrap_or_else(|_| PreserveIn { + target: build, + ..PreserveIn::default() + }); + send_json( + StatusCode::OK, + &json!({ + "ok": true, "tag": tag, "build": build, "current": cur_build, + "uptodate": build > 0 && cur_build == build, + "preserve": preserve_risk(&inp), + "keeps": tag_of(&e.home, &cur), + }), + ) + } + "/api/engine/rollback" => { + if !post { + return fail(StatusCode::METHOD_NOT_ALLOWED, "POST requis"); + } + if let Some(err) = crate::backends::tune_busy(store).await { + return send_json( + StatusCode::CONFLICT, + &json!({ "ok": false, "tuning": true, "error": err }), + ); + } + let cfg = store.config().await; + let cur = e.current_bin(&b, &cfg); + let Some(prev) = e.previous(&b, &cur).await else { + return fail( + StatusCode::BAD_REQUEST, + "aucune version précédente conservée", + ); + }; + let bin = prev["bin"].as_str().unwrap_or("").to_string(); + switch_simple(ctx, e, &b, &bin).await + } + _ => fail(StatusCode::NOT_FOUND, "route inconnue"), + } +} + +/// Bascule sans réseau (version installée, image, retour) : BIN, version +/// précédente notée, vérification du rendu périmée, moteur relancé s'il +/// tournait. +async fn switch_simple(ctx: &Ctx, e: &EngineImg, b: &Backends, bin: &str) -> Response { + let store = &ctx.store; + let left = e.current_bin(b, &store.config().await); + if let Err(err) = store.put(BK_CONFIG, "BIN", bin.as_bytes().to_vec()).await { + return fail(StatusCode::INTERNAL_SERVER_ERROR, err); + } + e.remember_previous(&left, bin); + e.render_cancel(); + let _ = crate::backends::service(ctx, "restart", true).await; + send_json(StatusCode::OK, &json!({ "ok": true, "bin": bin })) +} + +/// `handleEngineStatus`. +pub async fn status(e: &EngineImg, b: &Backends, store: &Store) -> Value { + let cfg = store.config().await; + let bin = e.current_bin(b, &cfg); + let (build, commit) = oci::build_of(&bin).await; + let source = e.source(b, &bin); + let installed: Vec = oci::installed(&e.home) + .into_iter() + .map(|tag| { + let p = oci::bin_for(&e.home, &tag); + json!({ + "tag": tag, "bin": p, "build": oci::tag_build(&tag), + "in_use": same_path(&p, &bin), + }) + }) + .collect(); + let trust = build_trust(source, build); + json!({ + "supported": e.supported, + "variant": e.variant(&bin), + "repo": format!("{}/{}", e.reg.host(), oci::ENGINE_REPO), + "bin": bin, + "build": build, + "commit": commit, + "source": source, + "image_bin": b.provided_engine_bin(), + "dir": oci::engine_dir(&e.home).to_string_lossy(), + "installed": if installed.is_empty() { Value::Null } else { Value::Array(installed) }, + "job": b.jobs.snapshot(0, false).await, + "notice": crate::serve::ckpt::engine_build_notice(trust), + "recommend": recommendation(trust, e.supported), + "previous": e.previous(b, &bin).await.unwrap_or(Value::Null), + "render_check": e.render_snapshot().map_or(Value::Null, |c| c.to_json()), + }) +} + +/// `engineRecommendedTag`. +async fn recommended_tag(e: &EngineImg, variant: &str) -> Result<(i64, String), String> { + let min = crate::serve::ckpt::ENGINE_MIN_RECOMMENDED; + let (build, tag) = e + .reg + .latest(variant) + .await + .map_err(|err| format!("registre injoignable, aucune version choisie : {err}"))?; + if build == 0 { + return Err(format!( + "le registre n'annonce pas le build du tag « {variant} » : impossible de vérifier qu'il atteint b{min}" + )); + } + if build < min { + return Err(format!( + "dernier build publié pour {variant} : b{build}, antérieur à la version recommandée b{min}" + )); + } + if let Err(err) = e.reg.resolve(oci::ENGINE_REPO, &tag).await { + return Err(format!( + "{tag} annoncé mais introuvable sur le registre : {err}" + )); + } + Ok((build, tag)) +} + +/// `engineHelpHas` : l'aide du binaire mentionne-t-elle ce drapeau ? +async fn help_has(bin: &str, flag: &str) -> bool { + let (out, _) = oci::run_bin(bin, "--help", Some(Duration::from_secs(15))).await; + out.contains(flag) +} + +/// `enginePreserveOffIf` : REASONING_PRESERVE=off, dans le preset actif +/// s'il y en a un, dans la configuration sinon. +async fn preserve_off(ctx: &Ctx, e: &EngineImg, new_bin: &str) -> Result { + let store = &ctx.store; + let cfg = store.config().await; + let v = cfg.get("REASONING_PRESERVE").map_or("", |s| s.trim()); + if !v.is_empty() { + return Ok(format!("REASONING_PRESERVE={v} déjà posée : inchangée")); + } + if !help_has(new_bin, "--no-reasoning-preserve").await { + return Err("le nouveau moteur ne connaît pas --no-reasoning-preserve".into()); + } + if let Some(err) = crate::backends::tune_busy(store).await { + return Err(err); + } + let order = crate::presets::parse_preset_order( + store + .get(BK_STATE, crate::presets::PRESET_ORDER_KEY) + .await + .ok() + .flatten() + .as_deref(), + ); + if let Ok(list) = crate::presets::list_presets(&e.home, &cfg, &order) + && let Some(p) = list.into_iter().find(|p| p.active) + { + let content = crate::presets_api::read_preset(&e.home, &p.id)?; + crate::presets_api::save_applying( + ctx, + &p.id, + &p.name, + &crate::presets_api::preset_set_key(&content, "REASONING_PRESERVE", "off"), + "save", + ) + .await?; + return Ok(format!( + "REASONING_PRESERVE=off posée dans le preset « {} »", + p.name + )); + } + store + .put(BK_CONFIG, "REASONING_PRESERVE", b"off".to_vec()) + .await?; + Ok("REASONING_PRESERVE=off posée dans la configuration active (aucun preset ne lui correspond)".into()) +} + +/// `engineSwitchTo` : BIN posé, moteur arrêté puis relancé s'il tournait. +async fn switch_to(ctx: &Ctx, log: &Log<'_>, bin: &str) -> Result<(), String> { + if let Some(err) = crate::backends::tune_busy(&ctx.store).await { + return Err(err); + } + let (was_up, err) = crate::backends::service_stop_if_up(ctx).await; + if was_up { + log.phase("arrêt du moteur…").await; + if !err.is_empty() { + log.line(&format!("[warn] arrêt du moteur : {err}")).await; + } + } + if let Err(e) = ctx + .store + .put(BK_CONFIG, "BIN", bin.as_bytes().to_vec()) + .await + { + if was_up { + let _ = crate::backends::service(ctx, "start", false).await; + } + return Err(format!( + "moteur installé mais échec d'écriture de BIN : {e}" + )); + } + log.line(&format!("BIN → {bin}")).await; + if was_up { + log.phase("redémarrage du moteur…").await; + let (_, e) = crate::backends::service(ctx, "start", false).await; + if !e.is_empty() { + log.line(&format!("[warn] redémarrage du moteur : {e}")) + .await; + } + } + Ok(()) +} + +/// `engineRunUpdate` : télécharger, ESSAYER, puis seulement basculer. +async fn run_update( + ctx: &Ctx, + e: &Arc, + b: &Backends, + mut tag: String, + preserve_off_asked: bool, +) { + let store = &ctx.store; + let log = Log { + jobs: &b.jobs, + store, + }; + let cfg = store.config().await; + let ancien = e.current_bin(b, &cfg); + let variant = e.variant(&ancien); + if tag.is_empty() { + log.phase("recherche de la dernière version publiée…").await; + match e.reg.latest(variant).await { + Err(err) => return b.jobs.fail(store, &err).await, + Ok((build, latest)) => { + tag = latest; + if build > 0 { + let (cur, _) = oci::build_of(&e.current_bin(b, &store.config().await)).await; + if cur == build { + return b + .jobs + .done(store, &format!("déjà à jour (build {build})")) + .await; + } + } + } + } + } + log.line(&format!("variante : {variant} — version visée : {tag}")) + .await; + let bin = match oci::install(&e.home, &e.reg, &tag, &log).await { + Ok(bin) => bin, + Err(err) => return b.jobs.fail(store, &err).await, + }; + log.phase("essai du nouveau moteur…").await; + if let Err(err) = oci::smoke_test(&bin, &ancien).await { + let _ = std::fs::remove_dir_all(crate::paths::go_dir(&bin)); + return b + .jobs + .fail( + store, + &format!("{err}\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur"), + ) + .await; + } + let (new_build, commit) = oci::build_of(&bin).await; + if new_build > 0 { + log.line(&format!("moteur testé : build {new_build} ({commit})")) + .await; + } + if preserve_off_asked { + match preserve_off(ctx, e, &bin).await { + Err(err) => { + return b + .jobs + .fail( + store, + &format!("REASONING_PRESERVE=off demandée mais pas posée : {err} — moteur courant inchangé ; le nouveau reste installé"), + ) + .await; + } + Ok(msg) => log.line(&msg).await, + } + } else { + let inp = tokio::time::timeout( + Duration::from_secs(20), + e.preserve_inputs(b, store, new_build), + ) + .await + .unwrap_or_else(|_| PreserveIn { + target: new_build, + ..PreserveIn::default() + }); + let risk = preserve_risk(&inp); + if risk["risk"] != "no" { + log.line(&format!( + "[warn] rendu du raisonnement : {}", + risk["why"].as_str().unwrap_or("") + )) + .await; + } + } + let reference = tokio::time::timeout(Duration::from_secs(30), e.render_before(store)) + .await + .ok() + .flatten(); + e.render_cancel(); + if let Err(err) = switch_to(ctx, &log, &bin).await { + return b.jobs.fail(store, &err).await; + } + e.remember_previous(&ancien, &bin); + let mut keep = HashSet::new(); + keep.insert(crate::paths::go_base(&crate::paths::go_dir(&bin))); + let prev = tag_of(&e.home, &ancien); + if !prev.is_empty() && !keep.contains(&prev) { + keep.insert(prev.clone()); + log.line(&format!( + "version précédente gardée pour revenir en arrière : {prev}" + )) + .await; + } + for l in oci::prune(&e.home, &keep) { + log.line(&l).await; + } + if let Some(r) = reference { + log.line("vérification du rendu du gabarit une fois le modèle chargé (panneau Moteur, journal [moteur])") + .await; + let (e2, store2, tag2) = (e.clone(), store.clone(), tag.clone()); + tokio::spawn(async move { e2.render_after(&store2, r, &tag2).await }); + } + b.jobs + .done(store, &format!("moteur mis à jour ({tag})")) + .await; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn confiance() { + assert_eq!(build_trust("custom", 10000), 0); + assert_eq!(build_trust("image", 4999), 0); + assert_eq!(build_trust("image", 5000), 5000); + assert!(recommendation(10000, true).is_object()); + assert!(recommendation(10864, true).is_null()); + assert!(recommendation(0, true).is_null()); + assert!(recommendation(10000, false).is_null()); + } + + #[test] + fn variantes() { + let d = tempfile::tempdir().unwrap(); + let e = EngineImg::new( + d.path().to_path_buf(), + oci::Registry::new("http://127.0.0.1:9".into(), false), + true, + Arc::new(TplProbe::new()), + ); + let bin = d.path().join("llama-server"); + let bin = bin.to_string_lossy(); + assert_eq!(e.variant(""), "server"); + assert_eq!(e.variant(&bin), "server"); + for (lib, want) in [ + ("libggml-vulkan.so", "server-vulkan"), + ("libggml-sycl.so", "server-intel"), + ("libggml-musa.so.1", "server-musa"), + ("libggml-cuda.so", "server-cuda"), + ] { + std::fs::write(d.path().join(lib), "").unwrap(); + assert_eq!(e.variant(&bin), want, "{lib}"); + } + } + + /// Une vérification périmée par une bascule plus récente ne range rien. + #[test] + fn verification_perimee() { + let d = tempfile::tempdir().unwrap(); + let e = EngineImg::new( + d.path().to_path_buf(), + oci::Registry::new("http://127.0.0.1:9".into(), false), + true, + Arc::new(TplProbe::new()), + ); + let old = e.render_begin(); + e.render_set(old, "pending", "a", ""); + let new = e.render_begin(); + assert!(!e.render_current(old)); + e.render_set(old, "same", "a", ""); + assert_eq!(e.render_snapshot().unwrap().status, "pending"); + e.render_set(new, "changed", "b", "d"); + let c = e.render_snapshot().unwrap(); + assert_eq!((c.status.as_str(), c.tag.as_str()), ("changed", "b")); + assert_eq!(c.to_json()["detail"], "d"); + e.render_cancel(); + assert!(e.render_snapshot().is_none()); + assert!(!e.render_current(new)); + } + + /// Les bornes du défaut de b10763. + #[test] + fn bornes_du_rendu() { + let base = PreserveIn { + tpl_known: true, + template: "preserve_thinking".into(), + preserves: TplTri::No, + ..PreserveIn::default() + }; + let risk = |i: PreserveIn| preserve_risk(&i)["why"].as_str().unwrap().to_string(); + assert!( + risk(PreserveIn { + target: 10762, + ..base.clone() + }) + .starts_with("b10762 n'a pas") + ); + assert!( + risk(PreserveIn { + target: 10763, + ..base.clone() + }) + .starts_with("le gabarit chargé retire") + ); + assert!(risk(base.clone()).starts_with("le gabarit chargé retire")); + assert!( + risk(PreserveIn { + current: 10763, + ..base.clone() + }) + .starts_with("le moteur courant (b10763)") + ); + assert!( + risk(PreserveIn { + current: 10762, + ..base.clone() + }) + .starts_with("le gabarit chargé retire") + ); + assert_eq!(preserve_risk(&base)["suggest"], true); + } + + #[test] + fn surcharges() { + let cfg = HashMap::from([( + "EXTRA_ARGS".to_string(), + "--no-reasoning-preserve".to_string(), + )]); + assert_eq!(preserve_overridden(&cfg, |_| String::new()), "EXTRA_ARGS"); + let cfg = HashMap::from([( + "EXTRA_ARGS".to_string(), + r#"--chat-template-kwargs '{"preserve_reasoning":false}'"#.to_string(), + )]); + assert_eq!( + preserve_overridden(&cfg, |_| String::new()), + "--chat-template-kwargs" + ); + let none = HashMap::new(); + assert_eq!( + preserve_overridden(&none, |k| if k == "LLAMA_ARG_REASONING_PRESERVE" { + " 0 ".into() + } else { + String::new() + }), + "LLAMA_ARG_REASONING_PRESERVE" + ); + assert_eq!(preserve_overridden(&none, |_| String::new()), ""); + } +} diff --git a/rust/loki-rs/src/engineimg/oci.rs b/rust/loki-rs/src/engineimg/oci.rs new file mode 100644 index 0000000..c56347c --- /dev/null +++ b/rust/loki-rs/src/engineimg/oci.rs @@ -0,0 +1,631 @@ +//! Le moteur llama.cpp pris dans l'image de conteneur officielle — port de +//! `backend_engine_oci.go` : jeton anonyme, manifeste (index multi- +//! architecture puis manifeste de la plateforme), couches du sommet vers la +//! base jusqu'à tenir un moteur complet, extraction à plat de `/app` avec ses +//! liens, bascule atomique du dossier, et lecture du numéro de build. + +use std::collections::HashSet; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; +use std::time::Duration; + +use regex::Regex; +use serde::Deserialize; +use serde_json::Value; + +use crate::backends::Log; +use crate::gojson::{self, Kind}; + +/// `ociEngineRepo`. +pub const ENGINE_REPO: &str = "ggml-org/llama.cpp"; + +/// Au-delà, une couche est le runtime de l'image, pas le moteur (`ociLayerCap`). +pub const LAYER_CAP: i64 = 600 << 20; + +/// `ociAccept`. +const ACCEPT: &str = "application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json"; + +/// `ociDoc` décodé : index ou manifeste. +const DOC_TYPE: &str = "ociDoc"; + +/// `ociBase` : `https://ghcr.io`, ou `LOKI_OCI_REGISTRY` (schéma ajouté au +/// besoin, `/` final retiré). +pub fn base_from_env(v: Option<&str>) -> String { + let v = v.unwrap_or("").trim().trim_end_matches('/'); + if v.is_empty() { + return "https://ghcr.io".into(); + } + if v.contains("://") { + v.into() + } else { + format!("https://{v}") + } +} + +/// `ociHost`. +pub fn host_of(base: &str) -> String { + base.trim_start_matches("https://") + .trim_start_matches("http://") + .trim_end_matches('/') + .to_string() +} + +/// `ociArch`. +pub fn oci_arch(arm64: bool) -> &'static str { + if arm64 { "arm64" } else { "amd64" } +} + +/// Une couche du manifeste. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct Layer { + #[serde(default, rename = "mediaType")] + pub media_type: String, + #[serde(default)] + pub digest: String, + #[serde(default)] + pub size: i64, +} + +/// Le client du registre. +#[derive(Clone)] +pub struct Registry { + pub base: String, + pub arm64: bool, + client: reqwest::Client, +} + +impl Registry { + pub fn new(base: String, arm64: bool) -> Self { + crate::chat::engine::install_crypto(); + Registry { + base, + arm64, + client: reqwest::Client::builder().build().unwrap_or_default(), + } + } + + pub fn host(&self) -> String { + host_of(&self.base) + } + + /// `ociToken` : un jeton de lecture anonyme. + pub async fn token(&self, repo: &str) -> Result { + let u = format!( + "{}/token?scope=repository:{repo}:pull&service={}", + self.base, + self.host() + ); + let resp = self + .client + .get(&u) + .timeout(Duration::from_secs(30)) + .send() + .await + .map_err(|e| format!("registre {} injoignable : {e}", self.host()))?; + if resp.status().as_u16() != 200 { + return Err(format!( + "{}/token : HTTP {}", + self.host(), + resp.status().as_u16() + )); + } + let body = resp.bytes().await.unwrap_or_default(); + let d = gojson::decode_struct( + &body, + "struct { Token string \"json:\\\"token\\\"\" }", + &[("token", Kind::Str)], + ); + let t = d.str("token"); + if d.err.is_some() || t.is_empty() { + return Err("jeton de lecture illisible".into()); + } + Ok(t) + } + + /// Un manifeste (index ou image) par référence. + async fn doc(&self, repo: &str, reference: &str, token: &str) -> Result { + let resp = self + .client + .get(format!("{}/v2/{repo}/manifests/{reference}", self.base)) + .header("Authorization", format!("Bearer {token}")) + .header("Accept", ACCEPT) + .send() + .await + .map_err(|e| e.to_string())?; + match resp.status().as_u16() { + 404 => return Err(format!("version inconnue dans le registre : {reference}")), + 200 => {} + s => return Err(format!("manifeste {reference} : HTTP {s}")), + } + let body = resp.bytes().await.map_err(|e| e.to_string())?; + let d = gojson::decode_struct( + &body, + DOC_TYPE, + &[ + ("mediaType", Kind::Str), + ("manifests", Kind::Any), + ("layers", Kind::Any), + ("annotations", Kind::Any), + ], + ); + if let Some(e) = d.err { + return Err(format!("manifeste {reference} illisible : {e}")); + } + Ok(Value::Object(d.fields)) + } + + /// `ociResolve` : les couches de l'image pour cette architecture, et le + /// build qu'elle annonce. + pub async fn resolve(&self, repo: &str, tag: &str) -> Result<(Vec, String), String> { + let token = self.token(repo).await?; + let version_of = |d: &Value| -> String { + d.get("annotations") + .and_then(|a| a.get("org.opencontainers.image.version")) + .and_then(Value::as_str) + .unwrap_or("") + .to_string() + }; + let mut doc = self.doc(repo, tag, &token).await?; + let mut version = version_of(&doc); + let manifests = doc + .get("manifests") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + if !manifests.is_empty() { + let want = oci_arch(self.arm64); + let digest = manifests + .iter() + .find(|m| m["platform"]["os"] == "linux" && m["platform"]["architecture"] == want) + .and_then(|m| m.get("digest").and_then(Value::as_str)) + .unwrap_or("") + .to_string(); + if digest.is_empty() { + return Err(format!("aucune image linux/{want} pour {tag}")); + } + doc = self.doc(repo, &digest, &token).await?; + let v = version_of(&doc); + if !v.is_empty() { + version = v; + } + } + let layers: Vec = doc + .get("layers") + .cloned() + .and_then(|l| serde_json::from_value(l).ok()) + .unwrap_or_default(); + if layers.is_empty() { + return Err(format!("manifeste {tag} sans couche")); + } + Ok((layers, version)) + } + + /// `engineOCILatest` : le dernier build publié pour la variante (numéro et + /// tag figé), lu sur l'annotation du tag mouvant. + pub async fn latest(&self, variant: &str) -> Result<(i64, String), String> { + let (_, version) = self.resolve(ENGINE_REPO, variant).await?; + if version.is_empty() { + return Ok((0, variant.into())); + } + let n = version.trim_start_matches('b').parse().unwrap_or(0); + Ok((n, format!("{variant}-{version}"))) + } +} + +/// `engineDir`. +pub fn engine_dir(home: &Path) -> PathBuf { + home.join("engine") +} + +static RE_TAG_BUILD: LazyLock = LazyLock::new(|| Regex::new(r"-b([0-9]+)$").expect("motif")); + +/// `engineTagBuild` : « server-cuda-b10450 » → 10450, 0 sans numéro. +pub fn tag_build(tag: &str) -> i64 { + RE_TAG_BUILD + .captures(tag) + .and_then(|m| m[1].parse().ok()) + .unwrap_or(0) +} + +/// `engineInstalled` : les versions présentes, de la plus ancienne à la plus +/// récente. +pub fn installed(home: &Path) -> Vec { + let dir = engine_dir(home); + let Ok(rd) = std::fs::read_dir(&dir) else { + return Vec::new(); + }; + let mut tags: Vec = rd + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_dir())) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .filter(|t| crate::backends::probe::is_file(&dir.join(t).join("llama-server"))) + .collect(); + tags.sort(); + tags.sort_by_key(|t| tag_build(t)); + tags +} + +/// `engineBinFor` : le llama-server d'une version installée, "" sinon. +pub fn bin_for(home: &Path, tag: &str) -> String { + let p = engine_dir(home) + .join(crate::paths::go_base(tag)) + .join("llama-server"); + if crate::backends::probe::is_file(&p) { + p.to_string_lossy().into_owned() + } else { + String::new() + } +} + +/// `enginePrune` : les versions installées hors `keep`, supprimées. +pub fn prune(home: &Path, keep: &HashSet) -> Vec { + let mut out = Vec::new(); + for tag in installed(home) { + if keep.contains(&tag) { + continue; + } + if std::fs::remove_dir_all(engine_dir(home).join(&tag)).is_ok() { + out.push(format!("ancienne version supprimée : {tag}")); + } + } + out +} + +/// `appEntryName` : « app/libllama.so » → « libllama.so » ; rien pour tout ce +/// qui n'est pas un fichier direct de /app (ni marqueur de suppression). +pub fn app_entry_name(raw: &str) -> Option { + let p = raw.trim().replace('\\', "/"); + let p = p.strip_prefix("./").unwrap_or(&p); + let rest = p.strip_prefix("app/")?; + if rest.is_empty() || rest.contains('/') || rest.starts_with(".wh.") { + return None; + } + Some(rest.to_string()) +} + +/// `engineSetComplete` : le binaire ET ses bibliothèques. +pub fn set_complete(seen: &HashSet) -> bool { + seen.contains("llama-server") + && seen.iter().any(|n| n.starts_with("libggml-base.so")) + && seen.iter().any(|n| n.starts_with("libllama.so")) +} + +/// `extractAppLayer` : les entrées `app/…` d'une couche, à plat dans `dir` ; +/// une couche supérieure fait foi (`seen`). Rend le nombre de fichiers. +pub fn extract_app_layer( + archive: &Path, + dir: &Path, + seen: &mut HashSet, + links: &mut Vec<(PathBuf, String)>, +) -> Result { + let f = std::fs::File::open(archive) + .map_err(|e| crate::paths::os_err("open", &archive.to_string_lossy(), &e))?; + let gz = flate2::read::GzDecoder::new(f); + let mut tr = tar::Archive::new(gz); + let mut count = 0; + for entry in tr.entries().map_err(|e| e.to_string())? { + let mut entry = entry.map_err(|e| e.to_string())?; + let raw = String::from_utf8_lossy(&entry.path_bytes()).into_owned(); + let Some(name) = app_entry_name(&raw) else { + continue; + }; + if seen.contains(&name) { + continue; + } + let p = dir.join(&name); + let kind = entry.header().entry_type(); + if kind.is_file() { + let mut buf = Vec::new(); + entry.read_to_end(&mut buf).map_err(|e| e.to_string())?; + std::fs::write(&p, &buf) + .map_err(|e| crate::paths::os_err("open", &p.to_string_lossy(), &e))?; + #[cfg(unix)] + if let Ok(mode) = entry.header().mode() { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&p, std::fs::Permissions::from_mode(mode & 0o777)); + } + seen.insert(name); + count += 1; + } else if kind.is_symlink() || kind.is_hard_link() { + let link = entry + .link_name_bytes() + .map(|b| String::from_utf8_lossy(&b).into_owned()) + .unwrap_or_default(); + let target = app_entry_name(&link).unwrap_or_else(|| crate::paths::go_base(&link)); + links.push((p, target)); + seen.insert(name); + count += 1; + } + } + Ok(count) +} + +/// `engineOCIInstall` : le moteur du tag dans `engine/` ; rend le chemin +/// du llama-server. Une version déjà là est rendue telle quelle. +pub async fn install( + home: &Path, + reg: &Registry, + tag: &str, + log: &Log<'_>, +) -> Result { + let tag = crate::paths::go_base(tag.trim()); + if tag.is_empty() || tag == "." { + return Err("version du moteur non précisée".into()); + } + let bin = bin_for(home, &tag); + if !bin.is_empty() { + log.line(&format!("déjà installé : {tag}")).await; + return Ok(bin); + } + log.phase(&format!("lecture du manifeste {tag}…")).await; + let (layers, version) = reg.resolve(ENGINE_REPO, &tag).await?; + if !version.is_empty() { + log.line(&format!( + "image {ENGINE_REPO}:{tag} — llama.cpp {version}, {} couches", + layers.len() + )) + .await; + } + let dest = engine_dir(home).join(&tag); + let tmp = PathBuf::from(format!("{}.part", dest.to_string_lossy())); + let _ = std::fs::remove_dir_all(&tmp); + std::fs::create_dir_all(&tmp) + .map_err(|e| crate::paths::os_err("mkdir", &tmp.to_string_lossy(), &e))?; + let res = install_into(reg, &tag, &layers, &tmp, &dest, log).await; + let _ = std::fs::remove_dir_all(&tmp); + res +} + +async fn install_into( + reg: &Registry, + tag: &str, + layers: &[Layer], + tmp: &Path, + dest: &Path, + log: &Log<'_>, +) -> Result { + let token = reg.token(ENGINE_REPO).await?; + let mut seen = HashSet::new(); + let mut links = Vec::new(); + for (i, l) in layers.iter().enumerate().rev() { + if l.size > LAYER_CAP { + log.line(&format!( + "couche {i} ignorée ({} Mo) : c'est le runtime de l'image, pas le moteur", + l.size / 1_000_000 + )) + .await; + break; + } + let part = tmp.join("layer.tgz"); + if l.size > 4 << 20 { + log.phase(&format!( + "téléchargement du moteur ({} Mo)…", + l.size / 1_000_000 + )) + .await; + } + let rq = reg + .client + .get(format!("{}/v2/{ENGINE_REPO}/blobs/{}", reg.base, l.digest)) + .header("Authorization", format!("Bearer {token}")); + crate::backends::log_download(log, rq, &part, l.size) + .await + .map_err(|e| format!("téléchargement de la couche {i} : {e}"))?; + let (p2, d2) = (part.clone(), tmp.to_path_buf()); + let mut s2 = std::mem::take(&mut seen); + let mut l2 = std::mem::take(&mut links); + let (n, s2, l2) = tokio::task::spawn_blocking(move || { + let r = extract_app_layer(&p2, &d2, &mut s2, &mut l2); + (r, s2, l2) + }) + .await + .map_err(|e| e.to_string())?; + seen = s2; + links = l2; + let _ = std::fs::remove_file(&part); + let n = n.map_err(|e| format!("extraction de la couche {i} : {e}"))?; + if n > 0 { + log.line(&format!("couche {i} : {n} fichiers du moteur")) + .await; + } + if set_complete(&seen) { + break; + } + } + crate::backends::prebuilt::apply_links(&links)?; + if !seen.contains("llama-server") { + return Err(format!( + "llama-server absent de l'image {tag} — variante inattendue ?" + )); + } + let _ = std::fs::remove_dir_all(dest); + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| crate::paths::os_err("mkdir", &parent.to_string_lossy(), &e))?; + } + std::fs::rename(tmp, dest).map_err(|e| e.to_string())?; + let bin = dest.join("llama-server"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)) + .map_err(|e| crate::paths::os_err("chmod", &bin.to_string_lossy(), &e))?; + } + let bin = bin.to_string_lossy().into_owned(); + log.line(&format!("moteur installé : {bin}")).await; + Ok(bin) +} + +static RE_BUILD_LONG: LazyLock = LazyLock::new(|| { + Regex::new(r"build[\t\n\f\r ]+([0-9]+),[\t\n\f\r ]*commit[\t\n\f\r ]+([0-9a-fA-F]+)") + .expect("motif") +}); +static RE_BUILD_SHORT: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?m)^[\t\n\f\r ]*(?:version|build)[\t\n\f\r ]*:[\t\n\f\r ]*([0-9]+)[\t\n\f\r ]*\(([0-9a-fA-F]+)\)", + ) + .expect("motif") +}); + +/// `parseEngineBuild` : (build, commit) des trois formes de la bannière. +pub fn parse_build(out: &str) -> (i64, String) { + for re in [&*RE_BUILD_LONG, &*RE_BUILD_SHORT] { + if let Some(m) = re.captures(out) { + return (m[1].parse().unwrap_or(0), m[2].to_string()); + } + } + (0, String::new()) +} + +/// Un binaire lancé avec ses bibliothèques à côté : (sortie mêlée, erreur au +/// format d'`exec`). `limit` : délai (tué au-delà). +pub async fn run_bin(bin: &str, arg: &str, limit: Option) -> (String, Option) { + let dir = crate::paths::go_dir(bin); + let env = crate::serve::run::Env::default(); + let (k, v) = crate::serve::run::library_path_value(&env, &dir); + let mut c = tokio::process::Command::new(bin); + c.arg(arg) + .env(k, v) + .stdin(std::process::Stdio::null()) + .kill_on_drop(true); + crate::backends::devices::combined(c, bin, limit).await +} + +/// `engineBuildOf` : le build d'un llama-server (0 s'il ne répond pas). +pub async fn build_of(bin: &str) -> (i64, String) { + if bin.is_empty() || !crate::backends::probe::is_file(Path::new(bin)) { + return (0, String::new()); + } + let (out, _) = run_bin(bin, "--version", Some(Duration::from_secs(15))).await; + parse_build(&out) +} + +/// `tailLines` : les n dernières lignes. +pub fn tail_lines(s: &str, n: usize) -> String { + let lines: Vec<&str> = s.trim_end_matches('\n').split('\n').collect(); + let from = lines.len().saturating_sub(n); + lines[from..].join("\n") +} + +/// `engineListDevices`. +pub async fn list_devices(bin: &str) -> Result { + let (out, err) = run_bin(bin, "--list-devices", None).await; + if let Some(e) = err { + return Err(format!( + "le moteur ne démarre pas ici : {e}\n{}", + tail_lines(&out, 12) + )); + } + Ok(crate::backends::devices::parse_list_devices(&out).len()) +} + +/// `engineSmokeTest` : le nouveau moteur démarre, et voit les cartes que +/// voyait l'actuel. +pub async fn smoke_test(bin: &str, ancien: &str) -> Result<(), String> { + if list_devices(bin).await? > 0 { + return Ok(()); + } + if ancien.is_empty() || ancien == bin { + return Ok(()); + } + match list_devices(ancien).await { + Ok(n) if n > 0 => Err(format!( + "le moteur téléchargé ne voit aucune carte graphique, alors que le moteur actuel en voit {n} — il tournerait sur le processeur, beaucoup plus lentement" + )), + _ => Ok(()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn entrees_app() { + assert_eq!( + app_entry_name("app/libllama.so").as_deref(), + Some("libllama.so") + ); + assert_eq!( + app_entry_name("./app/llama-server").as_deref(), + Some("llama-server") + ); + assert_eq!(app_entry_name("app/"), None); + assert_eq!(app_entry_name("app/sous/x"), None); + assert_eq!(app_entry_name("app/.wh.x"), None); + assert_eq!(app_entry_name("usr/app/x"), None); + } + + #[test] + fn builds() { + assert_eq!( + parse_build("version: 0.1.0-dev (build 10450, commit ece963f41)"), + (10450, "ece963f41".into()) + ); + assert_eq!( + parse_build("version: 10450 (ece963f)"), + (10450, "ece963f".into()) + ); + assert_eq!( + parse_build("build: 4589 (a1b2c3d)"), + (4589, "a1b2c3d".into()) + ); + assert_eq!(parse_build("rien"), (0, String::new())); + assert_eq!(tag_build("server-cuda-b10450"), 10450); + assert_eq!(tag_build("server-cuda"), 0); + } + + #[test] + fn moteur_complet() { + let set = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::>(); + assert!(!set_complete(&set(&["libggml-base.so", "libllama.so"]))); + assert!(!set_complete(&set(&["llama-server", "libggml-base.so.0"]))); + assert!(!set_complete(&set(&["llama-server", "libllama.so.0"]))); + assert!(set_complete(&set(&[ + "llama-server", + "libggml-base.so.0", + "libllama.so" + ]))); + assert_eq!(tail_lines("a\nb\nc\n", 2), "b\nc"); + assert_eq!(tail_lines("a", 5), "a"); + } + + /// Ne voir aucune carte n'est un échec que si le moteur courant en voyait. + #[tokio::test] + async fn essai_a_blanc() { + use std::os::unix::fs::PermissionsExt; + let d = tempfile::tempdir().unwrap(); + let script = |name: &str, body: &str| { + let p = d.path().join(name); + std::fs::write(&p, format!("#!/bin/sh\n{body}\n")).unwrap(); + std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).unwrap(); + p.to_string_lossy().into_owned() + }; + let none = script("aucune", "echo 'Available devices:'"); + let one = script( + "une", + "echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)'", + ); + let ko = script("ko", "echo panne; exit 3"); + assert_eq!(smoke_test(&none, "").await, Ok(())); + assert_eq!(smoke_test(&none, &none).await, Ok(())); + assert_eq!(smoke_test(&none, &ko).await, Ok(())); + assert_eq!(smoke_test(&one, &ko).await, Ok(())); + let e = smoke_test(&none, &one).await.unwrap_err(); + assert!(e.contains("en voit 1"), "{e}"); + let e = smoke_test(&ko, &one).await.unwrap_err(); + assert_eq!(e, "le moteur ne démarre pas ici : exit status 3\npanne"); + } + + #[test] + fn registre() { + assert_eq!(base_from_env(None), "https://ghcr.io"); + assert_eq!( + base_from_env(Some(" miroir.local:5000/ ")), + "https://miroir.local:5000" + ); + assert_eq!(base_from_env(Some("http://x")), "http://x"); + assert_eq!(host_of("https://ghcr.io/"), "ghcr.io"); + } +} diff --git a/rust/loki-rs/src/lib.rs b/rust/loki-rs/src/lib.rs index 980da5d..337feed 100644 --- a/rust/loki-rs/src/lib.rs +++ b/rust/loki-rs/src/lib.rs @@ -22,6 +22,7 @@ pub mod dictate; pub mod downloads; pub mod engine; pub mod enginectl; +pub mod engineimg; pub mod engstatus; pub mod gguf; pub mod gojson; diff --git a/rust/loki-rs/src/main.rs b/rust/loki-rs/src/main.rs index ee1c242..878cc2a 100644 --- a/rust/loki-rs/src/main.rs +++ b/rust/loki-rs/src/main.rs @@ -167,6 +167,13 @@ fn serve() -> ExitCode { // bascule, GPU) et LE job des moteurs, auquel les jobs du Go // (compilation, moteur de l'image, Strata) rapportent leurs étapes. let backends_rust = held_by_facade("LOKI_BACKENDS"); + // LOKI_ENGINE_IMAGE=rust : le panneau « moteur de l'image » (mise à + // jour depuis le registre officiel), avec les backends seulement : sa + // mise à jour est un job, et le job est à eux. + let engineimg_rust = held_by_facade("LOKI_ENGINE_IMAGE") && backends_rust; + if held_by_facade("LOKI_ENGINE_IMAGE") && !backends_rust { + eprintln!("[loki-rs] moteur de l'image tenu par le Go (LOKI_BACKENDS=rust requis)"); + } let engine_rust = held_by_facade("LOKI_ENGINE") && match loki_rs::enginectl::usable( std::env::var("LOKI_CONTAINER").ok().as_deref(), @@ -199,6 +206,7 @@ fn serve() -> ExitCode { ("LOKI_MCP", mcp_rust), ("LOKI_UPDATE", update_rust), ("LOKI_BACKENDS", backends_rust), + ("LOKI_ENGINE_IMAGE", engineimg_rust), ]; for (var, on) in held { if on { @@ -265,6 +273,28 @@ fn serve() -> ExitCode { None }; + // La sonde de gabarit de la conversation quand la façade la tient : + // le relevé du rendu rejoue la forme de ses vraies requêtes. + let engineimg = engineimg_rust.then(|| { + let probe = chat + .as_ref() + .map(|c| c.shared.tpl.clone()) + .unwrap_or_else(|| std::sync::Arc::new(loki_rs::chat::tplprobe::TplProbe::new())); + let reg = loki_rs::engineimg::oci::Registry::new( + loki_rs::engineimg::oci::base_from_env( + std::env::var("LOKI_OCI_REGISTRY").ok().as_deref(), + ), + cfg!(target_arch = "aarch64"), + ); + std::sync::Arc::new(loki_rs::engineimg::EngineImg::new( + engine_paths.home.clone(), + reg, + cfg!(target_os = "linux") + && (cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64")), + probe, + )) + }); + let files = if files_rust { Some(start_files(backend, &token).await) } else { @@ -321,6 +351,7 @@ fn serve() -> ExitCode { projects: projects_rust, update: update_rust, backends, + engineimg, mcp: mcp_rust.then(|| std::sync::Arc::new(loki_rs::mcp::Mcp::new(mem_home))), }; let res = proxy::serve(listener, cfg, shutdown_signal()).await; diff --git a/rust/loki-rs/src/paths.rs b/rust/loki-rs/src/paths.rs index b695929..3fb894f 100644 --- a/rust/loki-rs/src/paths.rs +++ b/rust/loki-rs/src/paths.rs @@ -359,6 +359,22 @@ pub fn go_dir(path: &str) -> String { format!("{}{dir}", &path[..vol_len]) } +/// `filepath.Base` : le dernier élément ; "." pour un chemin vide, "/" pour +/// une suite de séparateurs. +pub fn go_base(p: &str) -> String { + if p.is_empty() { + return ".".into(); + } + let t = p.trim_end_matches(|c: char| c == '/' || (cfg!(windows) && c == '\\')); + if t.is_empty() { + return "/".into(); + } + match t.rfind(|c: char| c == '/' || (cfg!(windows) && c == '\\')) { + Some(i) => t[i + 1..].to_string(), + None => t.to_string(), + } +} + /// `filepath.IsAbs`. pub fn go_is_abs(path: &str) -> bool { if !cfg!(windows) { diff --git a/rust/loki-rs/src/presets_api.rs b/rust/loki-rs/src/presets_api.rs index 35fc1ff..8fd4565 100644 --- a/rust/loki-rs/src/presets_api.rs +++ b/rust/loki-rs/src/presets_api.rs @@ -870,7 +870,7 @@ async fn live(store: &Store) -> Live { /// `SavePresetApplying` : enregistre ; si c'était le preset EN SERVICE (avant /// l'écriture), l'applique (`mode` : save ou apply). -async fn save_applying( +pub(crate) async fn save_applying( ctx: &Ctx, id: &str, name: &str, diff --git a/rust/loki-rs/src/proxy.rs b/rust/loki-rs/src/proxy.rs index 8e26eaa..4b7e64f 100644 --- a/rust/loki-rs/src/proxy.rs +++ b/rust/loki-rs/src/proxy.rs @@ -96,6 +96,10 @@ pub struct Config { /// Backends llama.cpp (statut, précompilé, bascule, GPU) et job des /// moteurs tenus par la façade (`LOKI_BACKENDS=rust`) ; `None` : au Go. pub backends: Option>, + /// Panneau « moteur de l'image » (version, mise à jour depuis le + /// registre, bascules, rendu du gabarit) tenu par la façade + /// (`LOKI_ENGINE_IMAGE=rust`) ; `None` : au Go. + pub engineimg: Option>, } struct Proxy { @@ -136,6 +140,7 @@ pub async fn serve( mcp: cfg.mcp, update: cfg.update, backends: cfg.backends, + engineimg: cfg.engineimg, internal_token: cfg.internal_token.clone(), }, }); @@ -240,6 +245,11 @@ impl Proxy { { return chatapi::serve(&self.ctx, chat, req, from_go).await; } + if let Some(e) = &self.ctx.engineimg + && crate::engineimg::handles(path) + { + return crate::engineimg::serve(&self.ctx, e, req, from_go).await; + } if let Some(b) = &self.ctx.backends && crate::backends::handles(path) { diff --git a/rust/loki-rs/tests/backends.rs b/rust/loki-rs/tests/backends.rs index f88c412..66389a8 100644 --- a/rust/loki-rs/tests/backends.rs +++ b/rust/loki-rs/tests/backends.rs @@ -22,6 +22,7 @@ use std::time::Duration; use bytes::Bytes; use common::fakego; +use common::parity::{apply_files, tar_gz, tree, write_file}; use http_body_util::Full; use hyper::service::service_fn; use hyper::{Request, Response}; @@ -89,87 +90,6 @@ async fn fake_github(rel: Arc>) -> String { format!("http://{addr}") } -/// Une archive .tar.gz ; les noms sont écrits tels quels (« ../x » compris). -fn tar_gz(entries: &[Value]) -> Vec { - let gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); - let mut b = tar::Builder::new(gz); - for e in entries { - let mut h = tar::Header::new_ustar(); - let name = s(&e["name"]).as_bytes(); - h.as_old_mut().name[..name.len()].copy_from_slice(name); - let link = s(&e["link"]).as_bytes(); - h.as_old_mut().linkname[..link.len()].copy_from_slice(link); - h.set_mtime(1_700_000_000); - h.set_mode(e["mode"].as_u64().unwrap_or(0) as u32); - let body = s(&e["body"]).as_bytes().to_vec(); - match s(&e["type"]) { - "dir" => h.set_entry_type(tar::EntryType::Directory), - "symlink" => { - h.set_entry_type(tar::EntryType::Symlink); - h.set_mode(0o777); - } - "hardlink" => h.set_entry_type(tar::EntryType::Link), - _ => { - h.set_entry_type(tar::EntryType::Regular); - h.set_size(body.len() as u64); - } - } - if s(&e["type"]) != "file" { - h.set_size(0); - } - h.set_cksum(); - let data: &[u8] = if s(&e["type"]) == "file" { &body } else { &[] }; - b.append(&h, data).unwrap(); - } - b.into_inner().unwrap().finish().unwrap() -} - -fn write_file(p: &Path, content: &str, mode: u32) { - use std::os::unix::fs::OpenOptionsExt; - let _ = std::fs::remove_file(p); - let mut f = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(mode) - .open(p) - .unwrap(); - std::io::Write::write_all(&mut f, content.as_bytes()).unwrap(); -} - -fn remove_all(p: &Path) { - match std::fs::symlink_metadata(p) { - Ok(m) if m.is_dir() => std::fs::remove_dir_all(p).unwrap(), - Ok(_) => std::fs::remove_file(p).unwrap(), - Err(_) => {} - } -} - -/// Les fichiers d'une étape (`parityApplyFiles`). -fn apply_files(home: &Path, files: &Value) { - for (n, v) in files.as_object().into_iter().flatten() { - let v = s(v); - let p = home.join(n.trim_end_matches('/')); - match v { - "-" => { - remove_all(&p); - continue; - } - "/" => { - std::fs::create_dir_all(&p).unwrap(); - continue; - } - _ => {} - } - std::fs::create_dir_all(p.parent().unwrap()).unwrap(); - if let Some(target) = v.strip_prefix("-> ") { - let _ = std::fs::remove_file(&p); - std::os::unix::fs::symlink(target, &p).unwrap(); - continue; - } - write_file(&p, v, if v.starts_with("#!") { 0o755 } else { 0o644 }); - } -} - /// Les opérations git d'une étape (`parityGit`). fn git_op(home: &Path, op: &str) { let f: Vec<&str> = op.splitn(4, ' ').collect(); @@ -240,54 +160,6 @@ fn set_machine(tools: &Path, machines: &Value, name: &str) { } } -/// `info.Mode().Perm().String()`. -fn perm_string(mode: u32) -> String { - let mut out = String::from("-"); - for shift in [6, 3, 0] { - let b = (mode >> shift) & 7; - out.push(if b & 4 != 0 { 'r' } else { '-' }); - out.push(if b & 2 != 0 { 'w' } else { '-' }); - out.push(if b & 1 != 0 { 'x' } else { '-' }); - } - out -} - -/// `parityTree`. -fn tree(home: &Path) -> Vec { - use std::os::unix::fs::PermissionsExt; - fn walk(home: &Path, p: &Path, out: &mut Vec) { - let Ok(m) = std::fs::symlink_metadata(p) else { - return; - }; - let rel = p.strip_prefix(home).unwrap().to_string_lossy().into_owned(); - if rel.contains("/.git/") || rel.ends_with("/.git") { - return; - } - if m.file_type().is_symlink() { - let t = std::fs::read_link(p).unwrap(); - out.push(format!("l {rel} -> {}", t.to_string_lossy())); - } else if m.is_dir() { - out.push(format!("d {rel}")); - for e in std::fs::read_dir(p).unwrap().flatten() { - walk(home, &e.path(), out); - } - } else { - let b = std::fs::read(p).unwrap(); - out.push(format!( - "f {rel} {} {}", - perm_string(m.permissions().mode() & 0o777), - String::from_utf8_lossy(&b) - )); - } - } - let mut out = Vec::new(); - for root in ["backends", "engine"] { - walk(home, &home.join(root), &mut out); - } - out.sort(); - out -} - struct Bench { facade: SocketAddr, go: Arc, diff --git a/rust/loki-rs/tests/common/fakego.rs b/rust/loki-rs/tests/common/fakego.rs index 16518e8..1385d0c 100644 --- a/rust/loki-rs/tests/common/fakego.rs +++ b/rust/loki-rs/tests/common/fakego.rs @@ -78,6 +78,9 @@ pub struct FakeGo { pub tune_busy: Mutex>, /// /internal/v1/preset/apply reçus : (chemin, mode). pub preset_applies: Mutex>, + /// /internal/v1/preset/apply écrit-il le preset dans la configuration + /// (`applyPresetFile` : clés « appareil » et BIN de la machine gardés) ? + pub apply_presets: Mutex, /// /internal/v1/chat/generating : le Go écrit une réponse. pub generating: Mutex, /// /internal/v1/service/prestart reçus. @@ -450,6 +453,21 @@ async fn handle(st: Arc, req: Request) -> Response if mode == "refus" || path.ends_with("refus.env") { return resp(500, r#"{"error":"application refusée"}"#); } + if *st.apply_presets.lock().unwrap() { + let text = std::fs::read_to_string(&path).unwrap_or_default(); + let mut next = loki_rs::presets::parse_env(&text); + let mut store = st.store.lock().unwrap(); + let cur = store.entry("config".into()).or_default(); + for k in loki_rs::presets::PRESERVED_KEYS { + if let Some(v) = cur.get(*k) { + next.insert(k.to_string(), String::from_utf8_lossy(v).into_owned()); + } + } + if let Some(bin) = cur.get("BIN").filter(|v| !v.is_empty()) { + next.insert("BIN".into(), String::from_utf8_lossy(bin).into_owned()); + } + *cur = next.into_iter().map(|(k, v)| (k, v.into_bytes())).collect(); + } st.preset_applies.lock().unwrap().push((path, mode)); resp(204, "") } @@ -642,6 +660,7 @@ pub struct Opts { pub mcp: Option>, pub update: bool, pub backends: Option>, + pub engineimg: Option>, } /// Comme [`start`], avec la conversation tenue par la façade @@ -744,6 +763,7 @@ pub async fn start_with( mcp: opts.mcp, update: opts.update, backends: opts.backends, + engineimg: opts.engineimg, }; tokio::spawn(loki_rs::proxy::serve(fl, cfg, async move { let _ = rx.await; diff --git a/rust/loki-rs/tests/common/mod.rs b/rust/loki-rs/tests/common/mod.rs index 919e867..4060910 100644 --- a/rust/loki-rs/tests/common/mod.rs +++ b/rust/loki-rs/tests/common/mod.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] pub mod fakego; +pub mod parity; pub mod toolsfx; use bytes::Bytes; diff --git a/rust/loki-rs/tests/common/parity.rs b/rust/loki-rs/tests/common/parity.rs new file mode 100644 index 0000000..9a53ae8 --- /dev/null +++ b/rust/loki-rs/tests/common/parity.rs @@ -0,0 +1,139 @@ +//! Aides des rejeux de parité qui posent des fichiers et lisent une +//! arborescence (`parityApplyFiles`, `parityTarGz`, `parityTree` du Go). + +use std::path::Path; + +use serde_json::Value; + +fn s(v: &Value) -> &str { + v.as_str().unwrap_or("") +} + +/// Une archive .tar.gz ; les noms sont écrits tels quels (« ../x » compris). +pub fn tar_gz(entries: &[Value]) -> Vec { + let gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + let mut b = tar::Builder::new(gz); + for e in entries { + let mut h = tar::Header::new_ustar(); + let name = s(&e["name"]).as_bytes(); + h.as_old_mut().name[..name.len()].copy_from_slice(name); + let link = s(&e["link"]).as_bytes(); + h.as_old_mut().linkname[..link.len()].copy_from_slice(link); + h.set_mtime(1_700_000_000); + h.set_mode(e["mode"].as_u64().unwrap_or(0) as u32); + let body = s(&e["body"]).as_bytes().to_vec(); + match s(&e["type"]) { + "dir" => h.set_entry_type(tar::EntryType::Directory), + "symlink" => { + h.set_entry_type(tar::EntryType::Symlink); + h.set_mode(0o777); + } + "hardlink" => h.set_entry_type(tar::EntryType::Link), + _ => { + h.set_entry_type(tar::EntryType::Regular); + h.set_size(body.len() as u64); + } + } + if s(&e["type"]) != "file" { + h.set_size(0); + } + h.set_cksum(); + let data: &[u8] = if s(&e["type"]) == "file" { &body } else { &[] }; + b.append(&h, data).unwrap(); + } + b.into_inner().unwrap().finish().unwrap() +} + +pub fn write_file(p: &Path, content: &str, mode: u32) { + use std::os::unix::fs::OpenOptionsExt; + let _ = std::fs::remove_file(p); + let mut f = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .mode(mode) + .open(p) + .unwrap(); + std::io::Write::write_all(&mut f, content.as_bytes()).unwrap(); +} + +pub fn remove_all(p: &Path) { + match std::fs::symlink_metadata(p) { + Ok(m) if m.is_dir() => std::fs::remove_dir_all(p).unwrap(), + Ok(_) => std::fs::remove_file(p).unwrap(), + Err(_) => {} + } +} + +/// Les fichiers d'une étape (`parityApplyFiles`). +pub fn apply_files(home: &Path, files: &Value) { + for (n, v) in files.as_object().into_iter().flatten() { + let v = s(v); + let p = home.join(n.trim_end_matches('/')); + match v { + "-" => { + remove_all(&p); + continue; + } + "/" => { + std::fs::create_dir_all(&p).unwrap(); + continue; + } + _ => {} + } + std::fs::create_dir_all(p.parent().unwrap()).unwrap(); + if let Some(target) = v.strip_prefix("-> ") { + let _ = std::fs::remove_file(&p); + std::os::unix::fs::symlink(target, &p).unwrap(); + continue; + } + write_file(&p, v, if v.starts_with("#!") { 0o755 } else { 0o644 }); + } +} + +/// `info.Mode().Perm().String()`. +pub fn perm_string(mode: u32) -> String { + let mut out = String::from("-"); + for shift in [6, 3, 0] { + let b = (mode >> shift) & 7; + out.push(if b & 4 != 0 { 'r' } else { '-' }); + out.push(if b & 2 != 0 { 'w' } else { '-' }); + out.push(if b & 1 != 0 { 'x' } else { '-' }); + } + out +} + +/// `parityTree`. +pub fn tree(home: &Path) -> Vec { + use std::os::unix::fs::PermissionsExt; + fn walk(home: &Path, p: &Path, out: &mut Vec) { + let Ok(m) = std::fs::symlink_metadata(p) else { + return; + }; + let rel = p.strip_prefix(home).unwrap().to_string_lossy().into_owned(); + if rel.contains("/.git/") || rel.ends_with("/.git") { + return; + } + if m.file_type().is_symlink() { + let t = std::fs::read_link(p).unwrap(); + out.push(format!("l {rel} -> {}", t.to_string_lossy())); + } else if m.is_dir() { + out.push(format!("d {rel}")); + for e in std::fs::read_dir(p).unwrap().flatten() { + walk(home, &e.path(), out); + } + } else { + let b = std::fs::read(p).unwrap(); + out.push(format!( + "f {rel} {} {}", + perm_string(m.permissions().mode() & 0o777), + String::from_utf8_lossy(&b) + )); + } + } + let mut out = Vec::new(); + for root in ["backends", "engine"] { + walk(home, &home.join(root), &mut out); + } + out.sort(); + out +} diff --git a/rust/loki-rs/tests/engineimg.rs b/rust/loki-rs/tests/engineimg.rs new file mode 100644 index 0000000..2fee06a --- /dev/null +++ b/rust/loki-rs/tests/engineimg.rs @@ -0,0 +1,607 @@ +//! Lot 24 : panneau « moteur de l'image » tenu par la façade +//! (`LOKI_ENGINE_IMAGE=rust`, avec `LOKI_BACKENDS=rust`). +//! +//! Le journal tiré du Go (`internal/loki/parity_engineimg_test.go` → +//! `tests/parity_engineimg/engineimg.json`) est rejoué par une façade, contre +//! un faux registre OCI (jeton, index multi-architecture, manifeste annoté, +//! couches .tar.gz) et un faux moteur local (`/health`, `/props`, +//! `/apply-template` : le `fakeTpl` du Go) pour la sonde de gabarit et la +//! vérification du rendu avant/après la bascule : statut, dernier build +//! publié, plan (dernière ou recommandée, risque de rendu du raisonnement), +//! mise à jour (couches, liens, essai à blanc, REASONING_PRESERVE=off dans le +//! preset actif ou la configuration, ménage, rendu comparé), bascules, +//! suppression, retour à la version précédente, et les échecs. + +mod common; + +use std::collections::BTreeMap; +use std::convert::Infallible; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use bytes::Bytes; +use common::fakego; +use common::parity::{apply_files, tar_gz, tree}; +use http_body_util::{BodyExt, Full}; +use hyper::service::service_fn; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; +use loki_rs::backends::Backends; +use loki_rs::backends::probe::Probe; +use loki_rs::chat::tplprobe::TplProbe; +use loki_rs::engineimg::{EngineImg, oci}; +use regex::Regex; +use serde_json::{Value, json}; +use tokio::net::TcpListener; + +fn fixture() -> Value { + serde_json::from_str(include_str!("parity_engineimg/engineimg.json")).expect("journal") +} + +fn s(v: &Value) -> &str { + v.as_str().unwrap_or("") +} + +const TUNE_BUSY: &str = "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la"; + +/// Sert un handler HTTP sur un port libre ; renvoie l'adresse. +async fn serve(h: F) -> SocketAddr +where + F: Fn(String, String, Option, Vec) -> (u16, Vec) + Send + Sync + 'static, +{ + let l = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = l.local_addr().unwrap(); + let h = Arc::new(h); + tokio::spawn(async move { + while let Ok((st, _)) = l.accept().await { + let h = h.clone(); + tokio::spawn(async move { + let svc = service_fn(move |req: Request| { + let h = h.clone(); + async move { + let method = req.method().to_string(); + let path = req.uri().path().to_string(); + let auth = req + .headers() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .map(str::to_string); + let body = req + .into_body() + .collect() + .await + .map(|b| b.to_bytes().to_vec()) + .unwrap_or_default(); + let (code, out) = h(method, path, auth, body); + let mut r = Response::new(Full::new(Bytes::from(out))); + *r.status_mut() = hyper::StatusCode::from_u16(code).unwrap(); + Ok::<_, Infallible>(r) + } + }); + let _ = hyper::server::conn::http1::Builder::new() + .serve_connection(TokioIo::new(st), svc) + .await; + }); + } + }); + addr +} + +/// Le faux registre : `{token, token_body, manifest, manifest_body, no_arch, +/// blob, version, fixed, layers}`. +async fn fake_registry(reg: Arc>) -> String { + let addr = serve(move |_, path, auth, _| { + let cur = reg.lock().unwrap().clone(); + let version = s(&cur["version"]).to_string(); + let mut blobs: BTreeMap> = BTreeMap::new(); + let mut layers = Vec::new(); + for (i, l) in cur["layers"].as_array().into_iter().flatten().enumerate() { + let data = tar_gz(l["entries"].as_array().map_or(&[][..], Vec::as_slice)); + let digest = format!("sha256:couche{i}"); + let size = l["size"].as_i64().unwrap_or(0); + let size = if size > 0 { size } else { data.len() as i64 }; + layers.push(json!({ + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": digest, "size": size, + })); + blobs.insert(digest, data); + } + let annotations = json!({ "org.opencontainers.image.version": version }); + let manifest = json!({ "schemaVersion": 2, "layers": layers, "annotations": annotations }); + let man_dig = "sha256:manifeste"; + let arch = if cur["no_arch"] == json!(true) { + "s390x" + } else { + oci::oci_arch(cfg!(target_arch = "aarch64")) + }; + let index = json!({ + "schemaVersion": 2, + "manifests": [ + { "digest": "sha256:autre", "platform": { "os": "linux", "architecture": "ppc64le" } }, + { "digest": man_dig, "platform": { "os": "linux", "architecture": arch } }, + ], + }); + let last = path.rsplit('/').next().unwrap_or("").to_string(); + if path == "/token" { + return match cur["token"].as_u64() { + Some(code) if code != 0 => (code as u16, Vec::new()), + _ if !s(&cur["token_body"]).is_empty() => { + (200, s(&cur["token_body"]).as_bytes().to_vec()) + } + _ => (200, br#"{"token":"jeton"}"#.to_vec()), + }; + } + if path.contains("/manifests/") { + if auth.as_deref() != Some("Bearer jeton") { + return (401, Vec::new()); + } + if let Some(code) = cur["manifest"].as_u64().filter(|c| *c != 0) { + return (code as u16, Vec::new()); + } + if !s(&cur["manifest_body"]).is_empty() { + return (200, s(&cur["manifest_body"]).as_bytes().to_vec()); + } + let fixed = cur["fixed"] + .as_array() + .into_iter() + .flatten() + .any(|f| s(f) == last); + return if last == man_dig { + (200, manifest.to_string().into_bytes()) + } else if last == "server-cuda" || fixed { + (200, index.to_string().into_bytes()) + } else { + (404, Vec::new()) + }; + } + if path.contains("/blobs/") { + if let Some(code) = cur["blob"].as_u64().filter(|c| *c != 0) { + return (code as u16, Vec::new()); + } + return match blobs.remove(&last) { + Some(d) => (200, d), + None => (404, Vec::new()), + }; + } + (404, Vec::new()) + }) + .await; + format!("http://{addr}") +} + +/// Le faux moteur local : le `fakeTpl` du Go (mode, gabarit, build), ou +/// arrêté (503 partout). +#[derive(Clone, Default)] +struct Local { + mode: String, + build: String, + tpl: String, + down: bool, + /// Statut imposé à `/apply-template` (0 : rendu). + apply: u16, +} + +/// `fakeTpl.render`. +fn render(mode: &str, body: &Value) -> (u16, Vec) { + let id_re = Regex::new("^[A-Za-z0-9]{9}$").unwrap(); + let empty = Vec::new(); + let msgs = body["messages"].as_array().unwrap_or(&empty); + let last_user = msgs.iter().rposition(|m| m["role"] == "user"); + let mut b = String::new(); + if let Some(tools) = body["tools"].as_array() { + b.push_str(&format!("\n", tools.len())); + } + for (i, m) in msgs.iter().enumerate() { + let role = s(&m["role"]); + b.push_str(&format!("<|im_start|>{role}\n")); + let r = s(&m["reasoning_content"]); + if !r.is_empty() + && role == "assistant" + && mode != "drop" + && (mode != "qwen3" || last_user.is_none_or(|u| i > u)) + { + b.push_str(&format!("{r}")); + } + b.push_str(s(&m["content"])); + for call in m["tool_calls"].as_array().into_iter().flatten() { + let id = s(&call["id"]); + if !id_re.is_match(id) || i + 1 >= msgs.len() || msgs[i + 1]["tool_call_id"] != id { + return ( + 500, + br#"{"error":{"code":500,"message":"Jinja Exception: tool call id invalide"}}"# + .to_vec(), + ); + } + b.push_str(&format!( + "{}", + s(&call["function"]["name"]) + )); + } + b.push_str("<|im_end|>\n"); + } + if body["add_generation_prompt"] == json!(true) || mode == "noagp" { + b.push_str("<|im_start|>assistant\n"); + } + (200, json!({ "prompt": b }).to_string().into_bytes()) +} + +async fn fake_engine(local: Arc>) -> u16 { + serve(move |_, path, _, body| { + let l = local.lock().unwrap().clone(); + if l.down { + return (503, Vec::new()); + } + match path.as_str() { + "/health" => (200, br#"{"status":"ok"}"#.to_vec()), + "/props" => { + let tpl = if l.tpl.is_empty() { &l.mode } else { &l.tpl }; + let build = if l.build.is_empty() { + "b7000-abc" + } else { + &l.build + }; + let v = json!({ + "build_info": build, + "model_path": "/models/Qwen3-8B-Q8_0.gguf", + "chat_template": tpl, + }); + (200, v.to_string().into_bytes()) + } + "/apply-template" if l.apply != 0 => ( + l.apply, + r#"{"error":{"code":500,"message":"Jinja Exception: gabarit cassé"}}"# + .as_bytes() + .to_vec(), + ), + "/apply-template" => { + let v: Value = serde_json::from_slice(&body).unwrap_or(Value::Null); + render(&l.mode, &v) + } + _ => (404, Vec::new()), + } + }) + .await + .port() +} + +struct Bench { + facade: SocketAddr, + go: Arc, + _stop: tokio::sync::oneshot::Sender<()>, + home: PathBuf, + b: Arc, + reg: Arc>, + reg_base: String, + local: Arc>, + port: u16, + _tmp: Vec, +} + +async fn bench() -> Bench { + bench_with(Duration::from_secs(10)).await +} + +async fn bench_with(render_wait: Duration) -> Bench { + let tmp = tempfile::tempdir().unwrap(); + let root = tempfile::tempdir().unwrap(); + let home = tmp.path().to_path_buf(); + let reg = Arc::new(Mutex::new(json!({}))); + let reg_base = fake_registry(reg.clone()).await; + let local = Arc::new(Mutex::new(Local::default())); + let port = fake_engine(local.clone()).await; + let b = Arc::new(Backends::with( + home.clone(), + home.join("image/llama-server") + .to_string_lossy() + .into_owned(), + Probe { + path: Some(vec![]), + root: root.path().to_path_buf(), + }, + String::new(), + )); + let mut probe = TplProbe::new(); + probe.auto = false; + probe.backoff = Duration::from_millis(1); + let mut e = EngineImg::new( + home.clone(), + oci::Registry::new(reg_base.clone(), cfg!(target_arch = "aarch64")), + true, + Arc::new(probe), + ); + e.render_poll = Duration::from_millis(1); + e.render_wait = render_wait; + e.env = Some(Default::default()); + let (facade, go, stop, _) = fakego::start_with( + home.clone(), + fakego::Opts { + backends: Some(b.clone()), + engineimg: Some(Arc::new(e)), + ..Default::default() + }, + ) + .await; + go.set("config", "PORT", &port.to_string()); + *go.apply_presets.lock().unwrap() = true; + Bench { + facade, + go, + _stop: stop, + home, + b, + reg, + reg_base, + local, + port, + _tmp: vec![tmp, root], + } +} + +impl Bench { + fn mask(&self, v: &str) -> String { + let ts = Regex::new(r#""(started_at|ended_at)":[1-9][0-9]*"#).unwrap(); + let at = Regex::new(r#""at":"[^"]*""#).unwrap(); + let v = v + .replace(&self.reg_base, "{{REG}}") + .replace(&oci::host_of(&self.reg_base), "{{REGHOST}}") + .replace(&self.home.to_string_lossy().into_owned(), "{{HOME}}"); + let v = ts.replace_all(&v, r#""$1":"{{T}}""#); + at.replace_all(&v, r#""at":"{{T}}""#).into_owned() + } + + fn unmask(&self, v: &str) -> String { + v.replace("{{HOME}}", &self.home.to_string_lossy()) + .replace("{{PORT}}", &self.port.to_string()) + } + + async fn call(&self, method: &str, path: &str, body: &str) -> (u16, Value) { + let (code, b) = fakego::send(self.facade, method, path, &[], body).await; + let out = serde_json::from_str(&self.mask(&String::from_utf8_lossy(&b))) + .unwrap_or_else(|_| panic!("{path} : corps illisible {b:?}")); + (code.as_u16(), out) + } + + fn set_local(&self, v: &Value) { + *self.local.lock().unwrap() = Local { + mode: s(&v["mode"]).into(), + build: s(&v["build"]).into(), + tpl: s(&v["template"]).into(), + down: v["down"] == json!(true), + apply: v["apply"].as_u64().unwrap_or(0) as u16, + }; + } + + async fn wait_job(&self) { + for _ in 0..1500 { + let (_, v) = self.call("GET", "/api/llamacpp/job", "").await; + if v["running"] != json!(true) { + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + panic!("le job ne finit pas"); + } + + async fn wait_render(&self) -> Value { + for _ in 0..1500 { + let (_, v) = self.call("GET", "/api/engine", "").await; + let c = &v["render_check"]; + if !c.is_null() && c["status"] != "pending" { + return c.clone(); + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + panic!("la vérification du rendu ne finit pas"); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn parite_du_panneau() { + let fx = fixture(); + if (fx["goarch"] == "arm64") != cfg!(target_arch = "aarch64") { + eprintln!("journal tiré sur une autre architecture : rejeu sauté"); + return; + } + let bn = bench().await; + for st in fx["steps"].as_array().unwrap() { + let name = s(&st["name"]); + let files: serde_json::Map = st["files"] + .as_object() + .into_iter() + .flatten() + .map(|(k, v)| (k.clone(), Value::String(bn.unmask(s(v))))) + .collect(); + apply_files(&bn.home, &Value::Object(files)); + for (k, v) in st["config"].as_object().into_iter().flatten() { + let v = bn.unmask(s(v)); + if v.is_empty() { + bn.go.del("config", k); + } else { + bn.go.set("config", k, &v); + } + } + if !st["registry"].is_null() { + *bn.reg.lock().unwrap() = st["registry"].clone(); + } + if !st["local"].is_null() { + bn.set_local(&st["local"]); + } + if let Some(e) = st["engine_env"].as_str() { + bn.b.set_engine_bin_env(&bn.unmask(e)); + } + *bn.go.tune_busy.lock().unwrap() = + (st["tune"].as_bool() == Some(true)).then(|| TUNE_BUSY.to_string()); + let (code, out) = bn + .call(s(&st["method"]), s(&st["path"]), &bn.unmask(s(&st["body"]))) + .await; + *bn.go.tune_busy.lock().unwrap() = None; + assert_eq!(code as i64, st["status"].as_i64().unwrap(), "{name}"); + assert_eq!(out, st["out"], "{name}"); + if st["wait"].as_bool() == Some(true) { + bn.wait_job().await; + let (_, job) = bn.call("GET", "/api/llamacpp/job?from=0", "").await; + assert_eq!(job, st["job"], "{name} : job"); + if !st["after_job"].is_null() { + bn.set_local(&st["after_job"]); + } + } + if st["wait_render"].as_bool() == Some(true) { + assert_eq!(bn.wait_render().await, st["render"], "{name} : rendu"); + } + let mut after = BTreeMap::new(); + for k in ["BIN", "REASONING_PRESERVE"] { + if let Some(v) = bn.go.get("config", k).filter(|v| !v.is_empty()) { + after.insert(format!("config/{k}"), Value::String(bn.mask(&v))); + } + } + if let Ok(b) = std::fs::read(bn.home.join("engine/previous-bin")) { + after.insert( + "previous-bin".to_string(), + Value::String(bn.mask(&String::from_utf8_lossy(&b))), + ); + } + assert_eq!( + Value::Object(after.into_iter().collect()), + st["after"], + "{name} : après" + ); + let t: Vec = tree(&bn.home) + .into_iter() + .map(|l| Value::String(bn.mask(&l))) + .collect(); + let t = if t.is_empty() { + Value::Null + } else { + Value::Array(t) + }; + assert_eq!(t, st["tree"], "{name} : arborescence"); + } +} + +/// Autour de la bascule d'une mise à jour, le moteur qui tournait est arrêté +/// puis relancé (le Go pilote le service) ; une bascule sans réseau le +/// redémarre s'il tourne. +#[tokio::test] +async fn service_arrete_puis_relance() { + let fx = fixture(); + let steps = fx["steps"].as_array().unwrap(); + let step = |n: &str| steps.iter().find(|s| s["name"] == n).unwrap().clone(); + let bn = bench().await; + let first = step("statut, moteur de l'image"); + let files: serde_json::Map = first["files"] + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.clone(), Value::String(bn.unmask(s(v))))) + .collect(); + apply_files(&bn.home, &Value::Object(files)); + *bn.reg.lock().unwrap() = first["registry"].clone(); + bn.set_local(&json!({ "down": true })); + *bn.go.service_active.lock().unwrap() = true; + + assert_eq!(bn.call("POST", "/api/engine/update", "{}").await.0, 200); + bn.wait_job().await; + let (_, job) = bn.call("GET", "/api/llamacpp/job", "").await; + let lines: Vec<&str> = job["lines"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + let bin = "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server"; + let at = lines.iter().position(|l| *l == bin).expect("BIN posé"); + assert_eq!(lines[at - 1], "▶ arrêt du moteur…"); + assert_eq!(lines[at + 1], "▶ redémarrage du moteur…"); + assert_eq!( + *bn.go.service_actions.lock().unwrap(), + vec![("stop".to_string(), true), ("start".to_string(), false)] + ); + + bn.go.service_actions.lock().unwrap().clear(); + let (c, out) = bn + .call("POST", "/api/engine/use", r#"{"tag":"image"}"#) + .await; + assert_eq!((c, out["ok"].clone()), (200, json!(true))); + assert_eq!( + *bn.go.service_actions.lock().unwrap(), + vec![("restart".to_string(), true)] + ); + + // Arrêté : la mise à jour ne le relance pas. + *bn.go.service_active.lock().unwrap() = false; + bn.go.service_actions.lock().unwrap().clear(); + let (c, _) = bn + .call( + "POST", + "/api/engine/update", + r#"{"tag":"server-cuda-b10900"}"#, + ) + .await; + assert_eq!(c, 200); + bn.wait_job().await; + assert_eq!( + *bn.go.service_actions.lock().unwrap(), + vec![("stop".to_string(), true)] + ); + let (_, job) = bn.call("GET", "/api/llamacpp/job", "").await; + assert!( + !job["lines"] + .as_array() + .unwrap() + .iter() + .any(|l| l.as_str().unwrap().contains("arrêt du moteur")), + "{job}" + ); +} + +/// Sans les backends dans la façade (pas de job), le panneau répond 503. +#[tokio::test] +async fn sans_backends() { + let tmp = tempfile::tempdir().unwrap(); + let e = EngineImg::new( + tmp.path().to_path_buf(), + oci::Registry::new("http://127.0.0.1:9".into(), false), + true, + Arc::new(TplProbe::new()), + ); + let (facade, _go, _stop, _) = fakego::start_with( + tmp.path().to_path_buf(), + fakego::Opts { + engineimg: Some(Arc::new(e)), + ..Default::default() + }, + ) + .await; + let (c, b) = fakego::send(facade, "GET", "/api/engine", &[], "").await; + assert_eq!(c.as_u16(), 503, "{b:?}"); +} + +/// Le nouveau moteur ne répond pas à temps (build inchangé) : relevé +/// « unknown », sans comparaison. +#[tokio::test] +async fn rendu_non_verifie_a_temps() { + let fx = fixture(); + let steps = fx["steps"].as_array().unwrap(); + let first = steps[0].clone(); + let bn = bench_with(Duration::from_millis(200)).await; + let files: serde_json::Map = first["files"] + .as_object() + .unwrap() + .iter() + .map(|(k, v)| (k.clone(), Value::String(bn.unmask(s(v))))) + .collect(); + apply_files(&bn.home, &Value::Object(files)); + *bn.reg.lock().unwrap() = first["registry"].clone(); + bn.set_local(&first["local"]); + assert_eq!(bn.call("POST", "/api/engine/update", "{}").await.0, 200); + bn.wait_job().await; + let c = bn.wait_render().await; + assert_eq!(c["status"], "unknown", "{c}"); + assert_eq!( + c["detail"], + "le nouveau moteur n'a pas répondu à temps : rendu non vérifié" + ); + assert_eq!(c["tag"], "server-cuda-b10900"); +} diff --git a/rust/loki-rs/tests/lot2.rs b/rust/loki-rs/tests/lot2.rs index 1b4ba15..7c9d6c6 100644 --- a/rust/loki-rs/tests/lot2.rs +++ b/rust/loki-rs/tests/lot2.rs @@ -107,6 +107,7 @@ async fn banc(cle: Option<&str>) -> Banc { mcp: None, update: false, backends: None, + engineimg: None, }; tokio::spawn(loki_rs::proxy::serve(l, cfg, async move { let _ = rx.await; diff --git a/rust/loki-rs/tests/parity_engineimg/engineimg.json b/rust/loki-rs/tests/parity_engineimg/engineimg.json new file mode 100644 index 0000000..58d97ec --- /dev/null +++ b/rust/loki-rs/tests/parity_engineimg/engineimg.json @@ -0,0 +1,4640 @@ +{ + "goarch": "amd64", + "steps": [ + { + "name": "statut, moteur de l'image", + "files": { + "image/libggml-cuda.so": "elf", + "image/llama-server": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 10450 (abc1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo 'usage';;\nesac\n" + }, + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/image/llama-server", + "build": 10450, + "commit": "abc1234", + "dir": "{{HOME}}/engine", + "image_bin": "{{HOME}}/image/llama-server", + "installed": null, + "job": { + "exists": false + }, + "notice": "moteur b10450 : les points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) y sont évincés trop tôt — jusqu'à ~8 k jetons recalculés à chaque reprise. Corrigé à partir de b10864.", + "previous": null, + "recommend": { + "current": 10450, + "gains": [ + "points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) gardés : plus de ~8 k jetons recalculés à chaque reprise (b10864)", + "MTP rapide (graphes CUDA) à partir de b11009", + "MTP de qwen4exp à partir de b11331" + ], + "min": 10864 + }, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "image", + "supported": true, + "variant": "server-cuda" + }, + "after": {}, + "tree": null + }, + { + "name": "dernière version publiée", + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "current": 10450, + "latest": 10900, + "ok": true, + "tag": "server-cuda-b10900", + "update": true, + "variant": "server-cuda" + }, + "after": {}, + "tree": null + }, + { + "name": "dernière version sans numéro", + "registry": { + "version": "", + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf x", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "current": 10450, + "latest": 0, + "ok": true, + "tag": "server-cuda", + "update": true, + "variant": "server-cuda" + }, + "after": {}, + "tree": null + }, + { + "name": "jeton illisible", + "registry": { + "token_body": "{\"token\":\"\"}", + "version": "b10900", + "layers": null + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "error": "jeton de lecture illisible", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "manifeste refusé", + "registry": { + "manifest": 403, + "version": "b10900", + "layers": null + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "error": "manifeste server-cuda : HTTP 403", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "manifeste illisible", + "registry": { + "manifest_body": "{\"mediaType\":", + "version": "b10900", + "layers": null + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "error": "manifeste server-cuda illisible : unexpected EOF", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "aucune image pour cette architecture", + "registry": { + "no_arch": true, + "version": "b10900", + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "error": "aucune image linux/amd64 pour server-cuda", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "jeton refusé", + "registry": { + "token": 500, + "version": "b10900", + "layers": null + }, + "method": "POST", + "path": "/api/engine/check", + "status": 200, + "out": { + "error": "{{REGHOST}}/token : HTTP 500", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan : dernière version", + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "yes", + "suggest": true, + "why": "le gabarit chargé retire la réflexion des tours passés ; à partir de b10763 le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel." + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan : version recommandée", + "method": "POST", + "path": "/api/engine/plan", + "body": "{\"recommended\":true}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "yes", + "suggest": true, + "why": "le gabarit chargé retire la réflexion des tours passés ; à partir de b10763 le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel." + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan recommandé, build trop ancien", + "registry": { + "version": "b10800", + "fixed": [ + "server-cuda-b10800" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10800", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{\"recommended\":true}", + "status": 200, + "out": { + "error": "dernier build publié pour server-cuda : b10800, antérieur à la version recommandée b10864", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan recommandé, tag figé absent", + "registry": { + "version": "b10900", + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{\"recommended\":true}", + "status": 200, + "out": { + "error": "server-cuda-b10900 annoncé mais introuvable sur le registre : version inconnue dans le registre : server-cuda-b10900", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan recommandé, sans numéro", + "registry": { + "version": "", + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf x", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{\"recommended\":true}", + "status": 200, + "out": { + "error": "le registre n'annonce pas le build du tag « server-cuda » : impossible de vérifier qu'il atteint b10864", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, dernière version sans numéro", + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 0, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "yes", + "suggest": true, + "why": "le gabarit chargé retire la réflexion des tours passés ; à partir de b10763 le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel." + }, + "tag": "server-cuda", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan en GET", + "method": "GET", + "path": "/api/engine/plan", + "status": 405, + "out": { + "error": "POST requis", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, moteur local arrêté", + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "", + "build": "", + "down": true + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "unknown", + "suggest": false, + "why": "moteur arrêté ou gabarit illisible : impossible de vérifier. À partir de b10763, un gabarit de type Qwen3.6 rend la réflexion des tours passés, vide puisque Loki ne la renvoie pas (sans REASONING_ECHO) ; REASONING_PRESERVE=off garde le rendu actuel. À l'inverse, sur un gabarit qui la garde déjà (Qwen3.8), off changerait le rendu." + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, REASONING_PRESERVE posée", + "config": { + "REASONING_PRESERVE": "on" + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "REASONING_PRESERVE=on est posée : le rendu ne dépend pas du défaut du moteur" + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": { + "config/REASONING_PRESERVE": "on" + }, + "tree": null + }, + { + "name": "plan, EXTRA_ARGS", + "config": { + "EXTRA_ARGS": "--reasoning-preserve", + "REASONING_PRESERVE": "" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "EXTRA_ARGS règle déjà preserve_reasoning" + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, gabarit sans les variables", + "config": { + "EXTRA_ARGS": "" + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "simple" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "le gabarit chargé ne lit pas preserve_thinking / clear_thinking : rendu inchangé" + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, gabarit qui garde tout", + "local": { + "mode": "keep", + "build": "b10451-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "le gabarit garde déjà la réflexion des tours passés : rendu inchangé (ne pas poser off)" + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, gabarit vide", + "local": { + "mode": "", + "build": "b10452-img" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "unknown", + "suggest": false, + "why": "moteur arrêté ou gabarit illisible : impossible de vérifier. À partir de b10763, un gabarit de type Qwen3.6 rend la réflexion des tours passés, vide puisque Loki ne la renvoie pas (sans REASONING_ECHO) ; REASONING_PRESERVE=off garde le rendu actuel. À l'inverse, sur un gabarit qui la garde déjà (Qwen3.8), off changerait le rendu." + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, build antérieur au défaut", + "registry": { + "version": "b10700", + "fixed": [ + "server-cuda-b10700" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10700", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10700, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "b10700 n'a pas encore preserve_reasoning par défaut (b10763)" + }, + "tag": "server-cuda-b10700", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "plan, preset externe", + "config": { + "EXTERNAL": "1" + }, + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10450, + "keeps": "", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "preset externe : son gabarit n'est pas rendu par ce moteur" + }, + "tag": "server-cuda-b10900", + "uptodate": false + }, + "after": {}, + "tree": null + }, + { + "name": "mise à jour pendant une optimisation", + "config": { + "EXTERNAL": "" + }, + "tune": true, + "method": "POST", + "path": "/api/engine/update", + "body": "{}", + "status": 409, + "out": { + "error": "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la", + "ok": false + }, + "after": {}, + "tree": null + }, + { + "name": "mise à jour, moteur local arrêté", + "registry": { + "version": "b10920", + "fixed": [ + "server-cuda-b10920" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10920", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "", + "build": "", + "down": true + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "▶ recherche de la dernière version publiée…", + "variante : server-cuda — version visée : server-cuda-b10920", + "▶ lecture du manifeste server-cuda-b10920…", + "image ggml-org/llama.cpp:server-cuda-b10920 — llama.cpp b10920, 4 couches", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10920/llama-server", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "[warn] rendu du raisonnement : moteur arrêté ou gabarit illisible : impossible de vérifier. À partir de b10763, un gabarit de type Qwen3.6 rend la réflexion des tours passés, vide puisque Loki ne la renvoie pas (sans REASONING_ECHO) ; REASONING_PRESERVE=off garde le rendu actuel. À l'inverse, sur un gabarit qui la garde déjà (Qwen3.8), off changerait le rendu.", + "BIN → {{HOME}}/engine/server-cuda-b10920/llama-server", + "✓ moteur mis à jour (server-cuda-b10920)" + ], + "new": "", + "next": 12, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10920)", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10920/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10920", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10920/libggml-base.so.0.20.0 -rw-r--r-- elf b10920", + "f engine/server-cuda-b10920/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10920/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10920/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10920/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10920/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10920/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "retour au moteur de l'image", + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\"image\"}", + "status": 200, + "out": { + "bin": "{{HOME}}/image/llama-server", + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10920/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10920", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10920/llama-server\n", + "f engine/server-cuda-b10920/libggml-base.so.0.20.0 -rw-r--r-- elf b10920", + "f engine/server-cuda-b10920/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10920/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10920/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10920/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10920/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10920/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "suppression de la version d'essai", + "method": "POST", + "path": "/api/engine/remove", + "body": "{\"tag\":\"server-cuda-b10920\"}", + "status": 200, + "out": { + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10920/llama-server\n" + }, + "tree": [ + "d engine", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10920/llama-server\n" + ] + }, + { + "name": "mise à jour", + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{}", + "wait": true, + "after_job": { + "mode": "keep", + "build": "b10900-new", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "wait_render": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "▶ recherche de la dernière version publiée…", + "variante : server-cuda — version visée : server-cuda-b10900", + "▶ lecture du manifeste server-cuda-b10900…", + "image ggml-org/llama.cpp:server-cuda-b10900 — llama.cpp b10900, 4 couches", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10900/llama-server", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "[warn] rendu du raisonnement : le gabarit chargé retire la réflexion des tours passés ; à partir de b10763 le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel.", + "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server", + "vérification du rendu du gabarit une fois le modèle chargé (panneau Moteur, journal [moteur])", + "✓ moteur mis à jour (server-cuda-b10900)" + ], + "new": "", + "next": 13, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "render": { + "at": "{{T}}", + "detail": "historique avec raisonnement : avant « …stion-a<|im_end|>⏎<|im_start|>assistant⏎loki-sonde-reponse-c1<|im_end|>⏎<|im_start|>user⏎loki-sonde-… » / après « …stion-a<|im_end|>⏎<|im_start|>assistant⏎loki-sonde-raisonnement-r1loki-sonde-reponse-… »", + "status": "changed", + "tag": "server-cuda-b10900" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut après la mise à jour", + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "commit": "def5678", + "dir": "{{HOME}}/engine", + "image_bin": "{{HOME}}/image/llama-server", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": true, + "tag": "server-cuda-b10900" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": { + "bin": "{{HOME}}/image/llama-server", + "build": 10450, + "source": "image" + }, + "recommend": null, + "render_check": { + "at": "{{T}}", + "detail": "historique avec raisonnement : avant « …stion-a<|im_end|>⏎<|im_start|>assistant⏎loki-sonde-reponse-c1<|im_end|>⏎<|im_start|>user⏎loki-sonde-… » / après « …stion-a<|im_end|>⏎<|im_start|>assistant⏎loki-sonde-raisonnement-r1loki-sonde-reponse-… »", + "status": "changed", + "tag": "server-cuda-b10900" + }, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "downloaded", + "supported": true, + "variant": "server-cuda" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "plan, déjà à jour", + "method": "POST", + "path": "/api/engine/plan", + "body": "{}", + "status": 200, + "out": { + "build": 10900, + "current": 10900, + "keeps": "server-cuda-b10900", + "ok": true, + "preserve": { + "risk": "no", + "suggest": false, + "why": "le moteur courant (b10900) a déjà preserve_reasoning par défaut" + }, + "tag": "server-cuda-b10900", + "uptodate": true + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "déjà à jour", + "method": "POST", + "path": "/api/engine/update", + "body": "{}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "▶ recherche de la dernière version publiée…", + "✓ déjà à jour (build 10900)" + ], + "new": "", + "next": 2, + "old": "", + "phase": "déjà à jour (build 10900)", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "retour à la version précédente", + "method": "POST", + "path": "/api/engine/rollback", + "status": 200, + "out": { + "bin": "{{HOME}}/image/llama-server", + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut après le retour", + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/image/llama-server", + "build": 10450, + "commit": "abc1234", + "dir": "{{HOME}}/engine", + "image_bin": "{{HOME}}/image/llama-server", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": false, + "tag": "server-cuda-b10900" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "déjà à jour (build 10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "moteur b10450 : les points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) y sont évincés trop tôt — jusqu'à ~8 k jetons recalculés à chaque reprise. Corrigé à partir de b10864.", + "previous": { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "source": "downloaded", + "tag": "server-cuda-b10900" + }, + "recommend": { + "current": 10450, + "gains": [ + "points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) gardés : plus de ~8 k jetons recalculés à chaque reprise (b10864)", + "MTP rapide (graphes CUDA) à partir de b11009", + "MTP de qwen4exp à partir de b11331" + ], + "min": 10864 + }, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "image", + "supported": true, + "variant": "server-cuda" + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "retour en GET", + "method": "GET", + "path": "/api/engine/rollback", + "status": 405, + "out": { + "error": "POST requis", + "ok": false + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "retour pendant une optimisation", + "tune": true, + "method": "POST", + "path": "/api/engine/rollback", + "status": 409, + "out": { + "error": "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la", + "ok": false, + "tuning": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "bascule vers la version installée", + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\" server-cuda-b10900 \"}", + "status": 200, + "out": { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "bascule vers l'image", + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\"image\"}", + "status": 200, + "out": { + "bin": "{{HOME}}/image/llama-server", + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "bascule vers une version absente", + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\"server-cuda-b1\"}", + "status": 400, + "out": { + "error": "version non installée : server-cuda-b1", + "ok": false + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "bascule pendant une optimisation", + "tune": true, + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\"image\"}", + "status": 409, + "out": { + "error": "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la", + "ok": false, + "tuning": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "suppression d'une version", + "method": "POST", + "path": "/api/engine/remove", + "body": "{\"tag\":\"../server-cuda-b10900\"}", + "status": 200, + "out": { + "ok": true + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n" + ] + }, + { + "name": "suppression d'une version absente", + "method": "POST", + "path": "/api/engine/remove", + "body": "{\"tag\":\"server-cuda-b10900\"}", + "status": 400, + "out": { + "error": "version non installée : server-cuda-b10900", + "ok": false + }, + "after": { + "config/BIN": "{{HOME}}/image/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n" + ] + }, + { + "name": "mise à jour épinglée, REASONING_PRESERVE=off dans le preset actif", + "files": { + "presets/actif.env": "PORT={{PORT}}\nBIN={{HOME}}/image/llama-server\n" + }, + "local": { + "mode": "qwen3", + "build": "b10450-img", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10900\",\"preserve_off\":true}", + "wait": true, + "after_job": { + "mode": "qwen3", + "build": "b10900-new", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "wait_render": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10900", + "▶ lecture du manifeste server-cuda-b10900…", + "image ggml-org/llama.cpp:server-cuda-b10900 — llama.cpp b10900, 4 couches", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10900/llama-server", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "REASONING_PRESERVE=off posée dans le preset « actif »", + "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server", + "vérification du rendu du gabarit une fois le modèle chargé (panneau Moteur, journal [moteur])", + "✓ moteur mis à jour (server-cuda-b10900)" + ], + "new": "", + "next": 12, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "render": { + "at": "{{T}}", + "status": "same", + "tag": "server-cuda-b10900" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "config/REASONING_PRESERVE": "off", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "suppression du moteur utilisé", + "method": "POST", + "path": "/api/engine/remove", + "body": "{\"tag\":\"server-cuda-b10900\"}", + "status": 409, + "out": { + "error": "c'est le moteur utilisé — bascule d'abord sur une autre version", + "ok": false + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "config/REASONING_PRESERVE": "off", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "REASONING_PRESERVE déjà posée", + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10900\",\"preserve_off\":true}", + "wait": true, + "after_job": { + "mode": "drop", + "build": "b10900-bis", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "wait_render": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10900", + "déjà installé : server-cuda-b10900", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "REASONING_PRESERVE=off déjà posée : inchangée", + "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server", + "vérification du rendu du gabarit une fois le modèle chargé (panneau Moteur, journal [moteur])", + "✓ moteur mis à jour (server-cuda-b10900)" + ], + "new": "", + "next": 8, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "render": { + "at": "{{T}}", + "status": "same", + "tag": "server-cuda-b10900" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "config/REASONING_PRESERVE": "off", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "REASONING_PRESERVE=off posée dans la configuration", + "files": { + "presets/actif.env": "-" + }, + "config": { + "REASONING_PRESERVE": "" + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10900\",\"preserve_off\":true}", + "wait": true, + "after_job": { + "mode": "qwen3", + "build": "b10900-ter", + "template": "{% if preserve_thinking %}garde{% endif %}", + "apply": 500 + }, + "wait_render": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10900", + "déjà installé : server-cuda-b10900", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "REASONING_PRESERVE=off posée dans la configuration active (aucun preset ne lui correspond)", + "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server", + "vérification du rendu du gabarit une fois le modèle chargé (panneau Moteur, journal [moteur])", + "✓ moteur mis à jour (server-cuda-b10900)" + ], + "new": "", + "next": 8, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "render": { + "at": "{{T}}", + "detail": "rendu impossible sur le nouveau moteur : historique sans raisonnement : HTTP 500 : {\"error\":{\"code\":500,\"message\":\"Jinja Exception: gabarit cassé\"}}", + "status": "unknown", + "tag": "server-cuda-b10900" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "config/REASONING_PRESERVE": "off", + "previous-bin": "{{HOME}}/image/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10900", + "f engine/previous-bin -rw-r--r-- {{HOME}}/image/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "nouvelle version, ménage", + "files": { + "engine/server-cuda-b10000/llama-server": "#!/bin/sh\n", + "engine/server-cuda-b10001": "/", + "engine/server-cuda-b9999/llama-server": "#!/bin/sh\n" + }, + "config": { + "REASONING_PRESERVE": "" + }, + "registry": { + "version": "b10950", + "fixed": [ + "server-cuda-b10950" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10950", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "size": 10485760, + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "", + "build": "", + "down": true + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "▶ recherche de la dernière version publiée…", + "variante : server-cuda — version visée : server-cuda-b10950", + "▶ lecture du manifeste server-cuda-b10950…", + "image ggml-org/llama.cpp:server-cuda-b10950 — llama.cpp b10950, 4 couches", + "▶ téléchargement du moteur (10 Mo)…", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10950/llama-server", + "▶ essai du nouveau moteur…", + "moteur testé : build 10950 (0ab1234)", + "BIN → {{HOME}}/engine/server-cuda-b10950/llama-server", + "version précédente gardée pour revenir en arrière : server-cuda-b10900", + "ancienne version supprimée : server-cuda-b9999", + "ancienne version supprimée : server-cuda-b10000", + "✓ moteur mis à jour (server-cuda-b10950)" + ], + "new": "", + "next": 15, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10950)", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut, version précédente téléchargée", + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "commit": "0ab1234", + "dir": "{{HOME}}/engine", + "image_bin": "{{HOME}}/image/llama-server", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": false, + "tag": "server-cuda-b10900" + }, + { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "in_use": true, + "tag": "server-cuda-b10950" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10950)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "source": "downloaded", + "tag": "server-cuda-b10900" + }, + "recommend": null, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "downloaded", + "supported": true, + "variant": "server-cuda" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "REASONING_PRESERVE=off inconnue du moteur", + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10950\",\"preserve_off\":true}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "REASONING_PRESERVE=off demandée mais pas posée : le nouveau moteur ne connaît pas --no-reasoning-preserve — moteur courant inchangé ; le nouveau reste installé", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10950", + "déjà installé : server-cuda-b10950", + "▶ essai du nouveau moteur…", + "moteur testé : build 10950 (0ab1234)", + "✗ REASONING_PRESERVE=off demandée mais pas posée : le nouveau moteur ne connaît pas --no-reasoning-preserve — moteur courant inchangé ; le nouveau reste installé" + ], + "new": "", + "next": 5, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "version inconnue du registre", + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b1\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "version inconnue dans le registre : server-cuda-b1", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b1", + "▶ lecture du manifeste server-cuda-b1…", + "✗ version inconnue dans le registre : server-cuda-b1" + ], + "new": "", + "next": 3, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "image sans serveur", + "registry": { + "version": "b10960", + "fixed": [ + "server-cuda-b10960" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10960", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10960\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "llama-server absent de l'image server-cuda-b10960 — variante inattendue ?", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10960", + "▶ lecture du manifeste server-cuda-b10960…", + "image ggml-org/llama.cpp:server-cuda-b10960 — llama.cpp b10960, 4 couches", + "couche 3 : 1 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "couche 0 ignorée (2147 Mo) : c'est le runtime de l'image, pas le moteur", + "✗ llama-server absent de l'image server-cuda-b10960 — variante inattendue ?" + ], + "new": "", + "next": 7, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "manifeste sans couche", + "registry": { + "version": "b10966", + "fixed": [ + "server-cuda-b10966" + ], + "layers": null + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10966\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "manifeste server-cuda-b10966 sans couche", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10966", + "▶ lecture du manifeste server-cuda-b10966…", + "✗ manifeste server-cuda-b10966 sans couche" + ], + "new": "", + "next": 3, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "image sans version annoncée", + "registry": { + "version": "", + "fixed": [ + "server-cuda-b10967" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10967", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10967\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "llama-server absent de l'image server-cuda-b10967 — variante inattendue ?", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10967", + "▶ lecture du manifeste server-cuda-b10967…", + "couche 3 : 1 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "couche 0 ignorée (2147 Mo) : c'est le runtime de l'image, pas le moteur", + "✗ llama-server absent de l'image server-cuda-b10967 — variante inattendue ?" + ], + "new": "", + "next": 6, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "couche du runtime au sommet", + "registry": { + "version": "b10961", + "fixed": [ + "server-cuda-b10961" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10961", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "size": 734003200, + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10961\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "llama-server absent de l'image server-cuda-b10961 — variante inattendue ?", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10961", + "▶ lecture du manifeste server-cuda-b10961…", + "image ggml-org/llama.cpp:server-cuda-b10961 — llama.cpp b10961, 4 couches", + "couche 3 ignorée (734 Mo) : c'est le runtime de l'image, pas le moteur", + "✗ llama-server absent de l'image server-cuda-b10961 — variante inattendue ?" + ], + "new": "", + "next": 5, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "moteur qui ne démarre pas", + "registry": { + "version": "b10962", + "fixed": [ + "server-cuda-b10962" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10962", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 10962 (aaa)';;\n--list-devices) i=0; while [ $i -lt 13 ]; do echo \"trace $i\"; i=$((i+1)); done; echo 'erreur: libcudart.so.13 introuvable'; exit 1;;\n--help) echo '';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10962\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "le moteur ne démarre pas ici : exit status 1\ntrace 2\ntrace 3\ntrace 4\ntrace 5\ntrace 6\ntrace 7\ntrace 8\ntrace 9\ntrace 10\ntrace 11\ntrace 12\nerreur: libcudart.so.13 introuvable\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10962", + "▶ lecture du manifeste server-cuda-b10962…", + "image ggml-org/llama.cpp:server-cuda-b10962 — llama.cpp b10962, 4 couches", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10962/llama-server", + "▶ essai du nouveau moteur…", + "✗ le moteur ne démarre pas ici : exit status 1\ntrace 2\ntrace 3\ntrace 4\ntrace 5\ntrace 6\ntrace 7\ntrace 8\ntrace 9\ntrace 10\ntrace 11\ntrace 12\nerreur: libcudart.so.13 introuvable\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur" + ], + "new": "", + "next": 8, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "moteur qui ne voit aucune carte", + "registry": { + "version": "b10963", + "fixed": [ + "server-cuda-b10963" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10963", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 10963 (bbb)';;\n--list-devices) echo 'Available devices:';;\n--help) echo '';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10963\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "le moteur téléchargé ne voit aucune carte graphique, alors que le moteur actuel en voit 1 — il tournerait sur le processeur, beaucoup plus lentement\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10963", + "▶ lecture du manifeste server-cuda-b10963…", + "image ggml-org/llama.cpp:server-cuda-b10963 — llama.cpp b10963, 4 couches", + "couche 3 : 2 fichiers du moteur", + "couche 2 : 5 fichiers du moteur", + "moteur installé : {{HOME}}/engine/server-cuda-b10963/llama-server", + "▶ essai du nouveau moteur…", + "✗ le moteur téléchargé ne voit aucune carte graphique, alors que le moteur actuel en voit 1 — il tournerait sur le processeur, beaucoup plus lentement\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur" + ], + "new": "", + "next": 8, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "jeton refusé pendant la mise à jour", + "registry": { + "token": 401, + "version": "b10964", + "layers": null + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10964\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "{{REGHOST}}/token : HTTP 401", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10964", + "▶ lecture du manifeste server-cuda-b10964…", + "✗ {{REGHOST}}/token : HTTP 401" + ], + "new": "", + "next": 3, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "couche introuvable", + "registry": { + "blob": 404, + "version": "b10965", + "fixed": [ + "server-cuda-b10965" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10965", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\"server-cuda-b10965\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "téléchargement de la couche 3 : HTTP 404", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : server-cuda-b10965", + "▶ lecture du manifeste server-cuda-b10965…", + "image ggml-org/llama.cpp:server-cuda-b10965 — llama.cpp b10965, 4 couches", + "✗ téléchargement de la couche 3 : HTTP 404" + ], + "new": "", + "next": 4, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "tag illisible", + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":\".\"}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "version du moteur non précisée", + "exists": true, + "lines": [ + "variante : server-cuda — version visée : .", + "✗ version du moteur non précisée" + ], + "new": "", + "next": 2, + "old": "", + "phase": "échec", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "corps illisible, preset externe", + "config": { + "EXTERNAL": "1" + }, + "registry": { + "version": "b10900", + "fixed": [ + "server-cuda-b10900" + ], + "layers": [ + { + "size": 2147483648, + "entries": [ + { + "name": "usr/local/cuda/lib64/libcudart.so", + "type": "file", + "body": "runtime", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "etc/hosts", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama-server", + "type": "file", + "body": "ancien", + "mode": 493 + }, + { + "name": "app/", + "type": "dir", + "mode": 493 + }, + { + "name": "app/libggml-base.so.0.20.0", + "type": "file", + "body": "elf b10900", + "mode": 420 + }, + { + "name": "app/libggml-base.so", + "type": "symlink", + "link": "libggml-base.so.0.20.0" + }, + { + "name": "app/libllama.so.0.1.0", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/libllama.so", + "type": "symlink", + "link": "/app/libllama.so.0.1.0" + }, + { + "name": "app/libggml-cuda.so", + "type": "file", + "body": "elf", + "mode": 420 + }, + { + "name": "app/sous/ignoré", + "type": "file", + "body": "x", + "mode": 420 + }, + { + "name": "app/.wh.supprimé", + "type": "file", + "mode": 420 + }, + { + "name": "etc/passwd", + "type": "file", + "body": "x", + "mode": 420 + } + ] + }, + { + "entries": [ + { + "name": "app/llama", + "type": "file", + "body": "#!/bin/true\n", + "mode": 493 + }, + { + "name": "app/llama-server", + "type": "file", + "body": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "mode": 448 + } + ] + } + ] + }, + "local": { + "mode": "qwen3", + "build": "b10900-ext", + "template": "{% if preserve_thinking %}garde{% endif %}" + }, + "method": "POST", + "path": "/api/engine/update", + "body": "{\"tag\":1}", + "wait": true, + "status": 200, + "out": { + "ok": true + }, + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "lines": [ + "▶ recherche de la dernière version publiée…", + "variante : server-cuda — version visée : server-cuda-b10900", + "déjà installé : server-cuda-b10900", + "▶ essai du nouveau moteur…", + "moteur testé : build 10900 (def5678)", + "BIN → {{HOME}}/engine/server-cuda-b10900/llama-server", + "version précédente gardée pour revenir en arrière : server-cuda-b10950", + "✓ moteur mis à jour (server-cuda-b10900)" + ], + "new": "", + "next": 8, + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10950/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10950/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut, moteur compilé", + "files": { + "custom/llama-server": "#!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 10800 (c0ffee1)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n" + }, + "config": { + "BIN": "{{HOME}}/custom/llama-server" + }, + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/custom/llama-server", + "build": 10800, + "commit": "c0ffee1", + "dir": "{{HOME}}/engine", + "image_bin": "{{HOME}}/image/llama-server", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": false, + "tag": "server-cuda-b10900" + }, + { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "in_use": false, + "tag": "server-cuda-b10950" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "source": "downloaded", + "tag": "server-cuda-b10950" + }, + "recommend": null, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "custom", + "supported": true, + "variant": "server" + }, + "after": { + "config/BIN": "{{HOME}}/custom/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10950/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10950/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "bascule vers l'image, sans moteur d'image", + "engine_env": "", + "method": "POST", + "path": "/api/engine/use", + "body": "{\"tag\":\"image\"}", + "status": 400, + "out": { + "error": "cette installation n'a pas de moteur d'image (LOKI_ENGINE_BIN)", + "ok": false + }, + "after": { + "config/BIN": "{{HOME}}/custom/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10950/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10950/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut final", + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/custom/llama-server", + "build": 10800, + "commit": "c0ffee1", + "dir": "{{HOME}}/engine", + "image_bin": "", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": false, + "tag": "server-cuda-b10900" + }, + { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "in_use": false, + "tag": "server-cuda-b10950" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "source": "downloaded", + "tag": "server-cuda-b10950" + }, + "recommend": null, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "custom", + "supported": true, + "variant": "server" + }, + "after": { + "config/BIN": "{{HOME}}/custom/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10950/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10950/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut sans aucun moteur", + "config": { + "BIN": "" + }, + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "", + "build": 0, + "commit": "", + "dir": "{{HOME}}/engine", + "image_bin": "", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": false, + "tag": "server-cuda-b10900" + }, + { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "in_use": false, + "tag": "server-cuda-b10950" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "source": "downloaded", + "tag": "server-cuda-b10950" + }, + "recommend": null, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "custom", + "supported": true, + "variant": "server" + }, + "after": { + "previous-bin": "{{HOME}}/engine/server-cuda-b10950/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10950/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + }, + { + "name": "statut, version précédente = moteur courant", + "files": { + "engine/previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "config": { + "BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server" + }, + "method": "GET", + "path": "/api/engine", + "status": 200, + "out": { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "commit": "def5678", + "dir": "{{HOME}}/engine", + "image_bin": "", + "installed": [ + { + "bin": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "build": 10900, + "in_use": true, + "tag": "server-cuda-b10900" + }, + { + "bin": "{{HOME}}/engine/server-cuda-b10950/llama-server", + "build": 10950, + "in_use": false, + "tag": "server-cuda-b10950" + } + ], + "job": { + "action": "engine", + "compiled": 0, + "ended_at": "{{T}}", + "error": "", + "exists": true, + "new": "", + "old": "", + "phase": "moteur mis à jour (server-cuda-b10900)", + "running": false, + "started_at": "{{T}}" + }, + "notice": "", + "previous": null, + "recommend": null, + "render_check": null, + "repo": "{{REGHOST}}/ggml-org/llama.cpp", + "source": "downloaded", + "supported": true, + "variant": "server-cuda" + }, + "after": { + "config/BIN": "{{HOME}}/engine/server-cuda-b10900/llama-server", + "previous-bin": "{{HOME}}/engine/server-cuda-b10900/llama-server\n" + }, + "tree": [ + "d engine", + "d engine/server-cuda-b10001", + "d engine/server-cuda-b10900", + "d engine/server-cuda-b10950", + "f engine/previous-bin -rw-r--r-- {{HOME}}/engine/server-cuda-b10900/llama-server\n", + "f engine/server-cuda-b10900/libggml-base.so.0.20.0 -rw-r--r-- elf b10900", + "f engine/server-cuda-b10900/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10900/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10900/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10900/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10900, commit def5678)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '--no-reasoning-preserve';;\nesac\n", + "f engine/server-cuda-b10950/libggml-base.so.0.20.0 -rw-r--r-- elf b10950", + "f engine/server-cuda-b10950/libggml-cuda.so -rw-r--r-- elf", + "f engine/server-cuda-b10950/libllama.so.0.1.0 -rw-r--r-- elf", + "f engine/server-cuda-b10950/llama -rwxr-xr-x #!/bin/true\n", + "f engine/server-cuda-b10950/llama-server -rwxr-xr-x #!/bin/sh\ncase \"$1\" in\n--version) echo 'version: 0.1.0-dev (build 10950, commit 0ab1234)';;\n--list-devices) echo 'Available devices:'; echo ' CUDA0: Carte (1000 MiB, 900 MiB free)';;\n--help) echo '';;\nesac\n", + "l engine/server-cuda-b10900/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10900/libllama.so -> libllama.so.0.1.0", + "l engine/server-cuda-b10950/libggml-base.so -> libggml-base.so.0.20.0", + "l engine/server-cuda-b10950/libllama.so -> libllama.so.0.1.0" + ] + } + ] +} diff --git a/rust/loki-rs/tests/proxy.rs b/rust/loki-rs/tests/proxy.rs index 8fdda9d..bf94372 100644 --- a/rust/loki-rs/tests/proxy.rs +++ b/rust/loki-rs/tests/proxy.rs @@ -113,6 +113,7 @@ async fn facade(backend: SocketAddr) -> (SocketAddr, oneshot::Sender<()>) { mcp: None, update: false, backends: None, + engineimg: None, }; tokio::spawn(loki_rs::proxy::serve(l, cfg, async move { let _ = rx.await;