use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use super::{BackendFailure, BackendKind, Capabilities, FailureCategory};
use crate::desired_state::{
BlobKind, BlobRef, Canonical, CanonicalError, CanonicalValue, Checksum,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CatalogBackend {
#[default]
ModelsDev,
}
impl CatalogBackend {
pub const fn kind(self) -> BackendKind {
match self {
Self::ModelsDev => BackendKind::ModelsDev,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SchemaVersion(&'static str);
impl SchemaVersion {
pub const MODELS_DEV_CATALOG_V1: Self = Self("models.dev/catalog.json/v1");
pub const fn as_str(self) -> &'static str {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ETag(pub String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HttpDate(pub String);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceValidators {
pub etag: Option<ETag>,
pub last_modified: Option<HttpDate>,
}
impl SourceValidators {
pub fn etag(etag: impl Into<String>) -> Self {
Self {
etag: Some(ETag(etag.into())),
last_modified: None,
}
}
pub fn is_empty(&self) -> bool {
self.etag.is_none() && self.last_modified.is_none()
}
pub fn carry_over(&mut self, stated: Self) {
if let Some(etag) = stated.etag {
self.etag = Some(etag);
}
if let Some(last_modified) = stated.last_modified {
self.last_modified = Some(last_modified);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CatalogContentId(Checksum);
impl CatalogContentId {
pub const fn from_checksum(checksum: Checksum) -> Self {
Self(checksum)
}
pub const fn checksum(self) -> Checksum {
self.0
}
pub fn short(self) -> String {
self.0
.as_bytes()
.iter()
.take(CONTENT_ID_SHORT_HEX / 2)
.map(|byte| format!("{byte:02x}"))
.collect()
}
}
pub const CONTENT_ID_SHORT_HEX: usize = 16;
impl std::fmt::Display for CatalogContentId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSnapshot {
pub source_url: String,
pub schema_version: SchemaVersion,
pub validators: SourceValidators,
pub fetched_at: SystemTime,
pub raw: BlobRef,
pub content_id: CatalogContentId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogSnapshot {
pub source: SourceSnapshot,
pub content: CatalogContent,
}
#[derive(Clone, PartialEq, Eq)]
pub struct RawPayload(Arc<[u8]>);
impl RawPayload {
pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
Self(bytes.into())
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Debug for RawPayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawPayload")
.field("bytes", &self.0.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogRefresh {
Unchanged {
validators: SourceValidators,
},
Updated {
snapshot: Box<CatalogSnapshot>,
payload: RawPayload,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RefusalReason {
Unreachable,
Denied,
Oversized,
UnsupportedEndpoint,
NotJson,
Schema,
IdMismatch,
Identifier,
UnknownStatus,
UnknownModality,
Price,
UnknownTierType,
DuplicateTier,
NeutralPrice,
UncanonicalizableText,
AmbiguousModelKey,
Content,
NotRetained,
UnsolicitedUnchanged,
Unknown,
}
pub const REFUSAL_REASONS: &[&str] = &[
"unreachable",
"denied",
"oversized",
"unsupported_endpoint",
"not_json",
"schema",
"id_mismatch",
"identifier",
"unknown_status",
"unknown_modality",
"price",
"unknown_tier_type",
"duplicate_tier",
"neutral_price",
"uncanonicalizable_text",
"ambiguous_model_key",
"content",
"not_retained",
"unsolicited_unchanged",
"unknown",
];
impl RefusalReason {
pub const ALL: &'static [Self] = &[
Self::Unreachable,
Self::Denied,
Self::Oversized,
Self::UnsupportedEndpoint,
Self::NotJson,
Self::Schema,
Self::IdMismatch,
Self::Identifier,
Self::UnknownStatus,
Self::UnknownModality,
Self::Price,
Self::UnknownTierType,
Self::DuplicateTier,
Self::NeutralPrice,
Self::UncanonicalizableText,
Self::AmbiguousModelKey,
Self::Content,
Self::NotRetained,
Self::UnsolicitedUnchanged,
Self::Unknown,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Unreachable => "unreachable",
Self::Denied => "denied",
Self::Oversized => "oversized",
Self::UnsupportedEndpoint => "unsupported_endpoint",
Self::NotJson => "not_json",
Self::Schema => "schema",
Self::IdMismatch => "id_mismatch",
Self::Identifier => "identifier",
Self::UnknownStatus => "unknown_status",
Self::UnknownModality => "unknown_modality",
Self::Price => "price",
Self::UnknownTierType => "unknown_tier_type",
Self::DuplicateTier => "duplicate_tier",
Self::NeutralPrice => "neutral_price",
Self::UncanonicalizableText => "uncanonicalizable_text",
Self::AmbiguousModelKey => "ambiguous_model_key",
Self::Content => "content",
Self::NotRetained => "not_retained",
Self::UnsolicitedUnchanged => "unsolicited_unchanged",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
reason: RefusalReason,
pointer: Option<JsonPointer>,
}
impl Refusal {
pub const fn new(reason: RefusalReason) -> Self {
Self {
reason,
pointer: None,
}
}
pub fn at(reason: RefusalReason, pointer: JsonPointer) -> Self {
Self {
reason,
pointer: Some(pointer),
}
}
pub const fn reason(&self) -> RefusalReason {
self.reason
}
pub const fn pointer(&self) -> Option<&JsonPointer> {
self.pointer.as_ref()
}
}
pub trait Refusable {
fn refusal(&self) -> Refusal;
}
impl Refusable for CatalogError {
fn refusal(&self) -> Refusal {
match self {
Self::Unavailable { refusal, .. }
| Self::Invalid { refusal, .. }
| Self::Denied { refusal, .. }
| Self::Misconfigured { refusal, .. } => refusal.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CatalogError {
#[error("catalogue source `{backend}` unavailable: {message}")]
Unavailable {
backend: &'static str,
refusal: Refusal,
message: String,
},
#[error("catalogue source `{backend}` returned unusable metadata: {message}")]
Invalid {
backend: &'static str,
refusal: Refusal,
message: String,
},
#[error("catalogue source `{backend}` refused the request: {message}")]
Denied {
backend: &'static str,
refusal: Refusal,
message: String,
},
#[error("catalogue source `{backend}` cannot serve a catalogue: {message}")]
Misconfigured {
backend: &'static str,
refusal: Refusal,
message: String,
},
}
impl CatalogError {
pub const fn refused_by(&self) -> &Refusal {
match self {
Self::Unavailable { refusal, .. }
| Self::Invalid { refusal, .. }
| Self::Denied { refusal, .. }
| Self::Misconfigured { refusal, .. } => refusal,
}
}
pub const fn unavailable(backend: &'static str, message: String) -> Self {
Self::Unavailable {
backend,
refusal: Refusal::new(RefusalReason::Unreachable),
message,
}
}
}
impl BackendFailure for CatalogError {
fn category(&self) -> FailureCategory {
match self {
Self::Unavailable { .. } => FailureCategory::Unavailable,
Self::Invalid { .. } => FailureCategory::Invalid,
Self::Denied { .. } => FailureCategory::Denied,
Self::Misconfigured { .. } => FailureCategory::NotFound,
}
}
}
#[async_trait]
pub trait CatalogSource: Send + Sync {
fn name(&self) -> &'static str;
fn capabilities(&self) -> Capabilities;
async fn refresh(
&self,
since: Option<&SourceValidators>,
) -> Result<CatalogRefresh, CatalogError>;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CatalogId(String);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InvalidCatalogId {
#[error("a catalogue identifier may not be empty")]
Empty,
#[error("catalogue identifier `{value}` is longer than {max} bytes")]
TooLong { value: String, max: usize },
#[error(
"catalogue identifier `{value}` contains `{character}`; \
only ASCII alphanumerics and `-._:/+@~` are accepted"
)]
Character { value: String, character: char },
#[error("catalogue identifier `{value}` has an empty path segment")]
Segment { value: String },
}
impl CatalogId {
const MAX_BYTES: usize = 128;
pub fn parse(value: &str) -> Result<Self, InvalidCatalogId> {
if value.is_empty() {
return Err(InvalidCatalogId::Empty);
}
if value.len() > Self::MAX_BYTES {
return Err(InvalidCatalogId::TooLong {
value: value.to_owned(),
max: Self::MAX_BYTES,
});
}
for character in value.chars() {
let permitted = character.is_ascii_alphanumeric()
|| matches!(character, '-' | '.' | '_' | ':' | '/' | '+' | '@' | '~');
if !permitted {
return Err(InvalidCatalogId::Character {
value: value.to_owned(),
character,
});
}
}
if value.split('/').any(str::is_empty) {
return Err(InvalidCatalogId::Segment {
value: value.to_owned(),
});
}
Ok(Self(value.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CatalogId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Canonical for CatalogId {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::string(&self.0)
}
}
pub type ProviderId = CatalogId;
pub type ModelId = CatalogId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Modality {
Text,
Image,
Audio,
Video,
Pdf,
}
impl Modality {
pub const ALL: &'static [Self] =
&[Self::Text, Self::Image, Self::Audio, Self::Video, Self::Pdf];
pub const fn as_str(self) -> &'static str {
match self {
Self::Text => "text",
Self::Image => "image",
Self::Audio => "audio",
Self::Video => "video",
Self::Pdf => "pdf",
}
}
pub fn parse(value: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|modality| modality.as_str() == value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ModelCapability {
Attachment,
Reasoning,
ToolCall,
Temperature,
StructuredOutput,
Interleaved,
OpenWeights,
Experimental,
}
impl ModelCapability {
pub const ALL: &'static [Self] = &[
Self::Attachment,
Self::Reasoning,
Self::ToolCall,
Self::Temperature,
Self::StructuredOutput,
Self::Interleaved,
Self::OpenWeights,
Self::Experimental,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Attachment => "attachment",
Self::Reasoning => "reasoning",
Self::ToolCall => "tool-call",
Self::Temperature => "temperature",
Self::StructuredOutput => "structured-output",
Self::Interleaved => "interleaved",
Self::OpenWeights => "open-weights",
Self::Experimental => "experimental",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum ModelLifecycle {
#[default]
Available,
Alpha,
Beta,
Deprecated,
}
impl ModelLifecycle {
pub const ALL: &'static [Self] = &[Self::Available, Self::Alpha, Self::Beta, Self::Deprecated];
pub const fn as_str(self) -> &'static str {
match self {
Self::Available => "available",
Self::Alpha => "alpha",
Self::Beta => "beta",
Self::Deprecated => "deprecated",
}
}
pub const fn deprecated(self) -> bool {
matches!(self, Self::Deprecated)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ModelLimits {
pub context_tokens: Option<u64>,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
impl Canonical for ModelLimits {
fn canonical(&self) -> CanonicalValue {
let mut fields = Vec::new();
for (key, value) in [
("context_tokens", self.context_tokens),
("input_tokens", self.input_tokens),
("output_tokens", self.output_tokens),
] {
if let Some(value) = value {
fields.push((key.to_owned(), CanonicalValue::integer(value)));
}
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObservedRate(u64);
impl ObservedRate {
pub const ZERO: Self = Self(0);
pub const fn from_nanos(nanos: u64) -> Self {
Self(nanos)
}
pub const fn nanos(self) -> u64 {
self.0
}
}
impl Canonical for ObservedRate {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::integer(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PriceRates {
pub input: ObservedRate,
pub output: ObservedRate,
pub cache_read: Option<ObservedRate>,
pub cache_write: Option<ObservedRate>,
pub reasoning: Option<ObservedRate>,
pub input_audio: Option<ObservedRate>,
pub output_audio: Option<ObservedRate>,
}
impl PriceRates {
pub const fn new(input: ObservedRate, output: ObservedRate) -> Self {
Self {
input,
output,
cache_read: None,
cache_write: None,
reasoning: None,
input_audio: None,
output_audio: None,
}
}
}
impl Canonical for PriceRates {
fn canonical(&self) -> CanonicalValue {
let mut fields = vec![
("input".to_owned(), self.input.canonical()),
("output".to_owned(), self.output.canonical()),
];
for (key, rate) in [
("cache_read", self.cache_read),
("cache_write", self.cache_write),
("reasoning", self.reasoning),
("input_audio", self.input_audio),
("output_audio", self.output_audio),
] {
if let Some(rate) = rate {
fields.push((key.to_owned(), rate.canonical()));
}
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PriceTierThreshold {
ContextOver { tokens: u64 },
}
impl Canonical for PriceTierThreshold {
fn canonical(&self) -> CanonicalValue {
match self {
Self::ContextOver { tokens } => CanonicalValue::map([
("type", CanonicalValue::string("context-over")),
("tokens", CanonicalValue::integer(*tokens)),
]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PriceTier {
pub threshold: PriceTierThreshold,
pub rates: PriceRates,
}
impl Canonical for PriceTier {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::map([
("threshold", self.threshold.canonical()),
("rates", self.rates.canonical()),
])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObservedPrice {
pub base: PriceRates,
pub tiers: Vec<PriceTier>,
}
impl ObservedPrice {
pub fn new(base: PriceRates) -> Self {
Self {
base,
tiers: Vec::new(),
}
}
}
impl Canonical for ObservedPrice {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::map([
("base", self.base.canonical()),
(
"tiers",
CanonicalValue::List(self.tiers.iter().map(Canonical::canonical).collect()),
),
])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ModelField {
DisplayName,
Family,
Capabilities,
InputModalities,
OutputModalities,
ContextTokens,
InputTokens,
OutputTokens,
Lifecycle,
KnowledgeCutoff,
ReleaseDate,
LastUpdated,
Endpoint,
PublishedModelId,
}
impl ModelField {
pub const ALL: &'static [Self] = &[
Self::DisplayName,
Self::Family,
Self::Capabilities,
Self::InputModalities,
Self::OutputModalities,
Self::ContextTokens,
Self::InputTokens,
Self::OutputTokens,
Self::Lifecycle,
Self::KnowledgeCutoff,
Self::ReleaseDate,
Self::LastUpdated,
Self::Endpoint,
Self::PublishedModelId,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::DisplayName => "display_name",
Self::Family => "family",
Self::Capabilities => "capabilities",
Self::InputModalities => "input_modalities",
Self::OutputModalities => "output_modalities",
Self::ContextTokens => "context_tokens",
Self::InputTokens => "input_tokens",
Self::OutputTokens => "output_tokens",
Self::Lifecycle => "lifecycle",
Self::KnowledgeCutoff => "knowledge_cutoff",
Self::ReleaseDate => "release_date",
Self::LastUpdated => "last_updated",
Self::Endpoint => "endpoint",
Self::PublishedModelId => "published_model_id",
}
}
const fn lifecycle(self) -> bool {
matches!(self, Self::Lifecycle)
}
const fn capability(self) -> bool {
matches!(
self,
Self::Capabilities | Self::InputModalities | Self::OutputModalities
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JsonPointer(String);
impl JsonPointer {
pub fn new(pointer: impl Into<String>) -> Self {
Self(pointer.into())
}
pub fn child(&self, token: &str) -> Self {
let escaped = token.replace('~', "~0").replace('/', "~1");
Self(format!("{}/{escaped}", self.0))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for JsonPointer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Canonical for JsonPointer {
fn canonical(&self) -> CanonicalValue {
CanonicalValue::string(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ModelFacts {
pub display_name: Option<String>,
pub family: Option<String>,
pub capabilities: BTreeSet<ModelCapability>,
pub input_modalities: BTreeSet<Modality>,
pub output_modalities: BTreeSet<Modality>,
pub limits: ModelLimits,
pub lifecycle: ModelLifecycle,
pub knowledge_cutoff: Option<String>,
pub release_date: Option<String>,
pub last_updated: Option<String>,
}
impl ModelFacts {
pub fn differences(&self, other: &Self) -> Vec<ModelField> {
let mut fields = Vec::new();
let mut differs = |condition: bool, field: ModelField| {
if condition {
fields.push(field);
}
};
differs(
self.display_name != other.display_name,
ModelField::DisplayName,
);
differs(self.family != other.family, ModelField::Family);
differs(
self.capabilities != other.capabilities,
ModelField::Capabilities,
);
differs(
self.input_modalities != other.input_modalities,
ModelField::InputModalities,
);
differs(
self.output_modalities != other.output_modalities,
ModelField::OutputModalities,
);
differs(
self.limits.context_tokens != other.limits.context_tokens,
ModelField::ContextTokens,
);
differs(
self.limits.input_tokens != other.limits.input_tokens,
ModelField::InputTokens,
);
differs(
self.limits.output_tokens != other.limits.output_tokens,
ModelField::OutputTokens,
);
differs(self.lifecycle != other.lifecycle, ModelField::Lifecycle);
differs(
self.knowledge_cutoff != other.knowledge_cutoff,
ModelField::KnowledgeCutoff,
);
differs(
self.release_date != other.release_date,
ModelField::ReleaseDate,
);
differs(
self.last_updated != other.last_updated,
ModelField::LastUpdated,
);
fields
}
}
impl Canonical for ModelFacts {
fn canonical(&self) -> CanonicalValue {
let mut fields = vec![
(
"capabilities".to_owned(),
CanonicalValue::set(
self.capabilities
.iter()
.map(|capability| CanonicalValue::string(capability.as_str())),
),
),
(
"input_modalities".to_owned(),
CanonicalValue::set(
self.input_modalities
.iter()
.map(|modality| CanonicalValue::string(modality.as_str())),
),
),
(
"output_modalities".to_owned(),
CanonicalValue::set(
self.output_modalities
.iter()
.map(|modality| CanonicalValue::string(modality.as_str())),
),
),
("limits".to_owned(), self.limits.canonical()),
(
"lifecycle".to_owned(),
CanonicalValue::string(self.lifecycle.as_str()),
),
];
for (key, value) in [
("display_name", self.display_name.as_deref()),
("family", self.family.as_deref()),
("knowledge_cutoff", self.knowledge_cutoff.as_deref()),
("release_date", self.release_date.as_deref()),
("last_updated", self.last_updated.as_deref()),
] {
if let Some(value) = value {
fields.push((key.to_owned(), CanonicalValue::string(value)));
}
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProviderEndpoint {
pub api_base: Option<String>,
pub client_package: Option<String>,
pub wire_shape: Option<String>,
}
impl ProviderEndpoint {
pub fn is_empty(&self) -> bool {
self.api_base.is_none() && self.client_package.is_none() && self.wire_shape.is_none()
}
}
impl Canonical for ProviderEndpoint {
fn canonical(&self) -> CanonicalValue {
let mut fields = Vec::new();
for (key, value) in [
("api_base", self.api_base.as_deref()),
("client_package", self.client_package.as_deref()),
("wire_shape", self.wire_shape.as_deref()),
] {
if let Some(value) = value {
fields.push((key.to_owned(), CanonicalValue::string(value)));
}
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProviderField {
DisplayName,
DocUrl,
Endpoint,
EnvVars,
}
impl ProviderField {
pub const ALL: &'static [Self] = &[
Self::DisplayName,
Self::DocUrl,
Self::Endpoint,
Self::EnvVars,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::DisplayName => "display_name",
Self::DocUrl => "doc_url",
Self::Endpoint => "endpoint",
Self::EnvVars => "env_vars",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogProvider {
pub id: ProviderId,
pub display_name: Option<String>,
pub doc_url: Option<String>,
pub endpoint: ProviderEndpoint,
pub env_vars: Vec<String>,
pub pointer: JsonPointer,
}
impl CatalogProvider {
pub fn differences(&self, other: &Self) -> Vec<ProviderField> {
let mut fields = Vec::new();
for (differs, field) in [
(
self.display_name != other.display_name,
ProviderField::DisplayName,
),
(self.doc_url != other.doc_url, ProviderField::DocUrl),
(self.endpoint != other.endpoint, ProviderField::Endpoint),
(self.env_vars != other.env_vars, ProviderField::EnvVars),
] {
if differs {
fields.push(field);
}
}
fields
}
}
impl Canonical for CatalogProvider {
fn canonical(&self) -> CanonicalValue {
let mut fields = vec![
("id".to_owned(), self.id.canonical()),
("endpoint".to_owned(), self.endpoint.canonical()),
(
"env_vars".to_owned(),
CanonicalValue::List(
self.env_vars
.iter()
.map(CanonicalValue::string)
.collect::<Vec<_>>(),
),
),
];
for (key, value) in [
("display_name", self.display_name.as_deref()),
("doc_url", self.doc_url.as_deref()),
] {
if let Some(value) = value {
fields.push((key.to_owned(), CanonicalValue::string(value)));
}
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderOffering {
pub provider: ProviderId,
pub model: ModelId,
pub published_model_id: String,
pub facts: ModelFacts,
pub overrides: Vec<(ModelField, JsonPointer)>,
pub price: Option<ObservedPrice>,
pub endpoint: ProviderEndpoint,
pub pointer: JsonPointer,
}
impl ProviderOffering {
pub fn has_overrides(&self) -> bool {
!self.overrides.is_empty()
}
pub fn overrides_field(&self, field: ModelField) -> bool {
self.overrides.iter().any(|(name, _)| *name == field)
}
}
impl Canonical for ProviderOffering {
fn canonical(&self) -> CanonicalValue {
let mut fields = vec![
("provider".to_owned(), self.provider.canonical()),
("model".to_owned(), self.model.canonical()),
(
"published_model_id".to_owned(),
CanonicalValue::string(&self.published_model_id),
),
("facts".to_owned(), self.facts.canonical()),
];
if let Some(price) = &self.price {
fields.push(("price".to_owned(), price.canonical()));
}
if !self.endpoint.is_empty() {
fields.push(("endpoint".to_owned(), self.endpoint.canonical()));
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogModelEntry {
pub id: ModelId,
pub neutral: Option<ModelFacts>,
pub offerings: Vec<ProviderOffering>,
}
impl CatalogModelEntry {
pub fn offering(&self, provider: &ProviderId) -> Option<&ProviderOffering> {
self.offerings
.iter()
.find(|offering| &offering.provider == provider)
}
pub fn offerings_by(&self, provider: &ProviderId) -> impl Iterator<Item = &ProviderOffering> {
self.offerings
.iter()
.filter(move |offering| &offering.provider == provider)
}
pub fn offering_published_as(
&self,
provider: &ProviderId,
published: &str,
) -> Option<&ProviderOffering> {
self.offerings.iter().find(|offering| {
&offering.provider == provider && offering.published_model_id == published
})
}
}
impl Canonical for CatalogModelEntry {
fn canonical(&self) -> CanonicalValue {
let mut fields = vec![
("id".to_owned(), self.id.canonical()),
(
"offerings".to_owned(),
CanonicalValue::List(self.offerings.iter().map(Canonical::canonical).collect()),
),
];
if let Some(neutral) = &self.neutral {
fields.push(("neutral".to_owned(), neutral.canonical()));
}
CanonicalValue::map(fields)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CatalogContentError {
#[error("provider `{provider}` appears twice")]
DuplicateProvider { provider: ProviderId },
#[error("model `{model}` appears twice")]
DuplicateModel { model: ModelId },
#[error("model `{model}` lists `{provider}`'s `{published}` twice")]
DuplicateOffering {
model: ModelId,
provider: ProviderId,
published: String,
},
#[error("model `{model}` is offered by `{provider}`, which the payload does not describe")]
UnknownProvider {
model: ModelId,
provider: ProviderId,
},
#[error("offering `{provider}`/`{published}` is filed under model `{model}`")]
OfferingModelMismatch {
model: ModelId,
provider: ProviderId,
published: String,
},
#[error("the payload describes no models")]
Empty,
#[error("the catalogue has no canonical form: {source}")]
Uncanonicalizable {
#[source]
source: CanonicalError,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogContent {
providers: Vec<CatalogProvider>,
models: Vec<CatalogModelEntry>,
content_id: CatalogContentId,
}
impl CatalogContent {
pub fn new(
providers: Vec<CatalogProvider>,
models: Vec<CatalogModelEntry>,
) -> Result<Self, CatalogContentError> {
if models.is_empty() {
return Err(CatalogContentError::Empty);
}
let mut providers = providers;
providers.sort_by(|left, right| left.id.cmp(&right.id));
if let Some(duplicate) = first_duplicate(providers.iter().map(|provider| &provider.id)) {
return Err(CatalogContentError::DuplicateProvider {
provider: duplicate.clone(),
});
}
let known: BTreeSet<&ProviderId> = providers.iter().map(|provider| &provider.id).collect();
let mut models = models;
models.sort_by(|left, right| left.id.cmp(&right.id));
if let Some(duplicate) = first_duplicate(models.iter().map(|model| &model.id)) {
return Err(CatalogContentError::DuplicateModel {
model: duplicate.clone(),
});
}
for model in &mut models {
model.offerings.sort_by(|left, right| {
left.provider
.cmp(&right.provider)
.then_with(|| left.published_model_id.cmp(&right.published_model_id))
});
if let Some(duplicate) = model.offerings.windows(2).find(|pair| {
pair[0].provider == pair[1].provider
&& pair[0].published_model_id == pair[1].published_model_id
}) {
return Err(CatalogContentError::DuplicateOffering {
model: model.id.clone(),
provider: duplicate[0].provider.clone(),
published: duplicate[0].published_model_id.clone(),
});
}
for offering in &model.offerings {
if !known.contains(&offering.provider) {
return Err(CatalogContentError::UnknownProvider {
model: model.id.clone(),
provider: offering.provider.clone(),
});
}
if offering.model != model.id {
return Err(CatalogContentError::OfferingModelMismatch {
model: model.id.clone(),
provider: offering.provider.clone(),
published: offering.published_model_id.clone(),
});
}
}
}
let content_id = CatalogContentId(
canonical_content(&providers, &models)
.checksum()
.map_err(|source| CatalogContentError::Uncanonicalizable { source })?,
);
Ok(Self {
providers,
models,
content_id,
})
}
pub fn providers(&self) -> &[CatalogProvider] {
&self.providers
}
pub fn models(&self) -> &[CatalogModelEntry] {
&self.models
}
pub fn provider(&self, id: &ProviderId) -> Option<&CatalogProvider> {
self.providers.iter().find(|provider| &provider.id == id)
}
pub fn model(&self, id: &ModelId) -> Option<&CatalogModelEntry> {
self.models.iter().find(|model| &model.id == id)
}
pub fn offering(&self, model: &ModelId, provider: &ProviderId) -> Option<&ProviderOffering> {
self.model(model)?.offering(provider)
}
pub fn offering_count(&self) -> usize {
self.models.iter().map(|model| model.offerings.len()).sum()
}
pub const fn content_id(&self) -> CatalogContentId {
self.content_id
}
pub fn diff(&self, previous: &Self) -> CatalogDiff {
CatalogDiff::between(previous, self)
}
}
impl Canonical for CatalogContent {
fn canonical(&self) -> CanonicalValue {
canonical_content(&self.providers, &self.models)
}
}
fn canonical_content(
providers: &[CatalogProvider],
models: &[CatalogModelEntry],
) -> CanonicalValue {
CanonicalValue::map([
(
"providers",
CanonicalValue::List(providers.iter().map(Canonical::canonical).collect()),
),
(
"models",
CanonicalValue::List(models.iter().map(Canonical::canonical).collect()),
),
])
}
fn first_duplicate<'a, T: PartialEq>(values: impl Iterator<Item = &'a T> + 'a) -> Option<&'a T> {
let mut previous: Option<&T> = None;
for value in values {
if previous == Some(value) {
return Some(value);
}
previous = Some(value);
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CatalogChange {
ProviderAdded {
provider: ProviderId,
},
ProviderRemoved {
provider: ProviderId,
},
ProviderChanged {
provider: ProviderId,
fields: Vec<ProviderField>,
},
ModelAdded {
model: ModelId,
},
ModelRemoved {
model: ModelId,
},
OfferingAdded {
model: ModelId,
provider: ProviderId,
published: String,
},
OfferingRemoved {
model: ModelId,
provider: ProviderId,
published: String,
},
NeutralChanged {
model: ModelId,
fields: Vec<ModelField>,
},
NeutralDescribed {
model: ModelId,
},
NeutralDropped {
model: ModelId,
},
LifecycleChanged {
model: ModelId,
provider: ProviderId,
published: String,
from: ModelLifecycle,
to: ModelLifecycle,
},
CapabilitiesChanged {
model: ModelId,
provider: ProviderId,
published: String,
fields: Vec<ModelField>,
},
MetadataChanged {
model: ModelId,
provider: ProviderId,
published: String,
fields: Vec<ModelField>,
},
PriceChanged {
model: ModelId,
provider: ProviderId,
published: String,
from: Option<Box<ObservedPrice>>,
to: Option<Box<ObservedPrice>>,
},
}
impl CatalogChange {
pub fn model(&self) -> Option<&ModelId> {
match self {
Self::ProviderAdded { .. }
| Self::ProviderRemoved { .. }
| Self::ProviderChanged { .. } => None,
Self::ModelAdded { model }
| Self::ModelRemoved { model }
| Self::NeutralChanged { model, .. }
| Self::NeutralDescribed { model }
| Self::NeutralDropped { model }
| Self::OfferingAdded { model, .. }
| Self::OfferingRemoved { model, .. }
| Self::LifecycleChanged { model, .. }
| Self::CapabilitiesChanged { model, .. }
| Self::MetadataChanged { model, .. }
| Self::PriceChanged { model, .. } => Some(model),
}
}
pub fn provider(&self) -> Option<&ProviderId> {
match self {
Self::ModelAdded { .. }
| Self::ModelRemoved { .. }
| Self::NeutralChanged { .. }
| Self::NeutralDescribed { .. }
| Self::NeutralDropped { .. } => None,
Self::ProviderAdded { provider }
| Self::ProviderRemoved { provider }
| Self::ProviderChanged { provider, .. }
| Self::OfferingAdded { provider, .. }
| Self::OfferingRemoved { provider, .. }
| Self::LifecycleChanged { provider, .. }
| Self::CapabilitiesChanged { provider, .. }
| Self::MetadataChanged { provider, .. }
| Self::PriceChanged { provider, .. } => Some(provider),
}
}
pub fn published(&self) -> Option<&str> {
match self {
Self::OfferingAdded { published, .. }
| Self::OfferingRemoved { published, .. }
| Self::LifecycleChanged { published, .. }
| Self::CapabilitiesChanged { published, .. }
| Self::MetadataChanged { published, .. }
| Self::PriceChanged { published, .. } => Some(published),
_ => None,
}
}
const fn rank(&self) -> u8 {
match self {
Self::ProviderAdded { .. } => 0,
Self::ProviderRemoved { .. } => 1,
Self::ProviderChanged { .. } => 2,
Self::ModelAdded { .. } => 3,
Self::ModelRemoved { .. } => 4,
Self::NeutralDescribed { .. } => 5,
Self::NeutralDropped { .. } => 6,
Self::NeutralChanged { .. } => 7,
Self::OfferingAdded { .. } => 8,
Self::OfferingRemoved { .. } => 9,
Self::LifecycleChanged { .. } => 10,
Self::CapabilitiesChanged { .. } => 11,
Self::MetadataChanged { .. } => 12,
Self::PriceChanged { .. } => 13,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CatalogDiffCounts {
pub providers_added: usize,
pub providers_removed: usize,
pub providers_changed: usize,
pub models_added: usize,
pub models_removed: usize,
pub offerings_added: usize,
pub offerings_removed: usize,
pub neutral_changed: usize,
pub lifecycle_changed: usize,
pub capabilities_changed: usize,
pub metadata_changed: usize,
pub prices_changed: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CatalogDiff {
changes: Vec<CatalogChange>,
}
impl CatalogDiff {
fn between(previous: &CatalogContent, current: &CatalogContent) -> Self {
let mut changes = Vec::new();
let previous_providers: BTreeMap<&ProviderId, &CatalogProvider> = previous
.providers()
.iter()
.map(|provider| (&provider.id, provider))
.collect();
for provider in current.providers() {
match previous_providers.get(&provider.id) {
None => changes.push(CatalogChange::ProviderAdded {
provider: provider.id.clone(),
}),
Some(before) => {
let fields = provider.differences(before);
if !fields.is_empty() {
changes.push(CatalogChange::ProviderChanged {
provider: provider.id.clone(),
fields,
});
}
}
}
}
for provider in previous.providers() {
if current.provider(&provider.id).is_none() {
changes.push(CatalogChange::ProviderRemoved {
provider: provider.id.clone(),
});
}
}
let previous_models: BTreeMap<&ModelId, &CatalogModelEntry> = previous
.models()
.iter()
.map(|model| (&model.id, model))
.collect();
let current_models: BTreeMap<&ModelId, &CatalogModelEntry> = current
.models()
.iter()
.map(|model| (&model.id, model))
.collect();
for (id, model) in ¤t_models {
if !previous_models.contains_key(id) {
changes.push(CatalogChange::ModelAdded {
model: (*id).clone(),
});
for offering in &model.offerings {
changes.push(CatalogChange::OfferingAdded {
model: (*id).clone(),
provider: offering.provider.clone(),
published: offering.published_model_id.clone(),
});
}
}
}
for (id, model) in &previous_models {
if !current_models.contains_key(id) {
changes.push(CatalogChange::ModelRemoved {
model: (*id).clone(),
});
for offering in &model.offerings {
changes.push(CatalogChange::OfferingRemoved {
model: (*id).clone(),
provider: offering.provider.clone(),
published: offering.published_model_id.clone(),
});
}
}
}
for (id, model) in ¤t_models {
let Some(before) = previous_models.get(id) else {
continue;
};
match (&before.neutral, &model.neutral) {
(None, Some(_)) => changes.push(CatalogChange::NeutralDescribed {
model: (*id).clone(),
}),
(Some(_), None) => changes.push(CatalogChange::NeutralDropped {
model: (*id).clone(),
}),
(Some(was), Some(now)) => {
let fields = now.differences(was);
if !fields.is_empty() {
changes.push(CatalogChange::NeutralChanged {
model: (*id).clone(),
fields,
});
}
}
(None, None) => {}
}
for offering in &model.offerings {
let Some(previous_offering) = paired(before, model, offering) else {
changes.push(CatalogChange::OfferingAdded {
model: (*id).clone(),
provider: offering.provider.clone(),
published: offering.published_model_id.clone(),
});
continue;
};
changes.extend(offering_changes(id, previous_offering, offering));
}
for offering in &before.offerings {
if paired(model, before, offering).is_none() {
changes.push(CatalogChange::OfferingRemoved {
model: (*id).clone(),
provider: offering.provider.clone(),
published: offering.published_model_id.clone(),
});
}
}
}
changes.sort_by(|left, right| {
left.model()
.cmp(&right.model())
.then_with(|| left.provider().cmp(&right.provider()))
.then_with(|| left.rank().cmp(&right.rank()))
.then_with(|| left.published().cmp(&right.published()))
});
Self { changes }
}
pub fn changes(&self) -> &[CatalogChange] {
&self.changes
}
pub fn is_empty(&self) -> bool {
self.changes.is_empty()
}
pub fn counts(&self) -> CatalogDiffCounts {
let mut counts = CatalogDiffCounts::default();
for change in &self.changes {
match change {
CatalogChange::ProviderAdded { .. } => counts.providers_added += 1,
CatalogChange::ProviderRemoved { .. } => counts.providers_removed += 1,
CatalogChange::ProviderChanged { .. } => counts.providers_changed += 1,
CatalogChange::NeutralChanged { .. }
| CatalogChange::NeutralDescribed { .. }
| CatalogChange::NeutralDropped { .. } => counts.neutral_changed += 1,
CatalogChange::ModelAdded { .. } => counts.models_added += 1,
CatalogChange::ModelRemoved { .. } => counts.models_removed += 1,
CatalogChange::OfferingAdded { .. } => counts.offerings_added += 1,
CatalogChange::OfferingRemoved { .. } => counts.offerings_removed += 1,
CatalogChange::LifecycleChanged { .. } => counts.lifecycle_changed += 1,
CatalogChange::CapabilitiesChanged { .. } => counts.capabilities_changed += 1,
CatalogChange::MetadataChanged { .. } => counts.metadata_changed += 1,
CatalogChange::PriceChanged { .. } => counts.prices_changed += 1,
}
}
counts
}
pub fn has_price_changes(&self) -> bool {
self.changes
.iter()
.any(|change| matches!(change, CatalogChange::PriceChanged { .. }))
}
}
fn paired<'a>(
entry: &'a CatalogModelEntry,
from: &CatalogModelEntry,
offering: &ProviderOffering,
) -> Option<&'a ProviderOffering> {
let mut published = entry.offerings_by(&offering.provider);
let first = published.next()?;
if published.next().is_none() && from.offerings_by(&offering.provider).count() == 1 {
return Some(first);
}
entry.offering_published_as(&offering.provider, &offering.published_model_id)
}
fn offering_changes(
model: &ModelId,
previous: &ProviderOffering,
current: &ProviderOffering,
) -> Vec<CatalogChange> {
let mut changes = Vec::new();
let mut differences = current.facts.differences(&previous.facts);
if previous.endpoint != current.endpoint {
differences.push(ModelField::Endpoint);
}
if previous.published_model_id != current.published_model_id {
differences.push(ModelField::PublishedModelId);
}
if differences.iter().any(|field| field.lifecycle()) {
changes.push(CatalogChange::LifecycleChanged {
model: model.clone(),
provider: current.provider.clone(),
published: current.published_model_id.clone(),
from: previous.facts.lifecycle,
to: current.facts.lifecycle,
});
}
let capability_fields: Vec<ModelField> = differences
.iter()
.copied()
.filter(|field| field.capability())
.collect();
if !capability_fields.is_empty() {
changes.push(CatalogChange::CapabilitiesChanged {
model: model.clone(),
provider: current.provider.clone(),
published: current.published_model_id.clone(),
fields: capability_fields,
});
}
let metadata_fields: Vec<ModelField> = differences
.iter()
.copied()
.filter(|field| !field.lifecycle() && !field.capability())
.collect();
if !metadata_fields.is_empty() {
changes.push(CatalogChange::MetadataChanged {
model: model.clone(),
provider: current.provider.clone(),
published: current.published_model_id.clone(),
fields: metadata_fields,
});
}
if previous.price != current.price {
changes.push(CatalogChange::PriceChanged {
model: model.clone(),
provider: current.provider.clone(),
published: current.published_model_id.clone(),
from: previous.price.clone().map(Box::new),
to: current.price.clone().map(Box::new),
});
}
changes
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Admission {
Unchanged { content_id: CatalogContentId },
Updated {
content_id: CatalogContentId,
diff: CatalogDiff,
},
Initial { content_id: CatalogContentId },
}
impl Admission {
pub const fn content_id(&self) -> CatalogContentId {
match self {
Self::Unchanged { content_id }
| Self::Updated { content_id, .. }
| Self::Initial { content_id } => *content_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refreshed {
Admitted(Admission),
Refused(Refusal),
}
impl Refreshed {
pub const fn admission(&self) -> Option<&Admission> {
match self {
Self::Admitted(admission) => Some(admission),
Self::Refused(_) => None,
}
}
pub const fn refusal(&self) -> Option<&Refusal> {
match self {
Self::Admitted(_) => None,
Self::Refused(refusal) => Some(refusal),
}
}
}
#[derive(Debug, Default)]
pub struct LastKnownGoodCatalog {
active: Option<CatalogSnapshot>,
consecutive_refusals: u32,
last_refusal: Option<Refusal>,
}
impl LastKnownGoodCatalog {
pub const fn new() -> Self {
Self {
active: None,
consecutive_refusals: 0,
last_refusal: None,
}
}
pub fn restored(
active: Option<CatalogSnapshot>,
consecutive_refusals: u32,
last_refusal: Option<Refusal>,
) -> Self {
Self {
active,
consecutive_refusals,
last_refusal,
}
}
pub fn active(&self) -> Option<&CatalogSnapshot> {
self.active.as_ref()
}
pub fn content(&self) -> Option<&CatalogContent> {
self.active.as_ref().map(|snapshot| &snapshot.content)
}
pub fn validators(&self) -> Option<&SourceValidators> {
self.active
.as_ref()
.map(|snapshot| &snapshot.source.validators)
}
pub fn can_confirm_unchanged(&self, asked_with: Option<&SourceValidators>) -> bool {
self.active.is_some() && asked_with.is_some_and(|validators| !validators.is_empty())
}
pub fn admit(&mut self, mut snapshot: CatalogSnapshot) -> Admission {
let content_id = snapshot.content.content_id();
let admission = match self.active.as_ref() {
None => Admission::Initial { content_id },
Some(active) if active.content.content_id() == content_id => {
let mut held = active.source.validators.clone();
held.carry_over(std::mem::take(&mut snapshot.source.validators));
snapshot.source.validators = held;
Admission::Unchanged { content_id }
}
Some(active) => Admission::Updated {
content_id,
diff: snapshot.content.diff(&active.content),
},
};
self.active = Some(snapshot);
self.consecutive_refusals = 0;
self.last_refusal = None;
admission
}
pub fn admit_as_of(
&mut self,
mut snapshot: CatalogSnapshot,
checked_at: SystemTime,
) -> Admission {
snapshot.source.fetched_at = checked_at;
self.admit(snapshot)
}
pub fn record_unchanged(
&mut self,
validators: SourceValidators,
checked_at: SystemTime,
) -> bool {
let Some(active) = self.active.as_mut() else {
return false;
};
active.source.validators.carry_over(validators);
active.source.fetched_at = checked_at;
self.consecutive_refusals = 0;
self.last_refusal = None;
true
}
pub fn admit_result<E: Refusable>(
&mut self,
parsed: Result<CatalogSnapshot, E>,
) -> Result<Admission, (E, Option<&CatalogSnapshot>)> {
match parsed {
Ok(snapshot) => Ok(self.admit(snapshot)),
Err(error) => {
self.record_refusal(error.refusal());
Err((error, self.active.as_ref()))
}
}
}
pub fn record_refresh<E: Refusable>(
&mut self,
refreshed: Result<CatalogRefresh, E>,
asked_with: Option<&SourceValidators>,
checked_at: SystemTime,
) -> Result<Refreshed, (E, Option<&CatalogSnapshot>)> {
match refreshed {
Ok(CatalogRefresh::Unchanged { validators }) => {
if !self.can_confirm_unchanged(asked_with) {
let refusal = Refusal::new(RefusalReason::UnsolicitedUnchanged);
self.record_refusal(refusal.clone());
return Ok(Refreshed::Refused(refusal));
}
let confirmed = self.record_unchanged(validators, checked_at);
debug_assert!(confirmed, "a confirmable answer has an active snapshot");
Ok(Refreshed::Admitted(Admission::Unchanged {
content_id: self
.active
.as_ref()
.expect("an unchanged answer was confirmed against an active snapshot")
.content
.content_id(),
}))
}
Ok(CatalogRefresh::Updated { snapshot, .. }) => {
Ok(Refreshed::Admitted(self.admit_as_of(*snapshot, checked_at)))
}
Err(error) => {
self.record_refusal(error.refusal());
Err((error, self.active.as_ref()))
}
}
}
pub fn record_refusal(&mut self, refusal: Refusal) {
self.consecutive_refusals = self.consecutive_refusals.saturating_add(1);
self.last_refusal = Some(refusal);
}
pub const fn consecutive_refusals(&self) -> u32 {
self.consecutive_refusals
}
pub const fn last_refusal(&self) -> Option<&Refusal> {
self.last_refusal.as_ref()
}
pub fn report(&self, now: SystemTime) -> CatalogReport {
CatalogReport {
active: self.active.as_ref().map(|snapshot| ActiveCatalog {
content_id: snapshot.content.content_id(),
fetched_at: snapshot.source.fetched_at,
age: now
.duration_since(snapshot.source.fetched_at)
.unwrap_or_default(),
}),
consecutive_refusals: self.consecutive_refusals,
last_refusal: self.last_refusal.as_ref().map(Refusal::reason),
}
}
}
pub const PERSISTENT_REFUSAL_THRESHOLD: u32 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActiveCatalog {
pub content_id: CatalogContentId,
pub fetched_at: SystemTime,
pub age: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CatalogReport {
pub active: Option<ActiveCatalog>,
pub consecutive_refusals: u32,
pub last_refusal: Option<RefusalReason>,
}
impl CatalogReport {
pub const fn persistent_refusal(&self) -> bool {
self.consecutive_refusals >= PERSISTENT_REFUSAL_THRESHOLD
}
pub fn active_age(&self) -> Option<Duration> {
self.active.map(|active| active.age)
}
}
pub fn source_snapshot(
source_url: impl Into<String>,
schema_version: SchemaVersion,
payload: &[u8],
content: &CatalogContent,
validators: SourceValidators,
fetched_at: SystemTime,
) -> SourceSnapshot {
SourceSnapshot {
source_url: source_url.into(),
schema_version,
validators,
fetched_at,
raw: BlobRef::of(BlobKind::CatalogSnapshot, payload),
content_id: content.content_id(),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::super::{BackendPath, Capability, fakes::InMemoryCatalog, responsibility};
use super::*;
fn provider(id: &str) -> CatalogProvider {
CatalogProvider {
id: ProviderId::parse(id).expect("fixture id"),
display_name: Some(id.to_owned()),
doc_url: None,
endpoint: ProviderEndpoint::default(),
env_vars: vec![format!("{}_API_KEY", id.to_uppercase())],
pointer: JsonPointer::new("").child("providers").child(id),
}
}
fn facts() -> ModelFacts {
ModelFacts {
display_name: Some("GPT-4o".to_owned()),
capabilities: [ModelCapability::ToolCall].into_iter().collect(),
input_modalities: [Modality::Text].into_iter().collect(),
output_modalities: [Modality::Text].into_iter().collect(),
limits: ModelLimits {
context_tokens: Some(128_000),
output_tokens: Some(16_384),
..ModelLimits::default()
},
..ModelFacts::default()
}
}
fn offering(provider: &str, model: &str, price: Option<ObservedPrice>) -> ProviderOffering {
ProviderOffering {
provider: ProviderId::parse(provider).expect("fixture id"),
model: ModelId::parse(model).expect("fixture id"),
published_model_id: model.to_owned(),
facts: facts(),
overrides: Vec::new(),
price,
endpoint: ProviderEndpoint::default(),
pointer: JsonPointer::new("")
.child("providers")
.child(provider)
.child("models")
.child(model),
}
}
#[test]
fn a_second_published_id_from_one_provider_is_diffed_as_its_own_offering() {
let mut alias = offering("openai", "gpt-4o", Some(price(1, 2)));
alias.published_model_id = "gpt-4o-latest".to_owned();
let mut dearer = alias.clone();
dearer.price = Some(price(1, 3));
let before = content(vec![offering("openai", "gpt-4o", Some(price(1, 2))), alias]);
let after = content(vec![
offering("openai", "gpt-4o", Some(price(1, 2))),
dearer,
]);
assert_ne!(before.content_id(), after.content_id());
let diff = after.diff(&before);
assert_eq!(diff.counts().prices_changed, 1);
assert_eq!(diff.counts().offerings_added, 0);
assert_eq!(diff.counts().offerings_removed, 0);
assert_eq!(
diff.changes()[0].published(),
Some("gpt-4o-latest"),
"and the report names which of the provider's ids got dearer"
);
}
#[test]
fn a_change_to_one_alias_is_distinguishable_from_a_change_to_the_other() {
let mut first = offering("openai", "gpt-4o", Some(price(1, 2)));
first.published_model_id = "gpt-4o-2024".to_owned();
let mut second = offering("openai", "gpt-4o", Some(price(1, 2)));
second.published_model_id = "gpt-4o-latest".to_owned();
let before = content(vec![first.clone(), second.clone()]);
let mut first_dearer = first;
first_dearer.price = Some(price(1, 3));
let mut second_deprecated = second;
second_deprecated.facts.lifecycle = ModelLifecycle::Deprecated;
let after = content(vec![first_dearer, second_deprecated]);
let diff = after.diff(&before);
let named: Vec<Option<&str>> = diff
.changes()
.iter()
.map(CatalogChange::published)
.collect();
assert_eq!(
named,
[Some("gpt-4o-latest"), Some("gpt-4o-2024")],
"each report names its own callable id"
);
assert!(matches!(
diff.changes()[0],
CatalogChange::LifecycleChanged { .. }
));
assert!(matches!(
diff.changes()[1],
CatalogChange::PriceChanged { .. }
));
}
#[test]
fn provenance_is_not_content() {
let stated = offering("openai", "gpt-4o", Some(price(1, 2)));
let mut elsewhere = stated.clone();
elsewhere.pointer = JsonPointer::new("").child("somewhere").child("else");
elsewhere.overrides = vec![(ModelField::DisplayName, JsonPointer::new("/made/up"))];
let before = content(vec![stated]);
let after = content(vec![elsewhere]);
assert_eq!(before.content_id(), after.content_id());
assert!(after.diff(&before).is_empty());
}
#[test]
fn a_catalogue_record_equals_its_own_round_trip() {
let mut described = offering("openai", "gpt-4o", Some(price(1, 2)));
described.facts.capabilities = [
ModelCapability::ToolCall,
ModelCapability::Attachment,
ModelCapability::Reasoning,
]
.into_iter()
.collect();
described.facts.input_modalities = [Modality::Text, Modality::Image, Modality::Audio]
.into_iter()
.collect();
let content = content(vec![described]);
let serializer = crate::desired_state::canonical::SerializerVersion::default();
for record in [
content.providers()[0].canonical(),
content.models()[0].canonical(),
content.models()[0].offerings[0].canonical(),
content.canonical(),
] {
let bytes = record.to_canonical_bytes().expect("canonical bytes");
assert_eq!(
serializer.decode(&bytes).expect("decode"),
record,
"a record built here must be the record storage returns"
);
}
}
#[test]
fn an_offering_that_comes_or_goes_names_the_id_callers_would_have_sent() {
let held = offering("openai", "gpt-4o", None);
let mut first = held.clone();
first.published_model_id = "gpt-4o-latest".to_owned();
let mut second = held.clone();
second.published_model_id = "gpt-4o-2024".to_owned();
let before = content(vec![held.clone()]);
let after = content(vec![held.clone(), first, second]);
let model = ModelId::parse("gpt-4o").expect("fixture id");
let provider = ProviderId::parse("openai").expect("fixture id");
let added = after.diff(&before);
assert_eq!(
added.changes(),
[
CatalogChange::OfferingAdded {
model: model.clone(),
provider: provider.clone(),
published: "gpt-4o-2024".to_owned(),
},
CatalogChange::OfferingAdded {
model: model.clone(),
provider: provider.clone(),
published: "gpt-4o-latest".to_owned(),
},
],
"two aliases arriving are two distinguishable changes"
);
let removed = before.diff(&after);
assert_eq!(
removed.changes(),
[
CatalogChange::OfferingRemoved {
model: model.clone(),
provider: provider.clone(),
published: "gpt-4o-2024".to_owned(),
},
CatalogChange::OfferingRemoved {
model,
provider,
published: "gpt-4o-latest".to_owned(),
},
],
"and withdrawing them names which callable id went"
);
}
fn price(input: u64, output: u64) -> ObservedPrice {
ObservedPrice::new(PriceRates::new(
ObservedRate::from_nanos(input),
ObservedRate::from_nanos(output),
))
}
fn content(offerings: Vec<ProviderOffering>) -> CatalogContent {
let mut providers: Vec<CatalogProvider> = offerings
.iter()
.map(|offering| provider(offering.provider.as_str()))
.collect();
providers.dedup_by(|left, right| left.id == right.id);
let mut models: BTreeMap<ModelId, CatalogModelEntry> = BTreeMap::new();
for offering in offerings {
models
.entry(offering.model.clone())
.or_insert_with(|| CatalogModelEntry {
id: offering.model.clone(),
neutral: Some(facts()),
offerings: Vec::new(),
})
.offerings
.push(offering);
}
CatalogContent::new(providers, models.into_values().collect()).expect("fixture content")
}
fn snapshot(content: CatalogContent, validators: SourceValidators) -> CatalogSnapshot {
let source = source_snapshot(
"https://models.dev/catalog.json",
SchemaVersion::MODELS_DEV_CATALOG_V1,
b"{}",
&content,
validators,
SystemTime::UNIX_EPOCH,
);
CatalogSnapshot { source, content }
}
fn refreshed(snapshot: CatalogSnapshot) -> CatalogRefresh {
CatalogRefresh::Updated {
snapshot: Box::new(snapshot),
payload: RawPayload::new(&b"{}"[..]),
}
}
#[test]
fn identical_content_is_one_identity_whatever_order_it_was_built_in() {
let forwards = content(vec![
offering(
"anthropic",
"claude-sonnet-4",
Some(price(3_000_000_000, 15_000_000_000)),
),
offering(
"openai",
"gpt-4o",
Some(price(2_500_000_000, 10_000_000_000)),
),
]);
let backwards = content(vec![
offering(
"openai",
"gpt-4o",
Some(price(2_500_000_000, 10_000_000_000)),
),
offering(
"anthropic",
"claude-sonnet-4",
Some(price(3_000_000_000, 15_000_000_000)),
),
]);
assert_eq!(forwards.content_id(), backwards.content_id());
assert_eq!(forwards, backwards);
}
#[test]
fn retrieval_metadata_does_not_change_content_identity() {
let content = content(vec![offering("openai", "gpt-4o", None)]);
let first = snapshot(content.clone(), SourceValidators::etag("\"one\""));
let second = CatalogSnapshot {
source: SourceSnapshot {
fetched_at: SystemTime::UNIX_EPOCH + Duration::from_secs(86_400),
validators: SourceValidators {
etag: Some(ETag("\"two\"".to_owned())),
last_modified: Some(HttpDate("Wed, 12 Aug 2026 20:00:00 GMT".to_owned())),
},
..first.source.clone()
},
content,
};
assert_eq!(first.source.content_id, second.source.content_id);
assert_ne!(first.source.validators, second.source.validators);
}
#[test]
fn admission_classifies_the_content_it_admits_not_the_id_beside_it() {
let mut catalogue = LastKnownGoodCatalog::new();
let before = content(vec![offering("openai", "gpt-4o", None)]);
let stale_id = before.content_id();
assert_eq!(
catalogue.admit(snapshot(before, SourceValidators::etag("\"one\""))),
Admission::Initial {
content_id: stale_id
}
);
let after = content(vec![offering(
"openai",
"gpt-4o",
Some(price(2_000_000_000, 10_000_000_000)),
)]);
let content_id = after.content_id();
let mislabelled = CatalogSnapshot {
source: SourceSnapshot {
content_id: stale_id,
..snapshot(after.clone(), SourceValidators::etag("\"two\"")).source
},
content: after,
};
let Admission::Updated {
content_id: id,
diff,
} = catalogue.admit(mislabelled)
else {
panic!("content that differs is an update whatever the record beside it says");
};
assert_eq!(id, content_id);
assert!(diff.has_price_changes());
assert_eq!(
catalogue.content().map(CatalogContent::content_id),
Some(content_id),
"and the id reported is the id of what became active"
);
}
#[test]
fn an_unchanged_answer_moves_the_validators_without_moving_the_content() {
let mut catalogue = LastKnownGoodCatalog::new();
assert!(
!catalogue.record_unchanged(SourceValidators::etag("\"one\""), SystemTime::UNIX_EPOCH),
"there is nothing to say is unchanged before a first import"
);
let held = content(vec![offering("openai", "gpt-4o", None)]);
let content_id = held.content_id();
catalogue.admit(snapshot(held, SourceValidators::etag("\"one\"")));
let checked_at = SystemTime::UNIX_EPOCH + Duration::from_secs(3_600);
assert!(catalogue.record_unchanged(SourceValidators::etag("\"two\""), checked_at));
assert_eq!(
catalogue.validators(),
Some(&SourceValidators::etag("\"two\"")),
"the next conditional request asks with what the source last answered"
);
let active = catalogue.active().expect("the content stayed active");
assert_eq!(active.source.fetched_at, checked_at);
assert_eq!(
active.content.content_id(),
content_id,
"an unchanged answer is not new content"
);
assert_eq!(active.source.content_id, content_id);
assert!(catalogue.record_unchanged(SourceValidators::default(), checked_at));
assert_eq!(
catalogue.validators(),
Some(&SourceValidators::etag("\"two\"")),
"an answer that repeats no tag has not withdrawn one: dropping it \
would leave nothing to ask conditionally with, and every later \
refresh would transfer the whole document"
);
let last_modified = SourceValidators {
etag: None,
last_modified: Some(HttpDate("Wed, 21 Oct 2015 07:28:00 GMT".to_owned())),
};
assert!(catalogue.record_unchanged(last_modified, checked_at));
assert_eq!(
catalogue.validators(),
Some(&SourceValidators {
etag: Some(ETag("\"two\"".to_owned())),
last_modified: Some(HttpDate("Wed, 21 Oct 2015 07:28:00 GMT".to_owned())),
}),
"and a partial answer states the one it carries without clearing the other"
);
}
#[test]
fn a_price_change_is_classified_apart_from_metadata() {
let before = content(vec![offering(
"openai",
"gpt-4o",
Some(price(2_500_000_000, 10_000_000_000)),
)]);
let after = content(vec![offering(
"openai",
"gpt-4o",
Some(price(2_000_000_000, 10_000_000_000)),
)]);
let diff = after.diff(&before);
assert!(diff.has_price_changes());
assert_eq!(diff.counts().prices_changed, 1);
assert_eq!(diff.counts().metadata_changed, 0);
assert_eq!(diff.counts().capabilities_changed, 0);
assert!(matches!(
diff.changes(),
[CatalogChange::PriceChanged { from, to, .. }]
if from.as_ref().map(|price| price.base.input.nanos()) == Some(2_500_000_000)
&& to.as_ref().map(|price| price.base.input.nanos()) == Some(2_000_000_000)
));
}
#[test]
fn lifecycle_capability_and_metadata_changes_are_separate_classes() {
let before = content(vec![offering("openai", "gpt-4o", None)]);
let mut changed = offering("openai", "gpt-4o", None);
changed.facts.lifecycle = ModelLifecycle::Deprecated;
changed
.facts
.capabilities
.insert(ModelCapability::Reasoning);
changed.facts.limits.context_tokens = Some(200_000);
let after = content(vec![changed]);
let counts = after.diff(&before).counts();
assert_eq!(counts.lifecycle_changed, 1);
assert_eq!(counts.capabilities_changed, 1);
assert_eq!(counts.metadata_changed, 1);
assert_eq!(counts.prices_changed, 0);
}
#[test]
fn every_change_the_identity_notices_the_diff_names() {
let before = content(vec![offering("openai", "gpt-4o", None)]);
let mut providers = before.providers().to_vec();
providers[0].env_vars.push("OPENAI_BASE_URL".to_owned());
let provider_moved = CatalogContent::new(providers, before.models().to_vec())
.expect("a provider's own metadata changed");
let mut neutral = before.models().to_vec();
neutral[0].neutral = None;
let neutral_dropped = CatalogContent::new(before.providers().to_vec(), neutral)
.expect("the neutral record went away");
let mut endpoint = offering("openai", "gpt-4o", None);
endpoint.endpoint = ProviderEndpoint {
api_base: Some("https://eu.api.openai.com/v1".to_owned()),
..ProviderEndpoint::default()
};
let endpoint_moved = content(vec![endpoint]);
let mut renamed = offering("openai", "gpt-4o", None);
renamed.published_model_id = "gpt-4o-2024-11-20".to_owned();
let republished = content(vec![renamed]);
for (case, after, expected) in [
(
"provider metadata",
provider_moved,
CatalogChange::ProviderChanged {
provider: ProviderId::parse("openai").expect("fixture id"),
fields: vec![ProviderField::EnvVars],
},
),
(
"the neutral record",
neutral_dropped,
CatalogChange::NeutralDropped {
model: ModelId::parse("gpt-4o").expect("fixture id"),
},
),
(
"an offering's endpoint",
endpoint_moved,
CatalogChange::MetadataChanged {
model: ModelId::parse("gpt-4o").expect("fixture id"),
provider: ProviderId::parse("openai").expect("fixture id"),
published: "gpt-4o".to_owned(),
fields: vec![ModelField::Endpoint],
},
),
(
"the id a request must send",
republished,
CatalogChange::MetadataChanged {
model: ModelId::parse("gpt-4o").expect("fixture id"),
provider: ProviderId::parse("openai").expect("fixture id"),
published: "gpt-4o-2024-11-20".to_owned(),
fields: vec![ModelField::PublishedModelId],
},
),
] {
assert_ne!(
before.content_id(),
after.content_id(),
"{case} is part of the identity"
);
assert_eq!(after.diff(&before).changes(), [expected], "{case}");
}
}
#[test]
fn additions_and_removals_name_their_offerings() {
let before = content(vec![offering("openai", "gpt-4o", None)]);
let after = content(vec![
offering("openai", "gpt-4o", None),
offering("anthropic", "claude-sonnet-4", None),
]);
let diff = after.diff(&before);
assert_eq!(diff.counts().models_added, 1);
assert_eq!(diff.counts().offerings_added, 1);
assert_eq!(diff.counts().models_removed, 0);
let reversed = before.diff(&after);
assert_eq!(reversed.counts().models_removed, 1);
assert_eq!(reversed.counts().offerings_removed, 1);
}
#[test]
fn a_diff_is_ordered_by_model_then_provider_then_kind() {
let before = content(vec![
offering("anthropic", "claude-sonnet-4", Some(price(1, 2))),
offering("openai", "gpt-4o", Some(price(1, 2))),
]);
let mut anthropic = offering("anthropic", "claude-sonnet-4", Some(price(3, 2)));
anthropic.facts.lifecycle = ModelLifecycle::Deprecated;
let after = content(vec![
anthropic,
offering("openai", "gpt-4o", Some(price(4, 2))),
]);
let diff = after.diff(&before);
let ordered: Vec<(Option<String>, Option<String>, u8)> = diff
.changes()
.iter()
.map(|change| {
(
change.model().map(ToString::to_string),
change.provider().map(ToString::to_string),
change.rank(),
)
})
.collect();
let mut sorted = ordered.clone();
sorted.sort();
assert_eq!(ordered, sorted, "changes come out in a stable order");
assert_eq!(after.diff(&before), after.diff(&before));
}
#[test]
fn content_rejects_a_dangling_or_duplicated_offering() {
let orphan = CatalogModelEntry {
id: ModelId::parse("gpt-4o").expect("id"),
neutral: None,
offerings: vec![offering("openai", "gpt-4o", None)],
};
assert_eq!(
CatalogContent::new(Vec::new(), vec![orphan.clone()]),
Err(CatalogContentError::UnknownProvider {
model: ModelId::parse("gpt-4o").expect("id"),
provider: ProviderId::parse("openai").expect("id"),
})
);
let doubled = CatalogModelEntry {
offerings: vec![
offering("openai", "gpt-4o", None),
offering("openai", "gpt-4o", None),
],
..orphan
};
assert_eq!(
CatalogContent::new(vec![provider("openai")], vec![doubled]),
Err(CatalogContentError::DuplicateOffering {
model: ModelId::parse("gpt-4o").expect("id"),
provider: ProviderId::parse("openai").expect("id"),
published: "gpt-4o".to_owned(),
})
);
assert_eq!(
CatalogContent::new(vec![provider("openai")], Vec::new()),
Err(CatalogContentError::Empty)
);
}
#[test]
fn an_offering_filed_under_the_wrong_model_is_refused() {
let entry = CatalogModelEntry {
id: ModelId::parse("gpt-4o-mini").expect("id"),
neutral: None,
offerings: vec![offering("openai", "gpt-4o", None)],
};
assert_eq!(
CatalogContent::new(vec![provider("openai")], vec![entry]),
Err(CatalogContentError::OfferingModelMismatch {
model: ModelId::parse("gpt-4o-mini").expect("id"),
provider: ProviderId::parse("openai").expect("id"),
published: "gpt-4o".to_owned(),
})
);
}
#[test]
fn ids_are_taken_as_published_and_validated_not_rewritten() {
for published in [
"anthropic/claude-sonnet-4",
"MiniMax-M1",
"Qwen/Qwen3-32B",
"gpt-4o@2024-08-06",
"accounts/fireworks/models/kimi~k2",
] {
assert_eq!(
ModelId::parse(published).expect("a published id").as_str(),
published,
"an id is stored as the provider publishes it"
);
}
assert_ne!(
ModelId::parse("MiniMax-M1").expect("id"),
ModelId::parse("minimax-m1").expect("id"),
"case distinguishes two published models"
);
assert_eq!(
ModelId::parse("gpt 4o"),
Err(InvalidCatalogId::Character {
value: "gpt 4o".to_owned(),
character: ' ',
})
);
assert_eq!(ModelId::parse(""), Err(InvalidCatalogId::Empty));
assert_eq!(
ModelId::parse("openai//gpt-4o"),
Err(InvalidCatalogId::Segment {
value: "openai//gpt-4o".to_owned(),
})
);
assert!(matches!(
ModelId::parse(&"m".repeat(129)),
Err(InvalidCatalogId::TooLong { max: 128, .. })
));
}
#[test]
fn a_rejected_import_leaves_the_active_catalogue_in_place() {
let mut catalogue = LastKnownGoodCatalog::new();
let good = snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
);
assert_eq!(
catalogue.admit(good.clone()),
Admission::Initial {
content_id: good.source.content_id
}
);
let rejected: Result<CatalogSnapshot, CatalogError> = Err(CatalogError::Invalid {
backend: "test",
refusal: Refusal::new(RefusalReason::Schema),
message: "schema drift".to_owned(),
});
let (error, active) = catalogue
.admit_result(rejected)
.expect_err("a drifted payload is refused");
assert_eq!(error.refused_by().reason(), RefusalReason::Schema);
assert_eq!(
active.map(|snapshot| snapshot.source.content_id),
Some(good.source.content_id)
);
assert_eq!(
catalogue.content().map(CatalogContent::offering_count),
Some(1)
);
assert_eq!(
catalogue.validators(),
Some(&SourceValidators::etag("\"one\""))
);
}
#[test]
fn admitting_identical_content_is_not_an_update() {
let mut catalogue = LastKnownGoodCatalog::new();
let content = content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]);
catalogue.admit(snapshot(content.clone(), SourceValidators::etag("\"one\"")));
let admission =
catalogue.admit(snapshot(content.clone(), SourceValidators::etag("\"two\"")));
assert_eq!(
admission,
Admission::Unchanged {
content_id: content.content_id()
}
);
assert_eq!(
catalogue.validators(),
Some(&SourceValidators::etag("\"two\""))
);
let admission = catalogue.admit(snapshot(content.clone(), SourceValidators::default()));
assert_eq!(
admission,
Admission::Unchanged {
content_id: content.content_id()
}
);
assert_eq!(
catalogue.validators(),
Some(&SourceValidators::etag("\"two\"")),
"an intermediary stripping the tag must not cost the tag"
);
let updated = content_with_price(3);
let admission = catalogue.admit(snapshot(updated, SourceValidators::default()));
assert!(matches!(admission, Admission::Updated { diff, .. } if diff.has_price_changes()));
assert_eq!(catalogue.validators(), Some(&SourceValidators::default()));
}
fn content_with_price(input: u64) -> CatalogContent {
content(vec![offering("openai", "gpt-4o", Some(price(input, 2)))])
}
#[tokio::test]
async fn a_first_refresh_returns_metadata_with_validators() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v1");
let CatalogRefresh::Updated { snapshot, .. } = source.refresh(None).await.expect("refresh")
else {
panic!("a first refresh has no prior validators to match");
};
assert_eq!(snapshot.source.validators, SourceValidators::etag("v1"));
assert_eq!(snapshot.content.offering_count(), 1);
assert_eq!(
snapshot.content.models()[0].id,
ModelId::parse("gpt-4o").expect("id")
);
assert_eq!(
snapshot.source.schema_version,
SchemaVersion::MODELS_DEV_CATALOG_V1
);
}
#[tokio::test]
async fn an_unchanged_upstream_is_not_an_empty_catalogue() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v1");
let refreshed = source
.refresh(Some(&SourceValidators::etag("v1")))
.await
.expect("refresh");
assert_eq!(
refreshed,
CatalogRefresh::Unchanged {
validators: SourceValidators::etag("v1")
}
);
assert_eq!(
source.transfers(),
0,
"an unchanged refresh transfers nothing"
);
}
#[tokio::test]
async fn a_changed_upstream_transfers_the_new_metadata() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v2");
let CatalogRefresh::Updated { snapshot, .. } = source
.refresh(Some(&SourceValidators::etag("v1")))
.await
.expect("refresh")
else {
panic!("changed validators must transfer");
};
assert_eq!(snapshot.source.validators, SourceValidators::etag("v2"));
assert_eq!(source.transfers(), 1);
}
#[tokio::test]
async fn observed_pricing_is_metadata_not_activation() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v1");
let CatalogRefresh::Updated { snapshot, .. } = source.refresh(None).await.unwrap() else {
panic!("expected metadata");
};
let offering = &snapshot.content.models()[0].offerings[0];
let price = offering.price.as_ref().expect("the fake publishes a price");
assert!(price.base.input.nanos() > 0);
assert!(!offering.facts.lifecycle.deprecated());
}
#[tokio::test]
async fn an_unreachable_source_is_retryable_and_never_a_boot_failure() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v1");
source.set_unavailable(true);
let error = source.refresh(None).await.expect_err("outage");
assert_eq!(error.category(), FailureCategory::Unavailable);
assert!(error.retryable());
source.set_unavailable(false);
assert!(matches!(
source.refresh(None).await,
Ok(CatalogRefresh::Updated { .. })
));
}
#[test]
fn the_refusal_vocabulary_and_its_string_duplicate_agree() {
let reasons: Vec<&str> = RefusalReason::ALL
.iter()
.map(|reason| reason.as_str())
.collect();
assert_eq!(REFUSAL_REASONS, reasons.as_slice());
let unique: BTreeSet<&str> = reasons.iter().copied().collect();
assert_eq!(unique.len(), reasons.len(), "a reason is named twice");
}
#[test]
fn a_short_content_id_is_a_fixed_width_prefix_of_its_digest() {
let content = content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]);
let short = content.content_id().short();
assert_eq!(short.len(), CONTENT_ID_SHORT_HEX);
assert!(short.chars().all(|c| c.is_ascii_hexdigit()));
assert!(content.content_id().checksum().to_string().contains(&short));
assert_eq!(
short,
content.content_id().short(),
"the same content is the same id"
);
}
#[test]
fn consecutive_refusals_accumulate_without_disturbing_what_is_active() {
let mut catalogue = LastKnownGoodCatalog::new();
let good = snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
);
catalogue.admit(good.clone());
let report = catalogue.report(SystemTime::UNIX_EPOCH);
assert_eq!(report.consecutive_refusals, 0);
assert!(!report.persistent_refusal());
catalogue.record_refusal(Refusal::new(RefusalReason::Unreachable));
let report = catalogue.report(SystemTime::UNIX_EPOCH);
assert_eq!(report.consecutive_refusals, 1);
assert_eq!(report.last_refusal, Some(RefusalReason::Unreachable));
assert!(
!report.persistent_refusal(),
"one bad minute upstream is not a page"
);
catalogue.record_refusal(Refusal::at(
RefusalReason::Schema,
JsonPointer::new("").child("models"),
));
let report = catalogue.report(SystemTime::UNIX_EPOCH);
assert_eq!(report.consecutive_refusals, PERSISTENT_REFUSAL_THRESHOLD);
assert_eq!(report.last_refusal, Some(RefusalReason::Schema));
assert!(
report.persistent_refusal(),
"a second refusal is the catalogue no longer advancing"
);
assert_eq!(
report.active.map(|active| active.content_id),
Some(good.content.content_id()),
"and none of it changed what is being served"
);
assert_eq!(
catalogue.last_refusal().and_then(Refusal::pointer),
Some(&JsonPointer::new("").child("models")),
"the pointer survives for the log line that a metric may not carry"
);
}
#[test]
fn a_confirmed_import_ends_the_run_of_refusals() {
let mut catalogue = LastKnownGoodCatalog::new();
catalogue.admit(snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
));
catalogue.record_refusal(Refusal::new(RefusalReason::NotJson));
catalogue.record_refusal(Refusal::new(RefusalReason::NotJson));
assert!(
catalogue
.report(SystemTime::UNIX_EPOCH)
.persistent_refusal()
);
let later = SystemTime::UNIX_EPOCH + Duration::from_secs(600);
assert!(catalogue.record_unchanged(SourceValidators::etag("\"one\""), later));
let report = catalogue.report(later);
assert_eq!(report.consecutive_refusals, 0);
assert_eq!(report.last_refusal, None);
assert_eq!(
report.active_age(),
Some(Duration::ZERO),
"a 304 is evidence the held content is current, not merely unchanged"
);
catalogue.record_refusal(Refusal::new(RefusalReason::NotJson));
catalogue.admit(snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 3)))]),
SourceValidators::etag("\"two\""),
));
assert_eq!(
catalogue.report(later).consecutive_refusals,
0,
"an admitted import ends the run too"
);
}
#[test]
fn active_age_grows_across_refusals_and_never_runs_backwards() {
let mut catalogue = LastKnownGoodCatalog::new();
assert_eq!(
catalogue.report(SystemTime::UNIX_EPOCH).active_age(),
None,
"a deployment that never imported has nothing stale"
);
let imported_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
let mut fresh = snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
);
fresh.source.fetched_at = imported_at;
catalogue.admit(fresh);
catalogue.record_refusal(Refusal::new(RefusalReason::Oversized));
assert_eq!(
catalogue
.report(imported_at + Duration::from_secs(3_600))
.active_age(),
Some(Duration::from_secs(3_600))
);
assert_eq!(
catalogue
.report(imported_at - Duration::from_secs(60))
.active_age(),
Some(Duration::ZERO),
"a clock that stepped backwards reads as fresh, never as negative"
);
}
#[test]
fn admitting_a_failure_counts_it_by_its_typed_reason() {
let mut catalogue = LastKnownGoodCatalog::new();
let refused: Result<CatalogSnapshot, CatalogError> = Err(CatalogError::Invalid {
backend: "test",
refusal: Refusal::at(
RefusalReason::Price,
JsonPointer::new("").child("cost").child("input"),
),
message: "https://models.dev/api.json: price is not a number".to_owned(),
});
let (error, active) = catalogue
.admit_result(refused)
.expect_err("a refused import");
assert!(active.is_none(), "there was nothing to keep active");
assert_eq!(error.refused_by().reason(), RefusalReason::Price);
let report = catalogue.report(SystemTime::UNIX_EPOCH);
assert_eq!(report.consecutive_refusals, 1);
assert_eq!(report.last_refusal, Some(RefusalReason::Price));
assert!(
!report
.last_refusal
.expect("a reason")
.as_str()
.contains('/'),
"the reason is a vocabulary word, never the pointer or the URL beside it"
);
}
#[test]
fn one_entry_point_counts_a_refresh_however_it_ended() {
let mut catalogue = LastKnownGoodCatalog::new();
let imported_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
let first = catalogue
.record_refresh::<CatalogError>(
Ok(refreshed(snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
))),
None,
imported_at,
)
.expect("an admitted import");
assert!(matches!(
first,
Refreshed::Admitted(Admission::Initial { .. })
));
let unreachable = CatalogError::unavailable("test", "connection refused".to_owned());
let (error, active) = catalogue
.record_refresh(Err(unreachable), None, imported_at)
.expect_err("a refused refresh");
assert_eq!(error.refused_by().reason(), RefusalReason::Unreachable);
assert!(
active.is_some(),
"and the last good catalogue keeps serving through it"
);
assert_eq!(catalogue.report(imported_at).consecutive_refusals, 1);
let checked_at = imported_at + Duration::from_secs(600);
let asked_with = catalogue.validators().cloned().expect("an active snapshot");
let confirmed = catalogue
.record_refresh::<CatalogError>(
Ok(CatalogRefresh::Unchanged {
validators: SourceValidators::etag("\"one\""),
}),
Some(&asked_with),
checked_at,
)
.expect("a confirmed answer");
assert!(matches!(
confirmed,
Refreshed::Admitted(Admission::Unchanged { .. })
));
let report = catalogue.report(checked_at);
assert_eq!(report.consecutive_refusals, 0, "a 304 ends the run");
assert_eq!(report.active_age(), Some(Duration::ZERO));
}
#[test]
fn an_unchanged_answer_with_nothing_held_is_counted_as_a_refusal() {
let mut catalogue = LastKnownGoodCatalog::new();
let checked_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
let refreshed = catalogue
.record_refresh::<CatalogError>(
Ok(CatalogRefresh::Unchanged {
validators: SourceValidators::etag("\"one\""),
}),
None,
checked_at,
)
.expect("an answer, not an error");
assert_eq!(refreshed.admission(), None, "nothing was admitted");
assert_eq!(
refreshed.refusal().map(Refusal::reason),
Some(RefusalReason::UnsolicitedUnchanged),
"and the caller is handed the reason to record, not just a count"
);
let report = catalogue.report(checked_at);
assert_eq!(report.active, None, "nothing became active");
assert_eq!(report.consecutive_refusals, 1);
assert_eq!(
report.last_refusal,
Some(RefusalReason::UnsolicitedUnchanged),
"and says so by name, because no error was produced to log"
);
}
#[test]
fn an_unchanged_answer_to_content_held_without_validators_is_a_refusal() {
let mut catalogue = LastKnownGoodCatalog::new();
let imported_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
catalogue.admit_as_of(
snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::default(),
),
imported_at,
);
assert!(
catalogue
.validators()
.expect("an active snapshot")
.is_empty(),
"nothing to make the next request conditional with"
);
catalogue.record_refusal(Refusal::new(RefusalReason::Unreachable));
let checked_at = imported_at + Duration::from_secs(3_600);
let refreshed = catalogue
.record_refresh::<CatalogError>(
Ok(CatalogRefresh::Unchanged {
validators: SourceValidators::default(),
}),
catalogue.validators().cloned().as_ref(),
checked_at,
)
.expect("an answer, not an error");
assert_eq!(
refreshed.refusal().map(Refusal::reason),
Some(RefusalReason::UnsolicitedUnchanged),
"an answer to a question nobody asked confirms nothing"
);
let report = catalogue.report(checked_at);
assert_eq!(
report.active_age(),
Some(Duration::from_secs(3_600)),
"so the active content keeps aging"
);
assert_eq!(
report.consecutive_refusals, 2,
"and the run continues rather than being cleared"
);
}
#[test]
fn an_unchanged_answer_to_a_request_that_carried_no_validator_is_a_refusal() {
let mut catalogue = LastKnownGoodCatalog::new();
let imported_at = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
catalogue.admit_as_of(
snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
),
imported_at,
);
assert!(
!catalogue
.validators()
.expect("an active snapshot")
.is_empty(),
"the held state alone would have made this confirmable"
);
catalogue.record_refusal(Refusal::new(RefusalReason::Unreachable));
let checked_at = imported_at + Duration::from_secs(3_600);
let refreshed = catalogue
.record_refresh::<CatalogError>(
Ok(CatalogRefresh::Unchanged {
validators: SourceValidators::etag("\"one\""),
}),
None,
checked_at,
)
.expect("an answer, not an error");
assert_eq!(
refreshed.refusal().map(Refusal::reason),
Some(RefusalReason::UnsolicitedUnchanged),
"nothing was sent for the source to have checked against"
);
let report = catalogue.report(checked_at);
assert_eq!(
report.active_age(),
Some(Duration::from_secs(3_600)),
"so the content keeps aging"
);
assert_eq!(
report.consecutive_refusals, 2,
"and the run continues rather than being cleared"
);
}
#[test]
fn an_admitted_import_is_aged_from_the_check_and_not_from_what_it_claims() {
let mut catalogue = LastKnownGoodCatalog::new();
let checked_at = SystemTime::UNIX_EPOCH + Duration::from_secs(86_400);
let stated = snapshot(
content(vec![offering("openai", "gpt-4o", Some(price(1, 2)))]),
SourceValidators::etag("\"one\""),
);
assert_eq!(stated.source.fetched_at, SystemTime::UNIX_EPOCH);
catalogue
.record_refresh::<CatalogError>(Ok(refreshed(stated)), None, checked_at)
.expect("an admitted import");
let report = catalogue.report(checked_at);
assert_eq!(
report.active_age(),
Some(Duration::ZERO),
"a fresh import is fresh however old the document says it is"
);
assert_eq!(
report.active.expect("an active catalogue").fetched_at,
checked_at
);
}
#[test]
fn a_seeded_import_is_aged_from_the_import_and_not_from_the_fixture() {
let mut catalogue = LastKnownGoodCatalog::new();
let booted_at =
crate::backends::models_dev::seed_fetched_at() + Duration::from_secs(90 * 86_400);
let seed = crate::backends::models_dev::seed_snapshot();
assert_eq!(
seed.source.fetched_at,
crate::backends::models_dev::seed_fetched_at()
);
assert!(matches!(
catalogue.admit_as_of(seed, booted_at),
Admission::Initial { .. }
));
assert_eq!(
catalogue.report(booted_at).active_age(),
Some(Duration::ZERO),
"a seed imported now is as current as this process has confirmed anything"
);
}
#[tokio::test]
async fn the_source_is_background_only_and_declares_incremental_refresh() {
let source = InMemoryCatalog::with_models(&[("openai", "gpt-4o")], "v1");
assert!(source.capabilities().has(Capability::IncrementalRefresh));
assert!(source.capabilities().has(Capability::PriceMetadata));
let responsibility = responsibility("CatalogSource").expect("declared responsibility");
assert_eq!(responsibility.path, BackendPath::Background);
assert!(responsibility.permits(CatalogBackend::default().kind()));
assert!(!responsibility.permits(BackendKind::Redis));
}
}