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:
Claude committed 2026-10-16 01:17:49 +00:00
1 parent f6c191922f
commit 8a61879bfd
15 files changed
+731 -16

No files matched your search

+4 -1
View File
@@ -853,6 +853,9 @@ struct WireMsg {
tool_calls: Option<Vec<Value>>,
}
/// Réponse lue mais sans choix.
pub const ERR_EMPTY_SUMMARY: &str = "résumé: réponse vide";
/// Décodage d'une réponse non streamée de /v1/chat/completions, comme
/// `postSummary` : seule la PREMIÈRE valeur JSON compte (un octet parasite
/// après elle est ignoré), aucun choix → « résumé: réponse vide ».
@@ -866,7 +869,7 @@ pub fn parse_summary_response(raw: &[u8]) -> Result<SummaryChoice, SummaryError>
Some(Ok(r)) => r,
};
let Some(ch) = resp.choices.and_then(|c| c.into_iter().next()) else {
return Err(SummaryError::Other("résumé: réponse vide".into()));
return Err(SummaryError::Other(ERR_EMPTY_SUMMARY.into()));
};
Ok(SummaryChoice {
content: ch.message.content.unwrap_or_default(),