mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Le fork est parti de la v0.9.4 ; l'amont en est à la v0.10.7. Reprise de ce qui manque VRAIMENT ici, en laissant de côté ce que loki a déjà résolu à sa façon (contexte MTP via --parallel 1, jauge de contexte, chrono de tour, vignettes d'images, ligne d'état de génération). Rendu du chat cadencé puis lissé (amont v0.9.5 issue #24, v0.10.5). Chaque token re-parsait le Markdown du bloc ENTIER : du O(n²) qui faisait ramer l'interface sur un long raisonnement — le moteur débitait toujours autant, mais les tokens semblaient arriver au ralenti et un simple rafraîchissement « réparait » tout. Le texte s'accumule désormais et n'est re-rendu qu'à intervalle adaptatif (16 ms sur un petit bloc, jusqu'à 500 ms sur un énorme), soldé à chaque frontière (outil, bascule de rôle, fin de tour, erreur, rejeu). Par-dessus, un lissage d'apparition découple l'arrivée de l'affichage : le décodage spéculatif rend les tokens par rafales, le texte sautait par paquets ; il s'écoule maintenant à cadence régulière. Rejeu exclu — relire un fil ne doit pas être une lente réécriture. Échantillonnage réglable par preset (amont v0.9.5/v0.9.6) : TEMP, TOP_P, TOP_K, MIN_P, PRESENCE_PENALTY, REPEAT_PENALTY, injectés dans chaque requête (donc sans redémarrage du moteur), vide = défaut du serveur. Sans ça seule la température voyageait et le reste retombait sur les défauts de llama.cpp, rarement ceux que recommande le modèle. Différence avec l'amont : REASONING_EFFORT n'est PAS traité là — loki lui réserve un chemin plus riche, et l'écrire ici écraserait `chat_template_kwargs`, donc la consigne « aucune ». Un seul message système, en tête, à l'envoi (amont v0.9.8, issue #26). steerSystem ne couvrait que les consignes de loki ; un historique venu d'ailleurs peut encore en porter deux, et Qwen3.x en --jinja répond alors « System message must be at the beginning ». Copie normalisée : l'historique affiché et persisté garde sa forme. Détection Vulkan multi-distro (amont issues #28, #29) : le chemin Debian codé en dur est invisible sur Fedora/RHEL/Atomic, où le plan de build retombait sur le CPU. ldconfig d'abord, puis les chemins connus. Dossier de travail (amont v0.10.2) : la consigne dit maintenant ce que le dossier EST — l'endroit par défaut de tout ce que le modèle produit — et nomme les dossiers système à ne pas toucher, au lieu d'interdire vaguement d'en sortir. Non repris : les tâches planifiées (~1200 lignes + interface, à décider), et le quoting cmd.exe par .bat temporaire (loki tourne en conteneur Linux).
106 lines
3.8 KiB
Go
106 lines
3.8 KiB
Go
package loki
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// Repris de l'amont AJEAN (v0.9.8) avec le correctif : les tests valent tels
|
|
// quels, la fonction étant portée à l'identique.
|
|
//
|
|
// LE test qui rassure : sur une conversation NORMALE (un seul system en tête,
|
|
// suivi de user/assistant/tool, appels d'outils et contenu multimodal compris),
|
|
// la normalisation ne doit RIEN changer — même contenu, même ordre. C'est la
|
|
// garantie que le fix est invisible pour les 99 % de cas qui marchaient déjà.
|
|
func TestNormalizeSystemMessagesNoOpOnNormalConversation(t *testing.T) {
|
|
in := []Message{
|
|
{Role: "system", Content: "Tu es Loki."},
|
|
{Role: "user", Content: "cherche la météo"},
|
|
{Role: "assistant", Content: "", ToolCalls: []ToolCall{{ID: "call_1", Type: "function"}}},
|
|
{Role: "tool", Content: "résultat brut", ToolCallID: "call_1"},
|
|
{Role: "assistant", Content: "Il fait beau."},
|
|
// contenu multimodal (image) : Content n'est pas une string
|
|
{Role: "user", Content: []any{map[string]any{"type": "text", "text": "et demain ?"}}},
|
|
}
|
|
// copie profonde de référence pour détecter toute mutation de l'entrée
|
|
ref := append([]Message(nil), in...)
|
|
out := normalizeSystemMessages(in)
|
|
if !reflect.DeepEqual(out, in) {
|
|
t.Errorf("séquence normale modifiée !\navant: %#v\naprès: %#v", in, out)
|
|
}
|
|
if !reflect.DeepEqual(in, ref) {
|
|
t.Errorf("l'entrée a été mutée (interdit) : %#v", in)
|
|
}
|
|
}
|
|
|
|
// L'ordre relatif de tous les messages NON-system (et le lien assistant→tool via
|
|
// ToolCallID) doit être préservé quand on ramène un system égaré en tête.
|
|
func TestNormalizeSystemMessagesPreservesOrderAndToolLink(t *testing.T) {
|
|
in := []Message{
|
|
{Role: "system", Content: "S1"},
|
|
{Role: "user", Content: "q"},
|
|
{Role: "assistant", Content: "", ToolCalls: []ToolCall{{ID: "call_9"}}},
|
|
{Role: "tool", Content: "r", ToolCallID: "call_9"},
|
|
{Role: "system", Content: "n'appelle plus d'outil"}, // égaré, en fin
|
|
}
|
|
out := normalizeSystemMessages(in)
|
|
want := []Message{
|
|
{Role: "system", Content: "S1\n\nn'appelle plus d'outil"},
|
|
{Role: "user", Content: "q"},
|
|
{Role: "assistant", Content: "", ToolCalls: []ToolCall{{ID: "call_9"}}},
|
|
{Role: "tool", Content: "r", ToolCallID: "call_9"},
|
|
}
|
|
if !reflect.DeepEqual(out, want) {
|
|
t.Errorf("ordre/lien outil cassé :\n%#v", out)
|
|
}
|
|
}
|
|
|
|
// Un system inséré en fin de séquence (relance anti-boucle) ou en double doit
|
|
// être ramené en un seul bloc, en tête : sinon les gabarits Qwen3.x renvoient
|
|
// « System message must be at the beginning » (issue #26).
|
|
func TestNormalizeSystemMessages(t *testing.T) {
|
|
sys := func(c string) Message { return Message{Role: "system", Content: c} }
|
|
usr := func(c string) Message { return Message{Role: "user", Content: c} }
|
|
|
|
firstRoleSystemOnce := func(t *testing.T, out []Message) {
|
|
count := 0
|
|
for i, m := range out {
|
|
if m.Role == "system" {
|
|
count++
|
|
if i != 0 {
|
|
t.Errorf("system trouvé en position %d, doit être en 0", i)
|
|
}
|
|
}
|
|
}
|
|
if count > 1 {
|
|
t.Errorf("%d messages system, doit être fusionné en un seul", count)
|
|
}
|
|
}
|
|
|
|
t.Run("system en fin", func(t *testing.T) {
|
|
out := normalizeSystemMessages([]Message{sys("A"), usr("q"), sys("stop")})
|
|
firstRoleSystemOnce(t, out)
|
|
if out[0].Content != "A\n\nstop" {
|
|
t.Errorf("fusion attendue 'A\\n\\nstop', obtenu %q", out[0].Content)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("attendu 2 messages, obtenu %d", len(out))
|
|
}
|
|
})
|
|
|
|
t.Run("sans system, inchangé", func(t *testing.T) {
|
|
in := []Message{usr("a"), usr("b")}
|
|
out := normalizeSystemMessages(in)
|
|
if len(out) != 2 || out[0].Role != "user" {
|
|
t.Fatalf("séquence sans system ne doit pas changer : %#v", out)
|
|
}
|
|
})
|
|
|
|
t.Run("system vide ignoré", func(t *testing.T) {
|
|
out := normalizeSystemMessages([]Message{sys(" "), usr("q")})
|
|
if len(out) != 1 || out[0].Role != "user" {
|
|
t.Fatalf("un system vide ne doit rien ajouter : %#v", out)
|
|
}
|
|
})
|
|
}
|