返回 CodeWhale
catalog_patch.rs
根目录 / crates / config / src / cloud_facts / catalog_patch.rs
1 //! Apply cloud model patches to a catalog layer map (layer 15: above bundled
2 //! and live models.dev, below provider `/v1/models`, config, and user rows).
3 //!
4 //! Patch semantics:
5 //! - `Upsert`: only the fields the patch sets shadow the row; a patch for a
6 //! row that does not exist is materialized only when it carries a context
7 //! window or an explicit `allow_unlisted` assertion (otherwise skipped with
8 //! a receipt). An attested id-only row keeps every unstated fact unknown.
9 //! - `Deprecate`: annotates (the note is carried in `reasoning_options` as a
10 //! `{"cloud_facts": {...}}` marker); never removes.
11 //! - `Hide`: removes the row only when it came from the bundled or live
12 //! models.dev layers. Provider-live/config/user rows are never hidden.
13 //!
14 //! [`complete_provider_live_row`] is the one seam above this layer: it fills
15 //! fields a provider-owned row leaves unknown without displacing anything the
16 //! provider actually said.
17
18 use std::collections::BTreeMap;
19
20 use serde_json::json;
21
22 use super::scope::ScopedFacts;
23 use super::types::{ModelFact, ModelOp};
24 use crate::catalog::{CatalogOffering, CatalogSource};
25 use crate::models_dev::ModelsDevCost;
26
27 /// Merge key used by the catalog compiler.
28 type Key = (String, String);
29
30 /// Receipt for one patch that changed nothing.
31 #[derive(Debug, Clone, PartialEq, Eq)]
32 pub struct SkippedPatch {
33 pub provider: String,
34 pub id: String,
35 pub reason: String,
36 }
37
38 /// Apply every model patch in `facts` to `rows`, returning skip receipts.
39 pub fn apply_model_patches(
40 rows: &mut BTreeMap<Key, CatalogOffering>,
41 facts: &ScopedFacts,
42 fetched_at: u64,
43 ) -> Vec<SkippedPatch> {
44 if !facts.is_current_at(crate::catalog::now_unix()) {
45 return Vec::new();
46 }
47 let source = CatalogSource::CloudFacts {
48 facts_version: facts.facts_version,
49 key_id: facts.key_id.clone(),
50 fetched_at,
51 valid_until: facts.valid_until,
52 };
53 apply_patches(rows, &facts.models, &source, true)
54 }
55
56 /// Apply field patches under `source`.
57 ///
58 /// Shared by the signed cloud layer and Codewhale's bundled corrections
59 /// ([`crate::catalog::corrections`]), so both correct a row the same way.
60 /// `materialize` lets an `Upsert` create a row that no lower layer listed;
61 /// corrections pass `false` because they only ever fix rows that exist.
62 pub(crate) fn apply_patches(
63 rows: &mut BTreeMap<Key, CatalogOffering>,
64 patches: &[ModelFact],
65 source: &CatalogSource,
66 materialize: bool,
67 ) -> Vec<SkippedPatch> {
68 let mut skipped = Vec::new();
69 for patch in patches {
70 let key = (patch.provider.clone(), patch.id.clone());
71 if rows.get(&key).is_some_and(|row| {
72 !matches!(
73 row.source,
74 CatalogSource::Bundled
75 | CatalogSource::CodewhaleBundled { .. }
76 | CatalogSource::ModelsDevLive { .. }
77 | CatalogSource::CloudFacts { .. }
78 )
79 }) {
80 skipped.push(SkippedPatch {
81 provider: patch.provider.clone(),
82 id: patch.id.clone(),
83 reason: "patch ignored: row comes from a higher layer".into(),
84 });
85 continue;
86 }
87 match patch.op {
88 ModelOp::Hide => match rows.get(&key) {
89 Some(row)
90 if matches!(
91 row.source,
92 CatalogSource::Bundled
93 | CatalogSource::CodewhaleBundled { .. }
94 | CatalogSource::ModelsDevLive { .. }
95 ) =>
96 {
97 rows.remove(&key);
98 }
99 Some(_) => skipped.push(SkippedPatch {
100 provider: patch.provider.clone(),
101 id: patch.id.clone(),
102 reason: "hide ignored: row comes from a higher layer".into(),
103 }),
104 None => skipped.push(SkippedPatch {
105 provider: patch.provider.clone(),
106 id: patch.id.clone(),
107 reason: "hide ignored: no such row".into(),
108 }),
109 },
110 ModelOp::Deprecate => match rows.get_mut(&key) {
111 Some(row) => {
112 annotate(row, patch, "deprecated");
113 }
114 None => skipped.push(SkippedPatch {
115 provider: patch.provider.clone(),
116 id: patch.id.clone(),
117 reason: "deprecate ignored: no such row".into(),
118 }),
119 },
120 ModelOp::Upsert => {
121 if let Some(row) = rows.get_mut(&key) {
122 // A capability patch must not refresh or relabel inherited prices.
123 let inherited_price_source = row.pricing_source().clone();
124 // Patches never restate modalities, so their authority
125 // stays with the layer that did.
126 row.modalities_source = Some(row.modalities_source().clone());
127 patch_fields(row, patch);
128 row.cost_source = Some(if patch_sets_price(patch) {
129 source.clone()
130 } else {
131 inherited_price_source
132 });
133 row.source = source.clone();
134 } else if materialize && (patch.context_window.is_some() || patch.allow_unlisted) {
135 // A row materializes when the payload says enough to be
136 // worth a row: a context window, or an explicit unlisted
137 // assertion that this id exists. An id-only attested row is
138 // deliberately bare — every limit, price and capability
139 // stays `None` (unknown) rather than being borrowed from a
140 // sibling model or a lower stale layer.
141 let mut row = CatalogOffering {
142 provider: patch.provider.clone(),
143 wire_model_id: patch.id.clone(),
144 endpoint_key: "chat".to_string(),
145 source: source.clone(),
146 ..CatalogOffering::default()
147 };
148 patch_fields(&mut row, patch);
149 if patch_sets_price(patch) {
150 row.cost_source = Some(source.clone());
151 }
152 rows.insert(key, row);
153 } else {
154 skipped.push(SkippedPatch {
155 provider: patch.provider.clone(),
156 id: patch.id.clone(),
157 reason: if materialize {
158 "upsert ignored: new row needs context_window or allow_unlisted"
159 } else {
160 "upsert ignored: no such row"
161 }
162 .into(),
163 });
164 }
165 }
166 }
167 }
168 skipped
169 }
170
171 /// Whether a patch owns the row's price: a new price block, or a withheld one.
172 fn patch_sets_price(patch: &ModelFact) -> bool {
173 patch.pricing.is_some() || patch.pricing_withheld.is_some()
174 }
175
176 /// Does this payload explicitly assert `(provider, id)` exists even when the
177 /// provider's own roster omits it?
178 ///
179 /// This is the *only* thing that may override a roster's omission. The client
180 /// keeps no history of past rosters, so it cannot tell a never-listed preview
181 /// from a retired model by itself — and must not guess. Absent the assertion,
182 /// the roster stays authoritative for every id it does and does not list.
183 ///
184 /// `facts` must be a scoped view ([`super::scope::scoped_view`]), which is
185 /// where the assertion is restricted to an `Upsert` in a payload that expires.
186 #[must_use]
187 pub fn is_unlisted_attested(facts: &ScopedFacts, provider: &str, id: &str) -> bool {
188 facts.models.iter().any(|patch| {
189 patch.allow_unlisted
190 && patch.op == ModelOp::Upsert
191 && patch.provider == provider
192 && patch.id == id
193 })
194 }
195
196 /// Fill fields a provider-owned live row does not state with signed values.
197 ///
198 /// A `/v1/models` roster that answers with ids alone has not said "context and
199 /// reasoning support are unknown" — it has said nothing about them. Layer
200 /// precedence still holds where the two disagree: this only writes fields the
201 /// provider row leaves `None`, and it never changes the row's own `source`,
202 /// which stays provider-live. Returns whether anything was filled.
203 ///
204 /// Deliberately excluded:
205 /// - Rows from any other layer. Bundled/Models.dev rows are patched by
206 /// [`apply_model_patches`]; config and user rows are the user's authority.
207 /// - Pricing. A filled price would have to carry a `CloudFacts` price source on
208 /// a provider-live row, and `fresh_dispatch_pricing_quote_at` admits a cloud
209 /// quote only when the whole row is `CloudFacts` — so the price would render
210 /// without being billable. Cloud prices therefore keep applying only where no
211 /// fresh roster owns the row.
212 /// - Capabilities the payload has no field for. Nothing is inferred.
213 pub fn complete_provider_live_row(row: &mut CatalogOffering, facts: &ScopedFacts) -> bool {
214 if !matches!(row.source, CatalogSource::Live { .. })
215 || !facts.is_current_at(crate::catalog::now_unix())
216 {
217 return false;
218 }
219 let Some(patch) = facts.models.iter().find(|patch| {
220 patch.op == ModelOp::Upsert
221 && patch.provider == row.provider
222 && patch.id == row.wire_model_id
223 }) else {
224 return false;
225 };
226 let mut filled = false;
227 let mut limit = row.limit.clone().unwrap_or_default();
228 if limit.context.is_none()
229 && let Some(context) = patch.context_window
230 {
231 limit.context = Some(context);
232 filled = true;
233 }
234 if limit.output.is_none()
235 && let Some(output) = patch.max_output
236 {
237 limit.output = Some(output);
238 filled = true;
239 }
240 if filled {
241 row.limit = Some(limit);
242 }
243 if row.reasoning.is_none()
244 && let Some(reasoning) = patch.reasoning
245 {
246 row.reasoning = Some(reasoning);
247 filled = true;
248 }
249 filled
250 }
251
252 fn patch_fields(row: &mut CatalogOffering, patch: &ModelFact) {
253 if patch.context_window.is_some() || patch.max_output.is_some() {
254 let mut limit = row.limit.clone().unwrap_or_default();
255 if let Some(context) = patch.context_window {
256 limit.context = Some(context);
257 }
258 if let Some(output) = patch.max_output {
259 limit.output = Some(output);
260 }
261 row.limit = Some(limit);
262 }
263 if patch.pricing_withheld.is_some() {
264 // No flat rate is honest for this row; unknown beats misleading.
265 row.cost = None;
266 } else if let Some(pricing) = &patch.pricing {
267 // A price block has one authority. Missing classes stay unknown instead
268 // of silently mixing an old row's prices with newly signed rates.
269 row.cost = Some(ModelsDevCost {
270 input: pricing.input_per_m,
271 output: pricing.output_per_m,
272 cache_read: pricing.cache_read_per_m,
273 cache_write: None,
274 });
275 }
276 if patch.reasoning.is_some() {
277 row.reasoning = patch.reasoning;
278 }
279 if let Some(options) = &patch.reasoning_options {
280 // Keep this layer's own annotations; replace only the controls.
281 let markers = row
282 .reasoning_options
283 .drain(..)
284 .filter(|value| value.get("cloud_facts").is_some())
285 .collect::<Vec<_>>();
286 row.reasoning_options = options.clone();
287 row.reasoning_options.extend(markers);
288 }
289 if patch.display_name.is_some() || patch.note.is_some() {
290 annotate(row, patch, "upsert");
291 }
292 }
293
294 fn annotate(row: &mut CatalogOffering, patch: &ModelFact, kind: &str) {
295 row.reasoning_options
296 .retain(|value| value.get("cloud_facts").is_none());
297 row.reasoning_options.push(json!({
298 "cloud_facts": {
299 "op": kind,
300 "display_name": patch.display_name,
301 "deprecated_at": patch.deprecated_at,
302 "replacement": patch.replacement,
303 "note": patch.note,
304 }
305 }));
306 }
307
307 lines RUST