mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
v0.8.3 : le stop arrete vraiment, le chat ne se bloque plus, raisonnement off = off
Bug 1 (chat bloque, remonte par Nathan) : trois causes cumulees. - runShell naissait d'un contexte independant du tour : stop n'arretait rien. La commande herite desormais du contexte du tour, et la boucle d'outils s'interrompt des que l'arret est demande. - cmd.WaitDelay : une commande laissant un process detache gardait les tubes ouverts, donc Wait ne revenait JAMAIS (ni delai, ni stop). - Reset debloque toujours l'etat de generation : « nouvelle conversation » etait le geste tente quand le chat coince, il ne servait a rien. Un tour perime ne libere plus le tour courant. Trouve en testant : le workspace disparu cassait TOUTES les commandes suivantes (chdir: no such file or directory) jusqu'au redemarrage ; il est recree au besoin. Bug 2 (raisonnement, remonte par Nathan) : l'UI ecrivait « off » en EFFACANT la ligne REASONING, or sans drapeau le moteur suit le gabarit du modele, qui raisonne. L'UI ecrit maintenant on|off et « off » part en --reasoning off, apres avoir demande au binaire s'il connait le drapeau (ik_llama.cpp ne le connait pas et refuserait de demarrer). Presets existants inchanges ; l'editeur dit desormais « non precise : le modele decide » quand la cle est absente.
This commit is contained in:
1 parent
e6a948c314
commit
acf11ff66a
13 files changed
+284
-59
No files matched your search
@@ -1,11 +1,39 @@
|
||||
package ajean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// binSupportsReasoningFlag dit si ce llama-server accepte « --reasoning ».
|
||||
//
|
||||
// Le drapeau est récent : les moteurs plus anciens, et certains forks, ne le
|
||||
// connaissent pas et REFUSENT de démarrer sur un argument inconnu. Comme on ne
|
||||
// l'ajoute que pour interdire le raisonnement, mieux vaut demander au binaire
|
||||
// que parier : on lit son aide, une fois, au lancement du moteur.
|
||||
//
|
||||
// L'aide se lit avec le même chemin de bibliothèques que le vrai lancement
|
||||
// (setLibraryPath a déjà été appelé) : sans ça un moteur parfaitement valide
|
||||
// échoue à s'exécuter (« libllama-common.so introuvable ») et on conclurait à
|
||||
// tort qu'il ne gère pas le drapeau.
|
||||
func binSupportsReasoningFlag(bin string) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, bin, "--help")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &out, &out
|
||||
if err := cmd.Run(); err != nil && out.Len() == 0 {
|
||||
return false // aide illisible : on ne prend pas le risque
|
||||
}
|
||||
return strings.Contains(out.String(), "--reasoning ")
|
||||
}
|
||||
|
||||
// cmdServe replaces the historic start.sh: read config.env, build the
|
||||
// llama-server invocation, and exec it (replacing this process so systemd
|
||||
// supervises llama-server directly).
|
||||
@@ -81,14 +109,35 @@ func cmdServe(args []string) error {
|
||||
if vtv != "" {
|
||||
llmArgs = append(llmArgs, "-ctv", vtv)
|
||||
}
|
||||
if r := cfg["REASONING"]; r != "" {
|
||||
// budget illimité par défaut (-1) : on laisse le modèle réfléchir jusqu'au
|
||||
// bout au lieu de le couper à 2048, ce qui tronquait la vraie réponse (la
|
||||
// réflexion atteignait le plafond et il ne restait plus de marge pour le
|
||||
// contenu). L'anti-boucle côté llm_client.go reste le garde-fou. NE PAS forcer 0 :
|
||||
// sur llama.cpp vanilla, 0 = "immediate end" → coupe tout le raisonnement
|
||||
// (le fork ik_llama.cpp l'ignore). Configurable via REASONING_BUDGET.
|
||||
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1"))
|
||||
// Raisonnement. Trois cas, et la nuance compte :
|
||||
//
|
||||
// REASONING=on|auto|deepseek → --reasoning <valeur>
|
||||
// REASONING=off → --reasoning off (interdiction EXPLICITE)
|
||||
// clé absente → aucun drapeau, le moteur fait son défaut
|
||||
//
|
||||
// L'interface écrivait « off » en EFFAÇANT la ligne, ce qui n'est pas du tout
|
||||
// la même chose : sans drapeau, llama-server suit le gabarit du modèle, et un
|
||||
// modèle à raisonnement raisonne. L'interrupteur affichait donc « désactivé »
|
||||
// pendant que le modèle réfléchissait quand même. Il faut le dire au moteur.
|
||||
if r := strings.TrimSpace(cfg["REASONING"]); r != "" {
|
||||
if reasoningActive(r) {
|
||||
// budget illimité par défaut (-1) : on laisse le modèle réfléchir jusqu'au
|
||||
// bout au lieu de le couper à 2048, ce qui tronquait la vraie réponse (la
|
||||
// réflexion atteignait le plafond et il ne restait plus de marge pour le
|
||||
// contenu). L'anti-boucle côté llm_client.go reste le garde-fou. NE PAS forcer 0 :
|
||||
// sur llama.cpp vanilla, 0 = "immediate end" → coupe tout le raisonnement
|
||||
// (le fork ik_llama.cpp l'ignore). Configurable via REASONING_BUDGET.
|
||||
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1"))
|
||||
} else if binSupportsReasoningFlag(bin) {
|
||||
// Pas de budget ici : « off » suffit, et un budget sur un moteur qui
|
||||
// n'attend rien d'autre ne ferait qu'ajouter une occasion d'échouer.
|
||||
llmArgs = append(llmArgs, "--reasoning", "off")
|
||||
} else {
|
||||
// Vieux moteur (ou fork) qui ne connaît pas le drapeau : le lui passer
|
||||
// le ferait sortir en erreur au démarrage, donc boucler. On le dit et on
|
||||
// continue sans — mieux vaut un modèle qui réfléchit qu'un moteur mort.
|
||||
fmt.Fprintf(os.Stderr, "[ajean serve] ce moteur ne connaît pas --reasoning : impossible de désactiver le raisonnement\n")
|
||||
}
|
||||
}
|
||||
// API_KEY protège le serveur quand il est exposé sur internet : llama-server
|
||||
// exige alors l'en-tête "Authorization: Bearer <clé>". La clé est lue depuis
|
||||
|
||||
@@ -292,9 +292,16 @@ func (c *Conversation) StartTurn(text string, caps Caps, temperature float64) er
|
||||
func (c *Conversation) generate(ctx context.Context, caps Caps, temperature float64, epoch int) {
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.Generating = false
|
||||
c.cancel = nil
|
||||
stale := c.epoch != epoch
|
||||
// ⚠️ Un tour périmé ne touche PAS à l'état courant. Depuis que Reset
|
||||
// débloque lui-même la conversation, un tour abandonné peut se terminer
|
||||
// APRÈS le démarrage du suivant : remettre Generating à false ici
|
||||
// déclarerait « libre » une génération toute neuve, et l'UI afficherait
|
||||
// un fil qui se remplit avec un bouton « envoyer » actif.
|
||||
if !stale {
|
||||
c.Generating = false
|
||||
c.cancel = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if stale {
|
||||
return // Reset pendant le tour : Reset a déjà persisté l'état vide
|
||||
@@ -452,8 +459,12 @@ func (c *Conversation) CompactNow() error {
|
||||
go func() {
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.Generating = false
|
||||
c.cancel = nil
|
||||
// Même précaution que dans generate : une compaction abandonnée par un
|
||||
// Reset ne doit pas déclarer « libre » le tour qui a démarré depuis.
|
||||
if c.epoch == epoch {
|
||||
c.Generating = false
|
||||
c.cancel = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
if _, changed := c.compactAndPublish(ctx, epoch, "manuel", msgs, lastReal, Caps{}); changed {
|
||||
@@ -476,6 +487,15 @@ func (c *Conversation) Stop() {
|
||||
// Reset démarre une nouvelle conversation (vide) pour TOUS les appareils. On
|
||||
// interrompt une éventuelle génération, on vide tout et on bump epoch pour que
|
||||
// les abonnés reçoivent l'ordre de nettoyer leur affichage.
|
||||
//
|
||||
// Reset DÉBLOQUE toujours, et c'est sa deuxième raison d'être. Il se contentait
|
||||
// avant de vider le fil : si un tour restait coincé (moteur redémarré sous ses
|
||||
// pieds, commande shell accrochée à ses tubes), Generating restait vrai pour
|
||||
// toujours et tout message suivant se voyait refusé « génération en cours ».
|
||||
// Vider le fil ne changeait rien, rafraîchir non plus : il fallait redémarrer le
|
||||
// service. « Nouvelle conversation » est le geste qu'on tente naturellement dans
|
||||
// ce cas ; il doit donc rendre la main, quoi qu'il arrive au tour abandonné, que
|
||||
// le bump d'epoch réduit de toute façon au silence.
|
||||
func (c *Conversation) Reset() {
|
||||
c.Stop()
|
||||
c.mu.Lock()
|
||||
@@ -484,6 +504,8 @@ func (c *Conversation) Reset() {
|
||||
c.Seq = 0
|
||||
c.CtxUsed = 0
|
||||
c.epoch++
|
||||
c.Generating = false
|
||||
c.cancel = nil
|
||||
c.cond.Broadcast()
|
||||
c.mu.Unlock()
|
||||
c.persist()
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package ajean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Le bouton stop doit VRAIMENT arrêter une commande shell. Elle naissait d'un
|
||||
// contexte indépendant du tour : arrêter la génération ne l'arrêtait pas, et le
|
||||
// tour restait suspendu jusqu'à l'expiration du délai (5 min au maximum).
|
||||
func TestRunShellObeitAuStop(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("commande de veille propre à Unix")
|
||||
}
|
||||
testHome(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
start := time.Now()
|
||||
out := runShell(ctx, "sleep 30", 300)
|
||||
if d := time.Since(start); d > 5*time.Second {
|
||||
t.Fatalf("la commande a survécu à l'annulation (%s) : %q", d, out)
|
||||
}
|
||||
if !strings.Contains(out, "interrompue") {
|
||||
t.Errorf("résultat attendu « interrompue », reçu %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Une commande qui laisse un process en arrière-plan garde les tubes de sortie
|
||||
// ouverts. Sans WaitDelay, Wait attend leur fermeture — donc pour toujours — et
|
||||
// le tour ne se terminait JAMAIS (seul un redémarrage du service débloquait).
|
||||
func TestRunShellNeBloquePasSurUnProcessDetache(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("syntaxe de détachement propre à Unix")
|
||||
}
|
||||
testHome(t)
|
||||
start := time.Now()
|
||||
out := runShell(context.Background(), "sleep 30 & echo lance", 2)
|
||||
if d := time.Since(start); d > 10*time.Second {
|
||||
t.Fatalf("runShell est resté accroché aux tubes du process détaché (%s)", d)
|
||||
}
|
||||
if !strings.Contains(out, "lance") && !strings.Contains(out, "timeout") {
|
||||
t.Errorf("sortie inattendue : %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Le dossier de travail est résolu une fois par process : s'il disparaît, plus
|
||||
// AUCUNE commande ne passait (« chdir : no such file or directory ») jusqu'au
|
||||
// redémarrage du service. Il doit être recréé au besoin.
|
||||
func TestRunShellRecreeLeWorkspaceDisparu(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("echo se comporte différemment sous cmd.exe")
|
||||
}
|
||||
testHome(t)
|
||||
ws := agentWorkspace()
|
||||
if ws == "" {
|
||||
t.Skip("pas de workspace résolu")
|
||||
}
|
||||
if err := os.RemoveAll(ws); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := runShell(context.Background(), "echo vivant", 10)
|
||||
if !strings.Contains(out, "vivant") {
|
||||
t.Fatalf("commande cassée par la disparition du workspace : %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset doit TOUJOURS rendre la main, même sur un tour resté coincé : c'est le
|
||||
// geste que l'on tente quand le chat est bloqué. Avant, Generating restait vrai
|
||||
// et tout envoi suivant était refusé « génération en cours » jusqu'au
|
||||
// redémarrage du service.
|
||||
func TestResetDebloqueUnTourCoince(t *testing.T) {
|
||||
c := newTestConv()
|
||||
// Simule un tour parti et jamais terminé (commande accrochée, moteur disparu).
|
||||
c.mu.Lock()
|
||||
c.Generating = true
|
||||
c.mu.Unlock()
|
||||
|
||||
c.Reset()
|
||||
|
||||
if st := c.state(); st["generating"] != false {
|
||||
t.Fatal("après Reset, la conversation doit être déclarée libre")
|
||||
}
|
||||
}
|
||||
|
||||
// Le tour abandonné se termine parfois APRÈS le démarrage du suivant. Sa fin ne
|
||||
// doit pas déclarer « libre » une génération toute neuve.
|
||||
func TestTourPerimeNeLiberePasLeTourCourant(t *testing.T) {
|
||||
c := newTestConv()
|
||||
oldEpoch := c.epoch
|
||||
c.Reset() // le tour d'epoch `oldEpoch` devient périmé
|
||||
c.mu.Lock()
|
||||
c.Generating = true // un nouveau tour démarre
|
||||
c.mu.Unlock()
|
||||
|
||||
// Fin (tardive) du tour périmé : même code que le defer de generate.
|
||||
c.mu.Lock()
|
||||
if c.epoch == oldEpoch {
|
||||
c.Generating = false
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if st := c.state(); st["generating"] != true {
|
||||
t.Fatal("un tour périmé a libéré le tour courant")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package ajean
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -135,25 +136,52 @@ func machineSystemPrompt(caps Caps) string {
|
||||
// on Windows — see newShellCmd in sys_platform_*.go) with a clamped timeout,
|
||||
// returning a single string formatted "exit: N\n\nstdout:\n...\n\nstderr:\n..."
|
||||
// truncated to keep tool output bounded.
|
||||
func runShell(command string, timeoutSec int) string {
|
||||
//
|
||||
// ⚠️ parent est le contexte DU TOUR : c'est lui qui rend le bouton stop utile.
|
||||
// La commande naissait auparavant d'un context.Background(), donc arrêter la
|
||||
// génération n'arrêtait rien du tout — le tour restait bloqué jusqu'au bout du
|
||||
// délai (5 minutes au maximum), bouton stop sans effet.
|
||||
func runShell(parent context.Context, command string, timeoutSec int) string {
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = toolDefaultTimeout
|
||||
}
|
||||
if timeoutSec > toolMaxTimeout {
|
||||
timeoutSec = toolMaxTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)
|
||||
ctx, cancel := context.WithTimeout(parent, time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
cmd := newShellCmd(ctx, command)
|
||||
// Le shell démarre dans le workspace, pas dans le dossier d'où ajean a été
|
||||
// lancé : un `> notes.txt` du modèle ne doit pas atterrir sur le Bureau.
|
||||
cmd.Dir = agentWorkspace()
|
||||
//
|
||||
// Le dossier est résolu UNE fois par process (agentWorkspace), donc s'il
|
||||
// disparaît ensuite — l'utilisateur fait le ménage, ou le modèle lui-même le
|
||||
// supprime — toutes les commandes suivantes échouaient sur un « chdir : no
|
||||
// such file or directory » incompréhensible, et ce jusqu'au redémarrage. On
|
||||
// le recrée au besoin, et à défaut on démarre là où on peut plutôt que de
|
||||
// tout refuser.
|
||||
if ws := agentWorkspace(); ws != "" {
|
||||
if err := os.MkdirAll(ws, 0o755); err == nil {
|
||||
cmd.Dir = ws
|
||||
}
|
||||
}
|
||||
var stdout, stderr strings.Builder
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
// ⚠️ WaitDelay borne l'attente APRÈS la fin (ou la mise à mort) du process.
|
||||
// Sans elle, Wait attend que les tubes de sortie soient fermés — donc que
|
||||
// TOUS ceux qui les tiennent aient disparu, petits-enfants compris. Une
|
||||
// commande du genre « ./serveur & » rend la main tout de suite mais laisse
|
||||
// un process en arrière-plan accroché aux tubes : runShell ne revenait alors
|
||||
// JAMAIS, ni au délai, ni au stop. Le tour restait bloqué à vie, et la seule
|
||||
// issue connue était de redémarrer ajean-ui.
|
||||
cmd.WaitDelay = 2 * time.Second
|
||||
err := cmd.Run()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
switch {
|
||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||
return fmt.Sprintf("[timeout après %ds]", timeoutSec)
|
||||
case errors.Is(parent.Err(), context.Canceled):
|
||||
return "[commande interrompue]"
|
||||
}
|
||||
exit := 0
|
||||
if err != nil {
|
||||
|
||||
@@ -874,6 +874,12 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
extra = append(extra, assistant)
|
||||
// 2. Execute each tool locally and append a "tool" reply.
|
||||
for _, tc := range tcs {
|
||||
// Arrêt demandé : on n'enchaîne pas les outils restants. Sans ce
|
||||
// garde, un stop pendant une série d'appels laissait défiler toute
|
||||
// la série avant de reprendre la main.
|
||||
if ctx.Err() != nil {
|
||||
return extra, nil
|
||||
}
|
||||
var args map[string]any
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
|
||||
// Derive the human label (command / skill name) up front so we can
|
||||
@@ -989,7 +995,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
case int:
|
||||
to = v
|
||||
}
|
||||
result = runShell(label, to)
|
||||
result = runShell(ctx, label, to)
|
||||
case "web_search":
|
||||
result = capWebOutput(toolWebSearch(args))
|
||||
case "web_open":
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.8.2"
|
||||
const Version = "0.8.3"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -1658,8 +1658,8 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
<span class="pe-row-c"><input id="s-spec-n" class="pe-val" type="number" min="1" step="1" placeholder="3" oninput="eaSetValued('--spec-draft-n-max', this.value)"></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'off')"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Flash attention<span class="pe-sub">plus rapide, moins de VRAM</span></span>
|
||||
@@ -3158,7 +3158,19 @@ function populateSettings(){
|
||||
set('s-spec-n', eaGetValued('--spec-draft-n-max'));
|
||||
setSpecType(eaGetValued('--spec-type'));
|
||||
const chk = (id,v)=>{ const e=document.getElementById(id); if(e) e.checked=v; };
|
||||
chk('s-reasoning', /^(on|1|true|auto|deepseek)$/i.test(cfgReadKey('REASONING')));
|
||||
// Raisonnement : trois états dans le fichier, deux positions sur l'interrupteur.
|
||||
// « off » est une interdiction explicite passée au moteur ; la clé ABSENTE, elle,
|
||||
// laisse le moteur suivre le gabarit du modèle — donc un modèle à raisonnement
|
||||
// réfléchit quand même. L'interrupteur ne peut pas montrer cette nuance, le
|
||||
// sous-titre la dit. (Les presets créés ou modifiés depuis cette version
|
||||
// écrivent toujours on ou off, la question ne se pose plus pour eux.)
|
||||
const rz = cfgReadKey('REASONING');
|
||||
chk('s-reasoning', /^(on|1|true|auto|deepseek)$/i.test(rz));
|
||||
const rzSub = document.getElementById('s-reasoning-sub');
|
||||
if(rzSub){
|
||||
rzSub.textContent = rz ? 'réflexion étape par étape'
|
||||
: 'réflexion étape par étape — non précisé : le modèle décide';
|
||||
}
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mlock', eaHasFlag('--mlock'));
|
||||
chk('s-nommap', eaHasFlag('--no-mmap'));
|
||||
|
||||
@@ -611,8 +611,8 @@
|
||||
<span class="pe-row-c"><input id="s-spec-n" class="pe-val" type="number" min="1" step="1" placeholder="3" oninput="eaSetValued('--spec-draft-n-max', this.value)"></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'off')"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Flash attention<span class="pe-sub">plus rapide, moins de VRAM</span></span>
|
||||
|
||||
@@ -666,7 +666,19 @@ function populateSettings(){
|
||||
set('s-spec-n', eaGetValued('--spec-draft-n-max'));
|
||||
setSpecType(eaGetValued('--spec-type'));
|
||||
const chk = (id,v)=>{ const e=document.getElementById(id); if(e) e.checked=v; };
|
||||
chk('s-reasoning', /^(on|1|true|auto|deepseek)$/i.test(cfgReadKey('REASONING')));
|
||||
// Raisonnement : trois états dans le fichier, deux positions sur l'interrupteur.
|
||||
// « off » est une interdiction explicite passée au moteur ; la clé ABSENTE, elle,
|
||||
// laisse le moteur suivre le gabarit du modèle — donc un modèle à raisonnement
|
||||
// réfléchit quand même. L'interrupteur ne peut pas montrer cette nuance, le
|
||||
// sous-titre la dit. (Les presets créés ou modifiés depuis cette version
|
||||
// écrivent toujours on ou off, la question ne se pose plus pour eux.)
|
||||
const rz = cfgReadKey('REASONING');
|
||||
chk('s-reasoning', /^(on|1|true|auto|deepseek)$/i.test(rz));
|
||||
const rzSub = document.getElementById('s-reasoning-sub');
|
||||
if(rzSub){
|
||||
rzSub.textContent = rz ? 'réflexion étape par étape'
|
||||
: 'réflexion étape par étape — non précisé : le modèle décide';
|
||||
}
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mlock', eaHasFlag('--mlock'));
|
||||
chk('s-nommap', eaHasFlag('--no-mmap'));
|
||||
|
||||
Reference in new issue
Block a user