mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 07:28:13 +02:00
Passe de qualité : contexte explosé par les captures, sonde vision, courses
Le journal moteur de production a livré la vraie cause première des 500 en cascade : « request (55407 tokens) exceeds the available context size (32768) ». Le message multimodal qui relaie une capture d'écran au modèle était PERSISTÉ dans l'historique ; son base64 (des dizaines de milliers de tokens) repartait à chaque tour, et la conversation dépassait définitivement la fenêtre — plus aucun tour ne passait, et l'exception Jinja du rattrapage (corrigée au commit précédent) masquait tout. - L'image devient ÉPHÉMÈRE : jointe au tour en cours, jamais à l'historique. Le modèle la regarde maintenant ; sa description textuelle, elle, reste. - stripImageParts guérit les conversations déjà empoisonnées au chargement et à la bascule : parties image retirées, texte aplati. - engineSeesImages : l'image n'est envoyée que si llama-server DÉCLARE la vision (/props, modalities.vision, cache 10 s). La clé MMPROJ ne suffit pas — projecteur d'un autre modèle ou modèle sans vision (gpt-oss), le gabarit sérialise le base64 en texte. La description de l'outil suit le même état : ne jamais promettre une image qui n'arrivera pas. Deux défauts trouvés à l'analyse, au passage : - renderBody reconstruit le DOM à chaque delta du streaming : une image déjà affichée était RE-TÉLÉCHARGÉE à chaque token arrivé après elle. Cache de blobs par URL, une seule requête par capture. - les opérations de discussions (création, bascule, renommage, suppression) entrelaçaient leurs lectures-écritures d'index sous requêtes simultanées : sérialisées par un verrou dédié. Tests : historique guéri (aplati sans l'image), transmission conditionnée à la sonde, description alignée ; suite complète, vet, staticcheck verts.
This commit is contained in:
7 files changed
+172
-14
No files matched your search
@@ -87,6 +87,11 @@ func TestConvDeleteSupprimeLesCaptures(t *testing.T) {
|
||||
func TestScreenshotSuitLEtatDeLaVision(t *testing.T) {
|
||||
t.Setenv("LOKI_HOME", t.TempDir())
|
||||
|
||||
// Sans projecteur, la sonde moteur ne doit même pas être consultée : on
|
||||
// s'assure que son cache est froid pour que le test reste hermétique.
|
||||
visionProbeMu.Lock()
|
||||
visionProbeAt, visionProbeSeen = time.Now(), false
|
||||
visionProbeMu.Unlock()
|
||||
if got := screenshotVisionNote(); !strings.Contains(got, "ne vois pas") {
|
||||
t.Fatalf("sans projecteur, la description doit annoncer l'absence de vision : %q", got)
|
||||
}
|
||||
@@ -105,10 +110,16 @@ func TestScreenshotSuitLEtatDeLaVision(t *testing.T) {
|
||||
t.Fatal("image transmise au modèle alors qu'aucun projecteur n'est configuré")
|
||||
}
|
||||
|
||||
// Projecteur configuré → description ET transmission changent.
|
||||
// Projecteur configuré ET moteur qui déclare la vision (on amorce le cache
|
||||
// de la sonde : aucun llama-server ne tourne pendant les tests) →
|
||||
// description ET transmission changent.
|
||||
if err := SetConfigKey("MMPROJ", "mmproj-test.gguf"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
visionProbeMu.Lock()
|
||||
visionProbeAt, visionProbeSeen = time.Now(), true
|
||||
visionProbeMu.Unlock()
|
||||
defer func() { visionProbeMu.Lock(); visionProbeAt = time.Time{}; visionProbeMu.Unlock() }()
|
||||
if got := screenshotVisionNote(); strings.Contains(got, "ne vois pas") {
|
||||
t.Fatalf("avec projecteur, la description ne doit plus nier la vision : %q", got)
|
||||
}
|
||||
@@ -125,6 +136,28 @@ func TestScreenshotSuitLEtatDeLaVision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Un base64 d'image persisté dans l'historique est rejoué à chaque tour : vu en
|
||||
// production, 55 000 tokens de requête pour 32 768 de contexte — plus aucun
|
||||
// tour ne passait. stripImageParts guérit les conversations existantes en
|
||||
// retirant les parties image et en aplatissant le texte restant.
|
||||
func TestStripImagePartsGueritLHistorique(t *testing.T) {
|
||||
msgs := []Message{
|
||||
{Role: "user", Content: "bonjour"}, // simple chaîne : intouchée
|
||||
{Role: "user", Content: []any{ // message multimodal persisté (via JSON)
|
||||
map[string]any{"type": "text", "text": "Voici la capture demandée."},
|
||||
map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:image/jpeg;base64,AAAA"}},
|
||||
}},
|
||||
}
|
||||
out := stripImageParts(msgs)
|
||||
if out[0].Content.(string) != "bonjour" {
|
||||
t.Fatalf("message texte modifié : %#v", out[0].Content)
|
||||
}
|
||||
got, ok := out[1].Content.(string)
|
||||
if !ok || got != "Voici la capture demandée." {
|
||||
t.Fatalf("le message multimodal doit être aplati en texte sans l'image, obtenu %#v", out[1].Content)
|
||||
}
|
||||
}
|
||||
|
||||
// Le chemin de la capture est extrait du texte rendu par l'outil : si le format
|
||||
// de ce texte change, le relais vers le modèle casse en silence.
|
||||
func TestCapturedRelPath(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user