mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Fusion de main : conversation Rust par défaut, télémétrie du tour
main a reçu en parallèle la télémétrie du tour tenu par la façade (/internal/v1/turn/perf), les tickets « en vol » du moteur et la conversation tenue par la façade par défaut (LOKI_CHAT=go pour revenir au Go). Le lot 29 faisait la même chose par /internal/v1/perf/record : un seul chemin est gardé, celui de main. - TurnHost::perf_record (main) remplace TurnHost::perf ; la nature est posée par l'hôte (verify pour la passe isolée seulement, main pour les corrections du builder, comme le Go). - Les résumés de compaction passent par le même point d'entrée, nature compact : chat/perf.rs ne garde que le relevé de la réponse entière (perfRecFromWire) et l'envoi. - La sonde du gabarit reste à la façade (lot 23) : /internal/v1/turn/perf ne la lance plus côté Go. - facade_perf.go retiré ; ses cas (perte, intercalé, alerte, preset externe, incomplète, GET) rejoignent web_internal_turnperf_test.go. - README : LOKI_CHAT défaut Rust, avec ce que les lots 19, 23, 29 et 30 y ajoutent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
20 files changed
+713
-457
No files matched your search
@@ -1,37 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// facade_perf.go — la télémétrie des complétions de la façade (lot 29 de la
|
||||
// migration). Quand la façade tient la conversation (LOKI_CHAT=rust), ses
|
||||
// complétions — tours, vérifications, résumés de compaction — rejoignent
|
||||
// l'anneau du Go (perf_log.go) : /api/perf/summary les compte, la perte de
|
||||
// cache se calcule contre la complétion précédente de la même discussion, et
|
||||
// l'alerte suit le seuil du preset actif (perfLostAlertAt), comme pour un
|
||||
// tour du Go.
|
||||
|
||||
// handleInternalPerfRecord : POST {rec, prefix, external} → {seq, lost,
|
||||
// lost_after, alert}. rec : une complétion (perfRec, sans at ni seq) ;
|
||||
// prefix : les empreintes cumulées des messages envoyés (perfPrefix), vide
|
||||
// pour une complétion incomplète ; external : preset externe (jamais
|
||||
// d'alerte).
|
||||
func handleInternalPerfRecord(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Rec perfRec `json:"rec"`
|
||||
Prefix []uint64 `json:"prefix"`
|
||||
External bool `json:"external"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
in.Rec.At, in.Rec.Seq, in.Rec.Lost, in.Rec.LostAfter = time.Time{}, 0, nil, ""
|
||||
rec := perfRecord(in.Rec, in.Prefix)
|
||||
alert := false
|
||||
if rec.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{"seq": rec.Seq, "lost": rec.Lost, "lost_after": rec.LostAfter, "alert": alert})
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// La télémétrie des complétions de la façade rejoint l'anneau du Go : perte
|
||||
// de cache calculée contre la complétion précédente de la même discussion,
|
||||
// ce qui s'est intercalé, et l'alerte au seuil du preset actif.
|
||||
func TestFacadePerf(t *testing.T) {
|
||||
testHome(t)
|
||||
post := func(body string) (int, map[string]any) {
|
||||
rec := httptest.NewRecorder()
|
||||
handleInternalPerfRecord(rec, httptest.NewRequest("POST", "/internal/v1/perf/record", strings.NewReader(body)))
|
||||
var out map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
||||
return rec.Code, out
|
||||
}
|
||||
// Première complétion : rien à comparer. Ce que la façade enverrait en
|
||||
// trop (at, seq, lost) est ignoré.
|
||||
code, out := post(`{"rec":{"kind":"main","conv":"fp1","iter":0,"complete":true,"total":5000,"cached":0,"new":5000,
|
||||
"seq":99,"lost":7,"lost_after":"x"},"prefix":[11,22]}`)
|
||||
if code != 200 || out["lost"] != nil || out["lost_after"] != "" || out["alert"] != false {
|
||||
t.Fatalf("première : %d %v", code, out)
|
||||
}
|
||||
seq := out["seq"].(float64)
|
||||
if seq == 99 || seq <= 0 {
|
||||
t.Fatalf("seq : %v", seq)
|
||||
}
|
||||
// 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(`{"rec":{"kind":"main","conv":"fp1","iter":1,"complete":true,"total":6000,"cached":100,"new":5900},"prefix":[11,22,33]}`)
|
||||
if code != 200 || out["lost"] != float64(4900) || out["lost_after"] != perfTask || out["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(`{"rec":{"kind":"main","conv":"fp1","iter":2,"complete":true,"total":7000,"cached":100},"prefix":[11,22,33,44],"external":true}`)
|
||||
if code != 200 || out["lost"] != float64(5900) || out["alert"] != false {
|
||||
t.Fatalf("externe : %d %v", code, out)
|
||||
}
|
||||
// Incomplète : ni perte ni alerte.
|
||||
code, out = post(`{"rec":{"kind":"main","conv":"fp1","iter":3,"complete":false,"cached":0}}`)
|
||||
if code != 200 || out["lost"] != nil || out["alert"] != false {
|
||||
t.Fatalf("incomplète : %d %v", code, out)
|
||||
}
|
||||
if code, _ := post(`{"rec":`); code != 400 {
|
||||
t.Fatalf("corps illisible : %d", code)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handleInternalPerfRecord(rec, httptest.NewRequest("GET", "/internal/v1/perf/record", nil))
|
||||
if rec.Code != 405 {
|
||||
t.Fatalf("GET : %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalOAIBegin(w, r)
|
||||
case "/internal/v1/oai/end":
|
||||
handleInternalOAIEnd(w, r)
|
||||
case "/internal/v1/oai/served":
|
||||
handleInternalOAIServed(w, r)
|
||||
case "/internal/v1/turn/perf":
|
||||
handleInternalTurnPerf(w, r)
|
||||
case "/internal/v1/tool/exec":
|
||||
handleInternalToolExec(w, r)
|
||||
case "/internal/v1/tool/context":
|
||||
@@ -135,8 +139,6 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalBenchPerf(w, r)
|
||||
case "/internal/v1/tune/context":
|
||||
handleInternalTuneContext(w, r)
|
||||
case "/internal/v1/perf/record":
|
||||
handleInternalPerfRecord(w, r)
|
||||
case "/internal/v1/tasks/run":
|
||||
handleInternalTaskRun(w, r)
|
||||
case "/internal/v1/push/send":
|
||||
|
||||
@@ -31,6 +31,11 @@ const oaiTicketMaxAge = 6 * time.Hour
|
||||
type oaiTicket struct {
|
||||
end func()
|
||||
when time.Time
|
||||
// seq / mainConv : requête d'un tour de discussion tenu par la façade —
|
||||
// une fois acceptée par le moteur (served), le slot porte cette discussion
|
||||
// (engineMarkMain). Vides pour un client /v1.
|
||||
seq uint64
|
||||
mainConv string
|
||||
}
|
||||
|
||||
var oaiTickets = struct {
|
||||
@@ -62,16 +67,20 @@ func handleInternalOAIBegin(w http.ResponseWriter, r *http.Request) {
|
||||
Read bool `json:"read"` // GET/HEAD : ne calcule rien dans un slot
|
||||
Side bool `json:"side"` // corps routé vers le second slot
|
||||
PostV1 bool `json:"post_v1"` // complétion d'un client externe
|
||||
// MainConv : étape d'un tour de cette discussion, tenue par la façade
|
||||
// (LOKI_CHAT=rust) — comptée comme la requête principale du Go.
|
||||
MainConv string `json:"main_conv"`
|
||||
}
|
||||
if r.Method != http.MethodPost || json.NewDecoder(r.Body).Decode(&in) != nil {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "POST JSON attendu"})
|
||||
return
|
||||
}
|
||||
var end func()
|
||||
var seq uint64
|
||||
if in.Read {
|
||||
end, _ = engineRequestBeginSide(func(*prewarmRun) bool { return true }, true)
|
||||
end, seq = engineRequestBeginSide(func(*prewarmRun) bool { return true }, true)
|
||||
} else {
|
||||
end, _ = engineRequestBeginSide(nil, in.Side)
|
||||
end, seq = engineRequestBeginSide(nil, in.Side)
|
||||
}
|
||||
if in.PostV1 {
|
||||
refuse := func(retry, msg, code string) {
|
||||
@@ -101,7 +110,7 @@ func handleInternalOAIBegin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
oaiTickets.next++
|
||||
id := oaiTickets.next
|
||||
oaiTickets.m[id] = oaiTicket{end: end, when: now}
|
||||
oaiTickets.m[id] = oaiTicket{end: end, when: now, seq: seq, mainConv: in.MainConv}
|
||||
oaiTickets.mu.Unlock()
|
||||
sendJSON(w, http.StatusOK, map[string]any{"ticket": id})
|
||||
}
|
||||
@@ -132,3 +141,27 @@ func oaiInflight() int {
|
||||
defer oaiTickets.mu.Unlock()
|
||||
return len(oaiTickets.m)
|
||||
}
|
||||
|
||||
// handleInternalOAIServed : la requête du ticket a été acceptée par le moteur
|
||||
// (réponse 200) — engineServed, et pour l'étape d'un tour de discussion,
|
||||
// engineMarkMain : le slot porte désormais cette discussion. Le ticket reste
|
||||
// ouvert jusqu'à end (fin de lecture du flux).
|
||||
func handleInternalOAIServed(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Ticket uint64 `json:"ticket"`
|
||||
}
|
||||
if r.Method != http.MethodPost || json.NewDecoder(r.Body).Decode(&in) != nil {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "POST JSON attendu"})
|
||||
return
|
||||
}
|
||||
oaiTickets.mu.Lock()
|
||||
t, ok := oaiTickets.m[in.Ticket]
|
||||
oaiTickets.mu.Unlock()
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
engineServed()
|
||||
engineMarkMain(t.seq, t.mainConv)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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