1use crate::common::auth::{AuthProvider, OpenAIAuth};
32use crate::common::client::create_http_client;
33use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
34use crate::videos::response::{DeleteVideoResponse, Video, VideoListResponse};
35use serde::{Deserialize, Serialize};
36use std::time::Duration;
37
38const VIDEOS_PATH: &str = "videos";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
43#[non_exhaustive]
44pub enum VideoModel {
45 #[serde(rename = "sora-2")]
47 #[default]
48 Sora2,
49 #[serde(rename = "sora-2-pro")]
51 Sora2Pro,
52}
53
54impl VideoModel {
55 pub fn as_str(&self) -> &'static str {
57 match self {
58 Self::Sora2 => "sora-2",
59 Self::Sora2Pro => "sora-2-pro",
60 }
61 }
62}
63
64impl std::fmt::Display for VideoModel {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.as_str())
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
72#[non_exhaustive]
73pub enum VideoSize {
74 #[serde(rename = "720x1280")]
76 #[default]
77 Size720x1280,
78 #[serde(rename = "1280x720")]
80 Size1280x720,
81 #[serde(rename = "1024x1792")]
83 Size1024x1792,
84 #[serde(rename = "1792x1024")]
86 Size1792x1024,
87}
88
89impl VideoSize {
90 pub fn as_str(&self) -> &'static str {
92 match self {
93 Self::Size720x1280 => "720x1280",
94 Self::Size1280x720 => "1280x720",
95 Self::Size1024x1792 => "1024x1792",
96 Self::Size1792x1024 => "1792x1024",
97 }
98 }
99}
100
101impl std::fmt::Display for VideoSize {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 write!(f, "{}", self.as_str())
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
112#[non_exhaustive]
113pub enum VideoSeconds {
114 #[serde(rename = "4")]
116 #[default]
117 Four,
118 #[serde(rename = "8")]
120 Eight,
121 #[serde(rename = "12")]
123 Twelve,
124}
125
126impl VideoSeconds {
127 pub fn as_str(&self) -> &'static str {
129 match self {
130 Self::Four => "4",
131 Self::Eight => "8",
132 Self::Twelve => "12",
133 }
134 }
135}
136
137impl std::fmt::Display for VideoSeconds {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "{}", self.as_str())
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
145#[serde(rename_all = "lowercase")]
146#[non_exhaustive]
147pub enum VideoVariant {
148 #[default]
150 Video,
151 Thumbnail,
153 Spritesheet,
155}
156
157impl VideoVariant {
158 pub fn as_str(&self) -> &'static str {
160 match self {
161 Self::Video => "video",
162 Self::Thumbnail => "thumbnail",
163 Self::Spritesheet => "spritesheet",
164 }
165 }
166}
167
168impl std::fmt::Display for VideoVariant {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 write!(f, "{}", self.as_str())
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
178pub struct InputReference {
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub file_id: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub image_url: Option<String>,
185}
186
187impl InputReference {
188 pub fn file_id<S: Into<String>>(file_id: S) -> Self {
190 Self { file_id: Some(file_id.into()), image_url: None }
191 }
192
193 pub fn image_url<S: Into<String>>(image_url: S) -> Self {
195 Self { file_id: None, image_url: Some(image_url.into()) }
196 }
197}
198
199#[derive(Debug, Clone, Default)]
204pub struct CreateVideoOptions {
205 pub model: Option<VideoModel>,
207 pub seconds: Option<VideoSeconds>,
209 pub size: Option<VideoSize>,
211 pub input_reference: Option<InputReference>,
213}
214
215impl CreateVideoOptions {
216 pub fn into_request<S: Into<String>>(self, prompt: S) -> CreateVideoRequest {
218 CreateVideoRequest { prompt: prompt.into(), model: self.model, seconds: self.seconds, size: self.size, input_reference: self.input_reference }
219 }
220}
221
222#[derive(Debug, Clone, Serialize)]
224pub struct CreateVideoRequest {
225 pub prompt: String,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub model: Option<VideoModel>,
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub seconds: Option<VideoSeconds>,
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub size: Option<VideoSize>,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub input_reference: Option<InputReference>,
239}
240
241#[derive(Debug, Clone, Serialize)]
243struct RemixVideoRequest {
244 prompt: String,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
249pub enum SortOrder {
250 Asc,
252 #[default]
254 Desc,
255}
256
257impl SortOrder {
258 pub fn as_str(&self) -> &'static str {
260 match self {
261 Self::Asc => "asc",
262 Self::Desc => "desc",
263 }
264 }
265}
266
267pub struct Videos {
290 auth: AuthProvider,
292 timeout: Option<Duration>,
294}
295
296impl Videos {
297 pub fn new() -> Result<Self> {
300 let auth = AuthProvider::openai_from_env()?;
301 Ok(Self { auth, timeout: None })
302 }
303
304 pub fn with_auth(auth: AuthProvider) -> Self {
306 Self { auth, timeout: None }
307 }
308
309 pub fn azure() -> Result<Self> {
311 let auth = AuthProvider::azure_from_env()?;
312 Ok(Self { auth, timeout: None })
313 }
314
315 pub fn detect_provider() -> Result<Self> {
317 let auth = AuthProvider::from_env()?;
318 Ok(Self { auth, timeout: None })
319 }
320
321 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
323 let auth = AuthProvider::from_url_with_key(base_url, api_key);
324 Self { auth, timeout: None }
325 }
326
327 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
329 let auth = AuthProvider::from_url(url)?;
330 Ok(Self { auth, timeout: None })
331 }
332
333 pub fn auth(&self) -> &AuthProvider {
335 &self.auth
336 }
337
338 pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
340 if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
341 let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
342 self.auth = AuthProvider::OpenAI(new_auth);
343 } else {
344 tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
345 }
346 self
347 }
348
349 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
355 self.timeout = Some(timeout);
356 self
357 }
358
359 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
361 let client = create_http_client(self.timeout)?;
362 let mut headers = request::header::HeaderMap::new();
363 self.auth.apply_headers(&mut headers)?;
364 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
365 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
366 Ok((client, headers))
367 }
368
369 async fn read_json<T: serde::de::DeserializeOwned>(response: request::Response) -> Result<T> {
371 let status = response.status();
372 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
373
374 if cfg!(test) {
375 tracing::info!("Response content: {}", content);
376 }
377
378 if !status.is_success() {
379 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
380 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
381 }
382 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
383 }
384
385 serde_json::from_str::<T>(&content).map_err(OpenAIToolError::SerdeJsonError)
386 }
387
388 pub async fn create<S: Into<String>>(&self, prompt: S, options: CreateVideoOptions) -> Result<Video> {
414 let (client, headers) = self.create_client()?;
415 let url = self.auth.endpoint(VIDEOS_PATH);
416 let body = serde_json::to_string(&options.into_request(prompt))?;
417
418 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
419
420 Self::read_json(response).await
421 }
422
423 pub async fn retrieve(&self, video_id: &str) -> Result<Video> {
429 let (client, headers) = self.create_client()?;
430 let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
431
432 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
433
434 Self::read_json(response).await
435 }
436
437 pub async fn list(&self, limit: Option<u32>, after: Option<&str>, order: Option<SortOrder>) -> Result<VideoListResponse> {
445 let (client, headers) = self.create_client()?;
446
447 let mut query: Vec<String> = Vec::new();
448 if let Some(limit) = limit {
449 query.push(format!("limit={}", limit));
450 }
451 if let Some(after) = after {
452 query.push(format!("after={}", after));
453 }
454 if let Some(order) = order {
455 query.push(format!("order={}", order.as_str()));
456 }
457
458 let endpoint = self.auth.endpoint(VIDEOS_PATH);
459 let url = if query.is_empty() { endpoint } else { format!("{}?{}", endpoint, query.join("&")) };
460
461 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
462
463 Self::read_json(response).await
464 }
465
466 pub async fn delete(&self, video_id: &str) -> Result<DeleteVideoResponse> {
472 let (client, headers) = self.create_client()?;
473 let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
474
475 let response = client.delete(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
476
477 Self::read_json(response).await
478 }
479
480 pub async fn content(&self, video_id: &str, variant: Option<VideoVariant>) -> Result<Vec<u8>> {
506 let (client, headers) = self.create_client()?;
507 let url = match variant {
508 Some(variant) => format!("{}/{}/content?variant={}", self.auth.endpoint(VIDEOS_PATH), video_id, variant.as_str()),
509 None => format!("{}/{}/content", self.auth.endpoint(VIDEOS_PATH), video_id),
510 };
511
512 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
513
514 let status = response.status();
515 if !status.is_success() {
516 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
517 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
518 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
519 }
520 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
521 }
522
523 let bytes = response.bytes().await.map_err(OpenAIToolError::RequestError)?;
524 Ok(bytes.to_vec())
525 }
526
527 pub async fn remix<S: Into<String>>(&self, video_id: &str, prompt: S) -> Result<Video> {
534 let (client, headers) = self.create_client()?;
535 let url = format!("{}/{}/remix", self.auth.endpoint(VIDEOS_PATH), video_id);
536 let body = serde_json::to_string(&RemixVideoRequest { prompt: prompt.into() })?;
537
538 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
539
540 Self::read_json(response).await
541 }
542}