mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Lot 31 : slots du moteur pour le tour tenu par la façade
SIDE_SLOT et SLOT_PERSIST ne gardent plus la conversation au Go. La machinerie reste au Go (configuration, requêtes en vol, tampon du slot) ; la façade la consulte avant chaque requête au moteur local. Go : - /internal/v1/turn/slot (facade_slots.go) rend engineSlotFor et, pour une étape de tour, recharge d'abord l'état gardé de la discussion (slotPersistRestore). La sauvegarde à la bascule voit les étapes de la façade : /internal/v1/oai/served marque le slot (engineMarkMain). - facadeChatBlocker : SIDE_SLOT et SLOT_PERSIST retirés. Rust : - TurnHost::engine_slot : l'id_slot posé dans chaque requête (0 pour le fil de la discussion, 1 pour la vérification), le ticket « en vol » ouvert sur le second slot. - Cache KV plein en plein calcul (sideSlotPoolError) : rejoué deux fois tel quel, sans compaction, puis l'erreur. - Résumés de compaction comptés en vol comme postSummary : transcription sur le second slot, continuation sur le slot 0. - La passe de vérification est un travail annexe (engineSideJob) : le Go efface le slot à la fin s'il a servi (écart du lot 7 levé). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
14 files changed
+679
-58
No files matched your search
@@ -48,19 +48,17 @@ func facadeChat() bool {
|
||||
|
||||
// facadeChatBlocker : le réglage qui garde la conversation ici, "" s'il n'y
|
||||
// en a pas. Ces clés opt-in demandent une machinerie restée au Go (bloc projet
|
||||
// figé, images gardées, slots du moteur, préchauffage). NUDGE_IN_TOOL n'en est
|
||||
// plus (lot 23) : la façade a sa sonde de gabarit ; ni les motifs de relance
|
||||
// personnalisés (retry_patterns, lot 30) : la façade les lit et les compile.
|
||||
// figé, images gardées, préchauffage). NUDGE_IN_TOOL n'en est plus (lot 23) :
|
||||
// la façade a sa sonde de gabarit ; ni les motifs de relance personnalisés
|
||||
// (retry_patterns, lot 30) : la façade les lit et les compile ; ni SIDE_SLOT et
|
||||
// SLOT_PERSIST (lot 31) : la façade demande son slot avant chaque requête
|
||||
// (facade_slots.go).
|
||||
func facadeChatBlocker(cfg map[string]string) string {
|
||||
switch {
|
||||
case projSnapEnabled(cfg):
|
||||
return "PROJ_SNAPSHOT"
|
||||
case keepTurnImagesOn(cfg):
|
||||
return "KEEP_TURN_IMAGES"
|
||||
case sideSlotOn(cfg):
|
||||
return "SIDE_SLOT"
|
||||
case slotPersistOn(cfg):
|
||||
return "SLOT_PERSIST"
|
||||
case prewarmMode(cfg) != "":
|
||||
return "PREWARM"
|
||||
}
|
||||
|
||||
@@ -298,8 +298,8 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
|
||||
t.Fatalf("remis à la façade malgré le réglage : %q %v", rec.Body.String(), f.recu())
|
||||
}
|
||||
for k, want := range map[string]string{
|
||||
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "", "SIDE_SLOT": "SIDE_SLOT",
|
||||
"SLOT_PERSIST": "SLOT_PERSIST", "PREWARM": "PREWARM",
|
||||
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "", "SIDE_SLOT": "",
|
||||
"SLOT_PERSIST": "", "PREWARM": "PREWARM",
|
||||
} {
|
||||
if got := facadeChatBlocker(map[string]string{k: "on"}); got != want {
|
||||
t.Errorf("%s : %q", k, got)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// facade_slots.go — les slots du moteur pour le tour tenu par la façade (lot 31
|
||||
// de la migration). La machinerie reste ici, où vivent la configuration, le
|
||||
// compteur des requêtes en vol et le tampon du slot de la discussion
|
||||
// (llm_slots.go) ; la façade demande, avant chaque requête au moteur local,
|
||||
// le slot où l'envoyer.
|
||||
//
|
||||
// - SIDE_SLOT (llm_sideslot.go) : le fil de la discussion sur le slot 0, le
|
||||
// reste (vérification, résumé sur transcription) sur le 1, tant que le
|
||||
// second slot est en service ; sinon aucun id_slot.
|
||||
// - SLOT_PERSIST (llm_slotpersist.go) : l'état gardé à la dernière bascule de
|
||||
// preset est rechargé avant la première requête de la discussion, si tout
|
||||
// concorde. La sauvegarde, elle, se fait à la bascule (déjà ici) : le slot
|
||||
// porte la discussion dès que le moteur a accepté une étape de son tour
|
||||
// (/internal/v1/oai/served, engineMarkMain).
|
||||
|
||||
// handleInternalTurnSlot : POST {"conv":"…","main":true} → {"slot":0|1|-1}.
|
||||
// main : requête du fil de la discussion (étape d'un tour, résumé en
|
||||
// continuation) ; conv : la discussion d'une étape de tour, pour le
|
||||
// rechargement SLOT_PERSIST — vide pour un résumé. -1 : aucun id_slot.
|
||||
func handleInternalTurnSlot(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Conv string `json:"conv"`
|
||||
Main bool `json:"main"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
ep := resolveChatEndpoint()
|
||||
if in.Main && in.Conv != "" {
|
||||
slotPersistRestore(ep, in.Conv)
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"slot": engineSlotFor(ep, in.Main)})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Le slot d'une requête de la façade : aucun sans la clé (et /props jamais
|
||||
// lu), le fil de la discussion sur le 0 et le reste sur le 1 quand le second
|
||||
// slot est en service.
|
||||
func TestFacadeTurnSlot(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
slot := func(body string) (int, float64) {
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/turn/slot", body)
|
||||
var out struct {
|
||||
Slot float64 `json:"slot"`
|
||||
}
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &out)
|
||||
return w.Code, out.Slot
|
||||
}
|
||||
prev := sideSlotProps
|
||||
sideSlotProps = func(chatEndpoint) (int, int, bool) { t.Fatal("/props lu sans la clé"); return 0, 0, false }
|
||||
for _, b := range []string{`{"conv":"c1","main":true}`, `{"main":false}`} {
|
||||
if code, s := slot(b); code != 200 || s != -1 {
|
||||
t.Fatalf("sans la clé %s : %d %v", b, code, s)
|
||||
}
|
||||
}
|
||||
sideSlotProps = prev
|
||||
if err := SetConfigKey("SIDE_SLOT", "on"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SetConfigKey("CTX", "8192"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stubSideProps(t, 2, 8192)
|
||||
if _, s := slot(`{"conv":"c1","main":true}`); s != 0 {
|
||||
t.Fatalf("étape du tour : slot %v", s)
|
||||
}
|
||||
if _, s := slot(`{"main":false}`); s != 1 {
|
||||
t.Fatalf("vérification : slot %v", s)
|
||||
}
|
||||
if _, s := slot(`{"main":true}`); s != 0 {
|
||||
t.Fatalf("résumé en continuation : slot %v", s)
|
||||
}
|
||||
// Second slot hors service : plus d'id_slot.
|
||||
stubSideProps(t, 1, 8192)
|
||||
if _, s := slot(`{"main":false}`); s != -1 {
|
||||
t.Fatalf("un seul slot : %v", s)
|
||||
}
|
||||
if code, _ := slot(`{`); code != 400 {
|
||||
t.Fatalf("corps illisible : %d", code)
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/turn/slot", ""); w.Code != 405 {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// SLOT_PERSIST : l'état gardé à la bascule est rechargé avant la première
|
||||
// étape du tour de CETTE discussion, une seule fois ; jamais pour une autre
|
||||
// discussion, une requête annexe ou un résumé.
|
||||
func TestFacadeTurnSlotRecharge(t *testing.T) {
|
||||
m, home := persistSetup(t, true)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
snap := slotPersistPrepare()
|
||||
if snap == nil {
|
||||
t.Fatal("rien à garder")
|
||||
}
|
||||
snap.save()
|
||||
name := slotPersistName(persistTestKey, "c1")
|
||||
for _, b := range []string{
|
||||
`{"conv":"autre","main":true}`,
|
||||
`{"conv":"c1","main":false}`,
|
||||
`{"main":true}`,
|
||||
`{"conv":"c1","main":true}`,
|
||||
`{"conv":"c1","main":true}`,
|
||||
} {
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/slot", b); w.Code != 200 {
|
||||
t.Fatalf("%s : %d", b, w.Code)
|
||||
}
|
||||
}
|
||||
want := []string{"save tmp-" + name, "restore " + name}
|
||||
if got := m.got(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("appels %v, attendu %v", got, want)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, "slots", name)); !os.IsNotExist(err) {
|
||||
t.Fatal("fichier gardé après la tentative")
|
||||
}
|
||||
}
|
||||
|
||||
// Une étape de tour de la façade acceptée par le moteur marque le slot :
|
||||
// c'est ce que la sauvegarde SLOT_PERSIST relit à la bascule.
|
||||
func TestFacadeTurnSlotSauvegarde(t *testing.T) {
|
||||
m, home := persistSetup(t, true)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
engineGate.mu.Lock()
|
||||
engineGate.main = engineMainStamp{}
|
||||
engineGate.mu.Unlock()
|
||||
if slotPersistPrepare() != nil {
|
||||
t.Fatal("slot sans discussion : rien à garder")
|
||||
}
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/oai/begin", `{"main_conv":"c2"}`)
|
||||
var b struct {
|
||||
Ticket uint64 `json:"ticket"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &b); err != nil || b.Ticket == 0 {
|
||||
t.Fatalf("begin : %s", w.Body.String())
|
||||
}
|
||||
tk := `{"ticket":` + jsonNum(b.Ticket) + `}`
|
||||
appelOAIInterne(t, "POST", "/internal/v1/oai/served", tk)
|
||||
appelOAIInterne(t, "POST", "/internal/v1/oai/end", tk)
|
||||
snap := slotPersistPrepare()
|
||||
if snap == nil {
|
||||
t.Fatal("étape de la façade non vue par la sauvegarde")
|
||||
}
|
||||
snap.save()
|
||||
name := slotPersistName(persistTestKey, "c2")
|
||||
if _, err := os.Stat(filepath.Join(home, "slots", name)); err != nil {
|
||||
t.Fatalf("état non gardé : %v (%v)", err, m.got())
|
||||
}
|
||||
}
|
||||
|
||||
func jsonNum(n uint64) string {
|
||||
b, _ := json.Marshal(n)
|
||||
return string(b)
|
||||
}
|
||||
@@ -73,6 +73,8 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalOAIServed(w, r)
|
||||
case "/internal/v1/turn/perf":
|
||||
handleInternalTurnPerf(w, r)
|
||||
case "/internal/v1/turn/slot":
|
||||
handleInternalTurnSlot(w, r)
|
||||
case "/internal/v1/tool/exec":
|
||||
handleInternalToolExec(w, r)
|
||||
case "/internal/v1/tool/context":
|
||||
|
||||
Reference in new issue
Block a user