1use std::{
4 collections::{BTreeMap, BTreeSet},
5 future::Future,
6 sync::{
7 Arc,
8 atomic::{AtomicUsize, Ordering},
9 },
10 time::{Duration, Instant},
11};
12
13use imagegen_bridge_core::{
14 BridgeError, ErrorCode, FallbackPolicy, ImageRequest, ImageResponse, Normalization,
15 ProviderAttempt, ProviderAttemptOutcome, ProviderContext, ProviderEvent, ProviderRoute,
16 RequestLimits, RevisedPromptPolicy, negotiate_request, validate_request,
17};
18use tokio::sync::{Mutex, Notify, OwnedSemaphorePermit, mpsc};
19use tokio_util::sync::CancellationToken;
20use uuid::Uuid;
21
22use crate::{
23 admission::AdmissionGate,
24 idempotency::{IdempotencyAction, IdempotencyConfig, IdempotencyCoordinator},
25 materialize::{MaterializationConfig, OutputMaterializer},
26 registry::ProviderRegistry,
27 transparency::prepare_transparency,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct ConcurrencyLimit {
33 pub max_concurrent: usize,
35 pub max_queued: usize,
37}
38
39impl Default for ConcurrencyLimit {
40 fn default() -> Self {
41 Self {
42 max_concurrent: 4,
43 max_queued: 16,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct RuntimeConfig {
51 pub request_limits: RequestLimits,
53 pub default_timeout: Duration,
55 pub cancellation_grace: Duration,
57 pub shutdown_grace: Duration,
59 pub global_limit: ConcurrencyLimit,
61 pub default_provider_limit: ConcurrencyLimit,
63 pub provider_limits: BTreeMap<String, ConcurrencyLimit>,
65 pub idempotency: IdempotencyConfig,
67 pub materialization: MaterializationConfig,
69}
70
71impl Default for RuntimeConfig {
72 fn default() -> Self {
73 Self {
74 request_limits: RequestLimits::default(),
75 default_timeout: Duration::from_secs(5 * 60),
76 cancellation_grace: Duration::from_secs(1),
77 shutdown_grace: Duration::from_secs(10),
78 global_limit: ConcurrencyLimit {
79 max_concurrent: 16,
80 max_queued: 64,
81 },
82 default_provider_limit: ConcurrencyLimit::default(),
83 provider_limits: BTreeMap::new(),
84 idempotency: IdempotencyConfig::default(),
85 materialization: MaterializationConfig::default(),
86 }
87 }
88}
89
90#[derive(Clone)]
92pub struct ExecutionContext {
93 pub request_id: Option<String>,
95 pub idempotency_scope: String,
97 pub cancellation: CancellationToken,
99}
100
101impl std::fmt::Debug for ExecutionContext {
102 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 formatter
104 .debug_struct("ExecutionContext")
105 .field("request_id", &self.request_id)
106 .field("idempotency_scope", &"[REDACTED SCOPE]")
107 .field("cancelled", &self.cancellation.is_cancelled())
108 .finish()
109 }
110}
111
112impl Default for ExecutionContext {
113 fn default() -> Self {
114 Self {
115 request_id: None,
116 idempotency_scope: "default".to_owned(),
117 cancellation: CancellationToken::new(),
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct RuntimeQueueSnapshot {
125 pub global_queued: usize,
127 pub providers_queued: BTreeMap<String, usize>,
129}
130
131pub struct ImagegenRuntime {
133 registry: ProviderRegistry,
134 config: RuntimeConfig,
135 global_gate: Arc<AdmissionGate>,
136 provider_gates: BTreeMap<String, Arc<AdmissionGate>>,
137 idempotency: IdempotencyCoordinator,
138 materializer: OutputMaterializer,
139 shutdown: CancellationToken,
140 activity: Arc<ActivityTracker>,
141 shutdown_result: Mutex<Option<Result<(), BridgeError>>>,
142}
143
144struct RouteExecution {
145 response: ImageResponse,
146 effective_request: ImageRequest,
147}
148
149impl std::fmt::Debug for ImagegenRuntime {
150 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 formatter
152 .debug_struct("ImagegenRuntime")
153 .field("registry", &self.registry)
154 .field("config", &self.config)
155 .field("shutdown", &self.shutdown.is_cancelled())
156 .finish_non_exhaustive()
157 }
158}
159
160impl ImagegenRuntime {
161 pub fn new(registry: ProviderRegistry, config: RuntimeConfig) -> Result<Self, BridgeError> {
163 if config.default_timeout.is_zero()
164 || config.default_timeout > Duration::from_millis(config.request_limits.max_timeout_ms)
165 {
166 return Err(configuration_error(
167 "default timeout must be within the configured request limit",
168 ));
169 }
170 if config.cancellation_grace.is_zero()
171 || config.cancellation_grace > Duration::from_secs(30)
172 {
173 return Err(configuration_error(
174 "cancellation grace must be greater than zero and at most 30 seconds",
175 ));
176 }
177 if config.shutdown_grace.is_zero() || config.shutdown_grace > Duration::from_secs(5 * 60) {
178 return Err(configuration_error(
179 "shutdown grace must be greater than zero and at most five minutes",
180 ));
181 }
182 for name in config.provider_limits.keys() {
183 registry.resolve(Some(name))?;
184 }
185 let global_gate = Arc::new(AdmissionGate::new(
186 config.global_limit.max_concurrent,
187 config.global_limit.max_queued,
188 "global",
189 )?);
190 let mut provider_gates = BTreeMap::new();
191 for (name, _) in registry.entries() {
192 let limit = config
193 .provider_limits
194 .get(name)
195 .copied()
196 .unwrap_or(config.default_provider_limit);
197 provider_gates.insert(
198 name.to_owned(),
199 Arc::new(AdmissionGate::new(
200 limit.max_concurrent,
201 limit.max_queued,
202 format!("provider:{name}"),
203 )?),
204 );
205 }
206 let idempotency = IdempotencyCoordinator::new(config.idempotency)?;
207 let materializer = OutputMaterializer::new(config.materialization.clone())?;
208 Ok(Self {
209 registry,
210 config,
211 global_gate,
212 provider_gates,
213 idempotency,
214 materializer,
215 shutdown: CancellationToken::new(),
216 activity: Arc::new(ActivityTracker::default()),
217 shutdown_result: Mutex::new(None),
218 })
219 }
220
221 #[must_use]
223 pub const fn registry(&self) -> &ProviderRegistry {
224 &self.registry
225 }
226
227 pub fn validate_request(&self, request: &ImageRequest) -> Result<(), BridgeError> {
229 validate_request(request, self.config.request_limits)
230 }
231
232 #[must_use]
234 pub const fn has_artifact_store(&self) -> bool {
235 self.materializer.has_artifact_store()
236 }
237
238 pub fn read_artifact(
240 &self,
241 artifact_id: &str,
242 ) -> Result<imagegen_bridge_artifacts::StoredArtifactContent, BridgeError> {
243 self.materializer.read_artifact(artifact_id)
244 }
245
246 pub fn read_artifact_thumbnail(
248 &self,
249 artifact_id: &str,
250 maximum_edge: u32,
251 ) -> Result<Vec<u8>, BridgeError> {
252 self.materializer.read_thumbnail(artifact_id, maximum_edge)
253 }
254
255 pub async fn execute(&self, request: ImageRequest) -> Result<ImageResponse, BridgeError> {
257 self.execute_with(request, ExecutionContext::default())
258 .await
259 }
260
261 pub async fn execute_with(
264 &self,
265 request: ImageRequest,
266 context: ExecutionContext,
267 ) -> Result<ImageResponse, BridgeError> {
268 self.execute_with_optional_events(request, context, None)
269 .await
270 }
271
272 pub async fn execute_with_events(
274 &self,
275 request: ImageRequest,
276 context: ExecutionContext,
277 events: mpsc::Sender<ProviderEvent>,
278 ) -> Result<ImageResponse, BridgeError> {
279 self.execute_with_optional_events(request, context, Some(events))
280 .await
281 }
282
283 async fn execute_with_optional_events(
284 &self,
285 request: ImageRequest,
286 context: ExecutionContext,
287 events: Option<mpsc::Sender<ProviderEvent>>,
288 ) -> Result<ImageResponse, BridgeError> {
289 if self.shutdown.is_cancelled() {
290 return Err(cancelled_error("runtime is shutting down"));
291 }
292 let _activity = self.activity.enter();
293 if self.shutdown.is_cancelled() {
294 return Err(cancelled_error("runtime is shutting down"));
295 }
296 validate_request(&request, self.config.request_limits)?;
297 let request_id = validated_request_id(context.request_id)?;
298 let timeout = request
299 .timeout_ms
300 .map_or(self.config.default_timeout, Duration::from_millis);
301 let deadline = Instant::now()
302 .checked_add(timeout)
303 .ok_or_else(|| configuration_error("request deadline overflowed"))?;
304 let operation = self.shutdown.child_token();
305
306 let mut token = None;
307 if let Some(key) = request.idempotency_key.as_deref() {
308 match self
309 .idempotency
310 .begin(&context.idempotency_scope, key, &request)
311 .await?
312 {
313 IdempotencyAction::Leader(leader) => token = Some(leader),
314 IdempotencyAction::Cached(response) => return Ok(*response),
315 IdempotencyAction::Wait(receiver) => {
316 return self
317 .await_controlled(
318 IdempotencyAction::wait(receiver, deadline, &operation),
319 deadline,
320 &context.cancellation,
321 &operation,
322 "request was cancelled while waiting for idempotent result",
323 )
324 .await;
325 }
326 }
327 }
328
329 let result = self
330 .execute_leader(
331 request,
332 request_id,
333 deadline,
334 &context.cancellation,
335 &operation,
336 events,
337 )
338 .await;
339 match (token, &result) {
340 (Some(token), Ok(response)) => {
341 self.idempotency.complete(token, response.clone()).await;
342 }
343 (Some(token), Err(error)) => {
344 self.idempotency.fail(token, error.clone()).await;
345 }
346 (None, _) => {}
347 }
348 result
349 }
350
351 #[must_use]
353 pub fn queue_snapshot(&self) -> RuntimeQueueSnapshot {
354 RuntimeQueueSnapshot {
355 global_queued: self.global_gate.queued(),
356 providers_queued: self
357 .provider_gates
358 .iter()
359 .map(|(name, gate)| (name.clone(), gate.queued()))
360 .collect(),
361 }
362 }
363
364 pub async fn shutdown(&self) -> Result<(), BridgeError> {
366 let mut stored_result = self.shutdown_result.lock().await;
367 if let Some(result) = stored_result.as_ref() {
368 return result.clone();
369 }
370 self.shutdown.cancel();
371 self.global_gate.close();
372 for gate in self.provider_gates.values() {
373 gate.close();
374 }
375 let mut first_error =
376 if tokio::time::timeout(self.config.shutdown_grace, self.activity.wait_until_idle())
377 .await
378 .is_err()
379 {
380 Some(
381 BridgeError::new(
382 ErrorCode::Timeout,
383 "runtime shutdown grace elapsed with active operations",
384 )
385 .retryable(true),
386 )
387 } else {
388 None
389 };
390 for (_, provider) in self.registry.entries() {
391 if let Err(error) = provider.shutdown().await
392 && first_error.is_none()
393 {
394 first_error = Some(error);
395 }
396 }
397 let result = first_error.map_or(Ok(()), Err);
398 *stored_result = Some(result.clone());
399 result
400 }
401
402 async fn execute_leader(
403 &self,
404 request: ImageRequest,
405 request_id: String,
406 deadline: Instant,
407 external_cancellation: &CancellationToken,
408 operation: &CancellationToken,
409 events: Option<mpsc::Sender<ProviderEvent>>,
410 ) -> Result<ImageResponse, BridgeError> {
411 let total_started = Instant::now();
412 let queue_started = Instant::now();
413 let global_permit = self
414 .acquire_controlled(
415 &self.global_gate,
416 deadline,
417 external_cancellation,
418 operation,
419 )
420 .await?;
421 let global_queue_ms = elapsed_ms(queue_started);
422 let primary_provider = request
423 .routing
424 .provider
425 .clone()
426 .unwrap_or_else(|| self.registry.default_name().to_owned());
427 let routes = std::iter::once(ProviderRoute {
428 provider: primary_provider.clone(),
429 model: request.routing.model.clone(),
430 })
431 .chain(request.routing.fallbacks.iter().cloned())
432 .collect::<Vec<_>>();
433 let mut unique_routes = BTreeSet::new();
434 for (index, route) in routes.iter().enumerate() {
435 if !unique_routes.insert((route.provider.as_str(), route.model.as_deref())) {
436 return Err(BridgeError::new(
437 ErrorCode::InvalidRequest,
438 "provider routing contains a duplicate resolved route",
439 )
440 .with_detail("field", format!("routing.fallbacks[{}]", index - 1))
441 .with_detail("provider", &route.provider)
442 .with_detail("model", route.model.as_deref()));
443 }
444 }
445 let mut attempts = Vec::with_capacity(routes.len());
446 let mut final_error = None;
447
448 for (index, route) in routes.iter().enumerate() {
449 let attempt_started = Instant::now();
450 match self
451 .execute_route(
452 &request,
453 &request_id,
454 route,
455 deadline,
456 external_cancellation,
457 operation,
458 events.clone(),
459 )
460 .await
461 {
462 Ok(execution) => {
463 let mut response = execution.response;
464 attempts.push(ProviderAttempt {
465 provider: response.provider.clone(),
466 model: Some(response.model.clone()),
467 outcome: ProviderAttemptOutcome::Succeeded,
468 error_code: None,
469 duration_ms: elapsed_ms(attempt_started),
470 });
471 if index > 0 {
472 response.normalizations.insert(
473 0,
474 Normalization {
475 field: "routing.provider".to_owned(),
476 requested: Some(serde_json::json!(primary_provider)),
477 effective: Some(serde_json::json!(response.provider)),
478 reason: "provider_fallback".to_owned(),
479 },
480 );
481 response.warnings.push("provider_fallback_used".to_owned());
482 }
483 if !request.routing.fallbacks.is_empty() {
484 response.attempts = attempts;
485 }
486 response.timings.queue_ms =
487 response.timings.queue_ms.saturating_add(global_queue_ms);
488 response.timings.total_ms = elapsed_ms(total_started);
489 self.materializer.attach_metadata(
490 &request,
491 &execution.effective_request,
492 &mut response,
493 )?;
494 drop(global_permit);
495 return Ok(response);
496 }
497 Err(error) => {
498 attempts.push(ProviderAttempt {
499 provider: route.provider.clone(),
500 model: route.model.clone(),
501 outcome: ProviderAttemptOutcome::Failed,
502 error_code: Some(error.code),
503 duration_ms: elapsed_ms(attempt_started),
504 });
505 let has_next = index + 1 < routes.len();
506 if !has_next || !should_fallback(&error, request.routing.fallback_policy) {
507 final_error = Some(error);
508 break;
509 }
510 final_error = Some(error);
511 }
512 }
513 }
514 drop(global_permit);
515 Err(final_error
516 .unwrap_or_else(|| configuration_error("provider routing produced no attempts"))
517 .with_detail("attempts", attempts))
518 }
519
520 #[allow(clippy::too_many_arguments)]
521 async fn execute_route(
522 &self,
523 request: &ImageRequest,
524 request_id: &str,
525 route: &ProviderRoute,
526 deadline: Instant,
527 external_cancellation: &CancellationToken,
528 operation: &CancellationToken,
529 events: Option<mpsc::Sender<ProviderEvent>>,
530 ) -> Result<RouteExecution, BridgeError> {
531 let provider = self.registry.resolve(Some(&route.provider))?;
532 let descriptor = provider.descriptor();
533 let provider_gate = self
534 .provider_gates
535 .get(&descriptor.name)
536 .ok_or_else(|| configuration_error("provider has no configured admission gate"))?;
537 let queue_started = Instant::now();
538 let _provider_permit = self
539 .acquire_controlled(provider_gate, deadline, external_cancellation, operation)
540 .await?;
541 let queue_ms = elapsed_ms(queue_started);
542 let capabilities = self
543 .await_controlled(
544 provider.capabilities(route.model.as_deref()),
545 deadline,
546 external_cancellation,
547 operation,
548 "request was cancelled during capability discovery",
549 )
550 .await
551 .map_err(|error| attach_provider(error, &descriptor.name))?;
552 if capabilities.provider != descriptor.name {
553 return Err(protocol_error(
554 "provider capability identity does not match its registry identity",
555 )
556 .with_provider(descriptor.name));
557 }
558 let mut routed_request = request.clone();
559 routed_request.routing.provider = Some(route.provider.clone());
560 routed_request.routing.model.clone_from(&route.model);
561 routed_request.routing.fallbacks.clear();
562 let prepared = prepare_transparency(&routed_request, &capabilities)?;
563 validate_request(&prepared.provider_request, self.config.request_limits)?;
564 let negotiated = negotiate_request(&prepared.provider_request, &capabilities)?;
565 let effective_request = negotiated.effective_request;
566
567 let provider_started = Instant::now();
568 let provider_result = self
569 .await_provider_execution(
570 provider.execute(
571 effective_request.clone(),
572 ProviderContext {
573 request_id: request_id.to_owned(),
574 deadline,
575 cancellation: operation.clone(),
576 events,
577 },
578 ),
579 deadline,
580 external_cancellation,
581 operation,
582 )
583 .await;
584 let provider_ms = elapsed_ms(provider_started);
585 let mut response =
586 provider_result.map_err(|error| attach_provider(error, &descriptor.name))?;
587 if let Some(expected_model) = capabilities.model.as_deref()
588 && response.model != expected_model
589 {
590 return Err(protocol_error(
591 "provider response model does not match discovered capabilities",
592 )
593 .with_provider(descriptor.name)
594 .with_detail("expected_model", expected_model)
595 .with_detail("actual_model", response.model));
596 }
597
598 response.id = request_id.to_owned();
599 response.provider = descriptor.name;
600 response.requested = request.parameters.clone();
601 response.effective = effective_request.parameters.clone();
602 if prepared.plan.is_some() {
603 response.effective.background = imagegen_bridge_core::Background::Transparent;
604 response
605 .warnings
606 .push("transparent_background_postprocessed".to_owned());
607 }
608 response.normalizations.splice(
609 0..0,
610 prepared
611 .normalizations
612 .into_iter()
613 .chain(negotiated.normalizations),
614 );
615 if effective_request.policies.revised_prompt == RevisedPromptPolicy::Omit {
616 response.revised_prompt = None;
617 }
618 response.timings.queue_ms = queue_ms;
619 response.timings.provider_ms = provider_ms;
620 let artifact_started = Instant::now();
621 response = self
622 .await_controlled(
623 self.materializer
624 .materialize(response, request, &effective_request, prepared.plan),
625 deadline,
626 external_cancellation,
627 operation,
628 "request was cancelled during output materialization",
629 )
630 .await?;
631 response.timings.artifact_ms = elapsed_ms(artifact_started);
632 Ok(RouteExecution {
633 response,
634 effective_request,
635 })
636 }
637
638 async fn acquire_controlled(
639 &self,
640 gate: &AdmissionGate,
641 deadline: Instant,
642 external: &CancellationToken,
643 operation: &CancellationToken,
644 ) -> Result<OwnedSemaphorePermit, BridgeError> {
645 self.await_controlled(
646 gate.acquire(deadline, operation),
647 deadline,
648 external,
649 operation,
650 "request was cancelled while waiting for capacity",
651 )
652 .await
653 }
654
655 async fn await_provider_execution<T, F>(
656 &self,
657 future: F,
658 deadline: Instant,
659 external: &CancellationToken,
660 operation: &CancellationToken,
661 ) -> Result<T, BridgeError>
662 where
663 F: Future<Output = Result<T, BridgeError>>,
664 {
665 if deadline <= Instant::now() {
666 operation.cancel();
667 return Err(timeout_error());
668 }
669 tokio::pin!(future);
670 let interrupted = tokio::select! {
671 result = &mut future => return classify_provider_execution_result(result),
672 () = external.cancelled() => {
673 operation.cancel();
674 (ErrorCode::Cancelled, "request was cancelled during provider execution")
675 }
676 () = self.shutdown.cancelled() => {
677 operation.cancel();
678 (ErrorCode::Cancelled, "runtime shut down during provider execution")
679 }
680 () = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {
681 operation.cancel();
682 (ErrorCode::Timeout, "request deadline elapsed during provider execution")
683 }
684 };
685 match tokio::time::timeout(self.config.cancellation_grace, &mut future).await {
686 Ok(Ok(value)) => Ok(value),
687 Ok(Err(error)) if !matches!(error.code, ErrorCode::Timeout | ErrorCode::Cancelled) => {
688 Err(error)
689 }
690 Ok(Err(_)) | Err(_) => Err(unknown_outcome_error(interrupted.0, interrupted.1)),
691 }
692 }
693
694 async fn await_controlled<T, F>(
695 &self,
696 future: F,
697 deadline: Instant,
698 external: &CancellationToken,
699 operation: &CancellationToken,
700 cancellation_message: &'static str,
701 ) -> Result<T, BridgeError>
702 where
703 F: Future<Output = Result<T, BridgeError>>,
704 {
705 if deadline <= Instant::now() {
706 operation.cancel();
707 return Err(timeout_error());
708 }
709 tokio::pin!(future);
710 tokio::select! {
711 result = &mut future => result,
712 () = external.cancelled() => {
713 operation.cancel();
714 let _ = tokio::time::timeout(self.config.cancellation_grace, &mut future).await;
715 Err(cancelled_error(cancellation_message))
716 }
717 () = self.shutdown.cancelled() => {
718 operation.cancel();
719 let _ = tokio::time::timeout(self.config.cancellation_grace, &mut future).await;
720 Err(cancelled_error("runtime is shutting down"))
721 }
722 () = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {
723 operation.cancel();
724 let _ = tokio::time::timeout(self.config.cancellation_grace, &mut future).await;
725 Err(timeout_error())
726 }
727 }
728 }
729}
730
731#[derive(Default)]
732struct ActivityTracker {
733 active: AtomicUsize,
734 idle: Notify,
735}
736
737impl ActivityTracker {
738 fn enter(self: &Arc<Self>) -> ActivityGuard {
739 self.active.fetch_add(1, Ordering::AcqRel);
740 ActivityGuard {
741 tracker: Arc::clone(self),
742 }
743 }
744
745 async fn wait_until_idle(&self) {
746 loop {
747 let notified = self.idle.notified();
748 if self.active.load(Ordering::Acquire) == 0 {
749 return;
750 }
751 notified.await;
752 }
753 }
754}
755
756struct ActivityGuard {
757 tracker: Arc<ActivityTracker>,
758}
759
760impl Drop for ActivityGuard {
761 fn drop(&mut self) {
762 if self.tracker.active.fetch_sub(1, Ordering::AcqRel) == 1 {
763 self.tracker.idle.notify_waiters();
764 }
765 }
766}
767
768fn validated_request_id(value: Option<String>) -> Result<String, BridgeError> {
769 let Some(value) = value else {
770 return Ok(Uuid::now_v7().to_string());
771 };
772 let valid = !value.is_empty()
773 && value.len() <= 128
774 && value
775 .bytes()
776 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'));
777 if valid {
778 Ok(value)
779 } else {
780 Err(BridgeError::new(
781 ErrorCode::InvalidRequest,
782 "request ID is invalid",
783 ))
784 }
785}
786
787fn attach_provider(mut error: BridgeError, provider: &str) -> BridgeError {
788 if error.provider.is_none() {
789 error.provider = Some(provider.to_owned());
790 }
791 error
792}
793
794fn should_fallback(error: &BridgeError, policy: FallbackPolicy) -> bool {
795 if error
796 .details
797 .get("outcome")
798 .and_then(serde_json::Value::as_str)
799 == Some("unknown")
800 {
801 return false;
802 }
803 if matches!(
804 error.code,
805 ErrorCode::InvalidRequest
806 | ErrorCode::PermissionDenied
807 | ErrorCode::SafetyRejected
808 | ErrorCode::Cancelled
809 | ErrorCode::Session
810 | ErrorCode::IdempotencyConflict
811 ) {
812 return false;
813 }
814 if matches!(
815 error.code,
816 ErrorCode::Configuration
817 | ErrorCode::Authentication
818 | ErrorCode::UnsupportedCapability
819 | ErrorCode::RateLimited
820 | ErrorCode::Overloaded
821 ) {
822 return true;
823 }
824 policy == FallbackPolicy::OnError
825 && matches!(
826 error.code,
827 ErrorCode::Upstream
828 | ErrorCode::Protocol
829 | ErrorCode::Artifact
830 | ErrorCode::Input
831 | ErrorCode::Internal
832 )
833}
834
835fn elapsed_ms(started: Instant) -> u64 {
836 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
837}
838
839fn timeout_error() -> BridgeError {
840 BridgeError::new(ErrorCode::Timeout, "request deadline elapsed").retryable(true)
841}
842
843fn unknown_outcome_error(code: ErrorCode, message: impl Into<String>) -> BridgeError {
844 BridgeError::new(code, message)
845 .retryable(false)
846 .with_detail("outcome", "unknown")
847}
848
849fn classify_provider_execution_result<T>(result: Result<T, BridgeError>) -> Result<T, BridgeError> {
850 match result {
851 Err(error)
852 if matches!(error.code, ErrorCode::Timeout | ErrorCode::Cancelled)
853 && error
854 .details
855 .get("outcome")
856 .and_then(serde_json::Value::as_str)
857 != Some("unknown") =>
858 {
859 Err(error.retryable(false).with_detail("outcome", "unknown"))
860 }
861 result => result,
862 }
863}
864
865fn cancelled_error(message: impl Into<String>) -> BridgeError {
866 BridgeError::new(ErrorCode::Cancelled, message)
867}
868
869fn configuration_error(message: impl Into<String>) -> BridgeError {
870 BridgeError::new(ErrorCode::Configuration, message)
871}
872
873fn protocol_error(message: impl Into<String>) -> BridgeError {
874 BridgeError::new(ErrorCode::Protocol, message)
875}
876
877#[cfg(test)]
878mod tests {
879 #![allow(clippy::panic, clippy::unwrap_used)]
880
881 use std::{
882 collections::BTreeSet,
883 sync::atomic::{AtomicBool, AtomicUsize, Ordering},
884 };
885
886 use async_trait::async_trait;
887 use base64::{Engine as _, engine::general_purpose::STANDARD};
888 use imagegen_bridge_artifacts::{ArtifactStore, ImageLimits, inspect_image};
889 use imagegen_bridge_core::{
890 Background, BatchCapabilities, BatchMode, CompatibilityMode, GeneratedImage,
891 GenerationParameters, ImageAction, ImagePayload, ImageProvider, ImageSize,
892 InputCapabilities, InputFidelity, Moderation, OutputFormat, ProviderCapabilities,
893 ProviderDescriptor, Quality, ResponseFormat, SizeCapabilities, SupportLevel, Timings,
894 U8Range, Usage,
895 };
896
897 use super::*;
898
899 const ONE_PIXEL_PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
900
901 struct FakeProvider {
902 calls: AtomicUsize,
903 delay: Duration,
904 saw_cancellation: AtomicBool,
905 corrupt_metadata: bool,
906 }
907
908 impl FakeProvider {
909 fn new(delay: Duration) -> Self {
910 Self {
911 calls: AtomicUsize::new(0),
912 delay,
913 saw_cancellation: AtomicBool::new(false),
914 corrupt_metadata: false,
915 }
916 }
917
918 fn corrupting() -> Self {
919 Self {
920 corrupt_metadata: true,
921 ..Self::new(Duration::ZERO)
922 }
923 }
924 }
925
926 #[async_trait]
927 impl ImageProvider for FakeProvider {
928 fn descriptor(&self) -> ProviderDescriptor {
929 ProviderDescriptor {
930 name: "fake".to_owned(),
931 display_name: "Fake".to_owned(),
932 version: "test".to_owned(),
933 experimental: false,
934 models: vec!["test-image".to_owned()],
935 }
936 }
937
938 async fn capabilities(
939 &self,
940 model: Option<&str>,
941 ) -> Result<ProviderCapabilities, BridgeError> {
942 Ok(fake_capabilities(model))
943 }
944
945 async fn execute(
946 &self,
947 request: ImageRequest,
948 context: ProviderContext,
949 ) -> Result<ImageResponse, BridgeError> {
950 self.calls.fetch_add(1, Ordering::AcqRel);
951 tokio::select! {
952 () = tokio::time::sleep(self.delay) => {}
953 () = context.cancellation.cancelled() => {
954 self.saw_cancellation.store(true, Ordering::Release);
955 return Err(BridgeError::new(ErrorCode::Cancelled, "fake cancelled"));
956 }
957 }
958 let bytes = STANDARD.decode(ONE_PIXEL_PNG).unwrap();
959 let metadata = inspect_image(&bytes, ImageLimits::default()).unwrap();
960 let image = GeneratedImage {
961 index: 0,
962 payload: ImagePayload::B64Json {
963 b64_json: ONE_PIXEL_PNG.to_owned(),
964 },
965 format: metadata.format,
966 width: metadata.width,
967 height: metadata.height,
968 bytes: metadata.bytes,
969 sha256: if self.corrupt_metadata {
970 "0".repeat(64)
971 } else {
972 metadata.sha256
973 },
974 generation_ms: None,
975 metadata_name: None,
976 };
977 Ok(ImageResponse {
978 id: context.request_id,
979 created: 0,
980 provider: "fake".to_owned(),
981 model: request
982 .routing
983 .model
984 .clone()
985 .unwrap_or_else(|| "fake-image".to_owned()),
986 requested: request.parameters.clone(),
987 effective: request.parameters.clone(),
988 normalizations: Vec::new(),
989 attempts: Vec::new(),
990 data: (0..request.parameters.n)
991 .map(|index| GeneratedImage {
992 index,
993 ..image.clone()
994 })
995 .collect(),
996 failures: Vec::new(),
997 revised_prompt: Some("safe revised prompt".to_owned()),
998 usage: Some(Usage::default()),
999 session: None,
1000 timings: Timings::default(),
1001 warnings: Vec::new(),
1002 })
1003 }
1004
1005 async fn check_ready(&self) -> Result<(), BridgeError> {
1006 Ok(())
1007 }
1008 }
1009
1010 #[derive(Clone, Copy)]
1011 enum RouteFailure {
1012 Authentication,
1013 KnownUpstream,
1014 Safety,
1015 UnknownOutcome,
1016 }
1017
1018 struct RouteProvider {
1019 name: &'static str,
1020 failure: Option<RouteFailure>,
1021 execute_calls: AtomicUsize,
1022 }
1023
1024 impl RouteProvider {
1025 fn new(name: &'static str, failure: Option<RouteFailure>) -> Self {
1026 Self {
1027 name,
1028 failure,
1029 execute_calls: AtomicUsize::new(0),
1030 }
1031 }
1032 }
1033
1034 #[async_trait]
1035 impl ImageProvider for RouteProvider {
1036 fn descriptor(&self) -> ProviderDescriptor {
1037 ProviderDescriptor {
1038 name: self.name.to_owned(),
1039 display_name: self.name.to_owned(),
1040 version: "test".to_owned(),
1041 experimental: false,
1042 models: vec!["route-image".to_owned()],
1043 }
1044 }
1045
1046 async fn capabilities(
1047 &self,
1048 model: Option<&str>,
1049 ) -> Result<ProviderCapabilities, BridgeError> {
1050 if matches!(self.failure, Some(RouteFailure::Authentication)) {
1051 return Err(BridgeError::new(
1052 ErrorCode::Authentication,
1053 "test provider is unavailable",
1054 ));
1055 }
1056 let mut capabilities = fake_capabilities(model);
1057 capabilities.provider = self.name.to_owned();
1058 Ok(capabilities)
1059 }
1060
1061 async fn execute(
1062 &self,
1063 request: ImageRequest,
1064 context: ProviderContext,
1065 ) -> Result<ImageResponse, BridgeError> {
1066 self.execute_calls.fetch_add(1, Ordering::AcqRel);
1067 match self.failure {
1068 Some(RouteFailure::Safety) => {
1069 return Err(BridgeError::safety_rejected("test safety rejection"));
1070 }
1071 Some(RouteFailure::UnknownOutcome) => {
1072 return Err(
1073 BridgeError::new(ErrorCode::Upstream, "test ambiguous failure")
1074 .with_detail("outcome", "unknown"),
1075 );
1076 }
1077 Some(RouteFailure::KnownUpstream) => {
1078 return Err(BridgeError::new(ErrorCode::Upstream, "test known failure")
1079 .with_detail("outcome", "failed"));
1080 }
1081 Some(RouteFailure::Authentication) | None => {}
1082 }
1083 let bytes = STANDARD.decode(ONE_PIXEL_PNG).unwrap();
1084 let metadata = inspect_image(&bytes, ImageLimits::default()).unwrap();
1085 Ok(ImageResponse {
1086 id: context.request_id,
1087 created: 0,
1088 provider: self.name.to_owned(),
1089 model: request
1090 .routing
1091 .model
1092 .clone()
1093 .unwrap_or_else(|| "route-image".to_owned()),
1094 requested: request.parameters.clone(),
1095 effective: request.parameters.clone(),
1096 normalizations: Vec::new(),
1097 attempts: Vec::new(),
1098 data: vec![GeneratedImage {
1099 index: 0,
1100 payload: ImagePayload::B64Json {
1101 b64_json: ONE_PIXEL_PNG.to_owned(),
1102 },
1103 format: metadata.format,
1104 width: metadata.width,
1105 height: metadata.height,
1106 bytes: metadata.bytes,
1107 sha256: metadata.sha256,
1108 generation_ms: None,
1109 metadata_name: None,
1110 }],
1111 failures: Vec::new(),
1112 revised_prompt: None,
1113 usage: None,
1114 session: None,
1115 timings: Timings::default(),
1116 warnings: Vec::new(),
1117 })
1118 }
1119
1120 async fn check_ready(&self) -> Result<(), BridgeError> {
1121 Ok(())
1122 }
1123 }
1124
1125 fn fake_capabilities(model: Option<&str>) -> ProviderCapabilities {
1126 let unsupported_inputs = InputCapabilities {
1127 support: SupportLevel::Unsupported,
1128 max_count: 0,
1129 max_bytes_each: 0,
1130 max_bytes_total: 0,
1131 };
1132 ProviderCapabilities {
1133 provider: "fake".to_owned(),
1134 implementation_version: "test".to_owned(),
1135 model: model.map(str::to_owned),
1136 experimental: false,
1137 generation: true,
1138 edits: false,
1139 count: U8Range { min: 1, max: 1 },
1140 batching: BatchCapabilities {
1141 mode: BatchMode::Native,
1142 native_count: U8Range { min: 1, max: 1 },
1143 max_parallel_outputs: 1,
1144 },
1145 sizes: SizeCapabilities {
1146 auto: true,
1147 allowed: BTreeSet::from([ImageSize::exact(2, 2).unwrap()]),
1148 arbitrary: false,
1149 min_edge: None,
1150 max_edge: None,
1151 edge_multiple: None,
1152 min_pixels: None,
1153 max_pixels: None,
1154 max_aspect_ratio: None,
1155 },
1156 aspect_ratio: SupportLevel::Unsupported,
1157 resolution: SupportLevel::Unsupported,
1158 qualities: BTreeSet::from([Quality::Auto]),
1159 output_formats: BTreeSet::from([OutputFormat::Png]),
1160 backgrounds: BTreeSet::from([Background::Auto]),
1161 transparent_background: SupportLevel::Emulated,
1162 moderation: BTreeSet::from([Moderation::Auto]),
1163 negative_prompt: SupportLevel::Emulated,
1164 revised_prompt: SupportLevel::Native,
1165 user_attribution: SupportLevel::Native,
1166 input_fidelities: BTreeSet::from([InputFidelity::Low, InputFidelity::High]),
1167 actions: BTreeSet::from([ImageAction::Auto, ImageAction::Generate, ImageAction::Edit]),
1168 reference_images: unsupported_inputs.clone(),
1169 edit_images: unsupported_inputs.clone(),
1170 masks: unsupported_inputs,
1171 partial_images: U8Range { min: 0, max: 0 },
1172 persistent_sessions: false,
1173 explicit_threads: false,
1174 }
1175 }
1176
1177 fn runtime(
1178 provider: &Arc<FakeProvider>,
1179 mutate: impl FnOnce(&mut RuntimeConfig),
1180 ) -> ImagegenRuntime {
1181 let registry =
1182 ProviderRegistry::new([Arc::clone(provider) as Arc<dyn ImageProvider>], "fake")
1183 .unwrap();
1184 let mut config = RuntimeConfig::default();
1185 mutate(&mut config);
1186 ImagegenRuntime::new(registry, config).unwrap()
1187 }
1188
1189 fn routing_runtime(
1190 primary: &Arc<RouteProvider>,
1191 fallback: &Arc<RouteProvider>,
1192 ) -> ImagegenRuntime {
1193 let registry = ProviderRegistry::new(
1194 [
1195 Arc::clone(primary) as Arc<dyn ImageProvider>,
1196 Arc::clone(fallback) as Arc<dyn ImageProvider>,
1197 ],
1198 primary.name,
1199 )
1200 .unwrap();
1201 ImagegenRuntime::new(registry, RuntimeConfig::default()).unwrap()
1202 }
1203
1204 #[tokio::test]
1205 async fn negotiates_then_independently_verifies_and_projects_metadata() {
1206 let provider = Arc::new(FakeProvider::new(Duration::ZERO));
1207 let runtime = runtime(&provider, |_| {});
1208 let mut request = ImageRequest::generate("test");
1209 request.parameters = GenerationParameters {
1210 n: 2,
1211 ..GenerationParameters::default()
1212 };
1213 request.policies.compatibility = CompatibilityMode::Normalize;
1214 request.policies.revised_prompt = RevisedPromptPolicy::Omit;
1215 request.output.response_format = ResponseFormat::Metadata;
1216 let response = runtime
1217 .execute_with(
1218 request,
1219 ExecutionContext {
1220 request_id: Some("request-1".to_owned()),
1221 ..ExecutionContext::default()
1222 },
1223 )
1224 .await
1225 .unwrap();
1226 assert_eq!(response.id, "request-1");
1227 assert_eq!(response.requested.n, 2);
1228 assert_eq!(response.effective.n, 1);
1229 assert_eq!(response.data.len(), 1);
1230 assert!(matches!(response.data[0].payload, ImagePayload::Metadata));
1231 assert!(response.revised_prompt.is_none());
1232 assert_eq!(provider.calls.load(Ordering::Acquire), 1);
1233 }
1234
1235 #[tokio::test]
1236 async fn strict_mode_rejects_verified_dimension_mismatch() {
1237 let provider = Arc::new(FakeProvider::new(Duration::ZERO));
1238 let runtime = runtime(&provider, |_| {});
1239 let mut request = ImageRequest::generate("test");
1240 request.parameters.size = ImageSize::exact(2, 2).unwrap();
1241 let error = runtime.execute(request).await.unwrap_err();
1242 assert_eq!(error.code, ErrorCode::Protocol);
1243 assert_eq!(error.details["expected"], "2x2");
1244 assert_eq!(error.details["actual"], "1x1");
1245 }
1246
1247 #[tokio::test]
1248 async fn normalize_mode_reports_verified_dimension_mismatch() {
1249 let provider = Arc::new(FakeProvider::new(Duration::ZERO));
1250 let runtime = runtime(&provider, |_| {});
1251 let mut request = ImageRequest::generate("test");
1252 request.parameters.size = ImageSize::exact(2, 2).unwrap();
1253 request.policies.compatibility = CompatibilityMode::Normalize;
1254 let response = runtime.execute(request).await.unwrap();
1255 assert_eq!(response.effective.size, ImageSize::exact(1, 1).unwrap());
1256 assert!(response.normalizations.iter().any(|entry| {
1257 entry.field == "parameters.size"
1258 && entry.reason == "provider_output_dimensions_differed"
1259 }));
1260 assert!(
1261 response
1262 .warnings
1263 .iter()
1264 .any(|warning| warning == "provider_output_dimensions_differed")
1265 );
1266 }
1267
1268 #[tokio::test]
1269 async fn replays_idempotent_responses_without_a_second_provider_call() {
1270 let provider = Arc::new(FakeProvider::new(Duration::ZERO));
1271 let runtime = runtime(&provider, |_| {});
1272 let mut request = ImageRequest::generate("test");
1273 request.idempotency_key = Some("stable-key".to_owned());
1274 let first = runtime.execute(request.clone()).await.unwrap();
1275 let second = runtime.execute(request).await.unwrap();
1276 assert_eq!(first, second);
1277 assert_eq!(provider.calls.load(Ordering::Acquire), 1);
1278 }
1279
1280 #[tokio::test]
1281 async fn publishes_bridge_owned_artifacts_only_after_verification() {
1282 let directory = tempfile::tempdir().unwrap();
1283 let store = Arc::new(ArtifactStore::new(directory.path(), ImageLimits::default()).unwrap());
1284 let provider = Arc::new(FakeProvider::new(Duration::ZERO));
1285 let runtime = runtime(&provider, |config| {
1286 config.materialization.artifact_store = Some(store);
1287 });
1288 let mut request = ImageRequest::generate("test");
1289 request.output.response_format = ResponseFormat::Artifact;
1290 request.output.filename_prefix = Some("Result".to_owned());
1291 let response = runtime.execute(request).await.unwrap();
1292 assert!(matches!(
1293 &response.data[0].payload,
1294 ImagePayload::Artifact { name: Some(name), .. } if name.starts_with("result-")
1295 ));
1296 }
1297
1298 #[tokio::test]
1299 async fn deadline_keeps_an_unknown_idempotency_tombstone() {
1300 let provider = Arc::new(FakeProvider::new(Duration::from_secs(5)));
1301 let runtime = runtime(&provider, |_| {});
1302 let mut request = ImageRequest::generate("test");
1303 request.timeout_ms = Some(100);
1304 request.idempotency_key = Some("retryable-key".to_owned());
1305 let first = runtime.execute(request.clone()).await.unwrap_err();
1306 assert_eq!(first.code, ErrorCode::Timeout);
1307 assert!(!first.retryable);
1308 assert_eq!(first.details["outcome"], "unknown");
1309 tokio::task::yield_now().await;
1310 assert!(provider.saw_cancellation.load(Ordering::Acquire));
1311 let second = runtime.execute(request).await.unwrap_err();
1312 assert_eq!(second.code, ErrorCode::Timeout);
1313 assert!(!second.retryable);
1314 assert_eq!(second.details["outcome"], "unknown");
1315 assert_eq!(provider.calls.load(Ordering::Acquire), 1);
1316 }
1317
1318 #[tokio::test]
1319 async fn rejects_provider_metadata_that_does_not_match_decoded_output() {
1320 let provider = Arc::new(FakeProvider::corrupting());
1321 let runtime = runtime(&provider, |_| {});
1322 let error = runtime
1323 .execute(ImageRequest::generate("test"))
1324 .await
1325 .unwrap_err();
1326 assert_eq!(error.code, ErrorCode::Protocol);
1327 }
1328
1329 #[tokio::test]
1330 async fn runtime_queue_rejects_work_beyond_its_explicit_bound() {
1331 let provider = Arc::new(FakeProvider::new(Duration::from_millis(40)));
1332 let runtime = Arc::new(runtime(&provider, |config| {
1333 config.global_limit = ConcurrencyLimit {
1334 max_concurrent: 1,
1335 max_queued: 1,
1336 };
1337 config.default_provider_limit = ConcurrencyLimit {
1338 max_concurrent: 1,
1339 max_queued: 1,
1340 };
1341 }));
1342 let first_runtime = Arc::clone(&runtime);
1343 let first =
1344 tokio::spawn(
1345 async move { first_runtime.execute(ImageRequest::generate("first")).await },
1346 );
1347 while provider.calls.load(Ordering::Acquire) == 0 {
1348 tokio::task::yield_now().await;
1349 }
1350 let second_runtime = Arc::clone(&runtime);
1351 let second = tokio::spawn(async move {
1352 second_runtime
1353 .execute(ImageRequest::generate("second"))
1354 .await
1355 });
1356 while runtime.queue_snapshot().global_queued == 0 {
1357 tokio::task::yield_now().await;
1358 }
1359 let error = runtime
1360 .execute(ImageRequest::generate("rejected"))
1361 .await
1362 .unwrap_err();
1363 assert_eq!(error.code, ErrorCode::Overloaded);
1364 assert!(first.await.unwrap().is_ok());
1365 assert!(second.await.unwrap().is_ok());
1366 }
1367
1368 #[tokio::test]
1369 async fn concurrent_idempotent_callers_share_one_provider_operation() {
1370 let provider = Arc::new(FakeProvider::new(Duration::from_millis(20)));
1371 let runtime = Arc::new(runtime(&provider, |_| {}));
1372 let mut request = ImageRequest::generate("same");
1373 request.idempotency_key = Some("shared-key".to_owned());
1374 let first_runtime = Arc::clone(&runtime);
1375 let first_request = request.clone();
1376 let first = tokio::spawn(async move { first_runtime.execute(first_request).await });
1377 while provider.calls.load(Ordering::Acquire) == 0 {
1378 tokio::task::yield_now().await;
1379 }
1380 let second = runtime.execute(request).await.unwrap();
1381 let first = first.await.unwrap().unwrap();
1382 assert_eq!(first, second);
1383 assert_eq!(provider.calls.load(Ordering::Acquire), 1);
1384 }
1385
1386 #[tokio::test]
1387 async fn unavailable_primary_falls_back_in_order_and_records_attempts() {
1388 let primary = Arc::new(RouteProvider::new(
1389 "primary",
1390 Some(RouteFailure::Authentication),
1391 ));
1392 let fallback = Arc::new(RouteProvider::new("fallback", None));
1393 let runtime = routing_runtime(&primary, &fallback);
1394 let mut request = ImageRequest::generate("test routing");
1395 request.routing.fallbacks.push(ProviderRoute {
1396 provider: "fallback".to_owned(),
1397 model: None,
1398 });
1399 let response = runtime.execute(request).await.unwrap();
1400 assert_eq!(response.provider, "fallback");
1401 assert_eq!(response.attempts.len(), 2);
1402 assert_eq!(
1403 response.attempts[0].error_code,
1404 Some(ErrorCode::Authentication)
1405 );
1406 assert_eq!(
1407 response.attempts[1].outcome,
1408 ProviderAttemptOutcome::Succeeded
1409 );
1410 assert!(
1411 response
1412 .warnings
1413 .contains(&"provider_fallback_used".to_owned())
1414 );
1415 assert_eq!(primary.execute_calls.load(Ordering::Acquire), 0);
1416 assert_eq!(fallback.execute_calls.load(Ordering::Acquire), 1);
1417 }
1418
1419 #[tokio::test]
1420 async fn fallback_never_bypasses_safety_or_unknown_outcomes() {
1421 for failure in [RouteFailure::Safety, RouteFailure::UnknownOutcome] {
1422 let primary = Arc::new(RouteProvider::new("primary", Some(failure)));
1423 let fallback = Arc::new(RouteProvider::new("fallback", None));
1424 let runtime = routing_runtime(&primary, &fallback);
1425 let mut request = ImageRequest::generate("test routing guard");
1426 request.routing.fallback_policy = FallbackPolicy::OnError;
1427 request.routing.fallbacks.push(ProviderRoute {
1428 provider: "fallback".to_owned(),
1429 model: None,
1430 });
1431 let error = runtime.execute(request).await.unwrap_err();
1432 assert_eq!(fallback.execute_calls.load(Ordering::Acquire), 0);
1433 assert_eq!(error.details["attempts"].as_array().unwrap().len(), 1);
1434 }
1435 }
1436
1437 #[tokio::test]
1438 async fn on_error_expands_but_does_not_replace_unavailable_policy() {
1439 for (policy, expected_calls) in [
1440 (FallbackPolicy::OnUnavailable, 0),
1441 (FallbackPolicy::OnError, 1),
1442 ] {
1443 let primary = Arc::new(RouteProvider::new(
1444 "primary",
1445 Some(RouteFailure::KnownUpstream),
1446 ));
1447 let fallback = Arc::new(RouteProvider::new("fallback", None));
1448 let runtime = routing_runtime(&primary, &fallback);
1449 let mut request = ImageRequest::generate("test known outcome policy");
1450 request.routing.fallback_policy = policy;
1451 request.routing.fallbacks.push(ProviderRoute {
1452 provider: "fallback".to_owned(),
1453 model: None,
1454 });
1455 let result = runtime.execute(request).await;
1456 assert_eq!(
1457 fallback.execute_calls.load(Ordering::Acquire),
1458 expected_calls
1459 );
1460 assert_eq!(result.is_ok(), policy == FallbackPolicy::OnError);
1461 }
1462 }
1463
1464 #[tokio::test]
1465 async fn implicit_primary_cannot_be_repeated_as_a_fallback() {
1466 let primary = Arc::new(RouteProvider::new("primary", None));
1467 let fallback = Arc::new(RouteProvider::new("fallback", None));
1468 let runtime = routing_runtime(&primary, &fallback);
1469 let mut request = ImageRequest::generate("test duplicate route");
1470 request.routing.fallbacks.push(ProviderRoute {
1471 provider: "primary".to_owned(),
1472 model: None,
1473 });
1474 let error = runtime.execute(request).await.unwrap_err();
1475 assert_eq!(error.code, ErrorCode::InvalidRequest);
1476 assert_eq!(primary.execute_calls.load(Ordering::Acquire), 0);
1477 }
1478
1479 #[tokio::test]
1480 async fn shutdown_cancels_and_drains_active_work_before_provider_release() {
1481 let provider = Arc::new(FakeProvider::new(Duration::from_secs(5)));
1482 let runtime = Arc::new(runtime(&provider, |_| {}));
1483 let executing_runtime = Arc::clone(&runtime);
1484 let executing = tokio::spawn(async move {
1485 executing_runtime
1486 .execute(ImageRequest::generate("active"))
1487 .await
1488 });
1489 while provider.calls.load(Ordering::Acquire) == 0 {
1490 tokio::task::yield_now().await;
1491 }
1492 runtime.shutdown().await.unwrap();
1493 runtime.shutdown().await.unwrap();
1494 let error = executing.await.unwrap().unwrap_err();
1495 assert_eq!(error.code, ErrorCode::Cancelled);
1496 assert!(provider.saw_cancellation.load(Ordering::Acquire));
1497 let error = runtime
1498 .execute(ImageRequest::generate("after shutdown"))
1499 .await
1500 .unwrap_err();
1501 assert_eq!(error.code, ErrorCode::Cancelled);
1502 }
1503}