kernel/event/record/renewal.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What one keep-warm renewal cost, so a person can read off the history
7//! what keeping a prefix's cache alive spends.
8//!
9//! The request is absent for the reason `ModelCalled` carries no body: a
10//! renewal resends a prefix whose bytes the run already recorded, and the
11//! room address the line is written under says which one.
12
13use serde::{Deserialize, Serialize};
14
15use crate::budget::UsdMicros;
16use crate::error::AxError;
17use crate::model::ModelUsage;
18
19/// `cache_renewed`: one renewal a worker sent, answered or refused.
20///
21/// A refusal is a line of its own rather than a note in the diagnostic
22/// log, because a provider that refuses renewals is a fact about what
23/// keep-warm costs, and the log is not the history.
24///
25/// `Refused` is tried first when reading: every field of `Answered` may
26/// be absent, so it would read any line.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[serde(untagged)]
30pub enum CacheRenewed {
31 /// The provider refused the renewal; its refusal, unchanged.
32 Refused {
33 /// The failure the provider returned.
34 refused: AxError,
35 },
36 /// The provider answered the renewal.
37 Answered {
38 /// Four token counts, as the provider reported them. Absent when
39 /// it reported none, which is not the same fact as zero.
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 #[cfg_attr(
42 feature = "schema",
43 schemars(with = "Option<std::collections::BTreeMap<String, u64>>")
44 )]
45 usage: Option<ModelUsage>,
46 /// What the provider itself billed, when it returns a figure.
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 billed_usd_micros: Option<UsdMicros>,
49 },
50}
51
52#[cfg(test)]
53#[allow(clippy::unwrap_used, reason = "test code")]
54mod tests {
55 use super::*;
56 use crate::event::Payload;
57
58 #[test]
59 fn a_refusal_reads_back_as_a_refusal_and_an_empty_answer_as_an_answer() {
60 let refused = CacheRenewed::Refused {
61 refused: AxError::failure(
62 crate::error::AxCode::Provider,
63 "renew a cached prefix",
64 "rate limited",
65 )
66 .with_recovery("renewal is skipped until the next real request"),
67 };
68 let answered = CacheRenewed::Answered {
69 usage: None,
70 billed_usd_micros: None,
71 };
72 for line in [refused, answered] {
73 assert_eq!(
74 Payload::of(&line).unwrap().read::<CacheRenewed>().unwrap(),
75 line
76 );
77 }
78 }
79}