Skip to main content

zai_rs/model/gen_image/
image_response.rs

1use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
2
3/// Image generation response payload
4///
5/// Every property is optional in OpenAPI, but a successful response must carry
6/// at least one documented, non-null property.
7#[derive(Debug, Clone, Serialize)]
8pub struct ImageResponse {
9    /// Request created time, Unix timestamp (seconds)
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub created: Option<u64>,
12
13    /// Array containing generated image URLs. Currently only one image.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub data: Option<Vec<ImageDataItem>>,
16
17    /// Content safety related information
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub content_filter: Option<Vec<ImageContentFilterInfo>>,
20}
21
22#[derive(Deserialize)]
23struct ImageResponseWire {
24    created: Option<u64>,
25    data: Option<Vec<ImageDataItem>>,
26    content_filter: Option<Vec<ImageContentFilterInfo>>,
27}
28
29impl<'de> Deserialize<'de> for ImageResponse {
30    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31    where
32        D: Deserializer<'de>,
33    {
34        let wire = ImageResponseWire::deserialize(deserializer)?;
35        if wire.created.is_none() && wire.data.is_none() && wire.content_filter.is_none() {
36            return Err(D::Error::custom(
37                "image response contained no documented fields",
38            ));
39        }
40        Ok(Self {
41            created: wire.created,
42            data: wire.data,
43            content_filter: wire.content_filter,
44        })
45    }
46}
47
48/// Single generated image item
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ImageDataItem {
51    /// Image link. Temporary URL valid for ~30 days; persist if needed.
52    pub url: String,
53}
54
55/// Content-safety metadata returned by image generation.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ImageContentFilterInfo {
58    /// Stage at which the safety filter applied.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub role: Option<ImageContentFilterRole>,
61    /// Severity from 0 (most severe) through 3 (least severe).
62    #[serde(
63        default,
64        skip_serializing_if = "Option::is_none",
65        deserialize_with = "deserialize_optional_filter_level"
66    )]
67    pub level: Option<i32>,
68}
69
70/// Safety-filter stage defined by the image response schema.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum ImageContentFilterRole {
74    /// Model output.
75    Assistant,
76    /// Current user input.
77    User,
78    /// Conversation history.
79    History,
80}
81
82fn deserialize_optional_filter_level<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
83where
84    D: Deserializer<'de>,
85{
86    let level = Option::<i32>::deserialize(deserializer)?;
87    if level.is_some_and(|level| !(0..=3).contains(&level)) {
88        return Err(D::Error::custom(
89            "image content-filter level must be between 0 and 3",
90        ));
91    }
92    Ok(level)
93}
94
95// --- Getters ---
96impl ImageResponse {
97    /// Request created time, Unix timestamp (seconds).
98    pub fn created(&self) -> Option<u64> {
99        self.created
100    }
101    /// Generated image items (currently a single item).
102    pub fn data(&self) -> Option<&[ImageDataItem]> {
103        self.data.as_deref()
104    }
105    /// Content-safety filter results, if any.
106    pub fn content_filter(&self) -> Option<&[ImageContentFilterInfo]> {
107        self.content_filter.as_deref()
108    }
109}
110
111impl ImageDataItem {
112    /// Temporary image URL (~30 days validity).
113    pub fn url(&self) -> &str {
114        &self.url
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn response_requires_one_documented_non_null_field() {
124        assert!(serde_json::from_str::<ImageResponse>("{}").is_err());
125        assert!(serde_json::from_str::<ImageResponse>(r#"{"data":null}"#).is_err());
126        assert!(serde_json::from_str::<ImageResponse>(r#"{"data":[]}"#).is_ok());
127    }
128
129    #[test]
130    fn image_url_is_required_and_filter_constraints_are_enforced() {
131        assert!(serde_json::from_str::<ImageDataItem>("{}").is_err());
132        assert!(serde_json::from_str::<ImageDataItem>(r#"{"url":"relative"}"#).is_ok());
133        assert!(
134            serde_json::from_str::<ImageContentFilterInfo>(r#"{"role":"assistant","level":3}"#)
135                .is_ok()
136        );
137        assert!(serde_json::from_str::<ImageContentFilterInfo>(r#"{"level":4}"#).is_err());
138        assert!(serde_json::from_str::<ImageContentFilterInfo>(r#"{"role":"future"}"#).is_err());
139    }
140}