Skip to main content

blazen_llm_core/compute/
traits.rs

1//! Compute platform traits for media generation providers.
2//!
3//! Implement [`ComputeProvider`] as the base, then add media-specific
4//! traits ([`ImageGeneration`], [`VideoGeneration`], etc.) for the
5//! capabilities your provider supports.
6//!
7//! ```rust,ignore
8//! use blazen_llm_core::compute::*;
9//! use blazen_llm_core::BlazenError;
10//!
11//! struct MyImageProvider { /* ... */ }
12//!
13//! #[async_trait::async_trait]
14//! impl ComputeProvider for MyImageProvider {
15//!     fn provider_id(&self) -> &str { "my-provider" }
16//!     async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError> { todo!() }
17//!     async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError> { todo!() }
18//!     async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError> { todo!() }
19//!     async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError> { todo!() }
20//! }
21//!
22//! #[async_trait::async_trait]
23//! impl ImageGeneration for MyImageProvider {
24//!     async fn generate_image(&self, request: ImageRequest) -> Result<ImageResult, BlazenError> {
25//!         // Your implementation
26//!         todo!()
27//!     }
28//!     async fn upscale_image(&self, request: UpscaleRequest) -> Result<ImageResult, BlazenError> {
29//!         // Your implementation
30//!         todo!()
31//!     }
32//! }
33//! ```
34
35use std::sync::Arc;
36
37use async_trait::async_trait;
38
39use super::job::{ComputeRequest, ComputeResult, JobHandle, JobStatus};
40use super::requests::{
41    BackgroundRemovalRequest, CadReconstructionRequest, CadRequest, CaptionRequest, DepthRequest,
42    DetectionRequest, EmbeddingRequest, ImageEditRequest, ImageRequest, MusicRequest,
43    ReconstructionRequest, RetopologyRequest, SegmentationRequest, SpeechRequest, TextureRequest,
44    ThreeDRequest, TranscriptionRequest, UpscaleRequest, VideoEditRequest, VideoRequest,
45    VoiceCloneRequest,
46};
47use super::results::{
48    AudioResult, CadReconstructionResult, CadResult, CaptionResult, DepthResult, DetectionResult,
49    EmbeddingResult, ImageResult, ReconstructionResult, RetopologyResult, SegmentationResult,
50    TextureResult, ThreeDResult, TranscriptionResult, VideoResult, VoiceHandle,
51};
52use crate::error::BlazenError;
53
54// ---------------------------------------------------------------------------
55// Core compute trait
56// ---------------------------------------------------------------------------
57
58/// A compute platform that supports async job submission and polling.
59///
60/// This is the base trait for compute providers like fal.ai, Replicate,
61/// `RunPod`, etc. It models the common pattern of: submit a job, poll for
62/// status, retrieve the result.
63#[async_trait]
64pub trait ComputeProvider: Send + Sync {
65    /// Provider identifier (e.g., "fal", "replicate", "runpod").
66    fn provider_id(&self) -> &str;
67
68    /// Submit a compute job and get a handle to track it.
69    async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError>;
70
71    /// Poll the current status of a submitted job.
72    async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError>;
73
74    /// Wait for a job to complete and return the result.
75    ///
76    /// The default implementation delegates to the provider. Providers may
77    /// override this with long-polling, `WebSockets`, or SSE.
78    async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError>;
79
80    /// Cancel a running or queued job.
81    async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError>;
82
83    /// Submit a job and wait for the result (convenience method).
84    ///
85    /// This is equivalent to calling [`ComputeProvider::submit`] followed
86    /// by [`ComputeProvider::result`].
87    async fn run(&self, request: ComputeRequest) -> Result<ComputeResult, BlazenError> {
88        let job = self.submit(request).await?;
89        self.result(job).await
90    }
91
92    /// Optional configuration metadata for this provider.
93    fn provider_config(&self) -> Option<&crate::traits::ProviderConfig> {
94        None
95    }
96}
97
98// Arc passthrough for the base compute trait so capability sub-traits
99// (e.g. `VideoEdit`) can also be forwarded through `Arc<P>` — their
100// `impl ... for Arc<P>` requires `Arc<P>: ComputeProvider`.
101#[async_trait]
102impl<P: ComputeProvider + ?Sized> ComputeProvider for Arc<P> {
103    fn provider_id(&self) -> &str {
104        (**self).provider_id()
105    }
106    async fn submit(&self, request: ComputeRequest) -> Result<JobHandle, BlazenError> {
107        (**self).submit(request).await
108    }
109    async fn status(&self, job: &JobHandle) -> Result<JobStatus, BlazenError> {
110        (**self).status(job).await
111    }
112    async fn result(&self, job: JobHandle) -> Result<ComputeResult, BlazenError> {
113        (**self).result(job).await
114    }
115    async fn cancel(&self, job: &JobHandle) -> Result<(), BlazenError> {
116        (**self).cancel(job).await
117    }
118    async fn run(&self, request: ComputeRequest) -> Result<ComputeResult, BlazenError> {
119        (**self).run(request).await
120    }
121    fn provider_config(&self) -> Option<&crate::traits::ProviderConfig> {
122        (**self).provider_config()
123    }
124}
125
126// ---------------------------------------------------------------------------
127// Media-specific traits
128// ---------------------------------------------------------------------------
129
130/// Image generation and upscaling capability.
131///
132/// Providers that support image generation (fal.ai, Replicate, etc.)
133/// implement this trait to provide a typed interface for image operations.
134#[async_trait]
135pub trait ImageGeneration: ComputeProvider {
136    /// Generate images from a text prompt.
137    async fn generate_image(&self, request: ImageRequest) -> Result<ImageResult, BlazenError>;
138
139    /// Upscale an existing image.
140    async fn upscale_image(&self, request: UpscaleRequest) -> Result<ImageResult, BlazenError>;
141
142    /// Edit an existing image (img2img, inpaint, outpaint, etc.).
143    ///
144    /// Returns [`BlazenError::Unsupported`] by default so existing image
145    /// providers keep compiling unchanged; providers that support editing
146    /// (fal.ai, etc.) override this.
147    async fn edit_image(&self, _request: ImageEditRequest) -> Result<ImageResult, BlazenError> {
148        Err(BlazenError::unsupported(
149            "image editing not supported by this provider",
150        ))
151    }
152}
153
154/// Video generation capability.
155///
156/// Providers that support video synthesis implement this trait.
157#[async_trait]
158pub trait VideoGeneration: ComputeProvider {
159    /// Generate a video from a text prompt.
160    async fn text_to_video(&self, request: VideoRequest) -> Result<VideoResult, BlazenError>;
161
162    /// Generate a video from a source image and prompt.
163    async fn image_to_video(&self, request: VideoRequest) -> Result<VideoResult, BlazenError>;
164}
165
166/// Video editing / conditioning capability (VACE-style: reference / control /
167/// inpaint / outpaint / extend → video). Beyond plain text→video.
168#[async_trait]
169pub trait VideoEdit: ComputeProvider {
170    /// Edit/condition a video from references, a control video, and/or a mask.
171    ///
172    /// Returns [`BlazenError::Unsupported`] by default so existing video
173    /// providers keep compiling unchanged; providers that support editing
174    /// (native VACE, fal, etc.) override this.
175    async fn edit_video(&self, _request: VideoEditRequest) -> Result<VideoResult, BlazenError> {
176        Err(BlazenError::unsupported(
177            "video editing not supported by this provider",
178        ))
179    }
180}
181
182/// Audio generation capability (TTS, music, sound effects).
183///
184/// Providers that support audio synthesis implement this trait.
185#[async_trait]
186pub trait AudioGeneration: ComputeProvider {
187    /// Synthesize speech from text.
188    async fn text_to_speech(&self, request: SpeechRequest) -> Result<AudioResult, BlazenError>;
189
190    /// Generate music from a prompt.
191    ///
192    /// Returns [`BlazenError::Unsupported`] by default.
193    async fn generate_music(&self, _request: MusicRequest) -> Result<AudioResult, BlazenError> {
194        Err(BlazenError::unsupported(
195            "music generation not supported by this provider",
196        ))
197    }
198
199    /// Generate sound effects from a prompt.
200    ///
201    /// Returns [`BlazenError::Unsupported`] by default.
202    async fn generate_sfx(&self, _request: MusicRequest) -> Result<AudioResult, BlazenError> {
203        Err(BlazenError::unsupported(
204            "sound effect generation not supported by this provider",
205        ))
206    }
207}
208
209/// Audio transcription capability (speech-to-text).
210///
211/// Providers that support transcription implement this trait.
212#[async_trait]
213pub trait Transcription: ComputeProvider {
214    /// Transcribe audio to text with optional diarization.
215    async fn transcribe(
216        &self,
217        request: TranscriptionRequest,
218    ) -> Result<TranscriptionResult, BlazenError>;
219}
220
221/// 3D model generation capability.
222///
223/// Providers that support 3D generation implement this trait.
224#[async_trait]
225pub trait ThreeDGeneration: ComputeProvider {
226    /// Generate a 3D model from a text prompt or source image.
227    async fn generate_3d(&self, request: ThreeDRequest) -> Result<ThreeDResult, BlazenError>;
228}
229
230/// Neural audio codec capability (PCM ↔ discrete tokens).
231///
232/// Providers that wrap a neural audio codec (`EnCodec`, `DAC`, `SNAC`, …)
233/// implement this trait to expose deterministic encode / decode passes
234/// outside the generative job API.
235///
236/// Both methods default to [`BlazenError::Unsupported`] so existing
237/// non-codec providers do not need to override them.
238#[async_trait]
239pub trait Codec: ComputeProvider {
240    /// Encode mono PCM samples (`f32` in `[-1.0, 1.0]`) into discrete
241    /// codebook tokens.
242    ///
243    /// Returns [`BlazenError::Unsupported`] by default.
244    async fn encode_audio(&self, _pcm: &[f32], _sample_rate: u32) -> Result<Vec<u32>, BlazenError> {
245        Err(BlazenError::unsupported(
246            "audio codec encode_audio not supported by this provider",
247        ))
248    }
249
250    /// Decode flat row-major codebook tokens back into mono PCM samples.
251    ///
252    /// Returns [`BlazenError::Unsupported`] by default.
253    async fn decode_audio(
254        &self,
255        _tokens: &[u32],
256        _num_codebooks: usize,
257    ) -> Result<Vec<f32>, BlazenError> {
258        Err(BlazenError::unsupported(
259            "audio codec decode_audio not supported by this provider",
260        ))
261    }
262}
263
264/// A compute provider that supports background removal on existing images.
265#[async_trait]
266pub trait BackgroundRemoval: ComputeProvider {
267    /// Remove the background from an image and return the result.
268    async fn remove_background(
269        &self,
270        request: BackgroundRemovalRequest,
271    ) -> Result<ImageResult, BlazenError>;
272}
273
274/// Image-analysis capability: segmentation, depth estimation, and captioning.
275///
276/// This is the public seam for image-understanding models. Blazen ships the
277/// fal proxy implementation; private native depth / segmentation / caption
278/// models (in the zreg-only tier) implement this same trait so they can be
279/// injected behind the public interface.
280///
281/// All three methods default to [`BlazenError::Unsupported`] so providers
282/// opt in only to the capabilities they actually offer.
283#[async_trait]
284pub trait ImageAnalysis: ComputeProvider {
285    /// Segment an image into one or more masks (automatic or promptable).
286    ///
287    /// Returns [`BlazenError::Unsupported`] by default.
288    async fn segment_image(
289        &self,
290        _request: SegmentationRequest,
291    ) -> Result<SegmentationResult, BlazenError> {
292        Err(BlazenError::unsupported(
293            "image segmentation not supported by this provider",
294        ))
295    }
296
297    /// Estimate a depth map from a single image.
298    ///
299    /// Returns [`BlazenError::Unsupported`] by default.
300    async fn estimate_depth(&self, _request: DepthRequest) -> Result<DepthResult, BlazenError> {
301        Err(BlazenError::unsupported(
302            "depth estimation not supported by this provider",
303        ))
304    }
305
306    /// Caption an image, or answer a question about it (VQA).
307    ///
308    /// Returns [`BlazenError::Unsupported`] by default.
309    async fn caption_image(&self, _request: CaptionRequest) -> Result<CaptionResult, BlazenError> {
310        Err(BlazenError::unsupported(
311            "image captioning not supported by this provider",
312        ))
313    }
314}
315
316/// Open-vocabulary object-detection capability: image + class prompts → boxes.
317///
318/// This is the public seam for open-vocabulary detectors (Grounding-DINO,
319/// YOLO-World, OWL-ViT, …). Blazen ships fal-proxied detection; private native
320/// detectors (in the zreg-only tier) implement this same trait so they can be
321/// injected behind the public interface, exactly like [`ImageAnalysis`].
322///
323/// The single method defaults to [`BlazenError::Unsupported`] so a provider
324/// opts in only when it actually offers detection.
325#[async_trait]
326pub trait ObjectDetection: ComputeProvider {
327    /// Detect objects in an image, constrained to the request's open-vocabulary
328    /// class prompts (`text_prompt` and/or `classes`).
329    ///
330    /// Returns [`BlazenError::Unsupported`] by default.
331    async fn detect_objects(
332        &self,
333        _request: DetectionRequest,
334    ) -> Result<DetectionResult, BlazenError> {
335        Err(BlazenError::unsupported(
336            "object detection not supported by this provider",
337        ))
338    }
339}
340
341/// Multi-view 3D reconstruction capability: N images / video → mesh + cloud.
342///
343/// This is the public seam for MASt3R-class reconstructors that recover scene
344/// geometry and camera poses from multiple overlapping views. Blazen ships no
345/// reconstruction backend today (fal has no `MASt3R` endpoint); private native
346/// reconstructors (in the zreg-only tier) implement this same trait so they can
347/// be injected behind the public interface, exactly like [`ObjectDetection`].
348///
349/// The single method defaults to [`BlazenError::Unsupported`] so a provider
350/// opts in only when it actually offers reconstruction.
351#[async_trait]
352pub trait Reconstruction: ComputeProvider {
353    /// Reconstruct 3D geometry (mesh + optional point cloud + camera poses)
354    /// from the request's multiple views (`image_urls` and/or `video_url`).
355    ///
356    /// Returns [`BlazenError::Unsupported`] by default.
357    async fn reconstruct(
358        &self,
359        _request: ReconstructionRequest,
360    ) -> Result<ReconstructionResult, BlazenError> {
361        Err(BlazenError::unsupported(
362            "3D reconstruction not supported by this provider",
363        ))
364    }
365}
366
367/// Text-to-CAD capability: natural-language instruction → CAD script → mesh.
368///
369/// This is the public seam for instruction-driven CAD code generators
370/// (BlenderLLM-class): an LLM emits an executable CAD program (e.g. Blender
371/// `bpy`) from a textual part description, which is then run headless to
372/// produce a mesh. The result carries **both** the generated mesh and the
373/// script source. Blazen ships no CAD backend today; private native CAD models
374/// (in the zreg-only tier) implement this same trait so they can be injected
375/// behind the public interface, exactly like [`Reconstruction`].
376///
377/// The single method defaults to [`BlazenError::Unsupported`] so a provider
378/// opts in only when it actually offers CAD generation.
379#[async_trait]
380pub trait TextToCad: ComputeProvider {
381    /// Generate a CAD model (mesh + the script that produced it) from the
382    /// request's natural-language `instruction`.
383    ///
384    /// Returns [`BlazenError::Unsupported`] by default.
385    async fn generate_cad(&self, _request: CadRequest) -> Result<CadResult, BlazenError> {
386        Err(BlazenError::unsupported(
387            "text-to-CAD generation not supported by this provider",
388        ))
389    }
390}
391
392/// Retopology capability: dense/scanned mesh → clean quad-dominant mesh.
393///
394/// This is the public seam for automatic retopology backends (`QuadriFlow` /
395/// `QuadGPT` / Quad-Remesher-class) that rebuild a (typically triangulated or
396/// scanned) mesh as an even-density, quad-dominant surface fit for
397/// animation/subdivision. Blazen ships no retopology backend today; private
398/// native retopologizers (in the zreg-only tier) implement this same trait so
399/// they can be injected behind the public interface, exactly like
400/// [`Reconstruction`].
401///
402/// The single method defaults to [`BlazenError::Unsupported`] so a provider
403/// opts in only when it actually offers retopology.
404#[async_trait]
405pub trait Retopology: ComputeProvider {
406    /// Retopologize the request's `mesh_url` into a clean, quad-dominant mesh.
407    ///
408    /// Returns [`BlazenError::Unsupported`] by default.
409    async fn retopologize(
410        &self,
411        _request: RetopologyRequest,
412    ) -> Result<RetopologyResult, BlazenError> {
413        Err(BlazenError::unsupported(
414            "retopology not supported by this provider",
415        ))
416    }
417}
418
419/// CAD-reconstruction capability: point cloud → parametric CAD model + program.
420///
421/// This is the public seam for point-cloud-to-CAD reverse-engineering backends
422/// (`CAD-Recode`-class): given a sampled point cloud, a model recovers an
423/// editable parametric model plus the CAD program (e.g. a `CadQuery` Python
424/// script) that constructs it. It is the point-cloud sibling of [`TextToCad`]'s
425/// instruction→CAD seam, and the result carries **both** the model and the
426/// script source. Blazen ships no CAD-reconstruction backend today; private
427/// native models (in the zreg-only tier) implement this same trait so they can
428/// be injected behind the public interface, exactly like [`Reconstruction`].
429///
430/// The single method defaults to [`BlazenError::Unsupported`] so a provider
431/// opts in only when it actually offers CAD reconstruction.
432#[async_trait]
433pub trait CadReconstruction: ComputeProvider {
434    /// Reconstruct a parametric CAD model (model + the program that produced
435    /// it) from the request's `point_cloud_url`.
436    ///
437    /// Returns [`BlazenError::Unsupported`] by default.
438    async fn reconstruct_cad(
439        &self,
440        _request: CadReconstructionRequest,
441    ) -> Result<CadReconstructionResult, BlazenError> {
442        Err(BlazenError::unsupported(
443            "CAD reconstruction not supported by this provider",
444        ))
445    }
446}
447
448/// Mesh-texturing capability: mesh + prompt → PBR texture set.
449///
450/// This is the public seam for text-conditioned mesh texturers (`Paint3D` /
451/// `TEXTure` / `MVDream`-class) that paint a physically-based material set onto an
452/// existing mesh. Blazen ships no texturing backend today; private native
453/// texturers (in the zreg-only tier) implement this same trait so they can be
454/// injected behind the public interface, exactly like [`Reconstruction`].
455///
456/// The single method defaults to [`BlazenError::Unsupported`] so a provider
457/// opts in only when it actually offers texturing.
458///
459/// # Why there is no `FalProvider` implementation (abstraction-only)
460///
461/// Unlike the other compute traits, `TextureGeneration` ships **no fal proxy**:
462///
463/// - **Native is blocked.** A native texturer needs ControlNet-style residual
464///   injection into the candle `UNet` (still missing — candle's `UNet` has no
465///   residual-injection seam) *and* a differentiable mesh rasteriser to bake
466///   the multi-view paint back onto UV space (no candle equivalent). Both gaps
467///   are tracked in the private `blazen-models-3d` tier.
468/// - **No fal endpoint fits.** As of 2026-06 fal.ai exposes *no* mesh-texturing
469///   endpoint. Every fal 3D app (`hunyuan3d/v2`, `trellis`, `hyper3d/rodin`,
470///   `triposr`) takes **text or an image** and emits a *new* mesh — the mesh is
471///   the output, never the input — so the texture set is baked during
472///   generation and there is no `(mesh + prompt) -> textures` API to proxy.
473///   The only mesh-in endpoint, `triposr/remeshing`, only re-topologises
474///   geometry and produces no textures. Forcing a [`TextureRequest::mesh_url`]
475///   into an `image_url` field would be semantically wrong (a mesh is not an
476///   image), so no proxy is wired.
477///
478/// `TextureGeneration` therefore stays **abstraction-only**: the public seam
479/// exists so a private native texturer (or a future fal texturing endpoint) can
480/// be injected behind it, but every shipped provider inherits the default
481/// `Unsupported` body until then.
482#[async_trait]
483pub trait TextureGeneration: ComputeProvider {
484    /// Paint a PBR texture set (albedo + optional normal/roughness/metallic)
485    /// onto the request's `mesh_url`, guided by the request's `prompt`.
486    ///
487    /// Returns [`BlazenError::Unsupported`] by default.
488    async fn generate_texture(
489        &self,
490        _request: TextureRequest,
491    ) -> Result<TextureResult, BlazenError> {
492        Err(BlazenError::unsupported(
493            "texture generation not supported by this provider",
494        ))
495    }
496}
497
498/// Multimodal embedding capability: image / text / audio → vector.
499///
500/// This is the public seam for multimodal embedders (CLIP / `SigLIP` / CLAP /
501/// sentence-transformer-class) that encode an input into a dense vector in a
502/// shared embedding space. Blazen ships no embedding backend today; private
503/// native embedders (in the zreg-only tier) implement this same trait so they
504/// can be injected behind the public interface, exactly like [`Reconstruction`].
505///
506/// The single method defaults to [`BlazenError::Unsupported`] so a provider
507/// opts in only when it actually offers embedding.
508#[async_trait]
509pub trait Embedding: ComputeProvider {
510    /// Embed the request's `input` (text, image URL, or audio URL) into a
511    /// dense vector.
512    ///
513    /// Returns [`BlazenError::Unsupported`] by default.
514    async fn embed(&self, _request: EmbeddingRequest) -> Result<EmbeddingResult, BlazenError> {
515        Err(BlazenError::unsupported(
516            "embedding not supported by this provider",
517        ))
518    }
519}
520
521// ---------------------------------------------------------------------------
522// Voice cloning
523// ---------------------------------------------------------------------------
524
525/// Voice cloning capability.
526///
527/// Distinct from `AudioGeneration::text_to_speech` because cloning creates
528/// a persisted voice that can be reused across later TTS calls. No
529/// Blazen-shipped provider implements this trait -- it exists so users
530/// building their own providers (via `CustomProvider`) can wire up
531/// services like `ElevenLabs` or `zvoice` into Blazen's capability system.
532#[async_trait]
533pub trait VoiceCloning: ComputeProvider {
534    /// Clone a voice from one or more reference audio clips and return
535    /// a persistent handle that can be passed as `SpeechRequest.voice`
536    /// on subsequent TTS calls.
537    async fn clone_voice(&self, request: VoiceCloneRequest) -> Result<VoiceHandle, BlazenError>;
538
539    /// List all voices known to this provider (presets + cloned).
540    ///
541    /// Returns `BlazenError::Unsupported` by default.
542    async fn list_voices(&self) -> Result<Vec<VoiceHandle>, BlazenError> {
543        Err(BlazenError::unsupported(
544            "list_voices not supported by this provider",
545        ))
546    }
547
548    /// Delete a previously cloned voice.
549    ///
550    /// Returns `BlazenError::Unsupported` by default.
551    async fn delete_voice(&self, voice: &VoiceHandle) -> Result<(), BlazenError> {
552        let _ = voice;
553        Err(BlazenError::unsupported(
554            "delete_voice not supported by this provider",
555        ))
556    }
557}
558
559// Backwards-compatible type alias for the old trait name.
560/// Alias for [`ImageGeneration`] -- the old name before the multi-modal
561/// expansion.
562pub trait ImageModel: ImageGeneration {}
563impl<T: ImageGeneration> ImageModel for T {}
564
565// ---------------------------------------------------------------------------
566// Tests
567// ---------------------------------------------------------------------------
568
569#[cfg(test)]
570mod tests {
571    use super::super::requests::{ImageEditRequest, ImageEditType};
572    use super::*;
573
574    /// A minimal non-fal [`ImageGeneration`] provider that implements only the
575    /// required methods and inherits the defaulted `edit_image`.
576    struct NoEditProvider;
577
578    #[async_trait]
579    impl ComputeProvider for NoEditProvider {
580        fn provider_id(&self) -> &'static str {
581            "no-edit"
582        }
583        async fn submit(&self, _request: ComputeRequest) -> Result<JobHandle, BlazenError> {
584            Err(BlazenError::unsupported("submit"))
585        }
586        async fn status(&self, _job: &JobHandle) -> Result<JobStatus, BlazenError> {
587            Err(BlazenError::unsupported("status"))
588        }
589        async fn result(&self, _job: JobHandle) -> Result<ComputeResult, BlazenError> {
590            Err(BlazenError::unsupported("result"))
591        }
592        async fn cancel(&self, _job: &JobHandle) -> Result<(), BlazenError> {
593            Err(BlazenError::unsupported("cancel"))
594        }
595    }
596
597    #[async_trait]
598    impl ImageGeneration for NoEditProvider {
599        async fn generate_image(&self, _request: ImageRequest) -> Result<ImageResult, BlazenError> {
600            Err(BlazenError::unsupported("generate"))
601        }
602        async fn upscale_image(
603            &self,
604            _request: UpscaleRequest,
605        ) -> Result<ImageResult, BlazenError> {
606            Err(BlazenError::unsupported("upscale"))
607        }
608        // `edit_image` intentionally NOT overridden -> uses the trait default.
609    }
610
611    #[tokio::test]
612    async fn default_edit_image_returns_unsupported() {
613        let provider = NoEditProvider;
614        let request = ImageEditRequest::new(
615            "https://example.com/src.png",
616            "make it pop",
617            ImageEditType::Img2Img,
618        );
619        let err = provider.edit_image(request).await.unwrap_err();
620        assert!(
621            matches!(err, BlazenError::Unsupported { .. }),
622            "expected Unsupported, got: {err:?}"
623        );
624    }
625
626    /// A provider that opts into [`ImageAnalysis`] but overrides none of its
627    /// methods, so all three inherit the defaulted `Unsupported` bodies.
628    struct NoAnalysisProvider;
629
630    #[async_trait]
631    impl ComputeProvider for NoAnalysisProvider {
632        fn provider_id(&self) -> &'static str {
633            "no-analysis"
634        }
635        async fn submit(&self, _request: ComputeRequest) -> Result<JobHandle, BlazenError> {
636            Err(BlazenError::unsupported("submit"))
637        }
638        async fn status(&self, _job: &JobHandle) -> Result<JobStatus, BlazenError> {
639            Err(BlazenError::unsupported("status"))
640        }
641        async fn result(&self, _job: JobHandle) -> Result<ComputeResult, BlazenError> {
642            Err(BlazenError::unsupported("result"))
643        }
644        async fn cancel(&self, _job: &JobHandle) -> Result<(), BlazenError> {
645            Err(BlazenError::unsupported("cancel"))
646        }
647    }
648
649    // Opts into the capability without overriding any method.
650    impl ImageAnalysis for NoAnalysisProvider {}
651
652    #[tokio::test]
653    async fn default_image_analysis_methods_return_unsupported() {
654        let provider = NoAnalysisProvider;
655
656        let seg_err = provider
657            .segment_image(SegmentationRequest::new("https://example.com/img.png"))
658            .await
659            .unwrap_err();
660        assert!(
661            matches!(seg_err, BlazenError::Unsupported { .. }),
662            "expected Unsupported, got: {seg_err:?}"
663        );
664
665        let depth_err = provider
666            .estimate_depth(DepthRequest::new("https://example.com/img.png"))
667            .await
668            .unwrap_err();
669        assert!(
670            matches!(depth_err, BlazenError::Unsupported { .. }),
671            "expected Unsupported, got: {depth_err:?}"
672        );
673
674        let cap_err = provider
675            .caption_image(CaptionRequest::new("https://example.com/img.png"))
676            .await
677            .unwrap_err();
678        assert!(
679            matches!(cap_err, BlazenError::Unsupported { .. }),
680            "expected Unsupported, got: {cap_err:?}"
681        );
682    }
683
684    // Opts into detection without overriding the method -> defaulted body.
685    impl ObjectDetection for NoAnalysisProvider {}
686
687    #[tokio::test]
688    async fn default_detect_objects_returns_unsupported() {
689        let provider = NoAnalysisProvider;
690        let err = provider
691            .detect_objects(
692                DetectionRequest::new("https://example.com/img.png").with_prompt("a dog. a cat."),
693            )
694            .await
695            .unwrap_err();
696        assert!(
697            matches!(err, BlazenError::Unsupported { .. }),
698            "expected Unsupported, got: {err:?}"
699        );
700    }
701
702    // Opts into reconstruction without overriding the method -> defaulted body.
703    impl Reconstruction for NoAnalysisProvider {}
704
705    #[tokio::test]
706    async fn default_reconstruct_returns_unsupported() {
707        let provider = NoAnalysisProvider;
708        let err = provider
709            .reconstruct(ReconstructionRequest::from_images([
710                "https://example.com/view1.png",
711                "https://example.com/view2.png",
712            ]))
713            .await
714            .unwrap_err();
715        assert!(
716            matches!(err, BlazenError::Unsupported { .. }),
717            "expected Unsupported, got: {err:?}"
718        );
719    }
720
721    // Opts into text-to-CAD without overriding the method -> defaulted body.
722    impl TextToCad for NoAnalysisProvider {}
723
724    #[tokio::test]
725    async fn default_generate_cad_returns_unsupported() {
726        let provider = NoAnalysisProvider;
727        let err = provider
728            .generate_cad(CadRequest::new("a hexagonal nut with a central hole"))
729            .await
730            .unwrap_err();
731        assert!(
732            matches!(err, BlazenError::Unsupported { .. }),
733            "expected Unsupported, got: {err:?}"
734        );
735    }
736
737    // Opts into retopology without overriding the method -> defaulted body.
738    impl Retopology for NoAnalysisProvider {}
739
740    #[tokio::test]
741    async fn default_retopologize_returns_unsupported() {
742        let provider = NoAnalysisProvider;
743        let err = provider
744            .retopologize(RetopologyRequest::from_mesh(
745                "https://example.com/dense.glb",
746            ))
747            .await
748            .unwrap_err();
749        assert!(
750            matches!(err, BlazenError::Unsupported { .. }),
751            "expected Unsupported, got: {err:?}"
752        );
753    }
754
755    // Opts into CAD reconstruction without overriding the method -> defaulted body.
756    impl CadReconstruction for NoAnalysisProvider {}
757
758    #[tokio::test]
759    async fn default_reconstruct_cad_returns_unsupported() {
760        let provider = NoAnalysisProvider;
761        let err = provider
762            .reconstruct_cad(CadReconstructionRequest::from_point_cloud(
763                "https://example.com/scan.ply",
764            ))
765            .await
766            .unwrap_err();
767        assert!(
768            matches!(err, BlazenError::Unsupported { .. }),
769            "expected Unsupported, got: {err:?}"
770        );
771    }
772
773    // Opts into texture generation without overriding the method -> defaulted body.
774    impl TextureGeneration for NoAnalysisProvider {}
775
776    #[tokio::test]
777    async fn default_generate_texture_returns_unsupported() {
778        let provider = NoAnalysisProvider;
779        let err = provider
780            .generate_texture(TextureRequest::from_mesh_and_prompt(
781                "https://example.com/mesh.glb",
782                "weathered bronze",
783            ))
784            .await
785            .unwrap_err();
786        assert!(
787            matches!(err, BlazenError::Unsupported { .. }),
788            "expected Unsupported, got: {err:?}"
789        );
790    }
791
792    // Opts into embedding without overriding the method -> defaulted body.
793    impl Embedding for NoAnalysisProvider {}
794
795    #[tokio::test]
796    async fn default_embed_returns_unsupported() {
797        let provider = NoAnalysisProvider;
798        let err = provider
799            .embed(EmbeddingRequest::from_text("a photo of a dragon"))
800            .await
801            .unwrap_err();
802        assert!(
803            matches!(err, BlazenError::Unsupported { .. }),
804            "expected Unsupported, got: {err:?}"
805        );
806    }
807}