Skip to main content

imagegen_bridge_core/
request.rs

1//! Normalized image request types.
2
3use std::path::PathBuf;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Deserializer, Serialize, de};
7
8use crate::{
9    ArtifactCollisionPolicy, ArtifactMetadataPolicy, AspectRatio, Background, BatchExecution,
10    CompatibilityMode, FallbackPolicy, ImageAction, ImageSize, InputFidelity, Moderation,
11    MultiImageFailurePolicy, NegativePromptMode, OutputFormat, Quality, Resolution, ResponseFormat,
12    RevisedPromptPolicy, SessionMode, TransparencyMode,
13};
14
15const COMMON_REQUEST_FIELDS: &[&str] = &[
16    "version",
17    "prompt",
18    "negative_prompt",
19    "parameters",
20    "routing",
21    "session",
22    "output",
23    "policies",
24    "idempotency_key",
25    "timeout_ms",
26    "user",
27];
28
29/// A complete provider-neutral image request.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
31pub struct ImageRequest {
32    /// Contract version. Currently `1`.
33    #[serde(default = "default_contract_version")]
34    pub version: String,
35    /// Positive prompt passed to image generation.
36    pub prompt: String,
37    /// Optional negative prompt interpreted according to bridge policy.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub negative_prompt: Option<String>,
40    /// Generation or edit inputs.
41    #[serde(flatten)]
42    pub operation: ImageOperation,
43    /// Image-generation parameters.
44    #[serde(default)]
45    pub parameters: GenerationParameters,
46    /// Provider and model routing controls.
47    #[serde(default)]
48    pub routing: RoutingOptions,
49    /// Session behavior for providers that support conversations.
50    #[serde(default)]
51    pub session: SessionOptions,
52    /// Output delivery and artifact controls.
53    #[serde(default)]
54    pub output: OutputOptions,
55    /// Fallback and compatibility policies.
56    #[serde(default)]
57    pub policies: RequestPolicies,
58    /// Optional client idempotency key.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub idempotency_key: Option<String>,
61    /// Optional request deadline in milliseconds.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub timeout_ms: Option<u64>,
64    /// Optional opaque end-user identifier forwarded only by configured providers.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub user: Option<String>,
67}
68
69impl<'de> Deserialize<'de> for ImageRequest {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: Deserializer<'de>,
73    {
74        let mut object = serde_json::Map::<String, serde_json::Value>::deserialize(deserializer)?;
75        for field in object.keys() {
76            if field != "operation"
77                && field != "reference_images"
78                && field != "images"
79                && field != "mask"
80                && !COMMON_REQUEST_FIELDS.contains(&field.as_str())
81            {
82                return Err(de::Error::unknown_field(
83                    field,
84                    &[
85                        "version",
86                        "prompt",
87                        "negative_prompt",
88                        "operation",
89                        "reference_images",
90                        "images",
91                        "mask",
92                        "parameters",
93                        "routing",
94                        "session",
95                        "output",
96                        "policies",
97                        "idempotency_key",
98                        "timeout_ms",
99                        "user",
100                    ],
101                ));
102            }
103        }
104
105        let operation_tag = object
106            .remove("operation")
107            .ok_or_else(|| de::Error::missing_field("operation"))?;
108        let operation_name = operation_tag
109            .as_str()
110            .ok_or_else(|| de::Error::custom("operation must be a string"))?;
111        let operation_fields: &[&str] = match operation_name {
112            "generate" => &["reference_images"],
113            "edit" => &["images", "mask", "reference_images"],
114            _ => {
115                return Err(de::Error::unknown_variant(
116                    operation_name,
117                    &["generate", "edit"],
118                ));
119            }
120        };
121        for forbidden in ["images", "mask", "reference_images"] {
122            if object.contains_key(forbidden) && !operation_fields.contains(&forbidden) {
123                return Err(de::Error::custom(format!(
124                    "field '{forbidden}' is invalid for operation '{operation_name}'"
125                )));
126            }
127        }
128        let mut operation = serde_json::Map::new();
129        operation.insert("operation".to_owned(), operation_tag);
130        for field in operation_fields {
131            if let Some(value) = object.remove(*field) {
132                operation.insert((*field).to_owned(), value);
133            }
134        }
135        let operation = serde_json::from_value(serde_json::Value::Object(operation))
136            .map_err(de::Error::custom)?;
137        let fields: RequestFields =
138            serde_json::from_value(serde_json::Value::Object(object)).map_err(de::Error::custom)?;
139        Ok(Self {
140            version: fields.version,
141            prompt: fields.prompt,
142            negative_prompt: fields.negative_prompt,
143            operation,
144            parameters: fields.parameters,
145            routing: fields.routing,
146            session: fields.session,
147            output: fields.output,
148            policies: fields.policies,
149            idempotency_key: fields.idempotency_key,
150            timeout_ms: fields.timeout_ms,
151            user: fields.user,
152        })
153    }
154}
155
156#[derive(Deserialize)]
157#[serde(deny_unknown_fields)]
158struct RequestFields {
159    #[serde(default = "default_contract_version")]
160    version: String,
161    prompt: String,
162    negative_prompt: Option<String>,
163    #[serde(default)]
164    parameters: GenerationParameters,
165    #[serde(default)]
166    routing: RoutingOptions,
167    #[serde(default)]
168    session: SessionOptions,
169    #[serde(default)]
170    output: OutputOptions,
171    #[serde(default)]
172    policies: RequestPolicies,
173    idempotency_key: Option<String>,
174    timeout_ms: Option<u64>,
175    user: Option<String>,
176}
177
178impl ImageRequest {
179    /// Creates a generation request using safe defaults.
180    #[must_use]
181    pub fn generate(prompt: impl Into<String>) -> Self {
182        Self {
183            version: default_contract_version(),
184            prompt: prompt.into(),
185            negative_prompt: None,
186            operation: ImageOperation::Generate {
187                reference_images: Vec::new(),
188            },
189            parameters: GenerationParameters::default(),
190            routing: RoutingOptions::default(),
191            session: SessionOptions::default(),
192            output: OutputOptions::default(),
193            policies: RequestPolicies::default(),
194            idempotency_key: None,
195            timeout_ms: None,
196            user: None,
197        }
198    }
199}
200
201fn default_contract_version() -> String {
202    crate::CONTRACT_VERSION.to_owned()
203}
204
205/// Operation-specific image inputs.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
207#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
208pub enum ImageOperation {
209    /// Generate an image, optionally using reference images.
210    Generate {
211        /// Images used as visual references.
212        #[serde(default, skip_serializing_if = "Vec::is_empty")]
213        reference_images: Vec<ImageInput>,
214    },
215    /// Edit one or more source images with an optional mask and references.
216    Edit {
217        /// Source images to edit.
218        images: Vec<ImageInput>,
219        /// Optional edit mask.
220        #[serde(skip_serializing_if = "Option::is_none")]
221        mask: Option<Box<ImageInput>>,
222        /// Additional visual references.
223        #[serde(default, skip_serializing_if = "Vec::is_empty")]
224        reference_images: Vec<ImageInput>,
225    },
226}
227
228impl ImageOperation {
229    /// Returns all reference inputs without edit sources or masks.
230    #[must_use]
231    pub fn reference_images(&self) -> &[ImageInput] {
232        match self {
233            Self::Generate { reference_images }
234            | Self::Edit {
235                reference_images, ..
236            } => reference_images,
237        }
238    }
239}
240
241/// Supported image input locations.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
243#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
244pub enum ImageSource {
245    /// Path resolved under configured allowed roots.
246    File {
247        /// Local filesystem path.
248        path: PathBuf,
249    },
250    /// Remote HTTP(S) URL, only when remote loading is enabled.
251    Url {
252        /// Remote URL.
253        url: String,
254    },
255    /// RFC 2397 data URL.
256    DataUrl {
257        /// Complete data URL.
258        data_url: String,
259    },
260    /// Base64-encoded image body.
261    Base64 {
262        /// Encoded body without a data URL prefix.
263        data: String,
264    },
265}
266
267/// One image input plus optional metadata.
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
269#[schemars(deny_unknown_fields)]
270pub struct ImageInput {
271    /// Source from which bytes will be loaded.
272    #[serde(flatten)]
273    pub source: ImageSource,
274    /// Optional expected media type.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub media_type: Option<String>,
277    /// Optional safe logical filename.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub filename: Option<String>,
280}
281
282impl<'de> Deserialize<'de> for ImageInput {
283    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284    where
285        D: Deserializer<'de>,
286    {
287        let mut fields = serde_json::Map::<String, serde_json::Value>::deserialize(deserializer)?;
288        let media_type = fields
289            .remove("media_type")
290            .map(serde_json::from_value::<Option<String>>)
291            .transpose()
292            .map_err(de::Error::custom)?
293            .flatten();
294        let filename = fields
295            .remove("filename")
296            .map(serde_json::from_value::<Option<String>>)
297            .transpose()
298            .map_err(de::Error::custom)?
299            .flatten();
300        let source =
301            serde_json::from_value(serde_json::Value::Object(fields)).map_err(de::Error::custom)?;
302        Ok(Self {
303            source,
304            media_type,
305            filename,
306        })
307    }
308}
309
310/// Image-generation parameters shared across providers.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
312#[serde(default, deny_unknown_fields)]
313pub struct GenerationParameters {
314    /// Number of requested output images.
315    pub n: u8,
316    /// Automatic or explicit output size.
317    pub size: ImageSize,
318    /// Optional aspect-ratio hint.
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub aspect_ratio: Option<AspectRatio>,
321    /// Optional coarse resolution hint.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub resolution: Option<Resolution>,
324    /// Requested quality.
325    pub quality: Quality,
326    /// Requested encoded image format.
327    pub output_format: OutputFormat,
328    /// Compression from 0 to 100 for JPEG or WebP.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub output_compression: Option<u8>,
331    /// Requested background behavior.
332    pub background: Background,
333    /// Requested moderation behavior.
334    pub moderation: Moderation,
335    /// Requested number of partial progress images.
336    pub partial_images: u8,
337    /// Behavior when one output in a multi-image request fails.
338    pub failure_policy: MultiImageFailurePolicy,
339    /// Optional input-image fidelity for edit/reference operations.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub input_fidelity: Option<InputFidelity>,
342    /// Generate/edit selection for transports with an image tool action.
343    pub action: ImageAction,
344}
345
346impl Default for GenerationParameters {
347    fn default() -> Self {
348        Self {
349            n: 1,
350            size: ImageSize::default(),
351            aspect_ratio: None,
352            resolution: None,
353            quality: Quality::default(),
354            output_format: OutputFormat::default(),
355            output_compression: None,
356            background: Background::default(),
357            moderation: Moderation::default(),
358            partial_images: 0,
359            failure_policy: MultiImageFailurePolicy::default(),
360            input_fidelity: None,
361            action: ImageAction::default(),
362        }
363    }
364}
365
366#[cfg(test)]
367mod serde_tests {
368    #![allow(clippy::unwrap_used)]
369
370    use super::*;
371
372    #[test]
373    fn image_request_round_trips_with_flattened_operation() {
374        let request = ImageRequest::generate("test");
375        let encoded = serde_json::to_value(&request).unwrap();
376        assert_eq!(encoded["operation"], "generate");
377        let decoded: ImageRequest = serde_json::from_value(encoded).unwrap();
378        assert_eq!(decoded, request);
379    }
380
381    #[test]
382    fn image_request_rejects_unknown_and_operation_specific_fields() {
383        let unknown = serde_json::json!({
384            "prompt": "test",
385            "operation": "generate",
386            "surprise": true
387        });
388        assert!(serde_json::from_value::<ImageRequest>(unknown).is_err());
389        let inconsistent = serde_json::json!({
390            "prompt": "test",
391            "operation": "generate",
392            "images": []
393        });
394        assert!(serde_json::from_value::<ImageRequest>(inconsistent).is_err());
395    }
396
397    #[test]
398    fn every_image_input_source_round_trips_and_rejects_unknown_fields() {
399        let inputs = [
400            ImageInput {
401                source: ImageSource::File {
402                    path: PathBuf::from("fixture.png"),
403                },
404                media_type: Some("image/png".to_owned()),
405                filename: Some("fixture.png".to_owned()),
406            },
407            ImageInput {
408                source: ImageSource::Url {
409                    url: "https://example.test/fixture.png".to_owned(),
410                },
411                media_type: None,
412                filename: None,
413            },
414            ImageInput {
415                source: ImageSource::DataUrl {
416                    data_url: "data:image/png;base64,aW1hZ2U=".to_owned(),
417                },
418                media_type: None,
419                filename: None,
420            },
421            ImageInput {
422                source: ImageSource::Base64 {
423                    data: "aW1hZ2U=".to_owned(),
424                },
425                media_type: Some("image/png".to_owned()),
426                filename: None,
427            },
428        ];
429        for input in inputs {
430            let encoded = serde_json::to_value(&input).unwrap();
431            assert_eq!(
432                serde_json::from_value::<ImageInput>(encoded).unwrap(),
433                input
434            );
435        }
436
437        let unknown = serde_json::json!({
438            "type": "url",
439            "url": "https://example.test/fixture.png",
440            "unexpected": true
441        });
442        assert!(serde_json::from_value::<ImageInput>(unknown).is_err());
443    }
444}
445
446/// Provider selection controls.
447#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
448#[serde(default, deny_unknown_fields)]
449pub struct RoutingOptions {
450    /// Explicit provider name, or the configured default when absent.
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub provider: Option<String>,
453    /// Explicit provider model, or the provider default when absent.
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub model: Option<String>,
456    /// Ordered provider/model routes tried after the primary route.
457    #[serde(default, skip_serializing_if = "Vec::is_empty")]
458    pub fallbacks: Vec<ProviderRoute>,
459    /// Conditions under which the next fallback route may run.
460    pub fallback_policy: FallbackPolicy,
461}
462
463/// One explicit provider/model fallback route.
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
465#[serde(deny_unknown_fields)]
466pub struct ProviderRoute {
467    /// Registered provider name.
468    pub provider: String,
469    /// Optional model override for this provider.
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub model: Option<String>,
472}
473
474/// Conversation/session controls.
475#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
476#[serde(default, deny_unknown_fields)]
477pub struct SessionOptions {
478    /// Isolated, persistent-key, or explicit-thread mode.
479    pub mode: SessionMode,
480    /// Caller-selected durable binding key for persistent mode.
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub key: Option<String>,
483    /// Existing provider thread ID for explicit-thread mode.
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub thread_id: Option<String>,
486}
487
488/// Output delivery controls.
489#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
490#[serde(default, deny_unknown_fields)]
491pub struct OutputOptions {
492    /// Response payload representation.
493    pub response_format: ResponseFormat,
494    /// Optional logical artifact filename prefix.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub filename_prefix: Option<String>,
497    /// Optional portable relative directory below the configured artifact root.
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub directory: Option<String>,
500    /// Optional exact single-image filename, with or without a matching extension.
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub filename: Option<String>,
503    /// Atomic behavior if an explicit filename already exists.
504    pub collision: ArtifactCollisionPolicy,
505    /// Optional portable metadata persistence beside each artifact.
506    pub metadata: ArtifactMetadataPolicy,
507    /// Transparent-background implementation and matte controls.
508    pub transparency: TransparencyOptions,
509}
510
511/// Transparent-background implementation and matte controls.
512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
513#[serde(default, deny_unknown_fields)]
514pub struct TransparencyOptions {
515    /// Automatic, provider-native, or local chroma-key transparency.
516    pub mode: TransparencyMode,
517    /// Optional explicit chroma key in `#RRGGBB` form.
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub key_color: Option<String>,
520    /// Per-channel distance at or below which a key pixel becomes transparent.
521    pub transparent_threshold: u8,
522    /// Per-channel distance at or above which a pixel becomes opaque.
523    pub opaque_threshold: u8,
524    /// Remove key-colored spill from partially transparent edges.
525    pub despill: bool,
526}
527
528impl Default for TransparencyOptions {
529    fn default() -> Self {
530        Self {
531            mode: TransparencyMode::Auto,
532            key_color: None,
533            transparent_threshold: 12,
534            opaque_threshold: 96,
535            despill: true,
536        }
537    }
538}
539
540/// Explicit fallback and visibility controls.
541#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
542#[serde(default, deny_unknown_fields)]
543pub struct RequestPolicies {
544    /// Provider capability compatibility behavior.
545    pub compatibility: CompatibilityMode,
546    /// Negative-prompt handling behavior.
547    pub negative_prompt: NegativePromptMode,
548    /// Revised-prompt visibility and requirement behavior.
549    pub revised_prompt: RevisedPromptPolicy,
550    /// Automatic, sequential, or bounded-parallel fan-out execution.
551    pub batch_execution: BatchExecution,
552}