use serde::{Deserialize, Serialize};
use crate::{
ZaiResult,
client::{
ZaiClient,
error::{ZaiError, codes},
},
model::AsyncResponse,
};
fn validation_error(message: impl Into<String>) -> ZaiError {
ZaiError::ApiError {
code: codes::SDK_VALIDATION,
message: message.into(),
}
}
fn valid_glm_image_size(value: &str) -> bool {
if matches!(value, "1728x960" | "960x1728") {
return true;
}
let Some((width, height)) = value.split_once('x') else {
return false;
};
let (Ok(width), Ok(height)) = (width.parse::<u32>(), height.parse::<u32>()) else {
return false;
};
(1024..=2048).contains(&width)
&& (1024..=2048).contains(&height)
&& width.is_multiple_of(32)
&& height.is_multiple_of(32)
&& u64::from(width) * u64::from(height) <= 1 << 22
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
enum AsyncImageModel {
#[serde(rename = "glm-image")]
GlmImage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AsyncImageQuality {
#[serde(rename = "hd")]
Hd,
}
#[derive(Clone, Serialize)]
pub struct AsyncImageGenerationRequest {
model: AsyncImageModel,
prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
quality: Option<AsyncImageQuality>,
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
watermark_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
}
impl std::fmt::Debug for AsyncImageGenerationRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("AsyncImageGenerationRequest")
.field("model", &self.model())
.field("prompt", &"[REDACTED]")
.field("quality", &self.quality)
.field("size", &self.size)
.field("watermark_enabled", &self.watermark_enabled)
.field("user_id", &self.user_id.as_ref().map(|_| "[REDACTED]"))
.finish()
}
}
impl AsyncImageGenerationRequest {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
model: AsyncImageModel::GlmImage,
prompt: prompt.into(),
quality: None,
size: None,
watermark_enabled: None,
user_id: None,
}
}
pub fn with_quality(mut self, quality: AsyncImageQuality) -> Self {
self.quality = Some(quality);
self
}
pub fn with_size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn with_watermark(mut self, enabled: bool) -> Self {
self.watermark_enabled = Some(enabled);
self
}
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub const fn model(&self) -> &'static str {
"glm-image"
}
pub fn prompt(&self) -> &str {
&self.prompt
}
pub const fn quality(&self) -> Option<AsyncImageQuality> {
self.quality
}
pub fn size(&self) -> Option<&str> {
self.size.as_deref()
}
pub const fn watermark_enabled(&self) -> Option<bool> {
self.watermark_enabled
}
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
}
pub fn validate(&self) -> ZaiResult<()> {
if self.prompt.trim().is_empty() {
return Err(validation_error("async image prompt must not be blank"));
}
if self
.size
.as_deref()
.is_some_and(|size| !valid_glm_image_size(size))
{
return Err(validation_error("invalid async GLM-Image size"));
}
if self
.user_id
.as_ref()
.is_some_and(|value| !(6..=128).contains(&value.chars().count()))
{
return Err(validation_error(
"async image user_id must contain between 6 and 128 characters",
));
}
Ok(())
}
pub async fn send_via(&self, client: &ZaiClient) -> ZaiResult<AsyncResponse> {
self.validate()?;
let route = crate::client::routes::IMAGES_GENERATE_ASYNC;
let url = client.endpoints().resolve_route(route, &[])?;
let response = client
.send_json::<_, AsyncResponse>(route.method(), url, self)
.await?;
response.validate()?;
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_serializes_the_fixed_model_and_closed_fields() {
let request = AsyncImageGenerationRequest::new("a cat")
.with_quality(AsyncImageQuality::Hd)
.with_size("960x1728")
.with_watermark(true)
.with_user_id("user-1");
assert_eq!(
serde_json::to_value(request).unwrap(),
serde_json::json!({
"model": "glm-image",
"prompt": "a cat",
"quality": "hd",
"size": "960x1728",
"watermark_enabled": true,
"user_id": "user-1"
})
);
let debug = format!(
"{:?}",
AsyncImageGenerationRequest::new("private prompt").with_user_id("private-user")
);
assert!(!debug.contains("private prompt"));
assert!(!debug.contains("private-user"));
}
#[test]
fn validation_rejects_blank_prompts_and_invalid_documented_values() {
assert!(AsyncImageGenerationRequest::new(" ").validate().is_err());
assert!(
AsyncImageGenerationRequest::new("cat")
.with_size("1025x1024")
.validate()
.is_err()
);
assert!(
AsyncImageGenerationRequest::new("cat")
.with_user_id("short")
.validate()
.is_err()
);
}
}