1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 Background, BridgeError, CompatibilityMode, ErrorCode, GenerationParameters, ImageOperation,
8 ImageRequest, ImageSize, Moderation, NegativePromptMode, Normalization, OutputFormat,
9 ProviderCapabilities, Quality, RevisedPromptPolicy, SessionMode, SupportLevel,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
14#[serde(deny_unknown_fields)]
15pub struct NegotiatedRequest {
16 pub requested: GenerationParameters,
18 pub effective_request: ImageRequest,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
22 pub normalizations: Vec<Normalization>,
23}
24
25pub fn negotiate_request(
32 request: &ImageRequest,
33 capabilities: &ProviderCapabilities,
34) -> Result<NegotiatedRequest, BridgeError> {
35 capabilities.validate()?;
36 let requested = request.parameters.clone();
37 let mut effective_request = request.clone();
38 let mut normalizations = Vec::new();
39 let mode = request.policies.compatibility;
40
41 check_model(request, capabilities)?;
42 check_operation(request, capabilities)?;
43 check_input_counts(request, capabilities)?;
44 check_sessions(request, capabilities)?;
45 check_user_attribution(request, capabilities)?;
46 check_input_fidelity(request, capabilities)?;
47 negotiate_action(&mut effective_request, capabilities, &mut normalizations)?;
48 negotiate_count(&mut effective_request, capabilities, &mut normalizations)?;
49 negotiate_size(&mut effective_request, capabilities, &mut normalizations)?;
50 negotiate_hints(&mut effective_request, capabilities, &mut normalizations)?;
51 negotiate_quality(&mut effective_request, capabilities, &mut normalizations)?;
52 negotiate_format(&mut effective_request, capabilities, &mut normalizations)?;
53 negotiate_background(&mut effective_request, capabilities, &mut normalizations)?;
54 negotiate_moderation(&mut effective_request, capabilities, &mut normalizations)?;
55 negotiate_partial_images(&mut effective_request, capabilities, &mut normalizations)?;
56 negotiate_negative_prompt(&mut effective_request, capabilities, &mut normalizations)?;
57
58 if request.policies.revised_prompt == RevisedPromptPolicy::Require
59 && capabilities.revised_prompt == SupportLevel::Unsupported
60 {
61 return Err(unsupported(
62 capabilities,
63 "policies.revised_prompt",
64 "provider cannot return a revised prompt",
65 ));
66 }
67
68 debug_assert_eq!(mode, effective_request.policies.compatibility);
69 Ok(NegotiatedRequest {
70 requested,
71 effective_request,
72 normalizations,
73 })
74}
75
76fn check_user_attribution(
77 request: &ImageRequest,
78 capabilities: &ProviderCapabilities,
79) -> Result<(), BridgeError> {
80 if request.user.is_none() || capabilities.user_attribution != SupportLevel::Unsupported {
81 return Ok(());
82 }
83 Err(unsupported(
84 capabilities,
85 "user",
86 "provider cannot consume the requested end-user attribution",
87 ))
88}
89
90fn check_input_fidelity(
91 request: &ImageRequest,
92 capabilities: &ProviderCapabilities,
93) -> Result<(), BridgeError> {
94 let Some(requested) = request.parameters.input_fidelity else {
95 return Ok(());
96 };
97 if capabilities.input_fidelities.contains(&requested) {
98 return Ok(());
99 }
100 Err(unsupported(
101 capabilities,
102 "parameters.input_fidelity",
103 "provider cannot honor the requested input fidelity",
104 )
105 .with_detail("requested", requested)
106 .with_detail("supported", &capabilities.input_fidelities))
107}
108
109fn negotiate_action(
110 request: &mut ImageRequest,
111 capabilities: &ProviderCapabilities,
112 normalizations: &mut Vec<Normalization>,
113) -> Result<(), BridgeError> {
114 let requested = request.parameters.action;
115 if capabilities.actions.contains(&requested) {
116 return Ok(());
117 }
118 if requested != crate::ImageAction::Auto
119 && capabilities.actions.contains(&crate::ImageAction::Auto)
120 {
121 record(
122 normalizations,
123 "parameters.action",
124 requested,
125 crate::ImageAction::Auto,
126 "provider_uses_automatic_action",
127 );
128 request.parameters.action = crate::ImageAction::Auto;
129 return Ok(());
130 }
131 Err(unsupported(
132 capabilities,
133 "parameters.action",
134 "provider cannot honor the requested image action",
135 )
136 .with_detail("requested", requested)
137 .with_detail("supported", &capabilities.actions))
138}
139
140fn check_model(
141 request: &ImageRequest,
142 capabilities: &ProviderCapabilities,
143) -> Result<(), BridgeError> {
144 let (Some(requested), Some(effective)) = (
145 request.routing.model.as_deref(),
146 capabilities.model.as_deref(),
147 ) else {
148 return Ok(());
149 };
150 if requested == effective {
151 return Ok(());
152 }
153 Err(unsupported(
154 capabilities,
155 "routing.model",
156 "provider cannot honor the requested image model",
157 )
158 .with_detail("requested_model", requested)
159 .with_detail("effective_model", effective))
160}
161
162fn check_operation(
163 request: &ImageRequest,
164 capabilities: &ProviderCapabilities,
165) -> Result<(), BridgeError> {
166 let (supported, field, message) = match request.operation {
167 ImageOperation::Generate { .. } => (
168 capabilities.generation,
169 "operation",
170 "provider does not support image generation",
171 ),
172 ImageOperation::Edit { .. } => (
173 capabilities.edits,
174 "operation",
175 "provider does not support image edits",
176 ),
177 };
178 if supported {
179 Ok(())
180 } else {
181 Err(unsupported(capabilities, field, message))
182 }
183}
184
185fn check_input_counts(
186 request: &ImageRequest,
187 capabilities: &ProviderCapabilities,
188) -> Result<(), BridgeError> {
189 let reference_count = request.operation.reference_images().len();
190 if reference_count > usize::from(capabilities.reference_images.max_count) {
191 return Err(unsupported(
192 capabilities,
193 "reference_images",
194 "reference image count exceeds provider capability",
195 ));
196 }
197 if reference_count > 0 && capabilities.reference_images.support == SupportLevel::Unsupported {
198 return Err(unsupported(
199 capabilities,
200 "reference_images",
201 "provider does not support reference images",
202 ));
203 }
204 if let ImageOperation::Edit { images, mask, .. } = &request.operation {
205 if images.len() > usize::from(capabilities.edit_images.max_count)
206 || capabilities.edit_images.support == SupportLevel::Unsupported
207 {
208 return Err(unsupported(
209 capabilities,
210 "images",
211 "edit images exceed provider capability",
212 ));
213 }
214 if mask.is_some()
215 && (capabilities.masks.support == SupportLevel::Unsupported
216 || capabilities.masks.max_count == 0)
217 {
218 return Err(unsupported(
219 capabilities,
220 "mask",
221 "provider does not support edit masks",
222 ));
223 }
224 }
225 Ok(())
226}
227
228fn check_sessions(
229 request: &ImageRequest,
230 capabilities: &ProviderCapabilities,
231) -> Result<(), BridgeError> {
232 match request.session.mode {
233 SessionMode::Isolated => Ok(()),
234 SessionMode::Persistent if capabilities.persistent_sessions => Ok(()),
235 SessionMode::Thread if capabilities.explicit_threads => Ok(()),
236 SessionMode::Persistent => Err(unsupported(
237 capabilities,
238 "session.mode",
239 "provider does not support persistent sessions",
240 )),
241 SessionMode::Thread => Err(unsupported(
242 capabilities,
243 "session.mode",
244 "provider does not support explicit threads",
245 )),
246 }
247}
248
249fn negotiate_count(
250 request: &mut ImageRequest,
251 capabilities: &ProviderCapabilities,
252 normalizations: &mut Vec<Normalization>,
253) -> Result<(), BridgeError> {
254 let requested = request.parameters.n;
255 if capabilities.count.contains(requested) {
256 return Ok(());
257 }
258 if request.policies.compatibility == CompatibilityMode::Strict {
259 return Err(unsupported(
260 capabilities,
261 "parameters.n",
262 "output count is outside provider capability",
263 ));
264 }
265 let effective = requested.clamp(capabilities.count.min, capabilities.count.max);
266 record(
267 normalizations,
268 "parameters.n",
269 requested,
270 effective,
271 "clamped_to_provider_range",
272 );
273 request.parameters.n = effective;
274 Ok(())
275}
276
277fn negotiate_size(
278 request: &mut ImageRequest,
279 capabilities: &ProviderCapabilities,
280 normalizations: &mut Vec<Normalization>,
281) -> Result<(), BridgeError> {
282 let requested = request.parameters.size.clone();
283 if size_supported(&requested, capabilities) {
284 return Ok(());
285 }
286 if request.policies.compatibility == CompatibilityMode::Strict {
287 return Err(unsupported(
288 capabilities,
289 "parameters.size",
290 "requested size is outside provider capability",
291 ));
292 }
293
294 let effective = if !requested.is_auto() && capabilities.sizes.auto {
295 ImageSize::default()
296 } else if let Some(size) = capabilities.sizes.allowed.iter().next() {
297 size.clone()
298 } else {
299 return Err(unsupported(
300 capabilities,
301 "parameters.size",
302 "provider has no usable size fallback",
303 ));
304 };
305 record(
306 normalizations,
307 "parameters.size",
308 requested.to_string(),
309 effective.to_string(),
310 "normalized_to_provider_size",
311 );
312 request.parameters.size = effective;
313 Ok(())
314}
315
316fn size_supported(size: &ImageSize, capabilities: &ProviderCapabilities) -> bool {
317 if size.is_auto() {
318 return capabilities.sizes.auto;
319 }
320 if capabilities.sizes.allowed.contains(size) {
321 return true;
322 }
323 let Some((width, height)) = size.dimensions() else {
324 return false;
325 };
326 if !capabilities.sizes.arbitrary {
327 return false;
328 }
329 let short = width.min(height);
330 let long = width.max(height);
331 let pixels = u64::from(width) * u64::from(height);
332 capabilities.sizes.min_edge.is_none_or(|min| short >= min)
333 && capabilities.sizes.max_edge.is_none_or(|max| long <= max)
334 && capabilities
335 .sizes
336 .edge_multiple
337 .is_none_or(|multiple| width % multiple == 0 && height % multiple == 0)
338 && capabilities
339 .sizes
340 .min_pixels
341 .is_none_or(|min| pixels >= min)
342 && capabilities
343 .sizes
344 .max_pixels
345 .is_none_or(|max| pixels <= max)
346 && capabilities
347 .sizes
348 .max_aspect_ratio
349 .is_none_or(|max| f64::from(long) / f64::from(short) <= max)
350}
351
352fn negotiate_hints(
353 request: &mut ImageRequest,
354 capabilities: &ProviderCapabilities,
355 normalizations: &mut Vec<Normalization>,
356) -> Result<(), BridgeError> {
357 if request.parameters.aspect_ratio.is_some()
358 && capabilities.aspect_ratio == SupportLevel::Unsupported
359 {
360 if request.policies.compatibility == CompatibilityMode::Strict {
361 return Err(unsupported(
362 capabilities,
363 "parameters.aspect_ratio",
364 "provider cannot honor an aspect-ratio hint",
365 ));
366 }
367 let requested = request
368 .parameters
369 .aspect_ratio
370 .take()
371 .map(|value| value.to_string());
372 record_optional(
373 normalizations,
374 "parameters.aspect_ratio",
375 requested,
376 None::<String>,
377 "provider_does_not_support_hint",
378 );
379 }
380 if request.parameters.resolution.is_some()
381 && capabilities.resolution == SupportLevel::Unsupported
382 {
383 if request.policies.compatibility == CompatibilityMode::Strict {
384 return Err(unsupported(
385 capabilities,
386 "parameters.resolution",
387 "provider cannot honor a resolution hint",
388 ));
389 }
390 let requested = request
391 .parameters
392 .resolution
393 .take()
394 .map(|value| value.to_string());
395 record_optional(
396 normalizations,
397 "parameters.resolution",
398 requested,
399 None::<String>,
400 "provider_does_not_support_hint",
401 );
402 }
403 Ok(())
404}
405
406fn negotiate_quality(
407 request: &mut ImageRequest,
408 capabilities: &ProviderCapabilities,
409 normalizations: &mut Vec<Normalization>,
410) -> Result<(), BridgeError> {
411 let requested = request.parameters.quality;
412 if capabilities.qualities.contains(&requested) {
413 return Ok(());
414 }
415 let effective = capabilities
416 .qualities
417 .get(&Quality::Auto)
418 .copied()
419 .or_else(|| capabilities.qualities.iter().next().copied())
420 .ok_or_else(|| {
421 unsupported(
422 capabilities,
423 "parameters.quality",
424 "provider declares no supported quality",
425 )
426 })?;
427 normalize_or_reject(
428 request.policies.compatibility,
429 capabilities,
430 normalizations,
431 "parameters.quality",
432 requested,
433 effective,
434 )?;
435 request.parameters.quality = effective;
436 Ok(())
437}
438
439fn negotiate_format(
440 request: &mut ImageRequest,
441 capabilities: &ProviderCapabilities,
442 normalizations: &mut Vec<Normalization>,
443) -> Result<(), BridgeError> {
444 let requested = request.parameters.output_format;
445 if !capabilities.output_formats.contains(&requested) {
446 let effective = capabilities
447 .output_formats
448 .get(&OutputFormat::Png)
449 .copied()
450 .or_else(|| capabilities.output_formats.iter().next().copied())
451 .ok_or_else(|| {
452 unsupported(
453 capabilities,
454 "parameters.output_format",
455 "provider declares no output format",
456 )
457 })?;
458 normalize_or_reject(
459 request.policies.compatibility,
460 capabilities,
461 normalizations,
462 "parameters.output_format",
463 requested,
464 effective,
465 )?;
466 request.parameters.output_format = effective;
467 }
468 if request.parameters.output_compression.is_some()
469 && !matches!(
470 request.parameters.output_format,
471 OutputFormat::Jpeg | OutputFormat::Webp
472 )
473 {
474 if request.policies.compatibility == CompatibilityMode::Strict {
475 return Err(unsupported(
476 capabilities,
477 "parameters.output_compression",
478 "effective output format does not support compression",
479 ));
480 }
481 let requested_compression = request.parameters.output_compression.take();
482 record_optional(
483 normalizations,
484 "parameters.output_compression",
485 requested_compression,
486 None::<u8>,
487 "effective_format_has_no_compression",
488 );
489 }
490 Ok(())
491}
492
493fn negotiate_background(
494 request: &mut ImageRequest,
495 capabilities: &ProviderCapabilities,
496 normalizations: &mut Vec<Normalization>,
497) -> Result<(), BridgeError> {
498 let requested = request.parameters.background;
499 if capabilities.backgrounds.contains(&requested) {
500 return Ok(());
501 }
502 let effective = capabilities
503 .backgrounds
504 .get(&Background::Auto)
505 .copied()
506 .or_else(|| capabilities.backgrounds.iter().next().copied())
507 .ok_or_else(|| {
508 unsupported(
509 capabilities,
510 "parameters.background",
511 "provider declares no background behavior",
512 )
513 })?;
514 normalize_or_reject(
515 request.policies.compatibility,
516 capabilities,
517 normalizations,
518 "parameters.background",
519 requested,
520 effective,
521 )?;
522 request.parameters.background = effective;
523 Ok(())
524}
525
526fn negotiate_moderation(
527 request: &mut ImageRequest,
528 capabilities: &ProviderCapabilities,
529 normalizations: &mut Vec<Normalization>,
530) -> Result<(), BridgeError> {
531 let requested = request.parameters.moderation;
532 if capabilities.moderation.contains(&requested) {
533 return Ok(());
534 }
535 let effective = capabilities
536 .moderation
537 .get(&Moderation::Auto)
538 .copied()
539 .or_else(|| capabilities.moderation.iter().next().copied())
540 .ok_or_else(|| {
541 unsupported(
542 capabilities,
543 "parameters.moderation",
544 "provider declares no moderation behavior",
545 )
546 })?;
547 normalize_or_reject(
548 request.policies.compatibility,
549 capabilities,
550 normalizations,
551 "parameters.moderation",
552 requested,
553 effective,
554 )?;
555 request.parameters.moderation = effective;
556 Ok(())
557}
558
559fn negotiate_partial_images(
560 request: &mut ImageRequest,
561 capabilities: &ProviderCapabilities,
562 normalizations: &mut Vec<Normalization>,
563) -> Result<(), BridgeError> {
564 let requested = request.parameters.partial_images;
565 if capabilities.partial_images.contains(requested) {
566 return Ok(());
567 }
568 if request.policies.compatibility == CompatibilityMode::Strict {
569 return Err(unsupported(
570 capabilities,
571 "parameters.partial_images",
572 "partial image count is outside provider capability",
573 ));
574 }
575 let effective = requested.clamp(
576 capabilities.partial_images.min,
577 capabilities.partial_images.max,
578 );
579 record(
580 normalizations,
581 "parameters.partial_images",
582 requested,
583 effective,
584 "clamped_to_provider_range",
585 );
586 request.parameters.partial_images = effective;
587 Ok(())
588}
589
590fn negotiate_negative_prompt(
591 request: &mut ImageRequest,
592 capabilities: &ProviderCapabilities,
593 normalizations: &mut Vec<Normalization>,
594) -> Result<(), BridgeError> {
595 let Some(negative_prompt) = request.negative_prompt.clone() else {
596 return Ok(());
597 };
598 let mode = request.policies.negative_prompt;
599 let merge = match mode {
600 NegativePromptMode::Reject => {
601 return Err(unsupported(
602 capabilities,
603 "negative_prompt",
604 "negative prompt is rejected by request policy",
605 ));
606 }
607 NegativePromptMode::Native => {
608 if capabilities.negative_prompt != SupportLevel::Native {
609 return Err(unsupported(
610 capabilities,
611 "negative_prompt",
612 "provider does not support a native negative prompt",
613 ));
614 }
615 false
616 }
617 NegativePromptMode::Merge => true,
618 NegativePromptMode::Auto => match capabilities.negative_prompt {
619 SupportLevel::Native => false,
620 SupportLevel::Emulated => true,
621 SupportLevel::Unsupported => {
622 return Err(unsupported(
623 capabilities,
624 "negative_prompt",
625 "provider cannot honor a negative prompt under auto policy",
626 ));
627 }
628 },
629 };
630 if merge {
631 request
632 .prompt
633 .push_str("\n\nNegative constraints (avoid these elements): ");
634 request.prompt.push_str(&negative_prompt);
635 request.negative_prompt = None;
636 normalizations.push(Normalization {
637 field: "negative_prompt".to_owned(),
638 requested: Some(serde_json::Value::Bool(true)),
639 effective: Some(serde_json::Value::String("merged_into_prompt".to_owned())),
640 reason: "negative_prompt_prompt_merge".to_owned(),
641 });
642 }
643 Ok(())
644}
645
646fn normalize_or_reject<T>(
647 mode: CompatibilityMode,
648 capabilities: &ProviderCapabilities,
649 normalizations: &mut Vec<Normalization>,
650 field: &str,
651 requested: T,
652 effective: T,
653) -> Result<(), BridgeError>
654where
655 T: Serialize + Copy,
656{
657 if mode == CompatibilityMode::Strict {
658 return Err(unsupported(
659 capabilities,
660 field,
661 "requested value is outside provider capability",
662 ));
663 }
664 record(
665 normalizations,
666 field,
667 requested,
668 effective,
669 "normalized_to_provider_value",
670 );
671 Ok(())
672}
673
674fn record<T: Serialize, U: Serialize>(
675 normalizations: &mut Vec<Normalization>,
676 field: &str,
677 requested: T,
678 effective: U,
679 reason: &str,
680) {
681 record_optional(
682 normalizations,
683 field,
684 Some(requested),
685 Some(effective),
686 reason,
687 );
688}
689
690fn record_optional<T: Serialize, U: Serialize>(
691 normalizations: &mut Vec<Normalization>,
692 field: &str,
693 requested: Option<T>,
694 effective: Option<U>,
695 reason: &str,
696) {
697 normalizations.push(Normalization {
698 field: field.to_owned(),
699 requested: requested.and_then(|value| serde_json::to_value(value).ok()),
700 effective: effective.and_then(|value| serde_json::to_value(value).ok()),
701 reason: reason.to_owned(),
702 });
703}
704
705fn unsupported(capabilities: &ProviderCapabilities, field: &str, message: &str) -> BridgeError {
706 BridgeError::new(ErrorCode::UnsupportedCapability, message)
707 .with_provider(&capabilities.provider)
708 .with_detail("field", field)
709 .with_detail("model", &capabilities.model)
710}
711
712#[cfg(test)]
713mod tests {
714 #![allow(clippy::unwrap_used)]
715
716 use std::collections::BTreeSet;
717
718 use super::*;
719 use crate::{
720 BatchCapabilities, BatchMode, InputCapabilities, RequestPolicies, SizeCapabilities, U8Range,
721 };
722
723 fn capabilities() -> ProviderCapabilities {
724 ProviderCapabilities {
725 provider: "test".to_owned(),
726 implementation_version: "1".to_owned(),
727 model: Some("test-image".to_owned()),
728 experimental: false,
729 generation: true,
730 edits: false,
731 count: U8Range { min: 1, max: 2 },
732 batching: BatchCapabilities {
733 mode: BatchMode::Native,
734 native_count: U8Range { min: 1, max: 2 },
735 max_parallel_outputs: 1,
736 },
737 sizes: SizeCapabilities {
738 auto: true,
739 allowed: BTreeSet::new(),
740 arbitrary: false,
741 min_edge: None,
742 max_edge: None,
743 edge_multiple: None,
744 min_pixels: None,
745 max_pixels: None,
746 max_aspect_ratio: None,
747 },
748 aspect_ratio: SupportLevel::Unsupported,
749 resolution: SupportLevel::Unsupported,
750 qualities: BTreeSet::from([Quality::Auto]),
751 output_formats: BTreeSet::from([OutputFormat::Png]),
752 backgrounds: BTreeSet::from([Background::Auto]),
753 transparent_background: SupportLevel::Unsupported,
754 moderation: BTreeSet::from([Moderation::Auto]),
755 negative_prompt: SupportLevel::Emulated,
756 revised_prompt: SupportLevel::Unsupported,
757 user_attribution: SupportLevel::Unsupported,
758 input_fidelities: BTreeSet::new(),
759 actions: BTreeSet::from([crate::ImageAction::Auto]),
760 reference_images: no_inputs(),
761 edit_images: no_inputs(),
762 masks: no_inputs(),
763 partial_images: U8Range { min: 0, max: 0 },
764 persistent_sessions: true,
765 explicit_threads: true,
766 }
767 }
768
769 #[test]
770 fn capability_validation_rejects_inconsistent_batching() {
771 let mut value = capabilities();
772 value.batching.mode = BatchMode::FanOut;
773 value.batching.native_count = value.count;
774 let error = value.validate().unwrap_err();
775 assert_eq!(error.code, ErrorCode::Protocol);
776
777 let mut value = capabilities();
778 value.batching.max_parallel_outputs = 0;
779 assert!(value.validate().is_err());
780 }
781
782 fn no_inputs() -> InputCapabilities {
783 InputCapabilities {
784 support: SupportLevel::Unsupported,
785 max_count: 0,
786 max_bytes_each: 0,
787 max_bytes_total: 0,
788 }
789 }
790
791 #[test]
792 fn strict_mode_rejects_unsupported_quality() {
793 let mut request = ImageRequest::generate("test");
794 request.parameters.quality = Quality::High;
795 let error = negotiate_request(&request, &capabilities()).unwrap_err();
796 assert_eq!(error.code, ErrorCode::UnsupportedCapability);
797 assert_eq!(
798 error.details.get("field"),
799 Some(&serde_json::Value::String("parameters.quality".to_owned()))
800 );
801 }
802
803 #[test]
804 fn explicit_model_mismatch_is_never_silently_normalized() {
805 let mut request = ImageRequest::generate("test");
806 request.routing.model = Some("another-image-model".to_owned());
807 request.policies.compatibility = CompatibilityMode::BestEffort;
808 let error = negotiate_request(&request, &capabilities()).unwrap_err();
809 assert_eq!(error.code, ErrorCode::UnsupportedCapability);
810 assert_eq!(
811 error.details.get("field"),
812 Some(&serde_json::Value::String("routing.model".to_owned()))
813 );
814 }
815
816 #[test]
817 fn unsupported_user_attribution_is_never_silently_dropped() {
818 let mut request = ImageRequest::generate("test");
819 request.user = Some("opaque-caller".to_owned());
820 request.policies.compatibility = CompatibilityMode::BestEffort;
821 let error = negotiate_request(&request, &capabilities()).unwrap_err();
822 assert_eq!(error.code, ErrorCode::UnsupportedCapability);
823 assert_eq!(
824 error.details.get("field"),
825 Some(&serde_json::Value::String("user".to_owned()))
826 );
827 }
828
829 #[test]
830 fn explicit_fidelity_is_checked_and_semantic_action_can_use_auto() {
831 let mut request = ImageRequest::generate("test");
832 request.parameters.input_fidelity = Some(crate::InputFidelity::High);
833 let error = negotiate_request(&request, &capabilities()).unwrap_err();
834 assert_eq!(error.details["field"], "parameters.input_fidelity");
835
836 request.parameters.input_fidelity = None;
837 request.parameters.action = crate::ImageAction::Generate;
838 let negotiated = negotiate_request(&request, &capabilities()).unwrap();
839 assert_eq!(
840 negotiated.effective_request.parameters.action,
841 crate::ImageAction::Auto
842 );
843 assert_eq!(negotiated.normalizations.len(), 1);
844 assert_eq!(negotiated.normalizations[0].field, "parameters.action");
845 assert_eq!(
846 negotiated.normalizations[0].reason,
847 "provider_uses_automatic_action"
848 );
849 }
850
851 #[test]
852 fn normalize_mode_reports_every_changed_parameter() {
853 let mut request = ImageRequest::generate("test");
854 request.policies = RequestPolicies {
855 compatibility: CompatibilityMode::Normalize,
856 ..RequestPolicies::default()
857 };
858 request.parameters.n = 3;
859 request.parameters.quality = Quality::High;
860 request.parameters.size = "1536x1024".parse().unwrap();
861
862 let negotiated = negotiate_request(&request, &capabilities()).unwrap();
863 assert_eq!(negotiated.effective_request.parameters.n, 2);
864 assert_eq!(
865 negotiated.effective_request.parameters.quality,
866 Quality::Auto
867 );
868 assert!(negotiated.effective_request.parameters.size.is_auto());
869 assert_eq!(negotiated.normalizations.len(), 3);
870 }
871
872 #[test]
873 fn emulated_negative_prompt_is_merged_without_echoing_its_text_in_metadata() {
874 let mut request = ImageRequest::generate("a portrait");
875 request.negative_prompt = Some("watermark".to_owned());
876 let negotiated = negotiate_request(&request, &capabilities()).unwrap();
877 assert!(negotiated.effective_request.prompt.contains("watermark"));
878 assert!(negotiated.effective_request.negative_prompt.is_none());
879 let metadata = serde_json::to_string(&negotiated.normalizations).unwrap();
880 assert!(!metadata.contains("watermark"));
881 }
882
883 #[test]
884 fn arbitrary_size_constraints_are_enforced() {
885 let mut capabilities = capabilities();
886 capabilities.sizes = SizeCapabilities {
887 auto: true,
888 allowed: BTreeSet::new(),
889 arbitrary: true,
890 min_edge: None,
891 max_edge: Some(3840),
892 edge_multiple: Some(16),
893 min_pixels: Some(655_360),
894 max_pixels: Some(8_294_400),
895 max_aspect_ratio: Some(3.0),
896 };
897 let mut request = ImageRequest::generate("test");
898 request.parameters.size = "1024x1024".parse().unwrap();
899 assert!(negotiate_request(&request, &capabilities).is_ok());
900
901 request.parameters.size = "4000x1024".parse().unwrap();
902 assert!(negotiate_request(&request, &capabilities).is_err());
903 }
904
905 #[test]
906 fn malformed_provider_ranges_are_rejected_without_panicking() {
907 for mode in [
908 CompatibilityMode::Strict,
909 CompatibilityMode::Normalize,
910 CompatibilityMode::BestEffort,
911 ] {
912 let mut request = ImageRequest::generate("test");
913 request.policies.compatibility = mode;
914
915 let mut invalid_count = capabilities();
916 invalid_count.count = U8Range { min: 2, max: 1 };
917 let error = negotiate_request(&request, &invalid_count).unwrap_err();
918 assert_eq!(error.code, ErrorCode::Protocol);
919
920 let mut invalid_partials = capabilities();
921 invalid_partials.partial_images = U8Range { min: 2, max: 1 };
922 let error = negotiate_request(&request, &invalid_partials).unwrap_err();
923 assert_eq!(error.code, ErrorCode::Protocol);
924 }
925 }
926}