diff --git a/README.md b/README.md index 6815126..5551b44 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,9 @@ Ajoutées par ce fork : exactement les jetons gardés (gabarit qui rend le dernier tour à l'identique), sinon recalcul comme avant. Ce que voit le modèle ne change pas : llama.cpp ne reprend un état que sur un préfixe de jetons identique. + Avec `PREWARM`, le préchauffage passe par le slot après chaque tour : une + bascule faite ensuite ne garde rien (les deux clés se recouvrent peu). Une + simple lecture d'un client (`/v1/models`, `/health`) ne compte pas. - **Spéculation par n-grammes** (clé `SPEC`, valeurs `ngram` et `mtp+ngram`, **off** par défaut, `loki config set SPEC ngram`, moteur relancé) : en mode Code, le modèle recopie sans cesse ce qui est déjà dans le contexte (chemins, diff --git a/internal/loki/chat_compact_cont_test.go b/internal/loki/chat_compact_cont_test.go index 8e98122..b55357c 100644 --- a/internal/loki/chat_compact_cont_test.go +++ b/internal/loki/chat_compact_cont_test.go @@ -634,3 +634,47 @@ func TestBuildChatPayloadNoSampling(t *testing.T) { t.Fatalf("échantillonnage posé malgré noSampling : %v", p) } } + +// Corrections de relecture du lot 2 : une lecture passée par les proxys de +// Loki (GET /v1/models, /health d'un client qui sonde) ne calcule rien dans un +// slot — elle ne retire pas le tampon de la discussion et n'annule pas un +// préchauffage. Une complétion de client, si. +func TestProxyReadKeepsSlotStamp(t *testing.T) { + testHome(t) + contReset(t) + if err := SetConfigKey("MODEL", "/models/a.gguf"); err != nil { + t.Fatal(err) + } + end, seq := engineRequestBegin(nil) + end() + engineMarkMain(seq, "c1") + cancelled := false + p := &prewarmRun{cancel: func() { cancelled = true }} + endP, ok := prewarmBegin(p) + if !ok { + t.Fatal("préchauffage refusé, moteur libre") + } + defer endP() + // Le préchauffage retire lui-même le tampon : reposé à la main, pour voir + // ce que fait la lecture seule. + engineGate.mu.Lock() + engineGate.main.seq = engineGate.seq + engineGate.mu.Unlock() + for _, m := range []string{http.MethodGet, http.MethodHead} { + r := httptest.NewRequest(m, "/v1/models", nil) + engineProxyBegin(r, false)() + } + if cancelled { + t.Error("une lecture a annulé le préchauffage") + } + if !engineSlotHolds("c1") { + t.Error("une lecture a retiré le tampon de la discussion") + } + engineProxyBegin(httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil), false)() + if !cancelled { + t.Error("une complétion de client n'a pas annulé le préchauffage") + } + if engineSlotHolds("c1") { + t.Error("tampon gardé après une complétion de client") + } +} diff --git a/internal/loki/llm_oai.go b/internal/loki/llm_oai.go index c1362c3..f9b05eb 100644 --- a/internal/loki/llm_oai.go +++ b/internal/loki/llm_oai.go @@ -123,8 +123,7 @@ func oaiHandler() http.Handler { // Requête en vol vers le moteur : l'isolation des travaux annexes // n'efface pas le slot pendant ce temps. Différé : ReverseProxy panique // (ErrAbortHandler) quand le client coupe en plein flux. - end, _ := engineRequestBeginSide(nil, side) - defer end() + defer engineProxyBegin(r, side)() // Complétion d'un client externe : elle prend le slot sous le nez de // la conversation. Notée pour nommer la perte de cache qui suit. if r.Method == http.MethodPost && strings.HasPrefix(p, "/v1/") { diff --git a/internal/loki/llm_slots.go b/internal/loki/llm_slots.go index 16f152c..e5f7c79 100644 --- a/internal/loki/llm_slots.go +++ b/internal/loki/llm_slots.go @@ -185,6 +185,22 @@ func engineRequestBeginSide(keep func(*prewarmRun) bool, side bool) (func(), uin }, seq } +// engineProxyBegin : engineRequestBeginSide pour une requête d'un client passée +// par les proxys de Loki (/v1, /health, /props, /metrics, /slots). Comptée en +// vol dans tous les cas, comme avant. Une lecture (GET, HEAD : /v1/models, une +// sonde /health d'Open WebUI…) ne calcule rien dans aucun slot : elle ne doit +// ni annuler un préchauffage (PREWARM), ni dire que le slot ne porte plus la +// discussion (COMPACT_CONTINUATION, SLOT_PERSIST). Sans ces clés, aucune +// différence. +func engineProxyBegin(r *http.Request, side bool) func() { + if r.Method == http.MethodGet || r.Method == http.MethodHead { + end, _ := engineRequestBeginSide(func(*prewarmRun) bool { return true }, true) + return end + } + end, _ := engineRequestBeginSide(nil, side) + return end +} + // slotsWrite : une action sur /slots (save, restore, erase) plutôt qu'une // lecture. Les proxys de Loki la refusent : seul Loki, en local, efface — et // save ou restore écriraient des Gio sur le disque pour un client distant. diff --git a/internal/loki/relay_link.go b/internal/loki/relay_link.go index b30f8eb..85efa84 100644 --- a/internal/loki/relay_link.go +++ b/internal/loki/relay_link.go @@ -530,8 +530,7 @@ func newLinkHandler(mux *http.ServeMux) http.Handler { http.Error(w, msg, status) return } - end, _ := engineRequestBeginSide(nil, side) // voir oaiHandler - defer end() + defer engineProxyBegin(r, side)() // voir oaiHandler lp.ServeHTTP(w, r) return }