mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 13:08:15 +02:00
Lot 29 : télémétrie des complétions de la façade
Quand la façade tient la conversation (LOKI_CHAT=rust), ses complétions rejoignent l'anneau de télémétrie du Go (perf_log.go) au lieu de passer inaperçues : /api/perf/summary les compte, l'optimiseur en tire ses poids, et l'interface reçoit à nouveau la perte de cache. - chat/perf.rs : perfRecFromStats, perfRecFromWire (lu sans échec possible, mêmes tolérances que perfNum, timings d'un autre type présents mais vides comme le Go) et perfPrefix (FNV-1a cumulé) ; - la boucle du tour range chaque complétion lue, numérotée dans le tour, avec ses empreintes si elle est complète ; une complétion coupée par un stop est rangée incomplète ; la dernière statistique porte la nature, la perte, ce qui s'est intercalé et l'alerte ; - natures : main (tour), verify (vérification du mode Code), compact (résumé de compaction, rangé dès que la réponse se lit) ; - le Go (/internal/v1/perf/record) range le relevé comme les siens et calcule l'alerte au seuil du preset actif, jamais pour un preset externe. Tests : relevés et empreintes (chat/perf.rs), télémétrie de la boucle (complète, coupée), du vrai hôte et des résumés (tests/lot6.rs), TestFacadePerf 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:
15 files changed
+731
-16
No files matched your search
@@ -125,6 +125,8 @@ pub struct FakeGo {
|
||||
/// pour une action donnée.
|
||||
pub service_os: Mutex<Vec<String>>,
|
||||
pub service_os_fail: Mutex<Option<(String, String)>>,
|
||||
/// /internal/v1/perf/record reçus (corps entiers).
|
||||
pub perf_records: Mutex<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
impl FakeGo {
|
||||
@@ -500,6 +502,14 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
|
||||
}
|
||||
resp(200, r#"{"ran":true,"error":""}"#)
|
||||
}
|
||||
"/internal/v1/perf/record" => {
|
||||
let body = req.into_body().collect().await.unwrap().to_bytes();
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||
let mut log = st.perf_records.lock().unwrap();
|
||||
log.push(v);
|
||||
let out = serde_json::json!({"seq": log.len(), "lost": 12, "lost_after": "task", "alert": false});
|
||||
resp(200, out.to_string())
|
||||
}
|
||||
"/internal/v1/tune/context" => {
|
||||
let v = serde_json::json!({
|
||||
"busy": *st.tune_ctx_busy.lock().unwrap(),
|
||||
|
||||
@@ -680,6 +680,7 @@ async fn tour_complet_par_le_go() {
|
||||
cfg: cfg.clone(),
|
||||
patterns: shared.patterns.clone(),
|
||||
port: 8080,
|
||||
perf: None,
|
||||
},
|
||||
compact: TurnCompact {
|
||||
cfg: CompactConfig::default(),
|
||||
@@ -710,6 +711,7 @@ async fn tour_complet_par_le_go() {
|
||||
echo: Default::default(),
|
||||
compact: CompactConfig::default(),
|
||||
patterns: shared.patterns.clone(),
|
||||
perf_kind: "main",
|
||||
};
|
||||
let question = Message {
|
||||
role: "user".into(),
|
||||
@@ -761,6 +763,38 @@ 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]["rec"]["complete"], false);
|
||||
assert_eq!(perfs[0]["prefix"], serde_json::json!([]));
|
||||
let r1 = &perfs[1]["rec"];
|
||||
assert_eq!(
|
||||
(
|
||||
r1["conv"].clone(),
|
||||
r1["kind"].clone(),
|
||||
r1["iter"].clone(),
|
||||
r1["total"].clone()
|
||||
),
|
||||
(
|
||||
serde_json::json!("c7"),
|
||||
serde_json::json!("main"),
|
||||
serde_json::json!(1),
|
||||
serde_json::json!(50)
|
||||
)
|
||||
);
|
||||
assert_eq!(perfs[1]["prefix"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(perfs[1]["external"], false);
|
||||
let last = evs
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|e| match e {
|
||||
StreamEvent::Stats(s) if !s.kind.is_empty() => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!((last.lost, last.lost_after.as_str()), (Some(12), "task"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -978,6 +1012,7 @@ async fn hote_et_sonde_de_gabarit() {
|
||||
cfg,
|
||||
patterns: shared.patterns.clone(),
|
||||
port: 1,
|
||||
perf: None,
|
||||
},
|
||||
compact: TurnCompact {
|
||||
cfg: CompactConfig::default(),
|
||||
@@ -1768,3 +1803,55 @@ async fn parite_du_tour_complet() {
|
||||
failures.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// Lot 29 : le résumé d'une compaction rejoint la télémétrie du Go
|
||||
/// (kind=compact, rattaché à la discussion), dès que la réponse se lit.
|
||||
#[tokio::test]
|
||||
async fn telemetrie_des_resumes() {
|
||||
use loki_rs::chat::compact::{Summarizer, TranscriptRequest};
|
||||
let (st, go, _stop, _t) = store().await;
|
||||
let (url, _seen) = fake_engine(vec![
|
||||
serde_json::json!({
|
||||
"choices": [{"message": {"content": "résumé"}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 3000, "completion_tokens": 200},
|
||||
"timings": {"prompt_n": 3000, "prompt_ms": 1500, "predicted_n": 200, "predicted_per_second": 40}
|
||||
})
|
||||
.to_string(),
|
||||
r#"{"choices":[]}"#.into(),
|
||||
"pas du json".into(),
|
||||
])
|
||||
.await;
|
||||
let s = EngineSummarizer {
|
||||
http: EngineHttp::new(),
|
||||
ep: ChatEndpoint {
|
||||
url,
|
||||
model: "loki".into(),
|
||||
key: String::new(),
|
||||
external: false,
|
||||
},
|
||||
cfg: Default::default(),
|
||||
patterns: RetryPatterns::builtin(),
|
||||
port: 8080,
|
||||
perf: Some((st, "c9".into())),
|
||||
};
|
||||
let req = || TranscriptRequest {
|
||||
system: "s".into(),
|
||||
transcript: "t".into(),
|
||||
max_tokens: 500,
|
||||
};
|
||||
assert_eq!(
|
||||
s.summarize_transcript(req()).await.unwrap().content,
|
||||
"résumé"
|
||||
);
|
||||
assert!(s.summarize_transcript(req()).await.is_err(), "réponse vide");
|
||||
assert!(s.summarize_transcript(req()).await.is_err(), "illisible");
|
||||
let perfs = go.perf_records.lock().unwrap().clone();
|
||||
assert_eq!(perfs.len(), 2, "la réponse illisible n'est pas rangée");
|
||||
assert_eq!(
|
||||
perfs[0]["rec"],
|
||||
serde_json::json!({ "kind": "compact", "conv": "c9", "iter": 0, "complete": true,
|
||||
"total": 3000, "new": 3000, "pp_ms": 1500.0, "gen": 200, "tg_tps": 40.0 })
|
||||
);
|
||||
assert_eq!(perfs[0]["prefix"], serde_json::json!([]));
|
||||
assert_eq!(perfs[1]["rec"]["complete"], false);
|
||||
}
|
||||
@@ -195,6 +195,7 @@ async fn replay(fx: Fixture) -> Result<(), String> {
|
||||
echo,
|
||||
patterns: RetryPatterns::builtin(),
|
||||
cfg,
|
||||
perf_kind: "main",
|
||||
};
|
||||
let host = ParityHost {
|
||||
replies: Mutex::new(fx.replies.into()),
|
||||
|
||||
Reference in new issue
Block a user