From 426d45308ca10271f1bdf5e5bdc0c0ab18ae9218 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:25:55 +0200 Subject: [PATCH 1/7] feat(model-catalog): PAYG remap format, parser, and conformance vectors 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 --- crates/cortexkit-model-catalog/Cargo.toml | 2 +- crates/cortexkit-model-catalog/src/lib.rs | 9 + .../src/payg_conformance.rs | 150 +++ .../cortexkit-model-catalog/src/payg_remap.rs | 555 +++++++++++ .../tests/golden/payg-class-vectors.json | 882 ++++++++++++++++++ .../tests/golden/payg-parse-vectors.json | 147 +++ .../tests/payg_class_vectors.rs | 296 ++++++ .../tests/payg_parse_vectors.rs | 224 +++++ .../tests/payg_remap_parse.rs | 264 ++++++ 9 files changed, 2528 insertions(+), 1 deletion(-) create mode 100644 crates/cortexkit-model-catalog/src/payg_conformance.rs create mode 100644 crates/cortexkit-model-catalog/src/payg_remap.rs create mode 100644 crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json create mode 100644 crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json create mode 100644 crates/cortexkit-model-catalog/tests/payg_class_vectors.rs create mode 100644 crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs create mode 100644 crates/cortexkit-model-catalog/tests/payg_remap_parse.rs diff --git a/crates/cortexkit-model-catalog/Cargo.toml b/crates/cortexkit-model-catalog/Cargo.toml index 942fe2b..8f160dd 100644 --- a/crates/cortexkit-model-catalog/Cargo.toml +++ b/crates/cortexkit-model-catalog/Cargo.toml @@ -4,7 +4,7 @@ # each brings its own snapshot and owns its own derived stores. [package] name = "cortexkit-model-catalog" -version = "0.2.0" +version = "0.3.0" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/cortexkit-model-catalog/src/lib.rs b/crates/cortexkit-model-catalog/src/lib.rs index 5f6fd17..ea1d16e 100644 --- a/crates/cortexkit-model-catalog/src/lib.rs +++ b/crates/cortexkit-model-catalog/src/lib.rs @@ -21,6 +21,15 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use serde_json::Value; +mod payg_conformance; +mod payg_remap; + +pub use payg_conformance::{run_vectors, PaygOutcome, PaygVector, PaygVectorSuite, VectorFailure}; +pub use payg_remap::{ + is_all_zero, NotSoldPerTokenEntry, OverridesUnpricedEntry, PaygModelId, PaygProviderRule, + PaygProviderRuleKind, PaygRemapDoc, PaygRemapEntry, PaygRemapParseError, ResolvesToEntry, +}; + /// Integer nanodollars per million tokens. $3/M tokens = 3_000_000_000. pub type RateNanosPerMtok = i64; diff --git a/crates/cortexkit-model-catalog/src/payg_conformance.rs b/crates/cortexkit-model-catalog/src/payg_conformance.rs new file mode 100644 index 0000000..fc9b378 --- /dev/null +++ b/crates/cortexkit-model-catalog/src/payg_conformance.rs @@ -0,0 +1,150 @@ +use serde::Deserialize; +use serde_json::Value; + +use crate::{CatalogDoc, PaygModelId, PaygRemapDoc}; + +/// One expected classification outcome from the PAYG conformance matrix. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PaygOutcome { + Priced, + NotSoldPerToken, + TargetNotInCatalog, + TargetNotPriceable, + DeclarationSuperseded, + NoEntry, +} + +/// One classification vector supplied by a conformance corpus. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PaygVector { + pub name: String, + pub cell: String, + #[serde(deserialize_with = "deserialize_remap_doc")] + pub remap: PaygRemapDoc, + #[serde(deserialize_with = "deserialize_catalog_doc")] + pub catalog: CatalogDoc, + #[serde(deserialize_with = "deserialize_model_id")] + pub model: PaygModelId, + pub expected: PaygOutcome, +} + +/// A complete, ordered classification-vector corpus. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct PaygVectorSuite { + pub vectors: Vec, +} + +/// One vector whose classifier result differed from the declared expectation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VectorFailure { + pub vector: String, + pub expected: PaygOutcome, + pub actual: PaygOutcome, +} + +/// Execute every vector against the caller's classification implementation. +pub fn run_vectors(vectors: &PaygVectorSuite, classify: F) -> Vec +where + F: Fn(&PaygRemapDoc, &CatalogDoc, &PaygModelId) -> PaygOutcome, +{ + vectors + .vectors + .iter() + .filter_map(|vector| { + let actual = classify(&vector.remap, &vector.catalog, &vector.model); + (actual != vector.expected).then(|| VectorFailure { + vector: vector.name.clone(), + expected: vector.expected, + actual, + }) + }) + .collect() +} + +fn deserialize_remap_doc<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + PaygRemapDoc::parse(&value.to_string()).map_err(serde::de::Error::custom) +} + +fn deserialize_catalog_doc<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + CatalogDoc::parse(&value.to_string()).map_err(serde::de::Error::custom) +} + +fn deserialize_model_id<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let id = String::deserialize(deserializer)?; + PaygModelId::parse(&id).map_err(serde::de::Error::custom) +} + +#[cfg(test)] +mod tests { + use crate::{CatalogDoc, PaygModelId, PaygRemapDoc}; + + use super::{run_vectors, PaygOutcome, PaygVector, PaygVectorSuite}; + + #[test] + fn reports_a_mismatch_from_the_caller_supplied_classifier() { + let vectors = PaygVectorSuite { + vectors: vec![PaygVector { + name: "priced-vector".into(), + cell: "overrides_unpriced/absent".into(), + remap: PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }"#, + ) + .unwrap(), + catalog: CatalogDoc::parse("{}").unwrap(), + model: PaygModelId::parse("provider/model").unwrap(), + expected: PaygOutcome::Priced, + }], + }; + + assert_eq!( + run_vectors(&vectors, |_, _, _| PaygOutcome::NoEntry), + vec![super::VectorFailure { + vector: "priced-vector".into(), + expected: PaygOutcome::Priced, + actual: PaygOutcome::NoEntry, + }] + ); + } + + #[test] + fn parses_vectors_with_their_catalog_and_remap_documents() { + let suite: PaygVectorSuite = serde_json::from_str( + r#"{ + "vectors": [{ + "name": "priced-vector", + "cell": "overrides_unpriced/absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }, + "catalog": {}, + "model": "provider/model", + "expected": "priced" + }] + }"#, + ) + .unwrap(); + + assert_eq!(suite.vectors[0].model.as_str(), "provider/model"); + assert_eq!(suite.vectors[0].expected, PaygOutcome::Priced); + } +} diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs new file mode 100644 index 0000000..1687cef --- /dev/null +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -0,0 +1,555 @@ +use std::collections::BTreeMap; + +use serde_json::Value; + +use crate::{dollars_to_nanos, CostSchedule, CostTier, RateNanosPerMtok}; + +const COUNTERFACTUAL: &str = "same_platform_list"; + +/// An exact provider-qualified model identifier used by PAYG remap documents. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PaygModelId(String); + +impl PaygModelId { + pub fn parse(id: &str) -> Result { + let Some((provider, model)) = id.split_once('/') else { + return Err(PaygRemapParseError::MalformedId { id: id.into() }); + }; + if provider.is_empty() || model.is_empty() { + return Err(PaygRemapParseError::MalformedId { id: id.into() }); + } + Ok(Self(id.into())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn provider(&self) -> &str { + self.0 + .split_once('/') + .map(|(provider, _)| provider) + .expect("PaygModelId is validated by parse") + } + + pub fn model(&self) -> &str { + self.0 + .split_once('/') + .map(|(_, model)| model) + .expect("PaygModelId is validated by parse") + } +} + +/// One complete, parsed PAYG remap document. +/// +/// ```rust,compile_fail +/// use cortexkit_model_catalog::PaygRemapDoc; +/// +/// let parsed = PaygRemapDoc::parse("{}"); +/// let _ = parsed.unwrap_or_default(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaygRemapDoc { + pub schema: u64, + pub providers: BTreeMap, + pub entries: BTreeMap, +} + +impl PaygRemapDoc { + pub fn parse(json: &str) -> Result { + let root: Value = serde_json::from_str(json) + .map_err(|error| PaygRemapParseError::Json(error.to_string()))?; + let root = root + .as_object() + .ok_or_else(|| PaygRemapParseError::Json("top level is not an object".into()))?; + + let schema = root + .get("schema") + .and_then(Value::as_u64) + .ok_or_else(|| PaygRemapParseError::Json("schema is not an unsigned integer".into()))?; + if schema != 1 { + return Err(PaygRemapParseError::UnknownSchema { schema }); + } + + let found = root + .get("counterfactual") + .and_then(Value::as_str) + .map_or_else(|| "".into(), Into::into); + if found != COUNTERFACTUAL { + return Err(PaygRemapParseError::CounterfactualMismatch { + expected: COUNTERFACTUAL, + found, + }); + } + + let providers = parse_provider_rules( + root.get("providers") + .ok_or_else(|| PaygRemapParseError::Json("providers is missing".into()))?, + )?; + let entries = parse_entries( + root.get("entries") + .ok_or_else(|| PaygRemapParseError::Json("entries is missing".into()))?, + )?; + + for (id, entry) in &entries { + if let PaygRemapEntry::ResolvesTo(resolve) = entry { + if resolve.target == *id { + return Err(PaygRemapParseError::SelfReferentialTarget { + id: id.as_str().into(), + }); + } + if entries.contains_key(&resolve.target) { + return Err(PaygRemapParseError::ChainedTarget { + id: id.as_str().into(), + target: resolve.target.as_str().into(), + }); + } + } + } + + Ok(Self { + schema, + providers, + entries, + }) + } +} + +/// The only provider-wide PAYG refusal rule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PaygProviderRuleKind { + ZerosAreNotPrices, +} + +/// A provider-scoped PAYG refusal rule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaygProviderRule { + pub kind: PaygProviderRuleKind, + pub id_prefix: Option, + pub source: String, + pub observed: String, +} + +/// A specific PAYG remap declaration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PaygRemapEntry { + ResolvesTo(ResolvesToEntry), + OverridesUnpriced(OverridesUnpricedEntry), + NotSoldPerToken(NotSoldPerTokenEntry), +} + +/// A declaration that points at one terminal catalog schedule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvesToEntry { + pub target: PaygModelId, + pub because: String, + pub source: String, + pub observed: String, +} + +/// A declaration that supplies a sourced schedule absent from the catalog. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OverridesUnpricedEntry { + pub cost: CostSchedule, + pub source: String, + pub observed: String, +} + +/// A declaration that the platform has no per-token rate for this identifier. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NotSoldPerTokenEntry { + pub reason: String, + pub source: String, + pub observed: String, +} + +/// A PAYG remap-document parse failure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PaygRemapParseError { + Json(String), + UnknownSchema { + schema: u64, + }, + CounterfactualMismatch { + expected: &'static str, + found: String, + }, + UnknownKind { + id: String, + kind: String, + }, + MalformedId { + id: String, + }, + MissingProvenance { + id: String, + field: &'static str, + }, + SelfReferentialTarget { + id: String, + }, + ChainedTarget { + id: String, + target: String, + }, + ZeroOverride { + id: String, + }, + InexactRate { + id: String, + field: &'static str, + value: String, + }, + NegativeRate { + id: String, + field: &'static str, + value: String, + }, + ContextBandNotRepresentable { + id: String, + }, + InvalidIdPrefix { + id: String, + }, +} + +impl std::fmt::Display for PaygRemapParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Json(error) => write!(f, "PAYG remap json: {error}"), + Self::UnknownSchema { schema } => write!(f, "unknown PAYG remap schema {schema}"), + Self::CounterfactualMismatch { expected, found } => { + write!( + f, + "PAYG remap counterfactual is {found:?}, expected {expected:?}" + ) + } + Self::UnknownKind { id, kind } => { + write!(f, "unknown PAYG remap kind {kind:?} for {id}") + } + Self::MalformedId { id } => write!(f, "malformed PAYG remap id {id:?}"), + Self::MissingProvenance { id, field } => { + write!(f, "PAYG remap declaration {id} is missing {field}") + } + Self::SelfReferentialTarget { id } => { + write!(f, "PAYG remap declaration {id} resolves to itself") + } + Self::ChainedTarget { id, target } => { + write!( + f, + "PAYG remap declaration {id} resolves through entry {target}" + ) + } + Self::ZeroOverride { id } => { + write!(f, "PAYG remap override {id} does not supply a positive rate") + } + Self::InexactRate { id, field, value } => write!( + f, + "PAYG remap rate {id}.{field} = {value} cannot scale exactly to nanodollars" + ), + Self::NegativeRate { id, field, value } => { + write!(f, "PAYG remap rate {id}.{field} = {value} is negative") + } + Self::ContextBandNotRepresentable { id } => write!( + f, + "PAYG remap override {id} uses context_over_200k; express that band through a tiers entry" + ), + Self::InvalidIdPrefix { id } => { + write!(f, "PAYG provider rule {id} has a non-string id_prefix") + } + } + } +} + +impl std::error::Error for PaygRemapParseError {} + +fn parse_provider_rules( + value: &Value, +) -> Result, PaygRemapParseError> { + let rules = value + .as_object() + .ok_or_else(|| PaygRemapParseError::Json("providers is not an object".into()))?; + let mut parsed = BTreeMap::new(); + for (id, value) in rules { + let rule = value.as_object().ok_or_else(|| { + PaygRemapParseError::Json(format!("provider rule {id} is not an object")) + })?; + let kind = required_string(rule, id, "kind")?; + let kind = match kind.as_str() { + "zeros_are_not_prices" => PaygProviderRuleKind::ZerosAreNotPrices, + _ => { + return Err(PaygRemapParseError::UnknownKind { + id: id.clone(), + kind, + }); + } + }; + let id_prefix = match rule.get("id_prefix") { + None | Some(Value::Null) => None, + Some(Value::String(prefix)) => Some(prefix.clone()), + Some(_) => return Err(PaygRemapParseError::InvalidIdPrefix { id: id.clone() }), + }; + parsed.insert( + id.clone(), + PaygProviderRule { + kind, + id_prefix, + source: required_provenance(rule, id, "source")?, + observed: required_provenance(rule, id, "observed")?, + }, + ); + } + Ok(parsed) +} + +fn parse_entries( + value: &Value, +) -> Result, PaygRemapParseError> { + let entries = value + .as_object() + .ok_or_else(|| PaygRemapParseError::Json("entries is not an object".into()))?; + let mut parsed = BTreeMap::new(); + for (raw_id, value) in entries { + let id = PaygModelId::parse(raw_id)?; + let entry = value.as_object().ok_or_else(|| { + PaygRemapParseError::Json(format!("PAYG remap entry {raw_id} is not an object")) + })?; + let source = required_provenance(entry, raw_id, "source")?; + let observed = required_provenance(entry, raw_id, "observed")?; + let kind = required_string(entry, raw_id, "kind")?; + let entry = match kind.as_str() { + "resolves_to" => PaygRemapEntry::ResolvesTo(ResolvesToEntry { + target: PaygModelId::parse(&required_string(entry, raw_id, "target")?)?, + because: required_string(entry, raw_id, "because")?, + source, + observed, + }), + "overrides_unpriced" => PaygRemapEntry::OverridesUnpriced(OverridesUnpricedEntry { + cost: parse_override_cost( + raw_id, + entry + .get("cost") + .ok_or_else(|| missing_required_field(raw_id, "cost"))?, + )?, + source, + observed, + }), + "not_sold_per_token" => PaygRemapEntry::NotSoldPerToken(NotSoldPerTokenEntry { + reason: required_string(entry, raw_id, "reason")?, + source, + observed, + }), + _ => { + return Err(PaygRemapParseError::UnknownKind { + id: raw_id.clone(), + kind, + }); + } + }; + parsed.insert(id, entry); + } + Ok(parsed) +} + +fn required_provenance( + entry: &serde_json::Map, + id: &str, + field: &'static str, +) -> Result { + entry + .get(field) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(Into::into) + .ok_or_else(|| PaygRemapParseError::MissingProvenance { + id: id.into(), + field, + }) +} + +fn required_string( + entry: &serde_json::Map, + id: &str, + field: &str, +) -> Result { + entry + .get(field) + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(Into::into) + .ok_or_else(|| missing_required_field(id, field)) +} + +fn missing_required_field(id: &str, field: &str) -> PaygRemapParseError { + PaygRemapParseError::Json(format!("PAYG remap declaration {id} is missing {field}")) +} + +fn parse_override_cost(id: &str, value: &Value) -> Result { + let cost = value.as_object().ok_or_else(|| { + PaygRemapParseError::Json(format!( + "PAYG remap override cost for {id} is not an object" + )) + })?; + for field in cost.keys() { + if field == "context_over_200k" { + return Err(PaygRemapParseError::ContextBandNotRepresentable { id: id.into() }); + } + if !matches!( + field.as_str(), + "input" + | "output" + | "cache_read" + | "cache_write" + | "reasoning" + | "input_audio" + | "output_audio" + | "tiers" + ) { + return Err(PaygRemapParseError::Json(format!( + "PAYG remap override cost for {id} has unknown field {field}" + ))); + } + } + + let schedule = CostSchedule { + input: parse_rate(cost, id, "input")?, + output: parse_rate(cost, id, "output")?, + cache_read: parse_rate(cost, id, "cache_read")?, + cache_write: parse_rate(cost, id, "cache_write")?, + reasoning: parse_rate(cost, id, "reasoning")?, + input_audio: parse_rate(cost, id, "input_audio")?, + output_audio: parse_rate(cost, id, "output_audio")?, + tiers: parse_tiers(cost, id)?, + }; + if !has_positive_rate(&schedule) { + return Err(PaygRemapParseError::ZeroOverride { id: id.into() }); + } + Ok(schedule) +} + +fn parse_tiers( + cost: &serde_json::Map, + id: &str, +) -> Result, PaygRemapParseError> { + let Some(tiers) = cost.get("tiers") else { + return Ok(Vec::new()); + }; + let tiers = tiers.as_array().ok_or_else(|| { + PaygRemapParseError::Json(format!( + "PAYG remap override tiers for {id} is not an array" + )) + })?; + let mut parsed = Vec::with_capacity(tiers.len()); + for tier in tiers { + let tier = tier.as_object().ok_or_else(|| { + PaygRemapParseError::Json(format!( + "PAYG remap override tier for {id} is not an object" + )) + })?; + for field in tier.keys() { + if !matches!( + field.as_str(), + "input" | "output" | "cache_read" | "cache_write" | "tier" + ) { + return Err(PaygRemapParseError::Json(format!( + "PAYG remap override tier for {id} has unknown field {field}" + ))); + } + } + let dimension = tier.get("tier").and_then(Value::as_object).ok_or_else(|| { + PaygRemapParseError::Json(format!("PAYG remap override tier for {id} lacks tier")) + })?; + if dimension.get("type").and_then(Value::as_str) != Some("context") { + return Err(PaygRemapParseError::Json(format!( + "PAYG remap override tier for {id} is not a context tier" + ))); + } + let min_context = dimension + .get("size") + .and_then(Value::as_u64) + .ok_or_else(|| { + PaygRemapParseError::Json(format!( + "PAYG remap override tier for {id} lacks tier.size" + )) + })?; + parsed.push(CostTier { + min_context, + input: parse_rate(tier, id, "input")?, + output: parse_rate(tier, id, "output")?, + cache_read: parse_rate(tier, id, "cache_read")?, + cache_write: parse_rate(tier, id, "cache_write")?, + }); + } + parsed.sort_by_key(|tier| tier.min_context); + Ok(parsed) +} + +fn parse_rate( + fields: &serde_json::Map, + id: &str, + field: &'static str, +) -> Result, PaygRemapParseError> { + let Some(value) = fields.get(field) else { + return Ok(None); + }; + if value.is_null() { + return Ok(None); + } + let rate = dollars_to_nanos(value).map_err(|value| PaygRemapParseError::InexactRate { + id: id.into(), + field, + value, + })?; + if rate < 0 { + return Err(PaygRemapParseError::NegativeRate { + id: id.into(), + field, + value: value.to_string(), + }); + } + Ok(Some(rate)) +} + +/// ยง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. +pub fn is_all_zero(cost: &CostSchedule) -> bool { + let direct = [ + cost.input, + cost.output, + cost.cache_read, + cost.cache_write, + cost.reasoning, + cost.input_audio, + cost.output_audio, + ]; + (cost.input == Some(0) || cost.output == Some(0)) + && direct.into_iter().flatten().all(|rate| rate == 0) + && cost.tiers.iter().all(|tier| { + [tier.input, tier.output, tier.cache_read, tier.cache_write] + .into_iter() + .flatten() + .all(|rate| rate == 0) + }) +} + +fn has_positive_rate(cost: &CostSchedule) -> bool { + let direct = [ + cost.input, + cost.output, + cost.cache_read, + cost.cache_write, + cost.reasoning, + cost.input_audio, + cost.output_audio, + ]; + direct.into_iter().flatten().any(|rate| rate > 0) + || cost.tiers.iter().any(|tier| { + [tier.input, tier.output, tier.cache_read, tier.cache_write] + .into_iter() + .flatten() + .any(|rate| rate > 0) + }) +} diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json new file mode 100644 index 0000000..2a64321 --- /dev/null +++ b/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json @@ -0,0 +1,882 @@ +{ + "vectors": [ + { + "name": "resolves-to-priced-source", + "cell": "resolves_to/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + }, + "target": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "declaration_superseded" + }, + { + "name": "resolves-to-zero-source-priced-target", + "cell": "resolves_to/all-zero/target-priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + }, + "target": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "priced" + }, + { + "name": "resolves-to-zero-source-none-target", + "cell": "resolves_to/all-zero/target-all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + }, + "target": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-zero-source-zero-target", + "cell": "resolves_to/all-zero/target-all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + }, + "target": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-zero-source-absent-target", + "cell": "resolves_to/all-zero/target-absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "target_not_in_catalog" + }, + { + "name": "resolves-to-none-source-priced-target", + "cell": "resolves_to/all-none/target-priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + }, + "target": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "priced" + }, + { + "name": "resolves-to-none-source-none-target", + "cell": "resolves_to/all-none/target-all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + }, + "target": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-none-source-zero-target", + "cell": "resolves_to/all-none/target-all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + }, + "target": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-none-source-absent-target", + "cell": "resolves_to/all-none/target-absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "target_not_in_catalog" + }, + { + "name": "resolves-to-absent-source-priced-target", + "cell": "resolves_to/absent/target-priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "target": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "priced" + }, + { + "name": "resolves-to-absent-source-none-target", + "cell": "resolves_to/absent/target-all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "target": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-absent-source-zero-target", + "cell": "resolves_to/absent/target-all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "target": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "target_not_priceable" + }, + { + "name": "resolves-to-absent-source-absent-target", + "cell": "resolves_to/absent/target-absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "resolves_to", + "target": "target/model", + "because": "same-platform schedule", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": {}, + "model": "source/model", + "expected": "target_not_in_catalog" + }, + { + "name": "override-reasoning-priced-source", + "cell": "overrides_unpriced/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "overrides_unpriced", + "cost": { + "input": 0.5, + "output": 3 + }, + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0, + "output": 0, + "reasoning": 5 + } + } + } + } + }, + "model": "source/model", + "expected": "declaration_superseded" + }, + { + "name": "override-qwen-reseller-zero", + "cell": "overrides_unpriced/all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "alibaba-token-plan/qwen3.7-plus": { + "kind": "overrides_unpriced", + "cost": { + "input": 0.5, + "output": 3, + "cache_read": 0.05, + "cache_write": 0.625, + "tiers": [ + { + "input": 2, + "output": 6, + "cache_read": 0.2, + "cache_write": 2.5, + "tier": { + "type": "context", + "size": 256000 + } + } + ] + }, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "alibaba-token-plan": { + "models": { + "qwen3.7-plus": { + "cost": { + "input": 0, + "output": 0, + "cache_read": 0, + "cache_write": 0 + } + } + } + }, + "opencode-go": { + "models": { + "qwen3.7-plus": { + "cost": { + "input": 0.4, + "output": 1.6, + "cache_read": 0.04, + "cache_write": 0.5 + } + } + } + }, + "alibaba": { + "models": { + "qwen3.7-plus": { + "cost": { + "input": 0.5, + "output": 3, + "cache_read": 0.05, + "cache_write": 0.625 + } + } + } + } + }, + "model": "alibaba-token-plan/qwen3.7-plus", + "expected": "priced" + }, + { + "name": "override-none-source", + "cell": "overrides_unpriced/all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "overrides_unpriced", + "cost": { + "input": 0.5, + "output": 3 + }, + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "priced" + }, + { + "name": "override-absent-source", + "cell": "overrides_unpriced/absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "overrides_unpriced", + "cost": { + "input": 0.5, + "output": 3 + }, + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": {}, + "model": "source/model", + "expected": "priced" + }, + { + "name": "not-sold-priced-source", + "cell": "not_sold_per_token/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "not_sold_per_token", + "reason": "plan-only", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "declaration_superseded" + }, + { + "name": "not-sold-zero-source", + "cell": "not_sold_per_token/all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "not_sold_per_token", + "reason": "plan-only", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "not-sold-none-source", + "cell": "not_sold_per_token/all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "not_sold_per_token", + "reason": "plan-only", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": { + "source": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "not-sold-absent-source", + "cell": "not_sold_per_token/absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "source/model": { + "kind": "not_sold_per_token", + "reason": "plan-only", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + } + }, + "catalog": {}, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "google-priced-provider-rule", + "cell": "zeros_are_not_prices/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "google": { + "kind": "zeros_are_not_prices", + "source": "https://example.test/google", + "observed": "2026-08-15" + } + }, + "entries": {} + }, + "catalog": { + "google": { + "models": { + "gemini-3.5-flash": { + "cost": { + "input": 1.5, + "output": 9, + "cache_read": 0.15, + "input_audio": 1.5 + } + } + } + } + }, + "model": "google/gemini-3.5-flash", + "expected": "no_entry" + }, + { + "name": "provider-rule-zero-source", + "cell": "zeros_are_not_prices/all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "source": { + "kind": "zeros_are_not_prices", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + }, + "entries": {} + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "provider-rule-none-source", + "cell": "zeros_are_not_prices/all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "source": { + "kind": "zeros_are_not_prices", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + }, + "entries": {} + }, + "catalog": { + "source": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "provider-rule-absent-source", + "cell": "zeros_are_not_prices/absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "source": { + "kind": "zeros_are_not_prices", + "source": "https://example.test/source", + "observed": "2026-08-15" + } + }, + "entries": {} + }, + "catalog": {}, + "model": "source/model", + "expected": "not_sold_per_token" + }, + { + "name": "no-declaration-priced-source", + "cell": "no_declaration/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 1 + } + } + } + } + }, + "model": "source/model", + "expected": "no_entry" + }, + { + "name": "no-declaration-zero-source", + "cell": "no_declaration/all-zero", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }, + "catalog": { + "source": { + "models": { + "model": { + "cost": { + "input": 0 + } + } + } + } + }, + "model": "source/model", + "expected": "no_entry" + }, + { + "name": "no-declaration-none-source", + "cell": "no_declaration/all-none", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }, + "catalog": { + "source": { + "models": { + "model": {} + } + } + }, + "model": "source/model", + "expected": "no_entry" + }, + { + "name": "no-declaration-absent-source", + "cell": "no_declaration/absent", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": {} + }, + "catalog": {}, + "model": "source/model", + "expected": "no_entry" + } + ] +} diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json new file mode 100644 index 0000000..fd3f4cc --- /dev/null +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -0,0 +1,147 @@ +{ + "vectors": [ + { + "name": "version-skew", + "input_json": "{\"schema\":2,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{}}", + "expect_error": { "variant": "UnknownSchema", "schema": 2 } + }, + { + "name": "schema-zero", + "input_json": "{\"schema\":0,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{}}", + "expect_error": { "variant": "UnknownSchema", "schema": 0 } + }, + { + "name": "counterfactual-mismatch", + "input_json": "{\"schema\":1,\"counterfactual\":\"different\",\"providers\":{},\"entries\":{}}", + "expect_error": { "variant": "CounterfactualMismatch", "expected": "same_platform_list", "found": "different" } + }, + { + "name": "unknown-kind-refuses-whole-document", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"unknown\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "UnknownKind", "id": "p/m", "kind": "unknown" } + }, + { + "name": "id-without-provider", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"/model\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "MalformedId", "id": "/model" } + }, + { + "name": "id-without-model", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"provider/\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "MalformedId", "id": "provider/" } + }, + { + "name": "entry-without-source", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "MissingProvenance", "id": "p/m", "field": "source" } + }, + { + "name": "provider-rule-without-source", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"observed\":\"2026-08-13\"}},\"entries\":{}}", + "expect_error": { "variant": "MissingProvenance", "id": "p", "field": "source" } + }, + { + "name": "entry-without-observed", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\"}}}", + "expect_error": { "variant": "MissingProvenance", "id": "p/m", "field": "observed" } + }, + { + "name": "provider-rule-without-observed", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"source\":\"https://vendor.example/pricing\"}},\"entries\":{}}", + "expect_error": { "variant": "MissingProvenance", "id": "p", "field": "observed" } + }, + { + "name": "entry-with-empty-source", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "MissingProvenance", "id": "p/m", "field": "source" } + }, + { + "name": "provider-rule-with-empty-source", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"source\":\"\",\"observed\":\"2026-08-13\"}},\"entries\":{}}", + "expect_error": { "variant": "MissingProvenance", "id": "p", "field": "source" } + }, + { + "name": "entry-with-empty-observed", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"\"}}}", + "expect_error": { "variant": "MissingProvenance", "id": "p/m", "field": "observed" } + }, + { + "name": "provider-rule-with-empty-observed", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"\"}},\"entries\":{}}", + "expect_error": { "variant": "MissingProvenance", "id": "p", "field": "observed" } + }, + { + "name": "self-target", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"resolves_to\",\"target\":\"p/m\",\"because\":\"same schedule\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "SelfReferentialTarget", "id": "p/m" } + }, + { + "name": "chained-target", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/a\":{\"kind\":\"resolves_to\",\"target\":\"p/b\",\"because\":\"origin schedule\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"},\"p/b\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "ChainedTarget", "id": "p/a", "target": "p/b" } + }, + { + "name": "zero-valued-overrides-unpriced", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"input\":0,\"output\":0,\"cache_read\":0,\"cache_write\":0,\"reasoning\":0,\"input_audio\":0,\"output_audio\":0,\"tiers\":[{\"tier\":{\"type\":\"context\",\"size\":128000},\"input\":0,\"output\":0,\"cache_read\":0,\"cache_write\":0}]},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "ZeroOverride", "id": "p/m" } + }, + { + "name": "rate-that-rounds-to-zero", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"input\":1e-10},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { "variant": "InexactRate", "id": "p/m", "field": "input", "value": "1e-10" } + }, + { + "name": "negative-rate", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"output\":-1},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { + "variant": "NegativeRate", + "id": "p/m", + "field": "output", + "value": "-1" + } + }, + { + "name": "context-band-requires-tier", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"context_over_200k\":{\"input\":2,\"output\":6}},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { + "variant": "ContextBandNotRepresentable", + "id": "p/m", + "message": "PAYG remap override p/m uses context_over_200k; express that band through a tiers entry" + } + } + , + { + "name": "zero-only-reasoning-override", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"reasoning\":0},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { + "variant": "ZeroOverride", + "id": "p/m" + } + }, + { + "name": "all-none-override", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { + "variant": "ZeroOverride", + "id": "p/m" + } + }, + { + "name": "all-zero-tier-only-override", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"tiers\":[{\"tier\":{\"type\":\"context\",\"size\":128000},\"input\":0,\"output\":0}]},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\"}}}", + "expect_error": { + "variant": "ZeroOverride", + "id": "p/m" + } + } + , + { + "name": "non-string-provider-id-prefix", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"id_prefix\":123,\"source\":\"https://vendor.example/rules\",\"observed\":\"2026-08-13\"}},\"entries\":{}}", + "expect_error": { + "variant": "InvalidIdPrefix", + "id": "p" + } + } + ] +} diff --git a/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs new file mode 100644 index 0000000..43c3d07 --- /dev/null +++ b/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs @@ -0,0 +1,296 @@ +//! Validate the frozen PAYG classification-vector corpus without classifying it. +//! +//! Any classifier implementation MUST execute this suite through `run_vectors`; a +//! classifier that does not is nonconforming. This crate owns the corpus shape, while +//! the classifier's home owns execution of the matrix outcomes. +//! The qwen schedules retain only the live rates needed by their cells, not a complete +//! models.dev record; context bands are represented by `tiers` in this crate. + +use std::collections::BTreeSet; + +use cortexkit_model_catalog::PaygVectorSuite; +use serde::Deserialize; + +const VECTORS: &str = include_str!("golden/payg-class-vectors.json"); + +const MATRIX_CELLS: &[&str] = &[ + "resolves_to/priced", + "resolves_to/all-zero/target-priced", + "resolves_to/all-zero/target-all-none", + "resolves_to/all-zero/target-all-zero", + "resolves_to/all-zero/target-absent", + "resolves_to/all-none/target-priced", + "resolves_to/all-none/target-all-none", + "resolves_to/all-none/target-all-zero", + "resolves_to/all-none/target-absent", + "resolves_to/absent/target-priced", + "resolves_to/absent/target-all-none", + "resolves_to/absent/target-all-zero", + "resolves_to/absent/target-absent", + "overrides_unpriced/priced", + "overrides_unpriced/all-zero", + "overrides_unpriced/all-none", + "overrides_unpriced/absent", + "not_sold_per_token/priced", + "not_sold_per_token/all-zero", + "not_sold_per_token/all-none", + "not_sold_per_token/absent", + "zeros_are_not_prices/priced", + "zeros_are_not_prices/all-zero", + "zeros_are_not_prices/all-none", + "zeros_are_not_prices/absent", + "no_declaration/priced", + "no_declaration/all-zero", + "no_declaration/all-none", + "no_declaration/absent", +]; + +const LEGAL_OUTCOMES: &[&str] = &[ + "priced", + "not_sold_per_token", + "target_not_in_catalog", + "target_not_priceable", + "declaration_superseded", + "no_entry", +]; + +const CELL_CONTRACT: &[(&str, &str)] = &[ + ("resolves_to/priced", "declaration_superseded"), + ("resolves_to/all-zero/target-priced", "priced"), + ( + "resolves_to/all-zero/target-all-none", + "target_not_priceable", + ), + ( + "resolves_to/all-zero/target-all-zero", + "target_not_priceable", + ), + ( + "resolves_to/all-zero/target-absent", + "target_not_in_catalog", + ), + ("resolves_to/all-none/target-priced", "priced"), + ( + "resolves_to/all-none/target-all-none", + "target_not_priceable", + ), + ( + "resolves_to/all-none/target-all-zero", + "target_not_priceable", + ), + ( + "resolves_to/all-none/target-absent", + "target_not_in_catalog", + ), + ("resolves_to/absent/target-priced", "priced"), + ("resolves_to/absent/target-all-none", "target_not_priceable"), + ("resolves_to/absent/target-all-zero", "target_not_priceable"), + ("resolves_to/absent/target-absent", "target_not_in_catalog"), + ("overrides_unpriced/priced", "declaration_superseded"), + ("overrides_unpriced/all-zero", "priced"), + ("overrides_unpriced/all-none", "priced"), + ("overrides_unpriced/absent", "priced"), + ("not_sold_per_token/priced", "declaration_superseded"), + ("not_sold_per_token/all-zero", "not_sold_per_token"), + ("not_sold_per_token/all-none", "not_sold_per_token"), + ("not_sold_per_token/absent", "not_sold_per_token"), + ("zeros_are_not_prices/priced", "no_entry"), + ("zeros_are_not_prices/all-zero", "not_sold_per_token"), + ("zeros_are_not_prices/all-none", "not_sold_per_token"), + ("zeros_are_not_prices/absent", "not_sold_per_token"), + ("no_declaration/priced", "no_entry"), + ("no_declaration/all-zero", "no_entry"), + ("no_declaration/all-none", "no_entry"), + ("no_declaration/absent", "no_entry"), +]; + +#[derive(Debug, Deserialize)] +struct RawVectorSuite { + vectors: Vec, +} + +#[derive(Debug, Deserialize)] +struct RawVector { + cell: String, + expected: String, +} + +#[test] +fn classification_vectors_are_a_complete_well_formed_matrix_corpus() { + let vectors: PaygVectorSuite = serde_json::from_str(VECTORS) + .expect("every classification vector parses through the public conformance type"); + let raw: RawVectorSuite = serde_json::from_str(VECTORS) + .expect("read classification vector cell references and outcome names"); + + let failures = validation_failures(&raw.vectors, vectors.vectors.len()); + assert!( + failures.is_empty(), + "PAYG classification vector corpus is malformed:\n{}", + failures.join("\n") + ); +} + +#[test] +fn cell_reference_guard_rejects_an_unknown_target_state() { + let vectors = vec![raw_vector( + "overrides_unpriced/all-zero/target-priced", + "priced", + )]; + + assert!(validate_cell_references(&vectors).is_err()); +} + +#[test] +fn legal_outcome_guard_rejects_a_prefix_of_a_real_outcome() { + let vectors = vec![raw_vector("no_declaration/absent", "priced-but-not-legal")]; + + assert!(validate_expected_outcomes(&vectors).is_err()); +} + +#[test] +fn cell_contract_guard_rejects_a_legal_but_wrong_target_state_outcome() { + let vectors = vec![raw_vector( + "resolves_to/all-zero/target-all-zero", + "declaration_superseded", + )]; + + assert!(validate_cell_contract(&vectors).is_err()); +} + +#[test] +fn coverage_guard_rejects_a_duplicate_cell() { + let mut vectors = complete_matrix_vectors(); + vectors.push(raw_vector("no_declaration/absent", "no_entry")); + + assert!(validate_exact_once_coverage(&vectors).is_err()); +} + +#[test] +fn coverage_guard_rejects_a_missing_cell() { + let mut vectors = complete_matrix_vectors(); + vectors.pop(); + + assert!(validate_exact_once_coverage(&vectors).is_err()); +} + +#[test] +fn coverage_guard_rejects_an_extra_cell() { + let mut vectors = complete_matrix_vectors(); + vectors.push(raw_vector("outside-the-matrix", "no_entry")); + + assert!(validate_exact_once_coverage(&vectors).is_err()); +} + +#[test] +fn validation_diagnostics_collect_independent_failures() { + let vectors = vec![raw_vector("outside-the-matrix", "not-an-outcome")]; + + let failures = validation_failures(&vectors, 0); + + assert_eq!(failures.len(), 5, "{failures:#?}"); + assert!(failures + .iter() + .any(|failure| failure.contains("expected 29 vectors"))); + assert!(failures + .iter() + .any(|failure| failure.contains("unknown matrix cell"))); + assert!(failures + .iter() + .any(|failure| failure.contains("illegal PAYG outcome"))); +} + +fn validate_cell_references(vectors: &[RawVector]) -> Result<(), String> { + for vector in vectors { + if !MATRIX_CELLS.contains(&vector.cell.as_str()) { + return Err(format!("unknown matrix cell: {}", vector.cell)); + } + } + Ok(()) +} + +fn validation_failures(vectors: &[RawVector], parsed_vector_count: usize) -> Vec { + let mut failures = Vec::new(); + let matrix_count_matches = vectors.len() == MATRIX_CELLS.len(); + if !matrix_count_matches { + failures.push(format!( + "expected {} vectors, found {}", + MATRIX_CELLS.len(), + vectors.len() + )); + } + if vectors.len() != parsed_vector_count { + failures.push(format!( + "raw fixture has {} vectors but public parsing produced {parsed_vector_count}", + vectors.len() + )); + } + for validation in [ + validate_cell_references(vectors), + validate_expected_outcomes(vectors), + validate_cell_contract(vectors), + ] { + if let Err(error) = validation { + failures.push(error); + } + } + if matrix_count_matches { + if let Err(error) = validate_exact_once_coverage(vectors) { + failures.push(error); + } + } + failures +} + +fn validate_expected_outcomes(vectors: &[RawVector]) -> Result<(), String> { + for vector in vectors { + if !LEGAL_OUTCOMES.contains(&vector.expected.as_str()) { + return Err(format!("illegal PAYG outcome: {}", vector.expected)); + } + } + Ok(()) +} + +fn validate_cell_contract(vectors: &[RawVector]) -> Result<(), String> { + for vector in vectors { + let expected = CELL_CONTRACT + .iter() + .find_map(|(cell, expected)| (*cell == vector.cell).then_some(*expected)) + .ok_or_else(|| format!("matrix cell has no expected outcome: {}", vector.cell))?; + if vector.expected != expected { + return Err(format!( + "matrix cell {} requires {expected}, found {}", + vector.cell, vector.expected + )); + } + } + Ok(()) +} + +fn validate_exact_once_coverage(vectors: &[RawVector]) -> Result<(), String> { + let seen = vectors + .iter() + .map(|vector| vector.cell.as_str()) + .collect::>(); + + if vectors.len() != MATRIX_CELLS.len() || seen.len() != vectors.len() { + return Err("matrix cells are missing or duplicated".into()); + } + if MATRIX_CELLS.iter().any(|cell| !seen.contains(cell)) { + return Err("matrix cells are missing".into()); + } + Ok(()) +} + +fn complete_matrix_vectors() -> Vec { + MATRIX_CELLS + .iter() + .map(|cell| raw_vector(cell, "no_entry")) + .collect() +} + +fn raw_vector(cell: &str, expected: &str) -> RawVector { + RawVector { + cell: cell.into(), + expected: expected.into(), + } +} diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs new file mode 100644 index 0000000..eadf765 --- /dev/null +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -0,0 +1,224 @@ +//! Execute the frozen PAYG remap parse-gate fixture against the public parser. +//! +//! `tests/golden/payg-parse-vectors.json` names the invalid documents each guard must +//! refuse. Change it only with a format-contract change and fresh mutation evidence: +//! a passing suite alone does not prove a parser guard remains load-bearing. + +use cortexkit_model_catalog::{PaygRemapDoc, PaygRemapParseError}; +use serde::Deserialize; + +const VECTORS: &str = include_str!("golden/payg-parse-vectors.json"); + +#[derive(Debug, Deserialize)] +struct VectorFile { + vectors: Vec, +} + +#[derive(Debug, Deserialize)] +struct Vector { + name: String, + input_json: String, + expect_error: ExpectedError, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "variant")] +enum ExpectedError { + UnknownSchema { + schema: u64, + }, + CounterfactualMismatch { + expected: String, + found: String, + }, + UnknownKind { + id: String, + kind: String, + }, + MalformedId { + id: String, + }, + MissingProvenance { + id: String, + field: String, + }, + SelfReferentialTarget { + id: String, + }, + ChainedTarget { + id: String, + target: String, + }, + ZeroOverride { + id: String, + }, + InexactRate { + id: String, + field: String, + value: String, + }, + NegativeRate { + id: String, + field: String, + value: String, + }, + ContextBandNotRepresentable { + id: String, + message: String, + }, + InvalidIdPrefix { + id: String, + }, +} + +#[test] +fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { + let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); + assert_eq!( + file.vectors.len(), + 24, + "one vector for each non-structural parse guard" + ); + + for vector in file.vectors { + let error = match PaygRemapDoc::parse(&vector.input_json) { + Ok(doc) => panic!("{} unexpectedly parsed: {doc:?}", vector.name), + Err(error) => error, + }; + assert_expected_error(&vector.name, error, vector.expect_error); + } +} + +#[test] +fn override_with_a_real_rate_beside_zero_is_not_all_zero() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "p/m": { + "kind": "overrides_unpriced", + "cost": { "input": 0, "output": 0, "reasoning": 1 }, + "source": "https://vendor.example/pricing", + "observed": "2026-08-13" + } + } + }"#, + ) + .expect("a mixed override has a real rate and must parse"); + + assert_eq!(doc.entries.len(), 1); +} + +// `payg_remap_parse::parses_provider_rule_and_resolve_target_without_lookup_fallback` +// owns the target-operand mutation: its terminal target is absent from entries, so +// `entries.contains_key(id)` must not turn a valid declaration into ChainedTarget. + +fn assert_expected_error(name: &str, error: PaygRemapParseError, expected: ExpectedError) { + match (error, expected) { + ( + PaygRemapParseError::UnknownSchema { schema: actual }, + ExpectedError::UnknownSchema { schema: expected }, + ) => assert_eq!(actual, expected, "{name}: UnknownSchema.schema"), + ( + PaygRemapParseError::CounterfactualMismatch { + expected: actual_expected, + found: actual_found, + }, + ExpectedError::CounterfactualMismatch { expected, found }, + ) => { + assert_eq!( + actual_expected, expected, + "{name}: CounterfactualMismatch.expected" + ); + assert_eq!(actual_found, found, "{name}: CounterfactualMismatch.found"); + } + ( + PaygRemapParseError::UnknownKind { + id: actual_id, + kind: actual_kind, + }, + ExpectedError::UnknownKind { id, kind }, + ) => { + assert_eq!(actual_id, id, "{name}: UnknownKind.id"); + assert_eq!(actual_kind, kind, "{name}: UnknownKind.kind"); + } + ( + PaygRemapParseError::MalformedId { id: actual }, + ExpectedError::MalformedId { id: expected }, + ) => { + assert_eq!(actual, expected, "{name}: MalformedId.id"); + } + ( + PaygRemapParseError::MissingProvenance { + id: actual_id, + field: actual_field, + }, + ExpectedError::MissingProvenance { id, field }, + ) => { + assert_eq!(actual_id, id, "{name}: MissingProvenance.id"); + assert_eq!(actual_field, field, "{name}: MissingProvenance.field"); + } + ( + PaygRemapParseError::SelfReferentialTarget { id: actual }, + ExpectedError::SelfReferentialTarget { id: expected }, + ) => assert_eq!(actual, expected, "{name}: SelfReferentialTarget.id"), + ( + PaygRemapParseError::ChainedTarget { + id: actual_id, + target: actual_target, + }, + ExpectedError::ChainedTarget { id, target }, + ) => { + assert_eq!(actual_id, id, "{name}: ChainedTarget.id"); + assert_eq!(actual_target, target, "{name}: ChainedTarget.target"); + } + ( + PaygRemapParseError::ZeroOverride { id: actual }, + ExpectedError::ZeroOverride { id: expected }, + ) => { + assert_eq!(actual, expected, "{name}: ZeroOverride.id"); + } + ( + PaygRemapParseError::InexactRate { + id: actual_id, + field: actual_field, + value: actual_value, + }, + ExpectedError::InexactRate { id, field, value }, + ) => { + assert_eq!(actual_id, id, "{name}: InexactRate.id"); + assert_eq!(actual_field, field, "{name}: InexactRate.field"); + assert_eq!(actual_value, value, "{name}: InexactRate.value"); + } + ( + PaygRemapParseError::NegativeRate { + id: actual_id, + field: actual_field, + value: actual_value, + }, + ExpectedError::NegativeRate { id, field, value }, + ) => { + assert_eq!(actual_id, id, "{name}: NegativeRate.id"); + assert_eq!(actual_field, field, "{name}: NegativeRate.field"); + assert_eq!(actual_value, value, "{name}: NegativeRate.value"); + } + ( + ref actual @ PaygRemapParseError::ContextBandNotRepresentable { id: ref actual_id }, + ExpectedError::ContextBandNotRepresentable { id, message }, + ) => { + assert_eq!(actual_id, &id, "{name}: ContextBandNotRepresentable.id"); + assert_eq!( + actual.to_string(), + message, + "{name}: ContextBandNotRepresentable" + ); + } + ( + PaygRemapParseError::InvalidIdPrefix { id: actual }, + ExpectedError::InvalidIdPrefix { id: expected }, + ) => assert_eq!(actual, expected, "{name}: InvalidIdPrefix.id"), + (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"), + } +} diff --git a/crates/cortexkit-model-catalog/tests/payg_remap_parse.rs b/crates/cortexkit-model-catalog/tests/payg_remap_parse.rs new file mode 100644 index 0000000..cfd1b02 --- /dev/null +++ b/crates/cortexkit-model-catalog/tests/payg_remap_parse.rs @@ -0,0 +1,264 @@ +use cortexkit_model_catalog::{ + is_all_zero, CostSchedule, PaygModelId, PaygProviderRuleKind, PaygRemapDoc, PaygRemapEntry, + PaygRemapParseError, +}; + +#[test] +fn all_none_schedule_is_unpriced_not_zero() { + assert!(!is_all_zero(&CostSchedule::default())); +} + +#[test] +fn all_zero_schedule_is_zero() { + let schedule = CostSchedule { + input: Some(0), + output: Some(0), + ..CostSchedule::default() + }; + + assert!(is_all_zero(&schedule)); +} + +#[test] +fn parses_minimal_schema_one_document() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "plan/model": { + "kind": "not_sold_per_token", + "reason": "plan_only_no_published_rate", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13" + } + } + }"#, + ) + .unwrap(); + + let id = PaygModelId::parse("plan/model").unwrap(); + assert_eq!(id.provider(), "plan"); + assert_eq!(id.model(), "model"); + assert!(matches!( + doc.entries.get(&id), + Some(PaygRemapEntry::NotSoldPerToken(_)) + )); + assert!(doc.providers.is_empty()); +} + +#[test] +fn rejects_document_level_parse_guards() { + let cases = [ + ( + r#"{ "schema": 2, "counterfactual": "same_platform_list", "providers": {}, "entries": {} }"#, + "unknown schema", + ), + ( + r#"{ "schema": 1, "counterfactual": "different", "providers": {}, "entries": {} }"#, + "counterfactual mismatch", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "model": { "kind": "not_sold_per_token", "reason": "plan", "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "malformed id", + ), + ]; + + for (json, name) in cases { + let error = PaygRemapDoc::parse(json).unwrap_err(); + match name { + "unknown schema" => assert!(matches!( + error, + PaygRemapParseError::UnknownSchema { schema: 2 } + )), + "counterfactual mismatch" => assert!(matches!( + error, + PaygRemapParseError::CounterfactualMismatch { .. } + )), + "malformed id" => assert!(matches!(error, PaygRemapParseError::MalformedId { .. })), + _ => unreachable!(), + } + } +} + +#[test] +fn parses_provider_rule_and_resolve_target_without_lookup_fallback() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "google": { + "kind": "zeros_are_not_prices", + "id_prefix": "antigravity-", + "source": "https://vendor.example/rules", + "observed": "2026-08-13" + } + }, + "entries": { + "reseller/model": { + "kind": "resolves_to", + "target": "origin/model", + "because": "origin api rate", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13" + } + } + }"#, + ) + .unwrap(); + + assert!(matches!( + doc.providers.get("google").map(|rule| &rule.kind), + Some(PaygProviderRuleKind::ZerosAreNotPrices) + )); + let reseller = PaygModelId::parse("reseller/model").unwrap(); + let origin = PaygModelId::parse("origin/model").unwrap(); + assert_ne!(reseller, origin); + let nested = PaygModelId::parse("provider/path/with/slashes").unwrap(); + assert_eq!(nested.provider(), "provider"); + assert_eq!(nested.model(), "path/with/slashes"); + match doc.entries.get(&reseller).unwrap() { + PaygRemapEntry::ResolvesTo(entry) => assert_eq!(entry.target, origin), + other => panic!("expected resolves_to, got {other:?}"), + } +} + +#[test] +fn rejects_malformed_provider_qualified_ids() { + for id in ["model", "/model", "provider/"] { + assert!(matches!( + PaygModelId::parse(id), + Err(PaygRemapParseError::MalformedId { .. }) + )); + } +} + +#[test] +fn rejects_invalid_entries_and_zero_overrides() { + let cases = [ + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "unknown", "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "unknown kind", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "not_sold_per_token", "reason": "plan", "observed": "2026-08-13" } } }"#, + "missing provenance", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "resolves_to", "target": "p/m", "because": "self", "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "self target", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "overrides_unpriced", "cost": { "input": 0 }, "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "zero override", + ), + ]; + + for (json, name) in cases { + let error = PaygRemapDoc::parse(json).unwrap_err(); + match name { + "unknown kind" => assert!(matches!(error, PaygRemapParseError::UnknownKind { .. })), + "missing provenance" => assert!(matches!( + error, + PaygRemapParseError::MissingProvenance { .. } + )), + "self target" => assert!(matches!( + error, + PaygRemapParseError::SelfReferentialTarget { .. } + )), + "zero override" => assert!(matches!(error, PaygRemapParseError::ZeroOverride { .. })), + _ => unreachable!(), + } + } +} + +#[test] +fn rejects_all_remaining_parse_guards() { + let cases = [ + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": { "p": { "kind": "zeros_are_not_prices", "observed": "2026-08-13" } }, "entries": {} }"#, + "provider provenance", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": { "p": { "kind": "unknown", "source": "https://vendor.example", "observed": "2026-08-13" } }, "entries": {} }"#, + "provider kind", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/a": { "kind": "resolves_to", "target": "p/b", "because": "a", "source": "https://vendor.example", "observed": "2026-08-13" }, "p/b": { "kind": "not_sold_per_token", "reason": "plan", "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "chained target", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "overrides_unpriced", "cost": { "input": 1e-10 }, "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "inexact rate", + ), + ( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "overrides_unpriced", "cost": { "output": -1 }, "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + "negative rate", + ), + ]; + + for (json, name) in cases { + let error = PaygRemapDoc::parse(json).unwrap_err(); + match name { + "provider provenance" => assert!(matches!( + error, + PaygRemapParseError::MissingProvenance { ref id, field: "source" } if id == "p" + )), + "provider kind" => assert!(matches!(error, PaygRemapParseError::UnknownKind { .. })), + "chained target" => assert!(matches!(error, PaygRemapParseError::ChainedTarget { .. })), + "inexact rate" => assert!(matches!(error, PaygRemapParseError::InexactRate { .. })), + "negative rate" => assert!(matches!(error, PaygRemapParseError::NegativeRate { .. })), + _ => unreachable!(), + } + } +} + +#[test] +fn parses_nonzero_override_schedule_and_rejects_unrepresentable_rate_blocks() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "plan/model": { + "kind": "overrides_unpriced", + "cost": { + "input": 0.5, + "output": 3, + "reasoning": 1, + "tiers": [ + { "tier": { "type": "context", "size": 256000 }, "input": 2, "output": 6 }, + { "tier": { "type": "context", "size": 128000 }, "input": 1, "output": 4 } + ] + }, + "source": "https://vendor.example/pricing", + "observed": "2026-08-13" + } + } + }"#, + ) + .unwrap(); + let id = PaygModelId::parse("plan/model").unwrap(); + match doc.entries.get(&id).unwrap() { + PaygRemapEntry::OverridesUnpriced(entry) => { + assert_eq!(entry.cost.input, Some(500_000_000)); + assert_eq!(entry.cost.output, Some(3_000_000_000)); + assert_eq!(entry.cost.reasoning, Some(1_000_000_000)); + assert_eq!(entry.cost.tiers[0].min_context, 128_000); + assert_eq!(entry.cost.tiers[1].min_context, 256_000); + } + other => panic!("expected overrides_unpriced, got {other:?}"), + } + + let error = PaygRemapDoc::parse( + r#"{ "schema": 1, "counterfactual": "same_platform_list", "providers": {}, "entries": { "p/m": { "kind": "overrides_unpriced", "cost": { "input": 1, "context_over_200k": { "input": 2 } }, "source": "https://vendor.example", "observed": "2026-08-13" } } }"#, + ) + .unwrap_err(); + assert!(matches!( + error, + PaygRemapParseError::ContextBandNotRepresentable { ref id } if id == "p/m" + )); +} From c82a72cc25a1d6bc658b3e4ee62081e0e072ed93 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:31:54 +0200 Subject: [PATCH 2/7] feat(model-catalog): optional effective_from, distinct from observed --- .../cortexkit-model-catalog/src/payg_remap.rs | 52 ++++++++ .../tests/golden/payg-parse-vectors.json | 9 ++ .../tests/payg_parse_vectors.rs | 112 +++++++++++++++++- 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs index 1687cef..4a8326b 100644 --- a/crates/cortexkit-model-catalog/src/payg_remap.rs +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -128,6 +128,7 @@ pub struct PaygProviderRule { pub id_prefix: Option, pub source: String, pub observed: String, + pub effective_from: Option, } /// A specific PAYG remap declaration. @@ -145,6 +146,7 @@ pub struct ResolvesToEntry { pub because: String, pub source: String, pub observed: String, + pub effective_from: Option, } /// A declaration that supplies a sourced schedule absent from the catalog. @@ -153,6 +155,7 @@ pub struct OverridesUnpricedEntry { pub cost: CostSchedule, pub source: String, pub observed: String, + pub effective_from: Option, } /// A declaration that the platform has no per-token rate for this identifier. @@ -161,6 +164,7 @@ pub struct NotSoldPerTokenEntry { pub reason: String, pub source: String, pub observed: String, + pub effective_from: Option, } /// A PAYG remap-document parse failure. @@ -211,6 +215,10 @@ pub enum PaygRemapParseError { InvalidIdPrefix { id: String, }, + InvalidEffectiveFrom { + id: String, + value: String, + }, } impl std::fmt::Display for PaygRemapParseError { @@ -257,6 +265,9 @@ impl std::fmt::Display for PaygRemapParseError { Self::InvalidIdPrefix { id } => { write!(f, "PAYG provider rule {id} has a non-string id_prefix") } + Self::InvalidEffectiveFrom { id, value } => { + write!(f, "PAYG remap declaration {id} has invalid effective_from {value:?}") + } } } } @@ -296,6 +307,7 @@ fn parse_provider_rules( id_prefix, source: required_provenance(rule, id, "source")?, observed: required_provenance(rule, id, "observed")?, + effective_from: optional_effective_from(rule, id)?, }, ); } @@ -316,6 +328,7 @@ fn parse_entries( })?; let source = required_provenance(entry, raw_id, "source")?; let observed = required_provenance(entry, raw_id, "observed")?; + let effective_from = optional_effective_from(entry, raw_id)?; let kind = required_string(entry, raw_id, "kind")?; let entry = match kind.as_str() { "resolves_to" => PaygRemapEntry::ResolvesTo(ResolvesToEntry { @@ -323,6 +336,7 @@ fn parse_entries( because: required_string(entry, raw_id, "because")?, source, observed, + effective_from, }), "overrides_unpriced" => PaygRemapEntry::OverridesUnpriced(OverridesUnpricedEntry { cost: parse_override_cost( @@ -333,11 +347,13 @@ fn parse_entries( )?, source, observed, + effective_from, }), "not_sold_per_token" => PaygRemapEntry::NotSoldPerToken(NotSoldPerTokenEntry { reason: required_string(entry, raw_id, "reason")?, source, observed, + effective_from, }), _ => { return Err(PaygRemapParseError::UnknownKind { @@ -367,6 +383,42 @@ fn required_provenance( }) } +/// `observed` is audit provenance, while `effective_from` selects a pricing era for downstream +/// arithmetic. Keep this stricter check separate: malformed effective dates can choose a wrong +/// rate, whereas malformed observations merely confuse a human auditor. This validates only the +/// `YYYY-MM-DD` shape, not calendar validity. +fn optional_effective_from( + entry: &serde_json::Map, + id: &str, +) -> Result, PaygRemapParseError> { + let Some(value) = entry.get("effective_from") else { + return Ok(None); + }; + let Some(date) = value.as_str() else { + return Err(PaygRemapParseError::InvalidEffectiveFrom { + id: id.into(), + value: value.to_string(), + }); + }; + if !is_yyyy_mm_dd(date) { + return Err(PaygRemapParseError::InvalidEffectiveFrom { + id: id.into(), + value: date.into(), + }); + } + Ok(Some(date.into())) +} + +fn is_yyyy_mm_dd(value: &str) -> bool { + value.len() == 10 + && value.as_bytes()[4] == b'-' + && value.as_bytes()[7] == b'-' + && value + .bytes() + .enumerate() + .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) +} + fn required_string( entry: &serde_json::Map, id: &str, diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index fd3f4cc..9d5cf8d 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -142,6 +142,15 @@ "variant": "InvalidIdPrefix", "id": "p" } + }, + { + "name": "malformed-entry-effective-from", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":\"2026-8-13\"}}}", + "expect_error": { + "variant": "InvalidEffectiveFrom", + "id": "p/m", + "value": "2026-8-13" + } } ] } diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index eadf765..266de09 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -4,7 +4,7 @@ //! refuse. Change it only with a format-contract change and fresh mutation evidence: //! a passing suite alone does not prove a parser guard remains load-bearing. -use cortexkit_model_catalog::{PaygRemapDoc, PaygRemapParseError}; +use cortexkit_model_catalog::{PaygModelId, PaygRemapDoc, PaygRemapEntry, PaygRemapParseError}; use serde::Deserialize; const VECTORS: &str = include_str!("golden/payg-parse-vectors.json"); @@ -69,6 +69,10 @@ enum ExpectedError { InvalidIdPrefix { id: String, }, + InvalidEffectiveFrom { + id: String, + value: String, + }, } #[test] @@ -76,7 +80,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.vectors.len(), - 24, + 25, "one vector for each non-structural parse guard" ); @@ -89,6 +93,100 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { } } +#[test] +fn malformed_effective_from_is_refused() { + let error = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "p/m": { + "kind": "not_sold_per_token", + "reason": "plan_only", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13", + "effective_from": "2026-8-13" + } + } + }"#, + ) + .expect_err("an effective date outside YYYY-MM-DD must be refused"); + + assert_eq!( + error.to_string(), + "PAYG remap declaration p/m has invalid effective_from \"2026-8-13\"" + ); +} + +#[test] +fn entry_effective_from_round_trips() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": { + "p": { + "kind": "zeros_are_not_prices", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13", + "effective_from": "2026-09-01" + } + }, + "entries": { + "p/with-effective-date": { + "kind": "not_sold_per_token", + "reason": "plan_only", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13", + "effective_from": "2026-09-01" + } + } + }"#, + ) + .expect("a shaped effective date must parse"); + + assert_eq!( + doc.providers["p"].effective_from.as_deref(), + Some("2026-09-01") + ); + let id = PaygModelId::parse("p/with-effective-date").expect("valid test id"); + let PaygRemapEntry::NotSoldPerToken(entry) = &doc.entries[&id] else { + panic!("expected not_sold_per_token entry"); + }; + assert_eq!(entry.effective_from.as_deref(), Some("2026-09-01")); +} + +#[test] +fn entry_without_effective_from_stays_absent() { + let doc = PaygRemapDoc::parse( + r#"{ + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "p/without-effective-date": { + "kind": "not_sold_per_token", + "reason": "plan_only", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13" + } + } + }"#, + ) + .expect("an entry without effective_from must remain valid"); + + let id = PaygModelId::parse("p/without-effective-date").expect("valid test id"); + let PaygRemapEntry::NotSoldPerToken(entry) = &doc.entries[&id] else { + panic!("expected not_sold_per_token entry"); + }; + assert_eq!(entry.effective_from, None); + assert_ne!( + entry.effective_from.as_deref(), + Some(entry.observed.as_str()) + ); +} + #[test] fn override_with_a_real_rate_beside_zero_is_not_all_zero() { let doc = PaygRemapDoc::parse( @@ -219,6 +317,16 @@ fn assert_expected_error(name: &str, error: PaygRemapParseError, expected: Expec PaygRemapParseError::InvalidIdPrefix { id: actual }, ExpectedError::InvalidIdPrefix { id: expected }, ) => assert_eq!(actual, expected, "{name}: InvalidIdPrefix.id"), + ( + PaygRemapParseError::InvalidEffectiveFrom { + id: actual_id, + value: actual_value, + }, + ExpectedError::InvalidEffectiveFrom { id, value }, + ) => { + assert_eq!(actual_id, id, "{name}: InvalidEffectiveFrom.id"); + assert_eq!(actual_value, value, "{name}: InvalidEffectiveFrom.value"); + } (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"), } } From 8be18d61ea1b26d28b5ffc499ad9e857fabc7563 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:58:33 +0200 Subject: [PATCH 3/7] fix(model-catalog): accept null effective_from, replace a vacuous assertion --- .../cortexkit-model-catalog/src/payg_remap.rs | 5 ++- .../tests/golden/payg-parse-vectors.json | 17 ++++++++ .../tests/payg_parse_vectors.rs | 41 ++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs index 4a8326b..c5ae425 100644 --- a/crates/cortexkit-model-catalog/src/payg_remap.rs +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -391,8 +391,9 @@ fn optional_effective_from( entry: &serde_json::Map, id: &str, ) -> Result, PaygRemapParseError> { - let Some(value) = entry.get("effective_from") else { - return Ok(None); + let value = match entry.get("effective_from") { + None | Some(Value::Null) => return Ok(None), + Some(value) => value, }; let Some(date) = value.as_str() else { return Err(PaygRemapParseError::InvalidEffectiveFrom { diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index 9d5cf8d..a0eaff7 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -151,6 +151,23 @@ "id": "p/m", "value": "2026-8-13" } + }, + { + "name": "non-string-entry-effective-from", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":123}}}", + "expect_error": { + "variant": "InvalidEffectiveFrom", + "id": "p/m", + "value": "123" + } + } + ], + "positive_vectors": [ + { + "name": "null-entry-effective-from-stays-absent", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":null}}}", + "id": "p/m", + "effective_from": null } ] } diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index 266de09..98c3d1f 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -12,6 +12,8 @@ const VECTORS: &str = include_str!("golden/payg-parse-vectors.json"); #[derive(Debug, Deserialize)] struct VectorFile { vectors: Vec, + #[serde(default)] + positive_vectors: Vec, } #[derive(Debug, Deserialize)] @@ -21,6 +23,14 @@ struct Vector { expect_error: ExpectedError, } +#[derive(Debug, Deserialize)] +struct PositiveVector { + name: String, + input_json: String, + id: String, + effective_from: Option, +} + #[derive(Debug, Deserialize)] #[serde(tag = "variant")] enum ExpectedError { @@ -80,7 +90,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.vectors.len(), - 25, + 26, "one vector for each non-structural parse guard" ); @@ -93,6 +103,30 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { } } +#[test] +fn parse_gate_accepts_every_positive_golden_vector() { + let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); + + assert_eq!( + file.positive_vectors.len(), + 1, + "one vector for an explicitly unset optional field" + ); + for vector in file.positive_vectors { + let doc = PaygRemapDoc::parse(&vector.input_json) + .unwrap_or_else(|error| panic!("{} unexpectedly refused: {error}", vector.name)); + let id = PaygModelId::parse(&vector.id).expect("golden vector must use a valid id"); + let PaygRemapEntry::NotSoldPerToken(entry) = &doc.entries[&id] else { + panic!("{} must parse a not_sold_per_token entry", vector.name); + }; + assert_eq!( + entry.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); + } +} + #[test] fn malformed_effective_from_is_refused() { let error = PaygRemapDoc::parse( @@ -180,11 +214,8 @@ fn entry_without_effective_from_stays_absent() { let PaygRemapEntry::NotSoldPerToken(entry) = &doc.entries[&id] else { panic!("expected not_sold_per_token entry"); }; + assert_eq!(entry.observed, "2026-08-13"); assert_eq!(entry.effective_from, None); - assert_ne!( - entry.effective_from.as_deref(), - Some(entry.observed.as_str()) - ); } #[test] From 4c8287e99fca5cf43fbba458b200f04d25dc0d1e Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:00:56 +0200 Subject: [PATCH 4/7] feat(model-catalog): rate_time_banded, an explicit refusal for time-varying list rates --- crates/cortexkit-model-catalog/src/lib.rs | 3 +- .../src/payg_conformance.rs | 55 ++++++++++++ .../cortexkit-model-catalog/src/payg_remap.rs | 46 +++++++++- .../tests/golden/payg-class-vectors.json | 31 +++++++ .../tests/golden/payg-parse-vectors.json | 37 ++++++++ .../tests/payg_class_vectors.rs | 5 +- .../tests/payg_parse_vectors.rs | 88 ++++++++++++++++--- 7 files changed, 251 insertions(+), 14 deletions(-) diff --git a/crates/cortexkit-model-catalog/src/lib.rs b/crates/cortexkit-model-catalog/src/lib.rs index ea1d16e..b4eea17 100644 --- a/crates/cortexkit-model-catalog/src/lib.rs +++ b/crates/cortexkit-model-catalog/src/lib.rs @@ -27,7 +27,8 @@ mod payg_remap; pub use payg_conformance::{run_vectors, PaygOutcome, PaygVector, PaygVectorSuite, VectorFailure}; pub use payg_remap::{ is_all_zero, NotSoldPerTokenEntry, OverridesUnpricedEntry, PaygModelId, PaygProviderRule, - PaygProviderRuleKind, PaygRemapDoc, PaygRemapEntry, PaygRemapParseError, ResolvesToEntry, + PaygProviderRuleKind, PaygRemapDoc, PaygRemapEntry, PaygRemapParseError, RateTimeBandedEntry, + ResolvesToEntry, }; /// Integer nanodollars per million tokens. $3/M tokens = 3_000_000_000. diff --git a/crates/cortexkit-model-catalog/src/payg_conformance.rs b/crates/cortexkit-model-catalog/src/payg_conformance.rs index fc9b378..6df8a30 100644 --- a/crates/cortexkit-model-catalog/src/payg_conformance.rs +++ b/crates/cortexkit-model-catalog/src/payg_conformance.rs @@ -9,6 +9,9 @@ use crate::{CatalogDoc, PaygModelId, PaygRemapDoc}; pub enum PaygOutcome { Priced, NotSoldPerToken, + /// Refuses a catalog scalar like other refusal outcomes because it cannot be correct for every + /// time band. + RateTimeBanded, TargetNotInCatalog, TargetNotPriceable, DeclarationSuperseded, @@ -147,4 +150,56 @@ mod tests { assert_eq!(suite.vectors[0].model.as_str(), "provider/model"); assert_eq!(suite.vectors[0].expected, PaygOutcome::Priced); } + + #[test] + fn reports_the_named_time_banded_vector_when_collapsed_to_not_sold() { + let vectors: PaygVectorSuite = + serde_json::from_str(include_str!("../tests/golden/payg-class-vectors.json")) + .expect("time-banded classification vector parses"); + + let time_banded = vectors + .vectors + .into_iter() + .find(|vector| vector.name == "time-banded-priced-source") + .expect("time-banded vector is present"); + let failures = run_vectors( + &PaygVectorSuite { + vectors: vec![time_banded], + }, + |_, _, _| PaygOutcome::NotSoldPerToken, + ); + + assert_eq!( + failures, + vec![super::VectorFailure { + vector: "time-banded-priced-source".into(), + expected: PaygOutcome::RateTimeBanded, + actual: PaygOutcome::NotSoldPerToken, + }] + ); + } + + #[test] + fn time_banded_vector_requires_its_distinct_outcome() { + let vectors: PaygVectorSuite = + serde_json::from_str(include_str!("../tests/golden/payg-class-vectors.json")) + .expect("time-banded classification vector parses"); + let time_banded = vectors + .vectors + .into_iter() + .find(|vector| vector.name == "time-banded-priced-source") + .expect("time-banded vector is present"); + + let failures = run_vectors( + &PaygVectorSuite { + vectors: vec![time_banded], + }, + |remap, _, model| match remap.entries.get(model) { + Some(crate::PaygRemapEntry::RateTimeBanded(_)) => PaygOutcome::RateTimeBanded, + _ => PaygOutcome::NoEntry, + }, + ); + + assert!(failures.is_empty(), "{failures:#?}"); + } } diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs index c5ae425..4190cfd 100644 --- a/crates/cortexkit-model-catalog/src/payg_remap.rs +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -115,10 +115,11 @@ impl PaygRemapDoc { } } -/// The only provider-wide PAYG refusal rule. +/// Provider-wide PAYG refusal rules. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PaygProviderRuleKind { ZerosAreNotPrices, + RateTimeBanded, } /// A provider-scoped PAYG refusal rule. @@ -137,6 +138,7 @@ pub enum PaygRemapEntry { ResolvesTo(ResolvesToEntry), OverridesUnpriced(OverridesUnpricedEntry), NotSoldPerToken(NotSoldPerTokenEntry), + RateTimeBanded(RateTimeBandedEntry), } /// A declaration that points at one terminal catalog schedule. @@ -167,6 +169,14 @@ pub struct NotSoldPerTokenEntry { pub effective_from: Option, } +/// A declaration that per-token list pricing varies by time and cannot be represented safely. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RateTimeBandedEntry { + pub source: String, + pub observed: String, + pub effective_from: Option, +} + /// A PAYG remap-document parse failure. #[derive(Debug, Clone, PartialEq, Eq)] pub enum PaygRemapParseError { @@ -219,6 +229,10 @@ pub enum PaygRemapParseError { id: String, value: String, }, + UnexpectedField { + id: String, + field: String, + }, } impl std::fmt::Display for PaygRemapParseError { @@ -268,6 +282,9 @@ impl std::fmt::Display for PaygRemapParseError { Self::InvalidEffectiveFrom { id, value } => { write!(f, "PAYG remap declaration {id} has invalid effective_from {value:?}") } + Self::UnexpectedField { id, field } => { + write!(f, "PAYG remap declaration {id} has unexpected field {field}") + } } } } @@ -288,6 +305,7 @@ fn parse_provider_rules( let kind = required_string(rule, id, "kind")?; let kind = match kind.as_str() { "zeros_are_not_prices" => PaygProviderRuleKind::ZerosAreNotPrices, + "rate_time_banded" => PaygProviderRuleKind::RateTimeBanded, _ => { return Err(PaygRemapParseError::UnknownKind { id: id.clone(), @@ -355,6 +373,14 @@ fn parse_entries( observed, effective_from, }), + "rate_time_banded" => { + reject_unexpected_time_banded_fields(entry, raw_id)?; + PaygRemapEntry::RateTimeBanded(RateTimeBandedEntry { + source, + observed, + effective_from, + }) + } _ => { return Err(PaygRemapParseError::UnknownKind { id: raw_id.clone(), @@ -367,6 +393,24 @@ fn parse_entries( Ok(parsed) } +fn reject_unexpected_time_banded_fields( + entry: &serde_json::Map, + id: &str, +) -> Result<(), PaygRemapParseError> { + for field in entry.keys() { + if !matches!( + field.as_str(), + "kind" | "source" | "observed" | "effective_from" + ) { + return Err(PaygRemapParseError::UnexpectedField { + id: id.into(), + field: field.clone(), + }); + } + } + Ok(()) +} + fn required_provenance( entry: &serde_json::Map, id: &str, diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json index 2a64321..0b26d63 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-class-vectors.json @@ -877,6 +877,37 @@ "catalog": {}, "model": "source/model", "expected": "no_entry" + }, + { + "name": "time-banded-priced-source", + "cell": "rate_time_banded/priced", + "remap": { + "schema": 1, + "counterfactual": "same_platform_list", + "providers": {}, + "entries": { + "deepseek/deepseek-v4-pro": { + "kind": "rate_time_banded", + "source": "https://vendor.example/pricing", + "observed": "2026-08-16" + } + } + }, + "catalog": { + "deepseek": { + "models": { + "deepseek-v4-pro": { + "cost": { + "input": 0.66, + "output": 1.98, + "cache_read": 0.022 + } + } + } + } + }, + "model": "deepseek/deepseek-v4-pro", + "expected": "rate_time_banded" } ] } diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index a0eaff7..bf88bb1 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -160,6 +160,24 @@ "id": "p/m", "value": "123" } + }, + { + "name": "time-banded-entry-rejects-rates", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"rate_time_banded\",\"rates\":{\"input\":0.66},\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p/m", + "field": "rates" + } + }, + { + "name": "time-banded-entry-rejects-target", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"rate_time_banded\",\"target\":\"p/other\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p/m", + "field": "target" + } } ], "positive_vectors": [ @@ -168,6 +186,25 @@ "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":null}}}", "id": "p/m", "effective_from": null + }, + { + "name": "time-banded-entry-provenance-round-trips", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"deepseek/deepseek-v4-pro\":{\"kind\":\"rate_time_banded\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\",\"effective_from\":\"2026-08-16\"}}}", + "id": "deepseek/deepseek-v4-pro", + "entry_kind": "rate_time_banded", + "source": "https://vendor.example/pricing", + "observed": "2026-08-16", + "effective_from": "2026-08-16" + }, + { + "name": "time-banded-provider-rule-provenance-round-trips", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"deepseek\":{\"kind\":\"rate_time_banded\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\",\"effective_from\":\"2026-08-16\"}},\"entries\":{}}", + "id": "deepseek/placeholder", + "provider": "deepseek", + "provider_kind": "rate_time_banded", + "source": "https://vendor.example/pricing", + "observed": "2026-08-16", + "effective_from": "2026-08-16" } ] } diff --git a/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs index 43c3d07..679d2ad 100644 --- a/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_class_vectors.rs @@ -35,6 +35,7 @@ const MATRIX_CELLS: &[&str] = &[ "not_sold_per_token/all-zero", "not_sold_per_token/all-none", "not_sold_per_token/absent", + "rate_time_banded/priced", "zeros_are_not_prices/priced", "zeros_are_not_prices/all-zero", "zeros_are_not_prices/all-none", @@ -48,6 +49,7 @@ const MATRIX_CELLS: &[&str] = &[ const LEGAL_OUTCOMES: &[&str] = &[ "priced", "not_sold_per_token", + "rate_time_banded", "target_not_in_catalog", "target_not_priceable", "declaration_superseded", @@ -94,6 +96,7 @@ const CELL_CONTRACT: &[(&str, &str)] = &[ ("not_sold_per_token/all-zero", "not_sold_per_token"), ("not_sold_per_token/all-none", "not_sold_per_token"), ("not_sold_per_token/absent", "not_sold_per_token"), + ("rate_time_banded/priced", "rate_time_banded"), ("zeros_are_not_prices/priced", "no_entry"), ("zeros_are_not_prices/all-zero", "not_sold_per_token"), ("zeros_are_not_prices/all-none", "not_sold_per_token"), @@ -190,7 +193,7 @@ fn validation_diagnostics_collect_independent_failures() { assert_eq!(failures.len(), 5, "{failures:#?}"); assert!(failures .iter() - .any(|failure| failure.contains("expected 29 vectors"))); + .any(|failure| failure.contains("expected 30 vectors"))); assert!(failures .iter() .any(|failure| failure.contains("unknown matrix cell"))); diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index 98c3d1f..e1c7a43 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -28,6 +28,11 @@ struct PositiveVector { name: String, input_json: String, id: String, + entry_kind: Option, + provider: Option, + provider_kind: Option, + source: Option, + observed: Option, effective_from: Option, } @@ -83,6 +88,10 @@ enum ExpectedError { id: String, value: String, }, + UnexpectedField { + id: String, + field: String, + }, } #[test] @@ -90,7 +99,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.vectors.len(), - 26, + 28, "one vector for each non-structural parse guard" ); @@ -109,21 +118,68 @@ fn parse_gate_accepts_every_positive_golden_vector() { assert_eq!( file.positive_vectors.len(), - 1, - "one vector for an explicitly unset optional field" + 3, + "one unset optional field plus entry and provider time-banded declarations" ); for vector in file.positive_vectors { let doc = PaygRemapDoc::parse(&vector.input_json) .unwrap_or_else(|error| panic!("{} unexpectedly refused: {error}", vector.name)); let id = PaygModelId::parse(&vector.id).expect("golden vector must use a valid id"); - let PaygRemapEntry::NotSoldPerToken(entry) = &doc.entries[&id] else { - panic!("{} must parse a not_sold_per_token entry", vector.name); - }; - assert_eq!( - entry.effective_from, vector.effective_from, - "{}: effective_from", - vector.name - ); + if let Some(expected_kind) = vector.entry_kind.as_deref() { + match (expected_kind, &doc.entries[&id]) { + ("not_sold_per_token", PaygRemapEntry::NotSoldPerToken(entry)) => { + if let Some(source) = vector.source.as_deref() { + assert_eq!(entry.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(entry.observed, observed, "{}: observed", vector.name); + } + assert_eq!( + entry.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); + } + ("rate_time_banded", PaygRemapEntry::RateTimeBanded(entry)) => { + if let Some(source) = vector.source.as_deref() { + assert_eq!(entry.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(entry.observed, observed, "{}: observed", vector.name); + } + assert_eq!( + entry.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); + } + (kind, entry) => panic!("{} must parse a {kind} entry, got {entry:?}", vector.name), + } + } + + if let (Some(provider), Some(expected_kind)) = + (vector.provider.as_deref(), vector.provider_kind.as_deref()) + { + let rule = &doc.providers[provider]; + match expected_kind { + "rate_time_banded" => assert!(matches!( + rule.kind, + cortexkit_model_catalog::PaygProviderRuleKind::RateTimeBanded + )), + kind => panic!("{} has unknown provider kind {kind}", vector.name), + } + if let Some(source) = vector.source.as_deref() { + assert_eq!(rule.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(rule.observed, observed, "{}: observed", vector.name); + } + assert_eq!( + rule.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); + } } } @@ -358,6 +414,16 @@ fn assert_expected_error(name: &str, error: PaygRemapParseError, expected: Expec assert_eq!(actual_id, id, "{name}: InvalidEffectiveFrom.id"); assert_eq!(actual_value, value, "{name}: InvalidEffectiveFrom.value"); } + ( + PaygRemapParseError::UnexpectedField { + id: actual_id, + field: actual_field, + }, + ExpectedError::UnexpectedField { id, field }, + ) => { + assert_eq!(actual_id, id, "{name}: UnexpectedField.id"); + assert_eq!(actual_field, field, "{name}: UnexpectedField.field"); + } (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"), } } From ae8d7f40c45c4ba981c44372c72c241c7a9fdfcf Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:07:40 +0200 Subject: [PATCH 5/7] test(model-catalog): every positive vector declares and asserts its parsed kind --- .../tests/golden/payg-parse-vectors.json | 4 +- .../tests/payg_parse_vectors.rs | 131 +++++++++++------- 2 files changed, 85 insertions(+), 50 deletions(-) diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index bf88bb1..78b821a 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -185,6 +185,7 @@ "name": "null-entry-effective-from-stays-absent", "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":null}}}", "id": "p/m", + "entry_kind": "not_sold_per_token", "effective_from": null }, { @@ -198,8 +199,9 @@ }, { "name": "time-banded-provider-rule-provenance-round-trips", - "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"deepseek\":{\"kind\":\"rate_time_banded\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\",\"effective_from\":\"2026-08-16\"}},\"entries\":{}}", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"deepseek\":{\"kind\":\"rate_time_banded\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\",\"effective_from\":\"2026-08-16\"}},\"entries\":{\"deepseek/placeholder\":{\"kind\":\"rate_time_banded\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\",\"effective_from\":\"2026-08-16\"}}}", "id": "deepseek/placeholder", + "entry_kind": "rate_time_banded", "provider": "deepseek", "provider_kind": "rate_time_banded", "source": "https://vendor.example/pricing", diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index e1c7a43..c9892d6 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -12,6 +12,10 @@ const VECTORS: &str = include_str!("golden/payg-parse-vectors.json"); #[derive(Debug, Deserialize)] struct VectorFile { vectors: Vec, +} + +#[derive(Debug, Deserialize)] +struct PositiveVectorFile { #[serde(default)] positive_vectors: Vec, } @@ -28,7 +32,7 @@ struct PositiveVector { name: String, input_json: String, id: String, - entry_kind: Option, + entry_kind: String, provider: Option, provider_kind: Option, source: Option, @@ -36,6 +40,18 @@ struct PositiveVector { effective_from: Option, } +#[derive(Debug, Deserialize)] +struct RawPositiveVectorFile { + #[serde(default)] + positive_vectors: Vec, +} + +#[derive(Debug, Deserialize)] +struct RawPositiveVector { + name: String, + entry_kind: Option, +} + #[derive(Debug, Deserialize)] #[serde(tag = "variant")] enum ExpectedError { @@ -114,7 +130,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { #[test] fn parse_gate_accepts_every_positive_golden_vector() { - let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); + let file: PositiveVectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.positive_vectors.len(), @@ -125,64 +141,81 @@ fn parse_gate_accepts_every_positive_golden_vector() { let doc = PaygRemapDoc::parse(&vector.input_json) .unwrap_or_else(|error| panic!("{} unexpectedly refused: {error}", vector.name)); let id = PaygModelId::parse(&vector.id).expect("golden vector must use a valid id"); - if let Some(expected_kind) = vector.entry_kind.as_deref() { - match (expected_kind, &doc.entries[&id]) { - ("not_sold_per_token", PaygRemapEntry::NotSoldPerToken(entry)) => { - if let Some(source) = vector.source.as_deref() { - assert_eq!(entry.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(entry.observed, observed, "{}: observed", vector.name); - } - assert_eq!( - entry.effective_from, vector.effective_from, - "{}: effective_from", - vector.name - ); + match (vector.entry_kind.as_str(), &doc.entries[&id]) { + ("not_sold_per_token", PaygRemapEntry::NotSoldPerToken(entry)) => { + if let Some(source) = vector.source.as_deref() { + assert_eq!(entry.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(entry.observed, observed, "{}: observed", vector.name); } - ("rate_time_banded", PaygRemapEntry::RateTimeBanded(entry)) => { - if let Some(source) = vector.source.as_deref() { - assert_eq!(entry.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(entry.observed, observed, "{}: observed", vector.name); - } - assert_eq!( - entry.effective_from, vector.effective_from, - "{}: effective_from", - vector.name - ); + assert_eq!( + entry.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); + } + ("rate_time_banded", PaygRemapEntry::RateTimeBanded(entry)) => { + if let Some(source) = vector.source.as_deref() { + assert_eq!(entry.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(entry.observed, observed, "{}: observed", vector.name); } - (kind, entry) => panic!("{} must parse a {kind} entry, got {entry:?}", vector.name), + assert_eq!( + entry.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); } + (kind, entry) => panic!("{} must parse a {kind} entry, got {entry:?}", vector.name), } - if let (Some(provider), Some(expected_kind)) = - (vector.provider.as_deref(), vector.provider_kind.as_deref()) - { - let rule = &doc.providers[provider]; - match expected_kind { - "rate_time_banded" => assert!(matches!( - rule.kind, - cortexkit_model_catalog::PaygProviderRuleKind::RateTimeBanded - )), - kind => panic!("{} has unknown provider kind {kind}", vector.name), - } - if let Some(source) = vector.source.as_deref() { - assert_eq!(rule.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(rule.observed, observed, "{}: observed", vector.name); + match (vector.provider.as_deref(), vector.provider_kind.as_deref()) { + (Some(provider), Some(expected_kind)) => { + let rule = &doc.providers[provider]; + match expected_kind { + "rate_time_banded" => assert!(matches!( + rule.kind, + cortexkit_model_catalog::PaygProviderRuleKind::RateTimeBanded + )), + kind => panic!("{} has unknown provider kind {kind}", vector.name), + } + if let Some(source) = vector.source.as_deref() { + assert_eq!(rule.source, source, "{}: source", vector.name); + } + if let Some(observed) = vector.observed.as_deref() { + assert_eq!(rule.observed, observed, "{}: observed", vector.name); + } + assert_eq!( + rule.effective_from, vector.effective_from, + "{}: effective_from", + vector.name + ); } - assert_eq!( - rule.effective_from, vector.effective_from, - "{}: effective_from", + (None, None) => {} + _ => panic!( + "{} must declare both provider and provider_kind", vector.name - ); + ), } } } +#[test] +fn positive_vectors_must_declare_entry_kind() { + let file: RawPositiveVectorFile = + serde_json::from_str(VECTORS).expect("parse raw PAYG parse vectors"); + + for vector in file.positive_vectors { + assert!( + vector.entry_kind.is_some(), + "{} must declare entry_kind", + vector.name + ); + } +} + #[test] fn malformed_effective_from_is_refused() { let error = PaygRemapDoc::parse( From ec38420fc0bb52d688f700350004e319056dc4aa Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Mon, 24 Aug 2026 07:46:47 +0200 Subject: [PATCH 6/7] fix(model-catalog): required provenance assertions, refuse duplicate ids and unknown fields --- crates/cortexkit-model-catalog/src/lib.rs | 2 + .../cortexkit-model-catalog/src/payg_remap.rs | 218 +++++++++++++++--- .../tests/golden/payg-parse-vectors.json | 54 +++++ .../tests/payg_parse_vectors.rs | 60 +++-- 4 files changed, 282 insertions(+), 52 deletions(-) diff --git a/crates/cortexkit-model-catalog/src/lib.rs b/crates/cortexkit-model-catalog/src/lib.rs index b4eea17..f00de03 100644 --- a/crates/cortexkit-model-catalog/src/lib.rs +++ b/crates/cortexkit-model-catalog/src/lib.rs @@ -6,6 +6,8 @@ //! fleet rule is that they must parse the SAME shape so a catalog schema //! drift cannot make them disagree silently. This crate is that shape. //! Consumers bring their own snapshot bytes and own their derived stores. +//! This crate ships no reference classifier: each consumer owns the join from a PAYG remap to +//! its catalog snapshot, while the conformance runner supplies their shared executable contract. //! //! Money discipline: models.dev publishes dollar-per-million-token rates as //! JSON decimal numbers. This crate converts them ONCE, at the parse diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs index 4190cfd..5826e7f 100644 --- a/crates/cortexkit-model-catalog/src/payg_remap.rs +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -1,5 +1,8 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use serde::de::{MapAccess, SeqAccess, Visitor}; +use serde::{Deserialize, Deserializer}; use serde_json::Value; use crate::{dollars_to_nanos, CostSchedule, CostTier, RateNanosPerMtok}; @@ -82,6 +85,8 @@ impl PaygRemapDoc { }); } + reject_duplicate_entry_ids(json)?; + let providers = parse_provider_rules( root.get("providers") .ok_or_else(|| PaygRemapParseError::Json("providers is missing".into()))?, @@ -233,6 +238,12 @@ pub enum PaygRemapParseError { id: String, field: String, }, + DuplicateEntry { + id: String, + }, + DuplicateIdPrefix { + id_prefix: String, + }, } impl std::fmt::Display for PaygRemapParseError { @@ -285,6 +296,10 @@ impl std::fmt::Display for PaygRemapParseError { Self::UnexpectedField { id, field } => { write!(f, "PAYG remap declaration {id} has unexpected field {field}") } + Self::DuplicateEntry { id } => write!(f, "PAYG remap has duplicate entry {id}"), + Self::DuplicateIdPrefix { id_prefix } => { + write!(f, "PAYG remap has duplicate provider id_prefix {id_prefix}") + } } } } @@ -298,10 +313,16 @@ fn parse_provider_rules( .as_object() .ok_or_else(|| PaygRemapParseError::Json("providers is not an object".into()))?; let mut parsed = BTreeMap::new(); + let mut id_prefixes = BTreeSet::new(); for (id, value) in rules { let rule = value.as_object().ok_or_else(|| { PaygRemapParseError::Json(format!("provider rule {id} is not an object")) })?; + reject_unexpected_fields( + rule, + id, + &["kind", "id_prefix", "source", "observed", "effective_from"], + )?; let kind = required_string(rule, id, "kind")?; let kind = match kind.as_str() { "zeros_are_not_prices" => PaygProviderRuleKind::ZerosAreNotPrices, @@ -318,6 +339,13 @@ fn parse_provider_rules( Some(Value::String(prefix)) => Some(prefix.clone()), Some(_) => return Err(PaygRemapParseError::InvalidIdPrefix { id: id.clone() }), }; + if let Some(prefix) = &id_prefix { + if !id_prefixes.insert(prefix.clone()) { + return Err(PaygRemapParseError::DuplicateIdPrefix { + id_prefix: prefix.clone(), + }); + } + } parsed.insert( id.clone(), PaygProviderRule { @@ -349,32 +377,64 @@ fn parse_entries( let effective_from = optional_effective_from(entry, raw_id)?; let kind = required_string(entry, raw_id, "kind")?; let entry = match kind.as_str() { - "resolves_to" => PaygRemapEntry::ResolvesTo(ResolvesToEntry { - target: PaygModelId::parse(&required_string(entry, raw_id, "target")?)?, - because: required_string(entry, raw_id, "because")?, - source, - observed, - effective_from, - }), - "overrides_unpriced" => PaygRemapEntry::OverridesUnpriced(OverridesUnpricedEntry { - cost: parse_override_cost( + "resolves_to" => { + reject_unexpected_fields( + entry, + raw_id, + &[ + "kind", + "target", + "because", + "source", + "observed", + "effective_from", + ], + )?; + PaygRemapEntry::ResolvesTo(ResolvesToEntry { + target: PaygModelId::parse(&required_string(entry, raw_id, "target")?)?, + because: required_string(entry, raw_id, "because")?, + source, + observed, + effective_from, + }) + } + "overrides_unpriced" => { + reject_unexpected_fields( + entry, + raw_id, + &["kind", "cost", "source", "observed", "effective_from"], + )?; + PaygRemapEntry::OverridesUnpriced(OverridesUnpricedEntry { + cost: parse_override_cost( + raw_id, + entry + .get("cost") + .ok_or_else(|| missing_required_field(raw_id, "cost"))?, + )?, + source, + observed, + effective_from, + }) + } + "not_sold_per_token" => { + reject_unexpected_fields( + entry, raw_id, - entry - .get("cost") - .ok_or_else(|| missing_required_field(raw_id, "cost"))?, - )?, - source, - observed, - effective_from, - }), - "not_sold_per_token" => PaygRemapEntry::NotSoldPerToken(NotSoldPerTokenEntry { - reason: required_string(entry, raw_id, "reason")?, - source, - observed, - effective_from, - }), + &["kind", "reason", "source", "observed", "effective_from"], + )?; + PaygRemapEntry::NotSoldPerToken(NotSoldPerTokenEntry { + reason: required_string(entry, raw_id, "reason")?, + source, + observed, + effective_from, + }) + } "rate_time_banded" => { - reject_unexpected_time_banded_fields(entry, raw_id)?; + reject_unexpected_fields( + entry, + raw_id, + &["kind", "source", "observed", "effective_from"], + )?; PaygRemapEntry::RateTimeBanded(RateTimeBandedEntry { source, observed, @@ -393,15 +453,13 @@ fn parse_entries( Ok(parsed) } -fn reject_unexpected_time_banded_fields( +fn reject_unexpected_fields( entry: &serde_json::Map, id: &str, + allowed: &[&str], ) -> Result<(), PaygRemapParseError> { for field in entry.keys() { - if !matches!( - field.as_str(), - "kind" | "source" | "observed" | "effective_from" - ) { + if !allowed.contains(&field.as_str()) { return Err(PaygRemapParseError::UnexpectedField { id: id.into(), field: field.clone(), @@ -411,6 +469,106 @@ fn reject_unexpected_time_banded_fields( Ok(()) } +#[derive(Debug)] +enum DuplicatePreservingValue { + Null, + Bool, + Number, + String, + Array, + Object(Vec<(String, Self)>), +} + +impl<'de> Deserialize<'de> for DuplicatePreservingValue { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct DuplicatePreservingVisitor; + + impl<'de> Visitor<'de> for DuplicatePreservingVisitor { + type Value = DuplicatePreservingValue; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a JSON value") + } + + fn visit_unit(self) -> Result { + Ok(DuplicatePreservingValue::Null) + } + + fn visit_bool(self, _: bool) -> Result { + Ok(DuplicatePreservingValue::Bool) + } + + fn visit_i64(self, _: i64) -> Result { + Ok(DuplicatePreservingValue::Number) + } + + fn visit_u64(self, _: u64) -> Result { + Ok(DuplicatePreservingValue::Number) + } + + fn visit_f64(self, _: f64) -> Result { + Ok(DuplicatePreservingValue::Number) + } + + fn visit_str(self, _: &str) -> Result { + Ok(DuplicatePreservingValue::String) + } + + fn visit_string(self, _: String) -> Result { + Ok(DuplicatePreservingValue::String) + } + + fn visit_seq(self, mut sequence: A) -> Result + where + A: SeqAccess<'de>, + { + while sequence + .next_element::()? + .is_some() + {} + Ok(DuplicatePreservingValue::Array) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut fields = Vec::new(); + while let Some(key) = map.next_key()? { + fields.push((key, map.next_value()?)); + } + Ok(DuplicatePreservingValue::Object(fields)) + } + } + + deserializer.deserialize_any(DuplicatePreservingVisitor) + } +} + +fn reject_duplicate_entry_ids(json: &str) -> Result<(), PaygRemapParseError> { + let root: DuplicatePreservingValue = + serde_json::from_str(json).map_err(|error| PaygRemapParseError::Json(error.to_string()))?; + let DuplicatePreservingValue::Object(root) = root else { + return Ok(()); + }; + let Some((_, DuplicatePreservingValue::Object(entries))) = + root.into_iter().find(|(field, _)| field == "entries") + else { + return Ok(()); + }; + + let mut ids = BTreeSet::new(); + for (id, _) in entries { + if !ids.insert(id.clone()) { + return Err(PaygRemapParseError::DuplicateEntry { id }); + } + } + Ok(()) +} + fn required_provenance( entry: &serde_json::Map, id: &str, diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index 78b821a..9df9411 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -178,6 +178,58 @@ "id": "p/m", "field": "target" } + }, + { + "name": "duplicate-entry-id", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"first\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"},\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"second\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "DuplicateEntry", + "id": "p/m" + } + }, + { + "name": "duplicate-provider-id-prefix", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p1\":{\"kind\":\"zeros_are_not_prices\",\"id_prefix\":\"shared-\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"},\"p2\":{\"kind\":\"zeros_are_not_prices\",\"id_prefix\":\"shared-\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}},\"entries\":{}}", + "expect_error": { + "variant": "DuplicateIdPrefix", + "id_prefix": "shared-" + } + }, + { + "name": "resolves-to-unknown-field", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"resolves_to\",\"target\":\"q/m\",\"because\":\"same schedule\",\"sources\":\"typo\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p/m", + "field": "sources" + } + }, + { + "name": "overrides-unpriced-unknown-field", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"overrides_unpriced\",\"cost\":{\"input\":1},\"effective_form\":\"typo\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p/m", + "field": "effective_form" + } + }, + { + "name": "not-sold-per-token-unknown-field", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan-only\",\"effective_form\":\"typo\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p/m", + "field": "effective_form" + } + }, + { + "name": "provider-rule-unknown-field", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{\"p\":{\"kind\":\"zeros_are_not_prices\",\"sources\":\"typo\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}},\"entries\":{}}", + "expect_error": { + "variant": "UnexpectedField", + "id": "p", + "field": "sources" + } } ], "positive_vectors": [ @@ -186,6 +238,8 @@ "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan_only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-13\",\"effective_from\":null}}}", "id": "p/m", "entry_kind": "not_sold_per_token", + "source": "https://vendor.example/pricing", + "observed": "2026-08-13", "effective_from": null }, { diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index c9892d6..9f719f6 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -35,8 +35,8 @@ struct PositiveVector { entry_kind: String, provider: Option, provider_kind: Option, - source: Option, - observed: Option, + source: String, + observed: String, effective_from: Option, } @@ -50,6 +50,8 @@ struct RawPositiveVectorFile { struct RawPositiveVector { name: String, entry_kind: Option, + source: Option, + observed: Option, } #[derive(Debug, Deserialize)] @@ -108,6 +110,12 @@ enum ExpectedError { id: String, field: String, }, + DuplicateEntry { + id: String, + }, + DuplicateIdPrefix { + id_prefix: String, + }, } #[test] @@ -115,7 +123,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.vectors.len(), - 28, + 34, "one vector for each non-structural parse guard" ); @@ -143,12 +151,8 @@ fn parse_gate_accepts_every_positive_golden_vector() { let id = PaygModelId::parse(&vector.id).expect("golden vector must use a valid id"); match (vector.entry_kind.as_str(), &doc.entries[&id]) { ("not_sold_per_token", PaygRemapEntry::NotSoldPerToken(entry)) => { - if let Some(source) = vector.source.as_deref() { - assert_eq!(entry.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(entry.observed, observed, "{}: observed", vector.name); - } + assert_eq!(entry.source, vector.source, "{}: source", vector.name); + assert_eq!(entry.observed, vector.observed, "{}: observed", vector.name); assert_eq!( entry.effective_from, vector.effective_from, "{}: effective_from", @@ -156,12 +160,8 @@ fn parse_gate_accepts_every_positive_golden_vector() { ); } ("rate_time_banded", PaygRemapEntry::RateTimeBanded(entry)) => { - if let Some(source) = vector.source.as_deref() { - assert_eq!(entry.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(entry.observed, observed, "{}: observed", vector.name); - } + assert_eq!(entry.source, vector.source, "{}: source", vector.name); + assert_eq!(entry.observed, vector.observed, "{}: observed", vector.name); assert_eq!( entry.effective_from, vector.effective_from, "{}: effective_from", @@ -181,12 +181,8 @@ fn parse_gate_accepts_every_positive_golden_vector() { )), kind => panic!("{} has unknown provider kind {kind}", vector.name), } - if let Some(source) = vector.source.as_deref() { - assert_eq!(rule.source, source, "{}: source", vector.name); - } - if let Some(observed) = vector.observed.as_deref() { - assert_eq!(rule.observed, observed, "{}: observed", vector.name); - } + assert_eq!(rule.source, vector.source, "{}: source", vector.name); + assert_eq!(rule.observed, vector.observed, "{}: observed", vector.name); assert_eq!( rule.effective_from, vector.effective_from, "{}: effective_from", @@ -203,7 +199,7 @@ fn parse_gate_accepts_every_positive_golden_vector() { } #[test] -fn positive_vectors_must_declare_entry_kind() { +fn positive_vectors_must_declare_expected_fields() { let file: RawPositiveVectorFile = serde_json::from_str(VECTORS).expect("parse raw PAYG parse vectors"); @@ -213,6 +209,16 @@ fn positive_vectors_must_declare_entry_kind() { "{} must declare entry_kind", vector.name ); + assert!( + vector.source.is_some(), + "{} must declare source", + vector.name + ); + assert!( + vector.observed.is_some(), + "{} must declare observed", + vector.name + ); } } @@ -457,6 +463,16 @@ fn assert_expected_error(name: &str, error: PaygRemapParseError, expected: Expec assert_eq!(actual_id, id, "{name}: UnexpectedField.id"); assert_eq!(actual_field, field, "{name}: UnexpectedField.field"); } + ( + PaygRemapParseError::DuplicateEntry { id: actual }, + ExpectedError::DuplicateEntry { id: expected }, + ) => assert_eq!(actual, expected, "{name}: DuplicateEntry.id"), + ( + PaygRemapParseError::DuplicateIdPrefix { id_prefix: actual }, + ExpectedError::DuplicateIdPrefix { + id_prefix: expected, + }, + ) => assert_eq!(actual, expected, "{name}: DuplicateIdPrefix.id_prefix"), (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"), } } From 4e5bbe3c0c9780eeb388b9f95744ececb6705315 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:10:21 +0200 Subject: [PATCH 7/7] fix(model-catalog): refuse duplicate keys at every level, not just entry ids --- .../cortexkit-model-catalog/src/payg_remap.rs | 65 +++++++++++++------ .../tests/golden/payg-parse-vectors.json | 16 +++++ .../tests/payg_parse_vectors.rs | 36 +++++++++- 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/crates/cortexkit-model-catalog/src/payg_remap.rs b/crates/cortexkit-model-catalog/src/payg_remap.rs index 5826e7f..d79e75f 100644 --- a/crates/cortexkit-model-catalog/src/payg_remap.rs +++ b/crates/cortexkit-model-catalog/src/payg_remap.rs @@ -85,7 +85,7 @@ impl PaygRemapDoc { }); } - reject_duplicate_entry_ids(json)?; + reject_duplicate_keys(json)?; let providers = parse_provider_rules( root.get("providers") @@ -244,6 +244,9 @@ pub enum PaygRemapParseError { DuplicateIdPrefix { id_prefix: String, }, + DuplicateKey { + key: String, + }, } impl std::fmt::Display for PaygRemapParseError { @@ -300,6 +303,7 @@ impl std::fmt::Display for PaygRemapParseError { Self::DuplicateIdPrefix { id_prefix } => { write!(f, "PAYG remap has duplicate provider id_prefix {id_prefix}") } + Self::DuplicateKey { key } => write!(f, "PAYG remap has duplicate key {key}"), } } } @@ -475,7 +479,7 @@ enum DuplicatePreservingValue { Bool, Number, String, - Array, + Array(Vec), Object(Vec<(String, Self)>), } @@ -525,11 +529,11 @@ impl<'de> Deserialize<'de> for DuplicatePreservingValue { where A: SeqAccess<'de>, { - while sequence - .next_element::()? - .is_some() - {} - Ok(DuplicatePreservingValue::Array) + let mut values = Vec::new(); + while let Some(value) = sequence.next_element()? { + values.push(value); + } + Ok(DuplicatePreservingValue::Array(values)) } fn visit_map(self, mut map: A) -> Result @@ -548,25 +552,44 @@ impl<'de> Deserialize<'de> for DuplicatePreservingValue { } } -fn reject_duplicate_entry_ids(json: &str) -> Result<(), PaygRemapParseError> { +fn reject_duplicate_keys(json: &str) -> Result<(), PaygRemapParseError> { let root: DuplicatePreservingValue = serde_json::from_str(json).map_err(|error| PaygRemapParseError::Json(error.to_string()))?; - let DuplicatePreservingValue::Object(root) = root else { - return Ok(()); - }; - let Some((_, DuplicatePreservingValue::Object(entries))) = - root.into_iter().find(|(field, _)| field == "entries") - else { - return Ok(()); - }; - let mut ids = BTreeSet::new(); - for (id, _) in entries { - if !ids.insert(id.clone()) { - return Err(PaygRemapParseError::DuplicateEntry { id }); + fn visit(value: DuplicatePreservingValue, path: &str) -> Result<(), PaygRemapParseError> { + match value { + DuplicatePreservingValue::Array(values) => { + for (index, value) in values.into_iter().enumerate() { + visit(value, &format!("{path}[{index}]"))?; + } + } + DuplicatePreservingValue::Object(fields) => { + let mut keys = BTreeSet::new(); + for (key, value) in fields { + if !keys.insert(key.clone()) { + return Err(if path == "entries" { + PaygRemapParseError::DuplicateEntry { id: key } + } else { + PaygRemapParseError::DuplicateKey { key } + }); + } + let child_path = if path.is_empty() { + key.clone() + } else { + format!("{path}.{key}") + }; + visit(value, &child_path)?; + } + } + DuplicatePreservingValue::Null + | DuplicatePreservingValue::Bool + | DuplicatePreservingValue::Number + | DuplicatePreservingValue::String => {} } + Ok(()) } - Ok(()) + + visit(root, "") } fn required_provenance( diff --git a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json index 9df9411..3f7c9a8 100644 --- a/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json +++ b/crates/cortexkit-model-catalog/tests/golden/payg-parse-vectors.json @@ -230,6 +230,22 @@ "id": "p", "field": "sources" } + }, + { + "name": "duplicate-top-level-entries-key", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan-only\",\"source\":\"https://vendor.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "DuplicateKey", + "key": "entries" + } + }, + { + "name": "duplicate-nested-entry-source-key", + "input_json": "{\"schema\":1,\"counterfactual\":\"same_platform_list\",\"providers\":{},\"entries\":{\"p/m\":{\"kind\":\"not_sold_per_token\",\"reason\":\"plan-only\",\"source\":\"https://first.example/pricing\",\"source\":\"https://second.example/pricing\",\"observed\":\"2026-08-16\"}}}", + "expect_error": { + "variant": "DuplicateKey", + "key": "source" + } } ], "positive_vectors": [ diff --git a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs index 9f719f6..6715c44 100644 --- a/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs +++ b/crates/cortexkit-model-catalog/tests/payg_parse_vectors.rs @@ -116,6 +116,9 @@ enum ExpectedError { DuplicateIdPrefix { id_prefix: String, }, + DuplicateKey { + key: String, + }, } #[test] @@ -123,7 +126,7 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); assert_eq!( file.vectors.len(), - 34, + 36, "one vector for each non-structural parse guard" ); @@ -136,6 +139,31 @@ fn parse_gate_rejects_every_golden_vector_with_its_exact_error() { } } +#[test] +fn duplicate_key_vectors_refuse_at_each_object_depth() { + let file: VectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); + + let accepted = [ + "duplicate-top-level-entries-key", + "duplicate-nested-entry-source-key", + ] + .into_iter() + .filter(|name| { + let vector = file + .vectors + .iter() + .find(|vector| vector.name == *name) + .unwrap_or_else(|| panic!("missing {name} vector")); + PaygRemapDoc::parse(&vector.input_json).is_ok() + }) + .collect::>(); + + assert!( + accepted.is_empty(), + "duplicate vectors parsed: {accepted:?}" + ); +} + #[test] fn parse_gate_accepts_every_positive_golden_vector() { let file: PositiveVectorFile = serde_json::from_str(VECTORS).expect("parse PAYG parse vectors"); @@ -200,6 +228,8 @@ fn parse_gate_accepts_every_positive_golden_vector() { #[test] fn positive_vectors_must_declare_expected_fields() { + // Serde's required-field error reports only a generated-fixture line number; this companion + // guard identifies the malformed vector by name when the positive corpus grows. let file: RawPositiveVectorFile = serde_json::from_str(VECTORS).expect("parse raw PAYG parse vectors"); @@ -473,6 +503,10 @@ fn assert_expected_error(name: &str, error: PaygRemapParseError, expected: Expec id_prefix: expected, }, ) => assert_eq!(actual, expected, "{name}: DuplicateIdPrefix.id_prefix"), + ( + PaygRemapParseError::DuplicateKey { key: actual }, + ExpectedError::DuplicateKey { key: expected }, + ) => assert_eq!(actual, expected, "{name}: DuplicateKey.key"), (actual, expected) => panic!("{name}: expected {expected:?}, got {actual:?}"), } }