OpenTokenMini
Get the code →Get our help →
For agents
02-measure/schemas/METHOD.md · raw · GitHub

schemas: specs

Code: schemas/cost.py (C-), schemas/check.py (C-sum, C-cache-ttl bound, S-). Constants named in each spec live at the top of those files.

C-tokens: disjoint token counts

Definition: every event carries tokens{input, output, cache_read, cache_write, reasoning}, ints >= 0, disjoint, so their sum is the billed token count. - input = uncached input only (excludes cache reads and cache writes). - output = visible output only (excludes reasoning). - When a harness reports inclusive counts, subtract: input = input_reported - cache_read - cache_write (flag input_includes_cache), output = output_reported - reasoning (flag output_includes_reasoning). - Edge: missing count = 0; a subtraction below 0 (inconsistent harness report) is clamped to 0. - user_prompt and tool_decision events carry all-zero tokens and cost (enforced by S-event-kind). Worked example: harness reports input 13000 incl. cache, cache_read 10000, cache_write 2000, output 250 incl. reasoning 50 → {input:1000, output:200, cache_read:10000, cache_write:2000, reasoning:50}. Test: 02-measure/tests/test_schemas.py Cost.test_c_tokens (also the clamp: input 5 incl. cache_read 10 → 0).

C-price-match: price lookup

schemas/prices.json maps a model substring to USD per million tokens (PER_TOKENS) for input, output, cache_read, cache_write. Lookup lowercases the model id and takes the first key (file order) that is a substring of it. More specific keys come first (opus-5-5 before opus). No match or null model → no price (no default). Seeded from the price table of the earlier from-transcripts.py. Estimated rows: gpt and gemini are one family-level price for all versions seen locally, with cache_write 0 (not billed separately); fable is a list price as recorded there, not re-checked. opus-5-5 is fitted from Claude Code OTel cost_usd (input 4, output 20, cache_read 0.2, cache_write_5m 5, cache_write_1h 8; flat cache_write = 5m rate); sonnet-5-5 (2/10/0.1/2.5/4) is a guess from one sample. Rows may carry a string basis saying where the numbers come from; cost code ignores it. Formula: price = prices[first k in prices if k in lower(model)], else None. Edge cases: null model → None; case-insensitive (Claude-HAIKU-5 → haiku-5); dated ids match their base (claude-opus-5-5-20260901 → opus-5-5, not opus); no match → None, cost then falls to C-cost rule 4. Worked example: claude-opus-5-5 → key opus-5-5 (output 20); Claude-Opus-4-1 → key opus (output 25); mystery-1 → None. Test: 02-measure/tests/test_schemas.py Cost.test_c_price_match.

C-list: list-price cost

cost.input = input * p.input / PER_TOKENS, cost.output = (output + reasoning) * p.output / PER_TOKENS, cost.cache_read = cache_read * p.cache_read / PER_TOKENS, cost.cache_write = cache_write * p.cache_write / PER_TOKENS, cost.total = sum of the four. Unit USD, unrounded floats. Reasoning is billed at the output rate and has no cost field of its own. Edge cases: zero tokens → all parts 0.0; no rounding or cap on huge counts (Python floats, ~1e-16 relative error); no price → C-list does not apply (C-cost rule 4). Worked example: model claude-sonnet-4 (sonnet: 3, 15, 0.3, 3.75), tokens from C-tokens → input 0.003, output 0.00375, cache_read 0.003, cache_write 0.0075, total 0.01725. Test: 02-measure/tests/test_schemas.py Cost.test_c_list.

C-allocate: reported total only

When the harness reports one USD total and no per-type split: weights = C-list parts if the model is priced and their total > 0; else token counts per type (reasoning added to output); part = total * weight / sum(weights). If all weights are 0 the whole total goes to input. cost.total = the reported total exactly; parts match it within SUM_TOL. Worked example: same tokens and model, reported total 0.0345 → input 0.006, output 0.0075, cache_read 0.006, cache_write 0.015. Edge cases: unpriced model with tokens input 1, output 3, total 1.0 → input 0.25, output 0.75; all-zero tokens, total 1.0 → input 1.0. Test: 02-measure/tests/test_schemas.py Cost.test_c_allocate.

C-cost: event cost and cost_basis

First that applies: 1. harness reports per-type USD → those values (reasoning cost already inside output), total = sum, cost_basis:"reported". 2. harness reports only a total → C-allocate, "reported_total_allocated". 3. model priced (C-price-match) → C-list, "list_price". 4. else all five cost fields 0.0, "unknown"; analyze counts these calls in coverage.<harness>.unpriced_calls. Never a guessed price. Edge cases: zero tokens with a price → all zeros, "list_price"; reported split with missing types → those types 0.0; null model → rule 4. Worked example: reported {input:0.1, output:0.2} → cost {0.1, 0.2, 0, 0, total 0.3}, "reported"; same tokens, model mystery-1, nothing reported → all 0.0, "unknown". Test: 02-measure/tests/test_schemas.py Cost.test_c_cost_reported_unknown (rules 1, 4), test_c_allocate (2), test_c_list (3).

C-cache-ttl: cache writes by TTL

