Moteur Strata : un second moteur, choisi par preset (ENGINE=strata)

Strata (github.com/Niko1221/Strata, MIT) fait tourner Qwen3.8 Flash Next
(MoE 125 B) sur une carte de joueur en répartissant les experts entre VRAM,
RAM et disque. Intégration reprise d'AJEAN (backend_moe.go, « AJEAN MoE
1.0 ») : paquet figé de la release moe-v1.0, vérifié par SHA-256.

- backend_strata.go : détection de la machine, quant conseillé, installation
  en tâche (même suivi que llama.cpp), preset ENGINE=strata, lancement du
  serveur de Strata par `loki serve`, réglages d'un modèle installé
- pré-vol, vision, liste des presets, journal de chargement : Strata reconnu
- proxy OpenAI et relais : Host local pour Strata (il refuse un Host inconnu)
- benchmark et optimiseur refusés sur Strata (protocole propre à llama-server)
- UI : ligne Strata dans Paramètres → Moteur (visible aussi avec le moteur de
  l'image), fenêtre d'installation et de réglages, moteur actif affiché
- image : python3 + python3-venv ; compose : memlock illimité

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-07 09:03:22 +02:00
1 parent 5dd14c9de5
commit 4a9a5a1712
24 files changed
+2310 -13

No files matched your search

+5 -1
View File
@@ -87,8 +87,12 @@ LABEL org.opencontainers.image.revision="${LOKI_VERSION}" \
# via npx ; tini en PID 1. curl et libgomp1 sont déjà dans l'image amont. # via npx ; tini en PID 1. curl et libgomp1 sont déjà dans l'image amont.
# Node 22 depuis NodeSource et non depuis Ubuntu (qui livre Node 18) : Playwright # Node 22 depuis NodeSource et non depuis Ubuntu (qui livre Node 18) : Playwright
# et la plupart des serveurs MCP réclament Node ≥ 20. # et la plupart des serveurs MCP réclament Node ≥ 20.
# python3 + python3-venv : l'installeur et le serveur du moteur Strata
# (backend_strata.go) sont en Python ≥ 3.10 et créent leur propre .venv dans
# /data/strata. Le moteur et le modèle ne sont PAS dans l'image : téléchargés
# à la demande depuis Paramètres → Moteur.
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
git tini ca-certificates curl gnupg \ git tini ca-certificates curl gnupg python3 python3-venv \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
+10
View File
@@ -52,6 +52,16 @@ et le **chiffrement de la mémoire** (`mem_crypto.go`, `mem_vault.go`,
**pas** reprise : elle vise ajean.link, le service de l'auteur amont. Loki **pas** reprise : elle vise ajean.link, le service de l'auteur amont. Loki
expose à la place un export/import de paquet chiffré en fichier local. expose à la place un export/import de paquet chiffré en fichier local.
## Moteur Strata
Le second moteur de Loki (`internal/loki/backend_strata.go`) est
**[Strata](https://github.com/Niko1221/Strata)** (MIT), spécialisé dans
Qwen3.8-Flash-Next. Son intégration est reprise d'AJEAN (« AJEAN MoE 1.0 »,
`backend_moe.go`) : Loki télécharge le paquet figé publié par AJEAN sur sa
release `moe-v1.0` (sources de Strata et binaire Linux CUDA 12 compilé, avec
les modifications d'AJEAN), vérifié par SHA-256. Ni Strata ni le modèle ne sont
inclus dans l'image : ils sont téléchargés à la demande.
## Services externes ## Services externes
Le tunnel d'accès distant continue de pointer vers **ajean.link**, le relais Le tunnel d'accès distant continue de pointer vers **ajean.link**, le relais
+27
View File
@@ -93,6 +93,33 @@ la recherche qu'au téléchargement. À défaut, la variable d'environnement
priorité. Un jeton en **lecture** suffit (huggingface.co/settings/tokens), et il priorité. Un jeton en **lecture** suffit (huggingface.co/settings/tokens), et il
n'est envoyé qu'aux adresses Hugging Face. n'est envoyé qu'aux adresses Hugging Face.
## Choisir son moteur : llama.cpp ou Strata
Loki a deux moteurs, et le choix se fait **par preset** (clé `ENGINE`) :
- **llama.cpp** (par défaut) — n'importe quel GGUF, réglages complets dans
l'éditeur de preset.
- **[Strata](https://github.com/Niko1221/Strata)** — spécialisé dans
**Qwen3.8 Flash Next** (MoE de 125 B) : les experts sont répartis entre
VRAM, RAM et disque pour faire tourner ce modèle sur une carte de joueur.
Intégration reprise d'AJEAN (paquet figé « AJEAN MoE 1.0 », vérifié par
SHA-256).
**Paramètres → Moteur → Strata** : la fenêtre lit la machine (cartes, RAM,
disque), propose la version (Classique ou Swift 1.5) et la qualité qui tient,
puis installe et active le modèle en un clic (téléchargement de 66 à 84 Go dans
`/data/strata`). Le modèle installé devient un preset comme les autres :
basculer dessus lance Strata, basculer sur un autre preset revient à llama.cpp.
Son engrenage ouvre les réglages propres à Strata (contexte jusqu'à 262 144,
cache KV fp16/8 bits, prédiction MTP, lecture des images, carte d'appoint).
Pré-requis : Linux + NVIDIA RTX 20 ou plus récente (8 Go de VRAM minimum, une
deuxième carte est utilisée comme carte d'appoint), environ 46 Go de RAM pour
la plus petite qualité, pilote compatible CUDA 12.8. Le conteneur doit pouvoir
verrouiller la mémoire : `ulimits: memlock: -1` (déjà présent dans les
fichiers compose fournis). Le benchmark et l'optimiseur ne s'appliquent qu'à
llama.cpp.
## Installation sur Unraid ## Installation sur Unraid
L'image est construite et publiée par GitHub Actions sur GHCR L'image est construite et publiée par GitHub Actions sur GHCR
+4
View File
@@ -64,4 +64,8 @@ services:
- /mnt/user/appdata/loki/data:/data - /mnt/user/appdata/loki/data:/data
# /models : dossier de .gguf déposés à la main (gros fichiers) # /models : dossier de .gguf déposés à la main (gros fichiers)
- /mnt/user/appdata/loki/models:/models - /mnt/user/appdata/loki/models:/models
# Moteur Strata (Qwen3.8 Flash Next) : il verrouille ses experts en RAM
# (mlock, 30 à 50 Go). La limite Docker par défaut (64 Ko) l'en empêche.
ulimits:
memlock: -1
restart: unless-stopped restart: unless-stopped
+4
View File
@@ -37,4 +37,8 @@ services:
- driver: nvidia - driver: nvidia
count: all count: all
capabilities: ["gpu"] capabilities: ["gpu"]
# Moteur Strata (Qwen3.8 Flash Next) : il verrouille ses experts en RAM
# (mlock, 30 à 50 Go). La limite Docker par défaut (64 Ko) l'en empêche.
ulimits:
memlock: -1
restart: unless-stopped restart: unless-stopped
+8
View File
@@ -272,6 +272,14 @@ func cmdServe(args []string) error {
} }
cfg = c cfg = c
} }
// Preset ENGINE=strata (backend_strata.go) : un autre moteur, lancé par son
// propre serveur. L'optimiseur ne règle que llama-server.
if isStrataConfig(cfg) {
if trialDir != "" {
return fmt.Errorf("l'optimiseur ne s'applique pas à un preset Strata")
}
return serveStrata(cfg)
}
bin := cfg["BIN"] bin := cfg["BIN"]
if bin == "" { if bin == "" {
return fmt.Errorf("BIN non défini — lance « loki edit »") return fmt.Errorf("BIN non défini — lance « loki edit »")
File diff suppressed because it is too large. Load diff
+343
View File
@@ -0,0 +1,343 @@
package loki
import (
"os"
"path/filepath"
"strings"
"testing"
)
func strataArgs(t *testing.T, cfg map[string]any) []string {
t.Helper()
var out []string
for _, a := range cfg["args"].([]any) {
out = append(out, a.(string))
}
return out
}
func argAfter(args []string, flag string) string {
for i := 0; i+1 < len(args); i++ {
if args[i] == flag {
return args[i+1]
}
}
return ""
}
func TestStrataBuildConfigApplyTuning(t *testing.T) {
base := map[string]any{
"exe": "/x/engine-cuda12/strata",
"args": []any{"--pack", "/p", "--prefill", "8192", "--max-context", "131072"},
"port": 8080.0,
}
out, err := strataBuildConfig(base, map[string]string{"PORT": "8081", "CTX": "65536"}, "k")
if err != nil {
t.Fatal(err)
}
a := strataArgs(t, out)
for flag, want := range map[string]string{"--prefill": "16384", "--short-read": "150", "--prompt-cache-root": "256", "--max-context": "65536", "--pack": "/p"} {
if got := argAfter(a, flag); got != want {
t.Errorf("%s = %q, attendu %q (args %v)", flag, got, want, a)
}
}
if strings.Count(strings.Join(a, " "), "--prefill") != 1 {
t.Errorf("--prefill dupliqué : %v", a)
}
if strataHasArg(a, "--remote-expert-opt") || strataHasArg(a, "--expert-cache-device1") {
t.Errorf("aide 2e carte sans 2e carte : %v", a)
}
if out["port"] != 8081 || out["api_key"] != "k" || out["host"] != "127.0.0.1" {
t.Errorf("port/clé/hôte : %v %v %v", out["port"], out["api_key"], out["host"])
}
// la config de l'installeur n'est pas modifiée
if base["args"].([]any)[3] != "8192" {
t.Error("la config de base a été modifiée")
}
}
func TestStrataBuildConfigHelperGPU(t *testing.T) {
base := map[string]any{"args": []any{"--pack", "/p", "--mmap-experts", "--vram-reserve-mib", "700"}, "gpu": 1.0,
"vision": map[string]any{"exe": "v", "gpu": true}}
out, err := strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "0"}, "")
if err != nil {
t.Fatal(err)
}
a := strataArgs(t, out)
if argAfter(a, "--expert-cache-device1") != "auto" || !strataHasArg(a, "--remote-expert-opt") {
t.Errorf("aide 2e carte absente : %v", a)
}
if strataHasArg(a, "--vram-reserve-mib") || strataHasArg(a, "700") {
t.Errorf("réserve de VRAM gardée : %v", a)
}
if _, has := out["api_key"]; has {
t.Error("api_key posée sans clé")
}
if _, has := out["gpu"]; has {
t.Error("clé gpu gardée avec une carte d'aide (le serveur en ferait un --layer-split)")
}
if out["vision"].(map[string]any)["cuda_device"] != 0 {
t.Errorf("encodeur pas sur la carte d'aide : %v", out["vision"])
}
if out["env"].(map[string]any)["STRATA_PLE_BATCH"] != "0" {
t.Errorf("STRATA_PLE_BATCH absent : %v", out["env"])
}
}
func TestStrataDropFits(t *testing.T) {
q := strataQuants["IQ3_XXS"]
env := strataEnv{Main: 1, Helper: 0, RAMGB: 46.9, GPUs: []strataGPU{{Index: 0, VRAMGB: 7.8}, {Index: 1, VRAMGB: 15.9}}}
if !strataDropFits(q, env) {
t.Error("serveur de test (46 Go + 3070) : le mode hors RAM devrait tenir")
}
f := strataFitFor(q, env, 0)
if f.Mmap || !f.Drop {
t.Errorf("fit = mmap %v drop %v, attendu drop", f.Mmap, f.Drop)
}
env.Helper = -1
if strataDropFits(q, env) {
t.Error("sans carte d'aide, pas de mode hors RAM")
}
env.Helper, env.RAMGB = 0, 32
if strataDropFits(q, env) {
t.Error("32 Go : ne tient pas, doit rester en mmap")
}
}
func TestStrataBuildConfigDrop(t *testing.T) {
home := t.TempDir()
t.Setenv("LOKI_HOME", home)
pack := filepath.Join(home, "pack")
_ = os.MkdirAll(pack, 0o755)
base := map[string]any{"args": []any{"--pack", pack, "--pcie-frac", "0.3"}, "env": map[string]any{}}
cfg := map[string]string{"STRATA_HELPER_GPU": "0", "STRATA_DROP": "1", "STRATA_MMAP": "0"}
// sans experts.bin ni moteur à jour : repli mmap (ce lancement écrit experts.bin)
out, _ := strataBuildConfig(base, cfg, "")
if out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != "1" || out["env"].(map[string]any)["STRATA_REMOTE_DROP"] != nil {
t.Fatalf("repli mmap attendu : %v", out["env"])
}
_ = os.WriteFile(filepath.Join(pack, "experts.bin"), []byte("x"), 0o644)
_ = os.MkdirAll(filepath.Join(strataSrcDir(), "engine-cuda12"), 0o755)
_ = os.WriteFile(strataEngineMarker(), []byte(strataEngineAsset+"\n"), 0o644)
out, _ = strataBuildConfig(base, cfg, "")
e := out["env"].(map[string]any)
if e["STRATA_REMOTE_DROP"] != "1" || e["STRATA_ARENA_MMAP"] != nil {
t.Errorf("mode hors RAM attendu : %v", e)
}
if a := strataArgs(t, out); strataHasArg(a, "--pcie-frac") {
t.Errorf("--pcie-frac gardé (la part PCIe doit être automatique) : %v", a)
}
// carte d'aide coupée : pas de mode hors RAM
cfg["STRATA_HELPER"] = "0"
out, _ = strataBuildConfig(base, cfg, "")
if out["env"].(map[string]any)["STRATA_REMOTE_DROP"] != nil {
t.Error("mode hors RAM sans carte d'aide")
}
}
func TestStrataBuildConfigMmap(t *testing.T) {
base := map[string]any{"args": []any{"--pack", "/p", "--mmap-experts"}, "env": map[string]any{"X": "1"}}
out, err := strataBuildConfig(base, map[string]string{"STRATA_HELPER_GPU": "0"}, "")
if err != nil {
t.Fatal(err)
}
a := strataArgs(t, out)
env := out["env"].(map[string]any)
if strataHasArg(a, "--mmap-experts") || argAfter(a, "--pcie-frac") != "0" || env["STRATA_ARENA_MMAP"] != "1" || env["X"] != "1" {
t.Errorf("mmap validé non appliqué : %v %v", a, env)
}
}
func TestStrataCudaDevices(t *testing.T) {
cases := []struct {
main, helper, want string
}{
{"1", "0", "1,0"},
{"0", "-1", "0"},
{"0", "", "0"},
{"", "", ""},
}
for _, c := range cases {
got := strataCudaDevices(map[string]string{"STRATA_MAIN_GPU": c.main, "STRATA_HELPER_GPU": c.helper})
if got != c.want {
t.Errorf("main=%q helper=%q : %q, attendu %q", c.main, c.helper, got, c.want)
}
}
}
func TestStrataRecommend(t *testing.T) {
// la machine de test : 5060 Ti 16 Go + 3070 8 Go, 47 Go de RAM, disque large
env := strataEnv{Supported: true, Main: 1, Helper: 0, RAMGB: 47, DiskFreeGB: 400,
GPUs: []strataGPU{{Index: 0, VRAMGB: 8, Arch: 86}, {Index: 1, VRAMGB: 16, Arch: 120}}}
if got := strataRecommend("swift", env); got != "IQ3_XXS" {
t.Errorf("swift : %q, attendu IQ3_XXS", got)
}
// IQ3_S demande de garder 50 Go d'experts en RAM : pas sur 47 Go
if got := strataRecommend("qwen", env); got != "IQ3_XXS" {
t.Errorf("qwen 47 Go : %q, attendu IQ3_XXS", got)
}
f := strataFitFor(strataQuants["IQ3_XXS"], env, 0)
// trop juste pour tout garder en RAM, mais la 3070 garde les siens : mode hors RAM
if !f.OK || f.Mmap || !f.Drop {
t.Errorf("IQ3_XXS sur 47 Go : ok=%v mmap=%v drop=%v (attendu ok, hors RAM)", f.OK, f.Mmap, f.Drop)
}
// 64 Go : IQ3_S tient en RAM
env.RAMGB = 64
if got := strataRecommend("qwen", env); got != "IQ3_S" {
t.Errorf("qwen 64 Go : %q, attendu IQ3_S", got)
}
// 16 Go de RAM et une seule carte de 8 Go : rien ne tient
small := strataEnv{Supported: true, Main: 0, Helper: -1, RAMGB: 16, DiskFreeGB: 400,
GPUs: []strataGPU{{Index: 0, VRAMGB: 8, Arch: 86}}}
if got := strataRecommend("swift", small); got != "" {
t.Errorf("petite machine : %q, attendu aucune recommandation", got)
}
// disque trop petit
env.DiskFreeGB = 50
if f := strataFitFor(strataQuants["Q2_0"], env, 0); f.OK {
t.Error("Q2_0 accepté avec 50 Go de disque")
}
}
func TestStrataPresetName(t *testing.T) {
if got := strataPresetName("swift", "IQ3_XXS"); got != "STRATA FLASH NEXT SWIFT 1.5 IQ3_XXS" {
t.Errorf("%q", got)
}
if got := strataPresetName("qwen", "Q2_0"); got != "STRATA FLASH NEXT CLASSIQUE Q2_0" {
t.Errorf("%q", got)
}
}
func TestStrataBuildConfigMmapFromPreset(t *testing.T) {
base := map[string]any{"args": []any{"--pack", "/p"}}
out, err := strataBuildConfig(base, map[string]string{"STRATA_MMAP": "1"}, "")
if err != nil {
t.Fatal(err)
}
a := strataArgs(t, out)
if argAfter(a, "--pcie-frac") != "0" || out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != "1" {
t.Errorf("STRATA_MMAP=1 non appliqué : %v %v", a, out["env"])
}
out, _ = strataBuildConfig(base, map[string]string{"STRATA_MMAP": "0"}, "")
if out["env"].(map[string]any)["STRATA_ARENA_MMAP"] != nil {
t.Errorf("mmap appliqué sans STRATA_MMAP : %v", out["env"])
}
}
func TestStrataFitCountsExistingData(t *testing.T) {
env := strataEnv{Supported: true, Main: 1, Helper: 0, RAMGB: 47, DiskFreeGB: 32,
GPUs: []strataGPU{{Index: 0, VRAMGB: 8, Arch: 86}, {Index: 1, VRAMGB: 16, Arch: 120}}}
if f := strataFitFor(strataQuants["IQ3_XXS"], env, 120); !f.OK {
t.Errorf("données déjà présentes, refusé quand même : %s", f.Why)
}
if f := strataFitFor(strataQuants["IQ3_XXS"], env, 0); f.OK {
t.Error("32 Go libres sans données : accepté")
}
}
func TestStrataBuildConfigPresetOverrides(t *testing.T) {
base := map[string]any{"args": []any{"--kv", "int8", "--spec", "4"}}
out, _ := strataBuildConfig(base, map[string]string{"STRATA_KV": "fp16", "STRATA_SPEC": "3"}, "")
a := strataArgs(t, out)
if argAfter(a, "--kv") != "fp16" || argAfter(a, "--spec") != "3" {
t.Errorf("%v", a)
}
}
func TestStrataDetailsFor(t *testing.T) {
home := t.TempDir()
t.Setenv("LOKI_HOME", home)
base := filepath.Join(home, "base.json")
if err := os.WriteFile(base, []byte(`{"args":["--pack","/p","--kv","int8","--spec","4","--max-context","131072"],"vision":{"exe":"v"}}`), 0o644); err != nil {
t.Fatal(err)
}
_ = os.MkdirAll(presetsDir(), 0o755)
preset := "ENGINE=strata\nSTRATA_CONFIG=" + base + "\nSTRATA_MAIN_GPU=1\nSTRATA_HELPER_GPU=0\nSTRATA_MMAP=1\nSTRATA_KV=fp16\nSTRATA_KV_RESIDENT=32768\nSTRATA_SPEC=3\n"
if err := os.WriteFile(filepath.Join(presetsDir(), "X.env"), []byte(preset), 0o644); err != nil {
t.Fatal(err)
}
env := strataEnv{GPUs: []strataGPU{{Index: 0, Name: "NVIDIA GeForce RTX 3070", VRAMGB: 8}, {Index: 1, Name: "NVIDIA GeForce RTX 5060 Ti", VRAMGB: 16}}}
d := strataDetailsFor("X", env)
if d == nil {
t.Fatal("pas de détails")
}
if d.KV != "fp16" || d.KVResident != "32768" || d.Spec != "3" || d.Ctx != "131072" || !d.Mmap ||
d.MainGPU != "RTX 5060 Ti (16 Go)" || d.HelperGPU != "RTX 3070 (8 Go)" || d.VisionGPU != "RTX 3070 (8 Go)" ||
d.ShortRead != "150" || d.CacheRoot != "256" || d.Prefill != "16384" {
t.Errorf("%+v", *d)
}
}
func TestStrataHaveGBPerChoice(t *testing.T) {
t.Setenv("LOKI_HOME", t.TempDir())
write := func(rel string, n int) {
p := filepath.Join(strataDataDir(), rel)
_ = os.MkdirAll(filepath.Dir(p), 0o755)
if err := os.WriteFile(p, make([]byte, n), 0o644); err != nil {
t.Fatal(err)
}
}
write("models/swift-IQ3_XXS/a.gguf", 3000)
write("packs/swift-iq3_xxs/experts.bin", 2000)
write("mtp/rt/x", 500)
write("models/IQ2_XS/b.gguf", 7000) // un autre choix : ne compte pas pour swift IQ3_XXS
if got := strataHaveGB("swift", "IQ3_XXS") * 1e9; got != 5500 {
t.Errorf("swift IQ3_XXS : %v octets, attendu 5500", got)
}
if got := strataHaveGB("swift", "IQ2_XS") * 1e9; got != 500 {
t.Errorf("swift IQ2_XS : %v octets, attendu 500 (seulement la tête MTP commune)", got)
}
}
func TestStrataApplySettings(t *testing.T) {
in := "# NAME=x\nENGINE=strata\nCTX=131072\nSTRATA_KV=fp16\nSTRATA_SPEC=3\nSTRATA_HELPER_GPU=0\n"
out, err := strataApplySettings(in, strataSettingsReq{Ctx: 65536, KV: "int8", Spec: 4, Vision: false, Helper: false})
if err != nil {
t.Fatal(err)
}
cfg := parseEnv(out)
if cfg["CTX"] != "65536" || cfg["STRATA_KV"] != "int8" || cfg["STRATA_SPEC"] != "4" || cfg["STRATA_VISION"] != "0" ||
cfg["STRATA_HELPER"] != "0" || cfg["STRATA_HELPER_GPU"] != "0" || cfg["ENGINE"] != "strata" {
t.Errorf("%q", out)
}
if strings.Count(out, "CTX=") != 1 {
t.Errorf("CTX dupliqué : %q", out)
}
if strataHelperOn(cfg) || strataCudaDevices(map[string]string{"STRATA_MAIN_GPU": "1", "STRATA_HELPER_GPU": "0", "STRATA_HELPER": "0"}) != "1" {
t.Error("carte d'aide coupée mais encore utilisée")
}
base := map[string]any{"args": []any{"--vision", "--pack", "/p"}, "vision": map[string]any{"exe": "v"}}
b, _ := strataBuildConfig(base, cfg, "")
if strataHasArg(strataArgs(t, b), "--vision") || b["vision"] != nil || strataHasArg(strataArgs(t, b), "--expert-cache-device1") {
t.Errorf("vision / aide coupées mais présentes : %v", b)
}
for _, bad := range []strataSettingsReq{{Ctx: 1000, KV: "fp16", Spec: 3}, {Ctx: 32768, KV: "q2", Spec: 3}, {Ctx: 32768, KV: "fp16", Spec: 9}} {
if _, err := strataApplySettings(in, bad); err == nil {
t.Errorf("accepté : %+v", bad)
}
}
}
func TestEngineOf(t *testing.T) {
cases := map[string]string{"": engineLlamacpp, "llamacpp": engineLlamacpp, "strata": engineStrata, " Strata ": engineStrata, "moe": engineStrata, "autre": engineLlamacpp}
for in, want := range cases {
if got := engineOf(map[string]string{"ENGINE": in}); got != want {
t.Errorf("ENGINE=%q : %q, attendu %q", in, got, want)
}
}
}
func TestModelLoadErrorStrata(t *testing.T) {
ok := strataServeMarker + " (paquet AJEAN MoE 1.0)\nloading the model (the first start takes a minute or two) ...\nready: http://127.0.0.1:8080/v1 (OpenAI: /v1/chat/completions)\n"
if got := modelLoadErrorFrom(ok); got != "" {
t.Errorf("Strata prêt signalé en échec : %q", got)
}
crash := strataServeMarker + "\nTraceback (most recent call last):\n File \"server.py\", line 1\nRuntimeError: boom\n"
if got := modelLoadErrorFrom(crash); !strings.Contains(got, "Strata") {
t.Errorf("trace Python non signalée : %q", got)
}
// une trace d'un lancement PRÉCÉDENT ne compte pas
if got := modelLoadErrorFrom(crash + strataServeMarker + "\nloading the model ...\n"); got != "" {
t.Errorf("ancienne trace prise pour l'échec en cours : %q", got)
}
}
+3
View File
@@ -180,6 +180,9 @@ func tunePreflight(ctx context.Context, inWeb bool) error {
if externalActive() { if externalActive() {
return errors.New("optimisation indisponible : le preset actif est une API externe") return errors.New("optimisation indisponible : le preset actif est une API externe")
} }
if strataActive() {
return errors.New("optimisation indisponible : le preset actif tourne sur Strata, réglé par sa propre fenêtre")
}
if err := tuneGuard(); err != nil { if err := tuneGuard(); err != nil {
return err return err
} }
+6
View File
@@ -828,6 +828,12 @@ func runBench(ctx context.Context, opts benchOpts, progress benchProgress) (*ben
if externalActive() { if externalActive() {
return nil, fmt.Errorf("benchmark indisponible : le preset actif est une API externe") return nil, fmt.Errorf("benchmark indisponible : le preset actif est une API externe")
} }
// Le protocole s'appuie sur /tokenize et sur la reprise du cache de
// llama-server ; Strata n'a pas le premier et garde sa conversation en cache
// malgré cache_prompt:false (la lecture serait comptée deux fois trop vite).
if strataActive() {
return nil, fmt.Errorf("benchmark indisponible : le preset actif tourne sur Strata (le débit s'affiche sous chaque réponse)")
}
if err := tuneGuard(); err != nil { if err := tuneGuard(); err != nil {
return nil, err return nil, err
} }
+5
View File
@@ -96,6 +96,11 @@ func oaiHandler() http.Handler {
Rewrite: func(pr *httputil.ProxyRequest) { Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("127.0.0.1:%d", LLMPort())}) pr.SetURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("127.0.0.1:%d", LLMPort())})
pr.Out.Host = pr.In.Host pr.Out.Host = pr.In.Host
// Strata refuse (403) un Host qu'il ne connaît pas (protection contre
// le DNS rebinding) : il se voit appelé sous son propre nom.
if strataActive() {
pr.Out.Host = pr.Out.URL.Host
}
}, },
ErrorHandler: func(w http.ResponseWriter, r *http.Request, e error) { ErrorHandler: func(w http.ResponseWriter, r *http.Request, e error) {
sendOAIError(w, http.StatusBadGateway, sendOAIError(w, http.StatusBadGateway,
+3
View File
@@ -472,6 +472,9 @@ func newLinkHandler(mux *http.ServeMux) http.Handler {
base := lp.Director base := lp.Director
lp.Director = func(req *http.Request) { lp.Director = func(req *http.Request) {
base(req) base(req)
// Le moteur se voit appelé sous son propre nom, pas sous celui du relais :
// Strata refuse (403) un Host inconnu (protection DNS rebinding).
req.Host = llama.Host
// Le client distant n'a pas la clé API de llama-server ; on l'injecte ici // Le client distant n'a pas la clé API de llama-server ; on l'injecte ici
// (l'auth réelle est faite par le relais via la clé de liaison du compte). // (l'auth réelle est faite par le relais via la clé de liaison du compte).
if apiKey != "" { if apiKey != "" {
+4
View File
@@ -41,6 +41,10 @@ func serviceAction(action string) error {
// visible que dans le journal. // visible que dans le journal.
func preflightEngine() error { func preflightEngine() error {
cfg := ReadConfig() cfg := ReadConfig()
// Strata n'a ni BIN ni MODEL : son installeur a tout rangé (backend_strata.go).
if isStrataConfig(cfg) {
return strataPreflight(cfg)
}
bin := strings.TrimSpace(cfg["BIN"]) bin := strings.TrimSpace(cfg["BIN"])
if bin == "" { if bin == "" {
return fmt.Errorf("BIN non défini — installe un moteur : %s (ou renseigne BIN avec %s)", return fmt.Errorf("BIN non défini — installe un moteur : %s (ou renseigne BIN avec %s)",
+319 -5
View File
@@ -802,6 +802,32 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
gris atténué du thème monochrome et devenait illisible en mode sombre. */ gris atténué du thème monochrome et devenait illisible en mode sombre. */
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:4px;padding:1px 6px} .lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:4px;padding:1px 6px}
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px} .lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
/* Strata (10b-strata.js) : liste des qualités, réglages et configuration. */
.slist>.lc-modes#lc-engines{border-top:1px solid var(--border)}
#lc-strata.moe-off{cursor:default}
#lc-strata.moe-off:hover{background:none}
#lc-strata.moe-off .lc-mode-t{color:var(--dim)}
#strata-quants{overflow:hidden}
#strata-quants .lc-mode.moe-sel{background:var(--accent-bg)}
#strata-quants .lc-mode.moe-sel .lc-mode-t{color:var(--accent)}
#strata-quants .lc-mode.moe-off{cursor:default}
#strata-quants .lc-mode.moe-off:hover{background:none}
#strata-quants .lc-mode.moe-off .lc-mode-t{color:var(--dim)}
.moe-details{padding:4px 10px}
.moe-details .moe-kv{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:12px;padding:5px 0}
.moe-details .moe-kv .switch{display:block}
.moe-details .moe-kv:not(:last-child){border-bottom:1px solid var(--border)}
.moe-details .moe-kv span:first-child{color:var(--dim)}
.moe-details .moe-kv span:last-child{text-align:right}
.moe-ctx{display:flex;align-items:center;gap:10px}
.moe-ctx input[type=range]{width:160px;height:16px;margin:0;padding:0;background:transparent;border:none;-webkit-appearance:none;appearance:none;cursor:ew-resize}
.moe-ctx input[type=range]:focus{outline:none}
.moe-ctx input[type=range]::-webkit-slider-runnable-track{height:4px;border:none;border-radius:999px;background:linear-gradient(to right,var(--accent) var(--p,50%),color-mix(in srgb,var(--dim) 25%,transparent) var(--p,50%))}
.moe-ctx input[type=range]::-moz-range-track{height:4px;border:none;border-radius:999px;background:color-mix(in srgb,var(--dim) 25%,transparent)}
.moe-ctx input[type=range]::-moz-range-progress{height:4px;border-radius:999px;background:var(--accent)}
.moe-ctx input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border:none;border-radius:50%;background:var(--accent)}
.moe-ctx input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:var(--accent)}
.moe-ctx span{min-width:62px;text-align:right;font-variant-numeric:tabular-nums}
/* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle /* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle
les trois cartes grandissaient d'un cran une fois l'état connu. */ les trois cartes grandissaient d'un cran une fois l'état connu. */
.lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
@@ -2560,7 +2586,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<button data-pane="tasks" onclick="settingsShow('tasks')"><span>Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></button> <button data-pane="tasks" onclick="settingsShow('tasks')"><span>Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></button>
<button data-pane="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button> <button data-pane="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button>
<div class="set-nav-h">Moteur</div> <div class="set-nav-h">Moteur</div>
<button data-pane="engine" onclick="settingsShow('engine')">Moteur llama.cpp</button> <button data-pane="engine" onclick="settingsShow('engine')">Moteur</button>
<button data-pane="engine-log" onclick="settingsShow('engine-log')">Journal du moteur</button> <button data-pane="engine-log" onclick="settingsShow('engine-log')">Journal du moteur</button>
<button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button> <button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button>
<div class="set-nav-h">Application</div> <div class="set-nav-h">Application</div>
@@ -2762,7 +2788,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
</div></div> </div></div>
</section> </section>
<section class="set-pane" data-pane="engine" id="lc-details"> <section class="set-pane" data-pane="engine" id="lc-details">
<div class="set-pane-h"><span>Moteur</span></div> <div class="set-pane-h"><span>Moteur</span><span class="muted" style="font-size:12px;font-weight:400">actif : <b id="lc-active-engine">…</b></span></div>
<!-- Une seule carte : état, choix du mode et journal d'installation sont le <!-- Une seule carte : état, choix du mode et journal d'installation sont le
même sujet. Les trois modes sont des LIGNES de cette carte (elles même sujet. Les trois modes sont des LIGNES de cette carte (elles
étaient des cartes dans une carte). --> étaient des cartes dans une carte). -->
@@ -2828,6 +2854,18 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="lc-mode-state" id="lc-custom-state"></div> <div class="lc-mode-state" id="lc-custom-state"></div>
</div> </div>
</div> </div>
<!-- Deuxième moteur, hors des modes llama.cpp (masqués quand le moteur vient
de l'image) : Strata, spécialisé Qwen3.8 Flash Next, paquet figé repris
d'AJEAN (backend_strata.go). Le choix du moteur se fait par preset :
activer un modèle Strata bascule sur Strata, un autre preset revient à
llama.cpp. -->
<div class="lc-modes" id="lc-engines">
<div class="lc-mode" id="lc-strata" onclick="openStrata()">
<div class="lc-mode-t">Strata — Qwen3.8 Flash Next</div>
<div class="lc-mode-d">moteur spécialisé pour ce MoE de 125 B (experts répartis entre VRAM, RAM et disque), installation en un clic</div>
<div class="lc-mode-state" id="lc-strata-state"></div>
</div>
</div>
<div id="lc-job" class="rowbody" style="display:none"> <div id="lc-job" class="rowbody" style="display:none">
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div> <div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div> <div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
@@ -3097,6 +3135,46 @@ html[data-files="1"] #files-btn{color:var(--accent)}
</div> </div>
</div> </div>
</div> </div>
<div id="strata-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeStrata()">
<div class="modal-card" style="max-width:520px">
<div class="modal-head">
<strong>Strata — Qwen3.8 Flash Next</strong>
<button class="modal-x" onclick="closeStrata()" aria-label="Fermer">×</button>
</div>
<div class="modal-body mform">
<div id="strata-machine" class="muted" style="font-size:12px;line-height:1.5"></div>
<div class="fld">
<label class="fld-l">Version</label>
<div class="seg" id="strata-fams"></div>
</div>
<div class="fld">
<label class="fld-l">Qualité</label>
<div class="slist" id="strata-quants"></div>
</div>
<div id="strata-disk" class="muted" style="font-size:12px;line-height:1.5"></div>
<div class="fld" id="strata-settings-fld" hidden>
<label class="fld-l">Réglages</label>
<div class="slist moe-details" id="strata-settings"></div>
</div>
<div class="fld" id="strata-details-fld" hidden>
<label class="fld-l">Configuration</label>
<div class="slist moe-details" id="strata-details"></div>
</div>
<div id="strata-note" class="muted" style="font-size:11px;line-height:1.5"></div>
<div class="muted" style="font-size:11px;line-height:1.5">Moteur <a href="https://github.com/Niko1221/Strata" target="_blank" rel="noopener">Strata</a> (MIT),
paquet figé et vérifié (SHA-256) publié par <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener">AJEAN</a>.
Linux + NVIDIA (RTX 20 ou plus récente). Le modèle est téléchargé dans <code>/data/strata</code>.</div>
</div>
<div class="modal-foot">
<div class="foot-l"></div>
<div class="foot-r">
<button onclick="closeStrata()">fermer</button>
<button id="strata-save" hidden onclick="strataSaveSettings()">Enregistrer</button>
<button class="btn-save" id="strata-go" onclick="strataInstall()">Installer</button>
</div>
</div>
</div>
</div>
<div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()"> <div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()">
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
@@ -4382,6 +4460,27 @@ async function loadCfg(){
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]); const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'&quot;')+'">'+String(v)+'</span></div>'; const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'&quot;')+'">'+String(v)+'</span></div>';
const rows=[]; const rows=[];
// Strata (backend_strata.go) : la config réelle de lancement, calculée par le
// serveur comme au démarrage (/api/strata → details du modèle actif).
if(/^(strata|moe)$/i.test((c.ENGINE||'').trim())){
const s = await jget('/api/strata').catch(()=>null);
let fam=null, fit=null;
for(const f of (s&&s.families)||[]){ const x=f.fits.find(x=>x.active); if(x){ fam=f; fit=x; } }
const d=fit&&fit.details;
rows.push(row('MOTEUR', 'Strata', 'paquet AJEAN MoE '+((s&&s.version)||'')));
if(typeof setLoadedModel === 'function') setLoadedModel('');
if(fam) rows.push(row('MODEL', 'Qwen3.8 Flash Next '+fam.label+' '+fit.quant.id));
if(d){
const k=n=>(+n>=1024 ? Math.round(+n/1024)+'K' : n);
rows.push(row('CTX', d.ctx));
rows.push(row('KV', d.kv+(d.kv_resident ? ' · '+k(d.kv_resident)+' VRAM' : '')));
rows.push(row('MTP', d.spec));
rows.push(row('EXPERTS', d.drop ? 'mlock' : d.mmap ? 'mmap' : 'RAM'));
if(d.vision_gpu) rows.push(row('VISION', 'on', d.vision_gpu));
}else if(c.CTX) rows.push(row('CTX', c.CTX));
document.getElementById('cfg').innerHTML = rows.join('');
return;
}
if(c.BIN){ if(c.BIN){
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde // Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
// toujours le chemin complet, quel que soit le libellé. // toujours le chemin complet, quel que soit le libellé.
@@ -4528,7 +4627,7 @@ async function loadPresets(){
// Deux éditeurs, deux mondes : les réglages d'un preset externe (URL, modèle // Deux éditeurs, deux mondes : les réglages d'un preset externe (URL, modèle
// distant, clé) n'ont rien à voir avec ceux d'un modèle local. // distant, clé) n'ont rien à voir avec ceux d'un modèle local.
edit.title = x.external ? 'réglages de l’API externe' : 'réglages du preset'; edit.title = x.external ? 'réglages de l’API externe' : 'réglages du preset';
edit.onclick=(e)=>{ e.stopPropagation(); x.external ? openExternal(x.id) : openPreset(x.id); }; edit.onclick=(e)=>{ e.stopPropagation(); x.external ? openExternal(x.id) : x.strata ? openStrata(x.id) : openPreset(x.id); };
row.appendChild(info); row.appendChild(edit); row.appendChild(info); row.appendChild(edit);
cont.appendChild(row); cont.appendChild(row);
}); });
@@ -5036,7 +5135,7 @@ async function loadAll(){
// MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute // MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute
// suppression de module doit donc retirer son appel de cette ligne, et le test // suppression de module doit donc retirer son appel de cette ligne, et le test
// navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard. // navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard.
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadThinkEffort(),loadTasks(),loadProjects()]); await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]);
releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois
} }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
@@ -8354,7 +8453,7 @@ let lcSeenEnd = false, lcEndShown = false;
function lcChipLabel(action){ function lcChipLabel(action){
return {install:'Compilation du moteur', update:'Mise à jour du moteur', return {install:'Compilation du moteur', update:'Mise à jour du moteur',
prebuilt:'Téléchargement du moteur', engine:'Mise à jour du moteur', prebuilt:'Téléchargement du moteur', engine:'Mise à jour du moteur',
custom:'Installation du backend'}[action] || 'Installation du moteur'; custom:'Installation du backend', strata:'Installation de Strata'}[action] || 'Installation du moteur';
} }
function lcChipSync(j){ function lcChipSync(j){
const chip = document.getElementById('lc-chip'); const chip = document.getElementById('lc-chip');
@@ -8788,6 +8887,206 @@ function lcToggleLog(){
if(pre.hasAttribute('hidden')){ pre.removeAttribute('hidden'); bar.classList.add('open'); pre.scrollTop = pre.scrollHeight; } if(pre.hasAttribute('hidden')){ pre.removeAttribute('hidden'); bar.classList.add('open'); pre.scrollTop = pre.scrollHeight; }
else { pre.setAttribute('hidden',''); bar.classList.remove('open'); } else { pre.setAttribute('hidden',''); bar.classList.remove('open'); }
} }
// Moteur Strata : installation « en un clic » du moteur spécialisé Qwen3.8 Flash
// Next (repris d'AJEAN, « AJEAN MoE »). L'utilisateur choisit la version (Swift
// ou Classique) et la qualité (quant) ; tout le reste (cartes, RAM, mmap,
// réglages) est déduit côté serveur (backend_strata.go). L'installation est une
// tâche comme celles de llama.cpp : même suivi (#lc-job), même pastille.
let strataState = null, strataFam = '', strataQuant = '', strataTarget = '';
async function loadStrata(){
let s;
try{ s = await jget('/api/strata'); }catch(_){ return; }
strataState = s;
const row = document.getElementById('lc-strata');
if(!row) return;
// Hors Linux + NVIDIA, la ligne reste visible mais dit pourquoi : choisir son
// moteur, c'est aussi savoir pourquoi l'un des deux n'est pas proposé.
const env = s.env || {};
row.classList.toggle('moe-off', !env.supported);
const st = document.getElementById('lc-strata-state');
if(st){
const n = (s.installed||[]).length;
st.textContent = !env.supported ? (env.reason || 'indisponible sur cette machine')
: s.active ? 'moteur actif' + (n > 1 ? ' · '+n+' modèles installés' : '')
: n ? n+' modèle(s) installé(s)' : '';
}
const eng = document.getElementById('lc-active-engine');
if(eng) eng.textContent = s.active ? 'Strata' : 'llama.cpp';
}
// presetId (optionnel) : ouverte depuis l'engrenage d'un modèle installé, la
// fenêtre se place directement sur lui.
async function openStrata(presetId){
// état relu à chaque ouverture : juste après une installation, l'ancien
// proposait encore « Installer »
await loadStrata();
if(!strataState) return;
const s = strataState, env = s.env || {};
if(!env.supported){ toast('Strata : '+(env.reason||'indisponible sur cette machine')); return; }
strataTarget = typeof presetId === 'string' ? presetId : '';
const cards = (env.gpus||[]).filter(g=>g.index===env.main || g.index===env.helper)
.sort((a,b)=>(a.index===env.main?-1:1))
.map(g=>g.name.replace(/^NVIDIA GeForce /,'')+' '+Math.round(g.vram_gb)+' Go');
document.getElementById('strata-machine').textContent =
'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM, '
+Math.round(env.disk_free_gb)+' Go libres. Réglages automatiques.';
if(!strataFam) strataFam = (s.families[0]||{}).id || '';
if(strataTarget){
for(const f of s.families){
const hit = f.fits.find(x=>x.preset===strataTarget);
if(hit){ strataFam = f.id; strataQuant = hit.quant.id; }
}
}
strataRenderFams();
strataRenderQuants(!strataTarget);
showModal('strata-modal');
}
function closeStrata(){ hideModal('strata-modal'); }
function strataRenderFams(){
const box = document.getElementById('strata-fams');
box.innerHTML = strataState.families.map(f=>
'<label><input type="radio" name="strata-fam" value="'+f.id+'"'+(f.id===strataFam?' checked':'')
+' onchange="strataFam=this.value;strataRenderQuants(true)">'
+'<span>'+escHtml(f.label)+'</span><span class="be-note">'+escHtml(f.about)+'</span></label>').join('');
}
function strataRenderQuants(pickReco){
const f = strataState.families.find(x=>x.id===strataFam);
if(!f) return;
// à l'ouverture : le modèle actif s'il est de cette version, sinon un installé, sinon le conseillé
if(pickReco || !f.fits.some(x=>x.quant.id===strataQuant && (x.ok || x.preset))){
const cur = f.fits.find(x=>x.active) || f.fits.find(x=>x.preset);
strataQuant = cur ? cur.quant.id : (f.recommend || '');
}
const box = document.getElementById('strata-quants');
box.innerHTML = f.fits.map(x=>{
const q = x.quant, sel = q.id===strataQuant;
const tag = x.active ? 'actif' : x.preset ? 'installé' : q.id===f.recommend ? 'conseillé' : '';
const reco = tag ? ' <span class="lc-reco">'+tag+'</span>' : '';
const usable = x.ok || !!x.preset; // installé : activable même si l'espace disque manque
const desc = x.preset ? escHtml(q.about)
: x.ok ? escHtml(q.about)+' · ~'+Math.round(x.disk_gb)+' Go sur le disque'
: escHtml(x.why);
return '<div class="lc-mode'+(sel?' moe-sel':'')+(usable?'':' moe-off')+'"'
+(usable?' onclick="strataQuant=\''+q.id+'\';strataRenderQuants(false)"':'')+'>'
+'<div class="lc-mode-t">'+q.id+reco+'</div><div class="lc-mode-d">'+desc+'</div></div>';
}).join('');
const fit = f.fits.find(x=>x.quant.id===strataQuant);
const go = document.getElementById('strata-go');
go.disabled = !fit || !!fit.active;
go.textContent = fit && fit.active ? 'Déjà actif' : fit && fit.preset ? 'Activer' : 'Installer';
strataRenderDisk(fit);
strataRenderSettings(fit);
strataRenderDetails(fit);
document.getElementById('strata-note').textContent = !fit ? 'Aucune version ne tient sur cette machine.'
: fit.active ? 'Modèle actif.' : fit.preset ? 'Modèle installé, prêt à être activé.'
: fit.mmap ? 'RAM insuffisante pour tout charger : experts lus depuis le disque (mmap, ~45 Go de plus). Installation et activation automatiques.'
: 'Installation et activation automatiques. Premier chargement : quelques minutes.';
}
// Place disque : libre, ce que ce choix occupe, ce qui restera.
function strataRenderDisk(fit){
const el = document.getElementById('strata-disk');
const free = Math.round((strataState.env||{}).disk_free_gb||0);
if(!fit){ el.textContent = ''; return; }
if(fit.preset){ el.textContent = 'Disque : '+free+' Go libres. Modèle déjà présent.'; return; }
const need = Math.round(fit.disk_gb), left = free - need;
el.textContent = left >= 5
? 'Disque : '+free+' Go libres, ~'+need+' Go requis, ~'+left+' Go restants.'
: 'Disque : '+free+' Go libres, ~'+need+' Go requis. Espace insuffisant.';
}
// Réglages modifiables d'un modèle installé.
function strataRenderSettings(fit){
const fld = document.getElementById('strata-settings-fld'), save = document.getElementById('strata-save');
const d = fit && fit.details;
fld.hidden = save.hidden = !d;
if(!d) return;
const sel = (id, opts, cur) => '<select class="ctl" id="'+id+'" style="width:auto">'
+ opts.map(([v,l])=>'<option value="'+v+'"'+(String(v)===String(cur)?' selected':'')+'>'+escHtml(l)+'</option>').join('')+'</select>';
const sw = (id, on) => '<label class="switch"><input type="checkbox" id="'+id+'"'+(on?' checked':'')+'><span class="slider"></span></label>';
const rows = [
['Contexte', '<span class="moe-ctx"><input type="range" id="strata-s-ctx" min="32768" max="262144" step="4096" value="'+(+d.ctx||131072)+'" oninput="strataCtxLbl()"><span id="strata-s-ctx-v"></span></span>'],
['Cache KV', sel('strata-s-kv', [['fp16', 'fp16 (meilleure qualité)'],['int8', '8 bits (plus léger)']], d.kv)],
['Prédiction (MTP)', sel('strata-s-spec', [[2,'2'],[3,'3'],[4,'4']], d.spec)],
['Lire les images', sw('strata-s-vision', d.vision)],
];
if(d.has_helper) rows.push(["Carte d'appoint", sw('strata-s-helper', d.helper_on)]);
document.getElementById('strata-settings').innerHTML = rows.map(([a,b])=>
'<div class="moe-kv"><span>'+escHtml(a)+'</span><span>'+b+'</span></div>').join('');
strataCtxLbl();
}
// valeur exacte en jetons (131 072 et non « 128K », source de confusion)
function strataCtxLbl(){
const r = document.getElementById('strata-s-ctx'), o = document.getElementById('strata-s-ctx-v');
if(!r || !o) return;
o.textContent = (+r.value).toLocaleString('fr-FR');
// partie remplie à gauche de la pastille (la piste est dessinée en CSS)
r.style.setProperty('--p', ((r.value - r.min) / (r.max - r.min) * 100) + '%');
}
async function strataSaveSettings(){
const f = strataState && strataState.families.find(x=>x.id===strataFam);
const fit = f && f.fits.find(x=>x.quant.id===strataQuant);
if(!fit || !fit.preset) return;
const v = id => document.getElementById(id);
const body = {preset: fit.preset, ctx: +v('strata-s-ctx').value, kv: v('strata-s-kv').value, spec: +v('strata-s-spec').value,
vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false};
if(fit.active && !await askConfirm('Le modèle va redémarrer avec ces réglages (quelques minutes).', {title:'Strata — Qwen3.8 Flash Next', okText:'Enregistrer'})) return;
const r = await jpost('/api/strata/settings', body);
if(!r.ok){ toast('Erreur : '+(r.error||'')); return; }
toast(r.restarted ? 'Réglages enregistrés, redémarrage en cours' : 'Réglages enregistrés');
strataTarget = fit.preset;
await openStrata(fit.preset);
if(typeof loadPresets === 'function') loadPresets();
}
// La config qui tourne (ou tournera) pour un modèle installé.
function strataRenderDetails(fit){
const fld = document.getElementById('strata-details-fld');
const d = fit && fit.details;
fld.hidden = !d;
if(!d) return;
const k = n => (+n >= 1024 ? Math.round(+n/1024)+'K' : n);
const rows = [
['Carte principale', d.main_gpu],
["Carte d'appoint", d.helper_gpu || 'aucune'],
['Lecture des images', d.vision_gpu || 'aucune'],
['Contexte', (+d.ctx).toLocaleString('fr-FR')+' jetons'],
['Cache KV', d.kv + (d.kv_resident ? ' · '+k(d.kv_resident)+' en VRAM, le reste en RAM' : '')],
['Prédiction (MTP)', d.spec+' jetons'],
['Experts hors GPU', d.drop ? "verrouillés en RAM, hors ceux de la carte d'appoint" : d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM'],
['Lecture des prompts', 'blocs de '+k(d.prefill)+', lecture directe sous '+d.short_read+' jetons'],
["Démarrage d'une conversation", 'reprise après le prompt système (dès '+d.cache_root+' jetons)'],
['Version du moteur', 'paquet AJEAN MoE '+d.version],
];
document.getElementById('strata-details').innerHTML = rows.map(([a,b])=>
'<div class="moe-kv"><span>'+escHtml(a)+'</span><span>'+escHtml(b)+'</span></div>').join('');
}
async function strataInstall(){
const f = strataState && strataState.families.find(x=>x.id===strataFam);
const fit = f && f.fits.find(x=>x.quant.id===strataQuant);
if(!fit || fit.active) return;
// déjà installé : simple bascule, la même que dans la liste des modèles
if(fit.preset){
closeStrata();
await switchTo(-1, f.label+' '+strataQuant, fit.preset);
loadStrata();
return;
}
const msg = 'Installer Qwen3.8 Flash Next '+f.label+' '+strataQuant+' sur Strata ? Téléchargement : ~'
+Math.round(fit.disk_gb)+' Go. Le modèle sera activé à la fin.';
if(!await askConfirm(msg, {title:'Strata — Qwen3.8 Flash Next', okText:'Installer'})) return;
const r = await jpost('/api/strata/install', {family:strataFam, quant:strataQuant});
if(!r.ok){ toast('Erreur : '+(r.error||'')); return; }
closeStrata();
const det = document.getElementById('lc-details');
if(det && 'open' in det) det.open = true;
lcStartPolling();
}
// 11-external.js — presets « externes » : le chat part vers une API // 11-external.js — presets « externes » : le chat part vers une API
// OpenAI-compatible distante (OpenAI, Groq, OpenRouter, un vLLM sur une autre // OpenAI-compatible distante (OpenAI, Groq, OpenRouter, un vLLM sur une autre
// machine…) au lieu du llama-server local. // machine…) au lieu du llama-server local.
@@ -11897,6 +12196,21 @@ const I18N_EN = {
'Tâches planifiées': 'Scheduled tasks', 'Tâches planifiées': 'Scheduled tasks',
'System prompt': 'System prompt', 'System prompt': 'System prompt',
'Moteur llama.cpp': 'llama.cpp engine', 'Moteur llama.cpp': 'llama.cpp engine',
// Moteur Strata (10b-strata.js)
'Version': 'Version',
'Qualité': 'Quality',
'Configuration': 'Configuration',
'Installer': 'Install',
'Activer': 'Activate',
'Déjà actif': 'Already active',
'Cache KV': 'KV cache',
'Prédiction (MTP)': 'Prediction (MTP)',
'Lire les images': 'Read images',
"Carte d'appoint": 'Helper card',
'Carte principale': 'Main card',
'conseillé': 'recommended',
'installé': 'installed',
'actif': 'active',
'Journal du moteur': 'Engine log', 'Journal du moteur': 'Engine log',
'Dictée': 'Dictation', 'Dictée': 'Dictation',
'Identité': 'Identity', 'Identité': 'Identity',
+54 -2
View File
@@ -329,7 +329,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<button data-pane="tasks" onclick="settingsShow('tasks')"><span>Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></button> <button data-pane="tasks" onclick="settingsShow('tasks')"><span>Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></button>
<button data-pane="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button> <button data-pane="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button>
<div class="set-nav-h">Moteur</div> <div class="set-nav-h">Moteur</div>
<button data-pane="engine" onclick="settingsShow('engine')">Moteur llama.cpp</button> <button data-pane="engine" onclick="settingsShow('engine')">Moteur</button>
<button data-pane="engine-log" onclick="settingsShow('engine-log')">Journal du moteur</button> <button data-pane="engine-log" onclick="settingsShow('engine-log')">Journal du moteur</button>
<button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button> <button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button>
<div class="set-nav-h">Application</div> <div class="set-nav-h">Application</div>
@@ -531,7 +531,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
</div></div> </div></div>
</section> </section>
<section class="set-pane" data-pane="engine" id="lc-details"> <section class="set-pane" data-pane="engine" id="lc-details">
<div class="set-pane-h"><span>Moteur</span></div> <div class="set-pane-h"><span>Moteur</span><span class="muted" style="font-size:12px;font-weight:400">actif : <b id="lc-active-engine">…</b></span></div>
<!-- Une seule carte : état, choix du mode et journal d'installation sont le <!-- Une seule carte : état, choix du mode et journal d'installation sont le
même sujet. Les trois modes sont des LIGNES de cette carte (elles même sujet. Les trois modes sont des LIGNES de cette carte (elles
étaient des cartes dans une carte). --> étaient des cartes dans une carte). -->
@@ -597,6 +597,18 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="lc-mode-state" id="lc-custom-state"></div> <div class="lc-mode-state" id="lc-custom-state"></div>
</div> </div>
</div> </div>
<!-- Deuxième moteur, hors des modes llama.cpp (masqués quand le moteur vient
de l'image) : Strata, spécialisé Qwen3.8 Flash Next, paquet figé repris
d'AJEAN (backend_strata.go). Le choix du moteur se fait par preset :
activer un modèle Strata bascule sur Strata, un autre preset revient à
llama.cpp. -->
<div class="lc-modes" id="lc-engines">
<div class="lc-mode" id="lc-strata" onclick="openStrata()">
<div class="lc-mode-t">Strata — Qwen3.8 Flash Next</div>
<div class="lc-mode-d">moteur spécialisé pour ce MoE de 125 B (experts répartis entre VRAM, RAM et disque), installation en un clic</div>
<div class="lc-mode-state" id="lc-strata-state"></div>
</div>
</div>
<div id="lc-job" class="rowbody" style="display:none"> <div id="lc-job" class="rowbody" style="display:none">
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div> <div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div> <div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
@@ -866,6 +878,46 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
</div> </div>
</div> </div>
</div> </div>
<div id="strata-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeStrata()">
<div class="modal-card" style="max-width:520px">
<div class="modal-head">
<strong>Strata — Qwen3.8 Flash Next</strong>
<button class="modal-x" onclick="closeStrata()" aria-label="Fermer">×</button>
</div>
<div class="modal-body mform">
<div id="strata-machine" class="muted" style="font-size:12px;line-height:1.5"></div>
<div class="fld">
<label class="fld-l">Version</label>
<div class="seg" id="strata-fams"></div>
</div>
<div class="fld">
<label class="fld-l">Qualité</label>
<div class="slist" id="strata-quants"></div>
</div>
<div id="strata-disk" class="muted" style="font-size:12px;line-height:1.5"></div>
<div class="fld" id="strata-settings-fld" hidden>
<label class="fld-l">Réglages</label>
<div class="slist moe-details" id="strata-settings"></div>
</div>
<div class="fld" id="strata-details-fld" hidden>
<label class="fld-l">Configuration</label>
<div class="slist moe-details" id="strata-details"></div>
</div>
<div id="strata-note" class="muted" style="font-size:11px;line-height:1.5"></div>
<div class="muted" style="font-size:11px;line-height:1.5">Moteur <a href="https://github.com/Niko1221/Strata" target="_blank" rel="noopener">Strata</a> (MIT),
paquet figé et vérifié (SHA-256) publié par <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener">AJEAN</a>.
Linux + NVIDIA (RTX 20 ou plus récente). Le modèle est téléchargé dans <code>/data/strata</code>.</div>
</div>
<div class="modal-foot">
<div class="foot-l"></div>
<div class="foot-r">
<button onclick="closeStrata()">fermer</button>
<button id="strata-save" hidden onclick="strataSaveSettings()">Enregistrer</button>
<button class="btn-save" id="strata-go" onclick="strataInstall()">Installer</button>
</div>
</div>
</div>
</div>
<div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()"> <div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()">
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
+21
View File
@@ -315,6 +315,27 @@ async function loadCfg(){
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]); const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'&quot;')+'">'+String(v)+'</span></div>'; const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'&quot;')+'">'+String(v)+'</span></div>';
const rows=[]; const rows=[];
// Strata (backend_strata.go) : la config réelle de lancement, calculée par le
// serveur comme au démarrage (/api/strata → details du modèle actif).
if(/^(strata|moe)$/i.test((c.ENGINE||'').trim())){
const s = await jget('/api/strata').catch(()=>null);
let fam=null, fit=null;
for(const f of (s&&s.families)||[]){ const x=f.fits.find(x=>x.active); if(x){ fam=f; fit=x; } }
const d=fit&&fit.details;
rows.push(row('MOTEUR', 'Strata', 'paquet AJEAN MoE '+((s&&s.version)||'')));
if(typeof setLoadedModel === 'function') setLoadedModel('');
if(fam) rows.push(row('MODEL', 'Qwen3.8 Flash Next '+fam.label+' '+fit.quant.id));
if(d){
const k=n=>(+n>=1024 ? Math.round(+n/1024)+'K' : n);
rows.push(row('CTX', d.ctx));
rows.push(row('KV', d.kv+(d.kv_resident ? ' · '+k(d.kv_resident)+' VRAM' : '')));
rows.push(row('MTP', d.spec));
rows.push(row('EXPERTS', d.drop ? 'mlock' : d.mmap ? 'mmap' : 'RAM'));
if(d.vision_gpu) rows.push(row('VISION', 'on', d.vision_gpu));
}else if(c.CTX) rows.push(row('CTX', c.CTX));
document.getElementById('cfg').innerHTML = rows.join('');
return;
}
if(c.BIN){ if(c.BIN){
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde // Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
// toujours le chemin complet, quel que soit le libellé. // toujours le chemin complet, quel que soit le libellé.
+2 -2
View File
@@ -114,7 +114,7 @@ async function loadPresets(){
// Deux éditeurs, deux mondes : les réglages d'un preset externe (URL, modèle // Deux éditeurs, deux mondes : les réglages d'un preset externe (URL, modèle
// distant, clé) n'ont rien à voir avec ceux d'un modèle local. // distant, clé) n'ont rien à voir avec ceux d'un modèle local.
edit.title = x.external ? 'réglages de l’API externe' : 'réglages du preset'; edit.title = x.external ? 'réglages de l’API externe' : 'réglages du preset';
edit.onclick=(e)=>{ e.stopPropagation(); x.external ? openExternal(x.id) : openPreset(x.id); }; edit.onclick=(e)=>{ e.stopPropagation(); x.external ? openExternal(x.id) : x.strata ? openStrata(x.id) : openPreset(x.id); };
row.appendChild(info); row.appendChild(edit); row.appendChild(info); row.appendChild(edit);
cont.appendChild(row); cont.appendChild(row);
}); });
@@ -622,7 +622,7 @@ async function loadAll(){
// MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute // MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute
// suppression de module doit donc retirer son appel de cette ligne, et le test // suppression de module doit donc retirer son appel de cette ligne, et le test
// navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard. // navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard.
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadThinkEffort(),loadTasks(),loadProjects()]); await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadComputer(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadStrata(),loadThinkEffort(),loadTasks(),loadProjects()]);
releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois
} }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
+1 -1
View File
@@ -57,7 +57,7 @@ let lcSeenEnd = false, lcEndShown = false;
function lcChipLabel(action){ function lcChipLabel(action){
return {install:'Compilation du moteur', update:'Mise à jour du moteur', return {install:'Compilation du moteur', update:'Mise à jour du moteur',
prebuilt:'Téléchargement du moteur', engine:'Mise à jour du moteur', prebuilt:'Téléchargement du moteur', engine:'Mise à jour du moteur',
custom:'Installation du backend'}[action] || 'Installation du moteur'; custom:'Installation du backend', strata:'Installation de Strata'}[action] || 'Installation du moteur';
} }
function lcChipSync(j){ function lcChipSync(j){
const chip = document.getElementById('lc-chip'); const chip = document.getElementById('lc-chip');
+200
View File
@@ -0,0 +1,200 @@
// Moteur Strata : installation « en un clic » du moteur spécialisé Qwen3.8 Flash
// Next (repris d'AJEAN, « AJEAN MoE »). L'utilisateur choisit la version (Swift
// ou Classique) et la qualité (quant) ; tout le reste (cartes, RAM, mmap,
// réglages) est déduit côté serveur (backend_strata.go). L'installation est une
// tâche comme celles de llama.cpp : même suivi (#lc-job), même pastille.
let strataState = null, strataFam = '', strataQuant = '', strataTarget = '';
async function loadStrata(){
let s;
try{ s = await jget('/api/strata'); }catch(_){ return; }
strataState = s;
const row = document.getElementById('lc-strata');
if(!row) return;
// Hors Linux + NVIDIA, la ligne reste visible mais dit pourquoi : choisir son
// moteur, c'est aussi savoir pourquoi l'un des deux n'est pas proposé.
const env = s.env || {};
row.classList.toggle('moe-off', !env.supported);
const st = document.getElementById('lc-strata-state');
if(st){
const n = (s.installed||[]).length;
st.textContent = !env.supported ? (env.reason || 'indisponible sur cette machine')
: s.active ? 'moteur actif' + (n > 1 ? ' · '+n+' modèles installés' : '')
: n ? n+' modèle(s) installé(s)' : '';
}
const eng = document.getElementById('lc-active-engine');
if(eng) eng.textContent = s.active ? 'Strata' : 'llama.cpp';
}
// presetId (optionnel) : ouverte depuis l'engrenage d'un modèle installé, la
// fenêtre se place directement sur lui.
async function openStrata(presetId){
// état relu à chaque ouverture : juste après une installation, l'ancien
// proposait encore « Installer »
await loadStrata();
if(!strataState) return;
const s = strataState, env = s.env || {};
if(!env.supported){ toast('Strata : '+(env.reason||'indisponible sur cette machine')); return; }
strataTarget = typeof presetId === 'string' ? presetId : '';
const cards = (env.gpus||[]).filter(g=>g.index===env.main || g.index===env.helper)
.sort((a,b)=>(a.index===env.main?-1:1))
.map(g=>g.name.replace(/^NVIDIA GeForce /,'')+' '+Math.round(g.vram_gb)+' Go');
document.getElementById('strata-machine').textContent =
'Machine : '+cards.join(' + ')+', '+Math.round(env.ram_gb)+' Go de RAM, '
+Math.round(env.disk_free_gb)+' Go libres. Réglages automatiques.';
if(!strataFam) strataFam = (s.families[0]||{}).id || '';
if(strataTarget){
for(const f of s.families){
const hit = f.fits.find(x=>x.preset===strataTarget);
if(hit){ strataFam = f.id; strataQuant = hit.quant.id; }
}
}
strataRenderFams();
strataRenderQuants(!strataTarget);
showModal('strata-modal');
}
function closeStrata(){ hideModal('strata-modal'); }
function strataRenderFams(){
const box = document.getElementById('strata-fams');
box.innerHTML = strataState.families.map(f=>
'<label><input type="radio" name="strata-fam" value="'+f.id+'"'+(f.id===strataFam?' checked':'')
+' onchange="strataFam=this.value;strataRenderQuants(true)">'
+'<span>'+escHtml(f.label)+'</span><span class="be-note">'+escHtml(f.about)+'</span></label>').join('');
}
function strataRenderQuants(pickReco){
const f = strataState.families.find(x=>x.id===strataFam);
if(!f) return;
// à l'ouverture : le modèle actif s'il est de cette version, sinon un installé, sinon le conseillé
if(pickReco || !f.fits.some(x=>x.quant.id===strataQuant && (x.ok || x.preset))){
const cur = f.fits.find(x=>x.active) || f.fits.find(x=>x.preset);
strataQuant = cur ? cur.quant.id : (f.recommend || '');
}
const box = document.getElementById('strata-quants');
box.innerHTML = f.fits.map(x=>{
const q = x.quant, sel = q.id===strataQuant;
const tag = x.active ? 'actif' : x.preset ? 'installé' : q.id===f.recommend ? 'conseillé' : '';
const reco = tag ? ' <span class="lc-reco">'+tag+'</span>' : '';
const usable = x.ok || !!x.preset; // installé : activable même si l'espace disque manque
const desc = x.preset ? escHtml(q.about)
: x.ok ? escHtml(q.about)+' · ~'+Math.round(x.disk_gb)+' Go sur le disque'
: escHtml(x.why);
return '<div class="lc-mode'+(sel?' moe-sel':'')+(usable?'':' moe-off')+'"'
+(usable?' onclick="strataQuant=\''+q.id+'\';strataRenderQuants(false)"':'')+'>'
+'<div class="lc-mode-t">'+q.id+reco+'</div><div class="lc-mode-d">'+desc+'</div></div>';
}).join('');
const fit = f.fits.find(x=>x.quant.id===strataQuant);
const go = document.getElementById('strata-go');
go.disabled = !fit || !!fit.active;
go.textContent = fit && fit.active ? 'Déjà actif' : fit && fit.preset ? 'Activer' : 'Installer';
strataRenderDisk(fit);
strataRenderSettings(fit);
strataRenderDetails(fit);
document.getElementById('strata-note').textContent = !fit ? 'Aucune version ne tient sur cette machine.'
: fit.active ? 'Modèle actif.' : fit.preset ? 'Modèle installé, prêt à être activé.'
: fit.mmap ? 'RAM insuffisante pour tout charger : experts lus depuis le disque (mmap, ~45 Go de plus). Installation et activation automatiques.'
: 'Installation et activation automatiques. Premier chargement : quelques minutes.';
}
// Place disque : libre, ce que ce choix occupe, ce qui restera.
function strataRenderDisk(fit){
const el = document.getElementById('strata-disk');
const free = Math.round((strataState.env||{}).disk_free_gb||0);
if(!fit){ el.textContent = ''; return; }
if(fit.preset){ el.textContent = 'Disque : '+free+' Go libres. Modèle déjà présent.'; return; }
const need = Math.round(fit.disk_gb), left = free - need;
el.textContent = left >= 5
? 'Disque : '+free+' Go libres, ~'+need+' Go requis, ~'+left+' Go restants.'
: 'Disque : '+free+' Go libres, ~'+need+' Go requis. Espace insuffisant.';
}
// Réglages modifiables d'un modèle installé.
function strataRenderSettings(fit){
const fld = document.getElementById('strata-settings-fld'), save = document.getElementById('strata-save');
const d = fit && fit.details;
fld.hidden = save.hidden = !d;
if(!d) return;
const sel = (id, opts, cur) => '<select class="ctl" id="'+id+'" style="width:auto">'
+ opts.map(([v,l])=>'<option value="'+v+'"'+(String(v)===String(cur)?' selected':'')+'>'+escHtml(l)+'</option>').join('')+'</select>';
const sw = (id, on) => '<label class="switch"><input type="checkbox" id="'+id+'"'+(on?' checked':'')+'><span class="slider"></span></label>';
const rows = [
['Contexte', '<span class="moe-ctx"><input type="range" id="strata-s-ctx" min="32768" max="262144" step="4096" value="'+(+d.ctx||131072)+'" oninput="strataCtxLbl()"><span id="strata-s-ctx-v"></span></span>'],
['Cache KV', sel('strata-s-kv', [['fp16', 'fp16 (meilleure qualité)'],['int8', '8 bits (plus léger)']], d.kv)],
['Prédiction (MTP)', sel('strata-s-spec', [[2,'2'],[3,'3'],[4,'4']], d.spec)],
['Lire les images', sw('strata-s-vision', d.vision)],
];
if(d.has_helper) rows.push(["Carte d'appoint", sw('strata-s-helper', d.helper_on)]);
document.getElementById('strata-settings').innerHTML = rows.map(([a,b])=>
'<div class="moe-kv"><span>'+escHtml(a)+'</span><span>'+b+'</span></div>').join('');
strataCtxLbl();
}
// valeur exacte en jetons (131 072 et non « 128K », source de confusion)
function strataCtxLbl(){
const r = document.getElementById('strata-s-ctx'), o = document.getElementById('strata-s-ctx-v');
if(!r || !o) return;
o.textContent = (+r.value).toLocaleString('fr-FR');
// partie remplie à gauche de la pastille (la piste est dessinée en CSS)
r.style.setProperty('--p', ((r.value - r.min) / (r.max - r.min) * 100) + '%');
}
async function strataSaveSettings(){
const f = strataState && strataState.families.find(x=>x.id===strataFam);
const fit = f && f.fits.find(x=>x.quant.id===strataQuant);
if(!fit || !fit.preset) return;
const v = id => document.getElementById(id);
const body = {preset: fit.preset, ctx: +v('strata-s-ctx').value, kv: v('strata-s-kv').value, spec: +v('strata-s-spec').value,
vision: v('strata-s-vision').checked, helper: v('strata-s-helper') ? v('strata-s-helper').checked : false};
if(fit.active && !await askConfirm('Le modèle va redémarrer avec ces réglages (quelques minutes).', {title:'Strata — Qwen3.8 Flash Next', okText:'Enregistrer'})) return;
const r = await jpost('/api/strata/settings', body);
if(!r.ok){ toast('Erreur : '+(r.error||'')); return; }
toast(r.restarted ? 'Réglages enregistrés, redémarrage en cours' : 'Réglages enregistrés');
strataTarget = fit.preset;
await openStrata(fit.preset);
if(typeof loadPresets === 'function') loadPresets();
}
// La config qui tourne (ou tournera) pour un modèle installé.
function strataRenderDetails(fit){
const fld = document.getElementById('strata-details-fld');
const d = fit && fit.details;
fld.hidden = !d;
if(!d) return;
const k = n => (+n >= 1024 ? Math.round(+n/1024)+'K' : n);
const rows = [
['Carte principale', d.main_gpu],
["Carte d'appoint", d.helper_gpu || 'aucune'],
['Lecture des images', d.vision_gpu || 'aucune'],
['Contexte', (+d.ctx).toLocaleString('fr-FR')+' jetons'],
['Cache KV', d.kv + (d.kv_resident ? ' · '+k(d.kv_resident)+' en VRAM, le reste en RAM' : '')],
['Prédiction (MTP)', d.spec+' jetons'],
['Experts hors GPU', d.drop ? "verrouillés en RAM, hors ceux de la carte d'appoint" : d.mmap ? 'lus depuis le disque (mmap)' : 'chargés en RAM'],
['Lecture des prompts', 'blocs de '+k(d.prefill)+', lecture directe sous '+d.short_read+' jetons'],
["Démarrage d'une conversation", 'reprise après le prompt système (dès '+d.cache_root+' jetons)'],
['Version du moteur', 'paquet AJEAN MoE '+d.version],
];
document.getElementById('strata-details').innerHTML = rows.map(([a,b])=>
'<div class="moe-kv"><span>'+escHtml(a)+'</span><span>'+escHtml(b)+'</span></div>').join('');
}
async function strataInstall(){
const f = strataState && strataState.families.find(x=>x.id===strataFam);
const fit = f && f.fits.find(x=>x.quant.id===strataQuant);
if(!fit || fit.active) return;
// déjà installé : simple bascule, la même que dans la liste des modèles
if(fit.preset){
closeStrata();
await switchTo(-1, f.label+' '+strataQuant, fit.preset);
loadStrata();
return;
}
const msg = 'Installer Qwen3.8 Flash Next '+f.label+' '+strataQuant+' sur Strata ? Téléchargement : ~'
+Math.round(fit.disk_gb)+' Go. Le modèle sera activé à la fin.';
if(!await askConfirm(msg, {title:'Strata — Qwen3.8 Flash Next', okText:'Installer'})) return;
const r = await jpost('/api/strata/install', {family:strataFam, quant:strataQuant});
if(!r.ok){ toast('Erreur : '+(r.error||'')); return; }
closeStrata();
const det = document.getElementById('lc-details');
if(det && 'open' in det) det.open = true;
lcStartPolling();
}
+15
View File
@@ -38,6 +38,21 @@ const I18N_EN = {
'Tâches planifiées': 'Scheduled tasks', 'Tâches planifiées': 'Scheduled tasks',
'System prompt': 'System prompt', 'System prompt': 'System prompt',
'Moteur llama.cpp': 'llama.cpp engine', 'Moteur llama.cpp': 'llama.cpp engine',
// Moteur Strata (10b-strata.js)
'Version': 'Version',
'Qualité': 'Quality',
'Configuration': 'Configuration',
'Installer': 'Install',
'Activer': 'Activate',
'Déjà actif': 'Already active',
'Cache KV': 'KV cache',
'Prédiction (MTP)': 'Prediction (MTP)',
'Lire les images': 'Read images',
"Carte d'appoint": 'Helper card',
'Carte principale': 'Main card',
'conseillé': 'recommended',
'installé': 'installed',
'actif': 'active',
'Journal du moteur': 'Engine log', 'Journal du moteur': 'Engine log',
'Dictée': 'Dictation', 'Dictée': 'Dictation',
'Identité': 'Identity', 'Identité': 'Identity',
+26
View File
@@ -767,6 +767,32 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
gris atténué du thème monochrome et devenait illisible en mode sombre. */ gris atténué du thème monochrome et devenait illisible en mode sombre. */
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:4px;padding:1px 6px} .lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:4px;padding:1px 6px}
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px} .lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
/* Strata (10b-strata.js) : liste des qualités, réglages et configuration. */
.slist>.lc-modes#lc-engines{border-top:1px solid var(--border)}
#lc-strata.moe-off{cursor:default}
#lc-strata.moe-off:hover{background:none}
#lc-strata.moe-off .lc-mode-t{color:var(--dim)}
#strata-quants{overflow:hidden}
#strata-quants .lc-mode.moe-sel{background:var(--accent-bg)}
#strata-quants .lc-mode.moe-sel .lc-mode-t{color:var(--accent)}
#strata-quants .lc-mode.moe-off{cursor:default}
#strata-quants .lc-mode.moe-off:hover{background:none}
#strata-quants .lc-mode.moe-off .lc-mode-t{color:var(--dim)}
.moe-details{padding:4px 10px}
.moe-details .moe-kv{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:12px;padding:5px 0}
.moe-details .moe-kv .switch{display:block}
.moe-details .moe-kv:not(:last-child){border-bottom:1px solid var(--border)}
.moe-details .moe-kv span:first-child{color:var(--dim)}
.moe-details .moe-kv span:last-child{text-align:right}
.moe-ctx{display:flex;align-items:center;gap:10px}
.moe-ctx input[type=range]{width:160px;height:16px;margin:0;padding:0;background:transparent;border:none;-webkit-appearance:none;appearance:none;cursor:ew-resize}
.moe-ctx input[type=range]:focus{outline:none}
.moe-ctx input[type=range]::-webkit-slider-runnable-track{height:4px;border:none;border-radius:999px;background:linear-gradient(to right,var(--accent) var(--p,50%),color-mix(in srgb,var(--dim) 25%,transparent) var(--p,50%))}
.moe-ctx input[type=range]::-moz-range-track{height:4px;border:none;border-radius:999px;background:color-mix(in srgb,var(--dim) 25%,transparent)}
.moe-ctx input[type=range]::-moz-range-progress{height:4px;border-radius:999px;background:var(--accent)}
.moe-ctx input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border:none;border-radius:50%;background:var(--accent)}
.moe-ctx input[type=range]::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:var(--accent)}
.moe-ctx span{min-width:62px;text-align:right;font-variant-numeric:tabular-nums}
/* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle /* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle
les trois cartes grandissaient d'un cran une fois l'état connu. */ les trois cartes grandissaient d'un cran une fois l'état connu. */
.lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
+18 -2
View File
@@ -109,7 +109,8 @@ func modelLoadErrorFrom(log string) string {
for i, l := range lines { for i, l := range lines {
// Un lancement refusé par LOAD_GUARD (backend_serve_moe.go) s'arrête // Un lancement refusé par LOAD_GUARD (backend_serve_moe.go) s'arrête
// avant tout chargement : c'est lui, la dernière tentative. // avant tout chargement : c'est lui, la dernière tentative.
if strings.Contains(l, "loading model") || strings.Contains(l, "load_model") || strings.Contains(l, loadGuardMarker) { if strings.Contains(l, "loading model") || strings.Contains(l, "load_model") || strings.Contains(l, loadGuardMarker) ||
strings.Contains(l, strataServeMarker) {
start = i start = i
} }
} }
@@ -117,8 +118,16 @@ func modelLoadErrorFrom(log string) string {
for _, l := range lines[start:] { for _, l := range lines[start:] {
low := strings.ToLower(l) low := strings.ToLower(l)
switch { switch {
case strings.Contains(low, "model loaded"), strings.Contains(low, "server is listening"): case strings.Contains(low, "model loaded"), strings.Contains(low, "server is listening"),
strings.HasPrefix(l, "ready: http://"): // serveur de Strata prêt
loaded = true loaded = true
// Serveur de Strata (Python) : une trace = il s'est arrêté, pas « il charge encore ».
case strings.Contains(l, "Traceback (most recent call last)"):
if reason == "" {
reason = "le serveur de Strata s'est arrêté (trace Python dans le journal)"
}
case strings.Contains(l, "[strata] config"):
reason = "configuration de Strata refusée (détail dans le journal) — réinstalle le modèle"
case strings.Contains(low, "failed to create mtp context"), case strings.Contains(low, "failed to create mtp context"),
strings.Contains(low, "failed to load draft model"), strings.Contains(low, "failed to load draft model"),
strings.Contains(low, "failed to initialize speculative decoding context"): strings.Contains(low, "failed to initialize speculative decoding context"):
@@ -435,6 +444,13 @@ func handlePresets(w http.ResponseWriter, r *http.Request) {
out = append(out, item) out = append(out, item)
continue continue
} }
// Strata : réglages dans sa propre fenêtre, pas l'éditeur llama.cpp.
if cfg := parseEnv(content); isStrataConfig(cfg) {
item["strata"] = true
if cfg["STRATA_VISION"] != "0" {
item["vision"] = true
}
}
if q := detectQuant(content); q != "" { if q := detectQuant(content); q != "" {
item["quant"] = q item["quant"] = q
} }
+3
View File
@@ -256,6 +256,9 @@ func newWebMux() *http.ServeMux {
api("/api/engine/remove", handleEngineRemove) // supprime une version téléchargée api("/api/engine/remove", handleEngineRemove) // supprime une version téléchargée
api("/api/engine/plan", handleEnginePlan) // sur clic : version visée + effet sur le rendu du gabarit api("/api/engine/plan", handleEnginePlan) // sur clic : version visée + effet sur le rendu du gabarit
api("/api/engine/rollback", handleEngineRollback) // revient au moteur d'avant la dernière bascule api("/api/engine/rollback", handleEngineRollback) // revient au moteur d'avant la dernière bascule
api("/api/strata", handleStrata) // Strata : machine, catalogue figé, recommandation, modèles installés
api("/api/strata/install", handleStrataInstall) // job : paquet figé + installeur de Strata + preset (suivi par /api/llamacpp/job)
api("/api/strata/settings", handleStrataSettings) // réglages d'un modèle Strata installé (relance s'il est actif)
api("/api/presets", handlePresets) api("/api/presets", handlePresets)
api("/api/preset", handlePreset) api("/api/preset", handlePreset)
api("/api/preset/save", handlePresetSave) api("/api/preset/save", handlePresetSave)
+4
View File
@@ -180,6 +180,10 @@ func visionEnabled() bool {
if externalActive() { if externalActive() {
return externalVisionActive() // modèle distant déclaré multimodal return externalVisionActive() // modèle distant déclaré multimodal
} }
// Strata : l'encodeur d'images est installé avec le modèle, sans MMPROJ.
if strataVisionActive() {
return true
}
return strings.TrimSpace(ReadConfig()["MMPROJ"]) != "" return strings.TrimSpace(ReadConfig()["MMPROJ"]) != ""
} }