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 38 commits into
Conversation
* cortexkit-lease: shared-mode leases (acquire_shared) Shared holders coexist with each other and block the exclusive writer (and vice versa) — reader-side protection for shared resources like the cross-module model cache: a reader takes a shared lease on a blob digest while validating/mmap-ing so GC (exclusive) can never delete under it. Shared handles do not bump the fence epoch (they are not writers); they report the last persisted writer epoch for observability. Semantics proven in tests: shared+shared coexist, shared blocks exclusive until the LAST shared holder drops, exclusive blocks shared, epoch neutrality, and a cross-process shared-vs-exclusive check (unix: python-fcntl child; Windows LockFileEx semantics are exercised by the same-process tests since its locks are per-handle). try_lock_shared is called via the fs2 trait fully-qualified: std 1.89+ added an inherent File::try_lock_shared that would shadow it. * fmt
…rsing, no data) (cortexkit#2) Two consumers read the catalog for different reasons (broca: capabilities on the serving path; astrocyte: pricing) and must parse the same shape so schema drift cannot make them disagree silently. Money discipline: dollar rates are converted once, at the parse boundary, to exact integer nanodollars per million tokens via decimal string scaling; a rate that cannot scale exactly is a parse error, never a rounded guess; a missing rate is None, never $0. Unmodeled fields are preserved verbatim in raw passthrough.
…ortexkit#3) No catalog publishes a negative price; a corrupted snapshot must fail loud (NegativeRate naming provider/model/field, same shape as InexactRate) rather than flow silently into consumers' signed money paths. Guards base rates and tier rates through one shared conversion gate.
…the parse boundary (cortexkit#4) * cortexkit-model-catalog: round sub-nanodollar precision half-even at the parse boundary Real models.dev snapshots carry IEEE-754 shortest-roundtrip artifacts from the upstream pipeline (0.8299999999999998 for an intended 0.83); the strict reject made a whole real-world snapshot unparseable. Digits beyond the nanodollar resolution now round half-even (error < 0.5 nano per Mtok); the dangerous case stays loud: a NONZERO rate that would round to ZERO is still rejected, so a real price can never silently become a free model. Verified against a live 3.2MB models.dev api.json (167 providers, 145 artifact rates). * harden rounding compare: checked doubling, absurd-precision rate is a loud error rem*2 could wrap i128 for a ~47-fractional-digit significand (divisor 10^38, rem > i128::MAX/2) — a wrapping multiply in a money parse path. checked_mul makes it a parse error (fail closed); test proves the exact overflow shape returns None rather than panicking or wrapping, and that sane long-fraction values still round.
…et payload (cortexkit#5) Pure serde types (ProviderUsage, Usage, RateWindow, AccountInfo, SavedResets, CreditExpiry, ExtraWindow) extracted from ai-provider-quota's model.rs so every consumer of the usage.get wire — the quota module that produces it, ALF's router reads, astrocyte's capacity axis, and the ck quota renderer — compiles against one definition and the shape cannot drift without a shared-crate PR each side reviews. Shape, not policy: read-time transform semantics (the quota module's banked-reset relaxation, which zeroes used_percent and carries the provider truth in raw_used_percent) are producer behavior documented on the fields but not enforced by these types. serde-only, no logic; serde_json is a dev-dependency for the wire-shape tests. The reserved prepaid-Balance seam is intentionally NOT included (it was never populated and is wire-neutral); it joins this crate additively when the balance axis is designed. Follows the cortexkit-model-catalog precedent.
…xkit#6) Adds ProviderUsage.api_provider (wire: apiProvider, camelCase, omitted when absent) carrying the canonical models.dev provider slug alongside the CodexBar provider name — e.g. "openai" for provider=="codex", "anthropic" for "claude", "google" for "gemini", "xai" for "grok". Three consumers (ALF's router, the ck CLI, astrocyte's capacity axis) each hand-roll the same CodexBar→canonical translation today; this field lets them key on one canonical name instead. Producers populate it when the canonical name is known and leave it absent for providers with no models.dev counterpart (consumers fall back to provider). Additive + skip-if-none: unpopulated entries serialize byte-identically to 0.1.
…o RateWindow (cortexkit#7) * cortexkit-provider-usage 0.3.0: add optional usedCount + totalCount to RateWindow Additive absolute-count fields for windows where the provider knows (or can derive) the consumed and total values. Enables human-facing UIs to show '10,336 / 40,000' alongside the percentage for richer context (e.g. qwen-cloud token-plan where the percentage is entitled but the absolute pair makes the enforcement gap legible). Both fields are skip-if-none + camelCase, so unpopulated windows serialize byte-identically to 0.2.0. * drop accidental self-referencing symlink
…ded entry (cortexkit#8) Every degraded entry looks alike on this wire. A provider nobody configured and a provider whose credential broke this morning both arrive as an entry with an error string, and the only thing telling them apart is that string -- which is prose with no stability promise, and which consumers are explicitly told not to parse. The consequence is a state change with no observable: a provider that worked yesterday and failed today moves a count by one and produces no other signal. On the current host that is 3 genuinely failing providers hidden among 25 that are unconfigured and always will be. errorClass carries the reason as a stable name derived from the producer taxonomy rather than from the message text. Classes today: credential_absent, credential_unusable, credential_rejected, no_quota_reported, upstream_failed, decode_failed. It is a String rather than an enum deliberately. The class list will grow, and on an observability surface an unrecognised value must not become a parse failure -- that would make a provider vanish from the output at exactly the moment its state changed. Consumers render an unknown class as degraded-with-unknown-reason, and a test pins that an unknown value decodes intact. Additive and non-breaking: absent on healthy entries, omitted when unset, and a producer that never sets it serializes exactly as before, which is also pinned by a test. degraded() is unchanged for existing callers; degraded_with_class() is the constructor for producers that know the class.
…it#9) SQLite sets no mode of its own, so open_sqlite left the file mode to the caller's umask and the shipped default was world-readable 0644. Measured on a real deployment: 11 of 11 module stores at 0644, including cortexkit-credentials. A crate that already decides WAL mode, busy timeout and foreign keys has taken responsibility for how the file behaves on disk. Leaving permissions to callers means the decision is made by the ambient umask, which is to say not made at all. A group-readable store is not a configuration this crate supports anyway: it hands out an exclusive single-writer lease, so an out-of-band reader is already outside the contract. The exposure, stated honestly rather than as an implied multi-user threat: a single-account host has no other human to read these files. What 0644 does expose them to is every process running as this user — every module, every worker, every tool — and anything that copies the tree: a backup, a restore, an install into a shared location, a container bind-mount. THE WAL IS THE HALF THAT GETS MISSED. Recently committed rows live there until a checkpoint, so protecting only the database leaves the newest data readable while the database file itself reads as correct. On the measured host the WALs are routinely larger than their databases — alfonso-core's is 23MB. Lease files are hardened too, in cortexkit-lease where they are created. Their exposure is integrity rather than privacy: the lease carries the persisted epoch that is the single-writer fence token, so a writable lease file lets a stale writer's fence be forged. Applied on OPEN rather than only at creation, because every store already deployed was created at 0644 — a creation-time-only fix protects exactly the installations with no history. A path that is not a regular file is refused rather than adjusted: following a symlink would chmod a file the caller never named, which is a privilege-escalation primitive wearing a hardening step's clothes. Each assertion is mutation-proved separately. Worth recording that the first version of the WAL test could not fail: it used a FIRST open, where SQLite creates the WAL inheriting the database's already-corrected mode. Dropping '-wal' from the protected suffixes passed it. The test now reopens a store with a leftover permissive WAL — the state an unclean shutdown leaves behind, and the only one where a permissive WAL can be waiting at open time.
The release procedure was undocumented, and its one visible artifact -- three lightweight tags -- reads like a convention that does not exist. The trigger matches on ref name, so tag shape cannot matter; that is not discoverable without reading the workflow, and someone will otherwise spend attention on it during an incident deciding whether a silent release is their tag's fault. Also records what to do when a tag produces no run: check service health first, confirm nothing is queued before retagging at the same commit (the concurrency group deliberately does not protect that case), and bind any wait to the published version rather than to a run appearing.
* Say what absence means for every optional field Sixteen of the twenty-four optional fields on ProviderUsage carried no doc comment at all. A consumer reading the type — which is where a Rust consumer stands, rather than in the producer's markdown contract — had to infer what each absence meant, and the inference that reads as unremarkable is usually the fail-open one. Doc comments only; no shape, serde attribute, or behaviour changes. Three of these are load-bearing rather than tidy: - The three window slots CAN HAVE HOLES. Each is filled from its own optional upstream field, so `secondary` may be absent while `tertiary` is present. A consumer stopping at the first gap misses real limits. - `saved_resets` absent is NOT "zero credits held". It also covers the credit-inventory lookup having failed on that fetch, since that lookup is separate from the usage fetch and may fail without degrading the entry. - `account` absent means the producer could not resolve an identity, not that the provider has one account — so an unlabelled entry is not evidence that a labelled one does not exist. The rest state the same thing in smaller ways: an absent `org_name` is not a personal account, an absent `plan_type` is not a missing plan, an absent `window` on an ExtraWindow is a limit whose figure could not be read rather than an absent limit. * State the consequence on the two fields most likely to be misread A doc comment that states a fact invites agreement; one that states what breaks invites care. Two of these were facts only. `raw_used_percent` said what it is and that UIs should display it, and never said which number to pace on. The natural reading — the raw figure is the truer one — routes work away from an account whose credit is about to be spent, and the credit expires whether or not it is used, so the cautious-looking reading is the lossy one. `source` said "observability only", which is true and does not stop anyone keying on it. It is per-lane rather than per-account and changes between polls with nothing having changed about the account, so a consumer treating a change as an event sees phantom transitions.
The absence docs merged at version 0.4.0, which is already published and immutable -- so commons master and the published 0.4.0 differed in content at the same version number, and every registry consumer (including the crate's own author) still compiled against the sixteen bare fields. docs.rs renders published releases, so the type a consumer hovers was the undocumented one. Doc-only patch bump. A caret pin on 0.4 picks it up on the next update with no manifest change.
A durably-paused run can only be ended by connecting to it, and the daemon refuses to bind a connection whose project root no longer exists. Cancel is the only exit from such a run, so both exits close together and the run sits intact and permanently unreachable. Thirteen were stranded on this host, ten of them by a directory rename, and repo renames keep feeding the class. The existing rejection is deliberate and documented, and it is load-bearing elsewhere: the projects registry uses it to DETECT dead roots, refusing registration of one and sorting vanished pairs into a missing bucket. So this adds a second constructor rather than changing the first, and the amended rationale on the strict one points at it. The fallback resolves the longest existing prefix and re-appends the rest, which is POSIX realpath's behaviour on a non-existent path -- canonicalize is the outlier in refusing partial resolution, so this matches a documented reference rather than inventing a rule. Lexical normalization was the obvious alternative and is wrong: consumers key durable state on the resolved string, and on macOS every temp directory is reached through a symlink, so a lexical result is a different string from the id minted while the root existed. The caller would bind successfully and address an empty lineage -- a confident "no such thing" rather than an error, which is worse than the bug. A dangling link reads as absent to both canonicalize and Path::exists because both follow links, so the naive walk-up stops one component too high and keeps the link's own name. Following it instead is what realpath does and is the choice that survives repair: if someone later creates the target, the strict constructor produces the same id, so ordinary maintenance cannot strand work admitted while the link dangled. Mutation-proved against three alternatives: lexical normalization reddens three tests, keeping the link name reddens the repair test, and relaxing the strict constructor reddens the refusal tests that the projects registry depends on.
from_path_allowing_missing landed in bc88d51 as an additive constructor. subc consumes this crate from crates.io at "0.1", so the new API needs a published version before the daemon can use it. Additive only -- from_path is unchanged, and entorhinal's dependency on its NonExistentPath rejection is untouched.
CKCRED corrected an assessment I made in the push-notification room. I said a divergence in this crate had a bounded blast radius and would fail loudly as a path mismatch. That is true for MY consumer and false for theirs, and they checked precisely because the claim relieved them. ProjectRootId::from_path is the canonicalizer behind two vault identities: the keychain service name holding the master key, and the vault id that fences an admin-operation MAC to one vault. A canonicalization change is a breaking change to both, and it presents as a LOCKED VAULT OVER AN INTACT STORE or as every admin MAC failing verification -- never as a path mismatch. Nothing in either failure says two builds disagree about what a path is. Nobody could see this from either side. I assessed the blast radius of a crate whose consumers I do not build; they had treated it as a path helper because that is what the name says. The fact existed in neither repo's documentation. So the fix is the sentence, at the type, where someone changing the canonical form will meet it -- not in the consumer, which is where it is already known, and not in a design note, which is where it would not be read. Same shape as five defects found in the sealed-payload spec today: the rule was right and its reason lived somewhere the reader would never connect. Sharpened by their own framing: THE NAME IS THE TRAP. This reads as a path helper and is a canonicalizer for security identities, so the reader most likely to change it is the one least likely to suspect the consequence. Nothing behavioural. I published 0.1.1 from this crate today after auditing the callers I could see, and this consumer was not among them.
Ufuk confirmed this repo is mine to manage. Writing that down, because the question came up today from two seats and the repository could not answer it: no CODEOWNERS, no MAINTAINERS, and every commit attributed to the human account because git records the human rather than the seat. It went unowned while five repos built against it, and "nobody" was the accurate answer for months. THREE THINGS A CONSUMER CANNOT LEARN FROM THE CRATES THEMSELVES: 1. The README claimed every crate is published. Measured against crates.io: TWO OF EIGHT are. The other six are unpublished by omission -- none sets publish = false. Release tags are not the authority either: provider-usage has five published versions and four tags. 2. Publishing creates a SECOND DISTRIBUTION PATH and is near-irreversible. Already live: claustrum compiles two copies of cortexkit-paths at the same version, one path and one registry, agreeing only because the published bytes currently match a sibling checkout. Found by CKCRED, invisible from here -- cargo tree -i refuses the query as ambiguous, which is the only surface that shows it. 3. A path dependency records no source and no checksum in Cargo.lock, so changed code compiles into every consuming repo with no diff and nothing for --locked to catch. THE VERSION NUMBER IS THE ENTIRE CHANNEL. Bump on behaviour or emitted bytes, never on prose -- a version that moves for comments trains its readers to bump reflexively, which is how it stops meaning anything. And the trap, now on the crate and in the table: cortexkit-paths reads as a path helper and is the canonicalizer for two vault identities. Changing its canonical form presents as a locked vault over an intact store, never as a path mismatch. CODEOWNERS deliberately does NOT encode the vault seat's standing review duty. Agent seats are not GitHub accounts, so per-path lines naming the same account would look like a routing rule while routing nothing -- a mechanism that appears to enforce and does not is worse than a convention that admits what it is.
Written by CALLO, reviewed by me under the ownership Ufuk settled tonight. Two files plus a workspace member line. 10 tests, clippy -D warnings clean, fmt clean -- all re-run here rather than taken from the handover. WHAT IT IS: HPKE base-mode sealing for push payloads, so a notification can carry the question text while the push provider, the relay and Apple see ciphertext. The opener is a separate implementation in another repository. THE ONE REVIEW FINDING, AND IT WAS NOT A MISSING METHOD: OpenError has four variants and the shared corpus requires each negative vector to carry an expected_failure from a THREE-value wire vocabulary. Nothing mapped between them, so the corpus generator would have implemented that mapping -- a second independent statement of one fact, in a different repo, which is the class we closed three times tonight in the spec. wire_code() now lives beside the enum and the generator reads it. I FOUND IT BY PROBING RATHER THAN READING, and the probe is now a test. The spec's `empty_ct` vector must report `malformed`; I measured what an empty-ciphertext envelope actually yields and got Aead, because 33 bytes CLEARS the length gate and fails authentication instead of shape. The vector still gets the right answer -- but only because Malformed and Aead both map to `malformed`. That is correct and non-obvious, and CALLO's test now records it at the site, so the next reader does not conclude the length gate is what produces the answer. MUTATION-PROVED BEFORE COMMITTING, by me and not by report: flipping UnknownVersion to report `malformed` reddens every_open_failure_maps_to_the_wire_ vocabulary by name; restored, 10 green. The mapping is fenced rather than decorative. TWO DECISIONS RECORDED AT THEIR SITES RATHER THAN AGREED IN A ROOM: - publish = false, with the DOUBLE-RESOLUTION reason at the key rather than "no external consumer" -- the second framing invites publication the day one appears, and the hazard at that moment is that publishing creates a second path to the same bytes. Already live elsewhere: claustrum compiles two copies of cortexkit-paths, one path and one registry. - open() enforces no size cap deliberately; the bound is the transport's, and a second cap would duplicate a limit owned elsewhere and could disagree with it. The module docs lead with what breaks in CONSUMERS, above what the crate does, because a change here is a wire-format divergence in a repo that does not build this code -- and because a path dependency carries no content hash, the version number is the only channel through which a consumer learns sealed output moved.
CKIOS proposes the right milestone for tonight: ONE notification, sealed by hand, that opens on the phone. Not the feature. It exercises every unknown -- key custody across two processes, seal/open agreement, the APNs environment (which is currently operator testimony rather than a measurement), and the deep link -- while each piece is small enough that a failure has one candidate cause. The room listed the sealer as STARTED and the corpus as NOT STARTED, and read that as blocking. It is not: sealing one payload needs the crate, not the corpus. The corpus proves CONFORMANCE between two implementations, which is a later question than whether a blob opens. So these three examples close that gap with no new library code: kp generate an X25519 recipient keypair handseal seal a payload to a public key, print hex, report the size delta handopen open one, or print the refusal AND its wire code MEASURED RATHER THAN ESTIMATED, on a realistic ask payload: 111 bytes plaintext -> 160 bytes sealed. 49 bytes of overhead: 1 version + 32 encapsulated key + 16 authentication tag. That number retires a question I had left open in the spec deliberately -- I refused to state a derived sealed-size figure before measuring one, because a derived number stated early becomes normative by accident. Now it is measured: the 2048-byte plaintext cap yields ~2097 sealed, comfortably inside APNs' 4KB ceiling, so the cap needs no revision. Round trip verified with its control: the correct key opens to the exact plaintext; a wrong recipient refuses as Aead, wire code `malformed`, which is the three-into-one collapse the spec requires rather than a leak about which part of the envelope was wrong. Examples rather than a binary or a script, so they live beside the crate they exercise and cannot drift from it.
A dead-code pass on this workspace flags seal() as unreachable outside tests and examples, which is true and is not a finding. The consumer is the notification submit endpoint and it is unbuilt, so there is no caller-shaped hole where a caller is required -- the distinction a later reader cannot make from the code alone. Deleting it would take the ciphersuite pinning and the envelope layout with it, and a separate implementation in another repository is already built against both. Nothing in this workspace would fail if they were lost. The examples carry the same weight: the handseal/handopen round trip is the only end-to-end exercise of the crate outside its own tests, so a cleanup that removes examples as clutter removes the only evidence the crate works against a real opener.
kp prints SK then PK, both 64 hex characters once the label is stripped, and handseal takes PK. Taking the first line instead seals to a keypair nobody holds: the seal succeeds, the blob is well-formed, and the only symptom appears on the receiving device as a notification that cannot be opened -- one of the failure modes that is hardest to attribute, because every sending-side check passes. Says so at both ends, since a reader who has kp's output on screen is not necessarily reading handseal's source and vice versa.
The sealing key and the APNs device token are both 32 bytes rendered as 64 hex characters, so nothing about either value says which it is. X25519 accepts essentially any 32 bytes as a public key, so pasting the token here SEALS SUCCESSFULLY to a keypair nobody holds: the blob is well formed, it reaches the device, and the only symptom is a notification that cannot be opened -- which reads as a decryption fault and sends the search to the keys rather than to the paste. The opposite swap is loud, so the hazard is one-directional and silent in the direction that costs the most. handseal now accepts the labelled block the producing side emits and picks its own line out of it, which removes the ordering an operator would otherwise have to remember. A block naming only the token is refused with that named as the cause. Values carrying prose are refused rather than repaired: such a value is a failure message written where a key belongs, so the fault it describes happened before the paste and re-running will not help. Proved by round trip rather than by inspection -- a blob sealed through a token-first block opens with the key's secret, and the control that makes that meaningful is a token-sealed blob refusing against the same secret.
The producing side emits label=value; this example was written against label: value. A block pasted into the wrong parser failed with a message about hex characters, which points at the value when the mismatch is actually in the format -- the same misdirection the labelled form exists to remove. Splitting on either character costs one line and removes a coordination requirement between two repositories that would otherwise have to agree on punctuation and stay agreed.
Providers that sell credit alongside a subscription report a balance with no period, which the existing shape cannot carry: a rate window is a percentage of a period, and a pool is an amount with neither. Today that data is fetched by producers and discarded, so an account with a depleted window and a live credit pool reads as unusable when it would have served the request. Kept apart from `usage` rather than folded in, because the two fail in opposite directions: over-consuming a window gets you throttled and recovers by waiting, over-consuming a balance gets you billed and recovers by paying. A balance therefore never becomes a window, never carries a reset, and never appears as a percentage. Three choices in here were forced by real payloads rather than picked. Amounts are integer minor units because DeepSeek and MiniMax both send decimal strings and Anthropic sends minor units with an exponent -- and because a balance is compared against zero on every routing decision that reads it, where binary floats are not safe. Pool ids carry the provider's own name, since wallets separate voucher from cash and credit without defining which is a gift, and renaming one `granted` would invent the label a spend policy keys on. And `basis` distinguishes a reported remainder from one derived against a shared total, because DeepSeek reports per-pool remainders while others report only grants -- which is the difference between an exact policy and a ceiling. Additive: an entry without pools serializes exactly as before, pinned by a test on the rendered text rather than the field.
PoolFunding and PoolBasis were closed enums, and this payload crosses a repository boundary: one project produces it, others consume it, and their versions move independently. So the first funding kind added after a consumer is built fails deserialization of the ENTIRE ProviderUsage entry rather than one field. Measured rather than argued: an entry carrying a healthy 42% window and two pools, one with an unrecognised funding, loses everything -- "unknown variant `crypto_grant`". An account's rate windows would vanish because of a credit pool the consumer had never heard of, and a vanished entry reads as the provider being unavailable. For funding the fallback costs nothing, because the fallback and the semantics already agree: a kind this consumer cannot name is one it must not spend from, which is what Unknown already meant. Basis needed a decision rather than a default. Its two poles are not symmetrical -- treating an exact remainder as a ceiling under-spends and costs nothing, while treating a ceiling as exact spends money that may not be there -- so an unrecognised value must fold to the conservative side. It folds to a new Unstated variant rather than to Derived, because both are read the same way but Derived is a claim about how a number was obtained, and answering "I do not know" with it would assert a fact the producer does not hold. That is the failure this type exists to prevent, one level up. Both proven by removing the fallbacks: each test reddens by name.
Add prepaid balances and credit pools to ProviderUsage
…absent The parser read context-pricing tier thresholds from keys models.dev has never emitted (context_over / min_context), so every threshold silently parsed to 0 via unwrap_or — meaning the over-threshold rate claimed to apply from context 0 on all 335 live tier rows. The fixture was authored from the same misunderstanding as the parser, so its non-vacuous assertion certified the defect. The real threshold lives at tier.tier.size (335/335 rows on the live payload). A missing threshold is now a loud MissingTierThreshold error, never a silent 0: a tier whose floor defaults to 0 is a silent repricing. Two new tests pin both failure directions (absent threshold refuses; the invented legacy keys do not satisfy it), mutation-proven by restoring the unwrap_or(0) — both redden, the baseline passes. Found by FUSI executing the shipped parser against live bytes; writeup at fusiform/docs/findings/2026-08-11-commons-tier-threshold.md.
…e row in the refusal FUSI's residual findings on ffdd06a, both probe-verified: (1) the parser read tier.tier.size without checking tier.tier.type, so a hypothetical per-image tier ({type: images, size: 1000}) was silently reinterpreted as a 1000-token context floor — the same assumed-meaning class as the original defect, one field over, and plausible upstream given models.dev already lists image/audio/video models. min_context is a claim that the dimension IS context; the type gate is part of the threshold's meaning. (2) MissingTierThreshold carried no row identifier while being fatal to the whole parse — a refusal over 6,253 models without a pointer turns a five-second fix into a bisect; it now carries provider/model like its neighbouring variants. Type-gate mutation-proven (removing it reddens the non-context test by name); live payload still parses 335/335 nonzero.
…e known while consumption is only a percentage qwen-cloud now publishes totalCount with no usedCount: the console's derived count carried a disagreement between two provider endpoints (observed percentage matched no integer count over the reported cap), so the emitter dropped it rather than rounding to a number that never existed. The doc pairing 'present alongside used_count' no longer holds, and used_count's contract is now stated: integral, upstream's own figure only, never recovered from a percentage and a cap. Doc-only change; no wire or serde behavior moves, no version bump per the version-check's doc-only exemption.
…ortexkit#12) A preserved last-known-good entry is currently byte-identical to a fresh one apart from fetchedAt, so a consumer cannot separate "this figure is old because the producer cannot reach the provider" from "this figure is old because nothing polled recently". Those have opposite remedies, and a consumer with only a timestamp has to guess with a wall-clock threshold -- which denies fresh-enough data in order to catch stale data. One consumer built exactly that and had a dispatch blocked on a 58-minute snapshot, which was genuinely stale but indistinguishable from a slow poll at the moment it mattered. `since` is deliberately not fetchedAt: the reading was taken when it was taken, and the failure began afterwards. The gap between them is how long the producer has been blind, which is what a staleness policy wants. Additive and absent on a fresh entry, so today's shape is byte-identical and a consumer predating the field decodes unchanged -- both pinned.
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.