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)]
50pub enum ImageModel {
51 #[serde(rename = "dall-e-2")]
55 DallE2,
56 #[serde(rename = "dall-e-3")]
60 DallE3,
61 #[serde(rename = "gpt-image-1")]
63 #[default]
64 GptImage1,
65 #[serde(rename = "gpt-image-1-mini")]
67 GptImage1Mini,
68 #[serde(rename = "gpt-image-1.5")]
70 GptImage1_5,
71 #[serde(rename = "gpt-image-2")]
73 GptImage2,
74 #[serde(rename = "chatgpt-image-latest")]
78 ChatGptImageLatest,
79}
80
81impl ImageModel {
82 pub fn as_str(&self) -> &'static str {
84 match self {
85 Self::DallE2 => "dall-e-2",
86 Self::DallE3 => "dall-e-3",
87 Self::GptImage1 => "gpt-image-1",
88 Self::GptImage1Mini => "gpt-image-1-mini",
89 Self::GptImage1_5 => "gpt-image-1.5",
90 Self::GptImage2 => "gpt-image-2",
91 Self::ChatGptImageLatest => "chatgpt-image-latest",
92 }
93 }
94}
95
96impl std::fmt::Display for ImageModel {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}", self.as_str())
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
104pub enum ImageSize {
105 #[serde(rename = "256x256")]
107 Size256x256,
108 #[serde(rename = "512x512")]
110 Size512x512,
111 #[serde(rename = "1024x1024")]
113 #[default]
114 Size1024x1024,
115 #[serde(rename = "1792x1024")]
117 Size1792x1024,
118 #[serde(rename = "1024x1792")]
120 Size1024x1792,
121 #[serde(rename = "1024x1536")]
123 Size1024x1536,
124 #[serde(rename = "1536x1024")]
126 Size1536x1024,
127 #[serde(rename = "auto")]
129 Auto,
130}
131
132impl ImageSize {
133 pub fn as_str(&self) -> &'static str {
135 match self {
136 Self::Size256x256 => "256x256",
137 Self::Size512x512 => "512x512",
138 Self::Size1024x1024 => "1024x1024",
139 Self::Size1792x1024 => "1792x1024",
140 Self::Size1024x1792 => "1024x1792",
141 Self::Size1024x1536 => "1024x1536",
142 Self::Size1536x1024 => "1536x1024",
143 Self::Auto => "auto",
144 }
145 }
146}
147
148impl std::fmt::Display for ImageSize {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 write!(f, "{}", self.as_str())
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
160#[serde(rename_all = "lowercase")]
161pub enum ImageQuality {
162 #[default]
164 Standard,
165 Hd,
167 Low,
169 Medium,
171 High,
173 Auto,
175}
176
177impl ImageQuality {
178 pub fn as_str(&self) -> &'static str {
180 match self {
181 Self::Standard => "standard",
182 Self::Hd => "hd",
183 Self::Low => "low",
184 Self::Medium => "medium",
185 Self::High => "high",
186 Self::Auto => "auto",
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
193#[serde(rename_all = "lowercase")]
194pub enum ImageStyle {
195 #[default]
197 Vivid,
198 Natural,
200}
201
202impl ImageStyle {
203 pub fn as_str(&self) -> &'static str {
205 match self {
206 Self::Vivid => "vivid",
207 Self::Natural => "natural",
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
214#[serde(rename_all = "snake_case")]
215pub enum ResponseFormat {
216 #[default]
218 Url,
219 B64Json,
221}
222
223impl ResponseFormat {
224 pub fn as_str(&self) -> &'static str {
226 match self {
227 Self::Url => "url",
228 Self::B64Json => "b64_json",
229 }
230 }
231}
232
233#[derive(Debug, Clone, Default)]
235pub struct GenerateOptions {
236 pub model: Option<ImageModel>,
238 pub n: Option<u32>,
240 pub quality: Option<ImageQuality>,
245 pub response_format: Option<ResponseFormat>,
250 pub size: Option<ImageSize>,
252 pub style: Option<ImageStyle>,
257 pub user: Option<String>,
259}
260
261#[derive(Debug, Clone, Default)]
263pub struct EditOptions {
264 pub mask: Option<String>,
266 pub model: Option<ImageModel>,
268 pub n: Option<u32>,
270 pub size: Option<ImageSize>,
272 pub response_format: Option<ResponseFormat>,
274 pub user: Option<String>,
276}
277
278#[derive(Debug, Clone, Default)]
280pub struct VariationOptions {
281 pub model: Option<ImageModel>,
283 pub n: Option<u32>,
285 pub response_format: Option<ResponseFormat>,
287 pub size: Option<ImageSize>,
289 pub user: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize)]
295struct GenerateRequest {
296 prompt: String,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 model: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 n: Option<u32>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 quality: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 response_format: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 size: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 style: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 user: Option<String>,
311}
312
313pub struct Images {
340 auth: AuthProvider,
342 timeout: Option<Duration>,
344}
345
346impl Images {
347 pub fn new() -> Result<Self> {
366 let auth = AuthProvider::openai_from_env()?;
367 Ok(Self { auth, timeout: None })
368 }
369
370 pub fn with_auth(auth: AuthProvider) -> Self {
372 Self { auth, timeout: None }
373 }
374
375 pub fn azure() -> Result<Self> {
377 let auth = AuthProvider::azure_from_env()?;
378 Ok(Self { auth, timeout: None })
379 }
380
381 pub fn detect_provider() -> Result<Self> {
383 let auth = AuthProvider::from_env()?;
384 Ok(Self { auth, timeout: None })
385 }
386
387 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
389 let auth = AuthProvider::from_url_with_key(base_url, api_key);
390 Self { auth, timeout: None }
391 }
392
393 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
395 let auth = AuthProvider::from_url(url)?;
396 Ok(Self { auth, timeout: None })
397 }
398
399 pub fn auth(&self) -> &AuthProvider {
401 &self.auth
402 }
403
404 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
414 self.timeout = Some(timeout);
415 self
416 }
417
418 fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
420 let client = create_http_client(self.timeout)?;
421 let mut headers = request::header::HeaderMap::new();
422 self.auth.apply_headers(&mut headers)?;
423 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
424 Ok((client, headers))
425 }
426
427 pub async fn generate(&self, prompt: &str, options: GenerateOptions) -> Result<ImageResponse> {
466 let (client, mut headers) = self.create_client()?;
467 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
468
469 let request_body = GenerateRequest {
470 prompt: prompt.to_string(),
471 model: options.model.map(|m| m.as_str().to_string()),
472 n: options.n,
473 quality: options.quality.map(|q| q.as_str().to_string()),
474 response_format: options.response_format.map(|f| f.as_str().to_string()),
475 size: options.size.map(|s| s.as_str().to_string()),
476 style: options.style.map(|s| s.as_str().to_string()),
477 user: options.user,
478 };
479
480 let body = serde_json::to_string(&request_body).map_err(OpenAIToolError::SerdeJsonError)?;
481
482 let url = format!("{}/generations", self.auth.endpoint(IMAGES_PATH));
483
484 let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
485
486 let status = response.status();
487 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
488
489 if cfg!(test) {
490 tracing::info!("Response content: {}", content);
491 }
492
493 if !status.is_success() {
494 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
495 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
496 }
497 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
498 }
499
500 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
501 }
502
503 pub async fn edit(&self, image_path: &str, prompt: &str, options: EditOptions) -> Result<ImageResponse> {
540 let (client, headers) = self.create_client()?;
541
542 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
544
545 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
546
547 let image_part = Part::bytes(image_content)
548 .file_name(image_filename)
549 .mime_str("image/png")
550 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
551
552 let mut form = Form::new().part("image", image_part).text("prompt", prompt.to_string());
553
554 if let Some(mask_path) = options.mask {
556 let mask_content = tokio::fs::read(&mask_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read mask: {}", e)))?;
557
558 let mask_filename = Path::new(&mask_path).file_name().and_then(|n| n.to_str()).unwrap_or("mask.png").to_string();
559
560 let mask_part = Part::bytes(mask_content)
561 .file_name(mask_filename)
562 .mime_str("image/png")
563 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
564
565 form = form.part("mask", mask_part);
566 }
567
568 if let Some(model) = options.model {
570 form = form.text("model", model.as_str().to_string());
571 }
572 if let Some(n) = options.n {
573 form = form.text("n", n.to_string());
574 }
575 if let Some(size) = options.size {
576 form = form.text("size", size.as_str().to_string());
577 }
578 if let Some(response_format) = options.response_format {
579 form = form.text("response_format", response_format.as_str().to_string());
580 }
581 if let Some(user) = options.user {
582 form = form.text("user", user);
583 }
584
585 let url = format!("{}/edits", self.auth.endpoint(IMAGES_PATH));
586
587 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
588
589 let status = response.status();
590 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
591
592 if cfg!(test) {
593 tracing::info!("Response content: {}", content);
594 }
595
596 if !status.is_success() {
597 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
598 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
599 }
600 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
601 }
602
603 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
604 }
605
606 pub async fn variation(&self, image_path: &str, options: VariationOptions) -> Result<ImageResponse> {
645 let (client, headers) = self.create_client()?;
646
647 let image_content = tokio::fs::read(image_path).await.map_err(|e| OpenAIToolError::Error(format!("Failed to read image: {}", e)))?;
649
650 let image_filename = Path::new(image_path).file_name().and_then(|n| n.to_str()).unwrap_or("image.png").to_string();
651
652 let image_part = Part::bytes(image_content)
653 .file_name(image_filename)
654 .mime_str("image/png")
655 .map_err(|e| OpenAIToolError::Error(format!("Failed to set MIME type: {}", e)))?;
656
657 let mut form = Form::new().part("image", image_part);
658
659 if let Some(model) = options.model {
661 form = form.text("model", model.as_str().to_string());
662 }
663 if let Some(n) = options.n {
664 form = form.text("n", n.to_string());
665 }
666 if let Some(size) = options.size {
667 form = form.text("size", size.as_str().to_string());
668 }
669 if let Some(response_format) = options.response_format {
670 form = form.text("response_format", response_format.as_str().to_string());
671 }
672 if let Some(user) = options.user {
673 form = form.text("user", user);
674 }
675
676 let url = format!("{}/variations", self.auth.endpoint(IMAGES_PATH));
677
678 let response = client.post(&url).headers(headers).multipart(form).send().await.map_err(OpenAIToolError::RequestError)?;
679
680 let status = response.status();
681 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
682
683 if cfg!(test) {
684 tracing::info!("Response content: {}", content);
685 }
686
687 if !status.is_success() {
688 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
689 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
690 }
691 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
692 }
693
694 serde_json::from_str::<ImageResponse>(&content).map_err(OpenAIToolError::SerdeJsonError)
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
710 fn test_new_image_models_as_str() {
711 assert_eq!(ImageModel::GptImage2.as_str(), "gpt-image-2");
712 assert_eq!(ImageModel::GptImage1Mini.as_str(), "gpt-image-1-mini");
713 }
714
715 #[test]
716 fn test_new_image_models_serialization() {
717 for (model, expected) in [(ImageModel::GptImage2, "gpt-image-2"), (ImageModel::GptImage1Mini, "gpt-image-1-mini")] {
718 let json = serde_json::to_string(&model).unwrap();
719 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
720 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
721 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
722 }
723 }
724
725 #[test]
728 fn test_gpt_image_sizes() {
729 assert_eq!(ImageSize::Size1024x1536.as_str(), "1024x1536");
730 assert_eq!(ImageSize::Size1536x1024.as_str(), "1536x1024");
731
732 for (size, expected) in [(ImageSize::Size1024x1536, "1024x1536"), (ImageSize::Size1536x1024, "1536x1024")] {
733 let json = serde_json::to_string(&size).unwrap();
734 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", size);
735 }
736 }
737
738 #[test]
741 fn test_gpt_image_quality_values() {
742 assert_eq!(ImageQuality::Low.as_str(), "low");
743 assert_eq!(ImageQuality::Medium.as_str(), "medium");
744 assert_eq!(ImageQuality::High.as_str(), "high");
745 assert_eq!(ImageQuality::Auto.as_str(), "auto");
746
747 assert_eq!(ImageQuality::Standard.as_str(), "standard");
749 assert_eq!(ImageQuality::Hd.as_str(), "hd");
750 }
751
752 #[test]
753 fn test_gpt_image_quality_serialization() {
754 for (quality, expected) in [
755 (ImageQuality::Low, "low"),
756 (ImageQuality::Medium, "medium"),
757 (ImageQuality::High, "high"),
758 (ImageQuality::Auto, "auto"),
759 (ImageQuality::Standard, "standard"),
760 (ImageQuality::Hd, "hd"),
761 ] {
762 let json = serde_json::to_string(&quality).unwrap();
763 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", quality);
764 let deserialized: ImageQuality = serde_json::from_str(&json).unwrap();
765 assert_eq!(deserialized, quality, "Serialization roundtrip failed for {:?}", quality);
766 }
767 }
768
769 #[test]
772 fn test_previously_missing_image_models() {
773 for (model, expected) in [(ImageModel::GptImage1_5, "gpt-image-1.5"), (ImageModel::ChatGptImageLatest, "chatgpt-image-latest")] {
774 assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
775 let json = serde_json::to_string(&model).unwrap();
776 assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
777 let deserialized: ImageModel = serde_json::from_str(&json).unwrap();
778 assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
779 }
780 }
781
782 #[test]
786 fn test_default_image_model_is_a_live_openai_model() {
787 assert_eq!(ImageModel::default(), ImageModel::GptImage1);
788 assert_ne!(ImageModel::default(), ImageModel::DallE3, "the default must not be a retired model");
789 }
790
791 #[test]
793 fn test_auto_image_size() {
794 assert_eq!(ImageSize::Auto.as_str(), "auto");
795 assert_eq!(serde_json::to_string(&ImageSize::Auto).unwrap(), "\"auto\"");
796 }
797}