1use crate::common::auth::AuthProvider;
30use crate::common::client::create_http_client;
31use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
32use crate::images::response::ImageResponse;
33use request::multipart::{Form, Part};
34use serde::{Deserialize, Serialize};
35use std::path::Path;
36use std::time::Duration;
37
38const IMAGES_PATH: &str = "images";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
50#[non_exhaustive]
51pub enum ImageModel {
52 #[serde(rename = "dall-e-2")]
56 DallE2,
57 #[serde(rename = "dall-e-3")]
61 DallE3,
62 #[serde(rename = "gpt-image-1")]
64 #[default]
65 GptImage1,
66 #[serde(rename = "gpt-image-1-mini")]
68 GptImage1Mini,
69 #[serde(rename = "gpt-image-1.5")]
71 GptImage1_5,
72 #[serde(rename = "gpt-image-2")]
74 GptImage2,
75 #[serde(rename = "chatgpt-image-latest")]
79 ChatGptImageLatest,
80}
81
82impl ImageModel {
83 pub fn as_str(&self) -> &'static str {
85 match self {
86 Self::DallE2 => "dall-e-2",
87 Self::DallE3 => "dall-e-3",
88 Self::GptImage1 => "gpt-image-1",
89 Self::GptImage1Mini => "gpt-image-1-mini",
90 Self::GptImage1_5 => "gpt-image-1.5",
91 Self::GptImage2 => "gpt-image-2",
92 Self::ChatGptImageLatest => "chatgpt-image-latest",
93 }
94 }
95}
96
97impl std::fmt::Display for ImageModel {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{}", self.as_str())
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
105#[non_exhaustive]
106pub enum ImageSize {
107 #[serde(rename = "256x256")]
109 Size256x256,
110 #[serde(rename = "512x512")]
112 Size512x512,
113 #[serde(rename = "1024x1024")]
115 #[default]
116 Size1024x1024,
117 #[serde(rename = "1792x1024")]
119 Size1792x1024,
120 #[serde(rename = "1024x1792")]
122 Size1024x1792,
123 #[serde(rename = "1024x1536")]
125 Size1024x1536,
126 #[serde(rename = "1536x1024")]
128 Size1536x1024,
129 #[serde(rename = "auto")]
131 Auto,
132}
133
134impl ImageSize {
135 pub fn as_str(&self) -> &'static str {
137 match self {
138 Self::Size256x256 => "256x256",
139 Self::Size512x512 => "512x512",
140 Self::Size1024x1024 => "1024x1024",
141 Self::Size1792x1024 => "1792x1024",
142 Self::Size1024x1792 => "1024x1792",
143 Self::Size1024x1536 => "1024x1536",
144 Self::Size1536x1024 => "1536x1024",
145 Self::Auto => "auto",
146 }
147 }
148}
149
150impl std::fmt::Display for ImageSize {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 write!(f, "{}", self.as_str())
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
162#[serde(rename_all = "lowercase")]
163#[non_exhaustive]
164pub enum ImageQuality {
165 #[default]
167 Standard,
168 Hd,
170 Low,
172 Medium,
174 High,
176 Auto,
178}
179
180impl ImageQuality {
181 pub fn as_str(&self) -> &'static str {
183 match self {
184 Self::Standard => "standard",
185 Self::Hd => "hd",
186 Self::Low => "low",
187 Self::Medium => "medium",
188 Self::High => "high",
189 Self::Auto => "auto",
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "lowercase")]
197pub enum ImageStyle {
198 #[default]
200 Vivid,
201 Natural,
203}
204
205impl ImageStyle {
206 pub fn as_str(&self) -> &'static str {
208 match self {
209 Self::Vivid => "vivid",
210 Self::Natural => "natural",
211 }
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
217#[serde(rename_all = "snake_case")]
218pub enum ResponseFormat {
219 #[default]
221 Url,
222 B64Json,
224}
225
226impl ResponseFormat {
227 pub fn as_str(&self) -> &'static str {
229 match self {
230 Self::Url => "url",
231 Self::B64Json => "b64_json",
232 }
233 }
234}
235
236#[derive(Debug, Clone, Default)]
238pub struct GenerateOptions {
239 pub model: Option<ImageModel>,
241 pub n: Option<u32>,
243 pub quality: Option<ImageQuality>,
248 pub response_format: Option<ResponseFormat>,
253 pub size: Option<ImageSize>,
255 pub style: Option<ImageStyle>,
260 pub user: Option<String>,
262}
263
264#[derive(Debug, Clone, Default)]
266pub struct EditOptions {
267 pub mask: Option<String>,
269 pub model: Option<ImageModel>,
271 pub n: Option<u32>,
273 pub size: Option<ImageSize>,
275 pub response_format: Option<ResponseFormat>,
277 pub user: Option<String>,
279}
280
281#[derive(Debug, Clone, Default)]
283pub struct VariationOptions {
284 pub model: Option<ImageModel>,
286 pub n: Option<u32>,
288 pub response_format: Option<ResponseFormat>,
290 pub size: Option<ImageSize>,
292 pub user: Option<String>,
294}
295
296#[derive(Debug, Clone, Serialize)]
298struct GenerateRequest {
299 prompt: String,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 model: Option<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 n: Option<u32>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 quality: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 response_format: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 size: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 style: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 user: Option<String>,
314}
315
316pub struct Images {
343 auth: AuthProvider,
345 timeout: Option<Duration>,
347}
348
349impl Images {
350 pub fn new() -> Result<Self> {
369 let auth = AuthProvider::openai_from_env()?;
370 Ok(Self { auth, timeout: None })
371 }
372
373 pub fn with_auth(auth: AuthProvider) -> Self {
375 Self { auth, timeout: None }
376 }
377
378 pub fn azure() -> Result<Self> {
380 let auth = AuthProvider::azure_from_env()?;
381 Ok(Self { auth, timeout: None })
382 }
383
384 pub fn detect_provider() -> Result<Self> {
386 let auth = AuthProvider::from_env()?;
387 Ok(Self { auth, timeout: None })
388 }
389
390 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
392 let auth = AuthProvider::from_url_with_key(base_url, api_key);
393 Self { auth, timeout: None }
394 }
395
396 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
398 let auth = AuthProvider::from_url(url)?;
399 Ok(Self { auth, timeout: None })
400 }
401
402 pub fn auth(&self) -> &AuthProvider {
404 &self.auth
405 }
406
407 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
417 self.timeout = Some(timeout);
418 self
419 }
420
421 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
423 let client = create_http_client(self.timeout)?;
424 let mut headers = request::header::HeaderMap::new();
425 self.auth.apply_headers(&mut headers)?;
426 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
427 Ok((client, headers))
428 }
429
430 pub async fn generate(&self, prompt: &str, options: GenerateOptions) -> Result<ImageResponse> {
469 let (client, mut headers) = self.create_client()?;
470 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
471
472 let request_body = GenerateRequest {
473 prompt: prompt.to_string(),
474 model: options.model.map(|m| m.as_str().to_string()),
475 n: options.n,
476 quality: options.quality.map(|q| q.as_str().to_string()),
477 response_format: options.response_format.map(|f| f.as_str().to_string()),
478 size: options.size.map(|s| s.as_str().to_string()),
479 style: options.style.map(|s| s.as_str().to_string()),
480 user: options.user,
481 };
482
483 let body = serde_json::to_string(&request_body).map_err(OpenAIToolError::SerdeJsonError)?;
484
485 let url = format!("{}/generations", self.auth.endpoint(IMAGES_PATH));
486
487 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
488
489 let status = response.status();
490 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
491
492 if cfg!(test) {
493 tracing::info!("Response content: {}", content);
494 }
495
496 if !status.is_success() {
497 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
498 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
499 }
500 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
501 }
502
503 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
504 }
505
506 pub async fn edit(&self, image_path: &str, prompt: &str, options: EditOptions) -> Result<ImageResponse> {
543 let (client, headers) = self.create_client()?;
544
545 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
547
548 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
549
550 let image_part = Part::bytes(image_content)
551 .file_name(image_filename)
552 .mime_str("image/png")
553 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
554
555 let mut form = Form::new().part("image", image_part).text("prompt", prompt.to_string());
556
557 if let Some(mask_path) = options.mask {
559 let mask_content = tokio::fs::read(&mask_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read mask: {}", e)))?;
560
561 let mask_filename = Path::new(&mask_path).file_name().and_then(|n| n.to_str()).unwrap_or("mask.png").to_string();
562
563 let mask_part = Part::bytes(mask_content)
564 .file_name(mask_filename)
565 .mime_str("image/png")
566 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
567
568 form = form.part("mask", mask_part);
569 }
570
571 if let Some(model) = options.model {
573 form = form.text("model", model.as_str().to_string());
574 }
575 if let Some(n) = options.n {
576 form = form.text("n", n.to_string());
577 }
578 if let Some(size) = options.size {
579 form = form.text("size", size.as_str().to_string());
580 }
581 if let Some(response_format) = options.response_format {
582 form = form.text("response_format", response_format.as_str().to_string());
583 }
584 if let Some(user) = options.user {
585 form = form.text("user", user);
586 }
587
588 let url = format!("{}/edits", self.auth.endpoint(IMAGES_PATH));
589
590 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
591
592 let status = response.status();
593 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
594
595 if cfg!(test) {
596 tracing::info!("Response content: {}", content);
597 }
598
599 if !status.is_success() {
600 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
601 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
602 }
603 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
604 }
605
606 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
607 }
608
609 pub async fn variation(&self, image_path: &str, options: VariationOptions) -> Result<ImageResponse> {
648 let (client, headers) = self.create_client()?;
649
650 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
652
653 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
654
655 let image_part = Part::bytes(image_content)
656 .file_name(image_filename)
657 .mime_str("image/png")
658 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
659
660 let mut form = Form::new().part("image", image_part);
661
662 if let Some(model) = options.model {
664 form = form.text("model", model.as_str().to_string());
665 }
666 if let Some(n) = options.n {
667 form = form.text("n", n.to_string());
668 }
669 if let Some(size) = options.size {
670 form = form.text("size", size.as_str().to_string());
671 }
672 if let Some(response_format) = options.response_format {
673 form = form.text("response_format", response_format.as_str().to_string());
674 }
675 if let Some(user) = options.user {
676 form = form.text("user", user);
677 }
678
679 let url = format!("{}/variations", self.auth.endpoint(IMAGES_PATH));
680
681 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
682
683 let status = response.status();
684 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
685
686 if cfg!(test) {
687 tracing::info!("Response content: {}", content);
688 }
689
690 if !status.is_success() {
691 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
692 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
693 }
694 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
695 }
696
697 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 #[test]
713 fn test_new_image_models_as_str() {
714 assert_eq!(ImageModel::GptImage2.as_str(), "gpt-image-2");
715 assert_eq!(ImageModel::GptImage1Mini.as_str(), "gpt-image-1-mini");
716 }
717
718 #[test]
719 fn test_new_image_models_serialization() {
720 for (model, expected) in [(ImageModel::GptImage2, "gpt-image-2"), (ImageModel::GptImage1Mini, "gpt-image-1-mini")] {
721 let json = serde_json::to_string(&model).unwrap();
722 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
723 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
724 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
725 }
726 }
727
728 #[test]
731 fn test_gpt_image_sizes() {
732 assert_eq!(ImageSize::Size1024x1536.as_str(), "1024x1536");
733 assert_eq!(ImageSize::Size1536x1024.as_str(), "1536x1024");
734
735 for (size, expected) in [(ImageSize::Size1024x1536, "1024x1536"), (ImageSize::Size1536x1024, "1536x1024")] {
736 let json = serde_json::to_string(&size).unwrap();
737 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", size);
738 }
739 }
740
741 #[test]
744 fn test_gpt_image_quality_values() {
745 assert_eq!(ImageQuality::Low.as_str(), "low");
746 assert_eq!(ImageQuality::Medium.as_str(), "medium");
747 assert_eq!(ImageQuality::High.as_str(), "high");
748 assert_eq!(ImageQuality::Auto.as_str(), "auto");
749
750 assert_eq!(ImageQuality::Standard.as_str(), "standard");
752 assert_eq!(ImageQuality::Hd.as_str(), "hd");
753 }
754
755 #[test]
756 fn test_gpt_image_quality_serialization() {
757 for (quality, expected) in [
758 (ImageQuality::Low, "low"),
759 (ImageQuality::Medium, "medium"),
760 (ImageQuality::High, "high"),
761 (ImageQuality::Auto, "auto"),
762 (ImageQuality::Standard, "standard"),
763 (ImageQuality::Hd, "hd"),
764 ] {
765 let json = serde_json::to_string(&quality).unwrap();
766 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", quality);
767 let deserialized: ImageQuality = serde_json::from_str(&json).unwrap();
768 assert_eq!(deserialized, quality, "Serialization roundtrip failed for {:?}", quality);
769 }
770 }
771
772 #[test]
775 fn test_previously_missing_image_models() {
776 for (model, expected) in [(ImageModel::GptImage1_5, "gpt-image-1.5"), (ImageModel::ChatGptImageLatest, "chatgpt-image-latest")] {
777 assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
778 let json = serde_json::to_string(&model).unwrap();
779 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
780 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
781 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
782 }
783 }
784
785 #[test]
789 fn test_default_image_model_is_a_live_openai_model() {
790 assert_eq!(ImageModel::default(), ImageModel::GptImage1);
791 assert_ne!(ImageModel::default(), ImageModel::DallE3, "the default must not be a retired model");
792 }
793
794 #[test]
796 fn test_auto_image_size() {
797 assert_eq!(ImageSize::Auto.as_str(), "auto");
798 assert_eq!(serde_json::to_string(&ImageSize::Auto).unwrap(), "\"auto\"");
799 }
800}