From 8c9bb6b450a0212d500b55b61e598e0fe85ccfa1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 22:15:30 +0000 Subject: [PATCH] =?UTF-8?q?Lot=2024=20:=20moteur=20de=20l'image=20tenu=20p?= =?UTF-8?q?ar=20la=20fa=C3=A7ade=20(LOKI=5FENGINE=5FIMAGE=3Drust)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port de web_engine.go, web_engine_reco.go et backend_engine_oci.go (rust/loki-rs/src/engineimg) : statut du moteur (build, source, variante, versions installées, avis, version recommandée, version précédente, relevé du rendu), dernier build publié, plan (dernière ou recommandée, risque de rendu du raisonnement au défaut de b10763), mise à jour depuis le registre officiel (couches du sommet, liens, runtime ignoré, essai à blanc, REASONING_PRESERVE=off dans le preset actif ou la configuration, ménage, vérification du rendu avant/après par la sonde de gabarit), bascules, suppression et retour à la version précédente. Tenu seulement avec LOKI_BACKENDS=rust : la mise à jour est un job des moteurs. La sonde est celle de la conversation quand la façade la tient. /api/engine/cmdline reste au Go, qui relaie les routes du tunnel. Parité : parity_engineimg_test.go tire du Go un journal (faux registre OCI, faux moteur local pour la sonde) rejoué par tests/engineimg.rs ; TestFacadeMoteurImage côté Go. Les aides de fichiers des rejeux passent dans tests/common/parity.rs. Mutations : 71 tuées sur 74, trois équivalentes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv --- README.md | 1 + ...ust-migration-lot24-moteur-image-design.md | 130 + internal/loki/facade_chat.go | 5 +- internal/loki/facade_chat_test.go | 63 + internal/loki/facade_engineimg.go | 34 + internal/loki/parity_engineimg_test.go | 544 ++ rust/loki-rs/src/api.rs | 4 + rust/loki-rs/src/backends/devices.rs | 35 +- rust/loki-rs/src/backends/mod.rs | 60 +- rust/loki-rs/src/backends/prebuilt.rs | 21 +- rust/loki-rs/src/engineimg/mod.rs | 1201 +++++ rust/loki-rs/src/engineimg/oci.rs | 631 +++ rust/loki-rs/src/lib.rs | 1 + rust/loki-rs/src/main.rs | 31 + rust/loki-rs/src/paths.rs | 16 + rust/loki-rs/src/presets_api.rs | 2 +- rust/loki-rs/src/proxy.rs | 10 + rust/loki-rs/tests/backends.rs | 130 +- rust/loki-rs/tests/common/fakego.rs | 20 + rust/loki-rs/tests/common/mod.rs | 1 + rust/loki-rs/tests/common/parity.rs | 139 + rust/loki-rs/tests/engineimg.rs | 607 +++ rust/loki-rs/tests/lot2.rs | 1 + .../tests/parity_engineimg/engineimg.json | 4640 +++++++++++++++++ rust/loki-rs/tests/proxy.rs | 1 + 25 files changed, 8155 insertions(+), 173 deletions(-) create mode 100644 docs/superpowers/specs/2026-10-15-rust-migration-lot24-moteur-image-design.md create mode 100644 internal/loki/facade_engineimg.go create mode 100644 internal/loki/parity_engineimg_test.go create mode 100644 rust/loki-rs/src/engineimg/mod.rs create mode 100644 rust/loki-rs/src/engineimg/oci.rs create mode 100644 rust/loki-rs/tests/common/parity.rs create mode 100644 rust/loki-rs/tests/engineimg.rs create mode 100644 rust/loki-rs/tests/parity_engineimg/engineimg.json 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;