use crate::error::LingerError;
use crate::transport::HttpRequest;
use crate::RequestId;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateImageRequest {
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moderation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_compression: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateImageRequest {
pub fn builder() -> CreateImageRequestBuilder {
CreateImageRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateImageRequestBuilder {
prompt: Option<String>,
model: Option<String>,
n: Option<u32>,
background: Option<String>,
moderation: Option<String>,
output_format: Option<String>,
output_compression: Option<u8>,
quality: Option<String>,
response_format: Option<String>,
size: Option<String>,
style: Option<String>,
user: Option<String>,
extra: BTreeMap<String, Value>,
}
impl CreateImageRequestBuilder {
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn n(mut self, n: u32) -> Self {
self.n = Some(n);
self
}
pub fn background(mut self, background: impl Into<String>) -> Self {
self.background = Some(background.into());
self
}
pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
self.moderation = Some(moderation.into());
self
}
pub fn output_format(mut self, output_format: impl Into<String>) -> Self {
self.output_format = Some(output_format.into());
self
}
pub fn output_compression(mut self, output_compression: u8) -> Self {
self.output_compression = Some(output_compression);
self
}
pub fn quality(mut self, quality: impl Into<String>) -> Self {
self.quality = Some(quality.into());
self
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateImageRequest, LingerError> {
let prompt = self
.prompt
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
validate_optional_string("model", self.model.as_deref())?;
validate_optional_string("background", self.background.as_deref())?;
validate_optional_string("moderation", self.moderation.as_deref())?;
validate_optional_string("output_format", self.output_format.as_deref())?;
validate_optional_string("quality", self.quality.as_deref())?;
validate_optional_string("response_format", self.response_format.as_deref())?;
validate_optional_string("size", self.size.as_deref())?;
validate_optional_string("style", self.style.as_deref())?;
validate_optional_string("user", self.user.as_deref())?;
if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
return Err(LingerError::invalid_config("n must be between 1 and 10"));
}
if self
.output_compression
.is_some_and(|output_compression| output_compression > 100)
{
return Err(LingerError::invalid_config(
"output_compression must be between 0 and 100",
));
}
Ok(CreateImageRequest {
prompt,
model: self.model,
n: self.n,
background: self.background,
moderation: self.moderation,
output_format: self.output_format,
output_compression: self.output_compression,
quality: self.quality,
response_format: self.response_format,
size: self.size,
style: self.style,
user: self.user,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum ImageInput {
FileId {
file_id: String,
},
ImageUrl {
image_url: String,
},
}
impl ImageInput {
pub fn file_id(file_id: impl Into<String>) -> Self {
Self::FileId {
file_id: file_id.into(),
}
}
pub fn image_url(image_url: impl Into<String>) -> Self {
Self::ImageUrl {
image_url: image_url.into(),
}
}
fn validate(&self) -> Result<(), LingerError> {
let (name, value) = match self {
Self::FileId { file_id } => ("file_id", file_id),
Self::ImageUrl { image_url } => ("image_url", image_url),
};
if value.trim().is_empty() {
return Err(LingerError::invalid_config(format!("{name} is required")));
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateImageEditRequest {
pub images: Vec<ImageInput>,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_fidelity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub moderation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_compression: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateImageEditRequest {
pub fn builder() -> CreateImageEditRequestBuilder {
CreateImageEditRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateImageEditRequestBuilder {
images: Vec<ImageInput>,
prompt: Option<String>,
model: Option<String>,
n: Option<u32>,
size: Option<String>,
background: Option<String>,
input_fidelity: Option<String>,
moderation: Option<String>,
quality: Option<String>,
output_compression: Option<u8>,
response_format: Option<String>,
user: Option<String>,
extra: BTreeMap<String, Value>,
}
impl CreateImageEditRequestBuilder {
pub fn image(mut self, image: ImageInput) -> Self {
self.images.push(image);
self
}
pub fn images(mut self, images: impl IntoIterator<Item = ImageInput>) -> Self {
self.images = images.into_iter().collect();
self
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn n(mut self, n: u32) -> Self {
self.n = Some(n);
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn background(mut self, background: impl Into<String>) -> Self {
self.background = Some(background.into());
self
}
pub fn input_fidelity(mut self, input_fidelity: impl Into<String>) -> Self {
self.input_fidelity = Some(input_fidelity.into());
self
}
pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
self.moderation = Some(moderation.into());
self
}
pub fn quality(mut self, quality: impl Into<String>) -> Self {
self.quality = Some(quality.into());
self
}
pub fn output_compression(mut self, output_compression: u8) -> Self {
self.output_compression = Some(output_compression);
self
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateImageEditRequest, LingerError> {
if self.images.is_empty() {
return Err(LingerError::invalid_config("images is required"));
}
for image in &self.images {
image.validate()?;
}
let prompt = self
.prompt
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
validate_optional_string("model", self.model.as_deref())?;
validate_optional_string("size", self.size.as_deref())?;
validate_optional_string("background", self.background.as_deref())?;
validate_optional_string("input_fidelity", self.input_fidelity.as_deref())?;
validate_optional_string("moderation", self.moderation.as_deref())?;
validate_optional_string("quality", self.quality.as_deref())?;
validate_optional_string("response_format", self.response_format.as_deref())?;
validate_optional_string("user", self.user.as_deref())?;
if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
return Err(LingerError::invalid_config("n must be between 1 and 10"));
}
if self
.output_compression
.is_some_and(|output_compression| output_compression > 100)
{
return Err(LingerError::invalid_config(
"output_compression must be between 0 and 100",
));
}
Ok(CreateImageEditRequest {
images: self.images,
prompt,
model: self.model,
n: self.n,
size: self.size,
background: self.background,
input_fidelity: self.input_fidelity,
moderation: self.moderation,
quality: self.quality,
output_compression: self.output_compression,
response_format: self.response_format,
user: self.user,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ImageUpload {
pub filename: String,
pub content_type: String,
content: Bytes,
}
impl ImageUpload {
pub fn from_bytes(
filename: impl Into<String>,
content: impl Into<Bytes>,
) -> Result<Self, LingerError> {
let filename = filename.into();
validate_header_param("filename", &filename)?;
Ok(Self {
filename,
content_type: "application/octet-stream".to_string(),
content: content.into(),
})
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
let content_type = content_type.into();
validate_header_value("content_type", &content_type)?;
self.content_type = content_type;
Ok(self)
}
pub fn bytes(&self) -> Bytes {
self.content.clone()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct CreateImageVariationRequest {
pub image: ImageUpload,
pub model: Option<String>,
pub n: Option<u32>,
pub response_format: Option<String>,
pub size: Option<String>,
pub user: Option<String>,
}
impl CreateImageVariationRequest {
pub fn builder() -> CreateImageVariationRequestBuilder {
CreateImageVariationRequestBuilder::default()
}
pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
let boundary = multipart_boundary(&self.image.content);
request.insert_header(
"content-type",
format!("multipart/form-data; boundary={boundary}"),
);
request.set_body_stream(self.multipart_stream(boundary));
}
fn multipart_stream(
&self,
boundary: String,
) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
let mut chunks = Vec::new();
push_optional_text_field(&mut chunks, &boundary, "model", self.model.as_deref());
push_optional_text_field(&mut chunks, &boundary, "size", self.size.as_deref());
push_optional_text_field(
&mut chunks,
&boundary,
"response_format",
self.response_format.as_deref(),
);
push_optional_text_field(&mut chunks, &boundary, "user", self.user.as_deref());
if let Some(n) = self.n {
push_text_field(&mut chunks, &boundary, "n", &n.to_string());
}
chunks.push(Ok(Bytes::from(format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
escape_multipart_param(&self.image.filename),
self.image.content_type
))));
chunks.push(Ok(self.image.content.clone()));
chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
futures_util::stream::iter(chunks)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateImageVariationRequestBuilder {
image: Option<ImageUpload>,
model: Option<String>,
n: Option<u32>,
response_format: Option<String>,
size: Option<String>,
user: Option<String>,
}
impl CreateImageVariationRequestBuilder {
pub fn image(mut self, image: ImageUpload) -> Self {
self.image = Some(image);
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn n(mut self, n: u32) -> Self {
self.n = Some(n);
self
}
pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
self.response_format = Some(response_format.into());
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn build(self) -> Result<CreateImageVariationRequest, LingerError> {
let image = self
.image
.ok_or_else(|| LingerError::invalid_config("image is required"))?;
validate_optional_string("model", self.model.as_deref())?;
validate_optional_string("response_format", self.response_format.as_deref())?;
validate_optional_string("size", self.size.as_deref())?;
validate_optional_string("user", self.user.as_deref())?;
if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
return Err(LingerError::invalid_config("n must be between 1 and 10"));
}
Ok(CreateImageVariationRequest {
image,
model: self.model,
n: self.n,
response_format: self.response_format,
size: self.size,
user: self.user,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct ImagesResponse {
pub created: u64,
#[serde(default)]
pub data: Vec<Image>,
#[serde(default)]
pub background: Option<String>,
#[serde(default)]
pub output_format: Option<String>,
#[serde(default)]
pub quality: Option<String>,
#[serde(default)]
pub size: Option<String>,
#[serde(default)]
pub usage: Option<ImageUsage>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl ImagesResponse {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct Image {
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub b64_json: Option<String>,
#[serde(default)]
pub revised_prompt: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ImageUsage {
pub input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
#[serde(default)]
pub input_tokens_details: ImageTokenDetails,
#[serde(default)]
pub output_tokens_details: ImageTokenDetails,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct ImageTokenDetails {
#[serde(default)]
pub text_tokens: Option<u64>,
#[serde(default)]
pub image_tokens: Option<u64>,
}
fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
if value.is_some_and(|value| value.trim().is_empty()) {
return Err(LingerError::invalid_config(format!(
"{name} must not be empty"
)));
}
Ok(())
}
fn push_text_field(
chunks: &mut Vec<Result<Bytes, LingerError>>,
boundary: &str,
name: &str,
value: &str,
) {
chunks.push(Ok(Bytes::from(format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
))));
}
fn push_optional_text_field(
chunks: &mut Vec<Result<Bytes, LingerError>>,
boundary: &str,
name: &str,
value: Option<&str>,
) {
if let Some(value) = value {
push_text_field(chunks, boundary, name, value);
}
}
fn multipart_boundary(content: &Bytes) -> String {
for counter in 0.. {
let boundary = format!("linger-openai-sdk-image-boundary-{counter}");
if !contains_bytes(content, boundary.as_bytes()) {
return boundary;
}
}
unreachable!("unbounded boundary counter")
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
if value.trim().is_empty() {
return Err(LingerError::invalid_config(format!("{name} is required")));
}
validate_header_value(name, value)
}
fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
if value.contains('\r') || value.contains('\n') {
return Err(LingerError::invalid_config(format!(
"{name} must not contain CR or LF"
)));
}
Ok(())
}
fn escape_multipart_param(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}