Repository navigation
fix(pricing): price Delegate's underscored model ids and flag unpriced judge/simulator cost - #223
abhiram-vad wants to merge 7 commits into
Conversation
…choes The Delegate backend names its models gpt_5_6_luna, claude_opus_5_5 and so on, and the rate card keys the hyphenated form (gpt-5-6-luna). The out-of-tree delegate-sdk adapter mapped "_" to "-" before it priced a turn; the port to the built-in agent dropped that mapping. A config written in the backend's own ids therefore priced every agent turn at None, so a run's cost held only its judges, and the pricing pre-flight warned that the model had no rate. delegate_rate_key does the mapping in one place. The adapter prices each turn under it, and check_pricing_coverage looks a Delegate agent's model up under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @abhiram-vad's task in 1m 59s —— View job PR Review in Progress
|
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — PR #223 "fix(delegate): price the underscored model ids the Delegate backend echoes" by @abhiram-vad · abhiram/fix/delegate-underscore-model-pricing → main · OPEN
Scope: PR #223 "fix(delegate): price the underscored model ids the Delegate backend echoes" by @abhiram-vad · abhiram/fix/delegate-underscore-model-pricing → main · OPEN · branch pr-223 (abhiram/fix/delegate-underscore-model-pricing) · 96edd48 · 2026-10-12T19:06Z · workflow variant
Change class: simple — maps the Delegate backend's underscored model ids to the rate-card key at the two pricing lookups (turn cost + pre-flight), with tests; no schema, control-flow or rate change
The codebase is healthy: no finding can change a task's score or final_status for identical agent output, type safety and security have no findings, and the only real risk is that the Delegate underscore-to-hyphen rate-key mapping sits outside the shared pricing seam, so cost accounting can drift again between the pre-flight, the runtime agent and the evalboard; the bottom line is to merge after you move that mapping into one shared place and add the missing inverse and fallback tests.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.9 / 10 | 0 | 0 | 0 | 1 | _run_models branch re-tests agent None-ness and splits one yield across two arms |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 9.8 / 10 | 0 | 0 | 0 | 2 | The new Delegate-only branch in _run_models is tested only on its happy path; nothing pins that other agents' ids are NOT rewritten |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 9.5 / 10 | 0 | 0 | 1 | 0 | Delegate rate-key mapping applied at two call sites, with an AgentKind.DELEGATE branch in core batch.py pre-flight that goes around the agent SPI and the shared _normalize_model seam |
| 6. Error Handling & Resilience | 9.9 / 10 | 0 | 0 | 0 | 1 | Delegate _finalize_turn still drops an unpriced turn's cost to None without a per-turn diagnostic |
| 7. API Surface & Maintainability | 9.8 / 10 | 0 | 0 | 0 | 2 | The pre-flight warning and the return value now name the rewritten rate key, not the model id the user wrote |
| 8. Evaluation Harness Quality | 9.9 / 10 | 0 | 0 | 0 | 1 | delegate_rate_key is a second normalization seam outside the mirrored _normalize_model, so the evalboard and Python/run.json resolve underscored Delegate ids differently |
Overall Score: 9.9 / 10 · Weakest Axis: Architecture & Design at 9.5 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 1 · 🔵 7 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
- [Axis 5] Delegate rate-key mapping applied at two call sites, with an AgentKind.DELEGATE branch in core batch.py pre-flight that goes around the agent SPI and the shared _normalize_model seam (
src/coder_eval/orchestration/batch.py:57) —_run_modelsused to treat every agent the same way (yield rt.task.agent.model if rt.task.agent else None). The PR replaces that with a hardcoded dispatch on one agent kind:
agent = rt.task.agent
if agent is not None and agent.model and agent.type == AgentKind.DELEGATE:
yield delegate_rate_key(agent.model)
else:
yield agent.model if agent else None
Before this PR, batch.py named only AgentKind.UNKNOWN. CLAUDE.md says agents register through the plugin SPI, that "there is no closed enum or dispatch to edit, and in-tree and third-party agents take the same path". Review criterion 16 rejects if x.type == ... ladders in core. This PR creates exactly such a dispatch: the in-tree Delegate agent now gets a core-side id-to-rate-key hook that a third-party agent cannot get without editing orchestration/batch.py. The id knowledge is also split across three modules: the rule in pricing.py:62, the call in delegate_agent.py:1069 and the branch here. Each call site that turns a Delegate model into a rate key must remember to apply it.
One fix removes the branch and both call-site wrappers. Put the mapping in the shared lookup as an exact-first fallback, for example in _lookup_rate / is_priced / calculate_cost: try key, then key.replace("_", "-"). No built-in rate key contains _, so built-in prices do not change. An underscore-keyed plugin rate is matched exactly first, so it still resolves to its own rate. After that, _run_models returns to agent-agnostic code and DelegateAgent calls calculate_cost(state.model_used or ...) with no wrapper. If the mapping must stay agent-scoped, use a polymorphic hook instead (for example an AgentConfig.rate_key() that defaults to self.model and that DelegateAgentConfig overrides). Do not branch on the enum in batch.py.
Nits
- [Axis 1] _run_models branch re-tests agent None-ness and splits one yield across two arms (
src/coder_eval/orchestration/batch.py:57) —if agent is not None and agent.model and agent.type == AgentKind.DELEGATE:/yield delegate_rate_key(agent.model)/else:/yield agent.model if agent else Nonechecksagenttwice and yields the same model through two paths. radon now scores it B(7), up from about B(5). If you keep the branch, compute the model once:model = agent.model if agent else Noneand then one yield. If you take the generic fix in the previous finding, restore the original single lineyield rt.task.agent.model if rt.task.agent else None. - [Axis 3] The new Delegate-only branch in _run_models is tested only on its happy path; nothing pins that other agents' ids are NOT rewritten (
tests/test_cost_accounting_paths.py:93) — The new testtest_delegate_backend_id_is_priced_under_its_rate_card_key(line 93:resolved.task.agent = DelegateAgentConfig(type="delegate", model="gpt_5_6_luna")) covers only the Delegate side of the new guard in batch.py:57 (if agent is not None and agent.model and agent.type == AgentKind.DELEGATE:). No test covers the inverse. If someone deleted theagent.type == AgentKind.DELEGATEcondition, or moved thereplace("_", "-")intois_priced, every test would still pass. A claude-code task withmodel: claude_sonnet_5would then pass the pre-flight silently, even though the runtime agent (which does not call delegate_rate_key) books that model's turns at cost None. The pre-flight and the runtime would disagree, and that disagreement is the defect this PR fixes. Add a mirror test:_resolved("claude_sonnet_5", tmp_path)(claude-code agent) givescheck_pricing_coverage(...) == ["claude_sonnet_5"]and logs the warning. Also give_resolvedanagent=parameter so the test does not have to replacetask.agentafter construction, because that replacement skips TaskDefinition's own validation. - [Axis 3] The underscored-id agent test cannot tell whether the configured-model fallback is normalized, because the echoed id and the configured id are the same (
tests/test_delegate_agent.py:1017) —test_cost_prices_the_underscored_id_the_backend_echoessets the same underscored id on both sides:model="gpt_5_6_luna"in_result(...)(line 1013) and_started_agent(..., model="gpt_5_6_luna")(line 1017). The production expression isdelegate_rate_key(state.model_used or self.config.model or "")(delegate_agent.py:1069). This test only exercises thestate.model_usedoperand. Suppose someone narrowed the fix todelegate_rate_key(state.model_used) if state.model_used else self.config.model. This test would still pass, but a backend frame that echoes no model would leave a config written asmodel: gpt_5_6_lunaunpriced. That config is the exact scenario the new notes paragraph describes. The existing no-echo test,test_cost_wired_into_record_for_configured_model, uses only a dashed id (virtuoso-1-5). Parametrize the new test over (echoed=None, config="gpt_5_6_luna") and (echoed="gpt_5_6_luna", config=None). Both cases must asserttotal_cost_usd == pytest.approx(0.20 + 1.20). - [Axis 6] Delegate _finalize_turn still drops an unpriced turn's cost to None without a per-turn diagnostic (
src/coder_eval/agents/delegate_agent.py:1075) — The Delegate backend reports no cost of its own, so this rate-card lookup is the only thing that prices any Delegate turn. A miss (if cost is not None: usage = usage.model_copy(...)at lines 1075-1076, with no else branch) leavestotal_cost_usdas None and logs nothing. This silent miss is what let the underscored-id defect ship without anyone noticing. After this PR, two cases still miss without a log line: a Delegate model left to the route (config.model unset, so the pre-flight in batch._run_models cannot see it), and a backend-echoedstate.model_usedthat is not in_DELEGATE_PRICING. The run is not totally blind: the run-levelRunSummary.cost_completeflag and the orchestrator's warning 'max_usd budget configured but no turn reported cost' (orchestrator.py:1174) both still fire. That is why this is Low, not Medium. ClaudeCodeAgent._backfill_cost and _reprice_for_litellm both log a warning on the same miss. Fix: add anelse: logger.warning("No pricing for delegate model %r (rate key %r); turn cost left unset", state.model_used or self.config.model, key)that names both the raw id and the normalized key. With that line, the next id-shape mismatch shows up on the first turn and not in a downstream suite's cost totals. - [Axis 7] The pre-flight warning and the return value now name the rewritten rate key, not the model id the user wrote (
src/coder_eval/orchestration/batch.py:58) —_run_modelsnow yieldsdelegate_rate_key(agent.model), sounpriced_modelsreturns, and the warning at line 84 ("No pricing rate for %s. ...") prints, the hyphenated key. Example: a task withmodel: gpt_9_imaginarywarns about'gpt-9-imaginary', which is a string that does not occur in the user's YAML. This also changes the documented return contract at line 78 (The sorted, de-duplicated unpriced model ids): it now returns rate-card keys, not configured model ids. Recommendation: check the rate key, but report the authored id (for example, collect(authored, key)pairs and return or log the authored one, or the formgpt_9_imaginary (rate key gpt-9-imaginary)). Also update the docstring so the return value is stated without ambiguity. - [Axis 7] The comment on the Delegate rate block still says the ids are hyphenated, but the new helper is about underscored ids and is also used for non-Delegate keys (
src/coder_eval/pricing.py:62) —delegate_rate_keysits directly under_DELEGATE_PRICING/DELEGATE_MODEL_IDS, whose comment at line 37 still reads# Models the Delegate agent serves, under the SDK's own hyphenated ids. But the helper also maps backend ids that land in the general_PRICINGtable. The PR body showsclaude_opus_5_5->claude-opus-5-5priced at the Anthropic rate. A reader can therefore think the helper only applies to_DELEGATE_PRICINGkeys. Recommendation: change the block comment to say that the backend echoes underscored ids and thatdelegate_rate_keyis the mapping for every id a Delegate agent reports, not only the ids in this block. Or move the helper to the single seam suggested in finding 1, so this comment does not need to cover it. - [Axis 8] delegate_rate_key is a second normalization seam outside the mirrored _normalize_model, so the evalboard and Python/run.json resolve underscored Delegate ids differently (
src/coder_eval/pricing.py:62) —def delegate_rate_key(model: str) -> str: ... return model.replace("_", "-")sits beside_normalize_model. evalboard/lib/pricing.ts:37 says itsnormalizeModelis the "mirror of src/coder_eval/pricing.py::_normalize_model". Before this PR, Python and the board both pricedgpt_5_6_lunaas unpriced, which was at least consistent. Now the Python task total shows the full cost, while the board's Tokens-to-USD view, the thinking-cost simulator andtallyModelsshow—, or splitgpt_5_6_lunaandgpt-5-6-lunainto two models. The PR body admits this gap and defers the fix. CE065 compares only the rate table, not normalization, so nothing catches the drift. Low because the board's task total and per-message Cost column stay correct. Track the board fix (portdelegateRateKeyto lib/pricing.ts at the same call sites). Also consider extending the CE065 parity check to cover the normalization helpers, so the next normalization rule cannot drift silently.
What's Missing
Daily/nightly:
- 🟡 The PR names the Scribe suite run, but it does not say what happens to the nightly pipeline. After merge, Delegate rows on underscored ids go from None to a real agent cost. In the example run that is about +$1.11 on a $0.79 judge-only total. RunSummary.cost_complete flips to true, and the downstream cost series show a step change. Runs already in blob storage (for example adhoc-2026-10-08_21-33-28) stay understated, because
evaluate <run_dir>re-grades but does not re-price agent turns, and the PR gives no backfill path. State this in the PR, or give a one-off re-price recipe for past runs. (trigger: src/coder_eval/agents/delegate_agent.py)
Downstream consumers:
- 🔵 The orchestrator's max_usd gate (orchestrator.py:1169-1180) skipped Delegate turns on underscored ids before, because every turn reported cost None ('no turn reported cost; skipping cost check'). It now enforces the budget. A nightly Delegate task with a tight max_usd that always passed can now stop with a budget error. Nothing in the PR or in docs/agents/DELEGATE.md says that this gate is newly live. (trigger: src/coder_eval/agents/delegate_agent.py)
Display & mapping dicts:
- 🔵 evalboard/lib/pricing.ts normalizeModel/resolvePricing (the documented mirror of _normalize_model) does not get the
_->-mapping. Underscored Delegate ids therefore show '—' in the Tokens-to-USD view and in the thinking-cost simulator, and tallyModels counts the same model twice. The PR body acknowledges this and defers it. CE065 diffs only the rate table, so nothing tracks it. (trigger: src/coder_eval/pricing.py) _(restates: Axis 8: delegate_rate_key is a second normalization seam outside the mirrored normalize_model)
Tests:
- 🔵 No test covers the unpriced side of the new Delegate pre-flight branch. A Delegate task with an underscored id that has no rate (for example
gpt_9_imaginary) should still return a non-empty list and log 'No pricing rate'. No test pins which spelling the warning and the return value use (the authored id or the rewritten key). (trigger: tests/test_cost_accounting_paths.py) (restates: Axis 7: The pre-flight warning and the return value now name the rewritten rate key, not the model id the user wrote) - 🔵 No test covers the inverse of the Delegate-only branch in batch._run_models: an underscored id on a non-Delegate agent (claude-code or pi) must still be reported as unpriced. The PR body's manual table shows this for pi, but no test pins it. (trigger: src/coder_eval/orchestration/batch.py) _(restates: Axis 3: The new Delegate-only branch in run_models is tested only on its happy path)
- 🔵 The configured-model fallback (
state.model_usedis None andconfig.modelisgpt_5_6_luna) is not tested through delegate_rate_key. The new agent test sets the same underscored id on both the echoed operand and the configured operand. (trigger: tests/test_delegate_agent.py) (restates: Axis 3: The underscored-id agent test cannot tell whether the configured-model fallback is normalized)
Parallel paths:
- 🔵 The user-facing Delegate guide is not updated. docs/agents/DELEGATE.md line 126 documents
model_usedas the result frame'smodel, and every example uses dashed ids (virtuoso-1-5). The guide does not say that underscored backend ids (gpt_5_6_luna,claude_opus_5_5) are accepted inagent.model, or that cost is priced under the hyphenated rate key whilemodel_usedkeeps the backend spelling. The rationale went only into .claude/notes/agents.md, which users do not read. (trigger: .claude/notes/agents.md)
Harness & Lint Improvements
Static checks (lint / type):
- [ce-lint] CE070 no-agent-kind-dispatch-in-core: an AST rule in tests/lint/rules/ce070_no_agent_kind_dispatch_in_core.py, wired in tests/lint/runner.py. It flags any Compare node (==, !=, is, in) and any
match/casepattern whose operand is a concreteAgentKind.<MEMBER>(orAgentKind.<MEMBER>.value). Scope: src/coder_eval/ except agents/ and plugins/. The sentinel members NONE and UNKNOWN are allowed because they mean 'no agent', not 'this agent'. Construction such asparse_agent_config(type=AgentKind.CLAUDE_CODE)in cli/evaluate_command.py:189 is not a comparison, so the rule does not flag it. Today the rule finds one existing hit: config.py:203 (agent_type == AgentKind.CLAUDE_CODE.valuein validate_api_keys). Fix that hit with a polymorphic hook, or record it as a documentednoqa: CE070with a reason. The rule enforces the CLAUDE.md contract 'no closed enum or dispatch to edit'. Per-agent behaviour must go through the agent SPI or an AgentConfig method, for exampleAgentConfig.rate_key(). It must not go through a branch in orchestration/. Prevents: The medium Architecture finding at src/coder_eval/orchestration/batch.py:57:if agent is not None and agent.model and agent.type == AgentKind.DELEGATE:adds a Delegate-only branch to the core pre-flight. It also prevents the follow-on Code Quality finding at the same line (the double None check and one yield split across two arms, radon B(7)), because that finding exists only because of the branch. - [ce-lint] Extend CE065 (tests/lint/pricing_mirror.py) so it mirrors the model-id normalizer, not only the rate table. Move the normalizer data out of
_normalize_modelinto module-level constants in src/coder_eval/pricing.py: the routing prefixes,_BEDROCK_REGION_PREFIXES, theanthropic.vendor prefix, and any character-substitution rule such as Delegate's_->-once that rule moves into the shared seam.render_pricingthen emits these constants into evalboard/lib/pricing.generated.ts, andnormalizeModelin evalboard/lib/pricing.ts reads them from that file. It must not keep its own hand-copied_ROUTING_PREFIXES/_REGION_PREFIXESarrays. After this change, a new normalization rule in Python that is not regenerated failsmake verifyas CE065 drift. Also add an assertion to the generator:is_pricedandcalculate_costmust reach the model id only through_normalize_model, and pricing.py must export no other public function that rewrites a model id (an AST check for a publicdefin pricing.py that returnsmodel.replace(...)/removeprefix(...)). This blocks a second seam such asdelegate_rate_key. Prevents: The Harness Quality finding at src/coder_eval/pricing.py:62:delegate_rate_keyis a second normalization seam outside the mirrored_normalize_model, so Python and run.json pricegpt_5_6_lunawhile the evalboard shows—or splits the model in two. It also prevents the API Surface finding at pricing.py:62 (the stale 'hyphenated ids' block comment that describes the helper), because the helper would be folded into the single seam. - [ce-lint] Remove the sharp edge first, then add the rule. Add one helper to pricing.py, for example
price_turn(model, ...) -> float | None. It wrapscalculate_costand logs one warning per distinct id on a miss, naming both the raw id and the normalized key (No pricing for model %r (rate key %r); turn cost left unset). Then add CE071 agents-price-through-price-turn (tests/lint/rules/ce071_agents_price_through_price_turn.py, wired in runner.py). It forbids importing or callingcalculate_cost/_lookup_ratefrom src/coder_eval/agents/. Today these call sites call calculate_cost directly, and most have no miss diagnostic: antigravity_agent.py:237, codex_agent.py:2292/2348/2384, delegate_agent.py:1068, opencode_agent.py:430, pi_agent.py:585 (claude_code_agent.py:1569/1593 logs on a miss). The rule also stops an agent from wrapping the id at its own call site (calculate_cost(delegate_rate_key(...))), because id mapping then has only one entry point. Prevents: The Error Handling finding at src/coder_eval/agents/delegate_agent.py:1075: an unpriced Delegate turn drops cost to None with no per-turn log. This silent miss is what let the underscored-id defect ship. The rule also prevents the per-call-site wrapper part of the Architecture finding (delegate_agent.py:1069), and it closes the same silent-miss gap in the other agents that do not log.
Harness improvements (not statically reachable):
- Add a pre-flight vs runtime pricing parity contract test, tests/test_pricing_preflight_parity.py. It is parametrized over every registered AgentKind (taken from the plugin registry, not a hand list), and for each kind over a set of model-id spellings: a canonical dashed id, the underscored backend form, a routing-prefixed form, and an unknown id. For each case it asserts that
check_pricing_coverage(orchestration/batch.py_run_models) reports the id as unpriced exactly when that agent's runtime cost path (driven through a stub turn) returnstotal_cost_usd is None. A Delegate-only happy-path test cannot catch a guard that was deleted or widened. This matrix catches it in both directions. Why not static: Pre-flight and runtime agree only if two code paths compute the same answer for the same input. This needs both paths to run, including each agent's turn-finalization logic. An AST check cannot evaluate what id an agent passes to the rate card at runtime. Prevents: The Test Health finding at tests/test_cost_accounting_paths.py:93 (only the Delegate side of the batch.py:57 guard is tested; nothing pins that claude-code ids are not rewritten). It also prevents the root defect class behind the Architecture finding at batch.py:57: pre-flight and runtime disagree about whether a model is priced. - Add a shared per-agent cost ContractCase table, modeled on CE036's live_verdict_contract. Each agent declares cases over (echoed model_used, configured config.model): (echoed, None), (None, configured), (echoed, configured), each with a dashed and an underscored id. Each case gives the expected
total_cost_usd. One generic test runs every agent through those cases. Add a lint-marked test in tests/test_custom_lint.py that fails when an agent registers without cost cases, the same way CE036 fails for a live criterion without ContractCases. Why not static: The contract is about the value an agent computes from a backend frame and its config, through the expressionstate.model_used or self.config.model. Only executing that expression shows whether each operand is normalized. A static rule can require that the cases exist. It cannot check that they pass. Prevents: The Test Health finding at tests/test_delegate_agent.py:1017: the underscored-id test uses the same id for the echoed model and the configured model, so the configured-model fallback at delegate_agent.py:1069 is never tested on its own. - Add a cross-language normalizer golden-vector fixture, for example tests/fixtures/model_id_normalization.json, that maps a raw id to the expected rate key:
eu.anthropic.claude-opus-4-8->claude-opus-4-8,openrouter/deepseek/deepseek-v4-pro->deepseek/deepseek-v4-pro,gpt_5_6_luna->gpt-5-6-luna, and so on. Both a pytest test (_normalize_model) and a vitest test (normalizeModelin evalboard/lib/tests/) read this one file, somake verifyandmake evalboard-verifyboth fail when the two implementations diverge. Why not static: The constants can be mirrored by generation (see the CE065 extension), but the control flow of normalizeModel stays hand-written TypeScript: prefix order, break-after-first-match, trim. Two implementations in two languages are behaviourally equivalent only if both run on the same inputs. Prevents: The Harness Quality finding at src/coder_eval/pricing.py:62: Python/run.json and the evalboard resolve underscored Delegate ids differently (the Tokens-to-USD view, the thinking-cost simulator and tallyModels show—or split one model in two). - In tests/test_cost_accounting_paths.py, add a caplog assertion that the pre-flight warning and the
check_pricing_coveragereturn value name the model id exactly as the task YAML wrote it (for examplegpt_9_imaginary). If the key is also shown, it must use the formgpt_9_imaginary (rate key gpt-9-imaginary). Update the_run_models/check_pricing_coveragedocstring so the return contract says whether it returns authored ids or rate keys. Why not static: Whether a user-facing message names the string the user typed is a semantic property of runtime output. It needs a resolved task and captured logs. No AST pattern tells an authored id from a derived key. Prevents: The API Surface finding at src/coder_eval/orchestration/batch.py:58: the pre-flight warning and the return value name the rewritten rate keygpt-9-imaginary, which does not appear in the user's YAML. This also drifts from the documented return contract ('unpriced model ids').
Top 5 Priority Actions
- Remove the hardcoded
agent.type == AgentKind.DELEGATEbranch at src/coder_eval/orchestration/batch.py:57. Put the_to-mapping in the shared pricing lookup instead (_normalize_model/_lookup_ratein src/coder_eval/pricing.py:62, try the exact key first, then the hyphenated key), or use a polymorphicAgentConfig.rate_key()hook. Then delete thedelegate_rate_keywrapper at src/coder_eval/agents/delegate_agent.py:1069 and restore the single agent-agnostic yield, which also fixes the double None-check in_run_models. - Add an
else: logger.warning(...)at src/coder_eval/agents/delegate_agent.py:1075 that names the raw model id and the rate key. Without it, an unpriced Delegate turn (a route-chosen model or an unknown echoed id) setstotal_cost_usdto None with no log, which is how the underscored-id defect shipped without notice. - Port the rate-key normalization to evalboard/lib/pricing.ts:37 (
normalizeModel), so the board and Python/run.json pricegpt_5_6_lunathe same way. Extend the CE065 parity check to diff the normalization helpers as well as the rate table, so the next normalization rule cannot drift silently. - Add an inverse test in tests/test_cost_accounting_paths.py:93 that shows a non-Delegate agent with
model: claude_sonnet_5stays unpriced incheck_pricing_coverage. Give_resolvedanagent=parameter so the test keeps TaskDefinition validation. Also parametrize tests/test_delegate_agent.py:1017 over (echoed=None, config="gpt_5_6_luna") and (echoed="gpt_5_6_luna", config=None), so the configured-model fallback is pinned. - Make the pre-flight at src/coder_eval/orchestration/batch.py:58-84 report the model id the user wrote (for example
gpt_9_imaginary (rate key gpt-9-imaginary)), not only the rewritten key, and update the_run_models/unpriced_modelsdocstring. Also correct the stale "hyphenated ids" comment at src/coder_eval/pricing.py:37, or delete it when the mapping moves to the shared seam.
Stats: 0 🔴 · 0 🟠 · 1 🟡 · 7 🔵 across 8 axes reviewed.
The board has the same id gap as pricing.py had. resolvePricing and the run header's model tally keyed on normalizeModel, which keeps "gpt_5_6_luna" as it is, so every token bucket on a Delegate row showed "—" in the USD view, the thinking-cost simulator had no rate, and tallyModels counted gpt_5_6_luna and gpt-5-6-luna as two models. rateKey mirrors pricing.py::delegate_rate_key. A row does not record its harness, so resolvePricing tries it only after the exact id, and an id that already has a rate keeps it. tallyModels groups on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simulator_cost_usd always prices the simulator's tokens from the rate card, but check_pricing_coverage did not look at simulation.model. A simulator model without a rate therefore booked no simulator cost and logged no warning: a downstream suite ran its simulator on claude-sonnet-5-5 before that rate existed and lost the cost silently. The pre-flight now adds the model of every enabled simulation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3e1f611 to
5a94948
Compare
…tor is unpriced _cost_complete looked only at the agent's turns, so a judge or simulator model with no rate left total_cost_usd a floor while cost_complete stayed true and tasks_cost_incomplete did not count the row. REPORT_SCHEMA and the pre-flight warning already said an unpriced model makes the run incomplete. The row now checks judge usage and simulator tokens too, and the warning names the simulator turn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Motivation
The Delegate backend names its models with underscores (
gpt_5_6_luna,claude_opus_5_5), and task configs use these names.pricing.pykeys Delegate models with hyphens (gpt-5-6-luna). Thedelegate-sdkadapter incoder_eval_uipathchanged_to-before it priced a turn. The port to the built-in agent dropped that step, so every agent turn on an underscored id priced atNone, and the pre-flight warned "No pricing rate".Example from the Scribe suite: in run
adhoc-2026-10-08_21-33-28(ADO 20261008.13,gpt_5_6_luna), the taskpdd-interview-client-portfolio-reportingused 34.6M agent tokens, about $1.11 at the luna rate. It booked $0.79, which is exactly the sum of its five judges.The same run showed two more gaps:
claude-sonnet-5-5, which had no rate in 0.12.12 (fix(pricing): add Haiku 5.5, Sonnet 5.5 and GPT-6.1 Sol, re-verify every row #220 added it). The pre-flight did not warn, because it did not check the simulator model.cost_completestayedtruewhen a judge or the simulator was unpriced, althoughREPORT_SCHEMA.mdand the pre-flight warning say that an unpriced model makes itfalse.Changes
pricing.delegate_rate_keychanges_to-.DelegateAgentprices each turn under it, andcheck_pricing_coveragelooks up a Delegate agent's model under it.model_usedkeeps the backend's own id._normalize_modeland the rates do not change.rateKeyinlib/pricing.tsmirrors the rule. A row does not record its harness, soresolvePricingtries it only after the exact id, andtallyModelsgroups on it._cost_completealso returnsfalsewhen a judge or the simulator used tokens with no cost. The pre-flight warning now names the simulator turn..claude/notes/agents.md,docs/REPORT_SCHEMA.md.Testing
mainand pass here: a Delegate turn ongpt_5_6_lunabooks $1.40 for 1M input and 1M output tokens; the pre-flight accepts a Delegate task ongpt_5_6_lunaand flags an unpriced simulator model; the board pricesgpt_5_6_lunaand counts it as one model withgpt-5-6-luna;cost_completeisfalsefor an unpriced judge or simulator.coder-eval runagainst a fake host that echoes the configured id:maingpt_5_6_lunaNone, pre-flight warnsclaude_opus_5_5None, pre-flight warnsgpt_6_lunaNone, pre-flight warnsgpt-5-6-lunagpt_9_imaginary(no rate)None, pre-flight warnsNone, pre-flight warnsgpt_5_6_lunaon thepiharness🤖 Generated with Claude Code