Skip to main content

imagegen_bridge_core/
capabilities.rs

1//! Provider capability descriptions used for explicit negotiation.
2
3use 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/// Degree to which a provider supports a semantic feature.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "snake_case")]
16pub enum SupportLevel {
17    /// The provider handles the feature natively.
18    Native,
19    /// The bridge can emulate the feature with a reported transformation.
20    Emulated,
21    /// The feature is unavailable.
22    Unsupported,
23}
24
25/// Inclusive range for a small integer parameter.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
27#[serde(deny_unknown_fields)]
28pub struct U8Range {
29    /// Inclusive minimum.
30    pub min: u8,
31    /// Inclusive maximum.
32    pub max: u8,
33}
34
35/// How a provider fulfills a request for more than one output image.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
37#[serde(rename_all = "snake_case")]
38pub enum BatchMode {
39    /// One upstream provider operation can return the requested output count.
40    Native,
41    /// The bridge fans the request out into multiple bounded upstream operations.
42    FanOut,
43}
44
45/// Effective and native multi-output behavior for a provider/model pair.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct BatchCapabilities {
49    /// Whether multi-output execution is native or bridge-managed fan-out.
50    pub mode: BatchMode,
51    /// Output-count range accepted by one upstream provider operation.
52    pub native_count: U8Range,
53    /// Maximum simultaneous upstream operations across active batches.
54    pub max_parallel_outputs: u8,
55}
56
57impl U8Range {
58    /// Returns true when the range contains the value.
59    #[must_use]
60    pub const fn contains(self, value: u8) -> bool {
61        value >= self.min && value <= self.max
62    }
63}
64
65/// Explicit-size constraints for a provider or model.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
67#[serde(deny_unknown_fields)]
68pub struct SizeCapabilities {
69    /// Whether `auto` is accepted.
70    pub auto: bool,
71    /// Fixed accepted sizes. Empty when generic constraints apply.
72    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
73    pub allowed: BTreeSet<ImageSize>,
74    /// Whether arbitrary explicit dimensions are accepted.
75    pub arbitrary: bool,
76    /// Minimum edge for arbitrary dimensions.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub min_edge: Option<u32>,
79    /// Maximum edge for arbitrary dimensions.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub max_edge: Option<u32>,
82    /// Required edge multiple for arbitrary dimensions.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub edge_multiple: Option<u32>,
85    /// Minimum total pixels for arbitrary dimensions.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub min_pixels: Option<u64>,
88    /// Maximum total pixels for arbitrary dimensions.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub max_pixels: Option<u64>,
91    /// Maximum long-edge to short-edge ratio.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub max_aspect_ratio: Option<f64>,
94}
95
96/// Input-image constraints.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98#[serde(deny_unknown_fields)]
99pub struct InputCapabilities {
100    /// Support level for the input class.
101    pub support: SupportLevel,
102    /// Maximum number of inputs.
103    pub max_count: u16,
104    /// Maximum decoded bytes per input.
105    pub max_bytes_each: u64,
106    /// Maximum decoded bytes across all inputs.
107    pub max_bytes_total: u64,
108}
109
110/// Complete capability declaration for one provider/model pair.
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
112#[serde(deny_unknown_fields)]
113pub struct ProviderCapabilities {
114    /// Stable provider name.
115    pub provider: String,
116    /// Provider implementation version.
117    pub implementation_version: String,
118    /// Model for which these capabilities apply.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub model: Option<String>,
121    /// Whether this adapter targets an unstable/private upstream interface.
122    pub experimental: bool,
123    /// Whether generation is supported.
124    pub generation: bool,
125    /// Whether edits are supported.
126    pub edits: bool,
127    /// Supported output count range.
128    pub count: U8Range,
129    /// How the advertised output count is fulfilled upstream.
130    pub batching: BatchCapabilities,
131    /// Supported size behavior.
132    pub sizes: SizeCapabilities,
133    /// Aspect-ratio hint support.
134    pub aspect_ratio: SupportLevel,
135    /// Coarse resolution hint support.
136    pub resolution: SupportLevel,
137    /// Supported qualities.
138    pub qualities: BTreeSet<Quality>,
139    /// Supported output encodings.
140    pub output_formats: BTreeSet<OutputFormat>,
141    /// Supported backgrounds.
142    pub backgrounds: BTreeSet<Background>,
143    /// Native or bridge-emulated transparent-background support.
144    pub transparent_background: SupportLevel,
145    /// Supported moderation values.
146    pub moderation: BTreeSet<Moderation>,
147    /// Negative prompt support.
148    pub negative_prompt: SupportLevel,
149    /// Revised-prompt availability.
150    pub revised_prompt: SupportLevel,
151    /// Opaque end-user attribution support.
152    pub user_attribution: SupportLevel,
153    /// Supported explicit input-fidelity values.
154    pub input_fidelities: BTreeSet<InputFidelity>,
155    /// Supported image-tool actions.
156    pub actions: BTreeSet<ImageAction>,
157    /// Reference-image constraints.
158    pub reference_images: InputCapabilities,
159    /// Edit-image constraints.
160    pub edit_images: InputCapabilities,
161    /// Mask constraints.
162    pub masks: InputCapabilities,
163    /// Supported partial image count.
164    pub partial_images: U8Range,
165    /// Whether provider-backed persistent sessions are supported.
166    pub persistent_sessions: bool,
167    /// Whether explicit upstream thread IDs are supported.
168    pub explicit_threads: bool,
169}
170
171impl ProviderCapabilities {
172    /// Rejects semantically inconsistent dynamic provider declarations.
173    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/// Provider identity shown in discovery endpoints.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct ProviderDescriptor {
235    /// Stable registry name.
236    pub name: String,
237    /// Human-readable provider title.
238    pub display_name: String,
239    /// Provider implementation version.
240    pub version: String,
241    /// Whether the adapter is experimental.
242    pub experimental: bool,
243    /// Image models that can be queried through the capability endpoint.
244    #[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}