Skip to main content

openrouter_rs/api/
images.rs

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/// One image URL payload used as an image generation reference.
23#[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/// Reference image used to guide image generation.
36#[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/// Provider-specific passthrough options for image generation.
58#[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/// Request payload for `POST /images`.
74#[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/// One generated image returned by `POST /images`.
137#[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/// Token and cost usage returned by image generation responses.
148#[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/// Non-streaming response returned by `POST /images`.
163#[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/// Descriptor for one supported image-generation request parameter.
175#[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/// Architecture metadata returned by image model discovery endpoints.
191#[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/// Image model metadata returned by `GET /images/models`.
201#[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/// One billable pricing line for an image provider.
217#[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/// Endpoint metadata for one image generation provider.
230#[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/// Response returned by `GET /images/models/{author}/{slug}/endpoints`.
246#[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/// Partial-image event emitted by streaming image generation.
256#[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/// Completion event emitted by streaming image generation.
268#[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/// Error details emitted by streaming image generation.
284#[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/// Error event emitted by streaming image generation.
299#[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/// Streaming image generation event payload.
310#[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/// SSE data wrapper returned by `POST /images` when `stream=true`.
321#[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
329/// Submit an image generation request.
330pub 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
381/// Stream image generation events.
382pub 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
490/// List all image generation models.
491pub 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
520/// List provider endpoints for one image generation model.
521pub 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}