okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petclaude: use one-hour cache writes3d822db

main
5.8 KiB184 linesraw
1// What a Claude request's longest-lived cache marks hold is never reported
2// by a call that reads them whole: the fade finds it in the request ledger,
3// from whichever session last wrote that head, and the E2E fixture provider
4// speaks no Claude protocol that could report the TTL-split writes.
5import albedo/daemon/conversation
6import albedo/daemon/requests
7import albedo/harness/cache_fade.{Fade, Step}
8import albedo/harness/cache_ttl
9import albedo/harness/runtime
10import albedo/openai_api/types
11import gleam/option.{type Option, None, Some}
12import gleeunit/should
13
14@external(erlang, "albedo_runtime_test_support", "temporary_database")
15fn temporary_database() -> String
16
17@external(erlang, "albedo_runtime_test_support", "cleanup")
18fn cleanup(path: String) -> Nil
19
20/// The marks Claude requests carry: tools, system prompt, and moving tail
21/// all use the one-hour TTL.
22const marks = [
23  types.CacheMark(types.ToolsSpan, 3600),
24  types.CacheMark(types.SystemSpan, 3600),
25  types.CacheMark(types.InputSpan(3), 3600),
26]
27
28fn claude() -> cache_ttl.Entry {
29  cache_ttl.Entry(
30    id: "claude",
31    layer: "default",
32    match: cache_ttl.Match(None, None, None),
33    policy: cache_ttl.Refresh,
34    clock: cache_ttl.Request,
35    tiers: Some([
36      cache_ttl.Tier(300, Some(1.25)),
37      cache_ttl.Tier(3600, Some(2.0)),
38    ]),
39    read: Some(0.1),
40    survival: None,
41    evidence: cache_ttl.Documented,
42    source: "",
43    checked: "",
44    note: "",
45  )
46}
47
48fn usage(read: Int, write_5m: Int, write_1h: Int) -> types.Usage {
49  types.Usage(
50    input_tokens: read + write_5m + write_1h + 10,
51    output_tokens: 4,
52    cached_input_tokens: Some(read),
53    cache_creation_tokens: Some(write_5m + write_1h),
54    cache_write_5m_tokens: Some(write_5m),
55    cache_write_1h_tokens: Some(write_1h),
56    reasoning_tokens: None,
57  )
58}
59
60fn fade(
61  usage: types.Usage,
62  measured: fn() -> Option(Int),
63) -> Option(cache_fade.Fade) {
64  cache_fade.fade(Some(claude()), marks, Some(usage), measured, 1000, 9000)
65}
66
67pub fn the_tail_expires_into_the_measured_head_test() -> Nil {
68  // The head is read whole with a cached tail after it, as on a fork or a
69  // rewound branch: after the one-hour tail expires, only the head the
70  // ledger measured is left to read, counted from the send.
71  fade(usage(25_000, 3000, 0), fn() { Some(20_000) })
72  |> should.equal(Some(Fade(1000, True, [Step(3_600_000, Some(0))])))
73  // No call has measured the head: what survives the tail is unknown.
74  fade(usage(25_000, 3000, 0), fn() { None })
75  |> should.equal(Some(Fade(1000, True, [Step(3_600_000, Some(0))])))
76}
77
78pub fn a_call_that_writes_the_head_keeps_all_it_read_test() -> Nil {
79  // A changed system prompt: the tools were read, the rest of the head
80  // written, so every token read survives the one-hour tail.
81  fade(usage(8000, 5000, 14_000), fn() { panic as "the call measured itself" })
82  |> should.equal(Some(Fade(1000, True, [Step(3_600_000, Some(0))])))
83}
84
85pub fn the_ledger_measures_a_head_across_sessions_test() -> Nil {
86  let path = temporary_database()
87  let assert Ok(host) = runtime.start(path)
88  let ledger = runtime.ledger(host)
89  let assert Ok(_) = conversation.initialise(ledger)
90  let record = fn(session, profile, model, head, usage) {
91    let assert Ok(_) =
92      requests.record(
93        ledger,
94        requests.Call(
95          session,
96          requests.Turn,
97          profile,
98          "claude",
99          None,
100          model,
101          0,
102          1,
103          requests.Completed,
104          Some(usage),
105          requests.Prefix(head, 1, Some(0), None, None),
106          marks,
107          session <> "-run",
108        ),
109      )
110    Nil
111  }
112  let assert Ok(_) = conversation.create(ledger, info("parent"))
113  let assert Ok(_) = conversation.create(ledger, info("fork"))
114  // The parent wrote the head after reading its tools: 2k + 18k.
115  record("parent", "claude", "opus", "head", usage(2000, 500, 18_000))
116  // The fork reads it whole, tail and all, which measures nothing.
117  record("fork", "claude", "opus", "head", usage(26_000, 700, 0))
118  requests.head_tokens(ledger, "claude", "opus", "head")
119  |> should.equal(Some(20_000))
120  // A rewrite of the same head measures it again; the latest one counts.
121  record("fork", "claude", "opus", "head", usage(0, 900, 20_500))
122  requests.head_tokens(ledger, "claude", "opus", "head")
123  |> should.equal(Some(20_500))
124  // Another head, model, or profile has its own measure.
125  requests.head_tokens(ledger, "claude", "opus", "other head")
126  |> should.equal(None)
127  requests.head_tokens(ledger, "claude", "sonnet", "head")
128  |> should.equal(None)
129  requests.head_tokens(ledger, "anthropic-api", "opus", "head")
130  |> should.equal(None)
131  runtime.stop(host)
132  cleanup(path)
133}
134
135fn info(id: String) -> conversation.Info {
136  conversation.Info(
137    id,
138    "new session",
139    "/tmp",
140    "claude",
141    "opus",
142    types.Responses,
143    conversation.Idle,
144    None,
145    None,
146  )
147}
148
149pub fn unmarked_providers_fade_by_their_table_entry_test() -> Nil {
150  let unmeasured = fn() { None }
151  let evict =
152    cache_ttl.Entry(
153      ..claude(),
154      policy: cache_ttl.Evict,
155      clock: cache_ttl.Response,
156      tiers: None,
157      survival: Some(cache_ttl.Survival(450, Some(3600))),
158    )
159  // Unknown past typical survival, gone past the bound, from the response.
160  cache_fade.fade(
161    Some(evict),
162    [],
163    Some(usage(4096, 0, 0)),
164    unmeasured,
165    1000,
166    9000,
167  )
168  |> should.equal(
169    Some(Fade(9000, False, [Step(450_000, None), Step(3_600_000, Some(0))])),
170  )
171  // A clock a hit restarts: gone when it runs out.
172  cache_fade.fade(
173    Some(cache_ttl.Entry(..claude(), tiers: Some([cache_ttl.Tier(1800, None)]))),
174    [],
175    Some(usage(4096, 0, 0)),
176    unmeasured,
177    1000,
178    9000,
179  )
180  |> should.equal(Some(Fade(1000, True, [Step(1_800_000, Some(0))])))
181  // Nothing known: no fading at all.
182  cache_fade.fade(None, [], Some(usage(4096, 0, 0)), unmeasured, 1000, 9000)
183  |> should.equal(None)
184}