1use std::collections::BTreeMap;
15
16use kernel::keep_warm::{CacheUse, KeepWarm, renewal_due};
17use kernel::{AxError, B3Hash, Ceiling, Increments, Model, ModelRequest, ModelReturn, TimeMs};
18
19#[derive(Debug, Clone)]
21pub struct Warmth {
22 setting: KeepWarm,
23 lead_ms: u64,
24 kept: BTreeMap<[B3Hash; 4], Kept>,
25}
26
27#[derive(Debug, Clone)]
29struct Kept {
30 request: ModelRequest<'static>,
31 cache: CacheUse,
32}
33
34impl Warmth {
35 pub fn new(setting: KeepWarm, lead_ms: u64) -> Warmth {
38 Warmth {
39 setting,
40 lead_ms,
41 kept: BTreeMap::new(),
42 }
43 }
44
45 pub fn sent(&mut self, request: &ModelRequest, at_ms: u64) {
47 match self.setting {
48 KeepWarm::Off => return,
49 KeepWarm::FiveMinute => {}
50 }
51 self.kept.insert(
52 request.segments,
53 Kept {
54 request: request.clone().into_owned(),
55 cache: CacheUse::sent(at_ms),
56 },
57 );
58 }
59
60 pub fn next_due(&self) -> Option<u64> {
63 self.kept
64 .values()
65 .filter_map(|kept| renewal_due(self.setting, kept.cache, self.lead_ms))
66 .min()
67 }
68
69 pub fn renew_due(
76 &mut self,
77 model: &mut dyn Model,
78 now_ms: u64,
79 ) -> Result<Vec<ModelReturn>, AxError> {
80 let (setting, lead_ms) = (self.setting, self.lead_ms);
81 self.kept
82 .retain(|_, kept| renewal_due(setting, kept.cache, lead_ms).is_some());
83 let mut answers = Vec::new();
84 for kept in self.kept.values_mut() {
85 if renewal_due(setting, kept.cache, lead_ms).is_some_and(|due| due <= now_ms) {
86 answers.push(model.call(&renewal_of(&kept.request))?);
87 kept.cache = kept.cache.renewed(now_ms);
88 }
89 }
90 Ok(answers)
91 }
92}
93
94fn renewal_of(request: &ModelRequest<'static>) -> ModelRequest<'static> {
98 let mut renewal = request.clone();
99 renewal.chat.max_tokens = Ceiling::new(1);
100 renewal
101}
102
103pub struct Warmed<C> {
112 model: Box<dyn Model + Send>,
113 warmth: Warmth,
114 clock: C,
115}
116
117impl<C: FnMut() -> Result<TimeMs, AxError>> Warmed<C> {
118 pub fn new(model: Box<dyn Model + Send>, setting: KeepWarm, clock: C) -> Warmed<C> {
120 Warmed {
121 model,
122 warmth: Warmth::new(setting, 0),
123 clock,
124 }
125 }
126
127 pub fn next_due(&self) -> Option<u64> {
129 self.warmth.next_due()
130 }
131
132 pub fn renew_due(&mut self, now_ms: u64) -> Result<Vec<ModelReturn>, AxError> {
138 let left = (self.clock)()?;
139 let answers = self.warmth.renew_due(self.model.as_mut(), now_ms)?;
140 let back = (self.clock)()?;
141 if !answers.is_empty() {
145 self.warmth.lead_ms = back.value().saturating_sub(left.value());
146 }
147 Ok(answers)
148 }
149
150 fn recorded(
153 &mut self,
154 request: &ModelRequest,
155 send: impl FnOnce(&mut dyn Model) -> Result<ModelReturn, AxError>,
156 ) -> Result<ModelReturn, AxError> {
157 let left = (self.clock)()?;
158 let answer = send(self.model.as_mut())?;
159 let back = (self.clock)()?;
160 self.warmth.lead_ms = back.value().saturating_sub(left.value());
161 self.warmth.sent(request, left.value());
162 Ok(answer)
163 }
164}
165
166impl<C: FnMut() -> Result<TimeMs, AxError>> Model for Warmed<C> {
167 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
168 self.recorded(req, |model| model.call(req))
169 }
170
171 fn call_streaming(
172 &mut self,
173 req: &ModelRequest,
174 onto: Increments<'_>,
175 ) -> Result<ModelReturn, AxError> {
176 self.recorded(req, |model| model.call_streaming(req, onto))
177 }
178}
179
180#[cfg(test)]
181#[allow(
182 clippy::unwrap_used,
183 clippy::expect_used,
184 clippy::indexing_slicing,
185 clippy::arithmetic_side_effects
186)]
187mod tests {
188 use super::*;
189 use kernel::consts_external::PROMPT_CACHE_TTL_SECS;
190 use kernel::{BuildingPolicy, ChatRequest, ContentBlock};
191
192 const TTL_MS: u64 = PROMPT_CACHE_TTL_SECS * 1000;
193 const LEAD_MS: u64 = 2_000;
194 const USED_AT: u64 = 1_000_000;
195
196 #[derive(Default)]
198 struct Provider {
199 seen: Vec<ModelRequest<'static>>,
200 }
201
202 impl Model for Provider {
203 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
204 self.seen.push(req.clone().into_owned());
205 Ok(ModelReturn::bare(
206 kernel::model::message_payload(&[ContentBlock::Text {
207 text: "warm".to_owned(),
208 }])
209 .unwrap(),
210 Vec::new(),
211 ))
212 }
213 }
214
215 fn request() -> ModelRequest<'static> {
216 ModelRequest {
217 policy: BuildingPolicy::default(),
218 segments: [B3Hash::digest(b"prefix"); 4],
219 chat: ChatRequest::empty("script", Ceiling::new(512).unwrap()),
220 }
221 }
222
223 fn wake_through_two_lifetimes(warmth: &mut Warmth, provider: &mut Provider) {
226 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
227 warmth.renew_due(provider, now).unwrap();
228 }
229 }
230
231 #[test]
232 fn the_default_setting_sends_no_request_of_its_own() {
233 let mut warmth = Warmth::new(KeepWarm::default(), LEAD_MS);
234 let mut provider = Provider::default();
235 warmth.sent(&request(), USED_AT);
236 assert_eq!(warmth.next_due(), None);
237 wake_through_two_lifetimes(&mut warmth, &mut provider);
238 assert!(
239 provider.seen.is_empty(),
240 "{} renewals sent",
241 provider.seen.len()
242 );
243 }
244
245 #[test]
246 fn five_minute_resends_the_last_request_once_with_one_output_token() {
247 let mut warmth = Warmth::new(KeepWarm::FiveMinute, LEAD_MS);
248 let mut provider = Provider::default();
249 warmth.sent(&request(), USED_AT);
250 assert_eq!(warmth.next_due(), Some(USED_AT + TTL_MS - LEAD_MS));
251 wake_through_two_lifetimes(&mut warmth, &mut provider);
252 let mut renewal = request();
253 renewal.chat.max_tokens = Ceiling::new(1);
254 assert_eq!(provider.seen, vec![renewal]);
255 assert_eq!(warmth.next_due(), None);
256 }
257
258 type Seen = std::sync::Arc<std::sync::Mutex<Vec<ModelRequest<'static>>>>;
261
262 struct Behind(Seen);
263
264 impl Model for Behind {
265 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
266 let mut provider = Provider::default();
267 let answer = provider.call(req);
268 self.0.lock().unwrap().push(req.clone().into_owned());
269 answer
270 }
271 }
272
273 fn door(setting: KeepWarm) -> (Warmed<impl FnMut() -> Result<TimeMs, AxError>>, Seen) {
274 let seen = Seen::default();
275 let clock = || Ok(TimeMs::new(USED_AT));
276 let warmed = Warmed::new(Box::new(Behind(seen.clone())), setting, clock);
277 (warmed, seen)
278 }
279
280 fn wake_door_through_two_lifetimes(
281 warmed: &mut Warmed<impl FnMut() -> Result<TimeMs, AxError>>,
282 ) {
283 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
284 warmed.renew_due(now).unwrap();
285 }
286 }
287
288 #[test]
289 fn the_default_door_forwards_a_run_and_sends_nothing_of_its_own() {
290 let (mut warmed, seen) = door(KeepWarm::default());
291 warmed.call(&request()).unwrap();
292 wake_door_through_two_lifetimes(&mut warmed);
293 assert_eq!(*seen.lock().unwrap(), vec![request()]);
294 }
295
296 #[test]
297 fn a_five_minute_door_renews_what_a_run_sent_through_it_once() {
298 let (mut warmed, seen) = door(KeepWarm::FiveMinute);
299 warmed.call_streaming(&request(), &mut |_| {}).unwrap();
300 assert_eq!(warmed.next_due(), Some(USED_AT + TTL_MS));
301 wake_door_through_two_lifetimes(&mut warmed);
302 let mut renewal = request();
303 renewal.chat.max_tokens = Ceiling::new(1);
304 assert_eq!(*seen.lock().unwrap(), vec![request(), renewal]);
305 }
306}