bcode_model_catalog_models 0.0.1-alpha.0

Bcode model catalog schema types
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]

//! Versioned schema types for Bcode model catalog documents.

use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

pub use live::{LIVE_SNAPSHOT_SCHEMA_VERSION, LiveCatalogSnapshot, LiveModel, LiveModelMetadata};

mod evidence;
mod live;
pub use evidence::{
    EVIDENCE_SCHEMA_VERSION, EvidenceCapability, EvidenceDocument, EvidenceOutcome,
    EvidenceResolution, EvidenceScope, ModelSupportEvidence,
};

/// Normalized model-catalog diagnostics exposed at the application boundary.
///
/// This portable snapshot uses the enclosing application protocol's version semantics.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogDiagnostics {
    /// Revision bundled with the running application.
    pub embedded_revision: String,
    /// Revision provided by the remote catalog, when available.
    pub remote_revision: Option<String>,
    /// Whether remote catalog access is enabled.
    pub remote_enabled: bool,
    /// Normalized cache availability status.
    pub cache_state: String,
    /// Age of the cached catalog in seconds, when known.
    pub cache_age_seconds: Option<u64>,
    /// Whether a refresh is currently running.
    pub refresh_in_progress: bool,
    /// Most recent refresh attempt as Unix epoch milliseconds.
    pub last_refresh_attempt_ms: Option<u64>,
    /// Most recent successful refresh as Unix epoch milliseconds.
    pub last_refresh_success_ms: Option<u64>,
    /// Public, secret-safe failure message from the most recent refresh.
    pub last_refresh_error: Option<String>,
}

/// Catalog schema version emitted by this crate.
pub const SCHEMA_VERSION: &str = "3.0.0";

/// Complete model catalog document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogDocument {
    /// Semantic schema version for this document.
    pub schema_version: String,
    /// Catalog revision, normally a git SHA or release identifier.
    pub catalog_revision: String,
    /// Generation timestamp, encoded as RFC 3339 text.
    pub generated_at: String,
    /// Providers keyed by stable provider id.
    #[serde(default)]
    pub providers: BTreeMap<String, ProviderCatalog>,
}

impl CatalogDocument {
    /// Construct an empty catalog document.
    #[must_use]
    pub fn empty(catalog_revision: impl Into<String>, generated_at: impl Into<String>) -> Self {
        Self {
            schema_version: SCHEMA_VERSION.to_string(),
            catalog_revision: catalog_revision.into(),
            generated_at: generated_at.into(),
            providers: BTreeMap::new(),
        }
    }
}

/// Catalog data for a single provider.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderCatalog {
    /// Stable provider id, for example `openai` or `bedrock`.
    pub provider_id: String,
    /// Human-readable provider name.
    pub display_name: String,
    /// Provider integration/API kind.
    pub kind: CatalogProviderKind,
    /// Provider homepage or documentation URL.
    #[serde(default)]
    pub website_url: Option<String>,
    /// Default interactive model id.
    #[serde(default)]
    pub default_model_id: Option<String>,
    /// Default Codex/subscription model id.
    #[serde(default)]
    pub default_codex_model_id: Option<String>,
    /// Bundled fallback model ids for providers without a live model-list API.
    #[serde(default)]
    pub fallback_model_ids: Vec<String>,
    /// Provider defaults used for discovered models without exact catalog metadata.
    #[serde(default)]
    pub defaults: Option<ModelCatalogDefaults>,
    /// Descriptive provider error-handling metadata.
    #[serde(default)]
    pub error_handling: ProviderErrorHandlingMetadata,
    /// Models keyed by provider-native model id.
    #[serde(default)]
    pub models: BTreeMap<String, ModelCatalogEntry>,
}

/// Descriptive provider error-handling metadata.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderErrorHandlingMetadata {
    /// Provider-declared recoverable error patterns.
    #[serde(default)]
    pub recoverable_error_patterns: Vec<RecoverableErrorPattern>,
}

/// Provider-declared recoverable error pattern.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPattern {
    /// Stable pattern identifier.
    pub id: String,
    /// Whether this pattern is enabled by default.
    #[serde(default)]
    pub enabled_by_default: bool,
    /// Provider/model scope for this pattern.
    #[serde(default)]
    pub scope: RecoverableErrorPatternScope,
    /// Error match conditions.
    #[serde(default)]
    pub r#match: RecoverableErrorPatternMatch,
}

