1use std::{borrow::Cow, collections::BTreeMap, fmt};
2
3use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
4use serde::{Deserialize, Serialize};
5
6use crate::*;
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct SafeEndpointIdentity(String);
10impl SafeEndpointIdentity {
11 pub const MAX_BYTES: usize = 2048;
12 pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
13 let value = value.into();
14 if value.is_empty()
15 || value.len() > Self::MAX_BYTES
16 || value.chars().any(char::is_control)
17 || value.contains('@')
18 || value.contains('?')
19 || value.contains('#')
20 {
21 Err(ManifestSchemaError::InvalidEndpoint)
22 } else {
23 Ok(Self(value))
24 }
25 }
26 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30}
31impl Serialize for SafeEndpointIdentity {
32 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
33 where
34 S: serde::Serializer,
35 {
36 s.serialize_str(&self.0)
37 }
38}
39impl<'de> Deserialize<'de> for SafeEndpointIdentity {
40 fn deserialize<D>(d: D) -> Result<Self, D::Error>
41 where
42 D: serde::Deserializer<'de>,
43 {
44 Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
45 }
46}
47impl JsonSchema for SafeEndpointIdentity {
48 fn inline_schema() -> bool {
49 true
50 }
51 fn schema_name() -> Cow<'static, str> {
52 Cow::Borrowed("SafeEndpointIdentity")
53 }
54 fn json_schema(_: &mut SchemaGenerator) -> Schema {
55 json_schema!({"type":"string","minLength":1,"maxLength":2048})
56 }
57}
58
59#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
60pub struct HeaderName(String);
61impl HeaderName {
62 pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
63 let value = value.into().to_ascii_lowercase();
64 if value.is_empty()
65 || value.len() > 128
66 || !value
67 .bytes()
68 .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
69 {
70 Err(ManifestSchemaError::InvalidHeader)
71 } else {
72 Ok(Self(value))
73 }
74 }
75 #[must_use]
76 pub fn as_str(&self) -> &str {
77 &self.0
78 }
79}
80impl Serialize for HeaderName {
81 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
82 where
83 S: serde::Serializer,
84 {
85 s.serialize_str(&self.0)
86 }
87}
88impl<'de> Deserialize<'de> for HeaderName {
89 fn deserialize<D>(d: D) -> Result<Self, D::Error>
90 where
91 D: serde::Deserializer<'de>,
92 {
93 Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
94 }
95}
96impl JsonSchema for HeaderName {
97 fn inline_schema() -> bool {
98 true
99 }
100 fn schema_name() -> Cow<'static, str> {
101 Cow::Borrowed("HeaderName")
102 }
103 fn json_schema(_: &mut SchemaGenerator) -> Schema {
104 json_schema!({"type":"string","minLength":1,"maxLength":128,"pattern":"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$"})
105 }
106}
107
108#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
109pub struct SafeStaticHeaderValue(String);
110impl SafeStaticHeaderValue {
111 pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
112 let value = value.into();
113 if value.len() > 8192 || value.chars().any(char::is_control) {
114 Err(ManifestSchemaError::InvalidHeader)
115 } else {
116 Ok(Self(value))
117 }
118 }
119 #[must_use]
120 pub fn as_str(&self) -> &str {
121 &self.0
122 }
123}
124impl Serialize for SafeStaticHeaderValue {
125 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
126 where
127 S: serde::Serializer,
128 {
129 s.serialize_str(&self.0)
130 }
131}
132impl<'de> Deserialize<'de> for SafeStaticHeaderValue {
133 fn deserialize<D>(d: D) -> Result<Self, D::Error>
134 where
135 D: serde::Deserializer<'de>,
136 {
137 Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
138 }
139}
140impl JsonSchema for SafeStaticHeaderValue {
141 fn inline_schema() -> bool {
142 true
143 }
144 fn schema_name() -> Cow<'static, str> {
145 Cow::Borrowed("SafeStaticHeaderValue")
146 }
147 fn json_schema(_: &mut SchemaGenerator) -> Schema {
148 json_schema!({"type":"string","maxLength":8192})
149 }
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
153#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
154pub enum FrozenProviderSource {
155 Managed {
156 provider_recipe: ProviderRecipeId,
157 source_record_digest: Sha256Digest,
158 recipe_fingerprint: Sha256Digest,
159 package_claim: String,
160 },
161 Custom {
162 safe_definition_fingerprint: Sha256Digest,
163 },
164}
165
166#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
167#[serde(rename_all = "snake_case")]
168pub enum FrozenCredentialSource {
169 AuthoredApiKey,
170 AuthoredOverride,
171 ProviderStore,
172 NoAuth,
173}
174
175#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
176#[serde(deny_unknown_fields)]
177pub struct FrozenSetupBinding {
178 pub setup_recipe: ProviderSetupRecipeId,
179 pub values: BTreeMap<SetupFieldId, SafeSetupValue>,
180 pub setup_fingerprint: Sha256Digest,
181}
182
183#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
184#[serde(deny_unknown_fields)]
185pub struct FrozenCredentialBinding {
186 pub source: FrozenCredentialSource,
187 pub auth_method: AuthMethodId,
188 #[schemars(length(max = 32))]
189 pub fields: Vec<AuthFieldName>,
190 pub parameters: BTreeMap<AuthParameterId, FrozenAuthParameterValue>,
191 pub owned_headers: Vec<HeaderName>,
192}
193impl<'de> Deserialize<'de> for FrozenCredentialBinding {
194 fn deserialize<D>(d: D) -> Result<Self, D::Error>
195 where
196 D: serde::Deserializer<'de>,
197 {
198 #[derive(Deserialize)]
199 #[serde(deny_unknown_fields)]
200 struct Wire {
201 source: FrozenCredentialSource,
202 auth_method: AuthMethodId,
203 fields: Vec<AuthFieldName>,
204 parameters: BTreeMap<AuthParameterId, FrozenAuthParameterValue>,
205 owned_headers: Vec<HeaderName>,
206 }
207 let w = Wire::deserialize(d)?;
208 if w.fields.len() > 32
209 || w.fields.windows(2).any(|p| p[0] >= p[1])
210 || w.parameters.len() > 32
211 || w.owned_headers.len() > 32
212 || w.owned_headers.windows(2).any(|p| p[0] >= p[1])
213 {
214 return Err(serde::de::Error::custom(
215 "credential shape must be bounded, sorted, and unique",
216 ));
217 }
218 Ok(Self {
219 source: w.source,
220 auth_method: w.auth_method,
221 fields: w.fields,
222 parameters: w.parameters,
223 owned_headers: w.owned_headers,
224 })
225 }
226}
227
228pub type FrozenProviderOptions = ProviderOptions;
229
230#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
231pub struct FrozenAuthParameterValue(String);
232
233impl FrozenAuthParameterValue {
234 pub fn new(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
235 let value = value.into();
236 if value.is_empty()
237 || value.len() > 512
238 || value.chars().any(char::is_control)
239 || value.contains("${env:")
240 {
241 Err(ManifestSchemaError::InvalidAuthParameter)
242 } else {
243 Ok(Self(value))
244 }
245 }
246
247 #[must_use]
248 pub fn as_str(&self) -> &str {
249 &self.0
250 }
251}
252
253impl Serialize for FrozenAuthParameterValue {
254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255 where
256 S: serde::Serializer,
257 {
258 serializer.serialize_str(&self.0)
259 }
260}
261
262impl<'de> Deserialize<'de> for FrozenAuthParameterValue {
263 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264 where
265 D: serde::Deserializer<'de>,
266 {
267 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
268 }
269}
270
271impl JsonSchema for FrozenAuthParameterValue {
272 fn inline_schema() -> bool {
273 true
274 }
275 fn schema_name() -> Cow<'static, str> {
276 Cow::Borrowed("FrozenAuthParameterValue")
277 }
278 fn json_schema(_: &mut SchemaGenerator) -> Schema {
279 json_schema!({"type":"string","minLength":1,"maxLength":512})
280 }
281}
282
283#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
284pub struct NormalizedDecimal(String);
285
286impl NormalizedDecimal {
287 pub fn from_f32(value: f32) -> Result<Self, ManifestSchemaError> {
288 if !value.is_finite() {
289 return Err(ManifestSchemaError::InvalidDecimal);
290 }
291 Ok(Self(value.to_string()))
292 }
293
294 pub fn parse(value: impl Into<String>) -> Result<Self, ManifestSchemaError> {
295 let value = value.into();
296 let parsed = value
297 .parse::<f32>()
298 .map_err(|_| ManifestSchemaError::InvalidDecimal)?;
299 if !parsed.is_finite() || parsed.to_string() != value {
300 return Err(ManifestSchemaError::InvalidDecimal);
301 }
302 Ok(Self(value))
303 }
304
305 #[must_use]
306 pub fn as_str(&self) -> &str {
307 &self.0
308 }
309
310 #[must_use]
311 pub fn get(&self) -> f32 {
312 self.0
313 .parse()
314 .expect("normalized decimal was validated at construction")
315 }
316}
317
318impl Serialize for NormalizedDecimal {
319 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320 where
321 S: serde::Serializer,
322 {
323 serializer.serialize_str(&self.0)
324 }
325}
326
327impl<'de> Deserialize<'de> for NormalizedDecimal {
328 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329 where
330 D: serde::Deserializer<'de>,
331 {
332 Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
333 }
334}
335
336impl JsonSchema for NormalizedDecimal {
337 fn inline_schema() -> bool {
338 true
339 }
340 fn schema_name() -> Cow<'static, str> {
341 Cow::Borrowed("NormalizedDecimal")
342 }
343 fn json_schema(_: &mut SchemaGenerator) -> Schema {
344 json_schema!({"type":"string","minLength":1,"maxLength":32})
345 }
346}
347
348#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
349#[serde(deny_unknown_fields)]
350pub struct FrozenRequestDefaults {
351 pub temperature: Option<NormalizedDecimal>,
352 pub top_p: Option<NormalizedDecimal>,
353 pub max_output_tokens: Option<u64>,
354 pub stop: Vec<String>,
355 pub seed: Option<i64>,
356 pub tool_choice: Option<ToolChoice>,
357}
358
359impl FrozenRequestDefaults {
360 pub fn validate(&self) -> Result<(), ManifestSchemaError> {
361 if self
362 .temperature
363 .as_ref()
364 .is_some_and(|value| !(0.0..=2.0).contains(&value.get()))
365 || self
366 .top_p
367 .as_ref()
368 .is_some_and(|value| !(0.0..=1.0).contains(&value.get()))
369 || self.max_output_tokens == Some(0)
370 || self.stop.len() > 8
371 || self
372 .stop
373 .iter()
374 .any(|value| value.is_empty() || value.len() > 256)
375 {
376 Err(ManifestSchemaError::InvalidDefaults)
377 } else {
378 Ok(())
379 }
380 }
381}
382
383#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
384#[serde(deny_unknown_fields)]
385pub struct FrozenResolvedRequestDefaults {
386 pub request: FrozenRequestDefaults,
387 pub reasoning: Option<CompiledReasoningBehavior>,
388}
389
390impl FrozenResolvedRequestDefaults {
391 pub fn validate(&self) -> Result<(), ManifestSchemaError> {
392 self.request.validate()?;
393 if matches!(
394 self.reasoning,
395 Some(CompiledReasoningBehavior::BudgetTokens { value }) if value < -1
396 ) {
397 Err(ManifestSchemaError::InvalidDefaults)
398 } else {
399 Ok(())
400 }
401 }
402}
403
404#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct FrozenVariantBlueprint {
407 pub id: VariantId,
408 #[schemars(with = "crate::LanguageModelDescriptorSchema")]
409 pub descriptor: oven_sdk::LanguageModelDescriptor,
410 pub defaults: FrozenResolvedRequestDefaults,
411 pub options: FrozenProviderOptions,
412 #[serde(default)]
413 pub static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
414 pub behavior_fingerprint: Sha256Digest,
415 pub selection_fingerprint: Sha256Digest,
416}
417
418#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct CompiledSafeModelBlueprint {
421 pub blueprint_fingerprint: Sha256Digest,
422 pub selection: ModelSelection,
423 pub source: FrozenProviderSource,
424 pub config_override_fingerprint: Sha256Digest,
425 pub setup_binding: FrozenSetupBinding,
426 pub credential_binding: FrozenCredentialBinding,
427 pub endpoint_identity: SafeEndpointIdentity,
428 pub provider_recipe: ProviderRecipeId,
429 pub protocol_recipe: ProtocolRecipeId,
430 pub setup_recipe: ProviderSetupRecipeId,
431 pub auth_method: AuthMethodId,
432 pub compiler_version: RecipeCompilerVersion,
433 #[schemars(with = "crate::LanguageModelDescriptorSchema")]
434 pub descriptor: oven_sdk::LanguageModelDescriptor,
435 pub defaults: FrozenResolvedRequestDefaults,
436 pub options: FrozenProviderOptions,
437 pub static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
438 #[schemars(length(max = 256))]
439 pub variants: Vec<FrozenVariantBlueprint>,
440 pub behavior_fingerprint: Sha256Digest,
441 pub selection_fingerprint: Sha256Digest,
442}
443impl<'de> Deserialize<'de> for CompiledSafeModelBlueprint {
444 fn deserialize<D>(d: D) -> Result<Self, D::Error>
445 where
446 D: serde::Deserializer<'de>,
447 {
448 #[derive(Deserialize)]
449 #[serde(deny_unknown_fields)]
450 struct Wire {
451 blueprint_fingerprint: Sha256Digest,
452 selection: ModelSelection,
453 source: FrozenProviderSource,
454 config_override_fingerprint: Sha256Digest,
455 setup_binding: FrozenSetupBinding,
456 credential_binding: FrozenCredentialBinding,
457 endpoint_identity: SafeEndpointIdentity,
458 provider_recipe: ProviderRecipeId,
459 protocol_recipe: ProtocolRecipeId,
460 setup_recipe: ProviderSetupRecipeId,
461 auth_method: AuthMethodId,
462 compiler_version: RecipeCompilerVersion,
463 descriptor: oven_sdk::LanguageModelDescriptor,
464 defaults: FrozenResolvedRequestDefaults,
465 options: FrozenProviderOptions,
466 static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
467 variants: Vec<FrozenVariantBlueprint>,
468 behavior_fingerprint: Sha256Digest,
469 selection_fingerprint: Sha256Digest,
470 }
471 let w = Wire::deserialize(d)?;
472 if w.selection.variant.is_some()
473 || w.defaults.validate().is_err()
474 || w.variants.len() > 256
475 || w.variants.windows(2).any(|p| p[0].id >= p[1].id)
476 || w.variants.iter().any(|variant| {
477 variant.defaults.validate().is_err()
478 || variant.descriptor.identity != w.descriptor.identity
479 })
480 {
481 return Err(serde::de::Error::custom(
482 "model blueprint behavior is invalid",
483 ));
484 }
485 Ok(Self {
486 blueprint_fingerprint: w.blueprint_fingerprint,
487 selection: w.selection,
488 source: w.source,
489 config_override_fingerprint: w.config_override_fingerprint,
490 setup_binding: w.setup_binding,
491 credential_binding: w.credential_binding,
492 endpoint_identity: w.endpoint_identity,
493 provider_recipe: w.provider_recipe,
494 protocol_recipe: w.protocol_recipe,
495 setup_recipe: w.setup_recipe,
496 auth_method: w.auth_method,
497 compiler_version: w.compiler_version,
498 descriptor: w.descriptor,
499 defaults: w.defaults,
500 options: w.options,
501 static_headers: w.static_headers,
502 variants: w.variants,
503 behavior_fingerprint: w.behavior_fingerprint,
504 selection_fingerprint: w.selection_fingerprint,
505 })
506 }
507}
508
509#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
510#[serde(deny_unknown_fields)]
511pub struct ModelSnapshotPayloadV1 {
512 pub catalog_revision: CatalogRevision,
513 pub recipe_registry_revision: RecipeRegistryRevision,
514 pub provider_state_revision: ProviderStateRevision,
515 pub model_revision: ModelRevision,
516 #[schemars(length(max = 4096))]
517 pub blueprints: Vec<CompiledSafeModelBlueprint>,
518}
519impl<'de> Deserialize<'de> for ModelSnapshotPayloadV1 {
520 fn deserialize<D>(d: D) -> Result<Self, D::Error>
521 where
522 D: serde::Deserializer<'de>,
523 {
524 #[derive(Deserialize)]
525 #[serde(deny_unknown_fields)]
526 struct Wire {
527 catalog_revision: CatalogRevision,
528 recipe_registry_revision: RecipeRegistryRevision,
529 provider_state_revision: ProviderStateRevision,
530 model_revision: ModelRevision,
531 blueprints: Vec<CompiledSafeModelBlueprint>,
532 }
533 let w = Wire::deserialize(d)?;
534 if w.blueprints.len() > 4096
535 || w.blueprints
536 .windows(2)
537 .any(|p| p[0].selection.model >= p[1].selection.model)
538 {
539 return Err(serde::de::Error::custom(
540 "blueprints must be strictly sorted by model key",
541 ));
542 }
543 Ok(Self {
544 catalog_revision: w.catalog_revision,
545 recipe_registry_revision: w.recipe_registry_revision,
546 provider_state_revision: w.provider_state_revision,
547 model_revision: w.model_revision,
548 blueprints: w.blueprints,
549 })
550 }
551}
552
553#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
554pub struct ModelSnapshotManifestSchemaVersion(());
555impl ModelSnapshotManifestSchemaVersion {
556 #[must_use]
557 pub const fn current() -> Self {
558 Self(())
559 }
560}
561impl Serialize for ModelSnapshotManifestSchemaVersion {
562 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
563 where
564 S: serde::Serializer,
565 {
566 s.serialize_u32(1)
567 }
568}
569impl<'de> Deserialize<'de> for ModelSnapshotManifestSchemaVersion {
570 fn deserialize<D>(d: D) -> Result<Self, D::Error>
571 where
572 D: serde::Deserializer<'de>,
573 {
574 let value = u32::deserialize(d)?;
575 if value == 1 {
576 Ok(Self::current())
577 } else {
578 Err(serde::de::Error::custom(
579 "unsupported model-snapshot manifest schema; expected 1",
580 ))
581 }
582 }
583}
584impl JsonSchema for ModelSnapshotManifestSchemaVersion {
585 fn inline_schema() -> bool {
586 true
587 }
588 fn schema_name() -> Cow<'static, str> {
589 Cow::Borrowed("ModelSnapshotManifestSchemaVersion")
590 }
591 fn json_schema(_: &mut SchemaGenerator) -> Schema {
592 json_schema!({"type":"integer","const":1})
593 }
594}
595
596#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
597#[serde(deny_unknown_fields)]
598pub struct ModelSnapshotManifestV1 {
599 pub schema_version: ModelSnapshotManifestSchemaVersion,
600 pub revision: ModelSnapshotRevision,
601 pub payload: ModelSnapshotPayloadV1,
602}
603
604#[derive(Clone, Copy, Debug, Eq, PartialEq)]
605pub enum ManifestSchemaError {
606 InvalidEndpoint,
607 InvalidHeader,
608 InvalidAuthParameter,
609 InvalidDecimal,
610 InvalidDefaults,
611}
612impl fmt::Display for ManifestSchemaError {
613 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614 f.write_str(match self {
615 Self::InvalidEndpoint => "invalid safe endpoint identity",
616 Self::InvalidHeader => "invalid safe static header",
617 Self::InvalidAuthParameter => "invalid frozen auth parameter",
618 Self::InvalidDecimal => "invalid normalized decimal",
619 Self::InvalidDefaults => "invalid frozen request defaults",
620 })
621 }
622}
623impl std::error::Error for ManifestSchemaError {}