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