mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Cache : corrections de relecture du lot 2 — KEEP_TURN_IMAGES retire les images avant tout résumé, partout
Le README promet que les images gardées partent d'abord et que le résumé (avec perte) ne suit que s'il reste nécessaire. C'était vrai en début de tour et entre deux étapes, pas ailleurs : la compaction de fin de tour, le filet réactif (prompt refusé) et la fenêtre pleine en génération retiraient les images ET résumaient d'un même geste, alors que le retrait seul suffisait. - Fin de tour : même retrait que le début de tour, puis le seuil est relu. - Prompt refusé pour débordement : images retirées, requête rejouée sans elles, avant la compaction de secours. - finish=length : images retirées d'abord ; l'étape est rejouée sans résumé si le contexte libéré suffit. Sans la clé, aucune image gardée : chemins d'avant à l'identique. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
78d15cefbf
commit
39487f9449
3 files changed
+94
No files matched your search
@@ -1033,6 +1033,12 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
|||||||
// retombe tout de suite et le tour suivant démarre avec de la marge.
|
// retombe tout de suite et le tour suivant démarre avec de la marge.
|
||||||
// Sauf si un Reset est passé (rien à compacter) ou si le tour a été annulé
|
// Sauf si un Reset est passé (rien à compacter) ou si le tour a été annulé
|
||||||
// (bouton stop) : on n'enchaîne pas plusieurs secondes de résumé sur un stop.
|
// (bouton stop) : on n'enchaîne pas plusieurs secondes de résumé sur un stop.
|
||||||
|
// KEEP_TURN_IMAGES : comme en début de tour, les images gardées partent
|
||||||
|
// d'abord si le contexte le demande, et le résumé ne suit que s'il reste
|
||||||
|
// nécessaire. Sans la clé, aucune image gardée : rien ne change.
|
||||||
|
if !stale && ctx.Err() == nil {
|
||||||
|
msgs, ctxUsed = c.keptImagesTurnStart(epoch, msgs, ctxUsed, peak)
|
||||||
|
}
|
||||||
if stale || ctx.Err() != nil || !compactNeeded(msgs, ctxUsed, peak) || compactRefusedSkip(conv, ctxUsed, msgs) {
|
if stale || ctx.Err() != nil || !compactNeeded(msgs, ctxUsed, peak) || compactRefusedSkip(conv, ctxUsed, msgs) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,3 +541,67 @@ func TestKeepImagesBudgetPleinAvantRangement(t *testing.T) {
|
|||||||
t.Fatal("image rangée alors que le budget était plein")
|
t.Fatal("image rangée alors que le budget était plein")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Corrections de relecture du lot 2 : prompt refusé pour débordement alors que
|
||||||
|
// des images sont gardées — elles partent d'abord et la requête est rejouée
|
||||||
|
// sans elles, sans résumé (avec perte) tant que ça suffit.
|
||||||
|
func TestKeepImagesRetireesAvantLeFiletReactif(t *testing.T) {
|
||||||
|
withWorkspace(t)
|
||||||
|
for k, v := range map[string]string{"KEEP_TURN_IMAGES": "on", "CTX": "2000"} {
|
||||||
|
if err := SetConfigKey(k, v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mu sync.Mutex
|
||||||
|
var bodies []map[string]any
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/props" {
|
||||||
|
_, _ = w.Write([]byte(`{"modalities":{"vision":true}}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
mu.Lock()
|
||||||
|
bodies = append(bodies, body)
|
||||||
|
n := len(bodies)
|
||||||
|
mu.Unlock()
|
||||||
|
if n == 1 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte(`{"error":{"code":400,"message":"request (2500 tokens) exceeds the available context size (2000 tokens), try increasing it"}}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
_, _ = w.Write([]byte(sseAnswer("fini", 0)))
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
u, _ := url.Parse(srv.URL)
|
||||||
|
for k, v := range map[string]string{"PORT": u.Port(), "MMPROJ": "/models/mmproj.gguf"} {
|
||||||
|
if err := SetConfigKey(k, v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visionProbeMu.Lock()
|
||||||
|
visionProbeAt = time.Time{}
|
||||||
|
visionProbeMu.Unlock()
|
||||||
|
hist := []Message{um("q"), atc("see_image"), tm("[ok]"), keptRelay(1000), am("vu"), um("suite")}
|
||||||
|
var published []Message
|
||||||
|
if _, err := runChat(mainCtx(t), hist, 0.7, Caps{Agent: true}, func(ev StreamEvent) bool {
|
||||||
|
if ev.NewHistory != nil {
|
||||||
|
published = ev.NewHistory
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(bodies) != 2 {
|
||||||
|
t.Fatalf("%d requêtes : un résumé est parti alors que retirer l'image suffisait", len(bodies))
|
||||||
|
}
|
||||||
|
if published == nil || hasKeptImages(published) || len(published) != len(hist) {
|
||||||
|
t.Fatalf("historique publié : %+v", published)
|
||||||
|
}
|
||||||
|
if relay := reqMessages(t, bodies[1])[3]; msgText(relay) != "Voici la capture demandée."+imageLostMarker {
|
||||||
|
t.Fatalf("image encore envoyée après son retrait : %+v", relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1560,6 +1560,18 @@ func runChatLoop(ctx context.Context, kt *keepImages, messages []Message, tools
|
|||||||
if echoOffTurn {
|
if echoOffTurn {
|
||||||
messages = stripReasoning(messages)
|
messages = stripReasoning(messages)
|
||||||
}
|
}
|
||||||
|
// KEEP_TURN_IMAGES : les images gardées partent d'abord, et la requête
|
||||||
|
// est rejouée sans elles ; le résumé (avec perte) ne vient qu'ensuite,
|
||||||
|
// s'il le faut encore. Sans la clé, aucune image gardée.
|
||||||
|
if contextOverflow(msg, messages) && hasKeptImages(messages) {
|
||||||
|
if s, freed := dropKeptImages(messages); freed > 0 {
|
||||||
|
logKeptImagesDropped("réactif", freed)
|
||||||
|
messages = s
|
||||||
|
extra = nil
|
||||||
|
publishHistory()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if compactEnabled() && !compactedRetry && contextOverflow(msg, messages) {
|
if compactEnabled() && !compactedRetry && contextOverflow(msg, messages) {
|
||||||
if c, changed := compactMessages(ctx, messages, caps); changed {
|
if c, changed := compactMessages(ctx, messages, caps); changed {
|
||||||
compactedRetry = true
|
compactedRetry = true
|
||||||
@@ -2646,6 +2658,18 @@ func runChatLoop(ctx context.Context, kt *keepImages, messages []Message, tools
|
|||||||
if finishReason == "length" && !ep.External && lengthReplays < 1 &&
|
if finishReason == "length" && !ep.External && lengthReplays < 1 &&
|
||||||
compactWouldTrigger(messages, stats.PromptTokensTotal+stats.GenTokens) {
|
compactWouldTrigger(messages, stats.PromptTokensTotal+stats.GenTokens) {
|
||||||
lengthReplays++
|
lengthReplays++
|
||||||
|
// KEEP_TURN_IMAGES : les images gardées d'abord ; le résumé ne suit
|
||||||
|
// que s'il reste nécessaire. Sans la clé, rien à retirer.
|
||||||
|
if s, freed := dropKeptImages(messages); freed > 0 {
|
||||||
|
logKeptImagesDropped("fenêtre-pleine", freed)
|
||||||
|
messages = s
|
||||||
|
extra = nil
|
||||||
|
publishHistory()
|
||||||
|
if !compactWouldTrigger(messages, max(stats.PromptTokensTotal+stats.GenTokens-freed, 1)) {
|
||||||
|
cb(StreamEvent{DropReasoning: true})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
yes, no := true, false
|
yes, no := true, false
|
||||||
cb(StreamEvent{Compacting: &yes})
|
cb(StreamEvent{Compacting: &yes})
|
||||||
c, changed := compactMessages(ctx, messages, caps)
|
c, changed := compactMessages(ctx, messages, caps)
|
||||||
|
|||||||
Reference in new issue
Block a user