mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 13:38:11 +02:00
Lot 39 : anneau de télémétrie et /api/perf/summary tenus par la façade
L'anneau des complétions (perf_log.go) est porté en Rust (src/perfring.rs) : perte de cache contre la complétion précédente de la même discussion et de la même nature, ce qui s'est intercalé, seuil d'alerte du preset (dans l'ordre du lancement, 2048 sur un modèle hybride), ligne [perf], résumé de /api/perf/summary. ChatShared le porte : tours, vérifications, résumés de compaction et préchauffage de la façade y rangent leurs complétions sans aller-retour au Go. Côté Go, quand la façade tient la conversation, perfRecord range chez elle (/internal/facade/v1/perf/record) et perfSnapshot relit son anneau (optimiseur) ; /api/perf/summary lui est remis. Façade injoignable : l'anneau du Go prend le relais. La route /internal/v1/turn/perf, plus appelée, est retirée. Parité : journal tiré du Go (parity_perf_test.go → tests/parity_perf/ perf.json : 14 complétions, résumé, onze seuils), rejoué par src/perfring/tests.rs ; deux tours qui perdent le cache (tests/lot6.rs), l'API de l'anneau pour le Go, et TestFacadePerfAnneau côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
23 files changed
+2068
-314
No files matched your search
@@ -552,7 +552,7 @@ func runTune(ctx context.Context, opts tuneOpts, tr *tuneTracker) (out *tuneResu
|
||||
|
||||
res := &tuneResult{PresetID: preset.ID, PresetName: preset.Name, BaseFP: configFingerprint(cfg),
|
||||
PresetFP: presetFingerprint([]byte(content)), OptIn: opts.OptIn, At: time.Now().Unix(),
|
||||
Weights: tuneWeightsFrom(perfLog.snapshot())}
|
||||
Weights: tuneWeightsFrom(perfSnapshot())}
|
||||
|
||||
// Référence : la ligne composée telle quelle, la sonde de la machine.
|
||||
tr.setPhase("référence : composition de la ligne de commande")
|
||||
|
||||
@@ -89,6 +89,8 @@ var facadeChatRoutes = map[string]bool{
|
||||
"/api/chat/export": true,
|
||||
"/api/chat/mode": true,
|
||||
"/api/chat/criteria": true,
|
||||
// Lot 39 : l'anneau de télémétrie est celui de la façade.
|
||||
"/api/perf/summary": true,
|
||||
}
|
||||
|
||||
// withFacadeChat remet la requête à la façade quand elle tient la
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// facade_perf.go — l'anneau de télémétrie tenu par la façade (lot 39 de la
|
||||
// migration Rust). Quand elle tient la conversation, toutes les complétions y
|
||||
// sont rangées :
|
||||
//
|
||||
// - celles de ses tours, directement ;
|
||||
// - celles que le Go fait encore (sous-agents, tâches IA, bench, compaction
|
||||
// et préchauffage de ses tours, clients externes de /v1), par
|
||||
// /internal/facade/v1/perf/record.
|
||||
//
|
||||
// /api/perf/summary est servi par elle (facadeChatRoutes) et l'optimiseur lit
|
||||
// l'anneau par perfSnapshot. Façade injoignable : la complétion est rangée
|
||||
// ici, comme sans façade — la télémétrie ne fait jamais échouer un tour.
|
||||
|
||||
// facadePerfJSON appelle l'API de télémétrie de la façade et décode sa
|
||||
// réponse.
|
||||
func facadePerfJSON(path string, body, out any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, "http://"+facadeAddr()+path, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(internalTokenHeader, internalToken())
|
||||
resp, err := facadeClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("façade : %s", resp.Status)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
// facadePerfRecord range une complétion dans l'anneau de la façade et la rend
|
||||
// telle que rangée (numéro, perte de cache, ce qui s'est intercalé).
|
||||
func facadePerfRecord(r perfRec, prefix []uint64) (perfRec, bool) {
|
||||
var out struct {
|
||||
Rec perfRec `json:"rec"`
|
||||
}
|
||||
err := facadePerfJSON("/internal/facade/v1/perf/record", map[string]any{"rec": r, "prefix": prefix}, &out)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[perf] complétion non transmise à la façade : %v\n", err)
|
||||
return perfRec{}, false
|
||||
}
|
||||
return out.Rec, true
|
||||
}
|
||||
|
||||
// perfSnapshot : l'anneau, de la plus ancienne complétion à la plus récente —
|
||||
// celui de la façade quand elle le tient.
|
||||
func perfSnapshot() []perfRec {
|
||||
if facadeChat() {
|
||||
var out struct {
|
||||
Recs []perfRec `json:"recs"`
|
||||
}
|
||||
if err := facadePerfJSON("/internal/facade/v1/perf/snapshot", map[string]any{}, &out); err == nil {
|
||||
return out.Recs
|
||||
}
|
||||
}
|
||||
return perfLog.snapshot()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Lot 39 : quand la façade tient la conversation, les complétions du Go vont
|
||||
// dans son anneau, et l'optimiseur le lit ; façade en panne, l'anneau du Go
|
||||
// prend le relais. /api/perf/summary lui est remis.
|
||||
func TestFacadePerfAnneau(t *testing.T) {
|
||||
testHome(t)
|
||||
freshPerf(t)
|
||||
f := demarrerFacade(t)
|
||||
f.internalStatus = 200
|
||||
f.internalBody = `{"rec":{"at":"2026-10-16T08:00:00Z","seq":7,"kind":"subagent","conv":"c1","iter":0,"complete":true,"total":10,"cached":2,"lost":3,"lost_after":"main"}}`
|
||||
got := perfRecord(perfRec{Kind: perfSubagent, Conv: "c1", Complete: true, Total: 10, Cached: iptr(2)}, []uint64{1, 2})
|
||||
if got.Seq != 7 || got.Lost == nil || *got.Lost != 3 || got.LostAfter != "main" {
|
||||
t.Fatalf("rangée par la façade : %+v", got)
|
||||
}
|
||||
if n := len(perfLog.snapshot()); n != 0 {
|
||||
t.Fatalf("rangée aussi ici : %d", n)
|
||||
}
|
||||
vu := f.recu()
|
||||
if len(vu) != 1 || !strings.HasPrefix(vu[0], "POST /internal/facade/v1/perf/record jeton ") ||
|
||||
!strings.Contains(vu[0], `"prefix":[1,2]`) || !strings.Contains(vu[0], `"kind":"subagent"`) {
|
||||
t.Fatalf("envoi : %v", vu)
|
||||
}
|
||||
|
||||
f.internalBody = `{"recs":[{"at":"2026-10-16T08:00:00Z","seq":1,"kind":"main","iter":0,"complete":true,"total":40}]}`
|
||||
if s := perfSnapshot(); len(s) != 1 || s[0].Total != 40 {
|
||||
t.Fatalf("anneau de la façade : %+v", s)
|
||||
}
|
||||
|
||||
// Façade en panne : l'anneau d'ici.
|
||||
f.internalStatus = 500
|
||||
got = perfRecord(perfRec{Kind: perfForeign}, nil)
|
||||
if got.Seq != 1 || got.Kind != perfForeign {
|
||||
t.Fatalf("repli : %+v", got)
|
||||
}
|
||||
if s := perfSnapshot(); len(s) != 1 || s[0].Kind != perfForeign {
|
||||
t.Fatalf("repli de l'anneau : %+v", s)
|
||||
}
|
||||
if !facadeChatRoutes["/api/perf/summary"] {
|
||||
t.Fatal("/api/perf/summary n'est pas remis à la façade")
|
||||
}
|
||||
}
|
||||
@@ -66,5 +66,5 @@ func handleInternalTuneContext(w http.ResponseWriter, r *http.Request) {
|
||||
bgJobsRunning())
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"busy": busy, "main_active": engineNeedsStop(),
|
||||
"perf": perfLog.snapshot()})
|
||||
"perf": perfSnapshot()})
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//go:build linux
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parity_perf_test.go — journal de référence de l'anneau de télémétrie (lot 39
|
||||
// de la migration Rust) : une suite de complétions rangées (chacune avec les
|
||||
// empreintes des messages envoyés), ce que l'anneau en a tiré (numéro, perte
|
||||
// de cache, ce qui s'est intercalé), la ligne [perf], le résumé de
|
||||
// /api/perf/summary, puis les seuils d'alerte d'une série de presets. Rangé
|
||||
// dans rust/loki-rs/tests/parity_perf/perf.json ; src/perfring/tests.rs le
|
||||
// rejoue.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteAnneauPerf
|
||||
|
||||
const parityPerfFile = "../../rust/loki-rs/tests/parity_perf/perf.json"
|
||||
|
||||
type parityPerfStep struct {
|
||||
Rec json.RawMessage `json:"rec"`
|
||||
Msgs []string `json:"msgs,omitempty"` // contenus des messages envoyés (rôle user)
|
||||
Prefix []string `json:"prefix,omitempty"`
|
||||
Out json.RawMessage `json:"out"`
|
||||
Line string `json:"line"`
|
||||
Alert int `json:"alert_1000"` // perfAlert au seuil 1000 : 1 vrai, 0 faux
|
||||
}
|
||||
|
||||
type parityPerfAlert struct {
|
||||
Cfg map[string]string `json:"cfg"`
|
||||
Env string `json:"env,omitempty"` // LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT
|
||||
Want int `json:"want"`
|
||||
}
|
||||
|
||||
type parityPerfJournal struct {
|
||||
Steps []parityPerfStep `json:"steps"`
|
||||
Summary json.RawMessage `json:"summary"`
|
||||
Empty json.RawMessage `json:"empty"`
|
||||
Alerts []parityPerfAlert `json:"alerts"`
|
||||
}
|
||||
|
||||
func TestPariteAnneauPerf(t *testing.T) {
|
||||
testHome(t)
|
||||
freshPerf(t)
|
||||
at := time.Date(2026, 10, 16, 8, 0, 0, 0, time.UTC)
|
||||
var j parityPerfJournal
|
||||
rec := func(r perfRec, msgs ...string) {
|
||||
var prefix []uint64
|
||||
if msgs != nil {
|
||||
ms := make([]Message, len(msgs))
|
||||
for i, c := range msgs {
|
||||
ms[i] = Message{Role: "user", Content: c}
|
||||
}
|
||||
prefix = perfPrefix(ms)
|
||||
}
|
||||
in, _ := json.Marshal(r)
|
||||
r.At = at
|
||||
at = at.Add(time.Second)
|
||||
got := perfLog.record(r, prefix)
|
||||
out, _ := json.Marshal(got)
|
||||
st := parityPerfStep{Rec: in, Msgs: msgs, Out: out, Line: perfLine(got)}
|
||||
for _, p := range prefix {
|
||||
st.Prefix = append(st.Prefix, strconv.FormatUint(p, 10))
|
||||
}
|
||||
if perfAlert(got, 1000) {
|
||||
st.Alert = 1
|
||||
}
|
||||
j.Steps = append(j.Steps, st)
|
||||
}
|
||||
main := func(conv string, iter, total int, cached *int) perfRec {
|
||||
return perfRec{Kind: perfMain, Conv: conv, Iter: iter, Complete: true, Total: total, Cached: cached}
|
||||
}
|
||||
full := perfRec{Kind: perfMain, Conv: "c1", Iter: 0, Complete: true, Total: 4000, Cached: iptr(3000),
|
||||
New: 1000, PPms: 2000.4, PPtps: 499.9, Gen: 50, TGtps: 40.25, DraftN: iptr(40), DraftAcc: iptr(30), TTFTms: ptr64(2100)}
|
||||
rec(full, "a")
|
||||
rec(main("c1", 1, 4600, iptr(4000)), "a", "b", "c") // prolonge : rien de perdu
|
||||
rec(perfRec{Kind: perfSubagent, Conv: "c1", Complete: true, Total: 300, Cached: iptr(0), New: 300, PPms: 600, PPtps: 500}, "z")
|
||||
rec(perfRec{Kind: perfForeign}) // client externe : vu, pas mesuré
|
||||
rec(main("c1", 2, 5200, iptr(200)), "a", "b", "c", "d") // perte après le sous-agent et le client
|
||||
rec(main("c1", 3, 5200, nil), "a", "b", "c", "d", "e") // cache inconnu
|
||||
rec(main("c1", 4, 5300, iptr(10)), "x", "b") // diverge
|
||||
rec(main("c2", 0, 20000, iptr(0)), "q") // autre discussion, gros prompt sans cache
|
||||
rec(perfRec{Kind: perfMain, Conv: "c2", Iter: 1, Cached: iptr(0)}, "q", "r") // complétion coupée
|
||||
rec(main("c2", 1, 21000, iptr(19000)), "q", "r") // prolonge malgré la coupure
|
||||
rec(perfRec{Kind: perfCompact, Conv: "c2", Complete: true, Total: 9000, Cached: iptr(8800), New: 200, PPms: 100, PPtps: 2000, Gen: 300, TGtps: 30, TTFTms: ptr64(900)})
|
||||
rec(perfRec{Kind: perfBench, Complete: true, Total: 33000, New: 33000, PPms: 30000, PPtps: 1100, Gen: 64, TGtps: 20.5})
|
||||
rec(perfRec{Kind: perfVerify, Conv: "c1", Iter: 0, Complete: true, Total: 12000, Cached: iptr(0), New: 12000, PPms: 9000.5, PPtps: 1333.3, Gen: 2, TGtps: 9.75, DraftN: iptr(0)})
|
||||
rec(main("c1", 0, 6000, iptr(5900)), "a", "b", "c", "d", "e", "f") // prolonge la dernière terminée de c1
|
||||
|
||||
sum, _ := json.Marshal(perfSummarize(perfLog.snapshot()))
|
||||
j.Summary = sum
|
||||
empty, _ := json.Marshal(perfSummarize(nil))
|
||||
j.Empty = empty
|
||||
|
||||
for _, c := range []parityPerfAlert{
|
||||
{Cfg: map[string]string{}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "2048"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "4096", "UBATCH": "1024"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "beaucoup"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": " 300 ", "UBATCH": "100"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "2048", "EXTRA_ARGS": "--checkpoint-min-step 8192 -ub 256"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "2048", "UBATCH": "1024", "EXTRA_ARGS": "-ub=2048"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "-5", "UBATCH": "0"}},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "2048"}, Env: "3000"},
|
||||
{Cfg: map[string]string{"CKPT_MIN_STEP": "2048"}, Env: "rien"},
|
||||
{Cfg: map[string]string{"MODEL": "absent.gguf"}},
|
||||
} {
|
||||
t.Setenv("LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT", c.Env)
|
||||
c.Want = perfLostAlertAt(c.Cfg)
|
||||
j.Alerts = append(j.Alerts, c)
|
||||
}
|
||||
|
||||
got, err := json.MarshalIndent(j, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = append(got, '\n')
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityPerfFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityPerfFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityPerfFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("l'anneau de télémétrie ne produit plus le journal rangé dans %s.\n"+
|
||||
"LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityPerfFile)
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,15 @@ func (p *perfStore) snapshot() []perfRec {
|
||||
// perfRecord range une complétion et, si LOKI_PERF_LOG est posée, l'écrit sur
|
||||
// stderr. Une ligne par complétion, c'est des dizaines par tour d'agent : pas
|
||||
// par défaut, les journaux Docker ont déjà celles de llama-server.
|
||||
//
|
||||
// Quand la façade tient la conversation, l'anneau est le sien (facade_perf.go) :
|
||||
// elle range et écrit la ligne.
|
||||
func perfRecord(r perfRec, prefix []uint64) perfRec {
|
||||
if facadeChat() {
|
||||
if got, ok := facadePerfRecord(r, prefix); ok {
|
||||
return got
|
||||
}
|
||||
}
|
||||
r = perfLog.record(r, prefix)
|
||||
if perfLogOn() {
|
||||
fmt.Fprintln(os.Stderr, perfLine(r))
|
||||
|
||||
@@ -71,8 +71,6 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalOAIEnd(w, r)
|
||||
case "/internal/v1/oai/served":
|
||||
handleInternalOAIServed(w, r)
|
||||
case "/internal/v1/turn/perf":
|
||||
handleInternalTurnPerf(w, r)
|
||||
case "/internal/v1/turn/projsnap/refresh":
|
||||
handleInternalTurnProjSnapRefresh(w, r)
|
||||
case "/internal/v1/turn/slot":
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Télémétrie d'une complétion d'un tour tenu par la façade Rust
|
||||
// (LOKI_CHAT=rust). La boucle Go range chaque complétion dans l'anneau de
|
||||
// perf_log.go (perfRecord) : c'est lui qui nourrit /api/perf/summary et
|
||||
// l'alerte « cache perdu » posée sur les stats finales d'une complétion. La
|
||||
// boucle Rust lui envoie la même chose, ici, au même moment.
|
||||
//
|
||||
// POST /internal/v1/turn/perf
|
||||
// {"kind":"main","conv":"c…","iter":0,"complete":true,"external":false,
|
||||
// "stats":{…StatsEvent…},"prefix":[…empreintes…]}
|
||||
// → {"kind":"main","lost":N,"lost_after":"…","lost_alert":true}
|
||||
//
|
||||
// kind : main (tour), verify (passe de vérification du mode Code) ou compact
|
||||
// (résumé de compaction, relevé dans la réponse entière par la façade).
|
||||
// prefix : empreintes cumulées des messages envoyés (perfPrefix), calculées
|
||||
// par l'appelant — elles n'ont à être cohérentes qu'avec ses propres envois,
|
||||
// d'une complétion à la suivante de la même discussion. La sonde du gabarit
|
||||
// n'en part pas : la façade la tient (facadeTplCaps).
|
||||
|
||||
const internalTurnPerfMaxBody = 8 << 20
|
||||
|
||||
func handleInternalTurnPerf(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
|
||||
return
|
||||
}
|
||||
var in struct {
|
||||
Kind string `json:"kind"`
|
||||
Conv string `json:"conv"`
|
||||
Iter int `json:"iter"`
|
||||
Complete bool `json:"complete"`
|
||||
External bool `json:"external"`
|
||||
Stats StatsEvent `json:"stats"`
|
||||
Prefix []uint64 `json:"prefix"`
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(r.Body, internalTurnPerfMaxBody+1))
|
||||
if err != nil || len(raw) > internalTurnPerfMaxBody || json.Unmarshal(raw, &in) != nil {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "télémétrie illisible"})
|
||||
return
|
||||
}
|
||||
if in.Kind == "" {
|
||||
in.Kind = perfMain
|
||||
}
|
||||
var prefix []uint64
|
||||
if in.Complete {
|
||||
prefix = in.Prefix
|
||||
}
|
||||
rec := perfRecord(perfRecFromStats(perfTag{kind: in.Kind, conv: in.Conv}, in.Iter, in.Complete, in.Stats), prefix)
|
||||
alert := false
|
||||
if in.Complete && !in.External && ((rec.Lost != nil && *rec.Lost > 0) || (rec.Cached != nil && *rec.Cached == 0)) {
|
||||
alert = perfAlert(rec, perfLostAlertAt(ReadConfig()))
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"kind": rec.Kind, "lost": rec.Lost, "lost_after": rec.LostAfter, "lost_alert": alert,
|
||||
})
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// La complétion d'un tour tenu par Rust arrive dans l'anneau de télémétrie,
|
||||
// comme celle de la boucle Go : /api/perf/summary la compte.
|
||||
func TestTurnPerfDansLAnneau(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
avant := perfSummarize(perfLog.snapshot())
|
||||
n0 := 0
|
||||
if k := avant.Kinds[perfMain]; k != nil {
|
||||
n0 = k.Completions
|
||||
}
|
||||
body := `{"kind":"main","conv":"c-perf","iter":0,"complete":true,"external":false,
|
||||
"stats":{"prompt_tokens":120,"prompt_tokens_total":120,"gen_tokens":8,"cache_tokens":0},
|
||||
"prefix":[1,2,3]}`
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/turn/perf", body)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("code %d : %s", w.Code, w.Body.String())
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil || got["kind"] != "main" {
|
||||
t.Fatalf("réponse : %s", w.Body.String())
|
||||
}
|
||||
apres := perfSummarize(perfLog.snapshot())
|
||||
if k := apres.Kinds[perfMain]; k == nil || k.Completions != n0+1 {
|
||||
t.Fatalf("complétion non rangée : %+v", apres.Kinds[perfMain])
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/perf", "{"); w.Code != 400 {
|
||||
t.Fatalf("corps illisible : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Perte de cache calculée contre la complétion précédente de la même
|
||||
// discussion, ce qui s'est intercalé, alerte au seuil du preset actif ; un
|
||||
// preset externe ou une complétion incomplète ne la signalent jamais.
|
||||
func TestTurnPerfPerteEtAlerte(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
post := func(body string) (int, map[string]any) {
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/turn/perf", body)
|
||||
var out map[string]any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &out)
|
||||
return w.Code, out
|
||||
}
|
||||
code, out := post(`{"kind":"main","conv":"fp1","iter":0,"complete":true,
|
||||
"stats":{"prompt_tokens":5000,"prompt_tokens_total":5000,"cache_tokens":0},"prefix":[11,22]}`)
|
||||
if code != 200 || out["lost"] != nil || out["lost_after"] != "" {
|
||||
t.Fatalf("première : %d %v", code, out)
|
||||
}
|
||||
// Une tâche passe entre deux.
|
||||
perfRecord(perfRec{Kind: perfTask, Conv: "t", Complete: true, Total: 10}, nil)
|
||||
// La suivante prolonge la première, mais n'en reprend que 100 jetons.
|
||||
code, out = post(`{"kind":"main","conv":"fp1","iter":1,"complete":true,
|
||||
"stats":{"prompt_tokens":5900,"prompt_tokens_total":6000,"cache_tokens":100},"prefix":[11,22,33]}`)
|
||||
if code != 200 || out["lost"] != float64(4900) || out["lost_after"] != perfTask || out["lost_alert"] != true {
|
||||
t.Fatalf("perte : %d %v", code, out)
|
||||
}
|
||||
if recs := perfLog.snapshot(); recs[len(recs)-1].Conv != "fp1" || recs[len(recs)-1].Iter != 1 || recs[len(recs)-1].New != 5900 {
|
||||
t.Fatalf("relevé rangé : %+v", recs[len(recs)-1])
|
||||
}
|
||||
// Preset externe : la perte est rangée, jamais signalée.
|
||||
code, out = post(`{"kind":"main","conv":"fp1","iter":2,"complete":true,"external":true,
|
||||
"stats":{"prompt_tokens_total":7000,"cache_tokens":100},"prefix":[11,22,33,44]}`)
|
||||
if code != 200 || out["lost"] != float64(5900) || out["lost_alert"] != false {
|
||||
t.Fatalf("externe : %d %v", code, out)
|
||||
}
|
||||
// Incomplète : ses empreintes sont ignorées, ni perte ni alerte.
|
||||
code, out = post(`{"kind":"main","conv":"fp1","iter":3,"complete":false,
|
||||
"stats":{"cache_tokens":0},"prefix":[11,22,33,44,55]}`)
|
||||
if code != 200 || out["lost"] != nil || out["lost_alert"] != false {
|
||||
t.Fatalf("incomplète : %d %v", code, out)
|
||||
}
|
||||
if recs := perfLog.snapshot(); recs[len(recs)-1].Complete {
|
||||
t.Fatalf("incomplète rangée complète : %+v", recs[len(recs)-1])
|
||||
}
|
||||
// Résumé de compaction : sa nature est gardée.
|
||||
code, out = post(`{"kind":"compact","conv":"fp1","complete":true,"stats":{"prompt_tokens_total":900}}`)
|
||||
if code != 200 || out["kind"] != perfCompact {
|
||||
t.Fatalf("compaction : %d %v", code, out)
|
||||
}
|
||||
// Sans nature : un tour.
|
||||
if _, out = post(`{"conv":"fp2","complete":true,"stats":{}}`); out["kind"] != perfMain {
|
||||
t.Fatalf("sans nature : %v", out)
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/turn/perf", ""); w.Code != 405 {
|
||||
t.Fatalf("GET : %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Une étape de tour acceptée par le moteur : le slot porte la discussion
|
||||
// (engineSlotHolds), comme engineMarkMain dans la boucle Go.
|
||||
func TestOAIServedMarqueLeSlot(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
w := appelOAIInterne(t, "POST", "/internal/v1/oai/begin", `{"main_conv":"c-slot"}`)
|
||||
var b struct {
|
||||
Ticket uint64 `json:"ticket"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &b); err != nil || b.Ticket == 0 {
|
||||
t.Fatalf("begin : %s", w.Body.String())
|
||||
}
|
||||
if engineSlotHolds("c-slot") {
|
||||
t.Fatal("slot marqué avant que le moteur ait accepté la requête")
|
||||
}
|
||||
tk := fmt.Sprintf(`{"ticket":%d}`, b.Ticket)
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/oai/served", tk); w.Code != 204 {
|
||||
t.Fatalf("served : %d", w.Code)
|
||||
}
|
||||
if !engineSlotHolds("c-slot") {
|
||||
t.Fatal("slot non marqué après served")
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/oai/end", tk); w.Code != 204 {
|
||||
t.Fatalf("end : %d", w.Code)
|
||||
}
|
||||
if w := appelOAIInterne(t, "POST", "/internal/v1/oai/served", tk); w.Code != 404 {
|
||||
t.Fatalf("served après end : %d", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user