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//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use serde_json::Value;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct SchemaComposition {
18    pub allof: bool,
19    pub anyof: bool,
20    pub oneof: bool,
21    pub if_then_else: bool,
22    pub not: bool,
23}
24
25#[expect(
26    clippy::struct_excessive_bools,
27    reason = "schema feature matrix: each bool is an independent JSON-Schema construct the \
28              provider does or does not accept, not state"
29)]
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct SchemaFeatures {
32    pub references: bool,
33    pub definitions: bool,
34    pub additional_properties: bool,
35    pub const_values: bool,
36    /// `exclusiveMinimum` / `exclusiveMaximum` numeric bounds. Gemini's
37    /// OpenAPI-subset parser rejects these; Anthropic and `OpenAI` accept them.
38    pub exclusive_bounds: bool,
39    /// `propertyNames` / `patternProperties` object constraints. Rejected by
40    /// Gemini's parser; accepted by Anthropic and `OpenAI`.
41    pub property_names: bool,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct ProviderCapabilities {
46    pub composition: SchemaComposition,
47    pub features: SchemaFeatures,
48}
49
50impl ProviderCapabilities {
51    pub const fn anthropic() -> Self {
52        Self {
53            composition: SchemaComposition {
54                allof: true,
55                anyof: true,
56                oneof: true,
57                if_then_else: true,
58                not: true,
59            },
60            features: SchemaFeatures {
61                references: true,
62                definitions: true,
63                additional_properties: true,
64                const_values: true,
65                exclusive_bounds: true,
66                property_names: true,
67            },
68        }
69    }
70
71    pub const fn openai() -> Self {
72        Self {
73            composition: SchemaComposition {
74                allof: true,
75                anyof: true,
76                oneof: true,
77                if_then_else: false,
78                not: false,
79            },
80            features: SchemaFeatures {
81                references: true,
82                definitions: true,
83                additional_properties: true,
84                const_values: true,
85                exclusive_bounds: true,
86                property_names: true,
87            },
88        }
89    }
90
91    pub const fn gemini() -> Self {
92        Self {
93            composition: SchemaComposition {
94                allof: false,
95                anyof: true,
96                oneof: false,
97                if_then_else: false,
98                not: false,
99            },
100            features: SchemaFeatures {
101                references: false,
102                definitions: false,
103                additional_properties: false,
104                const_values: false,
105                exclusive_bounds: false,
106                property_names: false,
107            },
108        }
109    }
110
111    pub fn requires_transformation(&self, schema: &Value) -> bool {
112        if let Some(obj) = schema.as_object() {
113            if obj.contains_key("allOf") && !self.composition.allof {
114                return true;
115            }
116            if obj.contains_key("anyOf") && !self.composition.anyof {
117                return true;
118            }
119            if obj.contains_key("oneOf") && !self.composition.oneof {
120                return true;
121            }
122            if obj.contains_key("if") && !self.composition.if_then_else {
123                return true;
124            }
125            if obj.contains_key("$ref") && !self.features.references {
126                return true;
127            }
128            if (obj.contains_key("definitions") || obj.contains_key("$defs"))
129                && !self.features.definitions
130            {
131                return true;
132            }
133            if obj.contains_key("not") && !self.composition.not {
134                return true;
135            }
136        }
137        false
138    }
139}