1use std::{
2 borrow::Cow,
3 collections::{BTreeMap, BTreeSet},
4 fmt,
5};
6
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 CompiledSafeModelBlueprint, FrozenCredentialBinding, FrozenProviderOptions,
12 FrozenProviderSource, FrozenResolvedRequestDefaults, FrozenSetupBinding, HeaderName, ModelKey,
13 ModelSelection, ModelSnapshotRevision, ProtocolRecipeId, ProviderId, ProviderModelId,
14 ProviderRecipeId, ProviderSetupRecipeId, RecipeCompilerVersion, SafeEndpointIdentity,
15 SafeStaticHeaderValue, Sha256Digest, VariantId,
16};
17
18#[derive(
19 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
20)]
21#[serde(rename_all = "kebab-case")]
22pub enum AdaptorId {
23 Anthropic,
24 OpenaiChat,
25 OpenaiResponses,
26 OpenaiCompatible,
27 GoogleGemini,
28 GoogleVertexGemini,
29 AwsBedrockConverse,
30 AzureOpenaiChat,
31 AzureOpenaiResponses,
32 CohereV2Chat,
33}
34
35impl AdaptorId {
36 #[must_use]
37 pub const fn as_str(self) -> &'static str {
38 match self {
39 Self::Anthropic => "anthropic",
40 Self::OpenaiChat => "openai-chat",
41 Self::OpenaiResponses => "openai-responses",
42 Self::OpenaiCompatible => "openai-compatible",
43 Self::GoogleGemini => "google-gemini",
44 Self::GoogleVertexGemini => "google-vertex-gemini",
45 Self::AwsBedrockConverse => "aws-bedrock-converse",
46 Self::AzureOpenaiChat => "azure-openai-chat",
47 Self::AzureOpenaiResponses => "azure-openai-responses",
48 Self::CohereV2Chat => "cohere-v2-chat",
49 }
50 }
51}
52
53pub(crate) struct LanguageModelDescriptorSchema;
54
55impl JsonSchema for LanguageModelDescriptorSchema {
56 fn inline_schema() -> bool {
57 true
58 }
59
60 fn schema_name() -> Cow<'static, str> {
61 Cow::Borrowed("LanguageModelDescriptor")
62 }
63
64 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
65 json_schema!({
66 "type":"object",
67 "additionalProperties":false,
68 "properties":{
69 "identity":{
70 "type":"object","additionalProperties":false,
71 "properties":{
72 "provider_id":{"type":"string","minLength":1},
73 "model_id":{"type":"string","minLength":1}
74 },
75 "required":["provider_id","model_id"]
76 },
77 "adapter_id":{"type":"string","minLength":1},
78 "capabilities":{
79 "type":"object","additionalProperties":false,
80 "properties":{
81 "features":{"type":"array","uniqueItems":true,"items":{"type":"string","enum":["tool_calling","parallel_tools","tool_input_deltas","reasoning","structured_output","temperature","top_p","max_output_tokens","prompt_caching","usage","provider_tools","sources"]}},
82 "limits":{
83 "type":"object","additionalProperties":false,
84 "properties":{
85 "context":{"type":["integer","null"],"minimum":0},
86 "input":{"type":["integer","null"],"minimum":0},
87 "output":{"type":["integer","null"],"minimum":0}
88 },
89 "required":["context","input","output"]
90 },
91 "modalities":{
92 "type":"object","additionalProperties":false,
93 "properties":{
94 "input":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}},
95 "output":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","minLength":1}}
96 },
97 "required":["input","output"]
98 },
99 "media":{
100 "type":"object","additionalProperties":false,
101 "properties":{
102 "input":{"type":"object","additionalProperties":{
103 "type":"object","additionalProperties":false,
104 "properties":{
105 "media_types":{"type":"array","minItems":1,"items":{"type":"string","minLength":3}},
106 "sources":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","enum":["inline_bytes","inline_text","url","provider_reference"]}}
107 },
108 "required":["media_types","sources"]
109 }}
110 },
111 "required":["input"]
112 },
113 "cancellation":{"type":"string","enum":["local_only","remote_best_effort","unsupported"]},
114 "compaction":{"type":"string","const":"unsupported"},
115 "replay":{
116 "type":"object","additionalProperties":false,
117 "properties":{
118 "policy":{"type":"string","enum":["never","if_valid","always"]},
119 "capability":{"type":"string","enum":["required","optional","unsupported"]},
120 "reasoning":{"type":"boolean"}
121 },
122 "required":["policy","capability","reasoning"]
123 }
124 },
125 "required":["features","limits","modalities","media","cancellation","compaction","replay"]
126 },
127 "provider_metadata":{"type":"object"}
128 },
129 "required":["identity","adapter_id","capabilities","provider_metadata"]
130 })
131 }
132}
133
134#[derive(
135 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
136)]
137#[serde(rename_all = "snake_case")]
138pub enum Modality {
139 Text,
140 Image,
141 Audio,
142 Pdf,
143 Video,
144}
145
146#[derive(
147 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
148)]
149#[serde(rename_all = "snake_case")]
150pub enum MediaKind {
151 Image,
152 Audio,
153 Pdf,
154 Video,
155}
156
157#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize)]
158#[serde(rename_all = "snake_case")]
159pub enum ReplayCapability {
160 Unsupported,
161 Optional,
162 Required,
163}
164
165#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize)]
166#[serde(rename_all = "snake_case")]
167pub enum CancellationCapability {
168 LocalOnly,
169 Provider,
170}
171
172#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
173pub struct MimeType(String);
174
175impl MimeType {
176 pub const MAX_BYTES: usize = 255;
177 pub fn new(value: impl Into<String>) -> Result<Self, ModelSchemaError> {
178 let value = value.into();
179 let valid = !value.is_empty()
180 && value.len() <= Self::MAX_BYTES
181 && !value.chars().any(char::is_control)
182 && value
183 .split_once('/')
184 .is_some_and(|(left, right)| !left.is_empty() && !right.is_empty())
185 && !value.bytes().any(|byte| byte.is_ascii_whitespace());
186 if !valid {
187 return Err(ModelSchemaError::InvalidMimeType);
188 }
189 Ok(Self(value))
190 }
191 #[must_use]
192 pub fn as_str(&self) -> &str {
193 &self.0
194 }
195}
196impl fmt::Display for MimeType {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 self.0.fmt(f)
199 }
200}
201impl Serialize for MimeType {
202 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
203 where
204 S: serde::Serializer,
205 {
206 s.serialize_str(&self.0)
207 }
208}
209impl<'de> Deserialize<'de> for MimeType {
210 fn deserialize<D>(d: D) -> Result<Self, D::Error>
211 where
212 D: serde::Deserializer<'de>,
213 {
214 Self::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
215 }
216}
217impl JsonSchema for MimeType {
218 fn inline_schema() -> bool {
219 true
220 }
221 fn schema_name() -> Cow<'static, str> {
222 Cow::Borrowed("MimeType")
223 }
224 fn json_schema(_: &mut SchemaGenerator) -> Schema {
225 json_schema!({"type":"string","minLength":3,"maxLength":255,"pattern":"^[^/\\s]+/[^/\\s]+$"})
226 }
227}
228
229#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct MediaCapability {
232 #[schemars(length(min = 1))]
233 pub mime_types: BTreeSet<MimeType>,
234 #[schemars(range(min = 1))]
235 pub max_bytes: u64,
236 #[schemars(range(min = 1))]
237 pub max_count: u32,
238}
239
240#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
241#[serde(deny_unknown_fields)]
242pub struct ModelCapabilities {
243 #[schemars(length(min = 1))]
244 pub input: BTreeSet<Modality>,
245 #[schemars(length(min = 1))]
246 pub output: BTreeSet<Modality>,
247 #[schemars(range(min = 1))]
248 pub context_tokens: u64,
249 #[schemars(range(min = 1))]
250 pub output_tokens: u64,
251 pub tool_calling: bool,
252 pub parallel_tool_calls: bool,
253 pub structured_output: bool,
254 pub reasoning: bool,
255 pub temperature: bool,
256 pub top_p: bool,
257 pub seed: bool,
258 pub native_replay: ReplayCapability,
259 pub cancellation: CancellationCapability,
260 pub media: BTreeMap<MediaKind, MediaCapability>,
261}
262
263impl ModelCapabilities {
264 pub fn validate(&self) -> Result<(), ModelSchemaError> {
265 if self.input.is_empty() || self.output.is_empty() {
266 return Err(ModelSchemaError::EmptyModalities);
267 }
268 if self.context_tokens == 0
269 || self.output_tokens == 0
270 || self.output_tokens > self.context_tokens
271 {
272 return Err(ModelSchemaError::InvalidTokenLimits);
273 }
274 if self.parallel_tool_calls && !self.tool_calling {
275 return Err(ModelSchemaError::ParallelToolsWithoutTools);
276 }
277 if self.output.contains(&Modality::Pdf) {
278 return Err(ModelSchemaError::InvalidOutputModality);
279 }
280 for (kind, capability) in &self.media {
281 let modality = match kind {
282 MediaKind::Image => Modality::Image,
283 MediaKind::Audio => Modality::Audio,
284 MediaKind::Pdf => Modality::Pdf,
285 MediaKind::Video => Modality::Video,
286 };
287 if !self.input.contains(&modality)
288 || capability.mime_types.is_empty()
289 || capability.max_bytes == 0
290 || capability.max_count == 0
291 {
292 return Err(ModelSchemaError::InvalidMediaCapability);
293 }
294 }
295 for modality in [
296 Modality::Image,
297 Modality::Audio,
298 Modality::Pdf,
299 Modality::Video,
300 ] {
301 let kind = match modality {
302 Modality::Image => MediaKind::Image,
303 Modality::Audio => MediaKind::Audio,
304 Modality::Pdf => MediaKind::Pdf,
305 Modality::Video => MediaKind::Video,
306 Modality::Text => unreachable!(),
307 };
308 if self.input.contains(&modality) != self.media.contains_key(&kind) {
309 return Err(ModelSchemaError::InvalidMediaCapability);
310 }
311 }
312 Ok(())
313 }
314}
315
316impl<'de> Deserialize<'de> for ModelCapabilities {
317 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
318 where
319 D: serde::Deserializer<'de>,
320 {
321 #[derive(Deserialize)]
322 #[serde(deny_unknown_fields)]
323 struct Wire {
324 input: BTreeSet<Modality>,
325 output: BTreeSet<Modality>,
326 context_tokens: u64,
327 output_tokens: u64,
328 tool_calling: bool,
329 parallel_tool_calls: bool,
330 structured_output: bool,
331 reasoning: bool,
332 temperature: bool,
333 top_p: bool,
334 seed: bool,
335 native_replay: ReplayCapability,
336 cancellation: CancellationCapability,
337 media: BTreeMap<MediaKind, MediaCapability>,
338 }
339 let wire = Wire::deserialize(deserializer)?;
340 let value = Self {
341 input: wire.input,
342 output: wire.output,
343 context_tokens: wire.context_tokens,
344 output_tokens: wire.output_tokens,
345 tool_calling: wire.tool_calling,
346 parallel_tool_calls: wire.parallel_tool_calls,
347 structured_output: wire.structured_output,
348 reasoning: wire.reasoning,
349 temperature: wire.temperature,
350 top_p: wire.top_p,
351 seed: wire.seed,
352 native_replay: wire.native_replay,
353 cancellation: wire.cancellation,
354 media: wire.media,
355 };
356 value.validate().map_err(serde::de::Error::custom)?;
357 Ok(value)
358 }
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
362pub struct FiniteF32(f32);
363
364impl FiniteF32 {
365 pub fn new(value: f32) -> Result<Self, ModelSchemaError> {
366 if !value.is_finite() {
367 return Err(ModelSchemaError::NonFiniteNumber);
368 }
369 Ok(Self(value))
370 }
371 #[must_use]
372 pub const fn get(self) -> f32 {
373 self.0
374 }
375}
376impl Serialize for FiniteF32 {
377 fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
378 where
379 S: serde::Serializer,
380 {
381 s.serialize_f32(self.0)
382 }
383}
384impl<'de> Deserialize<'de> for FiniteF32 {
385 fn deserialize<D>(d: D) -> Result<Self, D::Error>
386 where
387 D: serde::Deserializer<'de>,
388 {
389 Self::new(f32::deserialize(d)?).map_err(serde::de::Error::custom)
390 }
391}
392impl JsonSchema for FiniteF32 {
393 fn inline_schema() -> bool {
394 true
395 }
396 fn schema_name() -> Cow<'static, str> {
397 Cow::Borrowed("FiniteF32")
398 }
399 fn json_schema(_: &mut SchemaGenerator) -> Schema {
400 json_schema!({"type":"number"})
401 }
402}
403
404struct StopSequenceSchema;
405impl JsonSchema for StopSequenceSchema {
406 fn inline_schema() -> bool {
407 true
408 }
409 fn schema_name() -> Cow<'static, str> {
410 Cow::Borrowed("StopSequence")
411 }
412 fn json_schema(_: &mut SchemaGenerator) -> Schema {
413 json_schema!({"type":"string","minLength":1,"maxLength":256})
414 }
415}
416
417#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
418pub struct ModelToolName(String);
419impl ModelToolName {
420 pub fn new(value: impl Into<String>) -> Result<Self, ModelSchemaError> {
421 let value = value.into();
422 if value.is_empty() || value.len() > 64 || value.chars().any(char::is_control) {
423 return Err(ModelSchemaError::InvalidToolName);
424 }
425 Ok(Self(value))
426 }
427 #[must_use]
428 pub fn as_str(&self) -> &str {
429 &self.0
430 }
431}
432impl Serialize for ModelToolName {
433 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
434 where
435 S: serde::Serializer,
436 {
437 serializer.serialize_str(&self.0)
438 }
439}
440impl<'de> Deserialize<'de> for ModelToolName {
441 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442 where
443 D: serde::Deserializer<'de>,
444 {
445 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
446 }
447}
448impl JsonSchema for ModelToolName {
449 fn inline_schema() -> bool {
450 true
451 }
452 fn schema_name() -> Cow<'static, str> {
453 Cow::Borrowed("ModelToolName")
454 }
455 fn json_schema(_: &mut SchemaGenerator) -> Schema {
456 json_schema!({"type":"string","minLength":1,"maxLength":64,"pattern":"^[^\\p{Cc}\\p{Cf}]+$"})
457 }
458}
459
460#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
461#[serde(rename_all = "snake_case")]
462pub enum ToolChoice {
463 Auto,
464 None,
465 Required,
466 Named(ModelToolName),
467}
468
469#[derive(Clone, Debug, Default, JsonSchema, PartialEq, Serialize)]
470#[serde(deny_unknown_fields)]
471pub struct RequestDefaults {
472 #[serde(deserialize_with = "crate::deserialize_required_option")]
473 #[schemars(with = "TemperatureSchema", required)]
474 pub temperature: Option<FiniteF32>,
475 #[serde(deserialize_with = "crate::deserialize_required_option")]
476 #[schemars(with = "TopPSchema", required)]
477 pub top_p: Option<FiniteF32>,
478 #[serde(deserialize_with = "crate::deserialize_required_option")]
479 #[schemars(with = "PositiveOptionalU64Schema", required)]
480 pub max_output_tokens: Option<u64>,
481 #[schemars(with = "Vec<StopSequenceSchema>")]
482 #[schemars(length(max = 8))]
483 pub stop: Vec<String>,
484 #[serde(deserialize_with = "crate::deserialize_required_option")]
485 #[schemars(with = "crate::NullableSchema<i64>", required)]
486 pub seed: Option<i64>,
487 #[serde(deserialize_with = "crate::deserialize_required_option")]
488 #[schemars(with = "crate::NullableSchema<ToolChoice>", required)]
489 pub tool_choice: Option<ToolChoice>,
490}
491
492struct PositiveOptionalU64Schema;
493struct TemperatureSchema;
494struct TopPSchema;
495impl JsonSchema for PositiveOptionalU64Schema {
496 fn inline_schema() -> bool {
497 true
498 }
499 fn schema_name() -> Cow<'static, str> {
500 Cow::Borrowed("PositiveOptionalU64")
501 }
502 fn json_schema(_: &mut SchemaGenerator) -> Schema {
503 json_schema!({"anyOf":[{"type":"integer","minimum":1},{"type":"null"}]})
504 }
505}
506impl JsonSchema for TemperatureSchema {
507 fn inline_schema() -> bool {
508 true
509 }
510 fn schema_name() -> Cow<'static, str> {
511 Cow::Borrowed("Temperature")
512 }
513 fn json_schema(_: &mut SchemaGenerator) -> Schema {
514 json_schema!({"anyOf":[{"type":"number","minimum":0.0,"maximum":2.0},{"type":"null"}]})
515 }
516}
517impl JsonSchema for TopPSchema {
518 fn inline_schema() -> bool {
519 true
520 }
521 fn schema_name() -> Cow<'static, str> {
522 Cow::Borrowed("TopP")
523 }
524 fn json_schema(_: &mut SchemaGenerator) -> Schema {
525 json_schema!({"anyOf":[{"type":"number","minimum":0.0,"maximum":1.0},{"type":"null"}]})
526 }
527}
528
529impl RequestDefaults {
530 pub fn validate(&self) -> Result<(), ModelSchemaError> {
531 if self
532 .temperature
533 .is_some_and(|v| !(0.0..=2.0).contains(&v.get()))
534 {
535 return Err(ModelSchemaError::TemperatureOutOfRange);
536 }
537 if self.top_p.is_some_and(|v| !(0.0..=1.0).contains(&v.get())) {
538 return Err(ModelSchemaError::TopPOutOfRange);
539 }
540 if self.max_output_tokens == Some(0) {
541 return Err(ModelSchemaError::InvalidMaxOutputTokens);
542 }
543 if self.stop.len() > 8
544 || self
545 .stop
546 .iter()
547 .any(|stop| stop.is_empty() || stop.len() > 256)
548 {
549 return Err(ModelSchemaError::InvalidStopSequence);
550 }
551 Ok(())
552 }
553}
554
555impl<'de> Deserialize<'de> for RequestDefaults {
556 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
557 where
558 D: serde::Deserializer<'de>,
559 {
560 #[derive(Deserialize)]
561 #[serde(deny_unknown_fields)]
562 struct Wire {
563 #[serde(deserialize_with = "crate::deserialize_required_option")]
564 temperature: Option<FiniteF32>,
565 #[serde(deserialize_with = "crate::deserialize_required_option")]
566 top_p: Option<FiniteF32>,
567 #[serde(deserialize_with = "crate::deserialize_required_option")]
568 max_output_tokens: Option<u64>,
569 stop: Vec<String>,
570 #[serde(deserialize_with = "crate::deserialize_required_option")]
571 seed: Option<i64>,
572 #[serde(deserialize_with = "crate::deserialize_required_option")]
573 tool_choice: Option<ToolChoice>,
574 }
575 let wire = Wire::deserialize(deserializer)?;
576 let value = Self {
577 temperature: wire.temperature,
578 top_p: wire.top_p,
579 max_output_tokens: wire.max_output_tokens,
580 stop: wire.stop,
581 seed: wire.seed,
582 tool_choice: wire.tool_choice,
583 };
584 value.validate().map_err(serde::de::Error::custom)?;
585 Ok(value)
586 }
587}
588
589#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
590#[serde(rename_all = "snake_case")]
591pub enum ReasoningEffort {
592 None,
593 Minimal,
594 Low,
595 Medium,
596 High,
597 Xhigh,
598 Max,
599 Default,
600}
601
602#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
603#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
604pub enum CompiledReasoningBehavior {
605 Effort {
606 value: ReasoningEffort,
607 },
608 Toggle {
609 enabled: bool,
610 },
611 BudgetTokens {
612 #[schemars(range(min = -1))]
613 value: i64,
614 },
615}
616
617#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
618#[serde(deny_unknown_fields)]
619pub struct ResolvedRequestDefaults {
620 pub request: RequestDefaults,
621 #[serde(deserialize_with = "crate::deserialize_required_option")]
622 #[schemars(with = "crate::NullableSchema<CompiledReasoningBehavior>", required)]
623 pub reasoning: Option<CompiledReasoningBehavior>,
624}
625
626impl ResolvedRequestDefaults {
627 pub fn validate(&self) -> Result<(), ModelSchemaError> {
628 self.request.validate()?;
629 if matches!(
630 self.reasoning,
631 Some(CompiledReasoningBehavior::BudgetTokens { value }) if value < -1
632 ) {
633 return Err(ModelSchemaError::InvalidReasoningBudget);
634 }
635 Ok(())
636 }
637}
638
639impl<'de> Deserialize<'de> for ResolvedRequestDefaults {
640 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
641 where
642 D: serde::Deserializer<'de>,
643 {
644 #[derive(Deserialize)]
645 #[serde(deny_unknown_fields)]
646 struct Wire {
647 request: RequestDefaults,
648 #[serde(deserialize_with = "crate::deserialize_required_option")]
649 reasoning: Option<CompiledReasoningBehavior>,
650 }
651 let wire = Wire::deserialize(deserializer)?;
652 let value = Self {
653 request: wire.request,
654 reasoning: wire.reasoning,
655 };
656 value.validate().map_err(serde::de::Error::custom)?;
657 Ok(value)
658 }
659}
660
661struct ProviderOptionString512Schema;
662struct ProviderOptionString256Schema;
663struct ProviderApiPathSchema;
664struct ProviderBetaSchema;
665impl JsonSchema for ProviderOptionString512Schema {
666 fn inline_schema() -> bool {
667 true
668 }
669 fn schema_name() -> Cow<'static, str> {
670 Cow::Borrowed("ProviderOptionString512")
671 }
672 fn json_schema(_: &mut SchemaGenerator) -> Schema {
673 json_schema!({"type":"string","minLength":1,"maxLength":512,"pattern":"^[^\\p{Cc}\\p{Cf}]+$"})
674 }
675}
676impl JsonSchema for ProviderOptionString256Schema {
677 fn inline_schema() -> bool {
678 true
679 }
680 fn schema_name() -> Cow<'static, str> {
681 Cow::Borrowed("ProviderOptionString256")
682 }
683 fn json_schema(_: &mut SchemaGenerator) -> Schema {
684 json_schema!({"type":"string","minLength":1,"maxLength":256,"pattern":"^[^\\p{Cc}\\p{Cf}]+$"})
685 }
686}
687impl JsonSchema for ProviderApiPathSchema {
688 fn inline_schema() -> bool {
689 true
690 }
691 fn schema_name() -> Cow<'static, str> {
692 Cow::Borrowed("ProviderApiPath")
693 }
694 fn json_schema(_: &mut SchemaGenerator) -> Schema {
695 json_schema!({"type":"string","minLength":1,"maxLength":512,"pattern":"^/[^?#]*$"})
696 }
697}
698impl JsonSchema for ProviderBetaSchema {
699 fn inline_schema() -> bool {
700 true
701 }
702 fn schema_name() -> Cow<'static, str> {
703 Cow::Borrowed("ProviderBeta")
704 }
705 fn json_schema(_: &mut SchemaGenerator) -> Schema {
706 json_schema!({"type":"array","maxItems":32,"uniqueItems":true,"items":{"type":"string","minLength":1,"maxLength":512,"pattern":"^[^\\p{Cc}\\p{Cf}]+$"}})
707 }
708}
709
710#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
711#[serde(tag = "type", deny_unknown_fields)]
712pub enum ProviderOptions {
713 #[serde(rename = "anthropic")]
714 Anthropic {
715 #[serde(deserialize_with = "deserialize_provider_option_256")]
716 #[schemars(
717 with = "crate::NullableSchema<ProviderOptionString256Schema>",
718 required
719 )]
720 api_version: Option<String>,
721 #[serde(deserialize_with = "deserialize_provider_beta")]
722 #[schemars(with = "ProviderBetaSchema")]
723 beta: Vec<String>,
724 },
725 #[serde(rename = "openai-chat")]
726 OpenAiChat {
727 #[serde(deserialize_with = "deserialize_provider_option_512")]
728 #[schemars(
729 with = "crate::NullableSchema<ProviderOptionString512Schema>",
730 required
731 )]
732 organization: Option<String>,
733 #[serde(deserialize_with = "deserialize_provider_option_256")]
734 #[schemars(
735 with = "crate::NullableSchema<ProviderOptionString256Schema>",
736 required
737 )]
738 project: Option<String>,
739 },
740 #[serde(rename = "openai-responses")]
741 OpenAiResponses {
742 #[serde(deserialize_with = "deserialize_provider_option_512")]
743 #[schemars(
744 with = "crate::NullableSchema<ProviderOptionString512Schema>",
745 required
746 )]
747 organization: Option<String>,
748 #[serde(deserialize_with = "deserialize_provider_option_256")]
749 #[schemars(
750 with = "crate::NullableSchema<ProviderOptionString256Schema>",
751 required
752 )]
753 project: Option<String>,
754 #[serde(deserialize_with = "crate::deserialize_required_option")]
755 #[schemars(with = "crate::NullableSchema<bool>", required)]
756 store: Option<bool>,
757 },
758 #[serde(rename = "openai-compatible")]
759 OpenAiCompatible {
760 #[serde(deserialize_with = "deserialize_provider_api_path")]
761 #[schemars(with = "crate::NullableSchema<ProviderApiPathSchema>", required)]
762 api_path: Option<String>,
763 },
764 #[serde(rename = "google-gemini")]
765 GoogleGemini {
766 #[serde(deserialize_with = "deserialize_provider_option_256")]
767 #[schemars(
768 with = "crate::NullableSchema<ProviderOptionString256Schema>",
769 required
770 )]
771 api_version: Option<String>,
772 },
773 #[serde(rename = "google-vertex-gemini")]
774 GoogleVertexGemini {
775 #[serde(deserialize_with = "deserialize_provider_string_256")]
776 #[schemars(with = "ProviderOptionString256Schema")]
777 project: String,
778 #[serde(deserialize_with = "deserialize_provider_string_256")]
779 #[schemars(with = "ProviderOptionString256Schema")]
780 location: String,
781 },
782 #[serde(rename = "aws-bedrock-converse")]
783 AwsBedrockConverse {
784 #[serde(deserialize_with = "deserialize_provider_string_256")]
785 #[schemars(with = "ProviderOptionString256Schema")]
786 region: String,
787 },
788 #[serde(rename = "azure-openai-chat")]
789 AzureOpenAiChat {
790 #[serde(deserialize_with = "deserialize_provider_string_256")]
791 #[schemars(with = "ProviderOptionString256Schema")]
792 deployment: String,
793 #[serde(deserialize_with = "deserialize_provider_string_256")]
794 #[schemars(with = "ProviderOptionString256Schema")]
795 api_version: String,
796 },
797 #[serde(rename = "azure-openai-responses")]
798 AzureOpenAiResponses {
799 #[serde(deserialize_with = "deserialize_provider_string_256")]
800 #[schemars(with = "ProviderOptionString256Schema")]
801 deployment: String,
802 #[serde(deserialize_with = "deserialize_provider_string_256")]
803 #[schemars(with = "ProviderOptionString256Schema")]
804 api_version: String,
805 },
806 #[serde(rename = "cohere-v2-chat")]
807 CohereV2Chat {
808 #[serde(deserialize_with = "deserialize_provider_option_256")]
809 #[schemars(
810 with = "crate::NullableSchema<ProviderOptionString256Schema>",
811 required
812 )]
813 api_version: Option<String>,
814 },
815}
816
817impl ProviderOptions {
818 #[must_use]
819 pub const fn adapter_id(&self) -> AdaptorId {
820 match self {
821 Self::Anthropic { .. } => AdaptorId::Anthropic,
822 Self::OpenAiChat { .. } => AdaptorId::OpenaiChat,
823 Self::OpenAiResponses { .. } => AdaptorId::OpenaiResponses,
824 Self::OpenAiCompatible { .. } => AdaptorId::OpenaiCompatible,
825 Self::GoogleGemini { .. } => AdaptorId::GoogleGemini,
826 Self::GoogleVertexGemini { .. } => AdaptorId::GoogleVertexGemini,
827 Self::AwsBedrockConverse { .. } => AdaptorId::AwsBedrockConverse,
828 Self::AzureOpenAiChat { .. } => AdaptorId::AzureOpenaiChat,
829 Self::AzureOpenAiResponses { .. } => AdaptorId::AzureOpenaiResponses,
830 Self::CohereV2Chat { .. } => AdaptorId::CohereV2Chat,
831 }
832 }
833}
834
835#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
836#[serde(rename_all = "snake_case")]
837pub enum FrozenCacheTtl {
838 OneHour,
839 FiveMinutes,
840 Off,
841}
842
843#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
844#[serde(rename_all = "snake_case")]
845pub enum FrozenGoogleCacheMode {
846 Implicit,
847 Explicit,
848 Off,
849}
850
851#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
852#[serde(rename_all = "snake_case")]
853pub enum FrozenOpenAiCacheRetention {
854 InMemory,
855 #[serde(rename = "24h")]
856 TwentyFourHours,
857}
858
859#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
860#[serde(rename_all = "snake_case")]
861pub enum FrozenOpenAiCacheMode {
862 Implicit,
863 Explicit,
864}
865
866#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
867pub enum FrozenOpenAiPromptCacheTtl {
868 #[serde(rename = "30m")]
869 ThirtyMinutes,
870}
871
872#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
873#[serde(deny_unknown_fields)]
874pub struct FrozenBedrockMessageCachePoint {
875 pub history_index: u64,
876 pub ttl: FrozenCacheTtl,
877}
878
879#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
880#[serde(tag = "provider", rename_all = "snake_case", deny_unknown_fields)]
881pub enum FrozenCacheStrategy {
882 Anthropic {
883 system: FrozenCacheTtl,
884 tools: FrozenCacheTtl,
885 rolling: FrozenCacheTtl,
886 },
887 Bedrock {
888 system: FrozenCacheTtl,
889 tools: FrozenCacheTtl,
890 #[schemars(length(max = 4))]
891 messages: Vec<FrozenBedrockMessageCachePoint>,
892 },
893 Google {
894 mode: FrozenGoogleCacheMode,
895 #[serde(deserialize_with = "crate::deserialize_required_option")]
896 #[schemars(with = "crate::NullableSchema<String>", required)]
897 cached_content: Option<String>,
898 },
899 OpenAi {
900 #[serde(deserialize_with = "crate::deserialize_required_option")]
901 #[schemars(with = "crate::NullableSchema<String>", required)]
902 prompt_cache_key: Option<String>,
903 #[serde(deserialize_with = "crate::deserialize_required_option")]
904 #[schemars(with = "crate::NullableSchema<FrozenOpenAiCacheRetention>", required)]
905 prompt_cache_retention: Option<FrozenOpenAiCacheRetention>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
907 mode: Option<FrozenOpenAiCacheMode>,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 ttl: Option<FrozenOpenAiPromptCacheTtl>,
910 #[serde(default, skip_serializing_if = "Option::is_none")]
911 system: Option<bool>,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
913 rolling: Option<bool>,
914 },
915}
916
917impl FrozenCacheStrategy {
918 pub fn validate(&self) -> Result<(), &'static str> {
919 match self {
920 Self::Anthropic {
921 system,
922 tools,
923 rolling,
924 } => {
925 if *rolling == FrozenCacheTtl::OneHour
926 || !valid_cache_ttl_order([*tools, *system, *rolling])
927 {
928 return Err("invalid Anthropic frozen cache strategy");
929 }
930 }
931 Self::Bedrock {
932 system,
933 tools,
934 messages,
935 } => {
936 let count = usize::from(*system != FrozenCacheTtl::Off)
937 + usize::from(*tools != FrozenCacheTtl::Off)
938 + messages.len();
939 let mut indices = std::collections::BTreeSet::new();
940 if count > 4
941 || messages.iter().any(|point| {
942 point.ttl == FrozenCacheTtl::Off || !indices.insert(point.history_index)
943 })
944 || !valid_cache_ttl_order(
945 [*tools, *system]
946 .into_iter()
947 .chain(messages.iter().map(|point| point.ttl)),
948 )
949 {
950 return Err("invalid Bedrock frozen cache strategy");
951 }
952 }
953 Self::Google {
954 mode,
955 cached_content,
956 } => {
957 if (*mode == FrozenGoogleCacheMode::Explicit) != cached_content.is_some()
958 || cached_content.as_ref().is_some_and(|value| {
959 !value.starts_with("cachedContents/") || value.len() > 512
960 })
961 {
962 return Err("invalid Google frozen cache strategy");
963 }
964 }
965 Self::OpenAi {
966 prompt_cache_key,
967 mode,
968 ttl,
969 system,
970 rolling,
971 ..
972 } => {
973 if prompt_cache_key.as_ref().is_some_and(|key| {
974 key.chars().count() > 64 || key.chars().any(char::is_control)
975 }) || mode.is_some() != ttl.is_some()
976 || (mode.is_none()
977 && (system.unwrap_or_default() || rolling.unwrap_or_default()))
978 {
979 return Err("invalid OpenAI frozen cache strategy");
980 }
981 }
982 }
983 Ok(())
984 }
985}
986
987fn valid_cache_ttl_order(values: impl IntoIterator<Item = FrozenCacheTtl>) -> bool {
988 let mut saw_short = false;
989 for value in values {
990 match value {
991 FrozenCacheTtl::FiveMinutes => saw_short = true,
992 FrozenCacheTtl::OneHour if saw_short => return false,
993 FrozenCacheTtl::OneHour | FrozenCacheTtl::Off => {}
994 }
995 }
996 true
997}
998
999fn validate_provider_string<E>(value: &str, maximum: usize) -> Result<(), E>
1000where
1001 E: serde::de::Error,
1002{
1003 if value.is_empty() || value.len() > maximum || value.chars().any(char::is_control) {
1004 Err(E::custom("provider option string is invalid"))
1005 } else {
1006 Ok(())
1007 }
1008}
1009
1010fn deserialize_provider_string_256<'de, D>(deserializer: D) -> Result<String, D::Error>
1011where
1012 D: serde::Deserializer<'de>,
1013{
1014 let value = String::deserialize(deserializer)?;
1015 validate_provider_string::<D::Error>(&value, 256)?;
1016 Ok(value)
1017}
1018
1019fn deserialize_provider_option_256<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
1020where
1021 D: serde::Deserializer<'de>,
1022{
1023 let value = Option::<String>::deserialize(deserializer)?;
1024 if let Some(value) = &value {
1025 validate_provider_string::<D::Error>(value, 256)?;
1026 }
1027 Ok(value)
1028}
1029
1030fn deserialize_provider_option_512<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
1031where
1032 D: serde::Deserializer<'de>,
1033{
1034 let value = Option::<String>::deserialize(deserializer)?;
1035 if let Some(value) = &value {
1036 validate_provider_string::<D::Error>(value, 512)?;
1037 }
1038 Ok(value)
1039}
1040
1041fn deserialize_provider_api_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
1042where
1043 D: serde::Deserializer<'de>,
1044{
1045 let value = deserialize_provider_option_512(deserializer)?;
1046 if value
1047 .as_ref()
1048 .is_some_and(|path| !path.starts_with('/') || path.contains('?') || path.contains('#'))
1049 {
1050 return Err(serde::de::Error::custom("provider api_path is invalid"));
1051 }
1052 Ok(value)
1053}
1054
1055fn deserialize_provider_beta<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
1056where
1057 D: serde::Deserializer<'de>,
1058{
1059 let values = Vec::<String>::deserialize(deserializer)?;
1060 if values.len() > 32
1061 || values
1062 .iter()
1063 .any(|value| validate_provider_string::<D::Error>(value, 512).is_err())
1064 || values.iter().collect::<BTreeSet<_>>().len() != values.len()
1065 {
1066 return Err(serde::de::Error::custom(
1067 "provider beta options are invalid",
1068 ));
1069 }
1070 Ok(values)
1071}
1072
1073#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1074#[serde(rename_all = "snake_case")]
1075pub enum VariantOrigin {
1076 ModelsDevEffort,
1077 ModelsDevToggle,
1078 ModelsDevBudgetTokens,
1079 Explicit,
1080}
1081
1082#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1083#[serde(deny_unknown_fields)]
1084pub struct AvailableVariantDescriptor {
1085 pub id: VariantId,
1086 #[schemars(with = "ModelDisplayNameSchema")]
1087 pub display_name: String,
1088 pub origin: VariantOrigin,
1089 pub behavior_fingerprint: Sha256Digest,
1090}
1091
1092#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
1093#[serde(deny_unknown_fields)]
1094pub struct AvailableModelDescriptor {
1095 pub key: ModelKey,
1096 #[schemars(with = "ModelDisplayNameSchema")]
1097 pub display_name: String,
1098 pub capabilities: ModelCapabilities,
1099 #[schemars(length(max = 256))]
1100 pub variants: Vec<AvailableVariantDescriptor>,
1101 #[schemars(length(max = 256))]
1102 pub variant_order: Vec<VariantId>,
1103 #[serde(deserialize_with = "crate::deserialize_required_option")]
1104 #[schemars(with = "crate::NullableSchema<VariantId>", required)]
1105 pub default_variant: Option<VariantId>,
1106 pub behavior_fingerprint: Sha256Digest,
1107}
1108
1109struct ModelDisplayNameSchema;
1110impl JsonSchema for ModelDisplayNameSchema {
1111 fn inline_schema() -> bool {
1112 true
1113 }
1114 fn schema_name() -> Cow<'static, str> {
1115 Cow::Borrowed("ModelDisplayName")
1116 }
1117 fn json_schema(_: &mut SchemaGenerator) -> Schema {
1118 json_schema!({"type":"string","minLength":1,"maxLength":512,"pattern":"^(?=.*\\S)[^\\p{Cc}\\p{Cf}]+$"})
1119 }
1120}
1121
1122impl AvailableModelDescriptor {
1123 pub fn validate(&self) -> Result<(), ModelSchemaError> {
1124 validate_display_name(&self.display_name)?;
1125 self.capabilities.validate()?;
1126 if self.variants.len() > 256 {
1127 return Err(ModelSchemaError::TooManyVariants);
1128 }
1129 if self
1130 .variants
1131 .windows(2)
1132 .any(|pair| pair[0].id >= pair[1].id)
1133 {
1134 return Err(ModelSchemaError::VariantsNotStrictlySorted);
1135 }
1136 for variant in &self.variants {
1137 validate_display_name(&variant.display_name)?;
1138 }
1139 let variant_ids = self
1140 .variants
1141 .iter()
1142 .map(|variant| &variant.id)
1143 .collect::<BTreeSet<_>>();
1144 if self.variant_order.len() != self.variants.len()
1145 || self.variant_order.iter().collect::<BTreeSet<_>>() != variant_ids
1146 {
1147 return Err(ModelSchemaError::InvalidVariantOrder);
1148 }
1149 if self
1150 .default_variant
1151 .as_ref()
1152 .is_some_and(|id| !self.variants.iter().any(|variant| &variant.id == id))
1153 {
1154 return Err(ModelSchemaError::UnknownDefaultVariant);
1155 }
1156 Ok(())
1157 }
1158}
1159impl<'de> Deserialize<'de> for AvailableModelDescriptor {
1160 fn deserialize<D>(d: D) -> Result<Self, D::Error>
1161 where
1162 D: serde::Deserializer<'de>,
1163 {
1164 #[derive(Deserialize)]
1165 #[serde(deny_unknown_fields)]
1166 struct Wire {
1167 key: ModelKey,
1168 display_name: String,
1169 capabilities: ModelCapabilities,
1170 variants: Vec<AvailableVariantDescriptor>,
1171 variant_order: Vec<VariantId>,
1172 #[serde(deserialize_with = "crate::deserialize_required_option")]
1173 default_variant: Option<VariantId>,
1174 behavior_fingerprint: Sha256Digest,
1175 }
1176 let w = Wire::deserialize(d)?;
1177 let value = Self {
1178 key: w.key,
1179 display_name: w.display_name,
1180 capabilities: w.capabilities,
1181 variants: w.variants,
1182 variant_order: w.variant_order,
1183 default_variant: w.default_variant,
1184 behavior_fingerprint: w.behavior_fingerprint,
1185 };
1186 value.validate().map_err(serde::de::Error::custom)?;
1187 Ok(value)
1188 }
1189}
1190
1191#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
1192#[serde(deny_unknown_fields)]
1193pub struct ResolvedModelRef {
1194 #[serde(deserialize_with = "crate::deserialize_required_model_selection")]
1195 #[schemars(with = "crate::RequiredModelSelectionSchema")]
1196 pub selection: ModelSelection,
1197 pub provider_id: ProviderId,
1198 pub model_id: ProviderModelId,
1199 pub adapter_id: AdaptorId,
1200 pub selection_fingerprint: Sha256Digest,
1201}
1202
1203impl ResolvedModelRef {
1204 pub fn validate(&self) -> Result<(), ModelSchemaError> {
1205 if self.selection.model.provider_id() != self.provider_id
1206 || self.selection.model.model_id() != self.model_id
1207 {
1208 return Err(ModelSchemaError::ResolvedIdentityMismatch);
1209 }
1210 Ok(())
1211 }
1212}
1213impl<'de> Deserialize<'de> for ResolvedModelRef {
1214 fn deserialize<D>(d: D) -> Result<Self, D::Error>
1215 where
1216 D: serde::Deserializer<'de>,
1217 {
1218 #[derive(Deserialize)]
1219 #[serde(deny_unknown_fields)]
1220 struct Wire {
1221 #[serde(deserialize_with = "crate::deserialize_required_model_selection")]
1222 selection: ModelSelection,
1223 provider_id: ProviderId,
1224 model_id: ProviderModelId,
1225 adapter_id: AdaptorId,
1226 selection_fingerprint: Sha256Digest,
1227 }
1228 let w = Wire::deserialize(d)?;
1229 let value = Self {
1230 selection: w.selection,
1231 provider_id: w.provider_id,
1232 model_id: w.model_id,
1233 adapter_id: w.adapter_id,
1234 selection_fingerprint: w.selection_fingerprint,
1235 };
1236 value.validate().map_err(serde::de::Error::custom)?;
1237 Ok(value)
1238 }
1239}
1240
1241#[derive(Clone, Debug, JsonSchema, PartialEq, Serialize)]
1242#[serde(deny_unknown_fields)]
1243pub struct FrozenModelBinding {
1244 pub manifest_revision: ModelSnapshotRevision,
1245 pub blueprint_fingerprint: Sha256Digest,
1246 pub selection: ModelSelection,
1247 pub source: FrozenProviderSource,
1248 pub config_override_fingerprint: Sha256Digest,
1249 pub credential_binding: FrozenCredentialBinding,
1250 pub setup_binding: FrozenSetupBinding,
1251 pub endpoint_identity: SafeEndpointIdentity,
1252 pub provider_recipe: ProviderRecipeId,
1253 pub protocol_recipe: ProtocolRecipeId,
1254 pub setup_recipe: ProviderSetupRecipeId,
1255 pub compiler_version: RecipeCompilerVersion,
1256 #[schemars(with = "LanguageModelDescriptorSchema")]
1257 pub descriptor: oven_sdk::LanguageModelDescriptor,
1258 pub defaults: FrozenResolvedRequestDefaults,
1259 pub options: FrozenProviderOptions,
1260 pub static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
1261 pub behavior_fingerprint: Sha256Digest,
1262 pub selection_fingerprint: Sha256Digest,
1263}
1264
1265impl FrozenModelBinding {
1266 pub fn validate(&self) -> Result<(), ModelSchemaError> {
1267 if self.descriptor.identity.provider_id.as_str()
1268 != self.selection.model.provider_id().as_str()
1269 || self.descriptor.identity.model_id.as_str()
1270 != self.selection.model.model_id().as_str()
1271 {
1272 return Err(ModelSchemaError::DescriptorIdentityMismatch);
1273 }
1274 if self.descriptor.capabilities.compaction == oven_sdk::CompactionCapability::Native
1275 && !matches!(
1276 self.protocol_recipe.as_str(),
1277 "oven.openai.responses" | "oven.azure.openai.responses"
1278 )
1279 {
1280 return Err(ModelSchemaError::NativeCompactionUnsupported);
1281 }
1282 self.defaults
1283 .validate()
1284 .map_err(|_| ModelSchemaError::InvalidFrozenDefaults)?;
1285 Ok(())
1286 }
1287
1288 #[must_use]
1289 pub fn matches_blueprint(&self, blueprint: &CompiledSafeModelBlueprint) -> bool {
1290 let behavior = self.selection.variant.as_ref().map_or_else(
1291 || {
1292 Some((
1293 &blueprint.descriptor,
1294 &blueprint.defaults,
1295 &blueprint.options,
1296 &blueprint.static_headers,
1297 &blueprint.behavior_fingerprint,
1298 &blueprint.selection_fingerprint,
1299 ))
1300 },
1301 |variant| {
1302 blueprint
1303 .variants
1304 .iter()
1305 .find(|candidate| &candidate.id == variant)
1306 .map(|candidate| {
1307 (
1308 &candidate.descriptor,
1309 &candidate.defaults,
1310 &candidate.options,
1311 &candidate.static_headers,
1312 &candidate.behavior_fingerprint,
1313 &candidate.selection_fingerprint,
1314 )
1315 })
1316 },
1317 );
1318 let Some((
1319 descriptor,
1320 defaults,
1321 options,
1322 static_headers,
1323 behavior_fingerprint,
1324 selection_fingerprint,
1325 )) = behavior
1326 else {
1327 return false;
1328 };
1329 self.blueprint_fingerprint == blueprint.blueprint_fingerprint
1330 && self.selection.model == blueprint.selection.model
1331 && self.source == blueprint.source
1332 && self.config_override_fingerprint == blueprint.config_override_fingerprint
1333 && self.credential_binding == blueprint.credential_binding
1334 && self.setup_binding == blueprint.setup_binding
1335 && self.endpoint_identity == blueprint.endpoint_identity
1336 && self.provider_recipe == blueprint.provider_recipe
1337 && self.protocol_recipe == blueprint.protocol_recipe
1338 && self.setup_recipe == blueprint.setup_recipe
1339 && self.compiler_version == blueprint.compiler_version
1340 && &self.descriptor == descriptor
1341 && &self.defaults == defaults
1342 && &self.options == options
1343 && &self.static_headers == static_headers
1344 && &self.behavior_fingerprint == behavior_fingerprint
1345 && &self.selection_fingerprint == selection_fingerprint
1346 }
1347}
1348impl<'de> Deserialize<'de> for FrozenModelBinding {
1349 fn deserialize<D>(d: D) -> Result<Self, D::Error>
1350 where
1351 D: serde::Deserializer<'de>,
1352 {
1353 #[derive(Deserialize)]
1354 #[serde(deny_unknown_fields)]
1355 struct Wire {
1356 manifest_revision: ModelSnapshotRevision,
1357 blueprint_fingerprint: Sha256Digest,
1358 selection: ModelSelection,
1359 source: FrozenProviderSource,
1360 config_override_fingerprint: Sha256Digest,
1361 credential_binding: FrozenCredentialBinding,
1362 setup_binding: FrozenSetupBinding,
1363 endpoint_identity: SafeEndpointIdentity,
1364 provider_recipe: ProviderRecipeId,
1365 protocol_recipe: ProtocolRecipeId,
1366 setup_recipe: ProviderSetupRecipeId,
1367 compiler_version: RecipeCompilerVersion,
1368 descriptor: oven_sdk::LanguageModelDescriptor,
1369 defaults: FrozenResolvedRequestDefaults,
1370 options: FrozenProviderOptions,
1371 static_headers: BTreeMap<HeaderName, SafeStaticHeaderValue>,
1372 behavior_fingerprint: Sha256Digest,
1373 selection_fingerprint: Sha256Digest,
1374 }
1375 let w = Wire::deserialize(d)?;
1376 let value = Self {
1377 manifest_revision: w.manifest_revision,
1378 blueprint_fingerprint: w.blueprint_fingerprint,
1379 selection: w.selection,
1380 source: w.source,
1381 config_override_fingerprint: w.config_override_fingerprint,
1382 credential_binding: w.credential_binding,
1383 setup_binding: w.setup_binding,
1384 endpoint_identity: w.endpoint_identity,
1385 provider_recipe: w.provider_recipe,
1386 protocol_recipe: w.protocol_recipe,
1387 setup_recipe: w.setup_recipe,
1388 compiler_version: w.compiler_version,
1389 descriptor: w.descriptor,
1390 defaults: w.defaults,
1391 options: w.options,
1392 static_headers: w.static_headers,
1393 behavior_fingerprint: w.behavior_fingerprint,
1394 selection_fingerprint: w.selection_fingerprint,
1395 };
1396 value.validate().map_err(serde::de::Error::custom)?;
1397 Ok(value)
1398 }
1399}
1400
1401fn validate_display_name(value: &str) -> Result<(), ModelSchemaError> {
1402 if value.trim().is_empty() || value.len() > 512 || value.chars().any(char::is_control) {
1403 Err(ModelSchemaError::InvalidDisplayName)
1404 } else {
1405 Ok(())
1406 }
1407}
1408
1409#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1410pub enum ModelSchemaError {
1411 InvalidMimeType,
1412 EmptyModalities,
1413 InvalidTokenLimits,
1414 ParallelToolsWithoutTools,
1415 InvalidOutputModality,
1416 InvalidMediaCapability,
1417 NonFiniteNumber,
1418 TemperatureOutOfRange,
1419 TopPOutOfRange,
1420 InvalidMaxOutputTokens,
1421 InvalidStopSequence,
1422 InvalidDisplayName,
1423 TooManyVariants,
1424 VariantsNotStrictlySorted,
1425 InvalidVariantOrder,
1426 UnknownDefaultVariant,
1427 ResolvedIdentityMismatch,
1428 DescriptorIdentityMismatch,
1429 NativeCompactionUnsupported,
1430 ProviderOptionsAdapterMismatch,
1431 InvalidReasoningBudget,
1432 InvalidToolName,
1433 FingerprintEncoding,
1434 SelectionFingerprintMismatch,
1435 InvalidFrozenDefaults,
1436}
1437impl fmt::Display for ModelSchemaError {
1438 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1439 f.write_str(match self {
1440 Self::InvalidMimeType => "invalid MIME type",
1441 Self::EmptyModalities => "model input and output modalities must be nonempty",
1442 Self::InvalidTokenLimits => {
1443 "model token limits must be positive and output must not exceed context"
1444 }
1445 Self::ParallelToolsWithoutTools => "parallel tool calls require tool calling",
1446 Self::InvalidOutputModality => "PDF is not a valid output modality",
1447 Self::InvalidMediaCapability => {
1448 "non-text input modalities require exactly one matching positive media capability"
1449 }
1450 Self::NonFiniteNumber => "number must be finite",
1451 Self::TemperatureOutOfRange => "temperature must be in 0.0..=2.0",
1452 Self::TopPOutOfRange => "top_p must be in 0.0..=1.0",
1453 Self::InvalidMaxOutputTokens => "max_output_tokens must be positive",
1454 Self::InvalidStopSequence => {
1455 "stop must contain at most eight control-free strings of 1..=256 bytes"
1456 }
1457 Self::InvalidDisplayName => {
1458 "display name must be nonblank, control-free, and at most 512 bytes"
1459 }
1460 Self::TooManyVariants => "model has more than 256 variants",
1461 Self::VariantsNotStrictlySorted => "variants must be strictly sorted by ID",
1462 Self::InvalidVariantOrder => {
1463 "variant order must contain every available variant exactly once"
1464 }
1465 Self::UnknownDefaultVariant => "default variant is not present",
1466 Self::ResolvedIdentityMismatch => {
1467 "resolved provider/model fields do not match selection"
1468 }
1469 Self::DescriptorIdentityMismatch => {
1470 "frozen descriptor key does not match resolved selection"
1471 }
1472 Self::NativeCompactionUnsupported => {
1473 "provider-native compaction requires an OpenAI or Azure Responses recipe"
1474 }
1475 Self::ProviderOptionsAdapterMismatch => {
1476 "provider options do not match the resolved adapter"
1477 }
1478 Self::InvalidReasoningBudget => "reasoning token budget must be -1 or nonnegative",
1479 Self::InvalidToolName => "model tool name must be control-free and 1..=64 bytes",
1480 Self::FingerprintEncoding => "model selection fingerprint encoding failed",
1481 Self::SelectionFingerprintMismatch => {
1482 "selection fingerprint does not match the complete frozen model binding"
1483 }
1484 Self::InvalidFrozenDefaults => "invalid frozen request defaults",
1485 })
1486 }
1487}
1488impl std::error::Error for ModelSchemaError {}