Skip to main content

cookie_agent_protocol/
model.rs

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 {}