| 1 | //! Signed payload shape for cloud facts (`schema_version` 1). |
| 2 | //! |
| 3 | //! Every section is optional and defaulted so a payload that only carries |
| 4 | //! release truth still parses. Unknown fields are preserved in `unknown` so |
| 5 | //! `/status` debugging can show what a newer server sent, but they are never |
| 6 | //! acted on. |
| 7 | |
| 8 | use std::collections::BTreeMap; |
| 9 | |
| 10 | use serde::{Deserialize, Serialize}; |
| 11 | use serde_json::Value; |
| 12 | |
| 13 | fn default_applies_to() -> String { |
| 14 | "*".to_string() |
| 15 | } |
| 16 | |
| 17 | /// The signed facts payload. |
| 18 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 19 | pub struct CloudFacts { |
| 20 | pub schema_version: u32, |
| 21 | pub channel: String, |
| 22 | pub facts_version: u64, |
| 23 | #[serde(default)] |
| 24 | pub published_at: String, |
| 25 | /// RFC 3339 UTC timestamp after which the facts are considered stale. |
| 26 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 27 | pub not_after: Option<String>, |
| 28 | /// Cargo-style semver requirement the whole payload applies to. |
| 29 | #[serde(default = "default_applies_to")] |
| 30 | pub applies_to: String, |
| 31 | #[serde(default)] |
| 32 | pub models: Vec<ModelFact>, |
| 33 | #[serde(default)] |
| 34 | pub provider_defaults: BTreeMap<String, ProviderDefaultFact>, |
| 35 | #[serde(default)] |
| 36 | pub release: Option<ReleaseFact>, |
| 37 | #[serde(default)] |
| 38 | pub announcements: Vec<Announcement>, |
| 39 | #[serde(flatten)] |
| 40 | pub unknown: BTreeMap<String, Value>, |
| 41 | } |
| 42 | |
| 43 | /// What a model patch does to the catalog row it names. |
| 44 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] |
| 45 | #[serde(rename_all = "snake_case")] |
| 46 | pub enum ModelOp { |
| 47 | /// Patch the fields that are set; create the row when enough is known. |
| 48 | #[default] |
| 49 | Upsert, |
| 50 | /// Annotate as deprecated; never removes the row. |
| 51 | Deprecate, |
| 52 | /// Remove the row, but only when it came from the bundled/models.dev layers. |
| 53 | Hide, |
| 54 | } |
| 55 | |
| 56 | /// Per-million-token pricing patch. |
| 57 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 58 | pub struct PricingFact { |
| 59 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 60 | pub input_per_m: Option<f64>, |
| 61 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 62 | pub output_per_m: Option<f64>, |
| 63 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 64 | pub cache_read_per_m: Option<f64>, |
| 65 | } |
| 66 | |
| 67 | fn is_false(value: &bool) -> bool { |
| 68 | !*value |
| 69 | } |
| 70 | |
| 71 | /// A field-level patch to one `(provider, wire id)` catalog row. |
| 72 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 73 | pub struct ModelFact { |
| 74 | pub provider: String, |
| 75 | pub id: String, |
| 76 | #[serde(default)] |
| 77 | pub op: ModelOp, |
| 78 | /// Signed assertion that this exact id is available on the provider's |
| 79 | /// official endpoint even when the provider's own `/v1/models` roster does |
| 80 | /// not list it. |
| 81 | /// |
| 82 | /// Absent/false (the default, and what every older client sees) means the |
| 83 | /// roster stays authoritative for its own omissions. The assertion is only |
| 84 | /// honored on an `Upsert` inside a payload that carries `not_after`, so it |
| 85 | /// always expires on its own; see [`super::scope::scoped_view`]. It grants |
| 86 | /// nothing else: identity, endpoint, account-entitlement and user |
| 87 | /// precedence rules are unchanged. |
| 88 | #[serde(default, skip_serializing_if = "is_false")] |
| 89 | pub allow_unlisted: bool, |
| 90 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 91 | pub context_window: Option<u64>, |
| 92 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 93 | pub max_output: Option<u64>, |
| 94 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 95 | pub pricing: Option<PricingFact>, |
| 96 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 97 | pub reasoning: Option<bool>, |
| 98 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 99 | pub display_name: Option<String>, |
| 100 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 101 | pub deprecated_at: Option<String>, |
| 102 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 103 | pub replacement: Option<String>, |
| 104 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 105 | pub note: Option<String>, |
| 106 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 107 | pub applies_to: Option<String>, |
| 108 | } |
| 109 | |
| 110 | /// Provider default overrides. Only consulted when config.toml sets nothing. |
| 111 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 112 | pub struct ProviderDefaultFact { |
| 113 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 114 | pub default_model: Option<String>, |
| 115 | /// Accepted only when `https` and on the provider's official host family. |
| 116 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 117 | pub base_url: Option<String>, |
| 118 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 119 | pub applies_to: Option<String>, |
| 120 | } |
| 121 | |
| 122 | /// Release truth: what the newest install is and which versions are yanked. |
| 123 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 124 | pub struct ReleaseFact { |
| 125 | pub latest: String, |
| 126 | #[serde(default)] |
| 127 | pub yanked: Vec<String>, |
| 128 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 129 | pub min_supported: Option<String>, |
| 130 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 131 | pub notice: Option<String>, |
| 132 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 133 | pub release_url: Option<String>, |
| 134 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 135 | pub applies_to: Option<String>, |
| 136 | } |
| 137 | |
| 138 | /// Announcement severity. There is deliberately no `Critical`. |
| 139 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] |
| 140 | #[serde(rename_all = "snake_case")] |
| 141 | pub enum AnnouncementLevel { |
| 142 | #[default] |
| 143 | Info, |
| 144 | Warn, |
| 145 | } |
| 146 | |
| 147 | /// Where an announcement may be shown. |
| 148 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| 149 | #[serde(rename_all = "snake_case")] |
| 150 | pub enum Surface { |
| 151 | Tui, |
| 152 | Desktop, |
| 153 | Web, |
| 154 | } |
| 155 | |
| 156 | /// A one-line banner. Text only; no actions, no executable content. |
| 157 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] |
| 158 | pub struct Announcement { |
| 159 | pub id: String, |
| 160 | #[serde(default)] |
| 161 | pub level: AnnouncementLevel, |
| 162 | pub text: String, |
| 163 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 164 | pub url: Option<String>, |
| 165 | #[serde(default)] |
| 166 | pub surfaces: Vec<Surface>, |
| 167 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 168 | pub applies_to: Option<String>, |
| 169 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 170 | pub starts_at: Option<String>, |
| 171 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 172 | pub expires_at: Option<String>, |
| 173 | } |
| 174 | |
| 175 | /// Maximum announcement text length accepted by the client. |
| 176 | pub const MAX_ANNOUNCEMENT_CHARS: usize = 200; |
| 177 |