Repository navigation
Meter the judge's calls apart from the worker's - #56
Conversation
Every ask made around the work (drafting and its critic, the reference check, the review, the post-work audit, the arbiter) now reports its input, output, cache read and cache write tokens to a judge meter. Each call is priced from the worker's price table keyed on the judge's own model id; a model with no known price shows its tokens and $ unknown, never $0, and a subscription judge is paid by its plan. The judge's spend is reported apart from the worker's: judge on job_end (every call since the previous job, so pre-turn drafting counts with its turn), judge_usage journal entries, a judge line and a worker + judge total in the CLI, the TUI and desktop job lines, the ACP prompt response, mission handoffs, and the receipt (index row updated when the job ends, since the review runs after the receipt is written). --budget and the per-turn token and dollar ceilings now count worker and judge spend together. sessionTokens and the step lines stay the worker's, so the lean-budget measurements are unchanged.
bench/local/run.py adds judge_calls, judge_tokens_in/out, judge_cache_read/write and judge_cost_usd to each result row when job_end carries the judge's spend (null cost for an unpriced judge, never 0). docs/transparency.md describes the judge's meter and that a budget counts both; docs/lean.md names the new columns.
The per-run alarm now compares the run's total cost (worker + judge) and total prompt tokens against its limits, and the lane median is over the same totals, so a runaway judge stops the lane too. When the run had a judge the detail shows the split, and the STOPPED row carries the worker and judge figures beside the totals.
|
Follow-up commit: the bench cost alarm ( |
solvyxtech
left a comment
There was a problem hiding this comment.
Grok review of #56 at 44bb43d (comment-only, no merge)
Verdict: no blockers. I'd approve this. (Posted as a comment because GitHub won't take an approval from the PR's own account.)
Pairing rules: all fine. It's on claude/judge-metering and nothing goes straight to main. No subscription backends come back: planFor still only knows Grok Build and OpenCode. The added lines bring in no Claude Code, agy, Antigravity or Gemini CLI names. Nothing touches fine-tune data.
What I ran on 41136f8: npm run typecheck passes. test/judge-meter.test.ts gives 16/16. The full suite has 2451 tests, 2444 pass, 0 fail and 7 todo; the todos are the known adversarial expected failures. The runner exits 1 here only because Node 20's coverage reporter crashes after the tests finish (Could not report code coverage), which looks like this box's environment and not the PR. On 44bb43d, the bench pytest files test_cost_alarm.py and test_judge_columns.py give 18/18. CI was still pending when I posted this.
Non-blocking
-
The per-turn dollar ceiling never sees the judge when the worker has no price. This is at
src/engine.ts:3568-3570and:6713. Confidence: high.- What happens:
totalCostUsd()returnsundefinedwhen the worker is unpriced, soturnStartCostis undefined and the turn falls back to the token ceiling. Say someone runs a local or subscription worker with a paid API judge and sets only a money ceiling (/budget $1, somaxTurnTokensis 0). Thenceilingis 0 and the judge's dollars aren't capped at all. - Why it matters: a local worker with a paid judge is the setup a separate judge exists for, and
docs/transparency.md:188says the per-turn ceiling "in tokens or dollars counts the judge's spend". It's not a regression, since the judge wasn't counted before, but the docs promise more than the code does. - Fix: price the turn as worker plus judge whenever either one is priced, or say this case plainly in the docs. The new money-ceiling test only covers a priced worker.
- What happens:
-
The bench totals add a known worker cost to an unknown judge cost. This is at
bench/local/run.py:710-713. Confidence: medium.- What happens:
run_costsums only the values it knows. A run with a priced worker and an unpriced judge goes into the lane median as worker-only, right next to runs whose judge was priced. - Why it matters: that's the partial sum that
JudgeMeter.since()deliberately refuses ("a partial sum would read as the whole"). For an alarm, a lower bound is fine. For the median, it can quietly pull the limit down when lanes mix priced and unpriced judges. Worth a comment, or leave those rows out of the median.
- What happens:
-
since()keeps an old price after the price is cleared. This is atsrc/judge-meter.ts:169and:188. Confidence: high that it happens, low impact.- What happens:
since()re-prices each stored call, but a call that already carriescostUsdand now resolves to no price comes back from{ ...u }with the old figure. I reproduced it: price a model, record a call, thensetPricing(..., null), andtotal().costUsdis still 1. - Why it matters: the docstring says "Priced now, not when recorded", which isn't quite true. If you mean to keep the recorded price (which is arguably right after a
/modelswitch), say that. Otherwise dropcostUsdbefore re-pricing.
- What happens:
-
Drafting tokens now count toward an existing
--budget. Confidence: high, and it's intended. A small session budget can now be hit before the worker's first step. The CHANGELOG covers it. Maybe give the budget-hit message a hint for that case. -
The judge price lookup runs before the worker's. This is at
src/cli.tsx:930. Confidence: medium.priceJudgeis awaited before the worker's own price lookup, so startup pays for both lookups one after the other (each is capped byprobeSignal). Running them in parallel would avoid that. -
Small nits.
- In
src/receipts.ts:855-869,amendReview's old doc comment now sits on top ofamendJudge's, andamendReviewhas no doc of its own. src/cli.tsx:19-20has two separate imports from./format.js.- Cache writes are priced at the base input rate (
priceTokens). Anthropic bills them higher. That matches the worker's meter, so it's consistent, but both undercount.
- In
-
Naming note for Tyler (no action unless wanted).
docs/transparency.mdnames Anthropic's API as a price source. Anthropic is already a supported pay-as-you-go provider inproviders.ts, so I didn't count this against the bench-lane rename rule from #33.
The metering itself looks right. Every judge path goes through a meter: drafting and the critic through autoDraftFrom, plus the reference check, review, post-work audit and arbiter. job_end is emitted from one place, so the judgeReported mark can't skip calls. Unpriced spend is never shown as $0. The receipt file stays hash-bound, and only the index row gets amended.
Maat did not meter the separate judge model's calls: drafting and its critic, the reference check, the review, the post-work audit and the arbiter all asked the judge, and a bench run could only estimate that cost. This meters every one of them.
What is recorded. Each answered ask reports input, output, cache read and cache write tokens (and the provider's own
usage.costwhen sent) to a judge meter (src/judge-meter.ts). A reply with no usage is estimated and marked~. Calls are priced from the worker's price table (providers.ts), keyed on the judge's model id: the provider's published list (looked up at start for the judge and arbiter models), Anthropic's rates on Anthropic's API, or the worker's own prices when the judge is the worker's model. Unknown price: tokens shown,$ unknown, never$0. A subscription judge is paid by its plan.Where it shows, apart from the worker's spend.
job_end.judge: every judge call since the previous job, so pre-turn drafting counts with its turnjudge_usageentry per call (costUsd: nullwhen unpriced), rendered byJournal.summarizejudge ...line under the job line and under the session totals, thentotal $x (worker $a + judge $b)when both are priced. The TUI and desktop job lines, the ACP prompt_meta, and mission handoffs carry it toojudge ... so farline at the claim. The index row'sjudgeis updated at job end, because the review and the audit run after the receipt is writtenBudget.
--budget//budgetand the per-turn token and dollar ceilings count worker and judge spend together, and the budget-hit message says how much was the judge's. An unpriced judge adds tokens to a token budget and nothing to a dollar ceiling.sessionTokens,spendand the step lines stay the worker's.Bench.
bench/local/run.pyaddsjudge_calls,judge_tokens_in/out,judge_cache_read/write,judge_cost_usdto each row whenjob_endcarries them. The cost alarm still uses the worker's cost.Verification
npm run checkwith~/.local/binoff PATH: 2449 tests, 0 fail (5 todo, unchanged), e2e PASS, self-check PASStest/judge-meter.test.ts(16 tests): a scripted judge with known usage gives exact tokens and $ on job_end, the receipt row and body, and the journal; an unpriced model gives tokens with $ unknown; a token budget and a turn's dollar ceiling count judge spendbench/local/tests/test_judge_columns.py