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

+8
View File
@@ -272,6 +272,14 @@ func cmdServe(args []string) error {
}
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"]
if bin == "" {
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() {
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 {
return err
}
+6
View File
@@ -828,6 +828,12 @@ func runBench(ctx context.Context, opts benchOpts, progress benchProgress) (*ben
if externalActive() {
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 {
return nil, err
}
+5
View File
@@ -96,6 +96,11 @@ func oaiHandler() http.Handler {
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("127.0.0.1:%d", LLMPort())})
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) {
sendOAIError(w, http.StatusBadGateway,
+3
View File
@@ -472,6 +472,9 @@ func newLinkHandler(mux *http.ServeMux) http.Handler {
base := lp.Director
lp.Director = func(req *http.Request) {
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
// (l'auth réelle est faite par le relais via la clé de liaison du compte).
if apiKey != "" {
+4
View File
@@ -41,6 +41,10 @@ func serviceAction(action string) error {
// visible que dans le journal.
func preflightEngine() error {
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"])
if bin == "" {
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. */
.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}
/* 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
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}
@@ -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="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button>
<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="dictee" onclick="settingsShow('dictee')">Dictée</button>
<div class="set-nav-h">Application</div>
@@ -2762,7 +2788,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
</div></div>
</section>
<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
même sujet. Les trois modes sont des LIGNES de cette carte (elles
é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>
</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-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>
@@ -3097,6 +3135,46 @@ html[data-files="1"] #files-btn{color:var(--accent)}
</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 class="modal-card" style="max-width:560px">
<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 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=[];
// 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){
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
// 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
// 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.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);
cont.appendChild(row);
});
@@ -5036,7 +5135,7 @@ async function loadAll(){
// 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
// 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
}
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){
return {install:'Compilation du moteur', update:'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){
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; }
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
// OpenAI-compatible distante (OpenAI, Groq, OpenRouter, un vLLM sur une autre
// machine…) au lieu du llama-server local.
@@ -11897,6 +12196,21 @@ const I18N_EN = {
'Tâches planifiées': 'Scheduled tasks',
'System prompt': 'System prompt',
'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',
'Dictée': 'Dictation',
'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="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button>
<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="dictee" onclick="settingsShow('dictee')">Dictée</button>
<div class="set-nav-h">Application</div>
@@ -531,7 +531,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
</div></div>
</section>
<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
même sujet. Les trois modes sont des LIGNES de cette carte (elles
é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>
</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-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>
@@ -866,6 +878,46 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
</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 class="modal-card" style="max-width:560px">
<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 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=[];
// 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){
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
// 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
// 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.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);
cont.appendChild(row);
});
@@ -622,7 +622,7 @@ async function loadAll(){
// 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
// 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
}
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){
return {install:'Compilation du moteur', update:'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){
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',
'System prompt': 'System prompt',
'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',
'Dictée': 'Dictation',
'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. */
.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}
/* 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
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}
+18 -2
View File
@@ -109,7 +109,8 @@ func modelLoadErrorFrom(log string) string {
for i, l := range lines {
// Un lancement refusé par LOAD_GUARD (backend_serve_moe.go) s'arrête
// 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
}
}
@@ -117,8 +118,16 @@ func modelLoadErrorFrom(log string) string {
for _, l := range lines[start:] {
low := strings.ToLower(l)
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
// 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"),
strings.Contains(low, "failed to load draft model"),
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)
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 != "" {
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/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/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/preset", handlePreset)
api("/api/preset/save", handlePresetSave)
+4
View File
@@ -180,6 +180,10 @@ func visionEnabled() bool {
if externalActive() {
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"]) != ""
}