/// Provider/model scope for a recoverable error pattern.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPatternScope {
    /// Exact provider plugin id scope.
    #[serde(default)]
    pub provider_plugin_id: Option<String>,
    /// Provider plugin id substring scope.
    #[serde(default)]
    pub provider_plugin_id_contains: Option<String>,
    /// Exact model id scope.
    #[serde(default)]
    pub model_id: Option<String>,
    /// Model id substring scope.
    #[serde(default)]
    pub model_id_contains: Option<String>,
}

/// Error match conditions for a recoverable error pattern.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoverableErrorPatternMatch {
    /// Provider error category to match.
    #[serde(default)]
    pub category: Option<String>,
    /// Provider error code to match exactly.
    #[serde(default)]
    pub code: Option<String>,
    /// Provider error message to match exactly.
    #[serde(default)]
    pub message_equals: Option<String>,
    /// Provider error message substring to match.
    #[serde(default)]
    pub message_contains: Option<String>,
    /// Provider-native error message to match exactly.
    #[serde(default)]
    pub provider_message_equals: Option<String>,
    /// Provider-native error message substring to match.
    #[serde(default)]
    pub provider_message_contains: Option<String>,
}

/// Default metadata for provider-discovered models without exact catalog entries.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogDefaults {
    /// Default context window in tokens.
    #[serde(default)]
    pub context_window: Option<u32>,
    /// Default maximum output tokens.
    #[serde(default)]
    pub max_output_tokens: Option<u32>,
    /// Default maximum base64-encoded bytes for one image input.
    #[serde(default)]
    pub max_image_input_base64_bytes: Option<u64>,
    /// Default capability metadata.
    #[serde(default)]
    pub capabilities: CatalogCapabilities,
    /// Default reasoning-specific metadata.
    #[serde(default)]
    pub reasoning: Option<CatalogReasoning>,
}

/// Provider integration/API kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogProviderKind {
    /// Provider uses an OpenAI-compatible API shape.
    OpenAiCompatible,
    /// Amazon Bedrock provider.
    Bedrock,
    /// Anthropic-native provider.
    Anthropic,
    /// Google-native provider.
    Google,
    /// OpenRouter-compatible aggregation provider.
    OpenRouter,
    /// Another provider kind.
    Other,
}

/// Catalog entry for one model.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelCatalogEntry {
    /// Provider-native model id.
    pub model_id: String,
    /// Human-readable model name.
    pub display_name: String,
    /// Supported aliases; exact model ids or glob-like patterns ending in `*`.
    #[serde(default)]
    pub aliases: BTreeSet<String>,
    /// Provider/public availability status.
    pub status: CatalogModelStatus,
    /// Bcode support status for this model.
    #[serde(default)]
    pub bcode_support: BcodeSupportStatus,
    /// Model context window in tokens.
    #[serde(default)]
    pub context_window: Option<u32>,
    /// Maximum output tokens.
    #[serde(default)]
    pub max_output_tokens: Option<u32>,
    /// Maximum base64-encoded bytes for one image input.
    #[serde(default)]
    pub max_image_input_base64_bytes: Option<u64>,
    /// Provider-specific model family name.
    #[serde(default)]
    pub family: Option<String>,
    /// Provider-specific model kind/classification.
    #[serde(default)]
    pub provider_model_kind: Option<String>,
    /// Replacement model id, when this model is deprecated.
    #[serde(default)]
    pub replaced_by: Option<String>,
    /// Human notes for this model.
    #[serde(default)]
    pub notes: Option<String>,
    /// Public documentation URL for this model.
    #[serde(default)]
    pub documentation_url: Option<String>,
    /// Pricing metadata.
    #[serde(default)]
    pub pricing: Option<CatalogPricing>,
    /// Capability metadata.
    #[serde(default)]
    pub capabilities: CatalogCapabilities,
    /// Reasoning-specific metadata.
    #[serde(default)]
    pub reasoning: Option<CatalogReasoning>,
    /// Provider API surface required to invoke this model.
    ///
    /// Some providers serve newer models only through a surface an integration cannot drive. When
    /// a model requires an unsupported surface, discovery marks it unsupported rather than
    /// offering it and failing at request time.
    #[serde(default)]
    pub api_surface: Option<CatalogApiSurface>,
    /// Provider-native extended-thinking control shape, when reasoning is supported.
    #[serde(default)]
    pub thinking_mode: Option<CatalogThinkingMode>,
    /// Compatibility targets this model is objectively known to support.
    ///
    /// Deprecated for newly-authored catalog data. Use [`Self::deployments`] when operational
    /// metadata differs by serving target.
    #[serde(default)]
    pub supported_by: BTreeSet<ModelSupportTarget>,
    /// Explicit serving targets removed by trusted definitive surface evidence.
    /// This does not change support on other deployments or global model status.
    #[serde(default)]
    pub unsupported_by: BTreeSet<ModelSupportTarget>,
    /// Serving deployments with target-specific operational metadata.
    #[serde(default)]
    pub deployments: Vec<ModelDeployment>,
    /// Live provider metadata overlaid from generated snapshots.
    #[serde(default)]
    pub live: Option<LiveModelMetadata>,
    /// Metadata source/verification data.
    #[serde(default)]
    pub source: CatalogSourceMetadata,
}

