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