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)]
43pub enum VideoModel {
44 #[serde(rename = "sora-2")]
46 #[default]
47 Sora2,
48 #[serde(rename = "sora-2-pro")]
50 Sora2Pro,
51}
52
53impl VideoModel {
54 pub fn as_str(&self) -> &'static str {
56 match self {
57 Self::Sora2 => "sora-2",
58 Self::Sora2Pro => "sora-2-pro",
59 }
60 }
61}
62
63impl std::fmt::Display for VideoModel {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(f, "{}", self.as_str())
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
71pub enum VideoSize {
72 #[serde(rename = "720x1280")]
74 #[default]
75 Size720x1280,
76 #[serde(rename = "1280x720")]
78 Size1280x720,
79 #[serde(rename = "1024x1792")]
81 Size1024x1792,
82 #[serde(rename = "1792x1024")]
84 Size1792x1024,
85}
86
87impl VideoSize {
88 pub fn as_str(&self) -> &'static str {
90 match self {
91 Self::Size720x1280 => "720x1280",
92 Self::Size1280x720 => "1280x720",
93 Self::Size1024x1792 => "1024x1792",
94 Self::Size1792x1024 => "1792x1024",
95 }
96 }
97}
98
99impl std::fmt::Display for VideoSize {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 write!(f, "{}", self.as_str())
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
110pub enum VideoSeconds {
111 #[serde(rename = "4")]
113 #[default]
114 Four,
115 #[serde(rename = "8")]
117 Eight,
118 #[serde(rename = "12")]
120 Twelve,
121}
122
123impl VideoSeconds {
124 pub fn as_str(&self) -> &'static str {
126 match self {
127 Self::Four => "4",
128 Self::Eight => "8",
129 Self::Twelve => "12",
130 }
131 }
132}
133
134impl std::fmt::Display for VideoSeconds {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(f, "{}", self.as_str())
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
142#[serde(rename_all = "lowercase")]
143pub enum VideoVariant {
144 #[default]
146 Video,
147 Thumbnail,
149 Spritesheet,
151}
152
153impl VideoVariant {
154 pub fn as_str(&self) -> &'static str {
156 match self {
157 Self::Video => "video",
158 Self::Thumbnail => "thumbnail",
159 Self::Spritesheet => "spritesheet",
160 }
161 }
162}
163
164impl std::fmt::Display for VideoVariant {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 write!(f, "{}", self.as_str())
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
174pub struct InputReference {
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub file_id: Option<String>,
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub image_url: Option<String>,
181}
182
183impl InputReference {
184 pub fn file_id<S: Into<String>>(file_id: S) -> Self {
186 Self { file_id: Some(file_id.into()), image_url: None }
187 }
188
189 pub fn image_url<S: Into<String>>(image_url: S) -> Self {
191 Self { file_id: None, image_url: Some(image_url.into()) }
192 }
193}
194
195#[derive(Debug, Clone, Default)]
200pub struct CreateVideoOptions {
201 pub model: Option<VideoModel>,
203 pub seconds: Option<VideoSeconds>,
205 pub size: Option<VideoSize>,
207 pub input_reference: Option<InputReference>,
209}
210
211impl CreateVideoOptions {
212 pub fn into_request<S: Into<String>>(self, prompt: S) -> CreateVideoRequest {
214 CreateVideoRequest { prompt: prompt.into(), model: self.model, seconds: self.seconds, size: self.size, input_reference: self.input_reference }
215 }
216}
217
218#[derive(Debug, Clone, Serialize)]
220pub struct CreateVideoRequest {
221 pub prompt: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub model: Option<VideoModel>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub seconds: Option<VideoSeconds>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub size: Option<VideoSize>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub input_reference: Option<InputReference>,
235}
236
237#[derive(Debug, Clone, Serialize)]
239struct RemixVideoRequest {
240 prompt: String,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub enum SortOrder {
246 Asc,
248 #[default]
250 Desc,
251}
252
253impl SortOrder {
254 pub fn as_str(&self) -> &'static str {
256 match self {
257 Self::Asc => "asc",
258 Self::Desc => "desc",
259 }
260 }
261}
262
263pub struct Videos {
286 auth: AuthProvider,
288 timeout: Option<Duration>,
290}
291
292impl Videos {
293 pub fn new() -> Result<Self> {
296 let auth = AuthProvider::openai_from_env()?;
297 Ok(Self { auth, timeout: None })
298 }
299
300 pub fn with_auth(auth: AuthProvider) -> Self {
302 Self { auth, timeout: None }
303 }
304
305 pub fn azure() -> Result<Self> {
307 let auth = AuthProvider::azure_from_env()?;
308 Ok(Self { auth, timeout: None })
309 }
310
311 pub fn detect_provider() -> Result<Self> {
313 let auth = AuthProvider::from_env()?;
314 Ok(Self { auth, timeout: None })
315 }
316
317 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
319 let auth = AuthProvider::from_url_with_key(base_url, api_key);
320 Self { auth, timeout: None }
321 }
322
323 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
325 let auth = AuthProvider::from_url(url)?;
326 Ok(Self { auth, timeout: None })
327 }
328
329 pub fn auth(&self) -> &AuthProvider {
331 &self.auth
332 }
333
334 pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
336 if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
337 let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
338 self.auth = AuthProvider::OpenAI(new_auth);
339 } else {
340 tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
341 }
342 self
343 }
344
345 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
351 self.timeout = Some(timeout);
352 self
353 }
354
355 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
357 let client = create_http_client(self.timeout)?;
358 let mut headers = request::header::HeaderMap::new();
359 self.auth.apply_headers(&mut headers)?;
360 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
361 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
362 Ok((client, headers))
363 }
364
365 async fn read_json<T: serde::de::DeserializeOwned>(response: request::Response) -> Result<T> {
367 let status = response.status();
368 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
369
370 if cfg!(test) {
371 tracing::info!("Response content: {}", content);
372 }
373
374 if !status.is_success() {
375 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
376 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
377 }
378 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
379 }
380
381 serde_json::from_str::<T>(&content).map_err(OpenAIToolError::SerdeJsonError)
382 }
383
384 pub async fn create<S: Into<String>>(&self, prompt: S, options: CreateVideoOptions) -> Result<Video> {
410 let (client, headers) = self.create_client()?;
411 let url = self.auth.endpoint(VIDEOS_PATH);
412 let body = serde_json::to_string(&options.into_request(prompt))?;
413
414 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
415
416 Self::read_json(response).await
417 }
418
419 pub async fn retrieve(&self, video_id: &str) -> Result<Video> {
425 let (client, headers) = self.create_client()?;
426 let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
427
428 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
429
430 Self::read_json(response).await
431 }
432
433 pub async fn list(&self, limit: Option<u32>, after: Option<&str>, order: Option<SortOrder>) -> Result<VideoListResponse> {
441 let (client, headers) = self.create_client()?;
442
443 let mut query: Vec<String> = Vec::new();
444 if let Some(limit) = limit {
445 query.push(format!("limit={}", limit));
446 }
447 if let Some(after) = after {
448 query.push(format!("after={}", after));
449 }
450 if let Some(order) = order {
451 query.push(format!("order={}", order.as_str()));
452 }
453
454 let endpoint = self.auth.endpoint(VIDEOS_PATH);
455 let url = if query.is_empty() { endpoint } else { format!("{}?{}", endpoint, query.join("&")) };
456
457 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
458
459 Self::read_json(response).await
460 }
461
462 pub async fn delete(&self, video_id: &str) -> Result<DeleteVideoResponse> {
468 let (client, headers) = self.create_client()?;
469 let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
470
471 let response = client.delete(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
472
473 Self::read_json(response).await
474 }
475
476 pub async fn content(&self, video_id: &str, variant: Option<VideoVariant>) -> Result<Vec<u8>> {
502 let (client, headers) = self.create_client()?;
503 let url = match variant {
504 Some(variant) => format!("{}/{}/content?variant={}", self.auth.endpoint(VIDEOS_PATH), video_id, variant.as_str()),
505 None => format!("{}/{}/content", self.auth.endpoint(VIDEOS_PATH), video_id),
506 };
507
508 let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
509
510 let status = response.status();
511 if !status.is_success() {
512 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
513 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
514 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
515 }
516 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
517 }
518
519 let bytes = response.bytes().await.map_err(OpenAIToolError::RequestError)?;
520 Ok(bytes.to_vec())
521 }
522
523 pub async fn remix<S: Into<String>>(&self, video_id: &str, prompt: S) -> Result<Video> {
530 let (client, headers) = self.create_client()?;
531 let url = format!("{}/{}/remix", self.auth.endpoint(VIDEOS_PATH), video_id);
532 let body = serde_json::to_string(&RemixVideoRequest { prompt: prompt.into() })?;
533
534 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
535
536 Self::read_json(response).await
537 }
538}