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] 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" + )); +}