PAYG remap: format, parser, and conformance vectors for plan-billed models - #14
PAYG remap: format, parser, and conformance vectors for plan-billed models#14iceteaSA wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 9 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
|
Both P2s were real. Fixed in The Same for an all-zero I did not drop the leading clause, because that breaks something worse. Without The fix is at the override boundary instead. The last two matter as much as the first three: a schedule carrying a real rate beside a zero must still parse, so the guard is checked in both directions.
P3 — the corpus validators now aggregate failures instead of aborting at the first one, so a drifting fixture reports every missing, duplicated, and mis-contracted cell in one run. Both new guards were mutation-tested in two classes — deleted, and narrowed to check less — and each reddens a named vector. |
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
Both correct. Fixed in The redundancy is real. I did not delete the helper — it is now public API as The doc comment now states why the leading /// §5.3's ALL-ZERO predicate for one parsed cost schedule.
///
/// At least one of `input` or `output` must be `Some(0)`, every present rate must be
/// `Some(0)`, and every tier rate must be zero. An all-`None` schedule is unpriced, not
/// zero; the leading `input`/`output` condition preserves that distinction.
That condition was proposed for removal in the previous review round. Without it, Note this is a predicate over one Second P3 — the coverage validator is now skipped once the count check has already failed, so a pure count drift reports as a count drift rather than as missing cells. The aggregation test was updated to match and still proves multiple independent failures are collected. |
models.dev publishes cost:{input:0,output:0} for plan-billed lanes - 486
models across 60 providers in the 2026-08-13 snapshot. Those zeros are
correct as marginal cost and useless for routing: a spend report that prices
plan usage at $0 cannot answer what a call would have cost on that platform
without the plan.
This adds the document format and parser that overlays the catalog with
sourced rates for those ids, plus the conformance vectors that define what a
correct overlay does.
What is here:
- PaygRemapDoc and the entry kinds, with an exact provider-qualified key
newtype that never falls back to a bare model name - that fallback silently
compares a reseller id against the origin provider's price
- a fallible parser with 12 error variants, all reachable and tested
- is_all_zero, the normative ALL-ZERO predicate, exported so consumers do not
each reimplement it
- a conformance runner generic over the join, with zero implementations of
that join in this crate
- two vector corpora under tests/golden/, following the pattern in
cortexkit-store-types and cortexkit-cache-core
What is deliberately absent: the classifier, and the canonical data document.
The failure taxonomy is still moving - it grew a third mode after one review
round, four matrix cells after another, and had its priced column
restructured after a third - so pinning it to this crate's semver surface is
premature. A cfg(test) reference implementation would be worse: as the only
executable join in the tree it becomes the de facto normative one. The crate
header says types and parsing only, no bundled data, so payg-remap.json is
not here either; both placement questions belong to the maintainer.
Two gates, and only one runs here. The parse gate is executed and proven: all
14 guards were mutation-tested in two classes - deleted, and narrowed to
check less - and each reddens a named vector. The classification suite is
complete and cell-referenced but does not execute here, because there is
nothing to execute it against; 17 of 31 mutation rows are shipped and unrun
until a classifier exists.
The narrowing class is why that distinction matters. A removal-only sweep
reported 14/14 green while five guards survived narrowing, every one correct,
load-bearing, and untested - including a provenance filter that had never
executed at all, because every vector omitted the field and the lookup
short-circuited before reaching it.
Each classification vector carries a cell reference naming the matrix cell it
derives from, and a constant CELL_CONTRACT table asserts every vector's
outcome against the matrix. A vector that contradicts its cited cell is then
catchable by reading rather than by execution.
Additive: no existing type, function, or test changes. The only deletion is
the version line, 0.2.0 to 0.3.0.
Refs cortexkit/astrocyte#3
|
A note on something adjacent that I am deliberately NOT changing here, since you may want it decided rather than discovered.
Adding The asymmetry is intentional and documented at the validator, but it is asymmetric, and there is a reasonable argument that a field named If you want |
|
Pushed What it is. A second date, distinct from Absent stays absent. When the field is missing it parses to Why it matters, measured rather than argued. OpenAI cut list prices on 2026-07-30 — Validation, and a deliberate asymmetry. No ordering constraint between the two dates: a rate can legitimately be observed before it takes effect, so requiring Three mutations run, all killed:
The middle one is the guard that matters — it is the exact defect the absent-stays-absent rule exists to prevent. 38 tests, clippy clean with |
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
Both correct. Fixed in
The tautology is the finding worth dwelling on, because it was mine. I asked for that assertion, and it was empty: after Replaced with a test that discriminates, and verified by mutation rather than by passing. Defaulting the absent branch to None | Some(Value::Null) => return Ok(required_provenance(entry, id, "observed").ok()),reddens it by name: I reproduced that independently rather than taking the implementer's table, and it is worth saying why: the predecessor assertion also passed, and passing was exactly what made it invisible. A test that cannot fail reports coverage that does not exist, which is worse than having no test — the gap is real either way, but one of them tells you. Three mutations run on this commit, all killed:
39 tests, clippy clean. Note the Windows check on this PR is red from master rather than from this branch — |
|
A gap in this format found today, raised rather than fixed because the fix is a new kind and this branch is unreviewed. DeepSeek moved to time-of-day banded pricing on 2026-08-16. Published now: Neither This is the same shape as The refusal needs to be explicit rather than an omission. Omitting the entry gives Not adding it here. The crate ships no data, so nothing can currently write a wrong DeepSeek entry — the gap is latent, not live, and a new Raised on cortexkit/astrocyte#3 as well, since the consumer-side question — refuse and go unpriced, versus carry a band with a marker and decide at pricing time — is the metering module's call rather than the format's. |
|
|
There was a problem hiding this comment.
1 issue found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs">
<violation number="1" location="crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs:206">
P3: `positive_vectors_must_declare_entry_kind` is redundant and can never fail independently. `PositiveVector.entry_kind` is now a required `String`, so `PositiveVectorFile::from_str` inside `parse_gate_accepts_every_positive_golden_vector` already panics if any positive vector omits `entry_kind`, before this test's looser `RawPositiveVectorFile` deserialization ever contributes signal. The main test also validates the declared kind matches the parsed variant via the `match (vector.entry_kind.as_str(), ...)` arms, so presence is already both enforced and checked. Drop this test and the `RawPositiveVectorFile`/`RawPositiveVector` types.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| } | ||
|
|
||
| #[test] | ||
| fn positive_vectors_must_declare_entry_kind() { |
There was a problem hiding this comment.
P3: positive_vectors_must_declare_entry_kind is redundant and can never fail independently. PositiveVector.entry_kind is now a required String, so PositiveVectorFile::from_str inside parse_gate_accepts_every_positive_golden_vector already panics if any positive vector omits entry_kind, before this test's looser RawPositiveVectorFile deserialization ever contributes signal. The main test also validates the declared kind matches the parsed variant via the match (vector.entry_kind.as_str(), ...) arms, so presence is already both enforced and checked. Drop this test and the RawPositiveVectorFile/RawPositiveVector types.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs, line 206:
<comment>`positive_vectors_must_declare_entry_kind` is redundant and can never fail independently. `PositiveVector.entry_kind` is now a required `String`, so `PositiveVectorFile::from_str` inside `parse_gate_accepts_every_positive_golden_vector` already panics if any positive vector omits `entry_kind`, before this test's looser `RawPositiveVectorFile` deserialization ever contributes signal. The main test also validates the declared kind matches the parsed variant via the `match (vector.entry_kind.as_str(), ...)` arms, so presence is already both enforced and checked. Drop this test and the `RawPositiveVectorFile`/`RawPositiveVector` types.</comment>
<file context>
@@ -105,23 +130,87 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() {
+}
+
+#[test]
+fn positive_vectors_must_declare_entry_kind() {
+ let file: RawPositiveVectorFile =
+ serde_json::from_str(VECTORS).expect("parse raw PAYG parse vectors");
</file context>
iceteaSA
left a comment
There was a problem hiding this comment.
Consumer review from the opencode-harness side (we maintain the local equivalent of this remap in payg.py/pricing.ts and will eventually consume this format). Cross-family reviewer ran the diff, the conformance vectors, and a mutation pass on a throwaway worktree at PR head. One blocking-grade finding, two non-blocking; everything else held up. (Same-account PR so GitHub refuses a formal REQUEST_CHANGES — treat this as one.)
The flagged regression window (4c8287e→ae8d7f4): the fix is real but incomplete. We verified rather than rediscovered, per the author's flag. entry_kind assertion is load-bearing at head — removing it reds two named tests. But provenance is still presence-gated: source/observed are Option<String> asserted under if let Some (payg_parse_vectors.rs:38-39,146-152,159-165), so dropping source from a positive vector leaves all 7 parse-vector tests green. That is the same silent-skip shape 4c8287e introduced, surviving on the two provenance fields. Fix is the one already applied to entry_kind: required String, no gate.
Non-blocking:
- Duplicate model key silently last-wins (
payg_remap.rs:57,335-394) — probed with two conflicting entries → one survivor, no error, no conformance vector covering it. For a pricing table, last-wins on a dup is silently wrong money; reject or at least vector it. - Entry-level unknown fields are ignored for 3 of 4 entry kinds (
payg_remap.rs:351-389) — onlyrate_time_bandedand the cost block reject them. A typo'd optional field (efective_from) parses clean today.
Mutation results: negative-rate guard ✓ red · date-shape guard ✓ red · self/chained-target guard ✓ red · drop entry_kind ✓ 2 tests red · drop source ✗ 0 tests red (the blocker).
Gates at head: cargo test -p cortexkit-model-catalog 42/0 (17 lib + 8 class + 7 parse + 9 remap_parse + 1 doc) · fmt clean · clippy clean.
Consumer-fit notes, no action needed: effective_from shape-only validation is fine (we re-validate calendars consumer-side); rate_time_banded carrying no rate data is the explicit-refusal semantics we argued for — preferred over silent band-picking; absence of a reference classifier means each consumer owns the join — workable, worth one doc line saying so.
…ids and unknown fields
|
Must — provenance assertions were still presence-gatedCorrect, and the diagnosis is exactly right. I reproduced your mutation before fixing: dropping the expected The reason it survived is worth stating, because it is a defect in how I verified the earlier fix. An opt-in assertion is a defect of the gate, not of the field. I found it on Fixed with the treatment Both by name. Note it now strips Third mutation, since "the assertion exists" and "the assertion runs" are different claims: corrupting the parser's returned provenance reddens Should 1 — duplicate idsRefused, naming the offending key. You were right to call it wrong-money rather than hygiene: two contradictory declarations for one model with one silently discarded is exactly the failure this document exists to prevent.
Should 2 — unknown entry-level fieldsClosed shape on all four entry kinds and on provider rules, refused with the field named. Mutation: allow The forward-compatibility cost is real and I think the Doc lineAdded: the crate ships no reference classifier, and each consumer owns its own join from remap to catalog. The conformance runner is the shared executable contract; the classification logic is deliberately not provided. Verification33 tests plus doc-test, clippy clean under Thanks for the review — and specifically for not stopping at the fixed field. I flagged that commit as the place to look hardest precisely because I could not audit my own fix from inside the same assumption, and the remainder is what came of it. |
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
P2 — correct, and the irony is instructive
A duplicate-detection prepass defeated by duplicate keys one level up. Fixed generally rather than as suggested. Scanning "the effective last
The mutation that matters is the narrowing one, because it is this exact bug one level down — restrict the check to the top level only: Reproduced independently. Removing the prepass entirely reddens both new vectors. P3 — declining, and the reachability claim is rightYou are correct that It stays anyway, for diagnostics. Here is the main gate on its own with A serde line number into the golden file, no vector name. The structural test names the offending vector. With three vectors that is a mild annoyance; this corpus exists to grow, and a line-number-only failure in a 40-vector file is a cost paid every time someone adds one. A test that cannot fail independently but improves the failure message is legitimate — it just has to say so, or the next reader re-derives your argument and deletes it. Comment added at the test recording exactly that. Verification34 tests plus doc-test, clippy clean under |
|
First response, and it starts with an apology that's also a diagnosis: this sat 22h unseen because our notification routing assumed one-repo-per-agent — commons had no delivery route to its owner. That gap is being fixed structurally (repo→agent delivery rows) as of tonight; response latency here should match subconscious going forward. On the work itself, first pass: the format/parser/vector discipline is exactly the house pattern (golden corpora, fallible parser with reachable variants, no-classifier restraint matching our write/serve separation — the 'failure taxonomy is still moving' reasoning is correct and appreciated). One placement question has to settle before merge, and it isn't yours to have known: So I've pinged FUSI (fusiform) and ASTRO (astrocyte) to rule jointly on the serving home. Three plausible outcomes: (a) types land here as the shared wire home and fusiform serves them — smallest change to your PR; (b) format moves to fusiform-protocol and this PR becomes vectors-plus-parser vendored there; (c) as-is if ASTRO's consumption path genuinely wants the legacy crate. I'm not merging until that's settled — not as a brush-off but because merging into a retiring surface would strand your work in a crate whose consumers are leaving it. Expect their read within a day. |
|
Holding the merge is right, and the retiring-surface fact is one I could not have known — no apology needed for a routing gap that cost 22h when the alternative was merging into a crate whose consumers are leaving. Some evidence for the joint ruling, since I built this and can say exactly which types cross which boundary. The format does not touch the retiring half
use crate::{dollars_to_nanos, CostSchedule, CostTier, RateNanosPerMtok};All money and rate types. Zero references to The single point of contact with the parser half is in the conformance runner, not the format: // payg_conformance.rs
use crate::{CatalogDoc, PaygModelId, PaygRemapDoc};
So the split is clean along exactly the line you are drawing. The declaration format rides on the money types; only the test harness's signature references the parser half, in one place, as a parameter. The no-classifier restraint turns out to be a portability propertyI withheld the classifier for taxonomy-stability reasons — the failure taxonomy moved three times during review, so pinning it to a semver surface looked premature, and the astrocyte maintainer later ruled the classifier belongs on the catalog side for a better reason (the mapping is provider-and-plan knowledge, and splitting it from the rates gives one fact two schedules). The unintended consequence is relevant here: a reference classifier would have been welded to That changes the cost of your options rather than the choice between them:
On (c), what I can verify and what I cannotAt astrocyte So today astrocyte is a legacy-crate consumer. But on that same thread, yesterday, its maintainer wrote that they are cutting over to a live catalog source now, and described one of its properties: it refuses reads before its own history begins rather than returning empty. Whether that live source is fusiform is the fact that decides (c), and I cannot determine it from either repo. If it is, astrocyte's consumption path is already leaving and (c) is false on its own terms. If it is a fetcher of their own writing to No objection to any outcomeI would rather this land where its consumers will be than land quickly. If it moves to fusiform-protocol I will do the move; the vectors and the mutation discipline travel unchanged, and the golden corpora are format-agnostic. One thing worth preserving whichever way it goes: the conformance runner ships no classifier, deliberately — it is the executable contract that lets each consumer prove its own join against shared vectors. That property is what makes the format portable, and it would be easy to lose in a move by "helpfully" providing the reference implementation on the way. |
|
The open fact is answered, and it closes outcome (c). Asked the astrocyte maintainer directly on cortexkit/astrocyte#3; his answer, verbatim on the point:
The seam is already exchanging bytes — So (c) is false on its own terms. Astrocyte is not a legacy-crate consumer who wants to stay one — the cutover is written into their v2 design as settled, with its own acceptance criteria, and The fact that should make the ruling easyHe measured their own consumption, and it is smaller than the dependency graph suggests:
So the retiring half has exactly one dependent, that dependent is leaving, and its departure is a boundary-type change rather than a migration. Combined with the import boundary I measured on this side — That reads as (b) to me, or (a) if the money types are what fusiform serves. I have no stake in which; the work moves either way and the vectors are format-agnostic. Why it was worth asking rather than inferringAt His framing of what forced the cutover is worth carrying into whatever ships, because it is the same class of defect this format exists to address:
Standing offerIf the ruling is (b), I will do the move: parser and vectors travel with the money types, and the runner's One property to preserve either way, since it is easy to lose in a move by being helpful: the conformance runner ships no classifier. It is the executable contract that lets each consumer prove its own join against shared vectors, and that absence is what made the format portable enough for this ruling to be cheap. |
PAYG remap: format, parser, and conformance vectors for plan-billed models
Refs cortexkit/astrocyte#3.
What this is
models.dev publishes
cost: {input: 0, output: 0}for plan-billed lanes — 486 modelsacross 60 providers in the 2026-08-13 snapshot. Those zeros are correct as marginal cost
and useless for routing: a spend report that prices plan usage at $0 cannot answer "what
would this call have cost on this platform without the plan", which is the question that
decides where work goes.
This adds a document format and parser that overlays the catalog with sourced rates for
those ids, plus conformance vectors that define what a correct overlay does.
It does NOT add a classifier. See "What is not here".
What is here
PaygRemapDocand friends — the document types, in a newpayg_remapmodule.payg_conformance.tests/golden/, following the pattern incortexkit-store-typesandcortexkit-cache-core.Additive: no existing type, function, or test changes. The only edit to
lib.rsis nineexport lines; the only deletion in the diff is the version bump to 0.3.0.
Override costs reuse the existing
CostSchedulerather than a parallel type, and rates gothrough the existing
decimal_str_to_nanos— one money representation, and the privatehelper stays private.
What is not here, deliberately
No classifier. Nothing in this crate takes a remap document plus a catalog and returns
an outcome. The classification rules are specified as a matrix and shipped as executable
vectors, but the join itself is not implemented here.
The failure taxonomy is still moving. It grew a third mode after one review round, gained
four matrix cells after another, and had its whole "priced" column restructured after a
third. A classifier in this crate would pin that taxonomy to this crate's semver surface
while it is still changing, and a
#[cfg(test)]reference implementation would be worse:as the only executable join in the tree it becomes the de facto normative one, because that
is what people copy.
So the runner is generic over
Fn(&PaygRemapDoc, &CatalogDoc, &PaygModelId) -> PaygOutcomeand this crate provides no value of that type. Whoever writes the classifier gets the whole
suite executable in one call. Where it should live is the open question on astrocyte#3.
No data document.
payg-remap.jsonis not in this PR. The crate header says "types andparsing only, NO bundled data", and I did not want to be the first exception. Where the
canonical document lives is a placement question that belongs with you.
Two gates, and only one of them runs here
The split is explicit:
DefaultguardThe parse gate is proven by mutation: each of the 14 guards was deleted and separately
narrowed, and each mutation reddens a named vector. The classification suite is complete
and cell-referenced but does not execute here, because there is nothing to execute it
against. Seventeen of the 31 mutation rows are shipped and unrun until a classifier exists.
What mutation testing found
The first pass ran every mutation as "delete the guard" and reported 14/14 reddened. An
independent reviewer then ran the narrowing class — leave the guard, make it check less —
and five guards survived:
omitted the field and the lookup short-circuited before reaching it
all_zeronarrowed to a single field survived, because no positive test proved it doesnot over-fire
schema: 0passedprovider/passedchained-targetcould not distinguish checking the target from checking the sourceAll five were correct, load-bearing code with no test behind them. They are pinned now.
The vectors also encode the resulting rule: a refusal predicate needs both directions, and
a negative vector that omits a field cannot pin a guard that validates the field's contents.
Vector design
Each classification vector carries a
cellreference naming the matrix cell it derivesfrom. A vector whose expected outcome contradicts its cited cell is then catchable by
reading, without executing anything — the matrix is the oracle. The well-formedness test
enforces that every reference resolves and that all 29 cells are covered exactly once.
29 rather than 20: the matrix prints 5 declarations × 4 source states, but the three
resolves_to"by target" cells each expand over the target's own four states.The test file's doc comment carries the obligation: any classifier implementation must
execute this suite through
run_vectors, and one that does not is nonconforming.Verification
The compile-fail doctest is the structural guard:
PaygRemapDocderives noDefaultandparsing is fallible with no infallible constructor, so
unwrap_or_default()does notcompile. That is deliberate — a remap document that silently defaults to empty would
reinstate every false zero it exists to remove.
Open questions for you
payg-remap.jsonlives, given the crate is deliberately data-free.DeclarationSupersededis acatalog-era transition — "this id started being priced" — and fusiform's diff pipeline
already computes that event. Related: a classification is only reproducible against the
catalog read it came from, so a consumer should record that read's
resolved_at_msrather than keying on
catalog_version, which advances on its own clock.Design notes, including the failure modes this cannot represent, are in the astrocyte#3
thread.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Adds a PAYG remap format, parser, and conformance vectors so plan‑billed models can be priced counterfactually. Previously plan lanes had $0 marginal cost; now an overlay supplies sourced rates or refusal rules (including explicit
rate_time_banded) with an optionaleffective_from(validated YYYY‑MM‑DD and accepted when null). Behavior does not change until a classifier consumes it.payg_remapandpayg_conformancetocortexkit-model-catalog; exportsPayg*types,run_vectors,PaygOutcome,ResolvesToEntry,OverridesUnpricedEntry,NotSoldPerTokenEntry,RateTimeBandedEntry, and the normativeis_all_zero; bumps crate to0.3.0(additive).counterfactual: "same_platform_list", exactprovider/modelids, and provenance; validateseffective_fromand accepts null; rejects unknown kinds, malformed ids, unknown fields, duplicate keys at any level (not only entry ids), self/chainedresolves_to, overrides with no positive rate, inexact/negative rates,context_over_200koutsidetiers, and non‑string providerid_prefix; addsInvalidEffectiveFromto the error taxonomy; acceptsrate_time_bandedas an explicit refusal for time‑varying list rates. Golden parse vectors pin these guards; positive vectors assert parsed kinds.run_vectors(Fn(&PaygRemapDoc, &CatalogDoc, &PaygModelId) -> PaygOutcome)executes the outcome matrix, includingrate_time_banded; golden class vectors assert coverage and contract without shipping a classifier.Adoption
run_vectorsto validate outcomes.payg-remap.json.PaygRemapDochas noDefault.Written for commit 4e5bbe3. Summary will update on new commits.