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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
390pub 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
442pub 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
551pub 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
581pub 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}