Skip to main content

systemprompt_models/schema/
capabilities.rs

1//! Per-provider JSON-Schema capability matrices.
2//!
3//! [`ProviderCapabilities`] declares which JSON-Schema constructs a provider's
4//! tool/output schema parser accepts. It is the input to
5//! [`super::SchemaSanitizer`], which strips everything a provider does not
6//! support. The matrices live here in `shared/models` so both the gateway wire
7//! codecs and the agent-flow provider clients resolve the same authority; the
8//! wire protocol picks one via
9//! [`crate::profile::WireProtocol::schema_capabilities`].
10
11use serde_json::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct SchemaComposition {
15    pub allof: bool,
16    pub anyof: bool,
17    pub oneof: bool,
18    pub if_then_else: bool,
19    pub not: bool,
20}
21
22#[expect(
23    clippy::struct_excessive_bools,
24    reason = "schema feature matrix: each bool is an independent JSON-Schema construct the \
25              provider does or does not accept, not state"
26)]
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SchemaFeatures {
29    pub references: bool,
30    pub definitions: bool,
31    pub additional_properties: bool,
32    pub const_values: bool,
33    /// `exclusiveMinimum` / `exclusiveMaximum` numeric bounds. Gemini's
34    /// OpenAPI-subset parser rejects these; Anthropic and `OpenAI` accept them.
35    pub exclusive_bounds: bool,
36    /// `propertyNames` / `patternProperties` object constraints. Rejected by
37    /// Gemini's parser; accepted by Anthropic and `OpenAI`.
38    pub property_names: bool,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct ProviderCapabilities {
43    pub composition: SchemaComposition,
44    pub features: SchemaFeatures,
45}
46
47impl ProviderCapabilities {
48    pub const fn anthropic() -> Self {
49        Self {
50            composition: SchemaComposition {
51                allof: true,
52                anyof: true,
53                oneof: true,
54                if_then_else: true,
55                not: true,
56            },
57            features: SchemaFeatures {
58                references: true,
59                definitions: true,
60                additional_properties: true,
61                const_values: true,
62                exclusive_bounds: true,
63                property_names: true,
64            },
65        }
66    }
67
68    pub const fn openai() -> Self {
69        Self {
70            composition: SchemaComposition {
71                allof: true,
72                anyof: true,
73                oneof: true,
74                if_then_else: false,
75                not: false,
76            },
77            features: SchemaFeatures {
78                references: true,
79                definitions: true,
80                additional_properties: true,
81                const_values: true,
82                exclusive_bounds: true,
83                property_names: true,
84            },
85        }
86    }
87
88    pub const fn gemini() -> Self {
89        Self {
90            composition: SchemaComposition {
91                allof: false,
92                anyof: true,
93                oneof: false,
94                if_then_else: false,
95                not: false,
96            },
97            features: SchemaFeatures {
98                references: false,
99                definitions: false,
100                additional_properties: false,
101                const_values: false,
102                exclusive_bounds: false,
103                property_names: false,
104            },
105        }
106    }
107
108    pub fn requires_transformation(&self, schema: &Value) -> bool {
109        if let Some(obj) = schema.as_object() {
110            if obj.contains_key("allOf") && !self.composition.allof {
111                return true;
112            }
113            if obj.contains_key("anyOf") && !self.composition.anyof {
114                return true;
115            }
116            if obj.contains_key("oneOf") && !self.composition.oneof {
117                return true;
118            }
119            if obj.contains_key("if") && !self.composition.if_then_else {
120                return true;
121            }
122            if obj.contains_key("$ref") && !self.features.references {
123                return true;
124            }
125            if (obj.contains_key("definitions") || obj.contains_key("$defs"))
126                && !self.features.definitions
127            {
128                return true;
129            }
130            if obj.contains_key("not") && !self.composition.not {
131                return true;
132            }
133        }
134        false
135    }
136}