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 provider_account(&self) -> Option<kernel::event::record::ProviderAccountBinding> {
168 self.model.provider_account()
169 }
170
171 fn account_roster(&self) -> Option<kernel::model::AccountRoster> {
172 self.model.account_roster()
173 }
174
175 fn select_account(&mut self, account: &kernel::ServerLabel) {
176 self.model.select_account(account);
177 }
178
179 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
180 self.recorded(req, |model| model.call(req))
181 }
182
183 fn call_streaming(
184 &mut self,
185 req: &ModelRequest,
186 onto: Increments<'_>,
187 ) -> Result<ModelReturn, AxError> {
188 self.recorded(req, |model| model.call_streaming(req, onto))
189 }
190}
191
192#[cfg(test)]
193#[allow(
194 clippy::unwrap_used,
195 clippy::expect_used,
196 clippy::indexing_slicing,
197 clippy::arithmetic_side_effects
198)]
199mod tests {
200 use super::*;
201 use kernel::consts_external::PROMPT_CACHE_TTL_SECS;
202 use kernel::{BuildingPolicy, ChatRequest, ContentBlock};
203
204 const TTL_MS: u64 = PROMPT_CACHE_TTL_SECS * 1000;
205 const LEAD_MS: u64 = 2_000;
206 const USED_AT: u64 = 1_000_000;
207
208 #[derive(Default)]
210 struct Provider {
211 seen: Vec<ModelRequest<'static>>,
212 }
213
214 impl Model for Provider {
215 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
216 self.seen.push(req.clone().into_owned());
217 Ok(ModelReturn::bare(
218 kernel::model::message_payload(&[ContentBlock::Text {
219 text: "warm".to_owned(),
220 }])
221 .unwrap(),
222 Vec::new(),
223 ))
224 }
225 }
226
227 fn request() -> ModelRequest<'static> {
228 ModelRequest {
229 policy: BuildingPolicy::default(),
230 segments: [B3Hash::digest(b"prefix"); 4],
231 chat: ChatRequest::empty("script", Ceiling::new(512).unwrap()),
232 }
233 }
234
235 fn wake_through_two_lifetimes(warmth: &mut Warmth, provider: &mut Provider) {
238 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
239 warmth.renew_due(provider, now).unwrap();
240 }
241 }
242
243 #[test]
244 fn the_default_setting_sends_no_request_of_its_own() {
245 let mut warmth = Warmth::new(KeepWarm::default(), LEAD_MS);
246 let mut provider = Provider::default();
247 warmth.sent(&request(), USED_AT);
248 assert_eq!(warmth.next_due(), None);
249 wake_through_two_lifetimes(&mut warmth, &mut provider);
250 assert!(
251 provider.seen.is_empty(),
252 "{} renewals sent",
253 provider.seen.len()
254 );
255 }
256
257 #[test]
258 fn five_minute_resends_the_last_request_once_with_one_output_token() {
259 let mut warmth = Warmth::new(KeepWarm::FiveMinute, LEAD_MS);
260 let mut provider = Provider::default();
261 warmth.sent(&request(), USED_AT);
262 assert_eq!(warmth.next_due(), Some(USED_AT + TTL_MS - LEAD_MS));
263 wake_through_two_lifetimes(&mut warmth, &mut provider);
264 let mut renewal = request();
265 renewal.chat.max_tokens = Ceiling::new(1);
266 assert_eq!(provider.seen, vec![renewal]);
267 assert_eq!(warmth.next_due(), None);
268 }
269
270 type Seen = std::sync::Arc<std::sync::Mutex<Vec<ModelRequest<'static>>>>;
273
274 struct Behind(Seen);
275
276 impl Model for Behind {
277 fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
278 let mut provider = Provider::default();
279 let answer = provider.call(req);
280 self.0.lock().unwrap().push(req.clone().into_owned());
281 answer
282 }
283 }
284
285 fn door(setting: KeepWarm) -> (Warmed<impl FnMut() -> Result<TimeMs, AxError>>, Seen) {
286 let seen = Seen::default();
287 let clock = || Ok(TimeMs::new(USED_AT));
288 let warmed = Warmed::new(Box::new(Behind(seen.clone())), setting, clock);
289 (warmed, seen)
290 }
291
292 fn wake_door_through_two_lifetimes(
293 warmed: &mut Warmed<impl FnMut() -> Result<TimeMs, AxError>>,
294 ) {
295 for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
296 warmed.renew_due(now).unwrap();
297 }
298 }
299
300 #[test]
301 fn the_default_door_forwards_a_run_and_sends_nothing_of_its_own() {
302 let (mut warmed, seen) = door(KeepWarm::default());
303 warmed.call(&request()).unwrap();
304 wake_door_through_two_lifetimes(&mut warmed);
305 assert_eq!(*seen.lock().unwrap(), vec![request()]);
306 }
307
308 #[test]
309 fn a_five_minute_door_renews_what_a_run_sent_through_it_once() {
310 let (mut warmed, seen) = door(KeepWarm::FiveMinute);
311 warmed.call_streaming(&request(), &mut |_| {}).unwrap();
312 assert_eq!(warmed.next_due(), Some(USED_AT + TTL_MS));
313 wake_door_through_two_lifetimes(&mut warmed);
314 let mut renewal = request();
315 renewal.chat.max_tokens = Ceiling::new(1);
316 assert_eq!(*seen.lock().unwrap(), vec![request(), renewal]);
317 }
318}