/// Operational metadata for one concrete model serving deployment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ModelDeployment {
    /// Provider/authentication/API-surface identity for this deployment.
    #[serde(flatten)]
    pub target: ModelSupportTarget,
    /// Effective input context window exposed by this deployment.
    #[serde(default)]
    pub context_window: Option<u32>,
    /// Maximum output tokens exposed by this deployment.
    #[serde(default)]
    pub max_output_tokens: Option<u32>,
    /// Deployment-specific capability overrides. Empty means use model-level capabilities.
    #[serde(default)]
    pub capabilities: CatalogCapabilities,
    /// Deployment-specific reasoning metadata.
    #[serde(default)]
    pub reasoning: Option<CatalogReasoning>,
    /// Deployment-specific pricing metadata.
    #[serde(default)]
    pub pricing: Option<CatalogPricing>,
}

/// Objective compatibility target for a model.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ModelSupportTarget {
    /// Provider domain, for example `openai`, `openrouter`, or `bedrock`.
    pub provider: String,
    /// Authentication mode required for this target, for example `api_key` or
    /// `chatgpt_subscription`.
    pub auth_mode: String,
    /// Provider API surface, for example `responses_api`, `chat_completions`, or
    /// `chatgpt_codex`.
    pub api_surface: String,
    /// Optional integration/runtime qualifier, for example `bcode` or `provider_native`.
    #[serde(default)]
    pub integration: Option<String>,
}

impl ModelSupportTarget {
    /// Construct a support target.
    #[must_use]
    pub fn new(
        provider: impl Into<String>,
        auth_mode: impl Into<String>,
        api_surface: impl Into<String>,
        integration: Option<impl Into<String>>,
    ) -> Self {
        Self {
            provider: provider.into(),
            auth_mode: auth_mode.into(),
            api_surface: api_surface.into(),
            integration: integration.map(Into::into),
        }
    }

    /// Return true when this target is compatible with the requested active target.
    #[must_use]
    pub fn matches(&self, target: &Self) -> bool {
        self.provider == target.provider
            && self.auth_mode == target.auth_mode
            && self.api_surface == target.api_surface
            && (self.integration == target.integration
                || self.integration.is_none()
                || self.integration.as_deref() == Some("provider_native"))
    }
}

/// Provider/public status for a catalog model.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogModelStatus {
    /// Generally available/stable.
    Stable,
    /// Preview/beta/experimental.
    Preview,
    /// Deprecated but still possibly callable.
    Deprecated,
    /// Unavailable or removed.
    Unavailable,
    /// Status is not known.
    Unknown,
}

/// Bcode support status for a catalog model.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BcodeSupportStatus {
    /// Bcode support is known and expected to work.
    Supported,
    /// Some Bcode features may not work.
    PartiallySupported,
    /// Bcode does not currently support this model.
    Unsupported,
    /// Support status has not been verified.
    #[default]
    Unknown,
}

