Slots : corrections de relecture du lot 2 — une lecture d'un client ne prend plus le slot

Les proxys de Loki (/v1 et le relais) comptaient TOUTE requête d'un client
comme une requête qui passe par le slot : un GET /v1/models ou une sonde
/health (Open WebUI en envoie régulièrement) annulait le préchauffage en vol
et retirait le tampon de la discussion. Avec PREWARM, COMPACT_CONTINUATION ou
SLOT_PERSIST, la clé devenait inopérante dès qu'un client sondait le moteur.

- engineProxyBegin : une lecture (GET, HEAD) reste comptée en vol, comme
  avant, mais n'avance pas le numéro des requêtes et garde le préchauffage ;
  une complétion de client garde le comportement d'avant.
- README : avec PREWARM, SLOT_PERSIST ne garde presque rien (le préchauffage
  passe par le slot après chaque tour) — dit plutôt que découvert.

Sans aucune de ces clés, rien ne change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 12:26:42 +02:00
1 parent a94c752529
commit f0d5711a1a
5 files changed
+65 -4

No files matched your search

+44
View File
@@ -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")
}
}
+1 -2
View File
@@ -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/") {
+16
View File
@@ -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.
+1 -2
View File
@@ -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
}