返回 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 let mut skipped = Vec::new();
45 if !facts.is_current_at(crate::catalog::now_unix()) {
46 return skipped;
47 }
48 let source = CatalogSource::CloudFacts {
49 facts_version: facts.facts_version,
50 key_id: facts.key_id.clone(),
51 fetched_at,
52 valid_until: facts.valid_until,
53 };
54 for patch in &facts.models {
55 let key = (patch.provider.clone(), patch.id.clone());
56 if rows.get(&key).is_some_and(|row| {
57 !matches!(
58 row.source,
59 CatalogSource::Bundled
60 | CatalogSource::CodewhaleBundled { .. }
61 | CatalogSource::ModelsDevLive { .. }
62 | CatalogSource::CloudFacts { .. }
63 )
64 }) {
65 skipped.push(SkippedPatch {
66 provider: patch.provider.clone(),
67 id: patch.id.clone(),
68 reason: "patch ignored: row comes from a higher layer".into(),
69 });
70 continue;
71 }
72 match patch.op {
73 ModelOp::Hide => match rows.get(&key) {
74 Some(row)
75 if matches!(
76 row.source,
77 CatalogSource::Bundled
78 | CatalogSource::CodewhaleBundled { .. }
79 | CatalogSource::ModelsDevLive { .. }
80 ) =>
81 {
82 rows.remove(&key);
83 }
84 Some(_) => skipped.push(SkippedPatch {
85 provider: patch.provider.clone(),
86 id: patch.id.clone(),
87 reason: "hide ignored: row comes from a higher layer".into(),
88 }),
89 None => skipped.push(SkippedPatch {
90 provider: patch.provider.clone(),
91 id: patch.id.clone(),
92 reason: "hide ignored: no such row".into(),
93 }),
94 },
95 ModelOp::Deprecate => match rows.get_mut(&key) {
96 Some(row) => {
97 annotate(row, patch, "deprecated");
98 }
99 None => skipped.push(SkippedPatch {
100 provider: patch.provider.clone(),
101 id: patch.id.clone(),
102 reason: "deprecate ignored: no such row".into(),
103 }),
104 },
105 ModelOp::Upsert => {
106 if let Some(row) = rows.get_mut(&key) {
107 // A capability patch must not refresh or relabel inherited prices.
108 let inherited_price_source = row.pricing_source().clone();
109 patch_fields(row, patch);
110 row.cost_source = Some(if patch.pricing.is_some() {
111 source.clone()
112 } else {
113 inherited_price_source
114 });
115 row.source = source.clone();
116 } else if patch.context_window.is_some() || patch.allow_unlisted {
117 // A row materializes when the payload says enough to be
118 // worth a row: a context window, or an explicit unlisted
119 // assertion that this id exists. An id-only attested row is
120 // deliberately bare — every limit, price and capability
121 // stays `None` (unknown) rather than being borrowed from a
122 // sibling model or a lower stale layer.
123 let mut row = CatalogOffering {
124 provider: patch.provider.clone(),
125 wire_model_id: patch.id.clone(),
126 endpoint_key: "chat".to_string(),
127 source: source.clone(),
128 ..CatalogOffering::default()
129 };
130 patch_fields(&mut row, patch);
131 if patch.pricing.is_some() {
132 row.cost_source = Some(source.clone());
133 }
134 rows.insert(key, row);
135 } else {
136 skipped.push(SkippedPatch {
137 provider: patch.provider.clone(),
138 id: patch.id.clone(),
139 reason: "upsert ignored: new row needs context_window or allow_unlisted"
140 .into(),
141 });
142 }
143 }
144 }
145 }
146 skipped
147 }
148
149 /// Does this payload explicitly assert `(provider, id)` exists even when the
150 /// provider's own roster omits it?
151 ///
152 /// This is the *only* thing that may override a roster's omission. The client
153 /// keeps no history of past rosters, so it cannot tell a never-listed preview
154 /// from a retired model by itself — and must not guess. Absent the assertion,
155 /// the roster stays authoritative for every id it does and does not list.
156 ///
157 /// `facts` must be a scoped view ([`super::scope::scoped_view`]), which is
158 /// where the assertion is restricted to an `Upsert` in a payload that expires.
159 #[must_use]
160 pub fn is_unlisted_attested(facts: &ScopedFacts, provider: &str, id: &str) -> bool {
161 facts.models.iter().any(|patch| {
162 patch.allow_unlisted
163 && patch.op == ModelOp::Upsert
164 && patch.provider == provider
165 && patch.id == id
166 })
167 }
168
169 /// Fill fields a provider-owned live row does not state with signed values.
170 ///
171 /// A `/v1/models` roster that answers with ids alone has not said "context and
172 /// reasoning support are unknown" — it has said nothing about them. Layer
173 /// precedence still holds where the two disagree: this only writes fields the
174 /// provider row leaves `None`, and it never changes the row's own `source`,
175 /// which stays provider-live. Returns whether anything was filled.
176 ///
177 /// Deliberately excluded:
178 /// - Rows from any other layer. Bundled/Models.dev rows are patched by
179 /// [`apply_model_patches`]; config and user rows are the user's authority.
180 /// - Pricing. A filled price would have to carry a `CloudFacts` price source on
181 /// a provider-live row, and `fresh_dispatch_pricing_quote_at` admits a cloud
182 /// quote only when the whole row is `CloudFacts` — so the price would render
183 /// without being billable. Cloud prices therefore keep applying only where no
184 /// fresh roster owns the row.
185 /// - Capabilities the payload has no field for. Nothing is inferred.
186 pub fn complete_provider_live_row(row: &mut CatalogOffering, facts: &ScopedFacts) -> bool {
187 if !matches!(row.source, CatalogSource::Live { .. })
188 || !facts.is_current_at(crate::catalog::now_unix())
189 {
190 return false;
191 }
192 let Some(patch) = facts.models.iter().find(|patch| {
193 patch.op == ModelOp::Upsert
194 && patch.provider == row.provider
195 && patch.id == row.wire_model_id
196 }) else {
197 return false;
198 };
199 let mut filled = false;
200 let mut limit = row.limit.clone().unwrap_or_default();
201 if limit.context.is_none()
202 && let Some(context) = patch.context_window
203 {
204 limit.context = Some(context);
205 filled = true;
206 }
207 if limit.output.is_none()
208 && let Some(output) = patch.max_output
209 {
210 limit.output = Some(output);
211 filled = true;
212 }
213 if filled {
214 row.limit = Some(limit);
215 }
216 if row.reasoning.is_none()
217 && let Some(reasoning) = patch.reasoning
218 {
219 row.reasoning = Some(reasoning);
220 filled = true;
221 }
222 filled
223 }
224
225 fn patch_fields(row: &mut CatalogOffering, patch: &ModelFact) {
226 if patch.context_window.is_some() || patch.max_output.is_some() {
227 let mut limit = row.limit.clone().unwrap_or_default();
228 if let Some(context) = patch.context_window {
229 limit.context = Some(context);
230 }
231 if let Some(output) = patch.max_output {
232 limit.output = Some(output);
233 }
234 row.limit = Some(limit);
235 }
236 if let Some(pricing) = &patch.pricing {
237 // A price block has one authority. Missing classes stay unknown instead
238 // of silently mixing an old row's prices with newly signed rates.
239 row.cost = Some(ModelsDevCost {
240 input: pricing.input_per_m,
241 output: pricing.output_per_m,
242 cache_read: pricing.cache_read_per_m,
243 cache_write: None,
244 });
245 }
246 if patch.reasoning.is_some() {
247 row.reasoning = patch.reasoning;
248 }
249 if patch.display_name.is_some() || patch.note.is_some() {
250 annotate(row, patch, "upsert");
251 }
252 }
253
254 fn annotate(row: &mut CatalogOffering, patch: &ModelFact, kind: &str) {
255 row.reasoning_options
256 .retain(|value| value.get("cloud_facts").is_none());
257 row.reasoning_options.push(json!({
258 "cloud_facts": {
259 "op": kind,
260 "display_name": patch.display_name,
261 "deprecated_at": patch.deprecated_at,
262 "replacement": patch.replacement,
263 "note": patch.note,
264 }
265 }));
266 }
267
267 lines RUST