/// Token pricing metadata.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogPricing {
    /// ISO 4217 currency code.
    pub currency: String,
    /// Unit the price applies to.
    pub unit: CatalogPricingUnit,
    /// Input token price in currency micros.
    #[serde(default)]
    pub input_micros: Option<u64>,
    /// Cached input token price in currency micros.
    #[serde(default)]
    pub cached_input_micros: Option<u64>,
    /// Cache write token price in currency micros.
    #[serde(default)]
    pub cache_write_input_micros: Option<u64>,
    /// Output token price in currency micros.
    #[serde(default)]
    pub output_micros: Option<u64>,
    /// Request-input threshold separating short and long context rules.
    #[serde(default)]
    pub context_threshold_tokens: Option<u64>,
    /// Stable source revision for this normalized price set.
    #[serde(default)]
    pub revision: Option<String>,
    /// Conditional pricing rules. Empty means the flat fields above are authoritative.
    #[serde(default)]
    pub rules: Vec<CatalogPricingRule>,
}

/// One conditional token-pricing rule owned by the model catalog.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CatalogPricingRule {
    /// Billed token category.
    pub bucket: CatalogPricingBucket,
    /// Optional modality restriction.
    #[serde(default)]
    pub modality: Option<CatalogTokenModality>,
    /// Optional effective service tier.
    #[serde(default)]
    pub service_tier: Option<String>,
    /// Optional invocation class.
    #[serde(default)]
    pub invocation_class: Option<CatalogInvocationClass>,
    /// Optional cache retention in seconds.
    #[serde(default)]
    pub cache_ttl_seconds: Option<u64>,
    /// Inclusive minimum complete request input size.
    #[serde(default)]
    pub min_request_input_tokens: Option<u64>,
    /// Inclusive maximum complete request input size.
    #[serde(default)]
    pub max_request_input_tokens: Option<u64>,
    /// Optional deployment/billing scope.
    #[serde(default)]
    pub billing_scope: Option<String>,
    /// Price in currency micros per configured pricing unit.
    pub price_micros: u64,
}

/// Catalog token-pricing category.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogPricingBucket {
    Input,
    CacheReadInput,
    CacheWriteInput,
    Output,
}

/// Catalog token modality.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogTokenModality {
    Text,
    Image,
    Audio,
    Video,
}

/// Catalog invocation class.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogInvocationClass {
    OnDemand,
    Batch,
}

/// Pricing unit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogPricingUnit {
    /// Price is per one million tokens.
    PerMillionTokens,
}

/// Model capability flags.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct CatalogCapabilities {
    /// Accepts text input.
    #[serde(default)]
    pub text_input: bool,
    /// Accepts image input.
    #[serde(default)]
    pub image_input: bool,
    /// Produces text output.
    #[serde(default)]
    pub text_output: bool,
    /// Supports tool/function calling.
    #[serde(default)]
    pub tool_use: bool,
    /// Supports multiple independent tool calls in one model response.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parallel_tool_calls: Option<bool>,
    /// Supports requiring some tool call (`tool_choice = required` / `any`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub required_tool_choice: Option<bool>,
    /// Supports requiring one named tool.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub named_tool_choice: Option<bool>,
    /// Supports structured output controls.
    #[serde(default)]
    pub structured_outputs: bool,
    /// Supports reasoning controls or reasoning models.
    #[serde(default)]
    pub reasoning: bool,
    /// Supports prompt/cache discounts or cache controls.
    #[serde(default)]
    pub prompt_cache: bool,
    /// Supports developer-selected explicit cache breakpoints.
    #[serde(default)]
    pub explicit_prompt_cache: bool,
    /// Supported prompt-cache TTL values in seconds.
    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
    pub prompt_cache_ttl_seconds: BTreeSet<u64>,
    /// Minimum stable prefix length, in tokens, before the provider caches a prefix.
    ///
    /// Shorter prefixes are accepted by the provider but never written to or read from its
    /// prompt cache, so hosts should not spend cache breakpoints on them.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt_cache_min_prefix_tokens: Option<u64>,
    /// Supports native provider web search.
    #[serde(default)]
    pub native_web_search: bool,
}

