1use std::collections::BTreeSet;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 Background, BridgeError, ErrorCode, ImageAction, ImageSize, InputFidelity, Moderation,
10 OutputFormat, Quality,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "snake_case")]
16pub enum SupportLevel {
17 Native,
19 Emulated,
21 Unsupported,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27#[serde(deny_unknown_fields)]
28pub struct U8Range {
29 pub min: u8,
31 pub max: u8,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
37#[serde(rename_all = "snake_case")]
38pub enum BatchMode {
39 Native,
41 FanOut,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct BatchCapabilities {
49 pub mode: BatchMode,
51 pub native_count: U8Range,
53 pub max_parallel_outputs: u8,
55}
56
57impl U8Range {
58 #[must_use]
60 pub const fn contains(self, value: u8) -> bool {
61 value >= self.min && value <= self.max
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
67#[serde(deny_unknown_fields)]
68pub struct SizeCapabilities {
69 pub auto: bool,
71 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
73 pub allowed: BTreeSet<ImageSize>,
74 pub arbitrary: bool,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub min_edge: Option<u32>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub max_edge: Option<u32>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub edge_multiple: Option<u32>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub min_pixels: Option<u64>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub max_pixels: Option<u64>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub max_aspect_ratio: Option<f64>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98#[serde(deny_unknown_fields)]
99pub struct InputCapabilities {
100 pub support: SupportLevel,
102 pub max_count: u16,
104 pub max_bytes_each: u64,
106 pub max_bytes_total: u64,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
112#[serde(deny_unknown_fields)]
113pub struct ProviderCapabilities {
114 pub provider: String,
116 pub implementation_version: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub model: Option<String>,
121 pub experimental: bool,
123 pub generation: bool,
125 pub edits: bool,
127 pub count: U8Range,
129 pub batching: BatchCapabilities,
131 pub sizes: SizeCapabilities,
133 pub aspect_ratio: SupportLevel,
135 pub resolution: SupportLevel,
137 pub qualities: BTreeSet<Quality>,
139 pub output_formats: BTreeSet<OutputFormat>,
141 pub backgrounds: BTreeSet<Background>,
143 pub transparent_background: SupportLevel,
145 pub moderation: BTreeSet<Moderation>,
147 pub negative_prompt: SupportLevel,
149 pub revised_prompt: SupportLevel,
151 pub user_attribution: SupportLevel,
153 pub input_fidelities: BTreeSet<InputFidelity>,
155 pub actions: BTreeSet<ImageAction>,
157 pub reference_images: InputCapabilities,
159 pub edit_images: InputCapabilities,
161 pub masks: InputCapabilities,
163 pub partial_images: U8Range,
165 pub persistent_sessions: bool,
167 pub explicit_threads: bool,
169}
170
171impl ProviderCapabilities {
172 pub fn validate(&self) -> Result<(), BridgeError> {
174 if self.count.min == 0 || self.count.min > self.count.max {
175 return Err(invalid_capability(
176 self,
177 "provider output-count capability range is invalid",
178 ));
179 }
180 if self.batching.native_count.min == 0
181 || self.batching.native_count.min > self.batching.native_count.max
182 || self.batching.native_count.min < self.count.min
183 || self.batching.native_count.max > self.count.max
184 || self.batching.max_parallel_outputs == 0
185 || self.batching.max_parallel_outputs > self.count.max
186 || (self.batching.mode == BatchMode::Native && self.batching.native_count != self.count)
187 || (self.batching.mode == BatchMode::FanOut
188 && self.batching.native_count.max >= self.count.max)
189 {
190 return Err(invalid_capability(
191 self,
192 "provider batching capability is inconsistent with its output-count range",
193 ));
194 }
195 if self.partial_images.min > self.partial_images.max {
196 return Err(invalid_capability(
197 self,
198 "provider partial-image capability range is invalid",
199 ));
200 }
201 let declares_native = self.backgrounds.contains(&Background::Transparent);
202 if declares_native != (self.transparent_background == SupportLevel::Native) {
203 return Err(invalid_capability(
204 self,
205 "provider transparent-background capability is inconsistent",
206 ));
207 }
208 if self.transparent_background == SupportLevel::Emulated
209 && (!self
210 .backgrounds
211 .iter()
212 .any(|background| matches!(background, Background::Auto | Background::Opaque))
213 || !self
214 .output_formats
215 .iter()
216 .any(|format| matches!(format, OutputFormat::Png | OutputFormat::Webp)))
217 {
218 return Err(invalid_capability(
219 self,
220 "emulated transparent output requires a keyable background and alpha format",
221 ));
222 }
223 Ok(())
224 }
225}
226
227fn invalid_capability(capabilities: &ProviderCapabilities, message: &str) -> BridgeError {
228 BridgeError::new(ErrorCode::Protocol, message).with_provider(&capabilities.provider)
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct ProviderDescriptor {
235 pub name: String,
237 pub display_name: String,
239 pub version: String,
241 pub experimental: bool,
243 #[serde(default)]
245 pub models: Vec<String>,
246}
247
248#[cfg(test)]
249mod tests {
250 #![allow(clippy::unwrap_used)]
251
252 use super::ProviderDescriptor;
253
254 #[test]
255 fn provider_model_inventory_is_additive_for_older_payloads() {
256 let descriptor: ProviderDescriptor = serde_json::from_str(
257 r#"{"name":"test","display_name":"Test","version":"1","experimental":false}"#,
258 )
259 .unwrap();
260 assert!(descriptor.models.is_empty());
261
262 let encoded = serde_json::to_value(ProviderDescriptor {
263 models: vec!["gpt-image-2".to_owned()],
264 ..descriptor
265 })
266 .unwrap();
267 assert_eq!(encoded["models"][0], "gpt-image-2");
268 }
269}