Skip to main content

linger_openai_sdk/
videos.rs

1use crate::transport::{BodyStream, HttpRequest};
2use crate::LingerError;
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6
7/// EN: Video job metadata returned by the Videos API.
8/// 中文:Videos API 返回的视频任务元数据。
9#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct Video {
12    /// EN: Unique video job identifier.
13    /// 中文:唯一的视频任务标识符。
14    pub id: String,
15    /// EN: API object type, normally `video`.
16    /// 中文:API 对象类型,通常为 `video`。
17    pub object: String,
18    /// EN: Video generation model that produced the job.
19    /// 中文:生成该任务的视频模型。
20    pub model: String,
21    /// EN: Current lifecycle status for the video job.
22    /// 中文:视频任务当前生命周期状态。
23    pub status: String,
24    /// EN: Approximate completion percentage.
25    /// 中文:近似完成百分比。
26    pub progress: u32,
27    /// EN: Unix timestamp for when the job was created.
28    /// 中文:任务创建时间的 Unix 时间戳。
29    pub created_at: u64,
30    /// EN: Unix timestamp for completion, when finished.
31    /// 中文:任务完成时间的 Unix 时间戳,如已完成。
32    pub completed_at: Option<u64>,
33    /// EN: Unix timestamp for downloadable asset expiration, when set.
34    /// 中文:可下载资产过期时间的 Unix 时间戳,如已设置。
35    pub expires_at: Option<u64>,
36    /// EN: Prompt used to generate the video, when available.
37    /// 中文:用于生成视频的提示词,如可用。
38    pub prompt: Option<String>,
39    /// EN: Resolution of the generated video.
40    /// 中文:生成视频的分辨率。
41    pub size: String,
42    /// EN: Duration of the generated clip in seconds.
43    /// 中文:生成片段的秒数。
44    pub seconds: String,
45    /// EN: Source video id when this video is a remix.
46    /// 中文:当该视频为 remix 时的源视频 ID。
47    pub remixed_from_video_id: Option<String>,
48    /// EN: Error payload when video generation failed.
49    /// 中文:视频生成失败时的错误载荷。
50    pub error: Option<VideoError>,
51    /// EN: OpenAI request id from response headers.
52    /// 中文:响应头中的 OpenAI 请求 ID。
53    #[serde(skip)]
54    request_id: Option<RequestId>,
55}
56
57impl Video {
58    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
59        self.request_id = request_id;
60        self
61    }
62
63    /// EN: Returns the OpenAI request id, when present.
64    /// 中文:返回 OpenAI 请求 ID,如存在。
65    pub fn request_id(&self) -> Option<&RequestId> {
66        self.request_id.as_ref()
67    }
68}
69
70/// EN: Page of videos returned by `GET /v1/videos`.
71/// 中文:`GET /v1/videos` 返回的视频分页。
72#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
73#[non_exhaustive]
74pub struct VideoPage {
75    /// EN: API list object type.
76    /// 中文:API 列表对象类型。
77    pub object: String,
78    /// EN: Videos on this page.
79    /// 中文:本页视频。
80    #[serde(default)]
81    pub data: Vec<Video>,
82    /// EN: First video id on this page.
83    /// 中文:本页第一个视频 ID。
84    pub first_id: Option<String>,
85    /// EN: Last video id on this page.
86    /// 中文:本页最后一个视频 ID。
87    pub last_id: Option<String>,
88    /// EN: Whether more videos are available.
89    /// 中文:是否还有更多视频。
90    pub has_more: bool,
91    /// EN: OpenAI request id from response headers.
92    /// 中文:响应头中的 OpenAI 请求 ID。
93    #[serde(skip)]
94    request_id: Option<RequestId>,
95}
96
97impl VideoPage {
98    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
99        self.request_id = request_id;
100        self
101    }
102
103    /// EN: Returns the OpenAI request id, when present.
104    /// 中文:返回 OpenAI 请求 ID,如存在。
105    pub fn request_id(&self) -> Option<&RequestId> {
106        self.request_id.as_ref()
107    }
108}
109
110/// EN: JSON request body for `POST /v1/videos`.
111/// 中文:`POST /v1/videos` 的 JSON 请求体。
112#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
113#[non_exhaustive]
114pub struct CreateVideoRequest {
115    /// EN: Text prompt that describes the video to generate.
116    /// 中文:描述要生成视频的文本提示词。
117    pub prompt: String,
118    /// EN: Optional video generation model.
119    /// 中文:可选的视频生成模型。
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub model: Option<String>,
122    /// EN: Optional reference object that guides generation.
123    /// 中文:可选的参考对象,用于引导生成。
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub input_reference: Option<serde_json::Value>,
126    /// EN: Optional clip duration in seconds as a documented string value.
127    /// 中文:可选的视频秒数,使用文档中的字符串值。
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub seconds: Option<String>,
130    /// EN: Optional output resolution.
131    /// 中文:可选的输出分辨率。
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub size: Option<String>,
134}
135
136impl CreateVideoRequest {
137    /// EN: Returns a builder for video creation requests.
138    /// 中文:返回 video 创建请求的构建器。
139    pub fn builder() -> CreateVideoRequestBuilder {
140        CreateVideoRequestBuilder::default()
141    }
142}
143
144/// EN: Builder for `CreateVideoRequest`.
145/// 中文:`CreateVideoRequest` 的构建器。
146#[derive(Clone, Debug, Default)]
147#[non_exhaustive]
148pub struct CreateVideoRequestBuilder {
149    prompt: Option<String>,
150    model: Option<String>,
151    input_reference: Option<serde_json::Value>,
152    seconds: Option<String>,
153    size: Option<String>,
154}
155
156impl CreateVideoRequestBuilder {
157    /// EN: Sets the prompt for video generation.
158    /// 中文:设置视频生成提示词。
159    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
160        self.prompt = Some(prompt.into());
161        self
162    }
163
164    /// EN: Sets the optional video generation model.
165    /// 中文:设置可选的视频生成模型。
166    pub fn model(mut self, model: impl Into<String>) -> Self {
167        self.model = Some(model.into());
168        self
169    }
170
171    /// EN: Sets an optional reference object that guides generation.
172    /// 中文:设置可选的参考对象,用于引导生成。
173    pub fn input_reference(mut self, input_reference: serde_json::Value) -> Self {
174        self.input_reference = Some(input_reference);
175        self
176    }
177
178    /// EN: Sets the optional clip duration in seconds.
179    /// 中文:设置可选的视频秒数。
180    pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
181        self.seconds = Some(seconds.into());
182        self
183    }
184
185    /// EN: Sets the optional output resolution.
186    /// 中文:设置可选的输出分辨率。
187    pub fn size(mut self, size: impl Into<String>) -> Self {
188        self.size = Some(size.into());
189        self
190    }
191
192    /// EN: Builds a validated video creation request.
193    /// 中文:构建经过校验的视频创建请求。
194    pub fn build(self) -> Result<CreateVideoRequest, LingerError> {
195        let prompt = self
196            .prompt
197            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
198        if prompt.trim().is_empty() {
199            return Err(LingerError::invalid_config("prompt must not be empty"));
200        }
201        Ok(CreateVideoRequest {
202            prompt,
203            model: self.model,
204            input_reference: self.input_reference,
205            seconds: self.seconds,
206            size: self.size,
207        })
208    }
209}
210
211/// EN: Reference to a completed video used by JSON video APIs.
212/// 中文:JSON video API 使用的已完成视频引用。
213#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
214#[non_exhaustive]
215pub struct VideoReferenceInput {
216    /// EN: Completed video identifier.
217    /// 中文:已完成视频的标识符。
218    pub id: String,
219}
220
221impl VideoReferenceInput {
222    /// EN: Creates a validated video reference from an id.
223    /// 中文:根据 ID 创建经过校验的视频引用。
224    pub fn new(id: impl Into<String>) -> Result<Self, LingerError> {
225        let id = id.into();
226        let id = id.trim();
227        if id.is_empty() {
228            return Err(LingerError::invalid_config("video id is required"));
229        }
230        Ok(Self { id: id.to_owned() })
231    }
232}
233
234/// EN: JSON request body for `POST /v1/videos/edits`.
235/// 中文:`POST /v1/videos/edits` 的 JSON 请求体。
236#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
237#[non_exhaustive]
238pub struct CreateVideoEditRequest {
239    /// EN: Completed video to edit.
240    /// 中文:要编辑的已完成视频。
241    pub video: VideoReferenceInput,
242    /// EN: Text prompt describing how to edit the source video.
243    /// 中文:描述如何编辑源视频的文本提示词。
244    pub prompt: String,
245}
246
247impl CreateVideoEditRequest {
248    /// EN: Returns a builder for video edit requests.
249    /// 中文:返回 video edit 请求的构建器。
250    pub fn builder() -> CreateVideoEditRequestBuilder {
251        CreateVideoEditRequestBuilder::default()
252    }
253}
254
255/// EN: Builder for `CreateVideoEditRequest`.
256/// 中文:`CreateVideoEditRequest` 的构建器。
257#[derive(Clone, Debug, Default)]
258#[non_exhaustive]
259pub struct CreateVideoEditRequestBuilder {
260    video: Option<VideoReferenceInput>,
261    prompt: Option<String>,
262}
263
264impl CreateVideoEditRequestBuilder {
265    /// EN: Sets the completed video id to edit.
266    /// 中文:设置要编辑的已完成视频 ID。
267    pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
268        self.video = VideoReferenceInput::new(video_id).ok();
269        self
270    }
271
272    /// EN: Sets the prompt describing how to edit the source video.
273    /// 中文:设置描述如何编辑源视频的提示词。
274    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
275        self.prompt = Some(prompt.into());
276        self
277    }
278
279    /// EN: Builds a validated video edit request.
280    /// 中文:构建经过校验的视频编辑请求。
281    pub fn build(self) -> Result<CreateVideoEditRequest, LingerError> {
282        let video = self
283            .video
284            .ok_or_else(|| LingerError::invalid_config("video id is required"))?;
285        let prompt = self
286            .prompt
287            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
288        if prompt.trim().is_empty() {
289            return Err(LingerError::invalid_config("prompt must not be empty"));
290        }
291        Ok(CreateVideoEditRequest { video, prompt })
292    }
293}
294
295/// EN: JSON request body for `POST /v1/videos/extensions`.
296/// 中文:`POST /v1/videos/extensions` 的 JSON 请求体。
297#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
298#[non_exhaustive]
299pub struct CreateVideoExtensionRequest {
300    /// EN: Completed video to extend.
301    /// 中文:要扩展的已完成视频。
302    pub video: VideoReferenceInput,
303    /// EN: Updated prompt that directs the extension generation.
304    /// 中文:用于指导扩展生成的更新提示词。
305    pub prompt: String,
306    /// EN: Length of the newly generated extension segment in seconds.
307    /// 中文:新生成扩展片段的秒数。
308    pub seconds: String,
309}
310
311impl CreateVideoExtensionRequest {
312    /// EN: Returns a builder for video extension requests.
313    /// 中文:返回 video extension 请求的构建器。
314    pub fn builder() -> CreateVideoExtensionRequestBuilder {
315        CreateVideoExtensionRequestBuilder::default()
316    }
317}
318
319/// EN: Builder for `CreateVideoExtensionRequest`.
320/// 中文:`CreateVideoExtensionRequest` 的构建器。
321#[derive(Clone, Debug, Default)]
322#[non_exhaustive]
323pub struct CreateVideoExtensionRequestBuilder {
324    video: Option<VideoReferenceInput>,
325    prompt: Option<String>,
326    seconds: Option<String>,
327}
328
329impl CreateVideoExtensionRequestBuilder {
330    /// EN: Sets the completed video id to extend.
331    /// 中文:设置要扩展的已完成视频 ID。
332    pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
333        self.video = VideoReferenceInput::new(video_id).ok();
334        self
335    }
336
337    /// EN: Sets the prompt that directs the extension generation.
338    /// 中文:设置指导扩展生成的提示词。
339    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
340        self.prompt = Some(prompt.into());
341        self
342    }
343
344    /// EN: Sets the extension duration in seconds.
345    /// 中文:设置扩展片段的秒数。
346    pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
347        self.seconds = Some(seconds.into());
348        self
349    }
350
351    /// EN: Builds a validated video extension request.
352    /// 中文:构建经过校验的视频扩展请求。
353    pub fn build(self) -> Result<CreateVideoExtensionRequest, LingerError> {
354        let video = self
355            .video
356            .ok_or_else(|| LingerError::invalid_config("video id is required"))?;
357        let prompt = self
358            .prompt
359            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
360        if prompt.trim().is_empty() {
361            return Err(LingerError::invalid_config("prompt must not be empty"));
362        }
363        let seconds = self
364            .seconds
365            .ok_or_else(|| LingerError::invalid_config("seconds is required"))?;
366        if seconds.trim().is_empty() {
367            return Err(LingerError::invalid_config("seconds must not be empty"));
368        }
369        Ok(CreateVideoExtensionRequest {
370            video,
371            prompt,
372            seconds,
373        })
374    }
375}
376
377/// EN: Request body for `POST /v1/videos/{video_id}/remix`.
378/// 中文:`POST /v1/videos/{video_id}/remix` 的请求体。
379#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
380#[non_exhaustive]
381pub struct CreateVideoRemixRequest {
382    /// EN: Updated prompt that directs the remix generation.
383    /// 中文:用于指导 remix 生成的更新提示词。
384    pub prompt: String,
385}
386
387impl CreateVideoRemixRequest {
388    /// EN: Returns a builder for video remix requests.
389    /// 中文:返回 video remix 请求构建器。
390    pub fn builder() -> CreateVideoRemixRequestBuilder {
391        CreateVideoRemixRequestBuilder::default()
392    }
393}
394
395/// EN: Builder for `CreateVideoRemixRequest`.
396/// 中文:`CreateVideoRemixRequest` 的构建器。
397#[derive(Clone, Debug, Default)]
398pub struct CreateVideoRemixRequestBuilder {
399    prompt: Option<String>,
400}
401
402impl CreateVideoRemixRequestBuilder {
403    /// EN: Sets the updated prompt for the remix.
404    /// 中文:设置 remix 的更新提示词。
405    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
406        self.prompt = Some(prompt.into());
407        self
408    }
409
410    /// EN: Builds a validated video remix request.
411    /// 中文:构建经过校验的 video remix 请求。
412    pub fn build(self) -> Result<CreateVideoRemixRequest, LingerError> {
413        let prompt = self
414            .prompt
415            .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
416        if prompt.trim().is_empty() {
417            return Err(LingerError::invalid_config("prompt must not be empty"));
418        }
419        Ok(CreateVideoRemixRequest { prompt })
420    }
421}
422
423/// EN: Uploadable video file bytes and multipart metadata.
424/// 中文:可上传视频文件字节及 multipart 元数据。
425#[derive(Clone, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub struct VideoUpload {
428    /// EN: Filename sent in the multipart part.
429    /// 中文:multipart 分段中发送的文件名。
430    pub filename: String,
431    /// EN: Content type sent for the video part.
432    /// 中文:视频分段发送的内容类型。
433    pub content_type: String,
434    content: Bytes,
435}
436
437impl VideoUpload {
438    /// EN: Creates an upload from already available bytes without copying them.
439    /// 中文:通过已可用字节创建上传对象,不复制这些字节。
440    pub fn from_bytes(
441        filename: impl Into<String>,
442        content: impl Into<Bytes>,
443    ) -> Result<Self, LingerError> {
444        let filename = filename.into();
445        validate_header_param("filename", &filename)?;
446        Ok(Self {
447            filename,
448            content_type: "application/octet-stream".to_string(),
449            content: content.into(),
450        })
451    }
452
453    /// EN: Sets the video part content type.
454    /// 中文:设置视频分段的内容类型。
455    pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
456        let content_type = content_type.into();
457        validate_header_value("content_type", &content_type)?;
458        self.content_type = content_type;
459        Ok(self)
460    }
461
462    /// EN: Returns the video bytes as a cheap `Bytes` clone.
463    /// 中文:以廉价 `Bytes` 克隆返回视频字节。
464    pub fn bytes(&self) -> Bytes {
465        self.content.clone()
466    }
467}
468
469/// EN: Request body for `POST /v1/videos/characters`.
470/// 中文:`POST /v1/videos/characters` 的请求体。
471#[derive(Clone, Debug, PartialEq, Eq)]
472#[non_exhaustive]
473pub struct CreateVideoCharacterRequest {
474    /// EN: Display name for the created character.
475    /// 中文:创建后角色的显示名称。
476    pub name: String,
477    /// EN: Uploaded video used to create the character.
478    /// 中文:用于创建角色的上传视频。
479    pub video: VideoUpload,
480}
481
482impl CreateVideoCharacterRequest {
483    /// EN: Starts building a video character creation request.
484    /// 中文:开始构建 video character 创建请求。
485    pub fn builder() -> CreateVideoCharacterRequestBuilder {
486        CreateVideoCharacterRequestBuilder::default()
487    }
488
489    pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
490        let boundary = video_multipart_boundary(&self.name, &self.video.content);
491        request.insert_header(
492            "content-type",
493            format!("multipart/form-data; boundary={boundary}"),
494        );
495        request.set_body_stream(self.multipart_stream(boundary));
496    }
497
498    fn multipart_stream(
499        &self,
500        boundary: String,
501    ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
502        let mut chunks = Vec::new();
503        chunks.push(Ok(Bytes::from(format!(
504            "--{boundary}\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n{}\r\n",
505            self.name
506        ))));
507        chunks.push(Ok(Bytes::from(format!(
508            "--{boundary}\r\nContent-Disposition: form-data; name=\"video\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
509            escape_multipart_param(&self.video.filename),
510            self.video.content_type
511        ))));
512        chunks.push(Ok(self.video.content.clone()));
513        chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
514        futures_util::stream::iter(chunks)
515    }
516}
517
518/// EN: Builder for video character creation requests.
519/// 中文:video character 创建请求的构建器。
520#[derive(Clone, Debug, Default)]
521#[non_exhaustive]
522pub struct CreateVideoCharacterRequestBuilder {
523    name: Option<String>,
524    video: Option<VideoUpload>,
525}
526
527impl CreateVideoCharacterRequestBuilder {
528    /// EN: Sets the display name for the character.
529    /// 中文:设置角色显示名称。
530    pub fn name(mut self, name: impl Into<String>) -> Self {
531        self.name = Some(name.into());
532        self
533    }
534
535    /// EN: Sets the uploaded video used to create the character.
536    /// 中文:设置用于创建角色的上传视频。
537    pub fn video(mut self, video: VideoUpload) -> Self {
538        self.video = Some(video);
539        self
540    }
541
542    /// EN: Builds and validates the request.
543    /// 中文:构建并校验请求。
544    pub fn build(self) -> Result<CreateVideoCharacterRequest, LingerError> {
545        let name = self
546            .name
547            .filter(|value| !value.trim().is_empty())
548            .ok_or_else(|| LingerError::invalid_config("name is required"))?;
549        let video = self
550            .video
551            .ok_or_else(|| LingerError::invalid_config("video is required"))?;
552        Ok(CreateVideoCharacterRequest { name, video })
553    }
554}
555
556/// EN: Video character metadata returned by the Videos API.
557/// 中文:Videos API 返回的视频角色元数据。
558#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
559#[non_exhaustive]
560pub struct VideoCharacter {
561    /// EN: Character identifier, when available.
562    /// 中文:角色标识符,如可用。
563    pub id: Option<String>,
564    /// EN: Character display name, when available.
565    /// 中文:角色显示名称,如可用。
566    pub name: Option<String>,
567    /// EN: Unix timestamp for when the character was created.
568    /// 中文:角色创建时间的 Unix 时间戳。
569    pub created_at: u64,
570    /// EN: OpenAI request id from response headers.
571    /// 中文:响应头中的 OpenAI 请求 ID。
572    #[serde(skip)]
573    request_id: Option<RequestId>,
574}
575
576impl VideoCharacter {
577    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
578        self.request_id = request_id;
579        self
580    }
581
582    /// EN: Returns the OpenAI request id, when present.
583    /// 中文:返回 OpenAI 请求 ID,如存在。
584    pub fn request_id(&self) -> Option<&RequestId> {
585        self.request_id.as_ref()
586    }
587}
588
589/// EN: Error payload embedded in failed video jobs.
590/// 中文:失败视频任务中嵌入的错误载荷。
591#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
592#[non_exhaustive]
593pub struct VideoError {
594    /// EN: Machine-readable error code.
595    /// 中文:机器可读的错误代码。
596    pub code: String,
597    /// EN: Human-readable error message.
598    /// 中文:人类可读的错误消息。
599    pub message: String,
600}
601
602/// EN: Deletion result returned by `DELETE /v1/videos/{video_id}`.
603/// 中文:`DELETE /v1/videos/{video_id}` 返回的删除结果。
604#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
605#[non_exhaustive]
606pub struct VideoDeletion {
607    /// EN: API object type, normally `video.deleted`.
608    /// 中文:API 对象类型,通常为 `video.deleted`。
609    pub object: String,
610    /// EN: Whether the video was deleted.
611    /// 中文:视频是否已删除。
612    pub deleted: bool,
613    /// EN: Deleted video id.
614    /// 中文:已删除的视频 ID。
615    pub id: String,
616    /// EN: OpenAI request id from response headers.
617    /// 中文:响应头中的 OpenAI 请求 ID。
618    #[serde(skip)]
619    request_id: Option<RequestId>,
620}
621
622impl VideoDeletion {
623    pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
624        self.request_id = request_id;
625        self
626    }
627
628    /// EN: Returns the OpenAI request id, when present.
629    /// 中文:返回 OpenAI 请求 ID,如存在。
630    pub fn request_id(&self) -> Option<&RequestId> {
631        self.request_id.as_ref()
632    }
633}
634
635/// EN: Incremental video content response.
636/// 中文:增量视频内容响应。
637pub struct VideoContent {
638    request_id: Option<RequestId>,
639    body: BodyStream,
640}
641
642impl VideoContent {
643    pub(crate) fn new(request_id: Option<RequestId>, body: BodyStream) -> Self {
644        Self { request_id, body }
645    }
646
647    /// EN: Returns the OpenAI request id, when present.
648    /// 中文:返回 OpenAI 请求 ID,如存在。
649    pub fn request_id(&self) -> Option<&RequestId> {
650        self.request_id.as_ref()
651    }
652
653    /// EN: Consumes this response and returns the incremental content stream.
654    /// 中文:消耗此响应并返回增量内容流。
655    pub fn into_stream(self) -> BodyStream {
656        self.body
657    }
658}
659
660/// EN: Downloadable video content variant.
661/// 中文:可下载的视频内容变体。
662#[derive(Clone, Copy, Debug, PartialEq, Eq)]
663#[non_exhaustive]
664pub enum VideoContentVariant {
665    /// EN: Rendered MP4 video.
666    /// 中文:渲染后的 MP4 视频。
667    Video,
668    /// EN: Thumbnail preview image.
669    /// 中文:缩略图预览图片。
670    Thumbnail,
671    /// EN: Spritesheet preview image.
672    /// 中文:精灵图预览图片。
673    Spritesheet,
674}
675
676impl VideoContentVariant {
677    pub(crate) fn as_query_value(self) -> &'static str {
678        match self {
679            Self::Video => "video",
680            Self::Thumbnail => "thumbnail",
681            Self::Spritesheet => "spritesheet",
682        }
683    }
684}
685
686fn video_multipart_boundary(name: &str, content: &Bytes) -> String {
687    for counter in 0.. {
688        let boundary = format!("linger-openai-sdk-video-boundary-{counter}");
689        let boundary_bytes = boundary.as_bytes();
690        if !contains_bytes(name.as_bytes(), boundary_bytes)
691            && !contains_bytes(content, boundary_bytes)
692        {
693            return boundary;
694        }
695    }
696    unreachable!("unbounded boundary counter")
697}
698
699fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
700    if needle.is_empty() {
701        return true;
702    }
703    haystack
704        .windows(needle.len())
705        .any(|window| window == needle)
706}
707
708fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
709    if value.trim().is_empty() {
710        return Err(LingerError::invalid_config(format!("{name} is required")));
711    }
712    validate_header_value(name, value)
713}
714
715fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
716    if value.contains('\r') || value.contains('\n') {
717        return Err(LingerError::invalid_config(format!(
718            "{name} must not contain CR or LF"
719        )));
720    }
721    Ok(())
722}
723
724fn escape_multipart_param(value: &str) -> String {
725    value.replace('\\', "\\\\").replace('"', "\\\"")
726}