1use ferrum_types::AttentionExecutionPolicy;
2use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor};
3use serde::{Deserialize, Deserializer, Serialize};
4use sha2::{Digest, Sha256};
5use std::cmp::Ordering;
6use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8use std::io::{self, Write};
9
10use super::model::PreparedModelFamilyWire;
11use super::{
12 AdmissionFitPolicy, CapabilityCatalog, CompletionRetentionSpec, ContractVersion,
13 DeviceDescriptor, DynamicStorageProfile, ExecutablePlanView, ExecutionDeterminismRequirement,
14 ExecutionPlan, ModelFamilyRegistry, PlanNodeResolution, PreparedModelFamily, ProviderId,
15 ReusableExecutionPolicy, RuntimePolicy, SpecialTokenRole, TokenizerDescriptor,
16 UnvalidatedExecutionPlan, UnvalidatedExecutionPlanWire, UnvalidatedPreparedModelFamily,
17 VNextError,
18};
19
20pub const MAX_RESOLUTION_SOURCE_BYTES: usize = 32 * 1024 * 1024;
22pub const MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES: usize = 16 * 1024 * 1024;
24pub const MAX_RESOLUTION_JSON_DEPTH: usize = 128;
26pub const MAX_RESOLUTION_JSON_NODES: usize = 1_000_000;
28pub const MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES: usize = MAX_RESOLUTION_SOURCE_BYTES;
30pub const MAX_RESOLUTION_PROVENANCE_BYTES: usize = 4 * 1024;
32pub const MAX_RESOLUTION_FIELD_PATHS: usize = 4_096;
34pub const MAX_RESOLUTION_FIELD_PATH_BYTES: usize = 512;
36pub const MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES: usize = 1024 * 1024;
38
39fn is_canonical_sha256(value: &str) -> bool {
40 value.len() == 64
41 && value
42 .bytes()
43 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
44}
45
46fn canonical_fingerprint<T: Serialize + ?Sized>(
47 value: &T,
48 context: &'static str,
49) -> Result<String, VNextError> {
50 let value = serde_json::to_value(value).map_err(|error| VNextError::Serialization {
51 context,
52 message: error.to_string(),
53 })?;
54 canonical_value_fingerprint(&canonicalize_json(value), context)
55}
56
57struct Sha256Writer(Sha256);
58
59impl Write for Sha256Writer {
60 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
61 self.0.update(bytes);
62 Ok(bytes.len())
63 }
64
65 fn flush(&mut self) -> io::Result<()> {
66 Ok(())
67 }
68}
69
70fn canonical_value_fingerprint(
71 value: &serde_json::Value,
72 context: &'static str,
73) -> Result<String, VNextError> {
74 let mut writer = Sha256Writer(Sha256::new());
75 serde_json::to_writer(&mut writer, value).map_err(|error| VNextError::Serialization {
76 context,
77 message: error.to_string(),
78 })?;
79 Ok(format!("{:x}", writer.0.finalize()))
80}
81
82fn canonical_json_value<T: Serialize + ?Sized>(
83 value: &T,
84 context: &'static str,
85) -> Result<serde_json::Value, VNextError> {
86 serde_json::to_value(value)
87 .map(canonicalize_json)
88 .map_err(|error| VNextError::Serialization {
89 context,
90 message: error.to_string(),
91 })
92}
93
94fn canonicalize_json(value: serde_json::Value) -> serde_json::Value {
95 match value {
96 serde_json::Value::Array(values) => {
97 serde_json::Value::Array(values.into_iter().map(canonicalize_json).collect())
98 }
99 serde_json::Value::Object(values) => serde_json::Value::Object(
100 values
101 .into_iter()
102 .map(|(key, value)| (key, canonicalize_json(value)))
103 .collect::<BTreeMap<_, _>>()
104 .into_iter()
105 .collect(),
106 ),
107 value => value,
108 }
109}
110
111fn invalid_plan(field: impl Into<String>, reason: impl Into<String>) -> VNextError {
112 VNextError::InvalidResolvedModelPlan {
113 field: field.into(),
114 reason: reason.into(),
115 }
116}
117
118fn validate_resolution_source_bytes(source_bytes: &[u8]) -> Result<(), VNextError> {
119 if source_bytes.is_empty() || source_bytes.len() > MAX_RESOLUTION_SOURCE_BYTES {
120 return Err(invalid_plan(
121 "resolution_source_evidence.source_bytes",
122 format!("must contain between 1 and {MAX_RESOLUTION_SOURCE_BYTES} bytes"),
123 ));
124 }
125 Ok(())
126}
127
128#[derive(Clone, Copy)]
129struct ResolutionJsonBudget {
130 maximum_depth: usize,
131 maximum_nodes: usize,
132 maximum_key_and_string_bytes: usize,
133}
134
135impl ResolutionJsonBudget {
136 const SOURCE: Self = Self {
137 maximum_depth: MAX_RESOLUTION_JSON_DEPTH,
138 maximum_nodes: MAX_RESOLUTION_JSON_NODES,
139 maximum_key_and_string_bytes: MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES,
140 };
141}
142
143struct ResolutionJsonPreflight {
144 budget: ResolutionJsonBudget,
145 nodes: usize,
146 key_and_string_bytes: usize,
147 violation: Option<VNextError>,
148}
149
150impl ResolutionJsonPreflight {
151 fn new(budget: ResolutionJsonBudget) -> Self {
152 Self {
153 budget,
154 nodes: 0,
155 key_and_string_bytes: 0,
156 violation: None,
157 }
158 }
159
160 fn fail<E: serde::de::Error>(&mut self, error: VNextError) -> Result<(), E> {
161 self.violation = Some(error);
162 Err(E::custom(
163 "resolution source JSON exceeds its structural budget",
164 ))
165 }
166
167 fn account_node<E: serde::de::Error>(&mut self, depth: usize) -> Result<(), E> {
168 if depth > self.budget.maximum_depth {
169 return self.fail(invalid_plan(
170 "resolution_source_evidence.document.depth",
171 format!("must not exceed {}", self.budget.maximum_depth),
172 ));
173 }
174 let Some(nodes) = self.nodes.checked_add(1) else {
175 return self.fail(invalid_plan(
176 "resolution_source_evidence.document.nodes",
177 "node count overflowed",
178 ));
179 };
180 if nodes > self.budget.maximum_nodes {
181 return self.fail(invalid_plan(
182 "resolution_source_evidence.document.nodes",
183 format!("must not exceed {}", self.budget.maximum_nodes),
184 ));
185 }
186 self.nodes = nodes;
187 Ok(())
188 }
189
190 fn account_text<E: serde::de::Error>(&mut self, bytes: usize) -> Result<(), E> {
191 let Some(key_and_string_bytes) = self.key_and_string_bytes.checked_add(bytes) else {
192 return self.fail(invalid_plan(
193 "resolution_source_evidence.document.key_and_string_bytes",
194 "byte count overflowed",
195 ));
196 };
197 if key_and_string_bytes > self.budget.maximum_key_and_string_bytes {
198 return self.fail(invalid_plan(
199 "resolution_source_evidence.document.key_and_string_bytes",
200 format!(
201 "must not exceed {}",
202 self.budget.maximum_key_and_string_bytes
203 ),
204 ));
205 }
206 self.key_and_string_bytes = key_and_string_bytes;
207 Ok(())
208 }
209}
210
211struct ResolutionJsonValueSeed<'a> {
212 preflight: &'a mut ResolutionJsonPreflight,
213 depth: usize,
214}
215
216impl<'de> DeserializeSeed<'de> for ResolutionJsonValueSeed<'_> {
217 type Value = ();
218
219 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
220 where
221 D: Deserializer<'de>,
222 {
223 self.preflight.account_node::<D::Error>(self.depth)?;
224 deserializer.deserialize_any(ResolutionJsonValueVisitor {
225 preflight: self.preflight,
226 depth: self.depth,
227 })
228 }
229}
230
231struct ResolutionJsonValueVisitor<'a> {
232 preflight: &'a mut ResolutionJsonPreflight,
233 depth: usize,
234}
235
236impl<'de> Visitor<'de> for ResolutionJsonValueVisitor<'_> {
237 type Value = ();
238
239 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 formatter.write_str("one JSON value")
241 }
242
243 fn visit_unit<E>(self) -> Result<Self::Value, E> {
244 Ok(())
245 }
246
247 fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
248 Ok(())
249 }
250
251 fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
252 Ok(())
253 }
254
255 fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
256 Ok(())
257 }
258
259 fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
260 Ok(())
261 }
262
263 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
264 where
265 E: serde::de::Error,
266 {
267 self.preflight.account_text::<E>(value.len())
268 }
269
270 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
271 where
272 E: serde::de::Error,
273 {
274 self.preflight.account_text::<E>(value.len())
275 }
276
277 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
278 where
279 E: serde::de::Error,
280 {
281 self.preflight.account_text::<E>(value.len())
282 }
283
284 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
285 where
286 A: SeqAccess<'de>,
287 {
288 let child_depth = self
289 .depth
290 .checked_add(1)
291 .ok_or_else(|| serde::de::Error::custom("resolution source JSON depth overflowed"))?;
292 while sequence
293 .next_element_seed(ResolutionJsonValueSeed {
294 preflight: &mut *self.preflight,
295 depth: child_depth,
296 })?
297 .is_some()
298 {}
299 Ok(())
300 }
301
302 fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
303 where
304 A: MapAccess<'de>,
305 {
306 let child_depth = self
307 .depth
308 .checked_add(1)
309 .ok_or_else(|| serde::de::Error::custom("resolution source JSON depth overflowed"))?;
310 while object
311 .next_key_seed(ResolutionJsonKeySeed {
312 preflight: &mut *self.preflight,
313 })?
314 .is_some()
315 {
316 object.next_value_seed(ResolutionJsonValueSeed {
317 preflight: &mut *self.preflight,
318 depth: child_depth,
319 })?;
320 }
321 Ok(())
322 }
323}
324
325struct ResolutionJsonKeySeed<'a> {
326 preflight: &'a mut ResolutionJsonPreflight,
327}
328
329impl<'de> DeserializeSeed<'de> for ResolutionJsonKeySeed<'_> {
330 type Value = ();
331
332 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
333 where
334 D: Deserializer<'de>,
335 {
336 deserializer.deserialize_str(ResolutionJsonKeyVisitor {
337 preflight: self.preflight,
338 })
339 }
340}
341
342struct ResolutionJsonKeyVisitor<'a> {
343 preflight: &'a mut ResolutionJsonPreflight,
344}
345
346impl<'de> Visitor<'de> for ResolutionJsonKeyVisitor<'_> {
347 type Value = ();
348
349 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
350 formatter.write_str("a JSON object key")
351 }
352
353 fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E>
354 where
355 E: serde::de::Error,
356 {
357 self.preflight.account_text::<E>(value.len())
358 }
359
360 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
361 where
362 E: serde::de::Error,
363 {
364 self.preflight.account_text::<E>(value.len())
365 }
366
367 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
368 where
369 E: serde::de::Error,
370 {
371 self.preflight.account_text::<E>(value.len())
372 }
373}
374
375fn preflight_resolution_json_depth(
376 source_bytes: &[u8],
377 maximum_depth: usize,
378) -> Result<(), VNextError> {
379 let mut closing_delimiters = Vec::with_capacity(maximum_depth.saturating_add(1));
380 let mut in_string = false;
381 let mut escaped = false;
382
383 for byte in source_bytes.iter().copied() {
384 if in_string {
385 if escaped {
386 escaped = false;
387 } else if byte == b'\\' {
388 escaped = true;
389 } else if byte == b'"' {
390 in_string = false;
391 }
392 continue;
393 }
394
395 match byte {
396 b'"' => {
397 if closing_delimiters.len() > maximum_depth {
398 return Err(invalid_plan(
399 "resolution_source_evidence.document.depth",
400 format!("must not exceed {maximum_depth}"),
401 ));
402 }
403 in_string = true;
404 }
405 b'[' | b'{' => {
406 if closing_delimiters.len() > maximum_depth {
407 return Err(invalid_plan(
408 "resolution_source_evidence.document.depth",
409 format!("must not exceed {maximum_depth}"),
410 ));
411 }
412 closing_delimiters.push(if byte == b'[' { b']' } else { b'}' });
413 }
414 b']' | b'}' => {
415 if closing_delimiters.last() == Some(&byte) {
416 closing_delimiters.pop();
417 }
418 }
419 b' ' | b'\t' | b'\r' | b'\n' | b',' | b':' => {}
420 _ if closing_delimiters.len() > maximum_depth => {
421 return Err(invalid_plan(
422 "resolution_source_evidence.document.depth",
423 format!("must not exceed {maximum_depth}"),
424 ));
425 }
426 _ => {}
427 }
428 }
429 Ok(())
430}
431
432fn preflight_resolution_json(
433 source_bytes: &[u8],
434 budget: ResolutionJsonBudget,
435) -> Result<(), VNextError> {
436 preflight_resolution_json_depth(source_bytes, budget.maximum_depth)?;
440
441 let mut preflight = ResolutionJsonPreflight::new(budget);
442 let result = {
443 let mut deserializer = serde_json::Deserializer::from_slice(source_bytes);
444 ResolutionJsonValueSeed {
445 preflight: &mut preflight,
446 depth: 0,
447 }
448 .deserialize(&mut deserializer)
449 .and_then(|()| deserializer.end())
450 };
451 if let Some(error) = preflight.violation {
452 return Err(error);
453 }
454 result.map_err(|error| VNextError::Serialization {
455 context: "parse resolution source JSON",
456 message: error.to_string(),
457 })
458}
459
460fn validate_resolution_json_tree(document: &serde_json::Value) -> Result<(), VNextError> {
461 let mut stack = vec![(document, 0usize)];
462 let mut nodes = 0usize;
463 let mut key_and_string_bytes = 0usize;
464
465 while let Some((value, depth)) = stack.pop() {
466 if depth > MAX_RESOLUTION_JSON_DEPTH {
467 return Err(invalid_plan(
468 "resolution_source_evidence.document.depth",
469 format!("must not exceed {MAX_RESOLUTION_JSON_DEPTH}"),
470 ));
471 }
472 nodes = nodes.checked_add(1).ok_or_else(|| {
473 invalid_plan(
474 "resolution_source_evidence.document.nodes",
475 "node count overflowed",
476 )
477 })?;
478 if nodes > MAX_RESOLUTION_JSON_NODES {
479 return Err(invalid_plan(
480 "resolution_source_evidence.document.nodes",
481 format!("must not exceed {MAX_RESOLUTION_JSON_NODES}"),
482 ));
483 }
484
485 match value {
486 serde_json::Value::String(value) => {
487 key_and_string_bytes =
488 key_and_string_bytes
489 .checked_add(value.len())
490 .ok_or_else(|| {
491 invalid_plan(
492 "resolution_source_evidence.document.key_and_string_bytes",
493 "byte count overflowed",
494 )
495 })?;
496 }
497 serde_json::Value::Array(values) => {
498 let child_depth = depth.checked_add(1).ok_or_else(|| {
499 invalid_plan(
500 "resolution_source_evidence.document.depth",
501 "depth overflowed",
502 )
503 })?;
504 stack.extend(values.iter().map(|value| (value, child_depth)));
505 }
506 serde_json::Value::Object(values) => {
507 let child_depth = depth.checked_add(1).ok_or_else(|| {
508 invalid_plan(
509 "resolution_source_evidence.document.depth",
510 "depth overflowed",
511 )
512 })?;
513 for (key, value) in values {
514 key_and_string_bytes =
515 key_and_string_bytes.checked_add(key.len()).ok_or_else(|| {
516 invalid_plan(
517 "resolution_source_evidence.document.key_and_string_bytes",
518 "byte count overflowed",
519 )
520 })?;
521 stack.push((value, child_depth));
522 }
523 }
524 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {
525 }
526 }
527
528 if key_and_string_bytes > MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES {
529 return Err(invalid_plan(
530 "resolution_source_evidence.document.key_and_string_bytes",
531 format!("must not exceed {MAX_RESOLUTION_JSON_KEY_AND_STRING_BYTES}"),
532 ));
533 }
534 }
535
536 Ok(())
537}
538
539fn drop_json_iteratively(document: serde_json::Value) {
540 let mut stack = vec![document];
541 while let Some(value) = stack.pop() {
542 match value {
543 serde_json::Value::Array(mut values) => stack.append(&mut values),
544 serde_json::Value::Object(values) => stack.extend(values.into_values()),
545 serde_json::Value::Null
546 | serde_json::Value::Bool(_)
547 | serde_json::Value::Number(_)
548 | serde_json::Value::String(_) => {}
549 }
550 }
551}
552
553fn validate_and_canonicalize_resolution_json(
554 document: serde_json::Value,
555 fingerprint_context: &'static str,
556) -> Result<(serde_json::Value, String), VNextError> {
557 if let Err(error) = validate_resolution_json_tree(&document) {
558 drop_json_iteratively(document);
559 return Err(error);
560 }
561 let document = canonicalize_json(document);
562 let fingerprint = canonical_value_fingerprint(&document, fingerprint_context)?;
563 Ok((document, fingerprint))
564}
565
566fn validate_portable_identifier(
567 kind: &'static str,
568 value: &str,
569 maximum_length: usize,
570) -> Result<(), VNextError> {
571 if value.is_empty() || value.len() > maximum_length {
572 return Err(invalid_plan(
573 kind,
574 format!("must contain between 1 and {maximum_length} bytes"),
575 ));
576 }
577 if !value.bytes().all(|byte| {
578 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
579 }) {
580 return Err(invalid_plan(kind, "contains a non-portable character"));
581 }
582 Ok(())
583}
584
585fn validate_source_path(path: &str) -> bool {
586 !path.is_empty()
587 && !path.starts_with('/')
588 && !path.ends_with('/')
589 && !path.contains('\\')
590 && path
591 .split('/')
592 .all(|component| !matches!(component, "" | "." | ".."))
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(rename_all = "snake_case")]
597pub enum ModelSourceKind {
598 LocalDirectory,
599 LocalFile,
600 Repository,
601 ReleaseArtifact,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605pub struct OriginalModelSource {
606 pub kind: ModelSourceKind,
607 pub location: String,
608 pub requested_revision: Option<String>,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct FileFingerprint {
613 pub relative_path: String,
614 pub size_bytes: u64,
615 pub sha256: String,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct ResolvedModelSource {
620 pub canonical_location: String,
621 pub resolved_revision: String,
622 pub files: Vec<FileFingerprint>,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
626#[serde(rename_all = "snake_case")]
627pub enum ModelArtifactSourceRole {
628 Semantic,
629 Tokenizer,
630 Weights,
631}
632
633impl ModelArtifactSourceRole {
634 pub const ALL: [Self; 3] = [Self::Semantic, Self::Tokenizer, Self::Weights];
635
636 pub const fn as_str(self) -> &'static str {
637 match self {
638 Self::Semantic => "semantic",
639 Self::Tokenizer => "tokenizer",
640 Self::Weights => "weights",
641 }
642 }
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
646pub struct OriginalModelSources {
647 pub semantic: OriginalModelSource,
648 pub tokenizer: OriginalModelSource,
649 pub weights: OriginalModelSource,
650}
651
652impl OriginalModelSources {
653 pub const fn for_role(&self, role: ModelArtifactSourceRole) -> &OriginalModelSource {
654 match role {
655 ModelArtifactSourceRole::Semantic => &self.semantic,
656 ModelArtifactSourceRole::Tokenizer => &self.tokenizer,
657 ModelArtifactSourceRole::Weights => &self.weights,
658 }
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663pub struct ResolvedModelSources {
664 pub semantic: ResolvedModelSource,
665 pub tokenizer: ResolvedModelSource,
666 pub weights: ResolvedModelSource,
667}
668
669impl ResolvedModelSources {
670 pub const fn for_role(&self, role: ModelArtifactSourceRole) -> &ResolvedModelSource {
671 match role {
672 ModelArtifactSourceRole::Semantic => &self.semantic,
673 ModelArtifactSourceRole::Tokenizer => &self.tokenizer,
674 ModelArtifactSourceRole::Weights => &self.weights,
675 }
676 }
677
678 fn for_role_mut(&mut self, role: ModelArtifactSourceRole) -> &mut ResolvedModelSource {
679 match role {
680 ModelArtifactSourceRole::Semantic => &mut self.semantic,
681 ModelArtifactSourceRole::Tokenizer => &mut self.tokenizer,
682 ModelArtifactSourceRole::Weights => &mut self.weights,
683 }
684 }
685}
686
687pub const PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION: u32 = 1;
688
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
695#[serde(deny_unknown_fields)]
696pub struct ProductModelArtifactBinding {
697 pub role: ModelArtifactSourceRole,
698 pub source_file: String,
699 pub container_sha256: String,
700 #[serde(skip_serializing_if = "Option::is_none")]
701 pub content_sha256: Option<String>,
702}
703
704impl ProductModelArtifactBinding {
705 pub fn new(
706 role: ModelArtifactSourceRole,
707 source_file: impl Into<String>,
708 container_sha256: impl Into<String>,
709 content_sha256: Option<String>,
710 ) -> Result<Self, VNextError> {
711 let binding = Self {
712 role,
713 source_file: source_file.into(),
714 container_sha256: container_sha256.into(),
715 content_sha256,
716 };
717 binding.validate("product_model_artifact_binding")?;
718 Ok(binding)
719 }
720
721 fn validate(&self, field: &str) -> Result<(), VNextError> {
722 if !validate_source_path(&self.source_file) {
723 return Err(invalid_plan(
724 format!("{field}.source_file"),
725 "must be a portable relative source path",
726 ));
727 }
728 if !is_canonical_sha256(&self.container_sha256) {
729 return Err(invalid_plan(
730 format!("{field}.container_sha256"),
731 "must be a canonical SHA-256",
732 ));
733 }
734 if self
735 .content_sha256
736 .as_deref()
737 .is_some_and(|sha256| !is_canonical_sha256(sha256))
738 {
739 return Err(invalid_plan(
740 format!("{field}.content_sha256"),
741 "must be a canonical SHA-256",
742 ));
743 }
744 Ok(())
745 }
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756#[serde(deny_unknown_fields)]
757pub struct ProductModelSourceIdentity {
758 pub schema_version: u32,
759 pub requested_model: String,
760 pub resolved_model: String,
761 pub original_sources: OriginalModelSources,
762 pub resolved_sources: ResolvedModelSources,
763 pub semantic_config: ProductModelArtifactBinding,
764 pub tokenizer: ProductModelArtifactBinding,
765 pub template: ProductModelArtifactBinding,
766 #[serde(skip_serializing_if = "Option::is_none")]
767 pub weight_config: Option<ProductModelArtifactBinding>,
768}
769
770impl ProductModelSourceIdentity {
771 #[allow(clippy::too_many_arguments)]
772 pub fn new(
773 requested_model: impl Into<String>,
774 resolved_model: impl Into<String>,
775 original_sources: OriginalModelSources,
776 resolved_sources: ResolvedModelSources,
777 semantic_config: ProductModelArtifactBinding,
778 tokenizer: ProductModelArtifactBinding,
779 template: ProductModelArtifactBinding,
780 weight_config: Option<ProductModelArtifactBinding>,
781 ) -> Result<Self, VNextError> {
782 let identity = Self {
783 schema_version: PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION,
784 requested_model: requested_model.into(),
785 resolved_model: resolved_model.into(),
786 original_sources,
787 resolved_sources,
788 semantic_config,
789 tokenizer,
790 template,
791 weight_config,
792 };
793 identity.validate()?;
794 Ok(identity)
795 }
796
797 pub fn validate(&self) -> Result<(), VNextError> {
798 if self.schema_version != PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION {
799 return Err(invalid_plan(
800 "product_model_source_identity.schema_version",
801 format!("must be {}", PRODUCT_MODEL_SOURCE_IDENTITY_SCHEMA_VERSION),
802 ));
803 }
804 for (field, value) in [
805 ("requested_model", self.requested_model.as_str()),
806 ("resolved_model", self.resolved_model.as_str()),
807 ] {
808 if value.is_empty() || value.trim() != value {
809 return Err(invalid_plan(
810 format!("product_model_source_identity.{field}"),
811 "must be non-empty and have no surrounding whitespace",
812 ));
813 }
814 }
815 for role in ModelArtifactSourceRole::ALL {
816 self.validate_original_source(role)?;
817 self.validate_resolved_source(role)?;
818 }
819 self.validate_binding(
820 "semantic_config",
821 &self.semantic_config,
822 ModelArtifactSourceRole::Semantic,
823 )?;
824 self.validate_binding(
825 "tokenizer",
826 &self.tokenizer,
827 ModelArtifactSourceRole::Tokenizer,
828 )?;
829 self.validate_binding(
830 "template",
831 &self.template,
832 ModelArtifactSourceRole::Tokenizer,
833 )?;
834 if self.template.content_sha256.is_none() {
835 return Err(invalid_plan(
836 "product_model_source_identity.template.content_sha256",
837 "selected template content must be fingerprinted",
838 ));
839 }
840 if let Some(binding) = &self.weight_config {
841 self.validate_binding("weight_config", binding, ModelArtifactSourceRole::Weights)?;
842 }
843 Ok(())
844 }
845
846 fn validate_original_source(&self, role: ModelArtifactSourceRole) -> Result<(), VNextError> {
847 let source = self.original_sources.for_role(role);
848 if source.location.is_empty() || source.location.trim() != source.location {
849 return Err(invalid_plan(
850 format!(
851 "product_model_source_identity.original_sources.{}.location",
852 role.as_str()
853 ),
854 "must be non-empty and have no surrounding whitespace",
855 ));
856 }
857 if source
858 .requested_revision
859 .as_deref()
860 .is_some_and(|revision| revision.is_empty() || revision.trim() != revision)
861 {
862 return Err(invalid_plan(
863 format!(
864 "product_model_source_identity.original_sources.{}.requested_revision",
865 role.as_str()
866 ),
867 "must be non-empty and have no surrounding whitespace when present",
868 ));
869 }
870 Ok(())
871 }
872
873 fn validate_resolved_source(&self, role: ModelArtifactSourceRole) -> Result<(), VNextError> {
874 let source = self.resolved_sources.for_role(role);
875 for (field, value) in [
876 ("canonical_location", source.canonical_location.as_str()),
877 ("resolved_revision", source.resolved_revision.as_str()),
878 ] {
879 if value.is_empty() || value.trim() != value {
880 return Err(invalid_plan(
881 format!(
882 "product_model_source_identity.resolved_sources.{}.{field}",
883 role.as_str()
884 ),
885 "must be non-empty and have no surrounding whitespace",
886 ));
887 }
888 }
889 if source.files.is_empty() {
890 return Err(invalid_plan(
891 format!(
892 "product_model_source_identity.resolved_sources.{}.files",
893 role.as_str()
894 ),
895 "must not be empty",
896 ));
897 }
898 for (index, file) in source.files.iter().enumerate() {
899 let field = format!(
900 "product_model_source_identity.resolved_sources.{}.files[{index}]",
901 role.as_str()
902 );
903 if !validate_source_path(&file.relative_path) {
904 return Err(invalid_plan(
905 format!("{field}.relative_path"),
906 "must be a portable relative source path",
907 ));
908 }
909 if file.size_bytes == 0 {
910 return Err(invalid_plan(
911 format!("{field}.size_bytes"),
912 "must be positive",
913 ));
914 }
915 if !is_canonical_sha256(&file.sha256) {
916 return Err(invalid_plan(
917 format!("{field}.sha256"),
918 "must be a canonical SHA-256",
919 ));
920 }
921 }
922 if source
923 .files
924 .windows(2)
925 .any(|pair| pair[0].relative_path >= pair[1].relative_path)
926 {
927 return Err(invalid_plan(
928 format!(
929 "product_model_source_identity.resolved_sources.{}.files",
930 role.as_str()
931 ),
932 "must be strictly sorted by relative_path without duplicates",
933 ));
934 }
935 Ok(())
936 }
937
938 fn validate_binding(
939 &self,
940 field: &str,
941 binding: &ProductModelArtifactBinding,
942 expected_role: ModelArtifactSourceRole,
943 ) -> Result<(), VNextError> {
944 binding.validate(&format!("product_model_source_identity.{field}"))?;
945 if binding.role != expected_role {
946 return Err(invalid_plan(
947 format!("product_model_source_identity.{field}.role"),
948 format!("must be {expected_role:?}"),
949 ));
950 }
951 let source = self.resolved_sources.for_role(binding.role);
952 match source
953 .files
954 .iter()
955 .find(|file| file.relative_path == binding.source_file)
956 {
957 Some(file) if file.sha256 == binding.container_sha256 => Ok(()),
958 Some(file) => Err(invalid_plan(
959 format!("product_model_source_identity.{field}.container_sha256"),
960 format!("does not match resolved source file {}", file.relative_path),
961 )),
962 None => Err(invalid_plan(
963 format!("product_model_source_identity.{field}.source_file"),
964 format!("is absent from resolved {} source", binding.role.as_str()),
965 )),
966 }
967 }
968}
969
970#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
971pub struct ModelConfigFingerprint {
972 pub source_file: String,
973 pub sha256: String,
974 pub typed_config_sha256: String,
975}
976
977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
978pub struct EngineSelection {
979 pub provider_id: ProviderId,
980 pub contract_version: ContractVersion,
981 pub implementation_fingerprint: String,
982}
983
984#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
985#[serde(rename_all = "snake_case")]
986pub enum SchedulingDiscipline {
987 FirstReady,
988 Priority,
989 Deadline,
990}
991
992#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993pub struct RuntimeMemoryPolicy {
994 pub capacity_bytes: u64,
995 pub reserve_bytes: u64,
996 pub maximum_active_sequences: u32,
997 pub dynamic_storage_profile_order: Vec<DynamicStorageProfile>,
998}
999
1000#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1001pub struct AdmissionPolicy {
1002 pub maximum_queue_depth: u32,
1003 pub maximum_scheduled_tokens: u64,
1004 pub sequence_fit_policy: AdmissionFitPolicy,
1005 pub allow_defer: bool,
1006 pub cancellation_check_interval_steps: u32,
1007}
1008
1009#[derive(Serialize)]
1010struct RuntimePolicyFingerprintPayload<'a> {
1011 policy_id: &'a str,
1012 version: ContractVersion,
1013 scheduling: SchedulingDiscipline,
1014 memory: &'a RuntimeMemoryPolicy,
1015 admission: &'a AdmissionPolicy,
1016 attention_execution: AttentionExecutionPolicy,
1017 execution_determinism: ExecutionDeterminismRequirement,
1018 reusable_execution: &'a Option<ReusableExecutionPolicy>,
1019}
1020
1021#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1022pub struct ResolvedRuntimePolicy {
1023 policy_id: String,
1024 version: ContractVersion,
1025 scheduling: SchedulingDiscipline,
1026 memory: RuntimeMemoryPolicy,
1027 admission: AdmissionPolicy,
1028 attention_execution: AttentionExecutionPolicy,
1029 execution_determinism: ExecutionDeterminismRequirement,
1030 reusable_execution: Option<ReusableExecutionPolicy>,
1031 fingerprint: String,
1032}
1033
1034#[derive(Deserialize)]
1035#[serde(deny_unknown_fields)]
1036struct ResolvedRuntimePolicyWire {
1037 policy_id: String,
1038 version: ContractVersion,
1039 scheduling: SchedulingDiscipline,
1040 memory: RuntimeMemoryPolicy,
1041 admission: AdmissionPolicy,
1042 attention_execution: AttentionExecutionPolicy,
1043 execution_determinism: ExecutionDeterminismRequirement,
1044 reusable_execution: Option<ReusableExecutionPolicy>,
1045 fingerprint: String,
1046}
1047
1048impl<'de> Deserialize<'de> for ResolvedRuntimePolicy {
1049 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1050 where
1051 D: Deserializer<'de>,
1052 {
1053 let wire = ResolvedRuntimePolicyWire::deserialize(deserializer)?;
1054 let policy = Self::new(
1055 wire.policy_id,
1056 wire.version,
1057 wire.scheduling,
1058 wire.memory,
1059 wire.admission,
1060 wire.attention_execution,
1061 wire.execution_determinism,
1062 wire.reusable_execution,
1063 )
1064 .map_err(serde::de::Error::custom)?;
1065 if wire.fingerprint != policy.fingerprint {
1066 return Err(serde::de::Error::custom(format!(
1067 "runtime policy fingerprint mismatch: expected `{}`, actual `{}`",
1068 policy.fingerprint, wire.fingerprint
1069 )));
1070 }
1071 Ok(policy)
1072 }
1073}
1074
1075impl ResolvedRuntimePolicy {
1076 pub fn new(
1077 policy_id: impl Into<String>,
1078 version: ContractVersion,
1079 scheduling: SchedulingDiscipline,
1080 memory: RuntimeMemoryPolicy,
1081 admission: AdmissionPolicy,
1082 attention_execution: AttentionExecutionPolicy,
1083 execution_determinism: ExecutionDeterminismRequirement,
1084 reusable_execution: Option<ReusableExecutionPolicy>,
1085 ) -> Result<Self, VNextError> {
1086 let policy_id = policy_id.into();
1087 Self::validate_fields(
1088 &policy_id,
1089 version,
1090 &memory,
1091 &admission,
1092 attention_execution,
1093 reusable_execution.as_ref(),
1094 )?;
1095 let fingerprint = Self::compute_fingerprint(
1096 &policy_id,
1097 version,
1098 scheduling,
1099 &memory,
1100 &admission,
1101 attention_execution,
1102 execution_determinism,
1103 &reusable_execution,
1104 )?;
1105 Ok(Self {
1106 policy_id,
1107 version,
1108 scheduling,
1109 memory,
1110 admission,
1111 attention_execution,
1112 execution_determinism,
1113 reusable_execution,
1114 fingerprint,
1115 })
1116 }
1117
1118 fn validate_fields(
1119 policy_id: &str,
1120 version: ContractVersion,
1121 memory: &RuntimeMemoryPolicy,
1122 admission: &AdmissionPolicy,
1123 attention_execution: AttentionExecutionPolicy,
1124 reusable_execution: Option<&ReusableExecutionPolicy>,
1125 ) -> Result<(), VNextError> {
1126 validate_portable_identifier("runtime_policy.policy_id", policy_id, 160)?;
1127 if version.major == 0 {
1128 return Err(invalid_plan(
1129 "runtime_policy.version",
1130 "major version must be non-zero",
1131 ));
1132 }
1133 if memory.capacity_bytes == 0
1134 || memory.reserve_bytes >= memory.capacity_bytes
1135 || memory.maximum_active_sequences == 0
1136 || memory.dynamic_storage_profile_order.is_empty()
1137 || memory
1138 .dynamic_storage_profile_order
1139 .iter()
1140 .enumerate()
1141 .any(|(index, profile)| {
1142 memory.dynamic_storage_profile_order[index + 1..].contains(profile)
1143 })
1144 {
1145 return Err(invalid_plan(
1146 "runtime_policy.memory",
1147 "capacity and reserve must be valid and maximum_active_sequences must be non-zero",
1148 ));
1149 }
1150 if admission.maximum_queue_depth == 0
1151 || admission.maximum_scheduled_tokens == 0
1152 || admission.cancellation_check_interval_steps == 0
1153 {
1154 return Err(invalid_plan(
1155 "runtime_policy.admission",
1156 "queue depth, scheduled-token ceiling, and cancellation interval must be non-zero",
1157 ));
1158 }
1159 if !attention_execution.is_resolved() {
1160 return Err(invalid_plan(
1161 "runtime_policy.attention_execution",
1162 "attention execution policy must be resolved before plan compilation",
1163 ));
1164 }
1165 if let Some(reusable_execution) = reusable_execution {
1166 reusable_execution.validate()?;
1167 if reusable_execution.buckets().iter().any(|bucket| {
1168 bucket.capacity().maximum_sequences() > memory.maximum_active_sequences
1169 || bucket.capacity().maximum_tokens() > admission.maximum_scheduled_tokens
1170 }) {
1171 return Err(invalid_plan(
1172 "runtime_policy.reusable_execution",
1173 "bucket capacity exceeds the scheduler policy ceiling",
1174 ));
1175 }
1176 if reusable_execution
1177 .program_policy()
1178 .is_some_and(|program_policy| {
1179 program_policy.programs().iter().any(|program| {
1180 let shape = program.shape();
1181 shape.request_capacity() > memory.maximum_active_sequences
1182 || shape.token_capacity() > admission.maximum_scheduled_tokens
1183 })
1184 })
1185 {
1186 return Err(invalid_plan(
1187 "runtime_policy.reusable_execution",
1188 "program shape exceeds the scheduler policy ceiling",
1189 ));
1190 }
1191 }
1192 Ok(())
1193 }
1194
1195 fn compute_fingerprint(
1196 policy_id: &str,
1197 version: ContractVersion,
1198 scheduling: SchedulingDiscipline,
1199 memory: &RuntimeMemoryPolicy,
1200 admission: &AdmissionPolicy,
1201 attention_execution: AttentionExecutionPolicy,
1202 execution_determinism: ExecutionDeterminismRequirement,
1203 reusable_execution: &Option<ReusableExecutionPolicy>,
1204 ) -> Result<String, VNextError> {
1205 canonical_fingerprint(
1206 &RuntimePolicyFingerprintPayload {
1207 policy_id,
1208 version,
1209 scheduling,
1210 memory,
1211 admission,
1212 attention_execution,
1213 execution_determinism,
1214 reusable_execution,
1215 },
1216 "serialize resolved runtime policy",
1217 )
1218 }
1219
1220 pub fn policy_id(&self) -> &str {
1221 &self.policy_id
1222 }
1223
1224 pub fn version(&self) -> ContractVersion {
1225 self.version
1226 }
1227
1228 pub fn scheduling(&self) -> SchedulingDiscipline {
1229 self.scheduling
1230 }
1231
1232 pub fn memory(&self) -> &RuntimeMemoryPolicy {
1233 &self.memory
1234 }
1235
1236 pub fn admission(&self) -> &AdmissionPolicy {
1237 &self.admission
1238 }
1239
1240 pub const fn attention_execution(&self) -> AttentionExecutionPolicy {
1241 self.attention_execution
1242 }
1243
1244 pub const fn execution_determinism(&self) -> ExecutionDeterminismRequirement {
1245 self.execution_determinism
1246 }
1247
1248 pub fn reusable_execution(&self) -> Option<&ReusableExecutionPolicy> {
1249 self.reusable_execution.as_ref()
1250 }
1251
1252 pub fn fingerprint_str(&self) -> &str {
1253 &self.fingerprint
1254 }
1255}
1256
1257impl RuntimePolicy for ResolvedRuntimePolicy {
1258 fn version(&self) -> ContractVersion {
1259 self.version
1260 }
1261
1262 fn memory_capacity_bytes(&self) -> u64 {
1263 self.memory.capacity_bytes
1264 }
1265
1266 fn memory_reserve_bytes(&self) -> u64 {
1267 self.memory.reserve_bytes
1268 }
1269
1270 fn maximum_active_sequences(&self) -> u32 {
1271 self.memory.maximum_active_sequences
1272 }
1273
1274 fn maximum_scheduled_tokens(&self) -> u64 {
1275 self.admission.maximum_scheduled_tokens
1276 }
1277
1278 fn attention_execution_policy(&self) -> AttentionExecutionPolicy {
1279 self.attention_execution
1280 }
1281
1282 fn execution_determinism_requirement(&self) -> ExecutionDeterminismRequirement {
1283 self.execution_determinism
1284 }
1285
1286 fn dynamic_storage_profile_order(&self) -> &[DynamicStorageProfile] {
1287 &self.memory.dynamic_storage_profile_order
1288 }
1289
1290 fn reusable_execution_policy(&self) -> Option<&ReusableExecutionPolicy> {
1291 self.reusable_execution.as_ref()
1292 }
1293
1294 fn validate(&self) -> Result<(), VNextError> {
1295 Self::validate_fields(
1296 &self.policy_id,
1297 self.version,
1298 &self.memory,
1299 &self.admission,
1300 self.attention_execution,
1301 self.reusable_execution.as_ref(),
1302 )?;
1303 let computed = Self::compute_fingerprint(
1304 &self.policy_id,
1305 self.version,
1306 self.scheduling,
1307 &self.memory,
1308 &self.admission,
1309 self.attention_execution,
1310 self.execution_determinism,
1311 &self.reusable_execution,
1312 )?;
1313 if self.fingerprint != computed {
1314 return Err(invalid_plan(
1315 "runtime_policy.fingerprint",
1316 format!(
1317 "does not match typed fields: expected `{computed}`, actual `{}`",
1318 self.fingerprint
1319 ),
1320 ));
1321 }
1322 Ok(())
1323 }
1324}
1325
1326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1327pub struct RationalValue {
1328 numerator: i64,
1329 denominator: u64,
1330}
1331
1332#[derive(Deserialize)]
1333#[serde(deny_unknown_fields)]
1334struct RationalValueWire {
1335 numerator: i64,
1336 denominator: u64,
1337}
1338
1339impl<'de> Deserialize<'de> for RationalValue {
1340 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1341 where
1342 D: Deserializer<'de>,
1343 {
1344 let wire = RationalValueWire::deserialize(deserializer)?;
1345 Self::new(wire.numerator, wire.denominator).map_err(serde::de::Error::custom)
1346 }
1347}
1348
1349impl RationalValue {
1350 pub fn new(numerator: i64, denominator: u64) -> Result<Self, VNextError> {
1351 if denominator == 0 {
1352 return Err(invalid_plan(
1353 "sampling.rational",
1354 "denominator must be non-zero",
1355 ));
1356 }
1357 let divisor = greatest_common_divisor(numerator.unsigned_abs(), denominator);
1358 let numerator = ((numerator as i128) / (divisor as i128)) as i64;
1359 let denominator = denominator / divisor;
1360 Ok(Self {
1361 numerator,
1362 denominator,
1363 })
1364 }
1365
1366 fn validate(&self, field: &str) -> Result<(), VNextError> {
1367 if self.denominator == 0
1368 || greatest_common_divisor(self.numerator.unsigned_abs(), self.denominator) != 1
1369 || (self.numerator == 0 && self.denominator != 1)
1370 {
1371 return Err(invalid_plan(
1372 field,
1373 "rational value must be reduced with a non-zero denominator",
1374 ));
1375 }
1376 Ok(())
1377 }
1378
1379 fn compare(&self, numerator: i64, denominator: u64) -> Ordering {
1380 ((self.numerator as i128) * (denominator as i128))
1381 .cmp(&((numerator as i128) * (self.denominator as i128)))
1382 }
1383
1384 pub fn numerator(&self) -> i64 {
1385 self.numerator
1386 }
1387
1388 pub fn denominator(&self) -> u64 {
1389 self.denominator
1390 }
1391}
1392
1393fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 {
1394 while right != 0 {
1395 let remainder = left % right;
1396 left = right;
1397 right = remainder;
1398 }
1399 left
1400}
1401
1402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1403#[serde(rename_all = "snake_case")]
1404pub enum TriStatePolicy {
1405 ModelDefault,
1406 Enabled,
1407 Disabled,
1408}
1409
1410#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1411pub struct SamplingPolicy {
1412 temperature: RationalValue,
1413 top_p: RationalValue,
1414 top_k: Option<u32>,
1415 min_p: RationalValue,
1416 presence_penalty: RationalValue,
1417 repetition_penalty: RationalValue,
1418 seed: u64,
1419 thinking_policy: TriStatePolicy,
1420}
1421
1422#[derive(Deserialize)]
1423#[serde(deny_unknown_fields)]
1424struct SamplingPolicyWire {
1425 temperature: RationalValue,
1426 top_p: RationalValue,
1427 top_k: Option<u32>,
1428 min_p: RationalValue,
1429 presence_penalty: RationalValue,
1430 repetition_penalty: RationalValue,
1431 seed: u64,
1432 thinking_policy: TriStatePolicy,
1433}
1434
1435impl<'de> Deserialize<'de> for SamplingPolicy {
1436 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1437 where
1438 D: Deserializer<'de>,
1439 {
1440 let wire = SamplingPolicyWire::deserialize(deserializer)?;
1441 Self::new(
1442 wire.temperature,
1443 wire.top_p,
1444 wire.top_k,
1445 wire.min_p,
1446 wire.presence_penalty,
1447 wire.repetition_penalty,
1448 wire.seed,
1449 wire.thinking_policy,
1450 )
1451 .map_err(serde::de::Error::custom)
1452 }
1453}
1454
1455impl SamplingPolicy {
1456 #[allow(clippy::too_many_arguments)]
1457 pub fn new(
1458 temperature: RationalValue,
1459 top_p: RationalValue,
1460 top_k: Option<u32>,
1461 min_p: RationalValue,
1462 presence_penalty: RationalValue,
1463 repetition_penalty: RationalValue,
1464 seed: u64,
1465 thinking_policy: TriStatePolicy,
1466 ) -> Result<Self, VNextError> {
1467 let policy = Self {
1468 temperature,
1469 top_p,
1470 top_k,
1471 min_p,
1472 presence_penalty,
1473 repetition_penalty,
1474 seed,
1475 thinking_policy,
1476 };
1477 policy.validate()?;
1478 Ok(policy)
1479 }
1480
1481 fn validate(&self) -> Result<(), VNextError> {
1482 self.temperature.validate("sampling.temperature")?;
1483 self.top_p.validate("sampling.top_p")?;
1484 self.min_p.validate("sampling.min_p")?;
1485 self.presence_penalty
1486 .validate("sampling.presence_penalty")?;
1487 self.repetition_penalty
1488 .validate("sampling.repetition_penalty")?;
1489
1490 if self.temperature.compare(0, 1) == Ordering::Less {
1491 return Err(invalid_plan(
1492 "sampling.temperature",
1493 "must be greater than or equal to zero",
1494 ));
1495 }
1496 if self.top_p.compare(0, 1) != Ordering::Greater
1497 || self.top_p.compare(1, 1) == Ordering::Greater
1498 {
1499 return Err(invalid_plan(
1500 "sampling.top_p",
1501 "must be in the interval (0, 1]",
1502 ));
1503 }
1504 if self.top_k == Some(0) {
1505 return Err(invalid_plan(
1506 "sampling.top_k",
1507 "must be absent or greater than zero",
1508 ));
1509 }
1510 if self.min_p.compare(0, 1) == Ordering::Less
1511 || self.min_p.compare(1, 1) == Ordering::Greater
1512 {
1513 return Err(invalid_plan(
1514 "sampling.min_p",
1515 "must be in the interval [0, 1]",
1516 ));
1517 }
1518 if self.presence_penalty.compare(-2, 1) == Ordering::Less
1519 || self.presence_penalty.compare(2, 1) == Ordering::Greater
1520 {
1521 return Err(invalid_plan(
1522 "sampling.presence_penalty",
1523 "must be in the interval [-2, 2]",
1524 ));
1525 }
1526 if self.repetition_penalty.compare(0, 1) != Ordering::Greater {
1527 return Err(invalid_plan(
1528 "sampling.repetition_penalty",
1529 "must be greater than zero",
1530 ));
1531 }
1532 Ok(())
1533 }
1534
1535 pub fn temperature(&self) -> RationalValue {
1536 self.temperature
1537 }
1538
1539 pub fn top_p(&self) -> RationalValue {
1540 self.top_p
1541 }
1542
1543 pub fn top_k(&self) -> Option<u32> {
1544 self.top_k
1545 }
1546
1547 pub fn min_p(&self) -> RationalValue {
1548 self.min_p
1549 }
1550
1551 pub fn presence_penalty(&self) -> RationalValue {
1552 self.presence_penalty
1553 }
1554
1555 pub fn repetition_penalty(&self) -> RationalValue {
1556 self.repetition_penalty
1557 }
1558
1559 pub fn seed(&self) -> u64 {
1560 self.seed
1561 }
1562
1563 pub fn thinking_policy(&self) -> TriStatePolicy {
1564 self.thinking_policy
1565 }
1566}
1567
1568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1569pub struct StopTokenCollisionPolicy {
1570 allowed_model_roles: BTreeSet<SpecialTokenRole>,
1571}
1572
1573impl StopTokenCollisionPolicy {
1574 pub fn new(allowed_model_roles: BTreeSet<SpecialTokenRole>) -> Result<Self, VNextError> {
1575 if allowed_model_roles.contains(&SpecialTokenRole::Stop) {
1576 return Err(invalid_plan(
1577 "stop.collision_policy",
1578 "a stop-token collision policy can only name model-owned roles",
1579 ));
1580 }
1581 Ok(Self {
1582 allowed_model_roles,
1583 })
1584 }
1585
1586 pub fn require_distinct() -> Self {
1587 Self {
1588 allowed_model_roles: BTreeSet::new(),
1589 }
1590 }
1591
1592 pub fn allows(&self, model_role: SpecialTokenRole) -> bool {
1593 self.allowed_model_roles.contains(&model_role)
1594 }
1595
1596 pub fn allowed_model_roles(&self) -> &BTreeSet<SpecialTokenRole> {
1597 &self.allowed_model_roles
1598 }
1599}
1600
1601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1602pub struct StopPolicy {
1603 pub maximum_output_tokens: u32,
1604 pub token_ids: BTreeSet<u32>,
1605 pub strings: Vec<String>,
1606 pub collision_policy: StopTokenCollisionPolicy,
1607}
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1610#[serde(rename_all = "snake_case")]
1611pub enum StructuredOutputPolicy {
1612 Disabled,
1613 JsonObject,
1614 JsonSchema { schema_sha256: String },
1615}
1616
1617#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1618#[serde(rename_all = "snake_case")]
1619pub enum ResolutionField {
1620 OriginalSources,
1621 ResolvedSources,
1622 Config,
1623 ExternalMetadata,
1624 Family,
1625 WeightSchema,
1626 WeightFormat,
1627 Tokenizer,
1628 Template,
1629 SpecialTokens,
1630 Device,
1631 Capabilities,
1632 RuntimePreset,
1633 RuntimeMemory,
1634 Admission,
1635 Engine,
1636 ExecutionPlan,
1637 Sampling,
1638 Stop,
1639 StructuredOutput,
1640}
1641
1642#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1643#[serde(rename_all = "snake_case")]
1644pub enum ResolutionDecisionSource {
1645 UserInput,
1646 CommandLine,
1647 ConfigFile,
1648 ModelMetadata,
1649 TypedModelResolution,
1650 ProductDefault,
1651 RuntimePreset,
1652 CapabilityResolution,
1653 Planner,
1654}
1655
1656impl ResolutionField {
1657 pub const fn as_str(self) -> &'static str {
1658 match self {
1659 Self::OriginalSources => "original_sources",
1660 Self::ResolvedSources => "resolved_sources",
1661 Self::Config => "config",
1662 Self::ExternalMetadata => "external_metadata",
1663 Self::Family => "family",
1664 Self::WeightSchema => "weight_schema",
1665 Self::WeightFormat => "weight_format",
1666 Self::Tokenizer => "tokenizer",
1667 Self::Template => "template",
1668 Self::SpecialTokens => "special_tokens",
1669 Self::Device => "device",
1670 Self::Capabilities => "capabilities",
1671 Self::RuntimePreset => "runtime_preset",
1672 Self::RuntimeMemory => "runtime_memory",
1673 Self::Admission => "admission",
1674 Self::Engine => "engine",
1675 Self::ExecutionPlan => "execution_plan",
1676 Self::Sampling => "sampling",
1677 Self::Stop => "stop",
1678 Self::StructuredOutput => "structured_output",
1679 }
1680 }
1681
1682 pub const fn accepts_source(self, source: ResolutionDecisionSource) -> bool {
1686 use ResolutionDecisionSource as Source;
1687 use ResolutionField as Field;
1688
1689 match self {
1690 Field::OriginalSources => matches!(
1691 source,
1692 Source::UserInput | Source::CommandLine | Source::ConfigFile
1693 ),
1694 Field::ResolvedSources => {
1695 matches!(source, Source::ModelMetadata | Source::TypedModelResolution)
1696 }
1697 Field::Config => matches!(
1698 source,
1699 Source::ConfigFile | Source::ModelMetadata | Source::TypedModelResolution
1700 ),
1701 Field::ExternalMetadata
1702 | Field::Family
1703 | Field::WeightSchema
1704 | Field::Tokenizer
1705 | Field::Template
1706 | Field::SpecialTokens => {
1707 matches!(source, Source::ModelMetadata | Source::TypedModelResolution)
1708 }
1709 Field::WeightFormat => matches!(
1710 source,
1711 Source::CommandLine
1712 | Source::ConfigFile
1713 | Source::ModelMetadata
1714 | Source::TypedModelResolution
1715 ),
1716 Field::Device => matches!(
1717 source,
1718 Source::CommandLine
1719 | Source::ConfigFile
1720 | Source::ProductDefault
1721 | Source::CapabilityResolution
1722 ),
1723 Field::Capabilities => matches!(source, Source::CapabilityResolution),
1724 Field::RuntimePreset => matches!(
1725 source,
1726 Source::CommandLine
1727 | Source::ConfigFile
1728 | Source::ProductDefault
1729 | Source::RuntimePreset
1730 ),
1731 Field::RuntimeMemory | Field::Admission => matches!(
1732 source,
1733 Source::CommandLine | Source::ConfigFile | Source::RuntimePreset
1734 ),
1735 Field::Engine => {
1736 matches!(source, Source::RuntimePreset | Source::CapabilityResolution)
1737 }
1738 Field::ExecutionPlan => matches!(source, Source::Planner),
1739 Field::Sampling | Field::StructuredOutput => matches!(
1740 source,
1741 Source::UserInput
1742 | Source::CommandLine
1743 | Source::ConfigFile
1744 | Source::ProductDefault
1745 ),
1746 Field::Stop => matches!(
1747 source,
1748 Source::UserInput
1749 | Source::CommandLine
1750 | Source::ConfigFile
1751 | Source::ModelMetadata
1752 | Source::ProductDefault
1753 ),
1754 }
1755 }
1756}
1757
1758#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1759#[serde(try_from = "String", into = "String")]
1760pub struct ResolutionReasonId(String);
1761
1762impl ResolutionReasonId {
1763 pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1764 let value = value.into();
1765 validate_portable_identifier("resolution_reason_id", &value, 160)?;
1766 Ok(Self(value))
1767 }
1768
1769 pub fn as_str(&self) -> &str {
1770 &self.0
1771 }
1772}
1773
1774impl TryFrom<String> for ResolutionReasonId {
1775 type Error = VNextError;
1776
1777 fn try_from(value: String) -> Result<Self, Self::Error> {
1778 Self::new(value)
1779 }
1780}
1781
1782impl From<ResolutionReasonId> for String {
1783 fn from(value: ResolutionReasonId) -> Self {
1784 value.0
1785 }
1786}
1787
1788impl fmt::Display for ResolutionReasonId {
1789 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1790 formatter.write_str(&self.0)
1791 }
1792}
1793
1794#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1795#[serde(try_from = "String", into = "String")]
1796pub struct ResolutionFingerprint(String);
1797
1798impl ResolutionFingerprint {
1799 pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1800 let value = value.into();
1801 if !is_canonical_sha256(&value) {
1802 return Err(invalid_plan(
1803 "resolution_fingerprint",
1804 "must be a canonical lowercase SHA-256",
1805 ));
1806 }
1807 Ok(Self(value))
1808 }
1809
1810 pub fn as_str(&self) -> &str {
1811 &self.0
1812 }
1813}
1814
1815impl TryFrom<String> for ResolutionFingerprint {
1816 type Error = VNextError;
1817
1818 fn try_from(value: String) -> Result<Self, Self::Error> {
1819 Self::new(value)
1820 }
1821}
1822
1823impl From<ResolutionFingerprint> for String {
1824 fn from(value: ResolutionFingerprint) -> Self {
1825 value.0
1826 }
1827}
1828
1829impl fmt::Display for ResolutionFingerprint {
1830 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1831 formatter.write_str(&self.0)
1832 }
1833}
1834
1835#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1836#[serde(try_from = "String", into = "String")]
1837pub struct ResolutionArtifactId(String);
1838
1839impl ResolutionArtifactId {
1840 pub fn new(value: impl Into<String>) -> Result<Self, VNextError> {
1841 let value = value.into();
1842 validate_portable_identifier("resolution_artifact_id", &value, 160)?;
1843 Ok(Self(value))
1844 }
1845
1846 pub fn as_str(&self) -> &str {
1847 &self.0
1848 }
1849}
1850
1851impl TryFrom<String> for ResolutionArtifactId {
1852 type Error = VNextError;
1853
1854 fn try_from(value: String) -> Result<Self, Self::Error> {
1855 Self::new(value)
1856 }
1857}
1858
1859impl From<ResolutionArtifactId> for String {
1860 fn from(value: ResolutionArtifactId) -> Self {
1861 value.0
1862 }
1863}
1864
1865impl fmt::Display for ResolutionArtifactId {
1866 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1867 formatter.write_str(&self.0)
1868 }
1869}
1870
1871#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1875#[serde(rename_all = "snake_case", tag = "kind")]
1876pub enum ResolutionSourceProvenance {
1877 LockedModelFile {
1878 source_role: ModelArtifactSourceRole,
1879 relative_path: String,
1880 },
1881 Upstream {
1882 producer_id: String,
1883 producer_version: ContractVersion,
1884 producer_implementation_fingerprint: ResolutionFingerprint,
1885 revision: String,
1886 artifact_locator: String,
1887 },
1888}
1889
1890impl ResolutionSourceProvenance {
1891 fn validate(&self) -> Result<(), VNextError> {
1892 match self {
1893 Self::LockedModelFile {
1894 source_role: _,
1895 relative_path,
1896 } => {
1897 if !validate_source_path(relative_path) {
1898 return Err(invalid_plan(
1899 "resolution_source_provenance.locked_model_file",
1900 "relative path must identify one portable locked model file",
1901 ));
1902 }
1903 }
1904 Self::Upstream {
1905 producer_id,
1906 producer_version,
1907 revision,
1908 artifact_locator,
1909 ..
1910 } => {
1911 validate_portable_identifier(
1912 "resolution_source_provenance.producer_id",
1913 producer_id,
1914 160,
1915 )?;
1916 if producer_version.major == 0 {
1917 return Err(invalid_plan(
1918 "resolution_source_provenance.producer_version",
1919 "producer contract major version must be non-zero",
1920 ));
1921 }
1922 validate_portable_identifier(
1923 "resolution_source_provenance.revision",
1924 revision,
1925 256,
1926 )?;
1927 validate_portable_identifier(
1928 "resolution_source_provenance.artifact_locator",
1929 artifact_locator,
1930 512,
1931 )?;
1932 }
1933 }
1934 Ok(())
1935 }
1936
1937 pub fn locator(&self) -> &str {
1938 match self {
1939 Self::LockedModelFile {
1940 source_role: _,
1941 relative_path,
1942 } => relative_path,
1943 Self::Upstream {
1944 artifact_locator, ..
1945 } => artifact_locator,
1946 }
1947 }
1948}
1949
1950fn resolution_provenance_bytes(
1951 provenance: &ResolutionSourceProvenance,
1952) -> Result<usize, VNextError> {
1953 match provenance {
1954 ResolutionSourceProvenance::LockedModelFile {
1955 source_role,
1956 relative_path,
1957 } => source_role
1958 .as_str()
1959 .len()
1960 .checked_add(relative_path.len())
1961 .ok_or_else(|| {
1962 invalid_plan(
1963 "resolution_source_evidence.provenance",
1964 "provenance byte count overflowed",
1965 )
1966 }),
1967 ResolutionSourceProvenance::Upstream {
1968 producer_id,
1969 producer_implementation_fingerprint,
1970 revision,
1971 artifact_locator,
1972 ..
1973 } => [
1974 producer_id.len(),
1975 producer_implementation_fingerprint.as_str().len(),
1976 revision.len(),
1977 artifact_locator.len(),
1978 ]
1979 .into_iter()
1980 .try_fold(0usize, |total, length| {
1981 total.checked_add(length).ok_or_else(|| {
1982 invalid_plan(
1983 "resolution_source_evidence.provenance",
1984 "provenance byte count overflowed",
1985 )
1986 })
1987 }),
1988 }
1989}
1990
1991fn validate_resolution_field_paths(field_paths: &BTreeSet<String>) -> Result<(), VNextError> {
1992 if field_paths.is_empty() || field_paths.len() > MAX_RESOLUTION_FIELD_PATHS {
1993 return Err(invalid_plan(
1994 "resolution_source_evidence.field_paths",
1995 format!("must contain between 1 and {MAX_RESOLUTION_FIELD_PATHS} unique paths"),
1996 ));
1997 }
1998
1999 let mut total_bytes = 0usize;
2000 for path in field_paths {
2001 if !path.starts_with('/')
2002 || path.len() > MAX_RESOLUTION_FIELD_PATH_BYTES
2003 || path.trim() != path
2004 || path.bytes().any(|byte| byte.is_ascii_control())
2005 {
2006 return Err(invalid_plan(
2007 "resolution_source_evidence.field_paths",
2008 format!(
2009 "each path must be a portable JSON pointer of at most {MAX_RESOLUTION_FIELD_PATH_BYTES} bytes"
2010 ),
2011 ));
2012 }
2013 total_bytes = total_bytes.checked_add(path.len()).ok_or_else(|| {
2014 invalid_plan(
2015 "resolution_source_evidence.field_paths",
2016 "field-path byte count overflowed",
2017 )
2018 })?;
2019 if total_bytes > MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES {
2020 return Err(invalid_plan(
2021 "resolution_source_evidence.field_paths",
2022 format!("total path bytes must not exceed {MAX_RESOLUTION_FIELD_PATH_TOTAL_BYTES}"),
2023 ));
2024 }
2025 }
2026 Ok(())
2027}
2028
2029fn validate_resolution_source_availability(
2030 source_bytes: &[u8],
2031 provenance: &ResolutionSourceProvenance,
2032 field_paths: &BTreeSet<String>,
2033) -> Result<(), VNextError> {
2034 validate_resolution_source_bytes(source_bytes)?;
2035 let provenance_bytes = resolution_provenance_bytes(provenance)?;
2036 if provenance_bytes > MAX_RESOLUTION_PROVENANCE_BYTES {
2037 return Err(invalid_plan(
2038 "resolution_source_evidence.provenance",
2039 format!("must not exceed {MAX_RESOLUTION_PROVENANCE_BYTES} bytes"),
2040 ));
2041 }
2042 validate_resolution_field_paths(field_paths)?;
2043 provenance.validate()
2044}
2045
2046#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2047pub struct ResolutionSourceArtifact {
2048 id: ResolutionArtifactId,
2049 source: ResolutionDecisionSource,
2050 provenance: ResolutionSourceProvenance,
2051 parser: ResolutionParserDescriptor,
2052 content_size_bytes: u64,
2053 content_fingerprint: ResolutionFingerprint,
2054 canonical_document_fingerprint: ResolutionFingerprint,
2055 fields: BTreeMap<String, ResolutionFingerprint>,
2056}
2057
2058#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2059#[serde(deny_unknown_fields)]
2060pub struct UnvalidatedResolutionSourceArtifact {
2061 pub id: ResolutionArtifactId,
2062 pub source: ResolutionDecisionSource,
2063 pub provenance: ResolutionSourceProvenance,
2064 pub parser: ResolutionParserDescriptor,
2065 pub content_size_bytes: u64,
2066 pub content_fingerprint: ResolutionFingerprint,
2067 pub canonical_document_fingerprint: ResolutionFingerprint,
2068 pub fields: BTreeMap<String, ResolutionFingerprint>,
2069}
2070
2071#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2072pub struct ResolutionParserDescriptor {
2073 id: String,
2074 version: ContractVersion,
2075 implementation_fingerprint: ResolutionFingerprint,
2076}
2077
2078#[derive(Deserialize)]
2079#[serde(deny_unknown_fields)]
2080struct ResolutionParserDescriptorWire {
2081 id: String,
2082 version: ContractVersion,
2083 implementation_fingerprint: ResolutionFingerprint,
2084}
2085
2086impl<'de> Deserialize<'de> for ResolutionParserDescriptor {
2087 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2088 where
2089 D: Deserializer<'de>,
2090 {
2091 let wire = ResolutionParserDescriptorWire::deserialize(deserializer)?;
2092 Self::new(wire.id, wire.version, wire.implementation_fingerprint)
2093 .map_err(serde::de::Error::custom)
2094 }
2095}
2096
2097impl ResolutionParserDescriptor {
2098 pub fn new(
2099 id: impl Into<String>,
2100 version: ContractVersion,
2101 implementation_fingerprint: ResolutionFingerprint,
2102 ) -> Result<Self, VNextError> {
2103 let descriptor = Self {
2104 id: id.into(),
2105 version,
2106 implementation_fingerprint,
2107 };
2108 descriptor.validate()?;
2109 Ok(descriptor)
2110 }
2111
2112 fn validate(&self) -> Result<(), VNextError> {
2113 validate_portable_identifier("resolution_parser.id", &self.id, 160)?;
2114 if self.version.major == 0 {
2115 return Err(invalid_plan(
2116 "resolution_parser.version",
2117 "parser contract major version must be non-zero",
2118 ));
2119 }
2120 Ok(())
2121 }
2122
2123 pub fn id(&self) -> &str {
2124 &self.id
2125 }
2126
2127 pub const fn version(&self) -> ContractVersion {
2128 self.version
2129 }
2130
2131 pub fn implementation_fingerprint(&self) -> &ResolutionFingerprint {
2132 &self.implementation_fingerprint
2133 }
2134}
2135
2136pub trait ResolutionSourceParser: Send + Sync {
2139 fn descriptor(&self) -> Result<ResolutionParserDescriptor, VNextError>;
2140
2141 fn parse(
2142 &self,
2143 source: ResolutionDecisionSource,
2144 provenance: &ResolutionSourceProvenance,
2145 source_bytes: &[u8],
2146 ) -> Result<serde_json::Value, VNextError>;
2147}
2148
2149pub struct JsonResolutionSourceParser;
2150
2151pub static JSON_RESOLUTION_SOURCE_PARSER: JsonResolutionSourceParser = JsonResolutionSourceParser;
2152
2153impl ResolutionSourceParser for JsonResolutionSourceParser {
2154 fn descriptor(&self) -> Result<ResolutionParserDescriptor, VNextError> {
2155 ResolutionParserDescriptor::new(
2156 "resolution-parser.core-json",
2157 ContractVersion::new(1, 0),
2158 ResolutionFingerprint::new(canonical_fingerprint(
2159 &"ferrum.resolution-parser.core-json.v1",
2160 "fingerprint core JSON resolution parser",
2161 )?)?,
2162 )
2163 }
2164
2165 fn parse(
2166 &self,
2167 _source: ResolutionDecisionSource,
2168 _provenance: &ResolutionSourceProvenance,
2169 source_bytes: &[u8],
2170 ) -> Result<serde_json::Value, VNextError> {
2171 validate_resolution_source_bytes(source_bytes)?;
2172 preflight_resolution_json(source_bytes, ResolutionJsonBudget::SOURCE)?;
2173 serde_json::from_slice(source_bytes).map_err(|error| VNextError::Serialization {
2174 context: "parse resolution source JSON",
2175 message: error.to_string(),
2176 })
2177 }
2178}
2179
2180#[derive(Clone)]
2183pub struct ResolutionSourceEvidence<'a> {
2184 id: ResolutionArtifactId,
2185 source: ResolutionDecisionSource,
2186 provenance: ResolutionSourceProvenance,
2187 source_bytes: Vec<u8>,
2188 field_paths: BTreeSet<String>,
2189 parser: &'a dyn ResolutionSourceParser,
2190}
2191
2192impl<'a> ResolutionSourceEvidence<'a> {
2193 pub fn new(
2194 id: ResolutionArtifactId,
2195 source: ResolutionDecisionSource,
2196 provenance: ResolutionSourceProvenance,
2197 source_bytes: Vec<u8>,
2198 field_paths: BTreeSet<String>,
2199 parser: &'a dyn ResolutionSourceParser,
2200 ) -> Result<Self, VNextError> {
2201 validate_resolution_source_availability(&source_bytes, &provenance, &field_paths)?;
2202 Ok(Self {
2203 id,
2204 source,
2205 provenance,
2206 source_bytes,
2207 field_paths,
2208 parser,
2209 })
2210 }
2211
2212 pub fn validate(&self) -> Result<(), VNextError> {
2216 self.verify().map(drop)
2217 }
2218
2219 fn verify(&self) -> Result<ResolutionSourceArtifact, VNextError> {
2220 validate_resolution_source_availability(
2221 &self.source_bytes,
2222 &self.provenance,
2223 &self.field_paths,
2224 )?;
2225 let parser = self.parser.descriptor()?;
2226 parser.validate()?;
2227 let first_document =
2228 self.parser
2229 .parse(self.source, &self.provenance, &self.source_bytes)?;
2230 let (first_document, first_fingerprint) = validate_and_canonicalize_resolution_json(
2231 first_document,
2232 "fingerprint first parsed resolution source document",
2233 )?;
2234 let parser_after_first_parse = self.parser.descriptor()?;
2235 parser_after_first_parse.validate()?;
2236 let second_document =
2237 self.parser
2238 .parse(self.source, &self.provenance, &self.source_bytes)?;
2239 let (second_document, second_fingerprint) = validate_and_canonicalize_resolution_json(
2240 second_document,
2241 "fingerprint second parsed resolution source document",
2242 )?;
2243 let parser_after_second_parse = self.parser.descriptor()?;
2244 parser_after_second_parse.validate()?;
2245 if parser != parser_after_first_parse
2246 || parser != parser_after_second_parse
2247 || first_fingerprint != second_fingerprint
2248 || first_document != second_document
2249 {
2250 return Err(invalid_plan(
2251 "resolution_source_evidence.parser",
2252 "parser identity changed or repeated parsing of identical bytes was nondeterministic",
2253 ));
2254 }
2255 ResolutionSourceArtifact::from_verified_document(
2256 self.id.clone(),
2257 self.source,
2258 self.provenance.clone(),
2259 &self.source_bytes,
2260 parser,
2261 &first_document,
2262 first_fingerprint,
2263 self.field_paths.clone(),
2264 )
2265 }
2266
2267 pub fn id(&self) -> &ResolutionArtifactId {
2268 &self.id
2269 }
2270
2271 pub const fn source(&self) -> ResolutionDecisionSource {
2272 self.source
2273 }
2274
2275 pub fn provenance(&self) -> &ResolutionSourceProvenance {
2276 &self.provenance
2277 }
2278
2279 pub fn locator(&self) -> &str {
2280 self.provenance.locator()
2281 }
2282
2283 pub fn source_bytes(&self) -> &[u8] {
2284 &self.source_bytes
2285 }
2286
2287 pub fn field_paths(&self) -> &BTreeSet<String> {
2288 &self.field_paths
2289 }
2290}
2291
2292impl ResolutionSourceArtifact {
2293 fn from_verified_document(
2294 id: ResolutionArtifactId,
2295 source: ResolutionDecisionSource,
2296 provenance: ResolutionSourceProvenance,
2297 bytes: &[u8],
2298 parser: ResolutionParserDescriptor,
2299 document: &serde_json::Value,
2300 canonical_document_fingerprint: String,
2301 field_paths: BTreeSet<String>,
2302 ) -> Result<Self, VNextError> {
2303 validate_resolution_source_availability(bytes, &provenance, &field_paths)?;
2304 let content_size_bytes = u64::try_from(bytes.len()).map_err(|_| {
2305 invalid_plan(
2306 "resolution_source_artifact.content_size_bytes",
2307 "source byte length does not fit u64",
2308 )
2309 })?;
2310 let fields = field_paths
2311 .into_iter()
2312 .map(|path| {
2313 let value = document.pointer(&path).ok_or_else(|| {
2314 invalid_plan(
2315 "resolution_source_artifact.fields",
2316 format!("JSON pointer `{path}` is absent from the source document"),
2317 )
2318 })?;
2319 ResolutionFingerprint::new(canonical_value_fingerprint(
2320 value,
2321 "fingerprint resolution source field",
2322 )?)
2323 .map(|fingerprint| (path, fingerprint))
2324 })
2325 .collect::<Result<BTreeMap<_, _>, VNextError>>()?;
2326 Ok(Self {
2327 id,
2328 source,
2329 provenance,
2330 parser,
2331 content_size_bytes,
2332 content_fingerprint: ResolutionFingerprint::new(format!(
2333 "{:x}",
2334 Sha256::digest(bytes)
2335 ))?,
2336 canonical_document_fingerprint: ResolutionFingerprint::new(
2337 canonical_document_fingerprint,
2338 )?,
2339 fields,
2340 })
2341 }
2342
2343 pub fn id(&self) -> &ResolutionArtifactId {
2344 &self.id
2345 }
2346
2347 pub fn source(&self) -> ResolutionDecisionSource {
2348 self.source
2349 }
2350
2351 pub fn provenance(&self) -> &ResolutionSourceProvenance {
2352 &self.provenance
2353 }
2354
2355 pub fn locator(&self) -> &str {
2356 self.provenance.locator()
2357 }
2358
2359 pub fn content_size_bytes(&self) -> u64 {
2360 self.content_size_bytes
2361 }
2362
2363 pub fn content_fingerprint(&self) -> &ResolutionFingerprint {
2364 &self.content_fingerprint
2365 }
2366
2367 pub fn parser(&self) -> &ResolutionParserDescriptor {
2368 &self.parser
2369 }
2370
2371 pub fn canonical_document_fingerprint(&self) -> &ResolutionFingerprint {
2372 &self.canonical_document_fingerprint
2373 }
2374
2375 pub fn fields(&self) -> &BTreeMap<String, ResolutionFingerprint> {
2376 &self.fields
2377 }
2378}
2379
2380impl UnvalidatedResolutionSourceArtifact {
2381 fn revalidate(
2382 self,
2383 expected: &ResolutionSourceArtifact,
2384 ) -> Result<ResolutionSourceArtifact, VNextError> {
2385 if self.id != expected.id
2386 || self.source != expected.source
2387 || self.provenance != expected.provenance
2388 || self.parser != expected.parser
2389 || self.content_size_bytes != expected.content_size_bytes
2390 || self.content_fingerprint != expected.content_fingerprint
2391 || self.canonical_document_fingerprint != expected.canonical_document_fingerprint
2392 || self.fields != expected.fields
2393 {
2394 return Err(invalid_plan(
2395 "source_artifacts",
2396 format!(
2397 "serialized source artifact `{}` differs from externally verified evidence",
2398 self.id
2399 ),
2400 ));
2401 }
2402 Ok(expected.clone())
2403 }
2404}
2405
2406#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2407pub struct ResolutionDecisionEvidence {
2408 source_artifact_id: ResolutionArtifactId,
2409 source_field_path: String,
2410 chosen_value_fingerprint: ResolutionFingerprint,
2411}
2412
2413#[derive(Deserialize)]
2414#[serde(deny_unknown_fields)]
2415struct ResolutionDecisionEvidenceWire {
2416 source_artifact_id: ResolutionArtifactId,
2417 source_field_path: String,
2418 chosen_value_fingerprint: ResolutionFingerprint,
2419}
2420
2421impl<'de> Deserialize<'de> for ResolutionDecisionEvidence {
2422 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2423 where
2424 D: Deserializer<'de>,
2425 {
2426 let wire = ResolutionDecisionEvidenceWire::deserialize(deserializer)?;
2427 Self::new(
2428 wire.source_artifact_id,
2429 wire.source_field_path,
2430 wire.chosen_value_fingerprint,
2431 )
2432 .map_err(serde::de::Error::custom)
2433 }
2434}
2435
2436impl ResolutionDecisionEvidence {
2437 fn new(
2438 source_artifact_id: ResolutionArtifactId,
2439 source_field_path: impl Into<String>,
2440 chosen_value_fingerprint: ResolutionFingerprint,
2441 ) -> Result<Self, VNextError> {
2442 let source_field_path = source_field_path.into();
2443 if source_field_path.is_empty()
2444 || source_field_path.len() > 512
2445 || source_field_path.trim() != source_field_path
2446 || source_field_path
2447 .bytes()
2448 .any(|byte| byte.is_ascii_control())
2449 {
2450 return Err(invalid_plan(
2451 "decisions.evidence.source_field_path",
2452 "must be a non-empty portable path of at most 512 bytes",
2453 ));
2454 }
2455 Ok(Self {
2456 source_artifact_id,
2457 source_field_path,
2458 chosen_value_fingerprint,
2459 })
2460 }
2461
2462 pub fn source_artifact_id(&self) -> &ResolutionArtifactId {
2463 &self.source_artifact_id
2464 }
2465
2466 pub fn source_field_path(&self) -> &str {
2467 &self.source_field_path
2468 }
2469
2470 pub fn chosen_value_fingerprint(&self) -> &ResolutionFingerprint {
2471 &self.chosen_value_fingerprint
2472 }
2473}
2474
2475#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2476pub struct ResolutionDecision {
2477 field: ResolutionField,
2478 source: ResolutionDecisionSource,
2479 reason_id: ResolutionReasonId,
2480 evidence: ResolutionDecisionEvidence,
2481}
2482
2483#[derive(Deserialize)]
2484#[serde(deny_unknown_fields)]
2485struct ResolutionDecisionWire {
2486 field: ResolutionField,
2487 source: ResolutionDecisionSource,
2488 reason_id: ResolutionReasonId,
2489 evidence: ResolutionDecisionEvidence,
2490}
2491
2492impl<'de> Deserialize<'de> for ResolutionDecision {
2493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2494 where
2495 D: Deserializer<'de>,
2496 {
2497 let wire = ResolutionDecisionWire::deserialize(deserializer)?;
2498 Ok(Self::new(
2499 wire.field,
2500 wire.source,
2501 wire.reason_id,
2502 wire.evidence,
2503 ))
2504 }
2505}
2506
2507impl ResolutionDecision {
2508 fn new(
2509 field: ResolutionField,
2510 source: ResolutionDecisionSource,
2511 reason_id: ResolutionReasonId,
2512 evidence: ResolutionDecisionEvidence,
2513 ) -> Self {
2514 Self {
2515 field,
2516 source,
2517 reason_id,
2518 evidence,
2519 }
2520 }
2521
2522 pub fn field(&self) -> ResolutionField {
2523 self.field
2524 }
2525
2526 pub fn source(&self) -> ResolutionDecisionSource {
2527 self.source
2528 }
2529
2530 pub fn reason_id(&self) -> &ResolutionReasonId {
2531 &self.reason_id
2532 }
2533
2534 pub fn evidence(&self) -> &ResolutionDecisionEvidence {
2535 &self.evidence
2536 }
2537}
2538
2539#[derive(Debug, Clone, PartialEq, Eq)]
2543pub struct ResolutionDecisionBinding {
2544 field: ResolutionField,
2545 source: ResolutionDecisionSource,
2546 reason_id: ResolutionReasonId,
2547 source_artifact_id: ResolutionArtifactId,
2548 source_field_path: String,
2549}
2550
2551impl ResolutionDecisionBinding {
2552 pub fn new(
2553 field: ResolutionField,
2554 source: ResolutionDecisionSource,
2555 reason_id: ResolutionReasonId,
2556 source_artifact_id: ResolutionArtifactId,
2557 source_field_path: impl Into<String>,
2558 ) -> Result<Self, VNextError> {
2559 let source_field_path = source_field_path.into();
2560 if !field.accepts_source(source) {
2561 return Err(invalid_plan(
2562 "decision_bindings.source",
2563 format!("source `{source:?}` cannot author field `{field:?}`"),
2564 ));
2565 }
2566 if !source_field_path.starts_with('/')
2567 || source_field_path.len() > 512
2568 || source_field_path.trim() != source_field_path
2569 || source_field_path
2570 .bytes()
2571 .any(|byte| byte.is_ascii_control())
2572 {
2573 return Err(invalid_plan(
2574 "decision_bindings.source_field_path",
2575 "must be a portable JSON pointer of at most 512 bytes",
2576 ));
2577 }
2578 Ok(Self {
2579 field,
2580 source,
2581 reason_id,
2582 source_artifact_id,
2583 source_field_path,
2584 })
2585 }
2586
2587 pub fn field(&self) -> ResolutionField {
2588 self.field
2589 }
2590
2591 pub fn source(&self) -> ResolutionDecisionSource {
2592 self.source
2593 }
2594
2595 pub fn reason_id(&self) -> &ResolutionReasonId {
2596 &self.reason_id
2597 }
2598
2599 pub fn source_artifact_id(&self) -> &ResolutionArtifactId {
2600 &self.source_artifact_id
2601 }
2602
2603 pub fn source_field_path(&self) -> &str {
2604 &self.source_field_path
2605 }
2606}
2607
2608#[derive(Debug, Clone, PartialEq, Eq)]
2609pub struct ResolvedModelPlanInputs {
2610 pub original_sources: OriginalModelSources,
2611 pub resolved_sources: ResolvedModelSources,
2612 pub config: ModelConfigFingerprint,
2613 pub external_metadata_id: super::ExternalModelMetadataId,
2614 pub prepared_family: PreparedModelFamily,
2615 pub tokenizer: TokenizerDescriptor,
2616 pub device: DeviceDescriptor,
2617 pub capabilities: CapabilityCatalog,
2618 pub runtime: ResolvedRuntimePolicy,
2619 pub engine: EngineSelection,
2620 pub execution_plan: ExecutionPlan,
2621 pub sampling: SamplingPolicy,
2622 pub stop: StopPolicy,
2623 pub structured_output: StructuredOutputPolicy,
2624}
2625
2626#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2627pub struct ResolvedModelPlanParts {
2628 source_artifacts: Vec<ResolutionSourceArtifact>,
2629 pub original_sources: OriginalModelSources,
2630 pub resolved_sources: ResolvedModelSources,
2631 pub config: ModelConfigFingerprint,
2632 pub external_metadata_id: super::ExternalModelMetadataId,
2633 pub prepared_family: PreparedModelFamily,
2634 pub tokenizer: TokenizerDescriptor,
2635 pub device: DeviceDescriptor,
2636 pub capabilities: CapabilityCatalog,
2637 pub runtime: ResolvedRuntimePolicy,
2638 pub engine: EngineSelection,
2639 pub execution_plan: ExecutionPlan,
2640 pub sampling: SamplingPolicy,
2641 pub stop: StopPolicy,
2642 pub structured_output: StructuredOutputPolicy,
2643 decisions: Vec<ResolutionDecision>,
2644}
2645
2646impl ResolvedModelPlanParts {
2647 pub fn source_artifacts(&self) -> &[ResolutionSourceArtifact] {
2648 &self.source_artifacts
2649 }
2650
2651 pub fn decisions(&self) -> &[ResolutionDecision] {
2652 &self.decisions
2653 }
2654}
2655
2656pub struct ResolvedPlanValidationContext<'a> {
2661 registry: &'a dyn ModelFamilyRegistry,
2662 source_evidence: &'a [ResolutionSourceEvidence<'a>],
2663 node_resolutions: &'a [PlanNodeResolution],
2664 device: &'a DeviceDescriptor,
2665 capabilities: &'a CapabilityCatalog,
2666 runtime: &'a ResolvedRuntimePolicy,
2667 completion_retention: CompletionRetentionSpec,
2668}
2669
2670impl<'a> ResolvedPlanValidationContext<'a> {
2671 pub fn new(
2672 registry: &'a dyn ModelFamilyRegistry,
2673 source_evidence: &'a [ResolutionSourceEvidence<'a>],
2674 node_resolutions: &'a [PlanNodeResolution],
2675 device: &'a DeviceDescriptor,
2676 capabilities: &'a CapabilityCatalog,
2677 runtime: &'a ResolvedRuntimePolicy,
2678 ) -> Self {
2679 Self {
2680 registry,
2681 source_evidence,
2682 node_resolutions,
2683 device,
2684 capabilities,
2685 runtime,
2686 completion_retention: CompletionRetentionSpec::default(),
2687 }
2688 }
2689
2690 pub fn with_completion_retention(
2694 mut self,
2695 completion_retention: CompletionRetentionSpec,
2696 ) -> Self {
2697 self.completion_retention = completion_retention;
2698 self
2699 }
2700
2701 pub fn registry(&self) -> &dyn ModelFamilyRegistry {
2702 self.registry
2703 }
2704
2705 fn verify_source_artifacts(&self) -> Result<Vec<ResolutionSourceArtifact>, VNextError> {
2706 self.source_evidence
2707 .iter()
2708 .map(ResolutionSourceEvidence::verify)
2709 .collect()
2710 }
2711
2712 pub fn node_resolutions(&self) -> &[PlanNodeResolution] {
2713 self.node_resolutions
2714 }
2715
2716 pub fn device(&self) -> &DeviceDescriptor {
2717 self.device
2718 }
2719
2720 pub fn capabilities(&self) -> &CapabilityCatalog {
2721 self.capabilities
2722 }
2723
2724 pub fn runtime(&self) -> &ResolvedRuntimePolicy {
2725 self.runtime
2726 }
2727
2728 pub fn completion_retention(&self) -> &CompletionRetentionSpec {
2729 &self.completion_retention
2730 }
2731}
2732
2733#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2735pub struct ResolvedModelPlan {
2736 parts: ResolvedModelPlanParts,
2737 fingerprint: ResolutionFingerprint,
2738}
2739
2740impl ExecutablePlanView for ResolvedModelPlan {
2741 fn execution_plan(&self) -> &ExecutionPlan {
2742 &self.parts.execution_plan
2743 }
2744
2745 fn device(&self) -> &DeviceDescriptor {
2746 &self.parts.device
2747 }
2748
2749 fn capabilities(&self) -> &CapabilityCatalog {
2750 &self.parts.capabilities
2751 }
2752}
2753
2754#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
2755#[serde(deny_unknown_fields)]
2756pub struct UnvalidatedResolvedModelPlanParts {
2757 source_artifacts: Vec<UnvalidatedResolutionSourceArtifact>,
2758 original_sources: OriginalModelSources,
2759 resolved_sources: ResolvedModelSources,
2760 config: ModelConfigFingerprint,
2761 external_metadata_id: super::ExternalModelMetadataId,
2762 prepared_family: PreparedModelFamilyWire,
2763 tokenizer: TokenizerDescriptor,
2764 device: DeviceDescriptor,
2765 capabilities: CapabilityCatalog,
2766 runtime: ResolvedRuntimePolicy,
2767 engine: EngineSelection,
2768 execution_plan: UnvalidatedExecutionPlanWire,
2769 sampling: SamplingPolicy,
2770 stop: StopPolicy,
2771 structured_output: StructuredOutputPolicy,
2772 decisions: Vec<ResolutionDecision>,
2773}
2774
2775#[derive(Debug, Clone, PartialEq, Eq)]
2776pub struct UnvalidatedResolvedModelPlan {
2777 parts: UnvalidatedResolvedModelPlanParts,
2778 fingerprint: ResolutionFingerprint,
2779}
2780
2781#[derive(Serialize)]
2782struct ResolvedModelPlanWire {
2783 parts: UnvalidatedResolvedModelPlanParts,
2784 fingerprint: ResolutionFingerprint,
2785}
2786
2787#[derive(Deserialize, Serialize)]
2788#[serde(deny_unknown_fields)]
2789struct ResolvedModelPlanWireFields {
2790 parts: UnvalidatedResolvedModelPlanParts,
2791 fingerprint: ResolutionFingerprint,
2792}
2793
2794impl<'de> Deserialize<'de> for ResolvedModelPlanWire {
2795 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2796 where
2797 D: Deserializer<'de>,
2798 {
2799 let raw = serde_json::Value::deserialize(deserializer)?;
2800 let fields =
2801 ResolvedModelPlanWireFields::deserialize(&raw).map_err(serde::de::Error::custom)?;
2802 let canonical = serde_json::to_value(&fields).map_err(serde::de::Error::custom)?;
2803 if canonical != raw {
2804 return Err(serde::de::Error::custom(
2805 "resolved model plan wire contains unknown or non-canonical nested fields",
2806 ));
2807 }
2808 Ok(Self {
2809 parts: fields.parts,
2810 fingerprint: fields.fingerprint,
2811 })
2812 }
2813}
2814
2815impl From<ResolvedModelPlanWire> for UnvalidatedResolvedModelPlan {
2816 fn from(wire: ResolvedModelPlanWire) -> Self {
2817 Self {
2818 parts: wire.parts,
2819 fingerprint: wire.fingerprint,
2820 }
2821 }
2822}
2823
2824impl UnvalidatedResolvedModelPlan {
2825 pub fn revalidate(
2826 self,
2827 context: &ResolvedPlanValidationContext<'_>,
2828 ) -> Result<ResolvedModelPlan, VNextError> {
2829 let UnvalidatedResolvedModelPlanParts {
2830 source_artifacts,
2831 original_sources,
2832 resolved_sources,
2833 config,
2834 external_metadata_id,
2835 prepared_family,
2836 tokenizer,
2837 device,
2838 capabilities,
2839 runtime,
2840 engine,
2841 execution_plan,
2842 sampling,
2843 stop,
2844 structured_output,
2845 decisions,
2846 } = self.parts;
2847 let verified_source_artifacts = context.verify_source_artifacts()?;
2848 let source_artifacts =
2849 Self::revalidate_source_artifacts(source_artifacts, &verified_source_artifacts)?;
2850 let prepared_family =
2851 UnvalidatedPreparedModelFamily::from(prepared_family).revalidate(context.registry())?;
2852 if device != *context.device()
2853 || capabilities != *context.capabilities()
2854 || runtime != *context.runtime()
2855 {
2856 return Err(invalid_plan(
2857 "validation_context",
2858 "serialized device, capability catalog, or runtime policy differs from external trusted inputs",
2859 ));
2860 }
2861 let execution_plan = UnvalidatedExecutionPlan::from(execution_plan)
2862 .revalidate_with_completion_retention(
2863 &prepared_family,
2864 context.capabilities(),
2865 context.runtime(),
2866 context.node_resolutions().to_vec(),
2867 context.completion_retention().clone(),
2868 )?;
2869 let serialized_decisions = decisions;
2870 let decision_bindings = serialized_decisions
2871 .iter()
2872 .map(|decision| {
2873 ResolutionDecisionBinding::new(
2874 decision.field,
2875 decision.source,
2876 decision.reason_id.clone(),
2877 decision.evidence.source_artifact_id.clone(),
2878 decision.evidence.source_field_path.clone(),
2879 )
2880 })
2881 .collect::<Result<Vec<_>, VNextError>>()?;
2882 let rebuilt = ResolvedModelPlan::from_verified_inputs(
2883 ResolvedModelPlanInputs {
2884 original_sources,
2885 resolved_sources,
2886 config,
2887 external_metadata_id,
2888 prepared_family,
2889 tokenizer,
2890 device: context.device().clone(),
2891 capabilities: context.capabilities().clone(),
2892 runtime: context.runtime().clone(),
2893 engine,
2894 execution_plan,
2895 sampling,
2896 stop,
2897 structured_output,
2898 },
2899 decision_bindings,
2900 source_artifacts,
2901 context,
2902 )?;
2903 if rebuilt.parts.decisions != serialized_decisions {
2904 return Err(invalid_plan(
2905 "decisions",
2906 "serialized decisions differ from decisions rebuilt from external raw evidence",
2907 ));
2908 }
2909 if rebuilt.fingerprint != self.fingerprint {
2910 return Err(invalid_plan(
2911 "fingerprint",
2912 format!(
2913 "does not match typed reconstruction: expected `{}`, actual `{}`",
2914 rebuilt.fingerprint, self.fingerprint
2915 ),
2916 ));
2917 }
2918 Ok(rebuilt)
2919 }
2920
2921 fn revalidate_source_artifacts(
2922 serialized: Vec<UnvalidatedResolutionSourceArtifact>,
2923 expected: &[ResolutionSourceArtifact],
2924 ) -> Result<Vec<ResolutionSourceArtifact>, VNextError> {
2925 let mut expected_by_id = BTreeMap::new();
2926 for artifact in expected {
2927 if expected_by_id
2928 .insert(artifact.id().clone(), artifact)
2929 .is_some()
2930 {
2931 return Err(invalid_plan(
2932 "validation_context.source_artifacts",
2933 format!("duplicate externally verified artifact `{}`", artifact.id()),
2934 ));
2935 }
2936 }
2937 if serialized.len() != expected_by_id.len() {
2938 return Err(invalid_plan(
2939 "source_artifacts",
2940 "serialized and externally verified source artifact sets differ",
2941 ));
2942 }
2943
2944 let mut seen = BTreeSet::new();
2945 let mut validated = Vec::with_capacity(serialized.len());
2946 for artifact in serialized {
2947 if !seen.insert(artifact.id.clone()) {
2948 return Err(invalid_plan(
2949 "source_artifacts",
2950 format!("duplicate serialized artifact `{}`", artifact.id),
2951 ));
2952 }
2953 let expected = expected_by_id.get(&artifact.id).ok_or_else(|| {
2954 invalid_plan(
2955 "source_artifacts",
2956 format!(
2957 "serialized artifact `{}` has no externally verified evidence",
2958 artifact.id
2959 ),
2960 )
2961 })?;
2962 validated.push(artifact.revalidate(expected)?);
2963 }
2964 Ok(validated)
2965 }
2966}
2967
2968impl ResolvedModelPlan {
2969 pub fn new(
2970 inputs: ResolvedModelPlanInputs,
2971 decision_bindings: Vec<ResolutionDecisionBinding>,
2972 context: &ResolvedPlanValidationContext<'_>,
2973 ) -> Result<Self, VNextError> {
2974 let source_artifacts = context.verify_source_artifacts()?;
2977 Self::from_verified_inputs(inputs, decision_bindings, source_artifacts, context)
2978 }
2979
2980 fn from_verified_inputs(
2981 inputs: ResolvedModelPlanInputs,
2982 decision_bindings: Vec<ResolutionDecisionBinding>,
2983 source_artifacts: Vec<ResolutionSourceArtifact>,
2984 context: &ResolvedPlanValidationContext<'_>,
2985 ) -> Result<Self, VNextError> {
2986 Self::validate_external_inputs(&inputs, context)?;
2987 let ResolvedModelPlanInputs {
2988 original_sources,
2989 resolved_sources,
2990 config,
2991 external_metadata_id,
2992 prepared_family,
2993 tokenizer,
2994 device,
2995 capabilities,
2996 runtime,
2997 engine,
2998 execution_plan,
2999 sampling,
3000 stop,
3001 structured_output,
3002 } = inputs;
3003 let mut parts = ResolvedModelPlanParts {
3004 source_artifacts,
3005 original_sources,
3006 resolved_sources,
3007 config,
3008 external_metadata_id,
3009 prepared_family,
3010 tokenizer,
3011 device,
3012 capabilities,
3013 runtime,
3014 engine,
3015 execution_plan,
3016 sampling,
3017 stop,
3018 structured_output,
3019 decisions: Vec::new(),
3020 };
3021 Self::normalize(&mut parts);
3022 parts.decisions = Self::bind_decisions(&parts, decision_bindings)?;
3023 Self::normalize(&mut parts);
3024 Self::validate(
3025 &parts,
3026 context.node_resolutions(),
3027 context.completion_retention(),
3028 )?;
3029 let fingerprint = ResolutionFingerprint::new(canonical_fingerprint(
3030 &parts,
3031 "serialize resolved model plan",
3032 )?)?;
3033 Ok(Self { parts, fingerprint })
3034 }
3035
3036 fn validate_external_inputs(
3037 inputs: &ResolvedModelPlanInputs,
3038 context: &ResolvedPlanValidationContext<'_>,
3039 ) -> Result<(), VNextError> {
3040 let family_registration = context
3041 .registry()
3042 .resolve(inputs.prepared_family.family_id())?;
3043 let metadata_registration = context
3044 .registry()
3045 .resolve_external(&inputs.external_metadata_id)?;
3046 if !family_registration
3047 .external_metadata_ids()
3048 .contains(&inputs.external_metadata_id)
3049 || !std::ptr::eq(family_registration, metadata_registration)
3050 || inputs.prepared_family.external_metadata_id() != &inputs.external_metadata_id
3051 {
3052 return Err(invalid_plan(
3053 "external_metadata_id",
3054 format!(
3055 "external metadata `{}`, prepared identity `{}`, and internal family `{}` must identify the same registration and exact typed configuration",
3056 inputs.external_metadata_id,
3057 inputs.prepared_family.external_metadata_id(),
3058 inputs.prepared_family.family_id(),
3059 ),
3060 ));
3061 }
3062 let externally_prepared =
3063 family_registration.prepare(inputs.prepared_family.canonical_config())?;
3064 if externally_prepared != inputs.prepared_family {
3065 return Err(invalid_plan(
3066 "validation_context.model_registry",
3067 "prepared model family differs from external typed registry reconstruction",
3068 ));
3069 }
3070 if inputs.device != *context.device()
3071 || inputs.capabilities != *context.capabilities()
3072 || inputs.runtime != *context.runtime()
3073 {
3074 return Err(invalid_plan(
3075 "validation_context",
3076 "device, capability catalog, or runtime policy differs from external trusted inputs",
3077 ));
3078 }
3079 Ok(())
3080 }
3081
3082 fn bind_decisions(
3083 parts: &ResolvedModelPlanParts,
3084 bindings: Vec<ResolutionDecisionBinding>,
3085 ) -> Result<Vec<ResolutionDecision>, VNextError> {
3086 let expected_fingerprints = Self::decision_fingerprints(parts)?;
3087 let mut artifacts = BTreeMap::new();
3088 for artifact in &parts.source_artifacts {
3089 if artifacts.insert(artifact.id.clone(), artifact).is_some() {
3090 return Err(invalid_plan(
3091 "source_artifacts",
3092 format!("duplicate source artifact `{}`", artifact.id),
3093 ));
3094 }
3095 }
3096 let mut fields = BTreeSet::new();
3097 let mut used_artifacts = BTreeSet::new();
3098 let mut used_artifact_fields = BTreeSet::new();
3099 let mut decisions = Vec::with_capacity(bindings.len());
3100 for binding in bindings {
3101 if !fields.insert(binding.field) {
3102 return Err(invalid_plan(
3103 "decision_bindings",
3104 format!("field `{:?}` has more than one binding", binding.field),
3105 ));
3106 }
3107 if !binding.field.accepts_source(binding.source) {
3108 return Err(invalid_plan(
3109 "decision_bindings.source",
3110 format!(
3111 "source `{:?}` cannot author field `{:?}`",
3112 binding.source, binding.field
3113 ),
3114 ));
3115 }
3116 let expected = expected_fingerprints.get(&binding.field).ok_or_else(|| {
3117 invalid_plan(
3118 "decision_bindings.field",
3119 format!("field `{:?}` is not resolvable", binding.field),
3120 )
3121 })?;
3122 let artifact = artifacts.get(&binding.source_artifact_id).ok_or_else(|| {
3123 invalid_plan(
3124 "decision_bindings.source_artifact_id",
3125 format!(
3126 "binding for `{:?}` references unknown source artifact `{}`",
3127 binding.field, binding.source_artifact_id
3128 ),
3129 )
3130 })?;
3131 if artifact.source != binding.source {
3132 return Err(invalid_plan(
3133 "decision_bindings.source",
3134 format!(
3135 "binding for `{:?}` source differs from artifact `{}`",
3136 binding.field, artifact.id
3137 ),
3138 ));
3139 }
3140 let source_fingerprint =
3141 artifact
3142 .fields
3143 .get(&binding.source_field_path)
3144 .ok_or_else(|| {
3145 invalid_plan(
3146 "decision_bindings.source_field_path",
3147 format!(
3148 "binding for `{:?}` references a field absent from artifact `{}`",
3149 binding.field, artifact.id
3150 ),
3151 )
3152 })?;
3153 if source_fingerprint.as_str() != expected {
3154 return Err(invalid_plan(
3155 "decision_bindings.source_field_path",
3156 format!(
3157 "externally parsed field for `{:?}` differs from the resolved value",
3158 binding.field
3159 ),
3160 ));
3161 }
3162 used_artifacts.insert(artifact.id.clone());
3163 if !used_artifact_fields
3164 .insert((artifact.id.clone(), binding.source_field_path.clone()))
3165 {
3166 return Err(invalid_plan(
3167 "decision_bindings.source_field_path",
3168 "one parsed artifact field cannot issue more than one decision",
3169 ));
3170 }
3171 decisions.push(ResolutionDecision::new(
3172 binding.field,
3173 binding.source,
3174 binding.reason_id,
3175 ResolutionDecisionEvidence::new(
3176 binding.source_artifact_id,
3177 binding.source_field_path,
3178 source_fingerprint.clone(),
3179 )?,
3180 ));
3181 }
3182 if fields
3183 != expected_fingerprints
3184 .keys()
3185 .copied()
3186 .collect::<BTreeSet<_>>()
3187 {
3188 return Err(invalid_plan(
3189 "decision_bindings",
3190 "every resolved field requires exactly one external evidence binding",
3191 ));
3192 }
3193 if used_artifacts != artifacts.keys().cloned().collect::<BTreeSet<_>>() {
3194 return Err(invalid_plan(
3195 "source_artifacts",
3196 "externally verified source evidence and referenced binding sets differ",
3197 ));
3198 }
3199 let available_artifact_fields = artifacts
3200 .values()
3201 .flat_map(|artifact| {
3202 artifact
3203 .fields
3204 .keys()
3205 .cloned()
3206 .map(|path| (artifact.id.clone(), path))
3207 })
3208 .collect::<BTreeSet<_>>();
3209 if used_artifact_fields != available_artifact_fields {
3210 return Err(invalid_plan(
3211 "source_artifacts.fields",
3212 "externally parsed source fields and decision binding fields differ",
3213 ));
3214 }
3215 Ok(decisions)
3216 }
3217
3218 fn normalize(parts: &mut ResolvedModelPlanParts) {
3219 parts
3220 .source_artifacts
3221 .sort_by(|left, right| left.id.cmp(&right.id));
3222 for role in ModelArtifactSourceRole::ALL {
3223 parts
3224 .resolved_sources
3225 .for_role_mut(role)
3226 .files
3227 .sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
3228 }
3229 parts.stop.strings.sort();
3230 parts.decisions.sort_by_key(|decision| decision.field);
3231 }
3232
3233 fn validate(
3234 parts: &ResolvedModelPlanParts,
3235 node_resolutions: &[PlanNodeResolution],
3236 completion_retention: &CompletionRetentionSpec,
3237 ) -> Result<(), VNextError> {
3238 let mut source_artifacts = BTreeMap::new();
3239 for artifact in &parts.source_artifacts {
3240 if source_artifacts
3241 .insert(artifact.id.clone(), artifact)
3242 .is_some()
3243 {
3244 return Err(invalid_plan(
3245 "source_artifacts",
3246 format!("duplicate source artifact `{}`", artifact.id),
3247 ));
3248 }
3249 artifact.provenance.validate()?;
3250 match &artifact.provenance {
3251 ResolutionSourceProvenance::LockedModelFile {
3252 source_role,
3253 relative_path,
3254 } => {
3255 let Some(file) = parts
3256 .resolved_sources
3257 .for_role(*source_role)
3258 .files
3259 .iter()
3260 .find(|file| &file.relative_path == relative_path)
3261 else {
3262 return Err(invalid_plan(
3263 "source_artifacts.provenance",
3264 format!(
3265 "artifact `{}` does not resolve to {source_role:?} locked file `{relative_path}`",
3266 artifact.id,
3267 ),
3268 ));
3269 };
3270 if file.sha256 != artifact.content_fingerprint.as_str()
3271 || file.size_bytes != artifact.content_size_bytes
3272 {
3273 return Err(invalid_plan(
3274 "source_artifacts.provenance",
3275 format!(
3276 "artifact `{}` bytes differ from locked file `{relative_path}` SHA or size",
3277 artifact.id
3278 ),
3279 ));
3280 }
3281 }
3282 ResolutionSourceProvenance::Upstream { .. }
3283 if artifact.source == ResolutionDecisionSource::ModelMetadata =>
3284 {
3285 return Err(invalid_plan(
3286 "source_artifacts.provenance",
3287 format!(
3288 "model-metadata artifact `{}` must bind a locked model file",
3289 artifact.id
3290 ),
3291 ));
3292 }
3293 ResolutionSourceProvenance::Upstream { .. } => {}
3294 }
3295 }
3296 for role in ModelArtifactSourceRole::ALL {
3297 let original = parts.original_sources.for_role(role);
3298 let resolved = parts.resolved_sources.for_role(role);
3299 if original.location.trim().is_empty()
3300 || matches!(
3301 original.requested_revision.as_deref(),
3302 Some(revision) if revision.trim().is_empty()
3303 )
3304 || resolved.canonical_location.trim().is_empty()
3305 || resolved.resolved_revision.trim().is_empty()
3306 {
3307 return Err(invalid_plan(
3308 format!("{}_source", role.as_str()),
3309 "source locations and revisions must be non-empty",
3310 ));
3311 }
3312 if resolved.files.is_empty() {
3313 return Err(invalid_plan(
3314 format!("resolved_sources.{}.files", role.as_str()),
3315 "at least one fingerprinted file is required",
3316 ));
3317 }
3318 let mut paths = BTreeSet::new();
3319 if resolved.files.iter().any(|file| {
3320 !validate_source_path(&file.relative_path)
3321 || file.size_bytes == 0
3322 || !is_canonical_sha256(&file.sha256)
3323 || !paths.insert(file.relative_path.clone())
3324 }) {
3325 return Err(invalid_plan(
3326 format!("resolved_sources.{}.files", role.as_str()),
3327 "file paths, sizes, and canonical hashes must be valid and unique",
3328 ));
3329 }
3330 }
3331 let family = &parts.prepared_family;
3332 let program_fingerprint = family.program().fingerprint()?;
3333 let family_fingerprint = family.fingerprint()?;
3334 if parts.tokenizer.vocabulary_size == 0
3335 || family.metadata().template.template.trim().is_empty()
3336 || family.metadata().special_tokens.eos_token_ids.is_empty()
3337 {
3338 return Err(invalid_plan(
3339 "model_metadata",
3340 "program, tokenizer, template, and special-token metadata are incomplete",
3341 ));
3342 }
3343 Self::validate_source_file_binding(
3344 ModelArtifactSourceRole::Semantic,
3345 &parts.resolved_sources.semantic,
3346 &parts.config.source_file,
3347 &parts.config.sha256,
3348 "config",
3349 )?;
3350 if parts.config.typed_config_sha256 != family.config_fingerprint() {
3351 return Err(invalid_plan(
3352 "config.typed_config_sha256",
3353 "does not match the typed model family configuration",
3354 ));
3355 }
3356 Self::validate_source_file_binding(
3357 ModelArtifactSourceRole::Tokenizer,
3358 &parts.resolved_sources.tokenizer,
3359 &parts.tokenizer.source_file,
3360 &parts.tokenizer.sha256,
3361 "tokenizer",
3362 )?;
3363 Self::validate_source_file_binding(
3364 ModelArtifactSourceRole::Tokenizer,
3365 &parts.resolved_sources.tokenizer,
3366 &family.metadata().template.source_file,
3367 &family.metadata().template.sha256,
3368 "template",
3369 )?;
3370 Self::validate_token_contract(parts)?;
3371 parts.runtime.validate()?;
3372 parts
3373 .execution_plan
3374 .validate_against_with_completion_retention(
3375 family,
3376 &parts.capabilities,
3377 &parts.runtime,
3378 node_resolutions,
3379 completion_retention.clone(),
3380 )?;
3381 let runtime_fingerprint = super::canonical_runtime_policy_fingerprint(&parts.runtime)?;
3382 let capability_fingerprint = parts.capabilities.fingerprint()?;
3383 if &parts.device != parts.capabilities.device()
3384 || &parts.device.id != parts.execution_plan.payload().device_id()
3385 || family.family_id() != parts.execution_plan.payload().family_id()
3386 || family_fingerprint != parts.execution_plan.payload().prepared_family_fingerprint()
3387 || runtime_fingerprint != parts.execution_plan.payload().policy_fingerprint()
3388 || program_fingerprint != parts.execution_plan.payload().program_fingerprint()
3389 || &parts
3390 .execution_plan
3391 .payload()
3392 .execution_weights()
3393 .schema()
3394 .format_id
3395 != parts.execution_plan.payload().weight_format()
3396 || parts
3397 .execution_plan
3398 .payload()
3399 .execution_weights()
3400 .schema()
3401 .quantization_formats()
3402 != *parts.execution_plan.payload().quantization_formats()
3403 || capability_fingerprint
3404 != parts
3405 .execution_plan
3406 .payload()
3407 .capability_catalog_fingerprint()
3408 {
3409 return Err(invalid_plan(
3410 "resolved_contract_links",
3411 "family, full device descriptor, policy, capability, engine, and plan links disagree",
3412 ));
3413 }
3414 let engine = parts
3415 .capabilities
3416 .engine_provider(&parts.engine.provider_id, parts.engine.contract_version)?;
3417 if !is_canonical_sha256(&parts.engine.implementation_fingerprint)
3418 || engine.contract_version() != parts.engine.contract_version
3419 || engine.implementation_fingerprint() != parts.engine.implementation_fingerprint
3420 || engine.device_id() != &parts.device.id
3421 {
3422 return Err(invalid_plan(
3423 "engine",
3424 "engine provider version and implementation are not exactly bound to the resolved device",
3425 ));
3426 }
3427 for node in parts.execution_plan.payload().nodes() {
3428 let providers = parts
3429 .capabilities
3430 .providers_for_node(node.id(), node.operation_id())?;
3431 let selected = providers
3432 .iter()
3433 .find(|provider| provider.provider_id() == node.selection().selected_provider())
3434 .ok_or_else(|| VNextError::UnsupportedOperation {
3435 node_id: Some(node.id().to_string()),
3436 operation_id: node.operation_id().to_string(),
3437 device_id: parts.device.id.to_string(),
3438 reason: format!(
3439 "selected provider `{}` is absent from the resolved catalog",
3440 node.selection().selected_provider()
3441 ),
3442 })?;
3443 if !selected.version().satisfies(node.operation_version()) {
3444 return Err(VNextError::IncompatibleOperationVersion {
3445 node_id: Some(node.id().to_string()),
3446 operation_id: node.operation_id().to_string(),
3447 required_major: node.operation_version().major,
3448 required_minor: node.operation_version().minor,
3449 available_major: selected.version().major,
3450 available_minor: selected.version().minor,
3451 });
3452 }
3453 }
3458 parts.sampling.validate()?;
3459 if parts.stop.maximum_output_tokens == 0
3460 || parts.stop.strings.iter().any(|stop| stop.is_empty())
3461 || parts.stop.strings.windows(2).any(|pair| pair[0] == pair[1])
3462 || matches!(
3463 &parts.structured_output,
3464 StructuredOutputPolicy::JsonSchema { schema_sha256 }
3465 if !is_canonical_sha256(schema_sha256)
3466 )
3467 {
3468 return Err(invalid_plan(
3469 "generation_policy",
3470 "stop values must be non-empty and unique and structured-output hashes must be canonical",
3471 ));
3472 }
3473 let expected_fingerprints = Self::decision_fingerprints(parts)?;
3474 let mut actual_decisions = BTreeSet::new();
3475 let mut used_source_artifacts = BTreeSet::new();
3476 let mut used_source_fields = BTreeSet::new();
3477 for decision in &parts.decisions {
3478 if !actual_decisions.insert(decision.field) {
3479 return Err(invalid_plan(
3480 "decisions",
3481 format!("field `{:?}` has more than one decision", decision.field),
3482 ));
3483 }
3484 let expected = expected_fingerprints.get(&decision.field).ok_or_else(|| {
3485 invalid_plan(
3486 "decisions",
3487 format!("field `{:?}` is not resolvable", decision.field),
3488 )
3489 })?;
3490 if !decision.field.accepts_source(decision.source) {
3491 return Err(invalid_plan(
3492 "decisions.source",
3493 format!(
3494 "source `{:?}` is not allowed to author field `{:?}`",
3495 decision.source, decision.field
3496 ),
3497 ));
3498 }
3499 if decision.evidence.chosen_value_fingerprint.as_str() != expected {
3500 return Err(invalid_plan(
3501 format!("decisions.{:?}.chosen_value_fingerprint", decision.field),
3502 format!(
3503 "does not match the resolved value: expected `{expected}`, actual `{}`",
3504 decision.evidence.chosen_value_fingerprint
3505 ),
3506 ));
3507 }
3508 let artifact = source_artifacts
3509 .get(decision.evidence.source_artifact_id())
3510 .ok_or_else(|| {
3511 invalid_plan(
3512 "decisions.evidence.source_artifact_id",
3513 format!(
3514 "decision for `{:?}` references unknown source artifact `{}`",
3515 decision.field,
3516 decision.evidence.source_artifact_id()
3517 ),
3518 )
3519 })?;
3520 used_source_artifacts.insert(artifact.id.clone());
3521 if !used_source_fields.insert((
3522 artifact.id.clone(),
3523 decision.evidence.source_field_path.clone(),
3524 )) {
3525 return Err(invalid_plan(
3526 "decisions.evidence.source_field_path",
3527 "one parsed artifact field cannot issue more than one decision",
3528 ));
3529 }
3530 if artifact.source != decision.source {
3531 return Err(invalid_plan(
3532 "decisions.source",
3533 format!(
3534 "decision for `{:?}` source differs from artifact `{}`",
3535 decision.field, artifact.id
3536 ),
3537 ));
3538 }
3539 if let ResolutionSourceProvenance::LockedModelFile { source_role, .. } =
3540 &artifact.provenance
3541 {
3542 if !Self::locked_source_role_allowed(decision.field, *source_role) {
3543 return Err(invalid_plan(
3544 "decisions.source_role",
3545 format!(
3546 "decision for `{:?}` cannot use a {source_role:?} locked source",
3547 decision.field
3548 ),
3549 ));
3550 }
3551 }
3552 let source_field = artifact
3553 .fields
3554 .get(decision.evidence.source_field_path())
3555 .ok_or_else(|| {
3556 invalid_plan(
3557 "decisions.evidence.source_field_path",
3558 format!(
3559 "decision for `{:?}` references a field absent from artifact `{}`",
3560 decision.field, artifact.id
3561 ),
3562 )
3563 })?;
3564 if source_field != &decision.evidence.chosen_value_fingerprint {
3565 return Err(invalid_plan(
3566 "decisions.evidence.chosen_value_fingerprint",
3567 format!(
3568 "decision for `{:?}` differs from source artifact field `{}`",
3569 decision.field,
3570 decision.evidence.source_field_path()
3571 ),
3572 ));
3573 }
3574 }
3575 let required_decisions = expected_fingerprints
3576 .keys()
3577 .copied()
3578 .collect::<BTreeSet<_>>();
3579 if actual_decisions != required_decisions {
3580 return Err(invalid_plan(
3581 "decisions",
3582 "every resolved field requires exactly one typed, fingerprinted decision",
3583 ));
3584 }
3585 if used_source_artifacts != source_artifacts.keys().cloned().collect::<BTreeSet<_>>() {
3586 return Err(invalid_plan(
3587 "source_artifacts",
3588 "every source artifact must be referenced by at least one resolution decision",
3589 ));
3590 }
3591 let available_source_fields = source_artifacts
3592 .values()
3593 .flat_map(|artifact| {
3594 artifact
3595 .fields
3596 .keys()
3597 .cloned()
3598 .map(|path| (artifact.id.clone(), path))
3599 })
3600 .collect::<BTreeSet<_>>();
3601 if used_source_fields != available_source_fields {
3602 return Err(invalid_plan(
3603 "source_artifacts.fields",
3604 "parsed source fields and resolution decision fields differ",
3605 ));
3606 }
3607 Ok(())
3608 }
3609
3610 fn validate_source_file_binding(
3611 role: ModelArtifactSourceRole,
3612 source: &ResolvedModelSource,
3613 source_file: &str,
3614 sha256: &str,
3615 field: &str,
3616 ) -> Result<(), VNextError> {
3617 if !validate_source_path(source_file) || !is_canonical_sha256(sha256) {
3618 return Err(invalid_plan(
3619 format!("{field}.source_file"),
3620 "source path or hash is not canonical",
3621 ));
3622 }
3623 match source
3624 .files
3625 .iter()
3626 .find(|file| file.relative_path == source_file)
3627 {
3628 Some(file) if file.sha256 == sha256 => Ok(()),
3629 Some(file) => Err(invalid_plan(
3630 format!("{field}.sha256"),
3631 format!(
3632 "does not match resolved source row `{source_file}`: expected `{}`, actual `{sha256}`",
3633 file.sha256
3634 ),
3635 )),
3636 None => Err(invalid_plan(
3637 format!("{field}.source_file"),
3638 format!(
3639 "`{source_file}` is absent from resolved_sources.{}.files",
3640 role.as_str()
3641 ),
3642 )),
3643 }
3644 }
3645
3646 const fn locked_source_role_allowed(
3647 field: ResolutionField,
3648 role: ModelArtifactSourceRole,
3649 ) -> bool {
3650 match field {
3651 ResolutionField::Config
3652 | ResolutionField::ExternalMetadata
3653 | ResolutionField::Family => matches!(role, ModelArtifactSourceRole::Semantic),
3654 ResolutionField::Tokenizer | ResolutionField::Template => {
3655 matches!(role, ModelArtifactSourceRole::Tokenizer)
3656 }
3657 ResolutionField::SpecialTokens => matches!(
3658 role,
3659 ModelArtifactSourceRole::Semantic | ModelArtifactSourceRole::Tokenizer
3660 ),
3661 ResolutionField::WeightSchema => matches!(
3662 role,
3663 ModelArtifactSourceRole::Semantic | ModelArtifactSourceRole::Weights
3664 ),
3665 ResolutionField::WeightFormat => matches!(role, ModelArtifactSourceRole::Weights),
3666 ResolutionField::OriginalSources
3667 | ResolutionField::ResolvedSources
3668 | ResolutionField::Device
3669 | ResolutionField::Capabilities
3670 | ResolutionField::RuntimePreset
3671 | ResolutionField::RuntimeMemory
3672 | ResolutionField::Admission
3673 | ResolutionField::Engine
3674 | ResolutionField::ExecutionPlan
3675 | ResolutionField::Sampling
3676 | ResolutionField::Stop
3677 | ResolutionField::StructuredOutput => true,
3678 }
3679 }
3680
3681 fn validate_token_contract(parts: &ResolvedModelPlanParts) -> Result<(), VNextError> {
3682 let vocabulary_size = parts.tokenizer.vocabulary_size;
3683 let special = &parts.prepared_family.metadata().special_tokens;
3684 if special.collision_policy.allowed().iter().any(|collision| {
3685 collision.first() == SpecialTokenRole::Stop
3686 || collision.second() == SpecialTokenRole::Stop
3687 }) {
3688 return Err(invalid_plan(
3689 "special_tokens.collision_policy",
3690 "model metadata cannot authorize product-owned stop-token collisions",
3691 ));
3692 }
3693 let mut roles = Vec::new();
3694 if let Some(token_id) = special.bos_token_id {
3695 roles.push((SpecialTokenRole::Bos, token_id));
3696 }
3697 roles.extend(
3698 special
3699 .eos_token_ids
3700 .iter()
3701 .copied()
3702 .map(|token_id| (SpecialTokenRole::Eos, token_id)),
3703 );
3704 if let Some(token_id) = special.pad_token_id {
3705 roles.push((SpecialTokenRole::Pad, token_id));
3706 }
3707 roles.extend(
3708 parts
3709 .stop
3710 .token_ids
3711 .iter()
3712 .copied()
3713 .map(|token_id| (SpecialTokenRole::Stop, token_id)),
3714 );
3715 if roles
3716 .iter()
3717 .any(|(_, token_id)| u64::from(*token_id) >= vocabulary_size)
3718 {
3719 return Err(invalid_plan(
3720 "special_tokens",
3721 "a model or stop token id exceeds the tokenizer vocabulary",
3722 ));
3723 }
3724 let mut observed_stop_collisions = BTreeSet::new();
3725 for (index, (role, token_id)) in roles.iter().enumerate() {
3726 for (other_role, other_token_id) in &roles[..index] {
3727 if token_id == other_token_id && role != other_role {
3728 let (policy_field, allowed) = if *role == SpecialTokenRole::Stop
3729 || *other_role == SpecialTokenRole::Stop
3730 {
3731 let model_role = if *role == SpecialTokenRole::Stop {
3732 *other_role
3733 } else {
3734 *role
3735 };
3736 observed_stop_collisions.insert(model_role);
3737 (
3738 "stop.collision_policy",
3739 parts.stop.collision_policy.allows(model_role),
3740 )
3741 } else {
3742 (
3743 "special_tokens.collision_policy",
3744 special.collision_policy.allows(*role, *other_role),
3745 )
3746 };
3747 if !allowed {
3748 return Err(invalid_plan(
3749 policy_field,
3750 format!(
3751 "token id {token_id} is shared by {role:?} and {other_role:?} without an explicit policy"
3752 ),
3753 ));
3754 }
3755 }
3756 }
3757 }
3758 if observed_stop_collisions != *parts.stop.collision_policy.allowed_model_roles() {
3759 return Err(invalid_plan(
3760 "stop.collision_policy",
3761 "declared model-role collisions must exactly match observed stop-token aliases",
3762 ));
3763 }
3764 Ok(())
3765 }
3766
3767 fn decision_values(
3768 parts: &ResolvedModelPlanParts,
3769 ) -> Result<BTreeMap<ResolutionField, serde_json::Value>, VNextError> {
3770 #[derive(Serialize)]
3771 struct RuntimePresetValue<'a> {
3772 policy_id: &'a str,
3773 version: ContractVersion,
3774 scheduling: SchedulingDiscipline,
3775 }
3776
3777 let mut values = BTreeMap::new();
3778 macro_rules! insert_value {
3779 ($field:expr, $value:expr, $context:literal) => {
3780 values.insert($field, canonical_json_value($value, $context)?);
3781 };
3782 }
3783
3784 insert_value!(
3785 ResolutionField::OriginalSources,
3786 &parts.original_sources,
3787 "serialize original model sources decision"
3788 );
3789 insert_value!(
3790 ResolutionField::ResolvedSources,
3791 &parts.resolved_sources,
3792 "serialize resolved model sources decision"
3793 );
3794 insert_value!(
3795 ResolutionField::Config,
3796 &parts.config,
3797 "serialize model config decision"
3798 );
3799 insert_value!(
3800 ResolutionField::ExternalMetadata,
3801 &parts.external_metadata_id,
3802 "serialize external metadata decision"
3803 );
3804 insert_value!(
3805 ResolutionField::Family,
3806 parts.prepared_family.family_id(),
3807 "serialize model family decision"
3808 );
3809 insert_value!(
3810 ResolutionField::WeightSchema,
3811 parts.prepared_family.weight_schema(),
3812 "serialize weight schema decision"
3813 );
3814 insert_value!(
3815 ResolutionField::WeightFormat,
3816 &parts.prepared_family.weight_schema().format_id,
3817 "serialize weight format decision"
3818 );
3819 insert_value!(
3820 ResolutionField::Tokenizer,
3821 &parts.tokenizer,
3822 "serialize tokenizer decision"
3823 );
3824 insert_value!(
3825 ResolutionField::Template,
3826 &parts.prepared_family.metadata().template,
3827 "serialize template decision"
3828 );
3829 insert_value!(
3830 ResolutionField::SpecialTokens,
3831 &parts.prepared_family.metadata().special_tokens,
3832 "serialize special-token decision"
3833 );
3834 insert_value!(
3835 ResolutionField::Device,
3836 &parts.device,
3837 "serialize device decision"
3838 );
3839 insert_value!(
3840 ResolutionField::Capabilities,
3841 &parts.capabilities,
3842 "serialize capability decision"
3843 );
3844 insert_value!(
3845 ResolutionField::RuntimePreset,
3846 &RuntimePresetValue {
3847 policy_id: parts.runtime.policy_id(),
3848 version: parts.runtime.version(),
3849 scheduling: parts.runtime.scheduling(),
3850 },
3851 "serialize runtime preset decision"
3852 );
3853 insert_value!(
3854 ResolutionField::RuntimeMemory,
3855 parts.runtime.memory(),
3856 "serialize runtime memory decision"
3857 );
3858 insert_value!(
3859 ResolutionField::Admission,
3860 parts.runtime.admission(),
3861 "serialize admission decision"
3862 );
3863 insert_value!(
3864 ResolutionField::Engine,
3865 &parts.engine,
3866 "serialize engine decision"
3867 );
3868 insert_value!(
3869 ResolutionField::ExecutionPlan,
3870 parts.execution_plan.plan_hash().as_str(),
3871 "serialize execution-plan decision"
3872 );
3873 insert_value!(
3874 ResolutionField::Sampling,
3875 &parts.sampling,
3876 "serialize sampling decision"
3877 );
3878 insert_value!(
3879 ResolutionField::Stop,
3880 &parts.stop,
3881 "serialize stop decision"
3882 );
3883 insert_value!(
3884 ResolutionField::StructuredOutput,
3885 &parts.structured_output,
3886 "serialize structured-output decision"
3887 );
3888 Ok(values)
3889 }
3890
3891 fn decision_fingerprints(
3892 parts: &ResolvedModelPlanParts,
3893 ) -> Result<BTreeMap<ResolutionField, String>, VNextError> {
3894 Self::decision_values(parts)?
3895 .into_iter()
3896 .map(|(field, value)| {
3897 canonical_fingerprint(&value, "fingerprint resolved decision value")
3898 .map(|fingerprint| (field, fingerprint))
3899 })
3900 .collect()
3901 }
3902
3903 pub fn parts(&self) -> &ResolvedModelPlanParts {
3904 &self.parts
3905 }
3906
3907 pub fn execution_plan(&self) -> &ExecutionPlan {
3908 &self.parts.execution_plan
3909 }
3910
3911 pub fn fingerprint(&self) -> &str {
3912 self.fingerprint.as_str()
3913 }
3914
3915 pub fn to_json(&self) -> Result<Vec<u8>, VNextError> {
3916 serde_json::to_vec(self).map_err(|error| VNextError::Serialization {
3917 context: "serialize resolved model plan",
3918 message: error.to_string(),
3919 })
3920 }
3921
3922 pub fn decode_untrusted(bytes: &[u8]) -> Result<UnvalidatedResolvedModelPlan, VNextError> {
3923 if bytes.len() > MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES {
3924 return Err(invalid_plan(
3925 "resolved_model_plan.wire_bytes",
3926 format!("must not exceed {MAX_RESOLVED_MODEL_PLAN_WIRE_BYTES} bytes"),
3927 ));
3928 }
3929 serde_json::from_slice::<ResolvedModelPlanWire>(bytes)
3930 .map(Into::into)
3931 .map_err(|error| VNextError::Serialization {
3932 context: "decode untrusted resolved model plan",
3933 message: error.to_string(),
3934 })
3935 }
3936
3937 pub fn from_json_validated(
3938 bytes: &[u8],
3939 context: &ResolvedPlanValidationContext<'_>,
3940 ) -> Result<Self, VNextError> {
3941 Self::decode_untrusted(bytes)?.revalidate(context)
3942 }
3943}