1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Image generation API client.
use std::sync::Arc;
use reqwest::Method;
use crate::{
client::ClientInner,
errors::{Result, ValidationError},
generated::{ImagePinResponse, ImageRequest, ImageResponse},
http::HeaderList,
Error,
};
/// Client for image generation operations.
///
/// # Example
///
/// ```rust,ignore
/// use modelrelay::{Client, ImageRequest, ImageResponseFormat};
///
/// // Production use (default) - returns URLs
/// let response = client.images().generate(ImageRequest {
/// model: "gemini-2.5-flash-image".into(),
/// prompt: "A futuristic cityscape".into(),
/// response_format: None, // defaults to URL
/// }).await?;
///
/// println!("{}", response.data[0].url.as_deref().unwrap_or_default());
/// println!("{}", response.data[0].mime_type.as_deref().unwrap_or_default());
///
/// // Testing/development - returns base64
/// let response = client.images().generate(ImageRequest {
/// model: "gemini-2.5-flash-image".into(),
/// prompt: "A futuristic cityscape".into(),
/// response_format: Some(ImageResponseFormat::B64Json),
/// }).await?;
/// ```
#[derive(Clone)]
pub struct ImagesClient {
pub(crate) inner: Arc<ClientInner>,
}
impl ImagesClient {
/// Generate images from a text prompt.
///
/// By default, returns URLs (requires storage configuration on the server).
/// Use `response_format: Some(ImageResponseFormat::B64Json)` for testing without storage.
///
/// Model is optional when using a customer token with a tier that defines a default model.
pub async fn generate(&self, req: ImageRequest) -> Result<ImageResponse> {
if req.prompt.trim().is_empty() {
return Err(Error::Validation(
ValidationError::new("prompt is required").with_field("prompt"),
));
}
let path = "/images/generate";
let mut builder = self.inner.request(Method::POST, path)?;
builder = builder.json(&req);
let builder = self.inner.with_headers(
builder,
None,
&HeaderList::default(),
Some("application/json"),
)?;
let builder = self.inner.with_timeout(builder, None, true);
let ctx = self.inner.make_context(&Method::POST, path, None, None);
let resp: ImageResponse = self
.inner
.execute_json(builder, Method::POST, None, ctx)
.await?;
Ok(resp)
}
/// Get information about a specific image.
///
/// Returns the image's pinned status, expiration time, and URL.
///
/// # Example
///
/// ```rust,ignore
/// let info = client.images().get("img_abc123").await?;
/// println!("Pinned: {}", info.pinned);
/// if let Some(expires) = &info.expires_at {
/// println!("Expires: {}", expires);
/// }
/// ```
pub async fn get(&self, image_id: &str) -> Result<ImagePinResponse> {
if image_id.trim().is_empty() {
return Err(Error::Validation(
ValidationError::new("image_id is required").with_field("image_id"),
));
}
let path = format!("/images/{}", image_id);
let builder = self.inner.request(Method::GET, &path)?;
let builder = self
.inner
.with_headers(builder, None, &HeaderList::default(), None)?;
let builder = self.inner.with_timeout(builder, None, true);
let ctx = self.inner.make_context(&Method::GET, &path, None, None);
let resp: ImagePinResponse = self
.inner
.execute_json(builder, Method::GET, None, ctx)
.await?;
Ok(resp)
}
/// Pin an image to prevent it from expiring.
///
/// Pinned images remain accessible permanently (subject to tier limits).
/// Returns the updated image state including its permanent URL.
///
/// # Example
///
/// ```rust,ignore
/// let pinned = client.images().pin("img_abc123").await?;
/// assert!(pinned.pinned);
/// assert!(pinned.expires_at.is_none()); // No expiration when pinned
/// ```
pub async fn pin(&self, image_id: &str) -> Result<ImagePinResponse> {
if image_id.trim().is_empty() {
return Err(Error::Validation(
ValidationError::new("image_id is required").with_field("image_id"),
));
}
let path = format!("/images/{}/pin", image_id);
let builder = self.inner.request(Method::POST, &path)?;
let builder = self
.inner
.with_headers(builder, None, &HeaderList::default(), None)?;
let builder = self.inner.with_timeout(builder, None, true);
let ctx = self.inner.make_context(&Method::POST, &path, None, None);
let resp: ImagePinResponse = self
.inner
.execute_json(builder, Method::POST, None, ctx)
.await?;
Ok(resp)
}
/// Unpin an image, allowing it to expire.
///
/// The image will be set to expire after the default ephemeral period (7 days).
/// Returns the updated image state including the new expiration time.
///
/// # Example
///
/// ```rust,ignore
/// let unpinned = client.images().unpin("img_abc123").await?;
/// assert!(!unpinned.pinned);
/// assert!(unpinned.expires_at.is_some()); // Will expire in 7 days
/// ```
pub async fn unpin(&self, image_id: &str) -> Result<ImagePinResponse> {
if image_id.trim().is_empty() {
return Err(Error::Validation(
ValidationError::new("image_id is required").with_field("image_id"),
));
}
let path = format!("/images/{}/pin", image_id);
let builder = self.inner.request(Method::DELETE, &path)?;
let builder = self
.inner
.with_headers(builder, None, &HeaderList::default(), None)?;
let builder = self.inner.with_timeout(builder, None, true);
let ctx = self.inner.make_context(&Method::DELETE, &path, None, None);
let resp: ImagePinResponse = self
.inner
.execute_json(builder, Method::DELETE, None, ctx)
.await?;
Ok(resp)
}
}