/// Provider API surface required to invoke a model.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogApiSurface {
    /// Amazon Bedrock `Converse` / `ConverseStream` API.
    Converse,
    /// Amazon Bedrock `InvokeModel` API.
    InvokeModel,
    /// Anthropic Messages API surface (`/anthropic/v1/messages`, "bedrock-mantle").
    Messages,
    /// `OpenAI` Responses API surface (for example `/openai/v1/responses`, "bedrock-mantle").
    Responses,
}

/// Provider-native extended-thinking control shape.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogThinkingMode {
    /// Explicit token budget (`thinking.type = "enabled"`, `budget_tokens = N`).
    Budget,
    /// Adaptive thinking (`thinking.type = "adaptive"`); budget is not accepted.
    Adaptive,
}

/// Reasoning-specific metadata.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogReasoning {
    /// Supported effort values.
    #[serde(default)]
    pub effort_values: BTreeSet<String>,
    /// Default effort value.
    #[serde(default)]
    pub default_effort: Option<String>,
    /// Supported summary values.
    #[serde(default)]
    pub summary_values: BTreeSet<String>,
    /// Default summary value.
    #[serde(default)]
    pub default_summary: Option<String>,
    /// Raw provider reasoning text can be requested.
    #[serde(default)]
    pub raw_reasoning_supported: bool,
}

/// Source and verification metadata for a catalog model.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogSourceMetadata {
    /// Pricing source identifier or URL.
    #[serde(default)]
    pub pricing: Option<String>,
    /// Metadata source identifier or URL.
    #[serde(default)]
    pub metadata: Option<String>,
    /// Verification timestamp, encoded as RFC 3339 text.
    #[serde(default)]
    pub last_verified_at: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deployment_round_trip_preserves_target_specific_limits() {
        let deployment = ModelDeployment {
            target: ModelSupportTarget::new(
                "openai",
                "chatgpt_subscription",
                "chatgpt_codex",
                Some("bcode"),
            ),
            context_window: Some(372_000),
            max_output_tokens: Some(128_000),
            capabilities: CatalogCapabilities::default(),
            reasoning: None,
            pricing: None,
        };

        let value = serde_json::to_value(&deployment).expect("serialize deployment");
        let decoded: ModelDeployment =
            serde_json::from_value(value).expect("deserialize deployment");
        assert_eq!(decoded, deployment);
    }

    #[test]
    fn generic_integration_target_matches_specific_runtime_integration() {
        let generic = ModelSupportTarget::new("openai", "api_key", "responses_api", None::<String>);
        let runtime = ModelSupportTarget::new("openai", "api_key", "responses_api", Some("bcode"));

        assert!(generic.matches(&runtime));
    }

    #[test]
    fn api_surface_round_trips_every_known_wire_value() {
        for (surface, wire) in [
            (CatalogApiSurface::Converse, "converse"),
            (CatalogApiSurface::InvokeModel, "invoke_model"),
            (CatalogApiSurface::Messages, "messages"),
            (CatalogApiSurface::Responses, "responses"),
        ] {
            let encoded = serde_json::to_value(surface).expect("serialize api surface");
            assert_eq!(encoded, serde_json::Value::String(wire.to_string()));
            let decoded: CatalogApiSurface =
                serde_json::from_value(encoded).expect("deserialize api surface");
            assert_eq!(decoded, surface);
        }
    }

    #[test]
    fn unknown_api_surface_is_rejected_rather_than_coerced() {
        // Unknown future surfaces must fail closed instead of silently decoding as a known
        // older variant.
        let error = serde_json::from_value::<CatalogApiSurface>(serde_json::Value::String(
            "some_future_surface".to_string(),
        ))
        .expect_err("unknown api surface must be rejected");
        assert!(
            error.to_string().contains("unknown variant"),
            "unexpected error: {error}"
        );
    }

    #[test]
    fn different_api_surfaces_do_not_match() {
        let responses =
            ModelSupportTarget::new("openai", "api_key", "responses_api", Some("bcode"));
        let codex = ModelSupportTarget::new(
            "openai",
            "chatgpt_subscription",
            "chatgpt_codex",
            Some("bcode"),
        );

        assert!(!responses.matches(&codex));
    }
}