From a378fea2d41a3147ac86e6ecbe7770fd50e499e7 Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 16:26:54 -0700 Subject: [PATCH 1/6] Add CreateFileRequest --- async-openai/src/types/file.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/async-openai/src/types/file.rs b/async-openai/src/types/file.rs index 9a2e5090..de6afc73 100644 --- a/async-openai/src/types/file.rs +++ b/async-openai/src/types/file.rs @@ -19,6 +19,15 @@ pub enum FilePurpose { Vision, } +#[derive(Debug, Default, Clone, PartialEq)] +pub struct FileExpiresAfter { + /// Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`. + pub anchor: String, + + /// The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days). + pub seconds: u32, +} + #[derive(Debug, Default, Clone, Builder, PartialEq)] #[builder(name = "CreateFileRequestArgs")] #[builder(pattern = "mutable")] @@ -33,6 +42,9 @@ pub struct CreateFileRequest { /// /// Use "assistants" for [Assistants](https://platform.openai.com/docs/api-reference/assistants) and [Message](https://platform.openai.com/docs/api-reference/messages) files, "vision" for Assistants image file inputs, "batch" for [Batch API](https://platform.openai.com/docs/guides/batch), and "fine-tune" for [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning). pub purpose: FilePurpose, + + /// The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. + pub expires_after: FileExpiresAfter, } #[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] From 5f7a2d1dcbacac62a7be4f78bdd4fddee55ff558 Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 16:45:30 -0700 Subject: [PATCH 2/6] FileExpiresAfterAnchor --- async-openai/src/types/file.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/async-openai/src/types/file.rs b/async-openai/src/types/file.rs index de6afc73..2627b0f6 100644 --- a/async-openai/src/types/file.rs +++ b/async-openai/src/types/file.rs @@ -19,10 +19,17 @@ pub enum FilePurpose { Vision, } +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +pub enum FileExpiresAfterAnchor { + #[default] + #[serde(rename = "created_at")] + CreateAt, +} + #[derive(Debug, Default, Clone, PartialEq)] pub struct FileExpiresAfter { /// Anchor timestamp after which the expiration policy applies. Supported anchors: `created_at`. - pub anchor: String, + pub anchor: FileExpiresAfterAnchor, /// The number of seconds after the anchor time that the file will expire. Must be between 3600 (1 hour) and 2592000 (30 days). pub seconds: u32, From b14f10b9d3e7b75d7ce0f89d61bf60e1eb0a4575 Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 17:10:18 -0700 Subject: [PATCH 3/6] Add OpenAIFile::expires_at --- async-openai/src/types/file.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/async-openai/src/types/file.rs b/async-openai/src/types/file.rs index 2627b0f6..7a76940d 100644 --- a/async-openai/src/types/file.rs +++ b/async-openai/src/types/file.rs @@ -96,6 +96,8 @@ pub struct OpenAIFile { pub bytes: u32, /// The Unix timestamp (in seconds) for when the file was created. pub created_at: u32, + /// The Unix timestamp (in seconds) for when the file will expire. + pub expires_at: u32, /// The name of the file. pub filename: String, /// The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results` and `vision`. From 954326e666a99a0267a3279a2a7416e7e412610b Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 17:48:17 -0700 Subject: [PATCH 4/6] Add FileExpiresAfter to AsyncTryFrom --- async-openai/src/types/impls.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/async-openai/src/types/impls.rs b/async-openai/src/types/impls.rs index 8646e8f9..d65df0f2 100644 --- a/async-openai/src/types/impls.rs +++ b/async-openai/src/types/impls.rs @@ -29,7 +29,7 @@ use super::{ CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, DallE2ImageSize, EmbeddingInput, FileInput, FilePurpose, FunctionName, Image, ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse, ModerationInput, Prompt, Role, Stop, - TimestampGranularity, + TimestampGranularity, FileExpiresAfterAnchor }; /// for `impl_from!(T, Enum)`, implements @@ -278,6 +278,18 @@ impl Display for FilePurpose { } } +impl Display for FileExpiresAfterAnchor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::CreateAt => "create_at", + } + ) + } +} + impl ImagesResponse { /// Save each image in a dedicated Tokio task and return paths to saved files. /// For [ResponseFormat::Url] each file is downloaded in dedicated Tokio task. @@ -972,7 +984,9 @@ impl AsyncTryFrom for reqwest::multipart::Form { let file_part = create_file_part(request.file.source).await?; let form = reqwest::multipart::Form::new() .part("file", file_part) - .text("purpose", request.purpose.to_string()); + .text("purpose", request.purpose.to_string()) + .text("expires_after[anchor]", request.expires_after.anchor.to_string()) + .text("expires_after[seconds]", request.expires_after.seconds.to_string()); Ok(form) } } From e7c230295223ae8318292d3c843a7c9cab92617e Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 17:48:36 -0700 Subject: [PATCH 5/6] Change OpenAIFile::expires_at type --- async-openai/src/types/file.rs | 4 ++-- async-openai/src/types/impls.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/async-openai/src/types/file.rs b/async-openai/src/types/file.rs index 7a76940d..aeec331b 100644 --- a/async-openai/src/types/file.rs +++ b/async-openai/src/types/file.rs @@ -23,7 +23,7 @@ pub enum FilePurpose { pub enum FileExpiresAfterAnchor { #[default] #[serde(rename = "created_at")] - CreateAt, + CreatedAt } #[derive(Debug, Default, Clone, PartialEq)] @@ -97,7 +97,7 @@ pub struct OpenAIFile { /// The Unix timestamp (in seconds) for when the file was created. pub created_at: u32, /// The Unix timestamp (in seconds) for when the file will expire. - pub expires_at: u32, + pub expires_at: Option, /// The name of the file. pub filename: String, /// The intended purpose of the file. Supported values are `assistants`, `assistants_output`, `batch`, `batch_output`, `fine-tune`, `fine-tune-results` and `vision`. diff --git a/async-openai/src/types/impls.rs b/async-openai/src/types/impls.rs index d65df0f2..3efd3ce0 100644 --- a/async-openai/src/types/impls.rs +++ b/async-openai/src/types/impls.rs @@ -284,7 +284,7 @@ impl Display for FileExpiresAfterAnchor { f, "{}", match self { - Self::CreateAt => "create_at", + Self::CreatedAt => "created_at", } ) } From a16dfb6e18535975fe75d3a88359af93fc6d6d18 Mon Sep 17 00:00:00 2001 From: Makoto Emura Date: Mon, 13 Oct 2025 17:54:48 -0700 Subject: [PATCH 6/6] Make CreateFileRequest::expires_after optional --- async-openai/src/file.rs | 4 +++- async-openai/src/types/file.rs | 2 +- async-openai/src/types/impls.rs | 11 +++++++---- async-openai/src/vector_store_files.rs | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/async-openai/src/file.rs b/async-openai/src/file.rs index cfca19c7..b8b1ed81 100644 --- a/async-openai/src/file.rs +++ b/async-openai/src/file.rs @@ -70,7 +70,7 @@ impl<'c, C: Config> Files<'c, C> { #[cfg(test)] mod tests { use crate::{ - types::{CreateFileRequestArgs, FilePurpose}, + types::{CreateFileRequestArgs, FilePurpose, FileExpiresAfter, FileExpiresAfterAnchor}, Client, }; @@ -89,6 +89,7 @@ mod tests { let request = CreateFileRequestArgs::default() .file(test_file_path) .purpose(FilePurpose::FineTune) + .expires_after(FileExpiresAfter{ anchor: FileExpiresAfterAnchor::CreatedAt, seconds: 3600 }) .build() .unwrap(); @@ -111,6 +112,7 @@ mod tests { assert_eq!(openai_file.bytes, retrieved_file.bytes); assert_eq!(openai_file.filename, retrieved_file.filename); assert_eq!(openai_file.purpose, retrieved_file.purpose); + assert_eq!(openai_file.expires_at, retrieved_file.expires_at); /* // "To help mitigate abuse, downloading of fine-tune training files is disabled for free accounts." diff --git a/async-openai/src/types/file.rs b/async-openai/src/types/file.rs index aeec331b..ebd90a83 100644 --- a/async-openai/src/types/file.rs +++ b/async-openai/src/types/file.rs @@ -51,7 +51,7 @@ pub struct CreateFileRequest { pub purpose: FilePurpose, /// The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. - pub expires_after: FileExpiresAfter, + pub expires_after: Option, } #[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] diff --git a/async-openai/src/types/impls.rs b/async-openai/src/types/impls.rs index 3efd3ce0..d11bf604 100644 --- a/async-openai/src/types/impls.rs +++ b/async-openai/src/types/impls.rs @@ -982,11 +982,14 @@ impl AsyncTryFrom for reqwest::multipart::Form { async fn try_from(request: CreateFileRequest) -> Result { let file_part = create_file_part(request.file.source).await?; - let form = reqwest::multipart::Form::new() + let mut form = reqwest::multipart::Form::new() .part("file", file_part) - .text("purpose", request.purpose.to_string()) - .text("expires_after[anchor]", request.expires_after.anchor.to_string()) - .text("expires_after[seconds]", request.expires_after.seconds.to_string()); + .text("purpose", request.purpose.to_string()); + + if let Some(expires_after) = request.expires_after { + form = form.text("expires_after[anchor]", expires_after.anchor.to_string()) + .text("expires_after[seconds]", expires_after.seconds.to_string()); + } Ok(form) } } diff --git a/async-openai/src/vector_store_files.rs b/async-openai/src/vector_store_files.rs index 5ecaac06..cb7d2748 100644 --- a/async-openai/src/vector_store_files.rs +++ b/async-openai/src/vector_store_files.rs @@ -113,6 +113,7 @@ mod tests { String::from(":3").into_bytes(), ), purpose: FilePurpose::Assistants, + expires_after: None, }) .await?;