Claude bills 5-minute cache writes at 1.25x input and 1-hour writes at 2x input (matched Claude Code OTel cost to 1e-6). Optional token keys cache_write_5m, cache_write_1h (ints >= 0) are subsets of cache_write, which stays the total, so C-tokens disjointness and every sum over TOKEN_TYPES are unchanged. Optional price keys of the same names in prices.json (Claude rows: 1.25x / 2x input). Formula: rest = cache_write - cache_write_5m - cache_write_1h (missing key = 0); cost.cache_write = (rest * p.cache_write + cache_write_5m * p.get(cache_write_5m, p.cache_write) + cache_write_1h * p.get(cache_write_1h, p.cache_write)) / PER_TOKENS. Used by C-list and so by C-allocate weights. Edge cases: no split keys → flat cache_write price (unchanged behavior; the flat Claude values are kept as before and are inconsistent: opus = 1h rate, others = 5m rate, so readers with the split should always pass it); model without TTL prices (gpt, gemini) → flat rate; make_tokens raises cache_write to the split sum if smaller; check.py rejects cache_write_5m + cache_write_1h > cache_write at any depth. Worked example: sonnet (input 3 → 5m 3.75, 1h 6), cache_write 3500, cache_write_5m 1000, cache_write_1h 2000 → (500·3.75 + 1000·3.75 + 2000·6)/1e6 = 0.017625. Test: 02-measure/tests/test_schemas.py Cost.test_c_cache_ttl.

C-basis-worst: session cost_basis

BASIS_ORDER = reported < reported_total_allocated < list_price < unknown. A session's (or any aggregate's) basis is the latest in that order among its api_call events; no events → unknown. Worked example: [reported, list_price, reported] → list_price. Formula: basis = max(bases, key=BASIS_ORDER.index), unknown if empty. Edge: only api_call events count (user_prompt/tool_decision bases are ignored). Test: 02-measure/tests/test_schemas.py Cost.test_c_basis_worst.

C-sum: cost parts add up

For every object with numeric total, input, output, cache_read, cache_write (event, session, totals): |input + output + cache_read + cache_write - total| <= SUM_TOL (1e-6 USD). Checked by check.py at any depth. Worked example: {input:0.1, output:0.2, cache_read:0, cache_write:0, total:0.3} passes; total 0.31 fails. Edge cases: an object missing any of the five fields, or with a non-numeric one, is not a cost object for C-sum and is skipped (the schema reports the missing/bad field itself); 5e-7 off passes, 1e-5 fails. Test: 02-measure/tests/test_schemas.py Broken.test_session_analysis_decision_impact (session, analysis totals), Fixtures.test_cli_rejects_broken_copy (event via CLI).

S-event-kind

Rules (event rows): - decision and decision_source are non-null only on tool_decision; a tool_decision has non-null tool and decision. - user_prompt and tool_decision (ZERO_KINDS in check.py) have every tokens and cost value 0: any(tokens.values()) or any(cost.values()) → error. - No text field anywhere (the event schema allows no extra keys). Edge cases: their cost_basis is free (use "unknown"); an api_call with zero tokens is valid. Worked example: {kind:"tool_decision", tool:"Bash", decision:"accept", tokens:all 0, cost:all 0} passes; same with tokens.input:5 or cost.input:0.5,total:0.5 fails; api_call with decision_source:"user" fails. Test: 02-measure/tests/test_schemas.py Semantic.test_s_event_kind.

S-decision-choice

Rule: set(probabilities) ⊆ set(options) and choice in options or choice == "unsure". Edge cases: empty probabilities is valid; empty options allows only choice:"unsure"; probabilities need not sum to 1 (vendor scores). Worked example: options {a, b}, probabilities {a:0.7, b:0.3}, choice a passes; choice unsure passes; probability key zzz fails; choice maybe fails. Test: 02-measure/tests/test_schemas.py Semantic.test_s_decision_choice, Broken.test_session_analysis_decision_impact.

S-session-order

Rule: parse(end) >= parse(start), both parsed as aware datetimes (Z or ±HH:MM offset, optional fraction), so offsets compare correctly. Writers should still emit UTC Z. Edge cases: end == start passes (single-event session); unparseable ts → no S-session-order error (the ts pattern reports it). Worked example: start 2026-10-01T12:00:00+02:00 (10:00Z), end 2026-10-01T10:30:00Z passes; start 10:30:00Z, end 12:00:00+02:00 fails. Test: 02-measure/tests/test_schemas.py Semantic.test_s_session_order.

S-raw-payload

Rule: a v1 raw line (has v) has exactly one of data (parsed JSON), undecoded (summary of a dropped body: bytes, sha256, content_type, reason) and data_b64 (raw body, base64, only with KEEP_RAW_UNDECODED). Legacy lines (no v) are not checked. Edge cases: neither or both → error. v must be the integer 1 (const is type-strict: 1.0 or true fail). Worked example: {v:1, recv_ts, path:"/v1/logs", content_type, harness_hint:null, data_b64:"AA=="} passes; same without payload, or with both, fails. Test: 02-measure/tests/test_schemas.py Semantic.test_s_raw_payload, Semantic.test_const_strict.