A server's model call could plausibly be billed three ways. Only one of them keeps project #4's seatbelt honest.
Its own budget, per server
Tidy, and wrong. A per-server pot implies an ownership that does not exist, and it lets a server be comfortably "in budget" while your run is already broke.
Untracked, outside the tree
How it works if you don't think about it. Your
MAX_TREE_TOKENSceiling then guards everything except the one spender you don't control, which is a strange place to leave a hole.The same TreeBudget the agents draw from, tagged with who asked
← this one.
Why it has to be the same wallet
Project #4 built one wallet shared by reference across an agent tree, so that the sentence "the whole run stops at 600,000 tokens" was true rather than approximately true.
A server's draw goes on that same tab, or that sentence quietly becomes "600k, plus whatever the servers felt like."
options.budget.add(usage); // the SAME TreeBudget every agent draws fromOne line. It is the line that keeps project #4's seatbelt from becoming a comforting decoration.
One wallet, many spenders, every draw labelled.
The label is the new work
Sharing the wallet is one line. The label is the feature, and it is what
ledger_entries.server_key is for.
create table if not exists ledger_entries (
id bigserial primary key,
run_id uuid references runs(id) on delete cascade,
server_key text not null, -- THE column. everything else is detail.
tool_name text, model text not null,
estimated_cents real not null default 0,
actual_cents real not null default 0,
decision text not null check (decision in ('allowed','refused')),
reason text, round integer not null default 1,
input_tokens integer not null default 0, output_tokens integer not null default 0,
cache_write_tokens integer not null default 0, cache_read_tokens integer not null default 0,
created_at timestamptz not null default now()
);Which turns a question project #4 simply could not answer into a group by:
SPEND BY SERVER ────────────────────────────────────────────────────────── server allowed refused spent refused value kitchen 8 5 13.0¢ 24.2¢
The column people leave out
Look at the right-hand one. Refusals get ledger rows too, with
actual_cents = 0.
Two schema decisions worth defending
consumed_at timestamptz, not a boolean. "Issued at 14:02, spent at
14:03" is a completely different story from "issued and never used", and a
boolean throws that distinction away. The cost of keeping it is one column.
create table if not exists will not add your new columns. Pair every new
column with its own alter table ... add column if not exists — this is
project #4's gotcha 5, still true, and it fails silently: the table exists, so
the statement succeeds, and the column simply isn't there.
alter table runs add column if not exists mode text not null default 'single';What this buys, in one number
Nearly twice as much was refused as was spent. Without the refused column that is an invisible fact, and the ceiling looks like an ornament.