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:
Claude committed 2026-10-16 04:17:04 +00:00
1 parent 76d9dce74a
commit 35886da9b1
23 files changed
+2068 -314

No files matched your search

-22
View File
@@ -128,8 +128,6 @@ pub struct FakeGo {
/// pour une action donnée.
pub service_os: Mutex<Vec<String>>,
pub service_os_fail: Mutex<Option<(String, String)>>,
/// /internal/v1/turn/perf reçus (corps entiers).
pub perf_records: Mutex<Vec<serde_json::Value>>,
/// /internal/v1/turn/slot : second slot en service (le fil de la
/// discussion sur le 0, le reste sur le 1 ; sinon aucun) ; demandes
/// reçues (« conv main »).
@@ -781,26 +779,6 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
.push(format!("served {}", v["ticket"]));
resp(204, "")
}
"/internal/v1/turn/perf" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
st.oai_log.lock().unwrap().push(format!(
"perf kind={} conv={} iter={} complete={} prefix={}",
v["kind"].as_str().unwrap_or(""),
v["conv"].as_str().unwrap_or(""),
v["iter"],
v["complete"],
v["prefix"].as_array().map_or(0, Vec::len),
));
// Comme le Go : la nature reçue (un tour sans nature).
let kind = v["kind"]
.as_str()
.filter(|k| !k.is_empty())
.unwrap_or("main");
let out = serde_json::json!({"kind": kind, "lost": 3, "lost_after": "tâche", "lost_alert": true});
st.perf_records.lock().unwrap().push(v);
resp(200, out.to_string())
}
"/internal/v1/oai/end" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
+192 -50
View File
@@ -667,6 +667,7 @@ async fn tour_complet_par_le_go() {
patterns: RetryPatterns::builtin(),
tpl: Arc::new(loki_rs::chat::tplprobe::TplProbe::new()),
prewarm: Default::default(),
perf: Arc::new(loki_rs::perfring::PerfRing::new(Default::default())),
mcp: None,
tasks: None,
go_version: Default::default(),
@@ -771,29 +772,26 @@ async fn tour_complet_par_le_go() {
})
.collect();
assert_eq!(answer, "Voilà.");
// Télémétrie : chaque complétion rangée par le Go, rattachée à la
// discussion ; la première, sans usage, reste incomplète.
let perfs = go.perf_records.lock().unwrap().clone();
assert_eq!(perfs.len(), 2);
assert_eq!(perfs[0]["complete"], false);
assert_eq!(perfs[0]["prefix"], serde_json::json!([]));
let r1 = &perfs[1];
// Télémétrie (lot 39) : chaque complétion rangée dans l'anneau de la
// façade, rattachée à la discussion ; la première, sans usage, reste
// incomplète.
let recs = shared.perf.snapshot();
let got: Vec<_> = recs
.iter()
.map(|r| {
(
r.kind.as_str(),
r.conv.as_str(),
r.iter,
r.complete,
r.total,
)
})
.collect();
assert_eq!(
(
r1["conv"].clone(),
r1["kind"].clone(),
r1["iter"].clone(),
r1["stats"]["prompt_tokens_total"].clone()
),
(
serde_json::json!("c7"),
serde_json::json!("main"),
serde_json::json!(1),
serde_json::json!(50)
)
got,
[("main", "c7", 0, false, 0), ("main", "c7", 1, true, 50)]
);
assert_eq!(perfs[1]["prefix"].as_array().unwrap().len(), 3);
assert_eq!(perfs[1]["external"], false);
let last = evs
.iter()
.rev()
@@ -802,9 +800,10 @@ async fn tour_complet_par_le_go() {
_ => None,
})
.unwrap();
// Rien de comparable avant : la perte reste inconnue.
assert_eq!(
(last.kind.as_str(), last.lost, last.lost_after.as_str()),
("main", Some(3), "tâche")
("main", None, "")
);
}
@@ -992,22 +991,30 @@ async fn tour_de_bout_en_bout() {
2,
"{log:?}"
);
// Appel d'outil sans usage ni timings : complétion incomplète (comme le
// Go : ni empreintes) ; la réponse finale, avec usage, complète. La
// sonde du gabarit reste à la façade (`sonde_de_gabarit_apres_un_tour`).
for want in [
format!("perf kind=main conv={id} iter=0 complete=false prefix=0"),
format!("perf kind=main conv={id} iter=1 complete=true prefix=4"),
] {
assert!(log.contains(&want), "{want} absent de {log:?}");
}
// Appel d'outil sans usage ni timings : complétion incomplète ; la
// réponse finale, avec usage, complète — dans l'anneau de la façade
// (lot 39), rien par le Go.
assert!(!log.iter().any(|l| l.starts_with("perf ")), "{log:?}");
let recs: Vec<_> = svc
.shared
.perf
.snapshot()
.into_iter()
.map(|r| (r.kind, r.conv, r.iter, r.complete))
.collect();
assert_eq!(
recs,
[
("main".to_string(), id.clone(), 0, false),
("main".to_string(), id.clone(), 1, true)
]
);
let finals: Vec<_> = deltas(&svc, "stats")
.into_iter()
.filter(|s| s["lost_alert"] == true)
.filter(|s| s["kind"] == "main")
.collect();
assert_eq!(finals.len(), 1, "stats finales sans les marques du Go");
assert_eq!(finals[0]["lost"], 3);
assert_eq!(finals[0]["kind"], "main");
assert_eq!(finals.len(), 1, "stats finales sans leur nature");
assert_eq!(finals[0].get("lost_alert"), None);
let body = opened(&go, BK_CHAT, &format!("conv:{id}")).unwrap();
assert_eq!(decode_state(&body).messages.len(), 4);
// Réessai du même envoi : reconnu.
@@ -1943,6 +1950,7 @@ async fn telemetrie_des_resumes() {
.to_string(),
])
.await;
let ring = Arc::new(loki_rs::perfring::PerfRing::new(Default::default()));
let s = EngineSummarizer {
http: EngineHttp::new(),
ep: ChatEndpoint {
@@ -1954,7 +1962,7 @@ async fn telemetrie_des_resumes() {
cfg: Default::default(),
patterns: RetryPatterns::builtin(),
port: 8080,
go: Some((st, "c9".into())),
go: Some((st, "c9".into(), ring.clone())),
};
*go.turn_side_live.lock().unwrap() = true;
let req = || TranscriptRequest {
@@ -2018,16 +2026,16 @@ async fn telemetrie_des_resumes() {
.contains(&"begin main_conv=".to_string())
})
.await;
let perfs = go.perf_records.lock().unwrap().clone();
let perfs = ring.snapshot();
assert_eq!(perfs.len(), 3, "la réponse illisible n'est pas rangée");
let mut first = serde_json::to_value(&perfs[0]).unwrap();
first.as_object_mut().unwrap().remove("at");
assert_eq!(
perfs[0],
serde_json::json!({ "kind": "compact", "conv": "c9", "iter": 0, "complete": true,
"external": false, "prefix": [],
"stats": { "prompt_tokens_total": 3000, "prompt_tokens": 3000, "prompt_ms": 1500.0,
"gen_tokens": 200, "gen_per_second": 40.0 } })
first,
serde_json::json!({ "seq": 1, "kind": "compact", "conv": "c9", "iter": 0, "complete": true,
"total": 3000, "new": 3000, "pp_ms": 1500, "gen": 200, "tg_tps": 40 })
);
assert_eq!(perfs[1]["complete"], false);
assert!(!perfs[1].complete);
}
/// Lot 32 : KEEP_TURN_IMAGES tenu par la façade. L'image relayée par un
@@ -2202,11 +2210,12 @@ async fn prechauffage_par_la_facade() {
let first_end = log.iter().find(|l| l.starts_with("end")).unwrap();
let v: serde_json::Value = serde_json::from_str(first_end.trim_start_matches("end ")).unwrap();
assert_eq!(v, serde_json::json!({"id": 5, "total": 60, "cached": 42}));
let perfs = go.perf_records.lock().unwrap().clone();
assert!(
perfs
svc.shared
.perf
.snapshot()
.iter()
.any(|p| p["kind"] == "prewarm" && p["stats"]["prompt_tokens_total"] == 60)
.any(|p| p.kind == "prewarm" && p.total == 60)
);
// La vue du préchauffage est demandée sans rien à ranger (PROJ_SNAPSHOT).
assert!(
@@ -2252,11 +2261,11 @@ async fn prechauffage_refuse_ou_annule() {
r#"end {"cached":null,"id":5,"total":0}"#
);
assert!(
go.perf_records
.lock()
.unwrap()
svc.shared
.perf
.snapshot()
.iter()
.all(|p| p["kind"] != "prewarm")
.all(|p| p.kind != "prewarm")
);
}
@@ -2566,3 +2575,136 @@ async fn outil_task_par_la_facade() {
))
);
}
/// Lot 39 : l'anneau de télémétrie tenu par la façade. D'un tour à l'autre
/// de la même discussion, le cache perdu est mesuré ici et signalé dans les
/// stats finales (au-delà du seuil du preset).
#[tokio::test]
async fn cache_perdu_mesure_par_la_facade() {
let reply = |total: i64, cached: i64| {
sse(&[
serde_json::json!({"choices":[{"delta":{"content":"vu"}}]}),
serde_json::json!({"choices":[{"delta":{},"finish_reason":"stop"}],
"usage":{"prompt_tokens":total,"completion_tokens":2},
"timings":{"cache_n":cached,"prompt_n":total - cached,"prompt_ms":10.0,"prompt_per_second":100.0}}),
])
};
let (url, _seen) = fake_engine(vec![reply(5000, 0), reply(6000, 200)]).await;
let (svc, _go, _stop, _t) = service(Some(&url)).await;
for (i, m) in ["un", "deux"].iter().enumerate() {
svc.send(serde_json::json!({"message": m, "cid": format!("p{i}")}))
.await
.unwrap();
until_done(&svc, i + 1).await;
}
let id = svc.sessions.conv().id();
let recs = svc.shared.perf.snapshot();
let got: Vec<_> = recs
.iter()
.map(|r| (r.conv.as_str(), r.total, r.cached, r.lost))
.collect();
assert_eq!(
got,
[
(id.as_str(), 5000, Some(0), None),
(id.as_str(), 6000, Some(200), Some(4800))
]
);
let finals: Vec<_> = deltas(&svc, "stats")
.into_iter()
.filter(|s| s["kind"] == "main")
.collect();
assert_eq!(finals.len(), 2);
assert_eq!(finals[0].get("lost_alert"), None);
assert_eq!(
(finals[1]["lost"].clone(), finals[1]["lost_alert"].clone()),
(serde_json::json!(4800), serde_json::json!(true))
);
}
/// Lot 39 : ce que le Go confie à l'anneau de la façade, ce qu'il en relit,
/// et /api/perf/summary servi par elle.
#[tokio::test]
async fn anneau_de_la_facade_pour_le_go() {
let t = tempfile::tempdir().unwrap();
let (facade, go, _stop, chat) = fakego::start_with(
t.path().into(),
fakego::Opts {
chat: true,
..Default::default()
},
)
.await;
let chat = chat.unwrap();
let tok = [(loki_rs::auth::INTERNAL_HEADER, fakego::JETON)];
let rec = |total: i64, cached: Option<i64>, kind: &str| {
serde_json::json!({ "at": "0001-01-01T00:00:00Z", "seq": 0, "kind": kind, "conv": "c1",
"iter": 0, "complete": true, "total": total, "cached": cached })
};
let post = |body: serde_json::Value, path: &'static str| {
let b = body.to_string();
async move {
let (code, out) = fakego::send(facade, "POST", path, &tok, &b).await;
assert_eq!(code, 200, "{}", String::from_utf8_lossy(&out));
serde_json::from_slice::<serde_json::Value>(&out).unwrap()
}
};
let r = post(
serde_json::json!({ "rec": rec(3000, Some(0), "subagent"), "prefix": [11, 12] }),
"/internal/facade/v1/perf/record",
)
.await;
assert_eq!(
(r["rec"]["seq"].clone(), r["rec"]["lost"].clone()),
(serde_json::json!(1), serde_json::Value::Null)
);
assert_ne!(r["rec"]["at"], "0001-01-01T00:00:00Z", "heure posée");
// Un client externe passe, puis la suite du sous-agent : perte, et ce qui
// s'est intercalé.
post(
serde_json::json!({ "rec": { "kind": "foreign" } }),
"/internal/facade/v1/perf/record",
)
.await;
let r = post(
serde_json::json!({ "rec": rec(3500, Some(1000), "subagent"), "prefix": [11, 12, 13] }),
"/internal/facade/v1/perf/record",
)
.await;
assert_eq!(
(r["rec"]["lost"].clone(), r["rec"]["lost_after"].clone()),
(serde_json::json!(2000), serde_json::json!("foreign"))
);
let snap = post(serde_json::json!({}), "/internal/facade/v1/perf/snapshot").await;
let kinds: Vec<_> = snap["recs"]
.as_array()
.unwrap()
.iter()
.map(|r| r["kind"].as_str().unwrap().to_string())
.collect();
assert_eq!(kinds, ["subagent", "foreign", "subagent"]);
assert_eq!(chat.shared.perf.snapshot().len(), 3);
// Sans jeton : refusé.
let (code, _) = fakego::send(
facade,
"POST",
"/internal/facade/v1/perf/snapshot",
&[],
"{}",
)
.await;
assert_ne!(code, 200);
// Le résumé, servi par la façade.
let (code, out) = fakego::send(facade, "GET", "/api/perf/summary", &[], "").await;
assert_eq!(code, 200);
let s: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(s["entries"], 3);
assert_eq!(s["kinds"]["subagent"]["completions"], 2);
assert_eq!(s["kinds"]["subagent"]["lost_tokens"], 2000);
assert_eq!(s["kinds"]["foreign"]["incomplete"], 1);
assert_eq!(s["depth"].as_array().unwrap().len(), 4);
assert_eq!(s.get("template"), None);
let (code, _) = fakego::send(facade, "POST", "/api/perf/summary", &[], "").await;
assert_eq!(code, 405);
drop(go);
}
+681
View File
@@ -0,0 +1,681 @@
{
"steps": [
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 4000,
"cached": 3000,
"new": 1000,
"pp_ms": 2000.4,
"pp_tps": 499.9,
"gen": 50,
"tg_tps": 40.25,
"draft_n": 40,
"draft_accepted": 30,
"ttft_ms": 2100
},
"msgs": [
"a"
],
"prefix": [
"18380994072798496976"
],
"out": {
"at": "2026-10-16T08:00:00Z",
"seq": 1,
"kind": "main",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 4000,
"cached": 3000,
"new": 1000,
"pp_ms": 2000.4,
"pp_tps": 499.9,
"gen": 50,
"tg_tps": 40.25,
"draft_n": 40,
"draft_accepted": 30,
"ttft_ms": 2100
},
"line": "[perf] kind=main conv=c1 iter=0 total=4000 cached=3000 new=1000 pp_ms=2000 pp_tps=499.9 gen=50 tg_tps=40.2 draft=30/40 ttft_ms=2100 lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 1,
"complete": true,
"total": 4600,
"cached": 4000
},
"msgs": [
"a",
"b",
"c"
],
"prefix": [
"18380994072798496976",
"7707499087160536902",
"9701112914521480905"
],
"out": {
"at": "2026-10-16T08:00:01Z",
"seq": 2,
"kind": "main",
"conv": "c1",
"iter": 1,
"complete": true,
"total": 4600,
"cached": 4000,
"lost": 0
},
"line": "[perf] kind=main conv=c1 iter=1 total=4600 cached=4000 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=0 complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "subagent",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 300,
"cached": 0,
"new": 300,
"pp_ms": 600,
"pp_tps": 500
},
"msgs": [
"z"
],
"prefix": [
"2089108250578558367"
],
"out": {
"at": "2026-10-16T08:00:02Z",
"seq": 3,
"kind": "subagent",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 300,
"cached": 0,
"new": 300,
"pp_ms": 600,
"pp_tps": 500
},
"line": "[perf] kind=subagent conv=c1 iter=0 total=300 cached=0 new=300 pp_ms=600 pp_tps=500.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "foreign",
"iter": 0,
"complete": false
},
"out": {
"at": "2026-10-16T08:00:03Z",
"seq": 4,
"kind": "foreign",
"iter": 0,
"complete": false
},
"line": "[perf] kind=foreign conv=- iter=0 total=0 cached=? new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=false",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 2,
"complete": true,
"total": 5200,
"cached": 200
},
"msgs": [
"a",
"b",
"c",
"d"
],
"prefix": [
"18380994072798496976",
"7707499087160536902",
"9701112914521480905",
"8266243799656022901"
],
"out": {
"at": "2026-10-16T08:00:04Z",
"seq": 5,
"kind": "main",
"conv": "c1",
"iter": 2,
"complete": true,
"total": 5200,
"cached": 200,
"lost": 4400,
"lost_after": "foreign,subagent"
},
"line": "[perf] kind=main conv=c1 iter=2 total=5200 cached=200 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=4400(foreign,subagent) complete=true",
"alert_1000": 1
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 3,
"complete": true,
"total": 5200
},
"msgs": [
"a",
"b",
"c",
"d",
"e"
],
"prefix": [
"18380994072798496976",
"7707499087160536902",
"9701112914521480905",
"8266243799656022901",
"5788732863052660068"
],
"out": {
"at": "2026-10-16T08:00:05Z",
"seq": 6,
"kind": "main",
"conv": "c1",
"iter": 3,
"complete": true,
"total": 5200
},
"line": "[perf] kind=main conv=c1 iter=3 total=5200 cached=? new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 4,
"complete": true,
"total": 5300,
"cached": 10
},
"msgs": [
"x",
"b"
],
"prefix": [
"4430711847302402693",
"10853208800609132871"
],
"out": {
"at": "2026-10-16T08:00:06Z",
"seq": 7,
"kind": "main",
"conv": "c1",
"iter": 4,
"complete": true,
"total": 5300,
"cached": 10
},
"line": "[perf] kind=main conv=c1 iter=4 total=5300 cached=10 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c2",
"iter": 0,
"complete": true,
"total": 20000,
"cached": 0
},
"msgs": [
"q"
],
"prefix": [
"18326388500208184512"
],
"out": {
"at": "2026-10-16T08:00:07Z",
"seq": 8,
"kind": "main",
"conv": "c2",
"iter": 0,
"complete": true,
"total": 20000,
"cached": 0
},
"line": "[perf] kind=main conv=c2 iter=0 total=20000 cached=0 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 1
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c2",
"iter": 1,
"complete": false,
"cached": 0
},
"msgs": [
"q",
"r"
],
"prefix": [
"18326388500208184512",
"745432260197996294"
],
"out": {
"at": "2026-10-16T08:00:08Z",
"seq": 9,
"kind": "main",
"conv": "c2",
"iter": 1,
"complete": false,
"cached": 0
},
"line": "[perf] kind=main conv=c2 iter=1 total=0 cached=0 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=false",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c2",
"iter": 1,
"complete": true,
"total": 21000,
"cached": 19000
},
"msgs": [
"q",
"r"
],
"prefix": [
"18326388500208184512",
"745432260197996294"
],
"out": {
"at": "2026-10-16T08:00:09Z",
"seq": 10,
"kind": "main",
"conv": "c2",
"iter": 1,
"complete": true,
"total": 21000,
"cached": 19000,
"lost": 1000
},
"line": "[perf] kind=main conv=c2 iter=1 total=21000 cached=19000 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=1000 complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "compact",
"conv": "c2",
"iter": 0,
"complete": true,
"total": 9000,
"cached": 8800,
"new": 200,
"pp_ms": 100,
"pp_tps": 2000,
"gen": 300,
"tg_tps": 30,
"ttft_ms": 900
},
"out": {
"at": "2026-10-16T08:00:10Z",
"seq": 11,
"kind": "compact",
"conv": "c2",
"iter": 0,
"complete": true,
"total": 9000,
"cached": 8800,
"new": 200,
"pp_ms": 100,
"pp_tps": 2000,
"gen": 300,
"tg_tps": 30,
"ttft_ms": 900
},
"line": "[perf] kind=compact conv=c2 iter=0 total=9000 cached=8800 new=200 pp_ms=100 pp_tps=2000.0 gen=300 tg_tps=30.0 draft=? ttft_ms=900 lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "bench",
"iter": 0,
"complete": true,
"total": 33000,
"new": 33000,
"pp_ms": 30000,
"pp_tps": 1100,
"gen": 64,
"tg_tps": 20.5
},
"out": {
"at": "2026-10-16T08:00:11Z",
"seq": 12,
"kind": "bench",
"iter": 0,
"complete": true,
"total": 33000,
"new": 33000,
"pp_ms": 30000,
"pp_tps": 1100,
"gen": 64,
"tg_tps": 20.5
},
"line": "[perf] kind=bench conv=- iter=0 total=33000 cached=? new=33000 pp_ms=30000 pp_tps=1100.0 gen=64 tg_tps=20.5 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 0
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "verify",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 12000,
"cached": 0,
"new": 12000,
"pp_ms": 9000.5,
"pp_tps": 1333.3,
"gen": 2,
"tg_tps": 9.75,
"draft_n": 0
},
"out": {
"at": "2026-10-16T08:00:12Z",
"seq": 13,
"kind": "verify",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 12000,
"cached": 0,
"new": 12000,
"pp_ms": 9000.5,
"pp_tps": 1333.3,
"gen": 2,
"tg_tps": 9.75,
"draft_n": 0
},
"line": "[perf] kind=verify conv=c1 iter=0 total=12000 cached=0 new=12000 pp_ms=9000 pp_tps=1333.3 gen=2 tg_tps=9.8 draft=?/0 ttft_ms=? lost=? complete=true",
"alert_1000": 1
},
{
"rec": {
"at": "0001-01-01T00:00:00Z",
"seq": 0,
"kind": "main",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 6000,
"cached": 5900
},
"msgs": [
"a",
"b",
"c",
"d",
"e",
"f"
],
"prefix": [
"18380994072798496976",
"7707499087160536902",
"9701112914521480905",
"8266243799656022901",
"5788732863052660068",
"7575424036688383126"
],
"out": {
"at": "2026-10-16T08:00:13Z",
"seq": 14,
"kind": "main",
"conv": "c1",
"iter": 0,
"complete": true,
"total": 6000,
"cached": 5900
},
"line": "[perf] kind=main conv=c1 iter=0 total=6000 cached=5900 new=0 pp_ms=0 pp_tps=0.0 gen=0 tg_tps=0.0 draft=? ttft_ms=? lost=? complete=true",
"alert_1000": 0
}
],
"summary": {
"entries": 14,
"since": "2026-10-16T08:00:00Z",
"cache_hit_ratio": 0.4680778032036613,
"kinds": {
"bench": {
"completions": 1,
"incomplete": 0,
"turns": 1,
"new_tokens": 33000,
"prefill_sec": 30,
"new_tokens_per_turn": 33000,
"prefill_sec_per_turn": 30,
"lost_events": 0,
"lost_tokens": 0
},
"compact": {
"completions": 1,
"incomplete": 0,
"turns": 1,
"new_tokens": 200,
"prefill_sec": 0.1,
"new_tokens_per_turn": 200,
"prefill_sec_per_turn": 0.1,
"lost_events": 0,
"lost_tokens": 0,
"ttft_median_ms": 900
},
"foreign": {
"completions": 0,
"incomplete": 1,
"turns": 0,
"new_tokens": 0,
"prefill_sec": 0,
"lost_events": 0,
"lost_tokens": 0
},
"main": {
"completions": 8,
"incomplete": 1,
"turns": 3,
"new_tokens": 1000,
"prefill_sec": 2.0004,
"new_tokens_per_turn": 333.3333333333333,
"prefill_sec_per_turn": 0.6668,
"lost_events": 2,
"lost_tokens": 5400,
"ttft_median_ms": 2100,
"draft_n": 40,
"draft_accepted": 30,
"draft_rate": 0.75
},
"subagent": {
"completions": 1,
"incomplete": 0,
"turns": 1,
"new_tokens": 300,
"prefill_sec": 0.6,
"new_tokens_per_turn": 300,
"prefill_sec_per_turn": 0.6,
"lost_events": 0,
"lost_tokens": 0
},
"verify": {
"completions": 1,
"incomplete": 0,
"turns": 1,
"new_tokens": 12000,
"prefill_sec": 9.0005,
"new_tokens_per_turn": 12000,
"prefill_sec_per_turn": 9.0005,
"lost_events": 0,
"lost_tokens": 0
}
},
"depth": [
{
"bucket": "0-8k",
"n": 2,
"pp_median_tps": 499.95,
"tg_median_tps": 40.25
},
{
"bucket": "8-16k",
"n": 2,
"pp_median_tps": 1666.65,
"tg_median_tps": 19.875
},
{
"bucket": "16-32k",
"n": 0
},
{
"bucket": "32k+",
"n": 1,
"pp_median_tps": 1100,
"tg_median_tps": 20.5
}
],
"draft_n": 40,
"draft_accepted": 30,
"draft_rate": 0.75
},
"empty": {
"entries": 0,
"kinds": {},
"depth": [
{
"bucket": "0-8k",
"n": 0
},
{
"bucket": "8-16k",
"n": 0
},
{
"bucket": "16-32k",
"n": 0
},
{
"bucket": "32k+",
"n": 0
}
],
"draft_n": 0,
"draft_accepted": 0
},
"alerts": [
{
"cfg": {},
"want": 1000
},
{
"cfg": {
"CKPT_MIN_STEP": "2048"
},
"want": 2560
},
{
"cfg": {
"CKPT_MIN_STEP": "4096",
"UBATCH": "1024"
},
"want": 5120
},
{
"cfg": {
"CKPT_MIN_STEP": "beaucoup"
},
"want": 1000
},
{
"cfg": {
"CKPT_MIN_STEP": " 300 ",
"UBATCH": "100"
},
"want": 1000
},
{
"cfg": {
"CKPT_MIN_STEP": "2048",
"EXTRA_ARGS": "--checkpoint-min-step 8192 -ub 256"
},
"want": 8448
},
{
"cfg": {
"CKPT_MIN_STEP": "2048",
"EXTRA_ARGS": "-ub=2048",
"UBATCH": "1024"
},
"want": 4096
},
{
"cfg": {
"CKPT_MIN_STEP": "-5",
"UBATCH": "0"
},
"want": 1000
},
{
"cfg": {
"CKPT_MIN_STEP": "2048"
},
"env": "3000",
"want": 3512
},
{
"cfg": {
"CKPT_MIN_STEP": "2048"
},
"env": "rien",
"want": 2560
},
{
"cfg": {
"MODEL": "absent.gguf"
},
"want": 1000
}
]
}
+13 -3
View File
@@ -93,10 +93,16 @@ async fn criteria_comme_le_go() {
}
let args = st["args"].as_object().cloned().unwrap_or_default();
let verifier = st["verifier"].as_bool() == Some(true);
let before = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
let before = store
.get_open("chat", &format!("crit:{conv}"))
.await
.unwrap();
let out = exec(&store, conv, &args, verifier).await;
assert_eq!(out.result, st["out"].as_str().unwrap(), "{name}");
let raw = store.get_open("chat", &format!("crit:{conv}")).await.unwrap();
let raw = store
.get_open("chat", &format!("crit:{conv}"))
.await
.unwrap();
let stored = raw
.as_deref()
.filter(|b| !b.is_empty())
@@ -110,7 +116,11 @@ async fn criteria_comme_le_go() {
// L'écran reçoit la liste quand elle change (vide : effacée).
match &out.notify {
Some(list) => {
let want = if stored.is_null() { json!([]) } else { stored.clone() };
let want = if stored.is_null() {
json!([])
} else {
stored.clone()
};
assert_eq!(json!(list), want, "{name}");
}
None => assert_eq!(raw, before, "{name} : rien ne change sans notification"),