Skip to main content

openrouter_rs/api/
videos.rs

1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use reqwest::Client as HttpClient;
5use serde::{Deserialize, Serialize};
6use urlencoding::encode;
7
8use crate::{
9    error::OpenRouterError,
10    transport::{request as transport_request, response as transport_response},
11};
12
13/// One image URL payload used in video generation requests.
14#[derive(Serialize, Deserialize, Debug, Clone)]
15#[non_exhaustive]
16pub struct VideoImageUrl {
17    pub url: String,
18}
19
20impl VideoImageUrl {
21    pub fn new(url: impl Into<String>) -> Self {
22        Self { url: url.into() }
23    }
24}
25
26/// Reference media used to guide video generation.
27#[derive(Serialize, Deserialize, Debug, Clone)]
28#[non_exhaustive]
29pub struct VideoInputReference {
30    #[serde(rename = "type")]
31    pub content_type: String,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub image_url: Option<VideoImageUrl>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub audio_url: Option<VideoImageUrl>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub video_url: Option<VideoImageUrl>,
38}
39
40impl VideoInputReference {
41    pub fn new(url: impl Into<String>) -> Self {
42        Self::image(url)
43    }
44
45    pub fn image(url: impl Into<String>) -> Self {
46        Self {
47            content_type: "image_url".to_string(),
48            image_url: Some(VideoImageUrl::new(url)),
49            audio_url: None,
50            video_url: None,
51        }
52    }
53
54    pub fn audio(url: impl Into<String>) -> Self {
55        Self {
56            content_type: "audio_url".to_string(),
57            image_url: None,
58            audio_url: Some(VideoImageUrl::new(url)),
59            video_url: None,
60        }
61    }
62
63    pub fn video(url: impl Into<String>) -> Self {
64        Self {
65            content_type: "video_url".to_string(),
66            image_url: None,
67            audio_url: None,
68            video_url: Some(VideoImageUrl::new(url)),
69        }
70    }
71}
72
73/// Frame image used as the first or last frame of a generated video.
74#[derive(Serialize, Deserialize, Debug, Clone)]
75#[non_exhaustive]
76pub struct VideoFrameImage {
77    #[serde(rename = "type")]
78    pub content_type: String,
79    pub image_url: VideoImageUrl,
80    pub frame_type: String,
81}
82
83impl VideoFrameImage {
84    pub fn new(url: impl Into<String>, frame_type: impl Into<String>) -> Self {
85        Self {
86            content_type: "image_url".to_string(),
87            image_url: VideoImageUrl::new(url),
88            frame_type: frame_type.into(),
89        }
90    }
91}
92
93/// Provider-specific passthrough options for video generation.
94#[derive(Serialize, Deserialize, Debug, Clone, Default)]
95#[non_exhaustive]
96pub struct VideoProviderOptions {
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub options: Option<HashMap<String, serde_json::Value>>,
99}
100
101impl VideoProviderOptions {
102    pub fn new(options: HashMap<String, serde_json::Value>) -> Self {
103        Self {
104            options: Some(options),
105        }
106    }
107}
108
109/// Request payload for `POST /videos`.
110#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
111#[builder(build_fn(error = "OpenRouterError"))]
112#[non_exhaustive]
113pub struct VideoGenerationRequest {
114    #[builder(setter(into), default)]
115    #[serde(skip_serializing_if = "String::is_empty")]
116    pub prompt: String,
117    #[builder(setter(into))]
118    pub model: String,
119    #[builder(setter(into, strip_option), default)]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub aspect_ratio: Option<String>,
122    #[builder(setter(into, strip_option), default)]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub callback_url: Option<String>,
125    #[builder(setter(strip_option), default)]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub duration: Option<u32>,
128    #[builder(setter(strip_option), default)]
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub frame_images: Option<Vec<VideoFrameImage>>,
131    #[builder(setter(strip_option), default)]
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub generate_audio: Option<bool>,
134    #[builder(setter(strip_option), default)]
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub input_references: Option<Vec<VideoInputReference>>,
137    #[builder(setter(strip_option), default)]
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub provider: Option<VideoProviderOptions>,
140    #[builder(setter(into, strip_option), default)]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub resolution: Option<String>,
143    #[builder(setter(strip_option), default)]
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub seed: Option<i64>,
146    #[builder(setter(into, strip_option), default)]
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub size: Option<String>,
149}
150
151impl VideoGenerationRequest {
152    pub fn builder() -> VideoGenerationRequestBuilder {
153        VideoGenerationRequestBuilder::default()
154    }
155}
156
157/// Usage payload returned by video generation status responses.
158#[derive(Serialize, Deserialize, Debug, Clone)]
159#[non_exhaustive]
160pub struct VideoGenerationUsage {
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub cost: Option<f64>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub is_byok: Option<bool>,
165}
166
167/// Response payload returned by `POST /videos` and `GET /videos/{jobId}`.
168#[derive(Serialize, Deserialize, Debug, Clone)]
169#[non_exhaustive]
170pub struct VideoGenerationResponse {
171    pub id: String,
172    pub polling_url: String,
173    pub status: String,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub error: Option<String>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub generation_id: Option<String>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub unsigned_urls: Option<Vec<String>>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub usage: Option<VideoGenerationUsage>,
182}
183
184/// Video model metadata returned by `GET /videos/models`.
185#[derive(Serialize, Deserialize, Debug, Clone)]
186#[non_exhaustive]
187pub struct VideoModel {
188    pub id: String,
189    pub canonical_slug: String,
190    pub name: String,
191    pub created: u64,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub description: Option<String>,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub hugging_face_id: Option<String>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub pricing_skus: Option<HashMap<String, String>>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub supported_resolutions: Option<Vec<String>>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub supported_aspect_ratios: Option<Vec<String>>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub supported_sizes: Option<Vec<String>>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub supported_durations: Option<Vec<u32>>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub supported_frame_images: Option<Vec<String>>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub generate_audio: Option<bool>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub seed: Option<bool>,
212    #[serde(default)]
213    pub allowed_passthrough_parameters: Vec<String>,
214}
215
216/// Submit a video generation request.
217pub async fn create_video_generation(
218    base_url: &str,
219    api_key: &str,
220    x_title: &Option<String>,
221    http_referer: &Option<String>,
222    app_categories: &Option<Vec<String>>,
223    request: &VideoGenerationRequest,
224) -> Result<VideoGenerationResponse, OpenRouterError> {
225    let http_client = crate::transport::new_client()?;
226    create_video_generation_with_client(
227        &http_client,
228        base_url,
229        api_key,
230        x_title,
231        http_referer,
232        app_categories,
233        request,
234    )
235    .await
236}
237
238pub(crate) async fn create_video_generation_with_client(
239    http_client: &HttpClient,
240    base_url: &str,
241    api_key: &str,
242    x_title: &Option<String>,
243    http_referer: &Option<String>,
244    app_categories: &Option<Vec<String>>,
245    request: &VideoGenerationRequest,
246) -> Result<VideoGenerationResponse, OpenRouterError> {
247    let url = format!("{base_url}/videos");
248    let response = transport_request::with_client_request_headers(
249        transport_request::post(http_client, &url),
250        api_key,
251        x_title,
252        http_referer,
253        app_categories,
254    )?
255    .json(request)
256    .send()
257    .await?;
258
259    if response.status().is_success() {
260        transport_response::parse_json_response(response, "video generation").await
261    } else {
262        transport_response::handle_error(response).await?;
263        unreachable!()
264    }
265}
266
267/// List all video generation models.
268pub async fn list_video_models(
269    base_url: &str,
270    api_key: &str,
271) -> Result<Vec<VideoModel>, OpenRouterError> {
272    let http_client = crate::transport::new_client()?;
273    list_video_models_with_client(&http_client, base_url, api_key).await
274}
275
276pub(crate) async fn list_video_models_with_client(
277    http_client: &HttpClient,
278    base_url: &str,
279    api_key: &str,
280) -> Result<Vec<VideoModel>, OpenRouterError> {
281    let url = format!("{base_url}/videos/models");
282    let response =
283        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
284            .send()
285            .await?;
286
287    if response.status().is_success() {
288        let payload: crate::types::ApiResponse<Vec<VideoModel>> =
289            transport_response::parse_json_response(response, "video models").await?;
290        Ok(payload.data)
291    } else {
292        transport_response::handle_error(response).await?;
293        unreachable!()
294    }
295}
296
297/// Poll one video generation job by job id.
298pub async fn get_video_generation(
299    base_url: &str,
300    api_key: &str,
301    job_id: &str,
302) -> Result<VideoGenerationResponse, OpenRouterError> {
303    let http_client = crate::transport::new_client()?;
304    get_video_generation_with_client(&http_client, base_url, api_key, job_id).await
305}
306
307pub(crate) async fn get_video_generation_with_client(
308    http_client: &HttpClient,
309    base_url: &str,
310    api_key: &str,
311    job_id: &str,
312) -> Result<VideoGenerationResponse, OpenRouterError> {
313    let url = format!("{base_url}/videos/{}", encode(job_id));
314    let response =
315        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
316            .send()
317            .await?;
318
319    if response.status().is_success() {
320        transport_response::parse_json_response(response, "video generation status").await
321    } else {
322        transport_response::handle_error(response).await?;
323        unreachable!()
324    }
325}
326
327/// Download binary content for a completed video generation job.
328pub async fn get_video_content(
329    base_url: &str,
330    api_key: &str,
331    job_id: &str,
332    index: Option<u32>,
333) -> Result<Vec<u8>, OpenRouterError> {
334    let http_client = crate::transport::new_client()?;
335    get_video_content_with_client(&http_client, base_url, api_key, job_id, index).await
336}
337
338pub(crate) async fn get_video_content_with_client(
339    http_client: &HttpClient,
340    base_url: &str,
341    api_key: &str,
342    job_id: &str,
343    index: Option<u32>,
344) -> Result<Vec<u8>, OpenRouterError> {
345    let mut url = format!("{base_url}/videos/{}/content", encode(job_id));
346    if let Some(index) = index {
347        url = format!("{url}?index={index}");
348    }
349
350    let response =
351        transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
352            .send()
353            .await?;
354
355    if response.status().is_success() {
356        Ok(response.bytes().await?.to_vec())
357    } else {
358        transport_response::handle_error(response).await?;
359        unreachable!()
360    }
361}