Lot 23 : sonde de gabarit en Rust (avec LOKI_CHAT=rust)

Port de chat_tplprobe.go : la façade sonde le gabarit du moteur après ses
complétions (/health, /props, /apply-template), avec les verdicts
preserves_history, prefix_stable et renders_reasoning, le cache par
build/modèle/gabarit/forme et la relance bornée.

- l'écho du raisonnement suit le verdict de la sonde (echo_policy_for) ;
- NUDGE_IN_TOOL ne garde plus la conversation au Go : le rappel de budget
  va dans le dernier résultat d'outil quand le préfixe n'est pas stable ;
- le Go lit le verdict de la façade (/internal/facade/v1/chat/tpl) quand
  elle tient la conversation.

Parité : parity_tplprobe_test.go tire du Go un journal d'échanges rejoué
octet pour octet par tests/tplprobe.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
Claude committed 2026-10-15 21:34:57 +00:00
1 parent 78766a28d9
commit 0f3a3d52c1
17 files changed
+5006 -22

No files matched your search

+39 -1
View File
@@ -298,7 +298,7 @@ func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
t.Fatalf("remis à la façade malgré le réglage : %q %v", rec.Body.String(), f.recu())
}
for k, want := range map[string]string{
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "NUDGE_IN_TOOL", "SIDE_SLOT": "SIDE_SLOT",
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "", "SIDE_SLOT": "SIDE_SLOT",
"SLOT_PERSIST": "SLOT_PERSIST", "PREWARM": "PREWARM",
} {
if got := facadeChatBlocker(map[string]string{k: "on"}); got != want {
@@ -1064,3 +1064,41 @@ func TestFacadeBackends(t *testing.T) {
}
lcAppend("perdue") // tracée, sans panique
}
// Lot 23 : quand la façade tient la conversation, c'est elle qui sonde le
// gabarit ; le résumé des performances relit son verdict.
func TestFacadeSondeDeGabarit(t *testing.T) {
testHome(t)
freshTplProbe(t)
oublierDecisionFacade(t)
verdict := `{"ok":true,"result":{"preserves_history":"no","prefix_stable":"no","renders_reasoning":"yes","model":"m1.gguf","tools":2,"at":"2026-10-15T20:00:00.5Z"}}`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(internalTokenHeader) != jetonTest || r.URL.Path != "/internal/facade/v1/chat/tpl" {
w.WriteHeader(404)
return
}
io.WriteString(w, verdict)
}))
defer srv.Close()
t.Setenv(internalTokenEnv, jetonTest)
t.Setenv(facadeAddrEnv, strings.TrimPrefix(srv.URL, "http://"))
t.Setenv(facadeChatEnv, "rust")
r, ok := tplCapsCurrent()
if !ok || r.PrefixStable != tplNo || r.Model != "m1.gguf" || r.Tools != 2 || r.At.IsZero() {
t.Fatalf("verdict de la façade : %+v %v", r, ok)
}
rec := httptest.NewRecorder()
handlePerfSummary(rec, httptest.NewRequest(http.MethodGet, "/api/perf/summary", nil))
if !strings.Contains(rec.Body.String(), `"prefix_stable":"no"`) {
t.Fatalf("résumé : %s", rec.Body.String())
}
// Aucun verdict encore, ou façade injoignable : rien.
verdict = `{"ok":false}`
if _, ok := tplCapsCurrent(); ok {
t.Fatal("verdict inventé")
}
t.Setenv(facadeAddrEnv, "127.0.0.1:1")
if _, ok := tplCapsCurrent(); ok {
t.Fatal("verdict sans façade")
}
}