mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
Conversation tenue par Rust : télémétrie et requêtes en vol comme le Go
Trois choses manquaient au tour tenu par la façade, actives même sans clé opt-in, et qu'il fallait avant d'en faire le défaut : - la télémétrie de chaque complétion (perfRecord) : /api/perf/summary et l'alerte « cache perdu » posée sur les stats finales d'une étape. La boucle Rust la remet au Go (/internal/v1/turn/perf) au même moment que la boucle Go, avec ses empreintes de messages ; le Go range, calcule la perte et rend nature, jetons perdus et alerte, posés sur la copie finale des stats ; - la sonde du gabarit, lancée dans les mêmes conditions (requête de forme normale du moteur local) ; - le comptage des requêtes en vol : chaque requête au moteur local ouvre un ticket (/internal/v1/oai/begin), marqué servi à la réponse 200 (engineServed, engineMarkMain pour une étape de la discussion), rendu en fin de lecture du flux. Le slot de la discussion n'est ainsi ni effacé pendant une requête ni pris pour un autre. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98196e1730
commit
5edcad50a5
10 files changed
+483
-20
No files matched your search
@@ -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":
|
||||
|
||||
@@ -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,72 @@
|
||||
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…],
|
||||
// "probe":{"tools":[…],"kwargs":{…},"effort":""}}
|
||||
// → {"kind":"main","lost":N,"lost_after":"…","lost_alert":true}
|
||||
//
|
||||
// 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. probe : présent quand
|
||||
// la boucle Go aurait lancé la sonde du gabarit (requête de forme normale du
|
||||
// fil principal, moteur local).
|
||||
|
||||
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"`
|
||||
Probe *struct {
|
||||
Tools []Tool `json:"tools"`
|
||||
Kwargs map[string]any `json:"kwargs"`
|
||||
Effort string `json:"effort"`
|
||||
} `json:"probe"`
|
||||
}
|
||||
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()))
|
||||
}
|
||||
if in.Complete && !in.External && in.Probe != nil {
|
||||
tplProbeKick(in.Probe.Tools, in.Probe.Kwargs, in.Probe.Effort)
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{
|
||||
"kind": rec.Kind, "lost": rec.Lost, "lost_after": rec.LostAfter, "lost_alert": alert,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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