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 = "gpt-image-2.5-flare")]
87 GptImage2_5Flare,
88 #[serde(rename = "gpt-image-2.5-sunburst")]
101 GptImage2_5Sunburst,
102 #[serde(rename = "chatgpt-image-latest")]
106 ChatGptImageLatest,
107}
108
109impl ImageModel {
110 pub fn as_str(&self) -> &'static str {
112 match self {
113 Self::DallE2 => "dall-e-2",
114 Self::DallE3 => "dall-e-3",
115 Self::GptImage1 => "gpt-image-1",
116 Self::GptImage1Mini => "gpt-image-1-mini",
117 Self::GptImage1_5 => "gpt-image-1.5",
118 Self::GptImage2 => "gpt-image-2",
119 Self::GptImage2_5Flare => "gpt-image-2.5-flare",
120 Self::GptImage2_5Sunburst => "gpt-image-2.5-sunburst",
121 Self::ChatGptImageLatest => "chatgpt-image-latest",
122 }
123 }
124}
125
126impl std::fmt::Display for ImageModel {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(f, "{}", self.as_str())
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
134#[non_exhaustive]
135pub enum ImageSize {
136 #[serde(rename = "256x256")]
138 Size256x256,
139 #[serde(rename = "512x512")]
141 Size512x512,
142 #[serde(rename = "1024x1024")]
144 #[default]
145 Size1024x1024,
146 #[serde(rename = "1792x1024")]
148 Size1792x1024,
149 #[serde(rename = "1024x1792")]
151 Size1024x1792,
152 #[serde(rename = "1024x1536")]
154 Size1024x1536,
155 #[serde(rename = "1536x1024")]
157 Size1536x1024,
158 #[serde(rename = "auto")]
160 Auto,
161}
162
163impl ImageSize {
164 pub fn as_str(&self) -> &'static str {
166 match self {
167 Self::Size256x256 => "256x256",
168 Self::Size512x512 => "512x512",
169 Self::Size1024x1024 => "1024x1024",
170 Self::Size1792x1024 => "1792x1024",
171 Self::Size1024x1792 => "1024x1792",
172 Self::Size1024x1536 => "1024x1536",
173 Self::Size1536x1024 => "1536x1024",
174 Self::Auto => "auto",
175 }
176 }
177}
178
179impl std::fmt::Display for ImageSize {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 write!(f, "{}", self.as_str())
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
192#[serde(rename_all = "lowercase")]
193#[non_exhaustive]
194pub enum ImageQuality {
195 #[default]
197 Standard,
198 Hd,
200 Low,
202 Medium,
204 High,
206 Max,
208 Auto,
210}
211
212impl ImageQuality {
213 pub fn as_str(&self) -> &'static str {
215 match self {
216 Self::Standard => "standard",
217 Self::Hd => "hd",
218 Self::Low => "low",
219 Self::Medium => "medium",
220 Self::High => "high",
221 Self::Max => "max",
222 Self::Auto => "auto",
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
229#[serde(rename_all = "lowercase")]
230pub enum ImageStyle {
231 #[default]
233 Vivid,
234 Natural,
236}
237
238impl ImageStyle {
239 pub fn as_str(&self) -> &'static str {
241 match self {
242 Self::Vivid => "vivid",
243 Self::Natural => "natural",
244 }
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
250#[serde(rename_all = "snake_case")]
251pub enum ResponseFormat {
252 #[default]
254 Url,
255 B64Json,
257}
258
259impl ResponseFormat {
260 pub fn as_str(&self) -> &'static str {
262 match self {
263 Self::Url => "url",
264 Self::B64Json => "b64_json",
265 }
266 }
267}
268
269#[derive(Debug, Clone, Default)]
271pub struct GenerateOptions {
272 pub model: Option<ImageModel>,
274 pub n: Option<u32>,
276 pub quality: Option<ImageQuality>,
282 pub response_format: Option<ResponseFormat>,
287 pub size: Option<ImageSize>,
289 pub style: Option<ImageStyle>,
294 pub user: Option<String>,
296}
297
298#[derive(Debug, Clone, Default)]
300pub struct EditOptions {
301 pub mask: Option<String>,
303 pub model: Option<ImageModel>,
305 pub n: Option<u32>,
307 pub size: Option<ImageSize>,
309 pub response_format: Option<ResponseFormat>,
311 pub user: Option<String>,
313}
314
315#[derive(Debug, Clone, Default)]
317pub struct VariationOptions {
318 pub model: Option<ImageModel>,
320 pub n: Option<u32>,
322 pub response_format: Option<ResponseFormat>,
324 pub size: Option<ImageSize>,
326 pub user: Option<String>,
328}
329
330#[derive(Debug, Clone, Serialize)]
332struct GenerateRequest {
333 prompt: String,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 model: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 n: Option<u32>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 quality: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 response_format: Option<String>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 size: Option<String>,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 style: Option<String>,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 user: Option<String>,
348}
349
350pub struct Images {
377 auth: AuthProvider,
379 timeout: Option<Duration>,
381}
382
383impl Images {
384 pub fn new() -> Result<Self> {
403 let auth = AuthProvider::openai_from_env()?;
404 Ok(Self { auth, timeout: None })
405 }
406
407 pub fn with_auth(auth: AuthProvider) -> Self {
409 Self { auth, timeout: None }
410 }
411
412 pub fn azure() -> Result<Self> {
414 let auth = AuthProvider::azure_from_env()?;
415 Ok(Self { auth, timeout: None })
416 }
417
418 pub fn detect_provider() -> Result<Self> {
420 let auth = AuthProvider::from_env()?;
421 Ok(Self { auth, timeout: None })
422 }
423
424 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
426 let auth = AuthProvider::from_url_with_key(base_url, api_key);
427 Self { auth, timeout: None }
428 }
429
430 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
432 let auth = AuthProvider::from_url(url)?;
433 Ok(Self { auth, timeout: None })
434 }
435
436 pub fn auth(&self) -> &AuthProvider {
438 &self.auth
439 }
440
441 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
451 self.timeout = Some(timeout);
452 self
453 }
454
455 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
457 let client = create_http_client(self.timeout)?;
458 let mut headers = request::header::HeaderMap::new();
459 self.auth.apply_headers(&mut headers)?;
460 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
461 Ok((client, headers))
462 }
463
464 pub async fn generate(&self, prompt: &str, options: GenerateOptions) -> Result<ImageResponse> {
503 let (client, mut headers) = self.create_client()?;
504 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
505
506 let request_body = GenerateRequest {
507 prompt: prompt.to_string(),
508 model: options.model.map(|m| m.as_str().to_string()),
509 n: options.n,
510 quality: options.quality.map(|q| q.as_str().to_string()),
511 response_format: options.response_format.map(|f| f.as_str().to_string()),
512 size: options.size.map(|s| s.as_str().to_string()),
513 style: options.style.map(|s| s.as_str().to_string()),
514 user: options.user,
515 };
516
517 let body = serde_json::to_string(&request_body).map_err(OpenAIToolError::SerdeJsonError)?;
518
519 let url = format!("{}/generations", self.auth.endpoint(IMAGES_PATH));
520
521 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
522
523 let status = response.status();
524 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
525
526 if cfg!(test) {
527 tracing::info!("Response content: {}", content);
528 }
529
530 if !status.is_success() {
531 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
532 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
533 }
534 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
535 }
536
537 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
538 }
539
540 pub async fn edit(&self, image_path: &str, prompt: &str, options: EditOptions) -> Result<ImageResponse> {
577 let (client, headers) = self.create_client()?;
578
579 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
581
582 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
583
584 let image_part = Part::bytes(image_content)
585 .file_name(image_filename)
586 .mime_str("image/png")
587 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
588
589 let mut form = Form::new().part("image", image_part).text("prompt", prompt.to_string());
590
591 if let Some(mask_path) = options.mask {
593 let mask_content = tokio::fs::read(&mask_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read mask: {}", e)))?;
594
595 let mask_filename = Path::new(&mask_path).file_name().and_then(|n| n.to_str()).unwrap_or("mask.png").to_string();
596
597 let mask_part = Part::bytes(mask_content)
598 .file_name(mask_filename)
599 .mime_str("image/png")
600 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
601
602 form = form.part("mask", mask_part);
603 }
604
605 if let Some(model) = options.model {
607 form = form.text("model", model.as_str().to_string());
608 }
609 if let Some(n) = options.n {
610 form = form.text("n", n.to_string());
611 }
612 if let Some(size) = options.size {
613 form = form.text("size", size.as_str().to_string());
614 }
615 if let Some(response_format) = options.response_format {
616 form = form.text("response_format", response_format.as_str().to_string());
617 }
618 if let Some(user) = options.user {
619 form = form.text("user", user);
620 }
621
622 let url = format!("{}/edits", self.auth.endpoint(IMAGES_PATH));
623
624 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
625
626 let status = response.status();
627 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
628
629 if cfg!(test) {
630 tracing::info!("Response content: {}", content);
631 }
632
633 if !status.is_success() {
634 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
635 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
636 }
637 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
638 }
639
640 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
641 }
642
643 pub async fn variation(&self, image_path: &str, options: VariationOptions) -> Result<ImageResponse> {
682 let (client, headers) = self.create_client()?;
683
684 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
686
687 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
688
689 let image_part = Part::bytes(image_content)
690 .file_name(image_filename)
691 .mime_str("image/png")
692 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
693
694 let mut form = Form::new().part("image", image_part);
695
696 if let Some(model) = options.model {
698 form = form.text("model", model.as_str().to_string());
699 }
700 if let Some(n) = options.n {
701 form = form.text("n", n.to_string());
702 }
703 if let Some(size) = options.size {
704 form = form.text("size", size.as_str().to_string());
705 }
706 if let Some(response_format) = options.response_format {
707 form = form.text("response_format", response_format.as_str().to_string());
708 }
709 if let Some(user) = options.user {
710 form = form.text("user", user);
711 }
712
713 let url = format!("{}/variations", self.auth.endpoint(IMAGES_PATH));
714
715 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
716
717 let status = response.status();
718 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
719
720 if cfg!(test) {
721 tracing::info!("Response content: {}", content);
722 }
723
724 if !status.is_success() {
725 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
726 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
727 }
728 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
729 }
730
731 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
732 }
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 #[test]
747 fn test_new_image_models_as_str() {
748 assert_eq!(ImageModel::GptImage2.as_str(), "gpt-image-2");
749 assert_eq!(ImageModel::GptImage2_5Flare.as_str(), "gpt-image-2.5-flare");
750 assert_eq!(ImageModel::GptImage2_5Sunburst.as_str(), "gpt-image-2.5-sunburst");
751 assert_eq!(ImageModel::GptImage1Mini.as_str(), "gpt-image-1-mini");
752 }
753
754 #[test]
755 fn test_new_image_models_serialization() {
756 for (model, expected) in [
757 (ImageModel::GptImage2, "gpt-image-2"),
758 (ImageModel::GptImage2_5Flare, "gpt-image-2.5-flare"),
759 (ImageModel::GptImage2_5Sunburst, "gpt-image-2.5-sunburst"),
760 (ImageModel::GptImage1Mini, "gpt-image-1-mini"),
761 ] {
762 let json = serde_json::to_string(&model).unwrap();
763 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
764 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
765 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
766 }
767 }
768
769 #[test]
772 fn test_gpt_image_sizes() {
773 assert_eq!(ImageSize::Size1024x1536.as_str(), "1024x1536");
774 assert_eq!(ImageSize::Size1536x1024.as_str(), "1536x1024");
775
776 for (size, expected) in [(ImageSize::Size1024x1536, "1024x1536"), (ImageSize::Size1536x1024, "1536x1024")] {
777 let json = serde_json::to_string(&size).unwrap();
778 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", size);
779 }
780 }
781
782 #[test]
785 fn test_gpt_image_quality_values() {
786 assert_eq!(ImageQuality::Low.as_str(), "low");
787 assert_eq!(ImageQuality::Medium.as_str(), "medium");
788 assert_eq!(ImageQuality::High.as_str(), "high");
789 assert_eq!(ImageQuality::Max.as_str(), "max");
790 assert_eq!(ImageQuality::Auto.as_str(), "auto");
791
792 assert_eq!(ImageQuality::Standard.as_str(), "standard");
794 assert_eq!(ImageQuality::Hd.as_str(), "hd");
795 }
796
797 #[test]
798 fn test_gpt_image_quality_serialization() {
799 for (quality, expected) in [
800 (ImageQuality::Low, "low"),
801 (ImageQuality::Medium, "medium"),
802 (ImageQuality::High, "high"),
803 (ImageQuality::Max, "max"),
804 (ImageQuality::Auto, "auto"),
805 (ImageQuality::Standard, "standard"),
806 (ImageQuality::Hd, "hd"),
807 ] {
808 let json = serde_json::to_string(&quality).unwrap();
809 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", quality);
810 let deserialized: ImageQuality = serde_json::from_str(&json).unwrap();
811 assert_eq!(deserialized, quality, "Serialization roundtrip failed for {:?}", quality);
812 }
813 }
814
815 #[test]
818 fn test_previously_missing_image_models() {
819 for (model, expected) in [(ImageModel::GptImage1_5, "gpt-image-1.5"), (ImageModel::ChatGptImageLatest, "chatgpt-image-latest")] {
820 assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
821 let json = serde_json::to_string(&model).unwrap();
822 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
823 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
824 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
825 }
826 }
827
828 #[test]
832 fn test_default_image_model_is_a_live_openai_model() {
833 assert_eq!(ImageModel::default(), ImageModel::GptImage1);
834 assert_ne!(ImageModel::default(), ImageModel::DallE3, "the default must not be a retired model");
835 }
836
837 #[test]
839 fn test_auto_image_size() {
840 assert_eq!(ImageSize::Auto.as_str(), "auto");
841 assert_eq!(serde_json::to_string(&ImageSize::Auto).unwrap(), "\"auto\"");
842 }
843}