Skip to content
54 linesCodeBlameRaw
1/**
2 * One run's model spend, as a Durable Object named by its model session.
3 *
4 * Why an object per run: the count must agree across every isolate the
5 * run's requests reach, at once, or an agent that sends many requests in
6 * parallel spends the cap once per isolate. A per-isolate count written
7 * back now and then has exactly that hole, and the proxy has no database
8 * of its own to count in (billing's token count is written after each
9 * answer, for usage views, and read nowhere near this fast). One object
10 * per run is the natural unit: a run is one model session, its object sees
11 * every one of its requests in order, and it costs two short requests per
12 * model answer (admit and settle) plus one storage write, far below what
13 * the answer itself costs. The object forgets the run a while after its
14 * session has lapsed.
15 */
16import { DurableObject } from "cloudflare:workers";
17
18import { type Admission, SpendTally } from "./spend.ts";
19
20/** How long after its first answer a run's count is kept: longer than a model session lives (3 hours). */
21const FORGET_MS = 4 * 60 * 60_000;
22
23export class RunSpend extends DurableObject<object> {
24 private tally = new SpendTally();
25
26 constructor(ctx: DurableObjectState, env: object) {
27 super(ctx, env);
28 void ctx.blockConcurrencyWhile(async () => {
29 this.tally = new SpendTally((await ctx.storage.get<number>("spent")) ?? 0);
30 });
31 }
32
33 /** Whether the run may start another answer under `cap`, in millionths of a dollar. */
34 async admit(cap: number): Promise<Admission> {
35 const admission = this.tally.admit(cap, Date.now());
36 if (admission.ok && admission.spent === 0 && (await this.ctx.storage.getAlarm()) === null) {
37 await this.ctx.storage.setAlarm(Date.now() + FORGET_MS);
38 }
39 return admission;
40 }
41
42 /** An answer has ended, costing `micros`. Returns what the run has spent. */
43 async settle(ticket: string, micros: number): Promise<number> {
44 const before = this.tally.spent;
45 const spent = this.tally.settle(ticket, micros);
46 if (spent !== before) await this.ctx.storage.put("spent", spent);
47 return spent;
48 }
49
50 override async alarm(): Promise<void> {
51 await this.ctx.storage.deleteAll();
52 this.tally = new SpendTally();
53 }
54}