Lot 33 : préchauffage (PREWARM) tenu par la façade

PREWARM ne garde plus la conversation au Go. La façade tient
l'historique : elle assemble et envoie la requête du prochain tour ; le
Go garde le compteur des requêtes en vol et ce qui l'annule.

Go (facade_prewarm.go) :
- /internal/v1/prewarm/check : moteur libre d'un vrai travail, loin de
  minuit, en bonne santé, slot visé ;
- /internal/v1/prewarm/begin : rechargement SLOT_PERSIST puis inscription
  comme LA requête en vol ; son annulation est relayée à la façade ;
- /internal/v1/prewarm/end : place rendue, cache repris noté ;
- /internal/v1/oai/begin : prewarm_keep pour l'étape qui le prolonge ;
- fin de tâche planifiée : le déclencheur part à la façade.

Rust (chat/prewarm.rs) :
- assemblage par les mêmes fonctions que le tour, sentinelle « . », un
  seul jeton, non streamé ; abstention (capacités inconnues, tour en
  cours, discussion vide, compaction attendue, assemblage inattendu) ;
- déclencheurs : fin de tour, fin de tâche, changement de discussion ou
  de projet (full : après 3 s) ;
- le tour garde le préchauffage qu'il prolonge (empreintes des messages
  et des options de rendu) et coupe tout de suite les autres ;
- télémétrie kind=prewarm.

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 02:52:44 +00:00
1 parent f79b74dac3
commit ff5c6d4571
21 files changed
+1457 -35

No files matched your search

+40 -1
View File
@@ -48,6 +48,10 @@ struct Fake {
relay: Mutex<Option<Message>>,
keep_stores: Mutex<usize>,
keep_refuse: Mutex<bool>,
/// PREWARM : la requête prolonge-t-elle le préchauffage ? ce que `send`
/// a reçu.
prewarm_match: Mutex<bool>,
sent_keeps: Mutex<Vec<bool>>,
}
impl Fake {
@@ -77,7 +81,13 @@ fn stream_of(lines: Vec<String>, cut: bool) -> BodyStream {
}
impl TurnHost for Fake {
async fn send(&self, _ep: &ChatEndpoint, body: Vec<u8>) -> Result<Reply, SendError> {
async fn send(
&self,
_ep: &ChatEndpoint,
body: Vec<u8>,
keep: bool,
) -> Result<Reply, SendError> {
self.sent_keeps.lock().unwrap().push(keep);
self.bodies
.lock()
.unwrap()
@@ -163,6 +173,12 @@ impl TurnHost for Fake {
async fn keep_images(&self) -> Option<i64> {
*self.keep.lock().unwrap()
}
fn prewarm_keeps(&self, prefix: &[u64], payload: &Value) -> bool {
// Ce que l'hôte reçoit : les empreintes des messages envoyés et le
// corps de la requête.
assert_eq!(prefix.len(), payload["messages"].as_array().unwrap().len());
*self.prewarm_match.lock().unwrap()
}
async fn keep_image(&self, m: &Message) -> Option<(Message, i64)> {
*self.keep_stores.lock().unwrap() += 1;
if *self.keep_refuse.lock().unwrap() {
@@ -1055,3 +1071,26 @@ async fn images_retirees_fenetre_pleine() {
assert!(evs.iter().any(|e| matches!(e, StreamEvent::DropReasoning)));
assert_eq!(text(&evs), "fini");
}
/// PREWARM : la boucle demande à l'hôte si la requête prolonge le
/// préchauffage en vol et le lui transmet à l'envoi ; jamais vers une API
/// externe.
#[tokio::test]
async fn prechauffage_garde_par_la_requete_qui_le_prolonge() {
let host = Fake::new(vec![answer("ok")]);
*host.prewarm_match.lock().unwrap() = true;
let (r, _) = run(&host, &config(&[]), &Cancel::never()).await;
assert_eq!(r.err, None);
assert_eq!(*host.sent_keeps.lock().unwrap(), [true]);
let host = Fake::new(vec![answer("ok")]);
let (r, _) = run(&host, &config(&[]), &Cancel::never()).await;
assert_eq!(r.err, None);
assert_eq!(*host.sent_keeps.lock().unwrap(), [false]);
let host = Fake::new(vec![answer("ok")]);
*host.prewarm_match.lock().unwrap() = true;
let mut tc = config(&[]);
tc.ep.external = true;
let (r, _) = run(&host, &tc, &Cancel::never()).await;
assert_eq!(r.err, None);
assert_eq!(*host.sent_keeps.lock().unwrap(), [false]);
}