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#[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#[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#[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#[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#[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#[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#[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#[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
216pub 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
267pub 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
297pub 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
327pub 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}