1use std::collections::HashMap;
2
3use derive_builder::Builder;
4use futures_util::{
5 StreamExt,
6 stream::{self, BoxStream},
7};
8use reqwest::{Client as HttpClient, header::CONTENT_TYPE};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use urlencoding::encode;
12
13use crate::{
14 error::OpenRouterError,
15 strip_option_vec_setter,
16 transport::{
17 request as transport_request, response as transport_response, sse::response_lines,
18 },
19 utils::parse_sse_frames,
20};
21
22#[derive(Serialize, Deserialize, Debug, Clone)]
24#[non_exhaustive]
25pub struct ImageUrl {
26 pub url: String,
27}
28
29impl ImageUrl {
30 pub fn new(url: impl Into<String>) -> Self {
31 Self { url: url.into() }
32 }
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone)]
37#[non_exhaustive]
38pub struct ImageInputReference {
39 #[serde(rename = "type")]
40 pub content_type: String,
41 pub image_url: ImageUrl,
42}
43
44impl ImageInputReference {
45 pub fn new(url: impl Into<String>) -> Self {
46 Self::image_url(url)
47 }
48
49 pub fn image_url(url: impl Into<String>) -> Self {
50 Self {
51 content_type: "image_url".to_string(),
52 image_url: ImageUrl::new(url),
53 }
54 }
55}
56
57#[derive(Serialize, Deserialize, Debug, Clone, Default)]
59#[non_exhaustive]
60pub struct ImageProviderOptions {
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub options: Option<HashMap<String, Value>>,
63}
64
65impl ImageProviderOptions {
66 pub fn new(options: HashMap<String, Value>) -> Self {
67 Self {
68 options: Some(options),
69 }
70 }
71}
72
73#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
75#[builder(build_fn(error = "OpenRouterError"))]
76#[non_exhaustive]
77pub struct ImageGenerationRequest {
78 #[builder(setter(into))]
79 pub model: String,
80 #[builder(setter(into))]
81 pub prompt: String,
82 #[builder(setter(into, strip_option), default)]
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub aspect_ratio: Option<String>,
85 #[builder(setter(into, strip_option), default)]
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub background: Option<String>,
88 #[builder(setter(custom), default)]
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub input_references: Option<Vec<ImageInputReference>>,
91 #[builder(setter(strip_option), default)]
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub n: Option<u32>,
94 #[builder(setter(strip_option), default)]
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub output_compression: Option<u32>,
97 #[builder(setter(into, strip_option), default)]
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub output_format: Option<String>,
100 #[builder(setter(strip_option), default)]
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub provider: Option<ImageProviderOptions>,
103 #[builder(setter(into, strip_option), default)]
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub quality: Option<String>,
106 #[builder(setter(into, strip_option), default)]
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub resolution: Option<String>,
109 #[builder(setter(strip_option), default)]
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub seed: Option<i64>,
112 #[builder(setter(into, strip_option), default)]
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub size: Option<String>,
115 #[builder(setter(skip), default)]
116 #[serde(skip_serializing_if = "Option::is_none")]
117 stream: Option<bool>,
118}
119
120impl ImageGenerationRequestBuilder {
121 strip_option_vec_setter!(input_references, ImageInputReference);
122}
123
124impl ImageGenerationRequest {
125 pub fn builder() -> ImageGenerationRequestBuilder {
126 ImageGenerationRequestBuilder::default()
127 }
128
129 fn stream(&self, stream: bool) -> Self {
130 let mut req = self.clone();
131 req.stream = Some(stream);
132 req
133 }
134}
135
136#[derive(Serialize, Deserialize, Debug, Clone)]
138#[non_exhaustive]
139pub struct GeneratedImage {
140 pub b64_json: String,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub media_type: Option<String>,
143 #[serde(flatten)]
144 pub extra: HashMap<String, Value>,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone)]
149#[non_exhaustive]
150pub struct ImageGenerationUsage {
151 pub prompt_tokens: u64,
152 pub completion_tokens: u64,
153 pub total_tokens: u64,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub cost: Option<f64>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub is_byok: Option<bool>,
158 #[serde(flatten)]
159 pub extra: HashMap<String, Value>,
160}
161
162#[derive(Serialize, Deserialize, Debug, Clone)]
164#[non_exhaustive]
165pub struct ImageGenerationResponse {
166 pub created: u64,
167 pub data: Vec<GeneratedImage>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub usage: Option<ImageGenerationUsage>,
170 #[serde(flatten)]
171 pub extra: HashMap<String, Value>,
172}
173
174#[derive(Serialize, Deserialize, Debug, Clone)]
176#[non_exhaustive]
177pub struct ImageCapabilityDescriptor {
178 #[serde(rename = "type")]
179 pub capability_type: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub values: Option<Vec<String>>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub min: Option<f64>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub max: Option<f64>,
186 #[serde(flatten)]
187 pub extra: HashMap<String, Value>,
188}
189
190#[derive(Serialize, Deserialize, Debug, Clone)]
192#[non_exhaustive]
193pub struct ImageModelArchitecture {
194 pub input_modalities: Vec<String>,
195 pub output_modalities: Vec<String>,
196 #[serde(flatten)]
197 pub extra: HashMap<String, Value>,
198}
199
200#[derive(Serialize, Deserialize, Debug, Clone)]
202#[non_exhaustive]
203pub struct ImageModel {
204 pub id: String,
205 pub name: String,
206 pub description: String,
207 pub created: u64,
208 pub architecture: ImageModelArchitecture,
209 pub supported_parameters: HashMap<String, ImageCapabilityDescriptor>,
210 pub supports_streaming: bool,
211 pub endpoints: String,
212 #[serde(flatten)]
213 pub extra: HashMap<String, Value>,
214}
215
216#[derive(Serialize, Deserialize, Debug, Clone)]
218#[non_exhaustive]
219pub struct ImagePricingEntry {
220 pub billable: String,
221 pub unit: String,
222 pub cost_usd: f64,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub variant: Option<String>,
225 #[serde(flatten)]
226 pub extra: HashMap<String, Value>,
227}
228
229#[derive(Serialize, Deserialize, Debug, Clone)]
231#[non_exhaustive]
232pub struct ImageEndpoint {
233 pub provider_name: String,
234 pub provider_slug: String,
235 pub provider_tag: Option<String>,
236 pub supported_parameters: HashMap<String, ImageCapabilityDescriptor>,
237 #[serde(default)]
238 pub allowed_passthrough_parameters: Vec<String>,
239 pub supports_streaming: bool,
240 pub pricing: Vec<ImagePricingEntry>,
241 #[serde(flatten)]
242 pub extra: HashMap<String, Value>,
243}
244
245#[derive(Serialize, Deserialize, Debug, Clone)]
247#[non_exhaustive]
248pub struct ImageModelEndpointsResponse {
249 pub id: String,
250 pub endpoints: Vec<ImageEndpoint>,
251 #[serde(flatten)]
252 pub extra: HashMap<String, Value>,
253}
254
255#[derive(Serialize, Deserialize, Debug, Clone)]
257#[non_exhaustive]
258pub struct ImagePartialImageEvent {
259 #[serde(rename = "type")]
260 pub event_type: String,
261 pub partial_image_index: u32,
262 pub b64_json: String,
263 #[serde(flatten)]
264 pub extra: HashMap<String, Value>,
265}
266
267#[derive(Serialize, Deserialize, Debug, Clone)]
269#[non_exhaustive]
270pub struct ImageCompletedEvent {
271 #[serde(rename = "type")]
272 pub event_type: String,
273 pub b64_json: String,
274 pub created: u64,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub media_type: Option<String>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub usage: Option<ImageGenerationUsage>,
279 #[serde(flatten)]
280 pub extra: HashMap<String, Value>,
281}
282
283#[derive(Serialize, Deserialize, Debug, Clone)]
285#[non_exhaustive]
286pub struct ImageStreamError {
287 pub message: String,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub code: Option<String>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub param: Option<String>,
292 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
293 pub error_type: Option<String>,
294 #[serde(flatten)]
295 pub extra: HashMap<String, Value>,
296}
297
298#[derive(Serialize, Deserialize, Debug, Clone)]
300#[non_exhaustive]
301pub struct ImageStreamErrorEvent {
302 #[serde(rename = "type")]
303 pub event_type: String,
304 pub error: ImageStreamError,
305 #[serde(flatten)]
306 pub extra: HashMap<String, Value>,
307}
308
309#[derive(Serialize, Deserialize, Debug, Clone)]
311#[serde(untagged)]
312#[non_exhaustive]
313pub enum ImageStreamEvent {
314 PartialImage(ImagePartialImageEvent),
315 Completed(ImageCompletedEvent),
316 Error(ImageStreamErrorEvent),
317 Other(Value),
318}
319
320#[derive(Serialize, Deserialize, Debug, Clone)]
322#[non_exhaustive]
323pub struct ImageStreamingResponse {
324 pub data: ImageStreamEvent,
325 #[serde(flatten)]
326 pub extra: HashMap<String, Value>,
327}
328
329pub async fn create_image_generation(
331 base_url: &str,
332 api_key: &str,
333 x_title: &Option<String>,
334 http_referer: &Option<String>,
335 app_categories: &Option<Vec<String>>,
336 request: &ImageGenerationRequest,
337) -> Result<ImageGenerationResponse, OpenRouterError> {
338 let http_client = crate::transport::new_client()?;
339 create_image_generation_with_client(
340 &http_client,
341 base_url,
342 api_key,
343 x_title,
344 http_referer,
345 app_categories,
346 request,
347 )
348 .await
349}
350
351pub(crate) async fn create_image_generation_with_client(
352 http_client: &HttpClient,
353 base_url: &str,
354 api_key: &str,
355 x_title: &Option<String>,
356 http_referer: &Option<String>,
357 app_categories: &Option<Vec<String>>,
358 request: &ImageGenerationRequest,
359) -> Result<ImageGenerationResponse, OpenRouterError> {
360 let url = format!("{base_url}/images");
361 let request = request.stream(false);
362 let response = transport_request::with_client_request_headers(
363 transport_request::post(http_client, &url),
364 api_key,
365 x_title,
366 http_referer,
367 app_categories,
368 )?
369 .json(&request)
370 .send()
371 .await?;
372
373 if response.status().is_success() {
374 transport_response::parse_json_response(response, "image generation").await
375 } else {
376 transport_response::handle_error(response).await?;
377 unreachable!()
378 }
379}
380
381pub async fn stream_image_generation(
383 base_url: &str,
384 api_key: &str,
385 x_title: &Option<String>,
386 http_referer: &Option<String>,
387 app_categories: &Option<Vec<String>>,
388 request: &ImageGenerationRequest,
389) -> Result<BoxStream<'static, Result<ImageStreamingResponse, OpenRouterError>>, OpenRouterError> {
390 let http_client = crate::transport::new_client()?;
391 stream_image_generation_with_client(
392 &http_client,
393 base_url,
394 api_key,
395 x_title,
396 http_referer,
397 app_categories,
398 request,
399 )
400 .await
401}
402
403pub(crate) async fn stream_image_generation_with_client(
404 http_client: &HttpClient,
405 base_url: &str,
406 api_key: &str,
407 x_title: &Option<String>,
408 http_referer: &Option<String>,
409 app_categories: &Option<Vec<String>>,
410 request: &ImageGenerationRequest,
411) -> Result<BoxStream<'static, Result<ImageStreamingResponse, OpenRouterError>>, OpenRouterError> {
412 let url = format!("{base_url}/images");
413 let request = request.stream(true);
414 let response = transport_request::with_client_request_headers(
415 transport_request::post(http_client, &url),
416 api_key,
417 x_title,
418 http_referer,
419 app_categories,
420 )?
421 .json(&request)
422 .send()
423 .await?;
424
425 if response.status().is_success() {
426 if is_sse_response(&response) {
427 let lines = parse_sse_frames(response_lines(response))
428 .filter_map(async |line| match line {
429 Ok(frame) if frame.data == "[DONE]" => None,
430 Ok(frame) => Some(
431 serde_json::from_str::<ImageStreamingResponse>(&frame.data)
432 .map_err(OpenRouterError::Serialization),
433 ),
434 Err(error) => Some(Err(error)),
435 })
436 .boxed();
437
438 Ok(lines)
439 } else {
440 let response: ImageGenerationResponse =
441 transport_response::parse_json_response(response, "image generation").await?;
442 Ok(buffered_image_response_stream(response))
443 }
444 } else {
445 transport_response::handle_error(response).await?;
446 unreachable!()
447 }
448}
449
450fn is_sse_response(response: &reqwest::Response) -> bool {
451 response
452 .headers()
453 .get(CONTENT_TYPE)
454 .and_then(|value| value.to_str().ok())
455 .map(|value| {
456 value
457 .split(';')
458 .next()
459 .unwrap_or_default()
460 .trim()
461 .eq_ignore_ascii_case("text/event-stream")
462 })
463 .unwrap_or(false)
464}
465
466fn buffered_image_response_stream(
467 response: ImageGenerationResponse,
468) -> BoxStream<'static, Result<ImageStreamingResponse, OpenRouterError>> {
469 let created = response.created;
470 let data = response.data;
471 let mut usage = response.usage;
472 let response_extra = response.extra;
473
474 stream::iter(data.into_iter().map(move |image| {
475 Ok(ImageStreamingResponse {
476 data: ImageStreamEvent::Completed(ImageCompletedEvent {
477 event_type: "image_generation.completed".to_string(),
478 b64_json: image.b64_json,
479 created,
480 media_type: image.media_type,
481 usage: usage.take(),
482 extra: image.extra,
483 }),
484 extra: response_extra.clone(),
485 })
486 }))
487 .boxed()
488}
489
490pub async fn list_image_models(
492 base_url: &str,
493 api_key: &str,
494) -> Result<Vec<ImageModel>, OpenRouterError> {
495 let http_client = crate::transport::new_client()?;
496 list_image_models_with_client(&http_client, base_url, api_key).await
497}
498
499pub(crate) async fn list_image_models_with_client(
500 http_client: &HttpClient,
501 base_url: &str,
502 api_key: &str,
503) -> Result<Vec<ImageModel>, OpenRouterError> {
504 let url = format!("{base_url}/images/models");
505 let response =
506 transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
507 .send()
508 .await?;
509
510 if response.status().is_success() {
511 let payload: crate::types::ApiResponse<Vec<ImageModel>> =
512 transport_response::parse_json_response(response, "image models").await?;
513 Ok(payload.data)
514 } else {
515 transport_response::handle_error(response).await?;
516 unreachable!()
517 }
518}
519
520pub async fn list_image_model_endpoints(
522 base_url: &str,
523 api_key: &str,
524 author: &str,
525 slug: &str,
526) -> Result<ImageModelEndpointsResponse, OpenRouterError> {
527 let http_client = crate::transport::new_client()?;
528 list_image_model_endpoints_with_client(&http_client, base_url, api_key, author, slug).await
529}
530
531pub(crate) async fn list_image_model_endpoints_with_client(
532 http_client: &HttpClient,
533 base_url: &str,
534 api_key: &str,
535 author: &str,
536 slug: &str,
537) -> Result<ImageModelEndpointsResponse, OpenRouterError> {
538 let encoded_author = encode(author);
539 let encoded_slug = encode(slug);
540 let url = format!("{base_url}/images/models/{encoded_author}/{encoded_slug}/endpoints");
541 let response =
542 transport_request::with_bearer_auth(transport_request::get(http_client, &url), api_key)
543 .send()
544 .await?;
545
546 if response.status().is_success() {
547 transport_response::parse_json_response(response, "image model endpoints").await
548 } else {
549 transport_response::handle_error(response).await?;
550 unreachable!()
551 }
552}