use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use crate::ModelSchema;
pub use car_proto::canonical_json;
fn compare_catalog_ids(left: &str, right: &str) -> Ordering {
left.encode_utf16().cmp(right.encode_utf16())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatalogModelRow {
pub model: ModelSchema,
pub row_digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatalogSnapshot {
pub catalog_revision: String,
pub models: Vec<CatalogModelRow>,
}
impl CatalogSnapshot {
pub fn new<I>(models: I) -> Result<Self, String>
where
I: IntoIterator<Item = ModelSchema>,
{
let mut models: Vec<ModelSchema> = models.into_iter().collect();
models.sort_by(|left, right| compare_catalog_ids(&left.id, &right.id));
if let Some(duplicate) = models.windows(2).find(|pair| pair[0].id == pair[1].id) {
return Err(format!(
"duplicate immutable model id in catalog snapshot: {}",
duplicate[0].id
));
}
let models = models
.into_iter()
.map(|model| {
let row_digest = row_digest(&model)?;
Ok(CatalogModelRow { model, row_digest })
})
.collect::<Result<Vec<_>, String>>()?;
let catalog_revision = catalog_revision(&models)?;
Ok(Self {
catalog_revision,
models,
})
}
pub fn model_by_exact_id(&self, model_id: &str) -> Option<&CatalogModelRow> {
self.models
.binary_search_by(|row| compare_catalog_ids(&row.model.id, model_id))
.ok()
.map(|index| &self.models[index])
}
pub fn validate(&self) -> Result<(), String> {
if self
.models
.windows(2)
.any(|pair| compare_catalog_ids(&pair[0].model.id, &pair[1].model.id) != Ordering::Less)
{
return Err("catalog snapshot rows are not strictly sorted by immutable id".into());
}
for row in &self.models {
let actual = row_digest(&row.model)?;
if actual != row.row_digest {
return Err(format!(
"catalog row digest mismatch for {}: expected {}, got {}",
row.model.id, row.row_digest, actual
));
}
}
let actual = catalog_revision(&self.models)?;
if actual != self.catalog_revision {
return Err(format!(
"catalog revision mismatch: expected {}, got {}",
self.catalog_revision, actual
));
}
Ok(())
}
}
pub fn row_digest(model: &ModelSchema) -> Result<String, String> {
if model.id.trim().is_empty() {
return Err("model id must not be blank".into());
}
car_proto::canonical_sha256(model)
.map_err(|error| format!("serialize model {} for identity: {error}", model.id))
}
fn catalog_revision(rows: &[CatalogModelRow]) -> Result<String, String> {
car_proto::canonical_sha256(rows)
.map_err(|error| format!("serialize catalog rows for revision: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{
ApiProtocol, ModelCapability, ModelSchema, ModelSource, ProprietaryAuth,
ProprietaryProtocol, TrustTier,
};
use serde_json::Value;
#[derive(Deserialize)]
struct JcsFixture {
algorithm: String,
canonical_vectors: Vec<CanonicalVector>,
unsafe_integer_inputs: Vec<String>,
production_rows: Vec<ProductionRowVector>,
unicode_catalog_order: UnicodeCatalogOrderVector,
catalog_revision: String,
}
#[derive(Deserialize)]
struct CanonicalVector {
name: String,
input_json: String,
canonical_json: String,
sha256: String,
}
#[derive(Deserialize)]
struct ProductionRowVector {
model: Value,
row_digest: String,
}
#[derive(Deserialize)]
struct UnicodeCatalogOrderVector {
input_ids: Vec<String>,
expected_rows: Vec<UnicodeCatalogRowVector>,
catalog_revision: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct UnicodeCatalogRowVector {
model_id: String,
row_digest: String,
}
fn jcs_fixture() -> JcsFixture {
serde_json::from_str(include_str!(
"../tests/fixtures/catalog_identity_jcs_vectors.json"
))
.unwrap()
}
fn schema(id: &str, name: &str) -> ModelSchema {
ModelSchema {
id: id.into(),
name: name.into(),
provider: "test".into(),
family: "fixture".into(),
version: "immutable-v1".into(),
capabilities: vec![ModelCapability::Generate],
context_length: 32_768,
max_output_tokens: Some(4_096),
param_count: String::new(),
quantization: None,
performance: Default::default(),
cost: Default::default(),
source: ModelSource::RemoteApi {
endpoint: "https://example.test/v1".into(),
api_key_env: "TEST_API_KEY".into(),
api_key_envs: vec![],
api_version: None,
protocol: ApiProtocol::OpenAiCompat,
},
tags: vec!["fixture".into()],
supported_params: vec![],
public_benchmarks: vec![],
trust_tier: TrustTier::Curated,
deprecated: false,
available: false,
weights_ready: false,
}
}
#[test]
fn catalog_snapshot_uses_canonical_oauth2_pkce_and_accepts_legacy_input() {
let mut model = schema("parslee/test", "Parslee Test");
model.source = ModelSource::Proprietary {
provider: "parslee".into(),
endpoint: "https://api.example.test".into(),
auth: ProprietaryAuth::OAuth2Pkce {
authority: "https://auth.example.test".into(),
client_id: "car-test".into(),
scopes: vec!["inference:invoke".into()],
},
protocol: ProprietaryProtocol::default(),
};
let snapshot = CatalogSnapshot::new([model]).expect("build catalog snapshot");
let mut wire = serde_json::to_value(&snapshot).expect("serialize catalog snapshot");
assert_eq!(
wire["models"][0]["model"]["source"]["auth"]["type"],
"oauth2_pkce"
);
wire["models"][0]["model"]["source"]["auth"]["type"] = Value::String("o_auth2_pkce".into());
let legacy: CatalogSnapshot =
serde_json::from_value(wire).expect("accept legacy OAuth2 PKCE spelling");
legacy
.validate()
.expect("legacy input reserializes with the canonical row identity");
}
#[test]
fn canonical_json_sorts_object_keys_recursively() {
let left = serde_json::json!({
"z": {"b": 2, "a": 1},
"a": [{"y": true, "x": false}],
});
let right = serde_json::json!({
"a": [{"x": false, "y": true}],
"z": {"a": 1, "b": 2},
});
assert_eq!(
canonical_json(&left).unwrap(),
canonical_json(&right).unwrap()
);
}
#[test]
fn canonical_json_normalizes_ecmascript_numbers() {
assert_eq!(canonical_json(&serde_json::json!(5.0)).unwrap(), "5");
assert_eq!(canonical_json(&serde_json::json!(-0.0)).unwrap(), "0");
assert_eq!(canonical_json(&serde_json::json!(1e30)).unwrap(), "1e+30");
}
#[test]
fn canonical_json_rejects_non_finite_numbers() {
#[derive(Serialize)]
struct NonFinite {
value: f64,
}
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let error = canonical_json(&NonFinite { value }).unwrap_err();
assert!(error.contains("invalid float value"), "{error}");
}
}
#[test]
fn canonical_json_rejects_integers_outside_the_interoperable_safe_range() {
let fixture = jcs_fixture();
for input in fixture.unsafe_integer_inputs {
let value: Value = serde_json::from_str(&input).unwrap();
let error = canonical_json(&value).unwrap_err();
assert!(
error.contains("outside the interoperable safe integer range"),
"{input}: {error}"
);
}
}
#[test]
fn canonical_sha256_matches_independent_fixed_vectors() {
let fixture = jcs_fixture();
assert_eq!(fixture.algorithm, "RFC 8785");
for vector in fixture.canonical_vectors {
let value: Value = serde_json::from_str(&vector.input_json).unwrap();
let canonical = canonical_json(&value).unwrap();
assert_eq!(
canonical, vector.canonical_json,
"canonical mismatch for {}",
vector.name
);
assert_eq!(
car_proto::canonical_sha256(&value).unwrap(),
vector.sha256,
"digest mismatch for {}",
vector.name
);
}
}
#[test]
fn production_rows_and_revision_match_shared_jcs_fixture() {
let fixture = jcs_fixture();
let builtins: Vec<ModelSchema> =
serde_json::from_str(include_str!("builtin_catalog.json")).unwrap();
let mut selected = Vec::new();
for vector in fixture.production_rows {
let fixture_model: ModelSchema = serde_json::from_value(vector.model.clone()).unwrap();
let builtin = builtins
.iter()
.find(|model| model.id == fixture_model.id)
.unwrap();
assert_eq!(
canonical_json(builtin).unwrap(),
canonical_json(&vector.model).unwrap(),
"complete production row drifted for {}",
builtin.id
);
assert_eq!(row_digest(builtin).unwrap(), vector.row_digest);
selected.push(builtin.clone());
}
let snapshot = CatalogSnapshot::new(selected).unwrap();
assert_eq!(snapshot.catalog_revision, fixture.catalog_revision);
}
#[test]
fn unicode_catalog_order_and_revision_match_shared_jcs_fixture() {
let fixture = jcs_fixture().unicode_catalog_order;
let snapshot = CatalogSnapshot::new(
fixture
.input_ids
.iter()
.map(|id| schema(id, id))
.collect::<Vec<_>>(),
)
.unwrap();
let actual_rows = snapshot
.models
.iter()
.map(|row| UnicodeCatalogRowVector {
model_id: row.model.id.clone(),
row_digest: row.row_digest.clone(),
})
.collect::<Vec<_>>();
assert_eq!(
snapshot.catalog_revision,
fixture.catalog_revision,
"catalog row order: {:?}",
actual_rows
.iter()
.map(|row| row.model_id.as_str())
.collect::<Vec<_>>()
);
assert_eq!(actual_rows, fixture.expected_rows);
assert!(snapshot.validate().is_ok());
for row in &snapshot.models {
assert_eq!(
snapshot
.model_by_exact_id(&row.model.id)
.map(|found| found.row_digest.as_str()),
Some(row.row_digest.as_str())
);
}
let mut utf8_ordered = snapshot.clone();
utf8_ordered.models.swap(0, 1);
assert!(utf8_ordered.validate().is_err());
}
#[test]
fn row_digest_excludes_dynamic_availability() {
let mut model = schema("test/model-a:1", "Model A");
let before = row_digest(&model).unwrap();
model.available = true;
model.weights_ready = true;
assert_eq!(before, row_digest(&model).unwrap());
}
#[test]
fn row_digest_detects_immutable_row_tamper() {
let mut model = schema("test/model-a:1", "Model A");
let before = row_digest(&model).unwrap();
model.context_length += 1;
assert_ne!(before, row_digest(&model).unwrap());
}
#[test]
fn row_digest_rejects_non_finite_schema_numbers() {
let mut model = schema("test/model-a:1", "Model A");
model.cost.input_per_mtok = Some(f64::NAN);
let error = row_digest(&model).unwrap_err();
assert!(error.contains("invalid float value"), "{error}");
}
#[test]
fn catalog_revision_is_independent_of_input_order() {
let a = schema("test/model-a:1", "Model A");
let b = schema("test/model-b:1", "Model B");
let forward = CatalogSnapshot::new([a.clone(), b.clone()]).unwrap();
let reverse = CatalogSnapshot::new([b, a]).unwrap();
assert_eq!(forward.catalog_revision, reverse.catalog_revision);
assert_eq!(
serde_json::to_value(&forward.models).unwrap(),
serde_json::to_value(&reverse.models).unwrap()
);
}
#[test]
fn catalog_snapshot_new_rejects_duplicate_id_with_exact_error() {
let error = CatalogSnapshot::new([
schema("test/model-a:1", "Model A"),
schema("test/model-a:1", "Model A Duplicate"),
])
.unwrap_err();
assert_eq!(
error,
"duplicate immutable model id in catalog snapshot: test/model-a:1"
);
}
#[test]
fn catalog_identity_rejects_blank_model_ids() {
for id in ["", " ", "\t\n"] {
let model = schema(id, "Blank Model Id");
let digest_error = row_digest(&model).unwrap_err();
assert_eq!(digest_error, "model id must not be blank");
let snapshot_error = CatalogSnapshot::new([model]).unwrap_err();
assert_eq!(snapshot_error, "model id must not be blank");
}
}
#[test]
fn catalog_snapshot_validation_detects_row_digest_tamper() {
let mut snapshot = CatalogSnapshot::new([
schema("test/model-a:1", "Model A"),
schema("test/model-b:1", "Model B"),
])
.unwrap();
snapshot.models[0].row_digest = "0".repeat(64);
assert!(snapshot.validate().is_err());
}
#[test]
fn catalog_snapshot_validation_detects_revision_tamper() {
let mut snapshot = CatalogSnapshot::new([schema("test/model-a:1", "Model A")]).unwrap();
snapshot.catalog_revision = "f".repeat(64);
assert!(snapshot.validate().is_err());
}
#[test]
fn decoded_snapshot_rejects_duplicate_rows() {
let snapshot = CatalogSnapshot::new([
schema("test/model-a:1", "Model A"),
schema("test/model-b:1", "Model B"),
])
.unwrap();
let mut decoded: CatalogSnapshot =
serde_json::from_value(serde_json::to_value(snapshot).unwrap()).unwrap();
decoded.models[1] = decoded.models[0].clone();
assert!(decoded.validate().is_err());
}
#[test]
fn decoded_snapshot_rejects_out_of_order_rows() {
let snapshot = CatalogSnapshot::new([
schema("test/model-a:1", "Model A"),
schema("test/model-b:1", "Model B"),
])
.unwrap();
let mut decoded: CatalogSnapshot =
serde_json::from_value(serde_json::to_value(snapshot).unwrap()).unwrap();
decoded.models.swap(0, 1);
assert!(decoded.validate().is_err());
}
#[test]
fn exact_id_lookup_never_falls_back_to_display_name() {
let snapshot = CatalogSnapshot::new([
schema("test/model-a:1", "Shared Name"),
schema("test/model-b:1", "Shared Name"),
])
.unwrap();
assert!(snapshot.model_by_exact_id("Shared Name").is_none());
assert_eq!(
snapshot
.model_by_exact_id("test/model-a:1")
.unwrap()
.model
.id,
"test/model-a:1"
);
assert_eq!(
snapshot
.model_by_exact_id("test/model-b:1")
.unwrap()
.model
.id,
"test/model-b:1"
);
}
}