Skip to main content

rig_core/providers/venice/
image_generation.rs

1//! Venice image generation.
2//!
3//! Venice's image endpoint is its own wire, not OpenAI's: it is
4//! `POST /image/generate`, it takes `width`/`height` (plus Venice-only
5//! controls through `additional_params`), and it answers with
6//! `{ id, images: [base64], request, timing }` rather than OpenAI's
7//! `data[].b64_json`.
8
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11
12use crate::image_generation::{self, ImageGenerationError, ImageGenerationRequest};
13use crate::json_utils::merge_inplace;
14use crate::providers::internal::image_generation::{
15    GenericImageGenerationModel, JsonImageGenerationProvider, decode_base64_image,
16};
17
18// ================================================================
19// Venice Image Generation API
20// ================================================================
21/// `venice-sd35`
22pub const VENICE_SD35: &str = "venice-sd35";
23/// `z-image-turbo` — Venice's `default` and `fastest` image model.
24pub const Z_IMAGE_TURBO: &str = "z-image-turbo";
25/// `qwen-image` — Venice's `highest_quality` image model.
26pub const QWEN_IMAGE: &str = "qwen-image";
27/// `flux-2-pro`
28pub const FLUX_2_PRO: &str = "flux-2-pro";
29/// `hunyuan-image-v3`
30pub const HUNYUAN_IMAGE_V3: &str = "hunyuan-image-v3";
31
32/// How long Venice spent generating an image, in milliseconds.
33#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
34pub struct ImageGenerationTiming {
35    /// Inference time.
36    #[serde(default)]
37    pub inference_duration: f64,
38    /// Preprocessing time.
39    #[serde(default, rename = "inferencePreprocessingTime")]
40    pub inference_preprocessing_time: f64,
41    /// Queue time before inference started.
42    #[serde(default, rename = "inferenceQueueTime")]
43    pub inference_queue_time: f64,
44    /// Total wall-clock time.
45    #[serde(default)]
46    pub total: f64,
47}
48
49/// Venice's `POST /image/generate` payload.
50#[derive(Debug, Deserialize, Serialize)]
51pub struct ImageGenerationResponse {
52    /// Venice's generation id.
53    pub id: String,
54    /// Base64-encoded images, one per requested variant.
55    pub images: Vec<String>,
56    /// Venice's echo of the request it applied.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub request: Option<serde_json::Value>,
59    /// Generation timings.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub timing: Option<ImageGenerationTiming>,
62}
63
64impl TryFrom<ImageGenerationResponse>
65    for image_generation::ImageGenerationResponse<ImageGenerationResponse>
66{
67    type Error = ImageGenerationError;
68
69    fn try_from(value: ImageGenerationResponse) -> Result<Self, Self::Error> {
70        decode_base64_image(
71            value,
72            |response| response.images.first().map(String::as_str),
73            "No image data returned",
74            Some("Base64 decode error: "),
75        )
76    }
77}
78
79/// Venice image generation model.
80pub type ImageGenerationModel<T = reqwest::Client> =
81    GenericImageGenerationModel<super::client::VeniceExt, T>;
82
83impl JsonImageGenerationProvider for super::client::VeniceExt {
84    const IMAGE_GENERATION_PATH: &'static str = "/image/generate";
85    type Response = ImageGenerationResponse;
86
87    fn image_generation_request_body(
88        model: &str,
89        generation_request: ImageGenerationRequest,
90    ) -> Result<serde_json::Value, ImageGenerationError> {
91        // Venice returns base64 images unless `return_binary` is set; the
92        // decode above depends on that, so the flag stays off the request and
93        // is not something `additional_params` should turn on.
94        let mut request = json!({
95            "model": model,
96            "prompt": generation_request.prompt,
97            "width": generation_request.width,
98            "height": generation_request.height,
99        });
100
101        if let Some(additional_params) = generation_request.additional_params {
102            merge_inplace(&mut request, additional_params);
103        }
104
105        Ok(request)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::client::image_generation::ImageGenerationClient;
113    use crate::image_generation::ImageGenerationModel as _;
114
115    fn request() -> ImageGenerationRequest {
116        ImageGenerationRequest {
117            prompt: "a red circle on white".to_string(),
118            width: 256,
119            height: 256,
120            additional_params: None,
121        }
122    }
123
124    /// Venice answers a bad request with a flat `{"error": "…"}` body, not
125    /// OpenAI's nested error object; the shared envelope must still classify
126    /// it as an error and preserve the body verbatim.
127    #[tokio::test]
128    async fn image_generation_non_success_preserves_status_and_body() {
129        use crate::test_utils::RecordingHttpClient;
130
131        let body = r#"{"error":"Specified model not found: nope"}"#;
132        let http_client =
133            RecordingHttpClient::with_error_response(http::StatusCode::NOT_FOUND, body);
134        let client = crate::providers::venice::Client::builder()
135            .api_key("test-key")
136            .http_client(http_client)
137            .build()
138            .expect("build client");
139        let model = client.image_generation_model(VENICE_SD35);
140
141        let error = model
142            .image_generation(request())
143            .await
144            .expect_err("should fail with non-success status");
145
146        assert!(matches!(error, ImageGenerationError::HttpError(_)));
147        assert_eq!(
148            error.provider_response_status(),
149            Some(http::StatusCode::NOT_FOUND)
150        );
151        assert_eq!(error.provider_response_body(), Some(body));
152    }
153
154    #[tokio::test]
155    async fn image_generation_posts_venice_native_body() {
156        use crate::test_utils::RecordingHttpClient;
157
158        let http_client = RecordingHttpClient::new(r#"{"id":"abc","images":["aGVsbG8="]}"#);
159        let client = crate::providers::venice::Client::builder()
160            .api_key("test-key")
161            .http_client(http_client.clone())
162            .build()
163            .expect("build client");
164        let model = client.image_generation_model(VENICE_SD35);
165
166        let response = model
167            .image_generation(request())
168            .await
169            .expect("image generation should succeed");
170
171        assert_eq!(response.image, b"hello");
172        assert_eq!(response.response.id, "abc");
173
174        let requests = http_client.requests();
175        let recorded = requests.first().expect("one request");
176        assert!(recorded.uri.ends_with("/image/generate"));
177        let body: serde_json::Value =
178            serde_json::from_slice(&recorded.body).expect("body should be JSON");
179        assert_eq!(
180            body,
181            serde_json::json!({
182                "model": VENICE_SD35,
183                "prompt": "a red circle on white",
184                "width": 256,
185                "height": 256,
186            })
187        );
188    }
189}