Skip to main content

linger_openai_sdk/
images.rs

1use crate::error::LingerError;
2use crate::transport::HttpRequest;
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8
9/// EN: Request body for `POST /v1/images/generations`.
10/// 中文:`POST /v1/images/generations` 的请求体。
11#[derive(Clone, Debug, Serialize, PartialEq)]
12#[non_exhaustive]
13pub struct CreateImageRequest {
14    /// EN: Text prompt describing the image to generate.
15    /// 中文:描述要生成图像的文本提示。
16    pub prompt: String,
17    /// EN: Optional image generation model id.
18    /// 中文:可选的图像生成模型 ID。
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub model: Option<String>,
21    /// EN: Optional number of images to generate.
22    /// 中文:可选的生成图像数量。
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub n: Option<u32>,
25    /// EN: Optional background mode.
26    /// 中文:可选的背景模式。
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub background: Option<String>,
29    /// EN: Optional image moderation level.
30    /// 中文:可选的图像审核级别。
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub moderation: Option<String>,
33    /// EN: Optional output image format.
34    /// 中文:可选的输出图像格式。
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub output_format: Option<String>,
37    /// EN: Optional output compression percentage.
38    /// 中文:可选的输出压缩百分比。
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub output_compression: Option<u8>,
41    /// EN: Optional output quality.
42    /// 中文:可选的输出质量。
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub quality: Option<String>,
45    /// EN: Optional response format for supported models.
46    /// 中文:受支持模型的可选响应格式。
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub response_format: Option<String>,
49    /// EN: Optional generated image size.
50    /// 中文:可选的生成图像尺寸。
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub size: Option<String>,
53    /// EN: Optional style for supported models.
54    /// 中文:受支持模型的可选风格。
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub style: Option<String>,
57    /// EN: Optional end-user identifier.
58    /// 中文:可选的终端用户标识。
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub user: Option<String>,
61    /// EN: Forward-compatible optional fields not yet covered by handwritten types.
62    /// 中文:手写类型尚未覆盖的前向兼容可选字段。
63    #[serde(flatten)]
64    pub extra: BTreeMap<String, Value>,
65}
66
67impl CreateImageRequest {
68    /// EN: Starts building an image generation request.
69    /// 中文:开始构建图像生成请求。
70    pub fn builder() -> CreateImageRequestBuilder {
71        CreateImageRequestBuilder::default()
72    }
73}
74
75/// EN: Builder for image generation requests.
76/// 中文:图像生成请求的构建器。
77#[derive(Clone, Debug, Default)]
78#[non_exhaustive]
79pub struct CreateImageRequestBuilder {
80    prompt: Option<String>,
81    model: Option<String>,
82    n: Option<u32>,
83    background: Option<String>,
84    moderation: Option<String>,
85    output_format: Option<String>,
86    output_compression: Option<u8>,
87    quality: Option<String>,
88    response_format: Option<String>,
89    size: Option<String>,
90    style: Option<String>,
91    user: Option<String>,
92    extra: BTreeMap<String, Value>,
93}
94
95impl CreateImageRequestBuilder {
96    /// EN: Sets the image prompt.
97    /// 中文:设置图像提示。
98    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
99        self.prompt = Some(prompt.into());
100        self
101    }
102
103    /// EN: Sets the optional image model id.
104    /// 中文:设置可选的图像模型 ID。
105    pub fn model(mut self, model: impl Into<String>) -> Self {
106        self.model = Some(model.into());
107        self
108    }
109
110    /// EN: Sets the optional generated image count.
111    /// 中文:设置可选的生成图像数量。
112    pub fn n(mut self, n: u32) -> Self {
113        self.n = Some(n);
114        self
115    }
116
117    /// EN: Sets the optional background mode.
118    /// 中文:设置可选的背景模式。
119    pub fn background(mut self, background: impl Into<String>) -> Self {
120        self.background = Some(background.into());
121        self
122    }
123
124    /// EN: Sets the optional image moderation level.
125    /// 中文:设置可选的图像审核级别。
126    pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
127        self.moderation = Some(moderation.into());
128        self
129    }
130
131    /// EN: Sets the optional output image format.
132    /// 中文:设置可选的输出图像格式。
133    pub fn output_format(mut self, output_format: impl Into<String>) -> Self {
134        self.output_format = Some(output_format.into());
135        self
136    }
137
138    /// EN: Sets the optional output compression percentage.
139    /// 中文:设置可选的输出压缩百分比。
140    pub fn output_compression(mut self, output_compression: u8) -> Self {
141        self.output_compression = Some(output_compression);
142        self
143    }
144
145    /// EN: Sets the optional output quality.
146    /// 中文:设置可选的输出质量。
147    pub fn quality(mut self, quality: impl Into<String>) -> Self {
148        self.quality = Some(quality.into());
149        self
150    }
151
152    /// EN: Sets the optional response format.
153    /// 中文:设置可选的响应格式。
154    pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
155        self.response_format = Some(response_format.into());
156        self
157    }
158
159    /// EN: Sets the optional generated image size.
160    /// 中文:设置可选的生成图像尺寸。
161    pub fn size(mut self, size: impl Into<String>) -> Self {
162        self.size = Some(size.into());
163        self
164    }
165
166    /// EN: Sets the optional style.
167    /// 中文:设置可选的风格。
168    pub fn style(mut self, style: impl Into<String>) -> Self {
169        self.style = Some(style.into());
170        self
171    }
172
173    /// EN: Sets the optional end-user identifier.
174    /// 中文:设置可选的终端用户标识。
175    pub fn user(mut self, user: impl Into<String>) -> Self {
176        self.user = Some(user.into());
177        self
178    }
179
180    /// EN: Adds a forward-compatible JSON field.
181    /// 中文:添加前向兼容的 JSON 字段。
182    pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
183        self.extra.insert(name.into(), value);
184        self
185    }
186
187    /// EN: Builds and validates the request.
188    /// 中文:构建并校验请求。
189    pub fn build(self) -> Result<CreateImageRequest, LingerError> {
190        let prompt = self
191            .prompt
192            .filter(|value| !value.trim().is_empty())
193            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
194        validate_optional_string("model", self.model.as_deref())?;
195        validate_optional_string("background", self.background.as_deref())?;
196        validate_optional_string("moderation", self.moderation.as_deref())?;
197        validate_optional_string("output_format", self.output_format.as_deref())?;
198        validate_optional_string("quality", self.quality.as_deref())?;
199        validate_optional_string("response_format", self.response_format.as_deref())?;
200        validate_optional_string("size", self.size.as_deref())?;
201        validate_optional_string("style", self.style.as_deref())?;
202        validate_optional_string("user", self.user.as_deref())?;
203        if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
204            return Err(LingerError::invalid_config("n must be between 1 and 10"));
205        }
206        if self
207            .output_compression
208            .is_some_and(|output_compression| output_compression > 100)
209        {
210            return Err(LingerError::invalid_config(
211                "output_compression must be between 0 and 100",
212            ));
213        }
214        Ok(CreateImageRequest {
215            prompt,
216            model: self.model,
217            n: self.n,
218            background: self.background,
219            moderation: self.moderation,
220            output_format: self.output_format,
221            output_compression: self.output_compression,
222            quality: self.quality,
223            response_format: self.response_format,
224            size: self.size,
225            style: self.style,
226            user: self.user,
227            extra: self.extra,
228        })
229    }
230}
231
232/// EN: JSON image reference accepted by `POST /v1/images/edits`.
233/// 中文:`POST /v1/images/edits` 接受的 JSON 图像引用。
234#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
235#[serde(untagged)]
236pub enum ImageInput {
237    /// EN: References an uploaded OpenAI file id.
238    /// 中文:引用已上传的 OpenAI 文件 ID。
239    FileId {
240        /// EN: Uploaded file id.
241        /// 中文:已上传文件 ID。
242        file_id: String,
243    },
244    /// EN: References an image by URL.
245    /// 中文:通过 URL 引用图像。
246    ImageUrl {
247        /// EN: Image URL.
248        /// 中文:图像 URL。
249        image_url: String,
250    },
251}
252
253impl ImageInput {
254    /// EN: Creates an image input from an uploaded file id.
255    /// 中文:通过已上传文件 ID 创建图像输入。
256    pub fn file_id(file_id: impl Into<String>) -> Self {
257        Self::FileId {
258            file_id: file_id.into(),
259        }
260    }
261
262    /// EN: Creates an image input from an image URL.
263    /// 中文:通过图像 URL 创建图像输入。
264    pub fn image_url(image_url: impl Into<String>) -> Self {
265        Self::ImageUrl {
266            image_url: image_url.into(),
267        }
268    }
269
270    fn validate(&self) -> Result<(), LingerError> {
271        let (name, value) = match self {
272            Self::FileId { file_id } => ("file_id", file_id),
273            Self::ImageUrl { image_url } => ("image_url", image_url),
274        };
275        if value.trim().is_empty() {
276            return Err(LingerError::invalid_config(format!("{name} is required")));
277        }
278        Ok(())
279    }
280}
281
282/// EN: Request body for `POST /v1/images/edits`.
283/// 中文:`POST /v1/images/edits` 的请求体。
284#[derive(Clone, Debug, Serialize, PartialEq)]
285#[non_exhaustive]
286pub struct CreateImageEditRequest {
287    /// EN: Source images to edit.
288    /// 中文:要编辑的源图像。
289    pub images: Vec<ImageInput>,
290    /// EN: Text prompt describing the requested edit.
291    /// 中文:描述编辑要求的文本提示。
292    pub prompt: String,
293    /// EN: Optional image edit model id.
294    /// 中文:可选的图像编辑模型 ID。
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub model: Option<String>,
297    /// EN: Optional number of images to generate.
298    /// 中文:可选的生成图像数量。
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub n: Option<u32>,
301    /// EN: Optional output image size.
302    /// 中文:可选的输出图像尺寸。
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub size: Option<String>,
305    /// EN: Optional background mode.
306    /// 中文:可选的背景模式。
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub background: Option<String>,
309    /// EN: Optional input fidelity for matching source image style and features.
310    /// 中文:用于匹配源图像风格和特征的可选输入保真度。
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub input_fidelity: Option<String>,
313    /// EN: Optional image moderation level.
314    /// 中文:可选的图像审核级别。
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub moderation: Option<String>,
317    /// EN: Optional output quality.
318    /// 中文:可选的输出质量。
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub quality: Option<String>,
321    /// EN: Optional output compression percentage.
322    /// 中文:可选的输出压缩百分比。
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub output_compression: Option<u8>,
325    /// EN: Optional response format for supported models.
326    /// 中文:受支持模型的可选响应格式。
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub response_format: Option<String>,
329    /// EN: Optional end-user identifier.
330    /// 中文:可选的终端用户标识。
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub user: Option<String>,
333    /// EN: Forward-compatible optional fields not yet covered by handwritten types.
334    /// 中文:手写类型尚未覆盖的前向兼容可选字段。
335    #[serde(flatten)]
336    pub extra: BTreeMap<String, Value>,
337}
338
339impl CreateImageEditRequest {
340    /// EN: Starts building an image edit request.
341    /// 中文:开始构建图像编辑请求。
342    pub fn builder() -> CreateImageEditRequestBuilder {
343        CreateImageEditRequestBuilder::default()
344    }
345}
346
347/// EN: Builder for image edit requests.
348/// 中文:图像编辑请求的构建器。
349#[derive(Clone, Debug, Default)]
350#[non_exhaustive]
351pub struct CreateImageEditRequestBuilder {
352    images: Vec<ImageInput>,
353    prompt: Option<String>,
354    model: Option<String>,
355    n: Option<u32>,
356    size: Option<String>,
357    background: Option<String>,
358    input_fidelity: Option<String>,
359    moderation: Option<String>,
360    quality: Option<String>,
361    output_compression: Option<u8>,
362    response_format: Option<String>,
363    user: Option<String>,
364    extra: BTreeMap<String, Value>,
365}
366
367impl CreateImageEditRequestBuilder {
368    /// EN: Adds a source image reference.
369    /// 中文:添加一个源图像引用。
370    pub fn image(mut self, image: ImageInput) -> Self {
371        self.images.push(image);
372        self
373    }
374
375    /// EN: Replaces the source image list.
376    /// 中文:替换源图像列表。
377    pub fn images(mut self, images: impl IntoIterator<Item = ImageInput>) -> Self {
378        self.images = images.into_iter().collect();
379        self
380    }
381
382    /// EN: Sets the edit prompt.
383    /// 中文:设置编辑提示。
384    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
385        self.prompt = Some(prompt.into());
386        self
387    }
388
389    /// EN: Sets the optional image edit model id.
390    /// 中文:设置可选的图像编辑模型 ID。
391    pub fn model(mut self, model: impl Into<String>) -> Self {
392        self.model = Some(model.into());
393        self
394    }
395
396    /// EN: Sets the optional generated image count.
397    /// 中文:设置可选的生成图像数量。
398    pub fn n(mut self, n: u32) -> Self {
399        self.n = Some(n);
400        self
401    }
402
403    /// EN: Sets the optional output image size.
404    /// 中文:设置可选的输出图像尺寸。
405    pub fn size(mut self, size: impl Into<String>) -> Self {
406        self.size = Some(size.into());
407        self
408    }
409
410    /// EN: Sets the optional background mode.
411    /// 中文:设置可选的背景模式。
412    pub fn background(mut self, background: impl Into<String>) -> Self {
413        self.background = Some(background.into());
414        self
415    }
416
417    /// EN: Sets the optional input fidelity.
418    /// 中文:设置可选的输入保真度。
419    pub fn input_fidelity(mut self, input_fidelity: impl Into<String>) -> Self {
420        self.input_fidelity = Some(input_fidelity.into());
421        self
422    }
423
424    /// EN: Sets the optional image moderation level.
425    /// 中文:设置可选的图像审核级别。
426    pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
427        self.moderation = Some(moderation.into());
428        self
429    }
430
431    /// EN: Sets the optional output quality.
432    /// 中文:设置可选的输出质量。
433    pub fn quality(mut self, quality: impl Into<String>) -> Self {
434        self.quality = Some(quality.into());
435        self
436    }
437
438    /// EN: Sets the optional output compression percentage.
439    /// 中文:设置可选的输出压缩百分比。
440    pub fn output_compression(mut self, output_compression: u8) -> Self {
441        self.output_compression = Some(output_compression);
442        self
443    }
444
445    /// EN: Sets the optional response format.
446    /// 中文:设置可选的响应格式。
447    pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
448        self.response_format = Some(response_format.into());
449        self
450    }
451
452    /// EN: Sets the optional end-user identifier.
453    /// 中文:设置可选的终端用户标识。
454    pub fn user(mut self, user: impl Into<String>) -> Self {
455        self.user = Some(user.into());
456        self
457    }
458
459    /// EN: Adds a forward-compatible JSON field.
460    /// 中文:添加前向兼容的 JSON 字段。
461    pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
462        self.extra.insert(name.into(), value);
463        self
464    }
465
466    /// EN: Builds and validates the request.
467    /// 中文:构建并校验请求。
468    pub fn build(self) -> Result<CreateImageEditRequest, LingerError> {
469        if self.images.is_empty() {
470            return Err(LingerError::invalid_config("images is required"));
471        }
472        for image in &self.images {
473            image.validate()?;
474        }
475        let prompt = self
476            .prompt
477            .filter(|value| !value.trim().is_empty())
478            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
479        validate_optional_string("model", self.model.as_deref())?;
480        validate_optional_string("size", self.size.as_deref())?;
481        validate_optional_string("background", self.background.as_deref())?;
482        validate_optional_string("input_fidelity", self.input_fidelity.as_deref())?;
483        validate_optional_string("moderation", self.moderation.as_deref())?;
484        validate_optional_string("quality", self.quality.as_deref())?;
485        validate_optional_string("response_format", self.response_format.as_deref())?;
486        validate_optional_string("user", self.user.as_deref())?;
487        if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
488            return Err(LingerError::invalid_config("n must be between 1 and 10"));
489        }
490        if self
491            .output_compression
492            .is_some_and(|output_compression| output_compression > 100)
493        {
494            return Err(LingerError::invalid_config(
495                "output_compression must be between 0 and 100",
496            ));
497        }
498        Ok(CreateImageEditRequest {
499            images: self.images,
500            prompt,
501            model: self.model,
502            n: self.n,
503            size: self.size,
504            background: self.background,
505            input_fidelity: self.input_fidelity,
506            moderation: self.moderation,
507            quality: self.quality,
508            output_compression: self.output_compression,
509            response_format: self.response_format,
510            user: self.user,
511            extra: self.extra,
512        })
513    }
514}
515
516/// EN: Uploadable image file bytes and multipart metadata.
517/// 中文:可上传图像文件字节及 multipart 元数据。
518#[derive(Clone, Debug, PartialEq, Eq)]
519#[non_exhaustive]
520pub struct ImageUpload {
521    /// EN: Filename sent in the multipart part.
522    /// 中文:multipart 分段中发送的文件名。
523    pub filename: String,
524    /// EN: Content type sent for the image part.
525    /// 中文:图像分段发送的内容类型。
526    pub content_type: String,
527    content: Bytes,
528}
529
530impl ImageUpload {
531    /// EN: Creates an upload from already available bytes without copying them.
532    /// 中文:通过已可用字节创建上传对象,不复制这些字节。
533    pub fn from_bytes(
534        filename: impl Into<String>,
535        content: impl Into<Bytes>,
536    ) -> Result<Self, LingerError> {
537        let filename = filename.into();
538        validate_header_param("filename", &filename)?;
539        Ok(Self {
540            filename,
541            content_type: "application/octet-stream".to_string(),
542            content: content.into(),
543        })
544    }
545
546    /// EN: Sets the image part content type.
547    /// 中文:设置图像分段的内容类型。
548    pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
549        let content_type = content_type.into();
550        validate_header_value("content_type", &content_type)?;
551        self.content_type = content_type;
552        Ok(self)
553    }
554
555    /// EN: Returns the image bytes as a cheap `Bytes` clone.
556    /// 中文:以廉价 `Bytes` 克隆返回图像字节。
557    pub fn bytes(&self) -> Bytes {
558        self.content.clone()
559    }
560}
561
562/// EN: Request body descriptor for `POST /v1/images/variations`.
563/// 中文:`POST /v1/images/variations` 的请求体描述。
564#[derive(Clone, Debug, PartialEq)]
565#[non_exhaustive]
566pub struct CreateImageVariationRequest {
567    /// EN: Source image to vary.
568    /// 中文:要生成变体的源图像。
569    pub image: ImageUpload,
570    /// EN: Optional image variation model id.
571    /// 中文:可选的图像变体模型 ID。
572    pub model: Option<String>,
573    /// EN: Optional number of images to generate.
574    /// 中文:可选的生成图像数量。
575    pub n: Option<u32>,
576    /// EN: Optional response format.
577    /// 中文:可选的响应格式。
578    pub response_format: Option<String>,
579    /// EN: Optional generated image size.
580    /// 中文:可选的生成图像尺寸。
581    pub size: Option<String>,
582    /// EN: Optional end-user identifier.
583    /// 中文:可选的终端用户标识。
584    pub user: Option<String>,
585}
586
587impl CreateImageVariationRequest {
588    /// EN: Starts building an image variation request.
589    /// 中文:开始构建图像变体请求。
590    pub fn builder() -> CreateImageVariationRequestBuilder {
591        CreateImageVariationRequestBuilder::default()
592    }
593
594    pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
595        let boundary = multipart_boundary(&self.image.content);
596        request.insert_header(
597            "content-type",
598            format!("multipart/form-data; boundary={boundary}"),
599        );
600        request.set_body_stream(self.multipart_stream(boundary));
601    }
602
603    fn multipart_stream(
604        &self,
605        boundary: String,
606    ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
607        let mut chunks = Vec::new();
608        push_optional_text_field(&mut chunks, &boundary, "model", self.model.as_deref());
609        push_optional_text_field(&mut chunks, &boundary, "size", self.size.as_deref());
610        push_optional_text_field(
611            &mut chunks,
612            &boundary,
613            "response_format",
614            self.response_format.as_deref(),
615        );
616        push_optional_text_field(&mut chunks, &boundary, "user", self.user.as_deref());
617        if let Some(n) = self.n {
618            push_text_field(&mut chunks, &boundary, "n", &n.to_string());
619        }
620        chunks.push(Ok(Bytes::from(format!(
621            "--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
622            escape_multipart_param(&self.image.filename),
623            self.image.content_type
624        ))));
625        chunks.push(Ok(self.image.content.clone()));
626        chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
627        futures_util::stream::iter(chunks)
628    }
629}
630
631/// EN: Builder for image variation requests.
632/// 中文:图像变体请求的构建器。
633#[derive(Clone, Debug, Default)]
634#[non_exhaustive]
635pub struct CreateImageVariationRequestBuilder {
636    image: Option<ImageUpload>,
637    model: Option<String>,
638    n: Option<u32>,
639    response_format: Option<String>,
640    size: Option<String>,
641    user: Option<String>,
642}
643
644impl CreateImageVariationRequestBuilder {
645    /// EN: Sets the image to vary.
646    /// 中文:设置要生成变体的图像。
647    pub fn image(mut self, image: ImageUpload) -> Self {
648        self.image = Some(image);
649        self
650    }
651
652    /// EN: Sets the optional variation model id.
653    /// 中文:设置可选的图像变体模型 ID。
654    pub fn model(mut self, model: impl Into<String>) -> Self {
655        self.model = Some(model.into());
656        self
657    }
658
659    /// EN: Sets the optional generated image count.
660    /// 中文:设置可选的生成图像数量。
661    pub fn n(mut self, n: u32) -> Self {
662        self.n = Some(n);
663        self
664    }
665
666    /// EN: Sets the optional response format.
667    /// 中文:设置可选的响应格式。
668    pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
669        self.response_format = Some(response_format.into());
670        self
671    }
672
673    /// EN: Sets the optional generated image size.
674    /// 中文:设置可选的生成图像尺寸。
675    pub fn size(mut self, size: impl Into<String>) -> Self {
676        self.size = Some(size.into());
677        self
678    }
679
680    /// EN: Sets the optional end-user identifier.
681    /// 中文:设置可选的终端用户标识。
682    pub fn user(mut self, user: impl Into<String>) -> Self {
683        self.user = Some(user.into());
684        self
685    }
686
687    /// EN: Builds and validates the request.
688    /// 中文:构建并校验请求。
689    pub fn build(self) -> Result<CreateImageVariationRequest, LingerError> {
690        let image = self
691            .image
692            .ok_or_else(|| LingerError::invalid_config("image is required"))?;
693        validate_optional_string("model", self.model.as_deref())?;
694        validate_optional_string("response_format", self.response_format.as_deref())?;
695        validate_optional_string("size", self.size.as_deref())?;
696        validate_optional_string("user", self.user.as_deref())?;
697        if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
698            return Err(LingerError::invalid_config("n must be between 1 and 10"));
699        }
700        Ok(CreateImageVariationRequest {
701            image,
702            model: self.model,
703            n: self.n,
704            response_format: self.response_format,
705            size: self.size,
706            user: self.user,
707        })
708    }
709}
710
711/// EN: Response object returned by the Images API.
712/// 中文:Images API 返回的响应对象。
713#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
714#[non_exhaustive]
715pub struct ImagesResponse {
716    /// EN: Unix timestamp for response creation.
717    /// 中文:响应创建时间的 Unix 时间戳。
718    pub created: u64,
719    /// EN: Generated image items.
720    /// 中文:生成的图像项。
721    #[serde(default)]
722    pub data: Vec<Image>,
723    /// EN: Background mode returned by the API.
724    /// 中文:API 返回的背景模式。
725    #[serde(default)]
726    pub background: Option<String>,
727    /// EN: Output image format returned by the API.
728    /// 中文:API 返回的输出图像格式。
729    #[serde(default)]
730    pub output_format: Option<String>,
731    /// EN: Output quality returned by the API.
732    /// 中文:API 返回的输出质量。
733    #[serde(default)]
734    pub quality: Option<String>,
735    /// EN: Image size returned by the API.
736    /// 中文:API 返回的图像尺寸。
737    #[serde(default)]
738    pub size: Option<String>,
739    /// EN: Token usage, when returned.
740    /// 中文:token 用量,如响应中存在。
741    #[serde(default)]
742    pub usage: Option<ImageUsage>,
743    /// EN: Additional fields preserved for forward compatibility.
744    /// 中文:为前向兼容保留的额外字段。
745    #[serde(flatten)]
746    pub extra: BTreeMap<String, Value>,
747    /// EN: OpenAI request id from response headers.
748    /// 中文:响应头中的 OpenAI 请求 ID。
749    #[serde(skip)]
750    request_id: Option<RequestId>,
751}
752
753impl ImagesResponse {
754    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
755        self.request_id = request_id;
756        self
757    }
758
759    /// EN: Returns the OpenAI request id, when present.
760    /// 中文:返回 OpenAI 请求 ID,如存在。
761    pub fn request_id(&self) -> Option<&RequestId> {
762        self.request_id.as_ref()
763    }
764}
765
766/// EN: Single image item returned by the Images API.
767/// 中文:Images API 返回的单个图像项。
768#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
769#[non_exhaustive]
770pub struct Image {
771    /// EN: Image URL, when returned.
772    /// 中文:图像 URL,如响应中存在。
773    #[serde(default)]
774    pub url: Option<String>,
775    /// EN: Base64 encoded image, when returned.
776    /// 中文:Base64 编码图像,如响应中存在。
777    #[serde(default)]
778    pub b64_json: Option<String>,
779    /// EN: Revised prompt, when returned.
780    /// 中文:修订后的提示,如响应中存在。
781    #[serde(default)]
782    pub revised_prompt: Option<String>,
783    /// EN: Additional fields preserved for forward compatibility.
784    /// 中文:为前向兼容保留的额外字段。
785    #[serde(flatten)]
786    pub extra: BTreeMap<String, Value>,
787}
788
789/// EN: Image generation usage details.
790/// 中文:图像生成用量详情。
791#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
792#[non_exhaustive]
793pub struct ImageUsage {
794    /// EN: Input token count.
795    /// 中文:输入 token 数。
796    pub input_tokens: u64,
797    /// EN: Output token count.
798    /// 中文:输出 token 数。
799    pub output_tokens: u64,
800    /// EN: Total token count.
801    /// 中文:总 token 数。
802    pub total_tokens: u64,
803    /// EN: Input token details.
804    /// 中文:输入 token 详情。
805    #[serde(default)]
806    pub input_tokens_details: ImageTokenDetails,
807    /// EN: Output token details.
808    /// 中文:输出 token 详情。
809    #[serde(default)]
810    pub output_tokens_details: ImageTokenDetails,
811}
812
813/// EN: Token details for image generation usage.
814/// 中文:图像生成用量的 token 详情。
815#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
816#[non_exhaustive]
817pub struct ImageTokenDetails {
818    /// EN: Text token count, when returned.
819    /// 中文:文本 token 数,如响应中存在。
820    #[serde(default)]
821    pub text_tokens: Option<u64>,
822    /// EN: Image token count, when returned.
823    /// 中文:图像 token 数,如响应中存在。
824    #[serde(default)]
825    pub image_tokens: Option<u64>,
826}
827
828fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
829    if value.is_some_and(|value| value.trim().is_empty()) {
830        return Err(LingerError::invalid_config(format!(
831            "{name} must not be empty"
832        )));
833    }
834    Ok(())
835}
836
837fn push_text_field(
838    chunks: &mut Vec<Result<Bytes, LingerError>>,
839    boundary: &str,
840    name: &str,
841    value: &str,
842) {
843    chunks.push(Ok(Bytes::from(format!(
844        "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
845    ))));
846}
847
848fn push_optional_text_field(
849    chunks: &mut Vec<Result<Bytes, LingerError>>,
850    boundary: &str,
851    name: &str,
852    value: Option<&str>,
853) {
854    if let Some(value) = value {
855        push_text_field(chunks, boundary, name, value);
856    }
857}
858
859fn multipart_boundary(content: &Bytes) -> String {
860    for counter in 0.. {
861        let boundary = format!("linger-openai-sdk-image-boundary-{counter}");
862        if !contains_bytes(content, boundary.as_bytes()) {
863            return boundary;
864        }
865    }
866    unreachable!("unbounded boundary counter")
867}
868
869fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
870    if needle.is_empty() {
871        return true;
872    }
873    haystack
874        .windows(needle.len())
875        .any(|window| window == needle)
876}
877
878fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
879    if value.trim().is_empty() {
880        return Err(LingerError::invalid_config(format!("{name} is required")));
881    }
882    validate_header_value(name, value)
883}
884
885fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
886    if value.contains('\r') || value.contains('\n') {
887        return Err(LingerError::invalid_config(format!(
888            "{name} must not contain CR or LF"
889        )));
890    }
891    Ok(())
892}
893
894fn escape_multipart_param(value: &str) -> String {
895    value.replace('\\', "\\\\").replace('"', "\\\"")
896}