use std::collections::BTreeMap;
use kernel::keep_warm::{CacheUse, KeepWarm, renewal_due};
use kernel::{AxError, B3Hash, Ceiling, Increments, Model, ModelRequest, ModelReturn, TimeMs};
#[derive(Debug, Clone)]
pub struct Warmth {
setting: KeepWarm,
lead_ms: u64,
kept: BTreeMap<[B3Hash; 4], Kept>,
}
#[derive(Debug, Clone)]
struct Kept {
request: ModelRequest<'static>,
cache: CacheUse,
}
impl Warmth {
pub fn new(setting: KeepWarm, lead_ms: u64) -> Warmth {
Warmth {
setting,
lead_ms,
kept: BTreeMap::new(),
}
}
pub fn sent(&mut self, request: &ModelRequest, at_ms: u64) {
match self.setting {
KeepWarm::Off => return,
KeepWarm::FiveMinute => {}
}
self.kept.insert(
request.segments,
Kept {
request: request.clone().into_owned(),
cache: CacheUse::sent(at_ms),
},
);
}
pub fn next_due(&self) -> Option<u64> {
self.kept
.values()
.filter_map(|kept| renewal_due(self.setting, kept.cache, self.lead_ms))
.min()
}
pub fn renew_due(
&mut self,
model: &mut dyn Model,
now_ms: u64,
) -> Result<Vec<ModelReturn>, AxError> {
let (setting, lead_ms) = (self.setting, self.lead_ms);
self.kept
.retain(|_, kept| renewal_due(setting, kept.cache, lead_ms).is_some());
let mut answers = Vec::new();
for kept in self.kept.values_mut() {
if renewal_due(setting, kept.cache, lead_ms).is_some_and(|due| due <= now_ms) {
answers.push(model.call(&renewal_of(&kept.request))?);
kept.cache = kept.cache.renewed(now_ms);
}
}
Ok(answers)
}
}
fn renewal_of(request: &ModelRequest<'static>) -> ModelRequest<'static> {
let mut renewal = request.clone();
renewal.chat.max_tokens = Ceiling::new(1);
renewal
}
pub struct Warmed<C> {
model: Box<dyn Model + Send>,
warmth: Warmth,
clock: C,
}
impl<C: FnMut() -> Result<TimeMs, AxError>> Warmed<C> {
pub fn new(model: Box<dyn Model + Send>, setting: KeepWarm, clock: C) -> Warmed<C> {
Warmed {
model,
warmth: Warmth::new(setting, 0),
clock,
}
}
pub fn next_due(&self) -> Option<u64> {
self.warmth.next_due()
}
pub fn renew_due(&mut self, now_ms: u64) -> Result<Vec<ModelReturn>, AxError> {
let left = (self.clock)()?;
let answers = self.warmth.renew_due(self.model.as_mut(), now_ms)?;
let back = (self.clock)()?;
if !answers.is_empty() {
self.warmth.lead_ms = back.value().saturating_sub(left.value());
}
Ok(answers)
}
fn recorded(
&mut self,
request: &ModelRequest,
send: impl FnOnce(&mut dyn Model) -> Result<ModelReturn, AxError>,
) -> Result<ModelReturn, AxError> {
let left = (self.clock)()?;
let answer = send(self.model.as_mut())?;
let back = (self.clock)()?;
self.warmth.lead_ms = back.value().saturating_sub(left.value());
self.warmth.sent(request, left.value());
Ok(answer)
}
}
impl<C: FnMut() -> Result<TimeMs, AxError>> Model for Warmed<C> {
fn provider_account(&self) -> Option<kernel::event::record::ProviderAccountBinding> {
self.model.provider_account()
}
fn account_roster(&self) -> Option<kernel::model::AccountRoster> {
self.model.account_roster()
}
fn select_account(&mut self, account: &kernel::ServerLabel) {
self.model.select_account(account);
}
fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
self.recorded(req, |model| model.call(req))
}
fn call_streaming(
&mut self,
req: &ModelRequest,
onto: Increments<'_>,
) -> Result<ModelReturn, AxError> {
self.recorded(req, |model| model.call_streaming(req, onto))
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects
)]
mod tests {
use super::*;
use kernel::consts_external::PROMPT_CACHE_TTL_SECS;
use kernel::{BuildingPolicy, ChatRequest, ContentBlock};
const TTL_MS: u64 = PROMPT_CACHE_TTL_SECS * 1000;
const LEAD_MS: u64 = 2_000;
const USED_AT: u64 = 1_000_000;
#[derive(Default)]
struct Provider {
seen: Vec<ModelRequest<'static>>,
}
impl Model for Provider {
fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
self.seen.push(req.clone().into_owned());
Ok(ModelReturn::bare(
kernel::model::message_payload(&[ContentBlock::Text {
text: "warm".to_owned(),
}])
.unwrap(),
Vec::new(),
))
}
}
fn request() -> ModelRequest<'static> {
ModelRequest {
policy: BuildingPolicy::default(),
segments: [B3Hash::digest(b"prefix"); 4],
chat: ChatRequest::empty("script", Ceiling::new(512).unwrap()),
}
}
fn wake_through_two_lifetimes(warmth: &mut Warmth, provider: &mut Provider) {
for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
warmth.renew_due(provider, now).unwrap();
}
}
#[test]
fn the_default_setting_sends_no_request_of_its_own() {
let mut warmth = Warmth::new(KeepWarm::default(), LEAD_MS);
let mut provider = Provider::default();
warmth.sent(&request(), USED_AT);
assert_eq!(warmth.next_due(), None);
wake_through_two_lifetimes(&mut warmth, &mut provider);
assert!(
provider.seen.is_empty(),
"{} renewals sent",
provider.seen.len()
);
}
#[test]
fn five_minute_resends_the_last_request_once_with_one_output_token() {
let mut warmth = Warmth::new(KeepWarm::FiveMinute, LEAD_MS);
let mut provider = Provider::default();
warmth.sent(&request(), USED_AT);
assert_eq!(warmth.next_due(), Some(USED_AT + TTL_MS - LEAD_MS));
wake_through_two_lifetimes(&mut warmth, &mut provider);
let mut renewal = request();
renewal.chat.max_tokens = Ceiling::new(1);
assert_eq!(provider.seen, vec![renewal]);
assert_eq!(warmth.next_due(), None);
}
type Seen = std::sync::Arc<std::sync::Mutex<Vec<ModelRequest<'static>>>>;
struct Behind(Seen);
impl Model for Behind {
fn call(&mut self, req: &ModelRequest) -> Result<ModelReturn, AxError> {
let mut provider = Provider::default();
let answer = provider.call(req);
self.0.lock().unwrap().push(req.clone().into_owned());
answer
}
}
fn door(setting: KeepWarm) -> (Warmed<impl FnMut() -> Result<TimeMs, AxError>>, Seen) {
let seen = Seen::default();
let clock = || Ok(TimeMs::new(USED_AT));
let warmed = Warmed::new(Box::new(Behind(seen.clone())), setting, clock);
(warmed, seen)
}
fn wake_door_through_two_lifetimes(
warmed: &mut Warmed<impl FnMut() -> Result<TimeMs, AxError>>,
) {
for now in (USED_AT..=USED_AT + 2 * TTL_MS).step_by(1_000) {
warmed.renew_due(now).unwrap();
}
}
#[test]
fn the_default_door_forwards_a_run_and_sends_nothing_of_its_own() {
let (mut warmed, seen) = door(KeepWarm::default());
warmed.call(&request()).unwrap();
wake_door_through_two_lifetimes(&mut warmed);
assert_eq!(*seen.lock().unwrap(), vec![request()]);
}
#[test]
fn a_five_minute_door_renews_what_a_run_sent_through_it_once() {
let (mut warmed, seen) = door(KeepWarm::FiveMinute);
warmed.call_streaming(&request(), &mut |_| {}).unwrap();
assert_eq!(warmed.next_due(), Some(USED_AT + TTL_MS));
wake_door_through_two_lifetimes(&mut warmed);
let mut renewal = request();
renewal.chat.max_tokens = Ceiling::new(1);
assert_eq!(*seen.lock().unwrap(), vec![request(), renewal]);
}
}