From 3bb7eb7d9d25d3c379f43953a7e1ac1f6ed726e6 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Wed, 5 Nov 2025 17:50:24 -0800 Subject: [PATCH 1/5] feat: containers api --- async-openai/src/client.rs | 7 +- async-openai/src/container_files.rs | 76 ++++++++++++ async-openai/src/containers.rs | 60 +++++++++ async-openai/src/lib.rs | 4 + async-openai/src/types/containers.rs | 176 +++++++++++++++++++++++++++ async-openai/src/types/impls.rs | 29 ++++- async-openai/src/types/mod.rs | 2 + 7 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 async-openai/src/container_files.rs create mode 100644 async-openai/src/containers.rs create mode 100644 async-openai/src/types/containers.rs diff --git a/async-openai/src/client.rs b/async-openai/src/client.rs index bed0be9a..4acd6f1e 100644 --- a/async-openai/src/client.rs +++ b/async-openai/src/client.rs @@ -13,7 +13,7 @@ use crate::{ image::Images, moderation::Moderations, traits::AsyncTryFrom, - Assistants, Audio, AuditLogs, Batches, Chat, Completions, Conversations, Embeddings, + Assistants, Audio, AuditLogs, Batches, Chat, Completions, Containers, Conversations, Embeddings, FineTuning, Invites, Models, Projects, Responses, Threads, Uploads, Users, VectorStores, Videos, }; @@ -178,6 +178,11 @@ impl Client { Conversations::new(self) } + /// To call [Containers] group related APIs using this client. + pub fn containers(&self) -> Containers<'_, C> { + Containers::new(self) + } + pub fn config(&self) -> &C { &self.config } diff --git a/async-openai/src/container_files.rs b/async-openai/src/container_files.rs new file mode 100644 index 00000000..0cd17407 --- /dev/null +++ b/async-openai/src/container_files.rs @@ -0,0 +1,76 @@ +use bytes::Bytes; +use serde::Serialize; + +use crate::{ + config::Config, + error::OpenAIError, + types::{ + ContainerFileListResource, ContainerFileResource, CreateContainerFileRequest, + DeleteContainerFileResponse, + }, + Client, +}; + +/// Create and manage container files for use with the Code Interpreter tool. +pub struct ContainerFiles<'c, C: Config> { + client: &'c Client, + container_id: String, +} + +impl<'c, C: Config> ContainerFiles<'c, C> { + pub fn new(client: &'c Client, container_id: &str) -> Self { + Self { + client, + container_id: container_id.to_string(), + } + } + + /// Create a container file by uploading a raw file or by referencing an existing file ID. + #[crate::byot( + T0 = Clone, + R = serde::de::DeserializeOwned, + where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom", + )] + pub async fn create( + &self, + request: CreateContainerFileRequest, + ) -> Result { + self.client + .post_form(&format!("/containers/{}/files", self.container_id), request) + .await + } + + /// List container files. + #[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)] + pub async fn list(&self, query: &Q) -> Result + where + Q: Serialize + ?Sized, + { + self.client + .get_with_query(&format!("/containers/{}/files", self.container_id), &query) + .await + } + + /// Retrieve a container file. + #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)] + pub async fn retrieve(&self, file_id: &str) -> Result { + self.client + .get(format!("/containers/{}/files/{file_id}", self.container_id).as_str()) + .await + } + + /// Delete a container file. + #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)] + pub async fn delete(&self, file_id: &str) -> Result { + self.client + .delete(format!("/containers/{}/files/{file_id}", self.container_id).as_str()) + .await + } + + /// Returns the content of a container file. + pub async fn content(&self, file_id: &str) -> Result { + self.client + .get_raw(format!("/containers/{}/files/{file_id}/content", self.container_id).as_str()) + .await + } +} diff --git a/async-openai/src/containers.rs b/async-openai/src/containers.rs new file mode 100644 index 00000000..57a88a20 --- /dev/null +++ b/async-openai/src/containers.rs @@ -0,0 +1,60 @@ +use serde::Serialize; + +use crate::{ + config::Config, + container_files::ContainerFiles, + error::OpenAIError, + types::{ + ContainerListResource, ContainerResource, CreateContainerRequest, DeleteContainerResponse, + }, + Client, +}; + +pub struct Containers<'c, C: Config> { + client: &'c Client, +} + +impl<'c, C: Config> Containers<'c, C> { + pub fn new(client: &'c Client) -> Self { + Self { client } + } + + /// [ContainerFiles] API group + pub fn files(&self, container_id: &str) -> ContainerFiles<'_, C> { + ContainerFiles::new(self.client, container_id) + } + + /// Create a container. + #[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)] + pub async fn create( + &self, + request: CreateContainerRequest, + ) -> Result { + self.client.post("/containers", request).await + } + + /// List containers. + #[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)] + pub async fn list(&self, query: &Q) -> Result + where + Q: Serialize + ?Sized, + { + self.client.get_with_query("/containers", &query).await + } + + /// Retrieve a container. + #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)] + pub async fn retrieve(&self, container_id: &str) -> Result { + self.client + .get(format!("/containers/{container_id}").as_str()) + .await + } + + /// Delete a container. + #[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)] + pub async fn delete(&self, container_id: &str) -> Result { + self.client + .delete(format!("/containers/{container_id}").as_str()) + .await + } +} diff --git a/async-openai/src/lib.rs b/async-openai/src/lib.rs index e8e81311..79042328 100644 --- a/async-openai/src/lib.rs +++ b/async-openai/src/lib.rs @@ -148,6 +148,8 @@ mod chat; mod client; mod completion; pub mod config; +mod container_files; +mod containers; mod conversation_items; mod conversations; mod download; @@ -185,6 +187,8 @@ pub use batches::Batches; pub use chat::Chat; pub use client::Client; pub use completion::Completions; +pub use container_files::ContainerFiles; +pub use containers::Containers; pub use conversation_items::ConversationItems; pub use conversations::Conversations; pub use embedding::Embeddings; diff --git a/async-openai/src/types/containers.rs b/async-openai/src/types/containers.rs new file mode 100644 index 00000000..b5d6201e --- /dev/null +++ b/async-openai/src/types/containers.rs @@ -0,0 +1,176 @@ +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::error::OpenAIError; + +use super::InputSource; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ContainerResource { + /// Unique identifier for the container. + pub id: String, + /// The type of this object. + pub object: String, + /// Name of the container. + pub name: String, + /// Unix timestamp (in seconds) when the container was created. + pub created_at: u32, + /// Status of the container (e.g., active, deleted). + pub status: String, + /// The container will expire after this time period. The anchor is the reference point for the expiration. + /// The minutes is the number of minutes after the anchor before the container expires. + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_after: Option, + /// Unix timestamp (in seconds) when the container was last active. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_active_at: Option, +} + +/// Expiration policy for containers. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ContainerExpiresAfter { + /// Time anchor for the expiration time. Currently only 'last_active_at' is supported. + pub anchor: ContainerExpiresAfterAnchor, + pub minutes: u32, +} + +/// Anchor for container expiration. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ContainerExpiresAfterAnchor { + LastActiveAt, +} + +/// Request to create a container. +/// openapi spec type: CreateContainerBody +#[derive(Debug, Default, Clone, Builder, PartialEq, Serialize)] +#[builder(name = "CreateContainerRequestArgs")] +#[builder(pattern = "mutable")] +#[builder(setter(into, strip_option), default)] +#[builder(derive(Debug))] +#[builder(build_fn(error = "OpenAIError"))] +pub struct CreateContainerRequest { + /// Name of the container to create. + pub name: String, + /// IDs of files to copy to the container. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_ids: Option>, + /// Container expiration time in minutes relative to the 'anchor' time. + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_after: Option, +} + +/// Response when listing containers. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ContainerListResource { + /// The type of object returned, must be 'list'. + pub object: String, + /// A list of containers. + pub data: Vec, + /// The ID of the first container in the list. + pub first_id: Option, + /// The ID of the last container in the list. + pub last_id: Option, + /// Whether there are more containers available. + pub has_more: bool, +} + +/// Response when deleting a container. +#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] +pub struct DeleteContainerResponse { + pub id: String, + pub object: String, + pub deleted: bool, +} + +/// Query parameters for listing containers. +#[derive(Debug, Serialize, Default, Clone, Builder, PartialEq)] +#[builder(name = "ListContainersQueryArgs")] +#[builder(pattern = "mutable")] +#[builder(setter(into, strip_option), default)] +#[builder(derive(Debug))] +#[builder(build_fn(error = "OpenAIError"))] +pub struct ListContainersQuery { + /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + /// A cursor for use in pagination. `after` is an object ID that defines your place in the list. + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, +} + +// Container File types + +/// The container file object represents a file in a container. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ContainerFileResource { + /// Unique identifier for the file. + pub id: String, + /// The type of this object (`container.file`). + pub object: String, + /// The container this file belongs to. + pub container_id: String, + /// Unix timestamp (in seconds) when the file was created. + pub created_at: u32, + /// Size of the file in bytes. + pub bytes: u32, + /// Path of the file in the container. + pub path: String, + /// Source of the file (e.g., `user`, `assistant`). + pub source: String, +} + +/// Request to create a container file. +/// openapi spec type: CreateContainerFileBody +#[derive(Debug, Default, Clone, PartialEq)] +pub struct CreateContainerFileRequest { + /// The File object (not file name) to be uploaded. + pub file: Option, + /// Name of the file to create. + pub file_id: Option, +} + +/// Response when listing container files. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ContainerFileListResource { + /// The type of object returned, must be 'list'. + pub object: String, + /// A list of container files. + pub data: Vec, + /// The ID of the first file in the list. + pub first_id: Option, + /// The ID of the last file in the list. + pub last_id: Option, + /// Whether there are more files available. + pub has_more: bool, +} + +/// Response when deleting a container file. +#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] +pub struct DeleteContainerFileResponse { + pub id: String, + pub object: String, + pub deleted: bool, +} + +/// Query parameters for listing container files. +#[derive(Debug, Serialize, Default, Clone, Builder, PartialEq)] +#[builder(name = "ListContainerFilesQueryArgs")] +#[builder(pattern = "mutable")] +#[builder(setter(into, strip_option), default)] +#[builder(derive(Debug))] +#[builder(build_fn(error = "OpenAIError"))] +pub struct ListContainerFilesQuery { + /// A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and `desc` for descending order. + #[serde(skip_serializing_if = "Option::is_none")] + pub order: Option, + /// A cursor for use in pagination. `after` is an object ID that defines your place in the list. + #[serde(skip_serializing_if = "Option::is_none")] + pub after: Option, +} diff --git a/async-openai/src/types/impls.rs b/async-openai/src/types/impls.rs index 972c6043..1bab963e 100644 --- a/async-openai/src/types/impls.rs +++ b/async-openai/src/types/impls.rs @@ -24,11 +24,12 @@ use super::{ ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageContent, ChatCompletionRequestToolMessage, ChatCompletionRequestToolMessageContent, ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent, - ChatCompletionRequestUserMessageContentPart, ChatCompletionToolChoiceOption, CreateFileRequest, - CreateImageEditRequest, CreateImageVariationRequest, CreateMessageRequestContent, - CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, CreateVideoRequest, - DallE2ImageSize, EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, - Image, ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse, + ChatCompletionRequestUserMessageContentPart, ChatCompletionToolChoiceOption, + CreateContainerFileRequest, CreateFileRequest, CreateImageEditRequest, + CreateImageVariationRequest, CreateMessageRequestContent, CreateSpeechResponse, + CreateTranscriptionRequest, CreateTranslationRequest, CreateVideoRequest, DallE2ImageSize, + EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, Image, + ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse, ModerationInput, Prompt, Role, Stop, TimestampGranularity, }; @@ -1020,6 +1021,24 @@ impl AsyncTryFrom for reqwest::multipart::Form { } } +impl AsyncTryFrom for reqwest::multipart::Form { + type Error = OpenAIError; + + async fn try_from(request: CreateContainerFileRequest) -> Result { + let mut form = reqwest::multipart::Form::new(); + + // Either file or file_id should be provided + if let Some(file_source) = request.file { + let file_part = create_file_part(file_source).await?; + form = form.part("file", file_part); + } else if let Some(file_id) = request.file_id { + form = form.text("file_id", file_id); + } + + Ok(form) + } +} + impl AsyncTryFrom for reqwest::multipart::Form { type Error = OpenAIError; diff --git a/async-openai/src/types/mod.rs b/async-openai/src/types/mod.rs index c6474aa5..fd2906f4 100644 --- a/async-openai/src/types/mod.rs +++ b/async-openai/src/types/mod.rs @@ -9,6 +9,7 @@ mod batch; mod chat; mod common; mod completion; +mod containers; mod embedding; mod file; mod fine_tuning; @@ -42,6 +43,7 @@ pub use batch::*; pub use chat::*; pub use common::*; pub use completion::*; +pub use containers::*; pub use embedding::*; pub use file::*; pub use fine_tuning::*; From 96acd50db1a275cbe4c5f5a4bbe8520f4e849863 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Wed, 5 Nov 2025 17:50:35 -0800 Subject: [PATCH 2/5] feat: add examples/containers --- examples/containers/Cargo.toml | 10 +++ examples/containers/src/main.rs | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 examples/containers/Cargo.toml create mode 100644 examples/containers/src/main.rs diff --git a/examples/containers/Cargo.toml b/examples/containers/Cargo.toml new file mode 100644 index 00000000..40736578 --- /dev/null +++ b/examples/containers/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "containers" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +async-openai = {path = "../../async-openai"} +tokio = { version = "1.43.0", features = ["full"] } + diff --git a/examples/containers/src/main.rs b/examples/containers/src/main.rs new file mode 100644 index 00000000..8b7b74f0 --- /dev/null +++ b/examples/containers/src/main.rs @@ -0,0 +1,113 @@ +use async_openai::{ + types::{ + ContainerExpiresAfter, ContainerExpiresAfterAnchor, CreateContainerFileRequest, + CreateContainerRequestArgs, InputSource, + }, + Client, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize the OpenAI client + let client = Client::new(); + + // Create a new container + println!("Creating a new container..."); + let create_request = CreateContainerRequestArgs::default() + .name("My Test Container") + .expires_after(ContainerExpiresAfter { + anchor: ContainerExpiresAfterAnchor::LastActiveAt, + minutes: 20, + }) + .build()?; + + let container = client.containers().create(create_request).await?; + println!("Created container with ID: {}", container.id); + println!("Container name: {}", container.name); + println!("Container status: {}", container.status); + + // List all containers + println!("\nListing all containers..."); + let query = [("limit", "10")]; + let list_response = client.containers().list(&query).await?; + println!("Found {} containers", list_response.data.len()); + for c in &list_response.data { + println!(" - {} ({})", c.name, c.id); + } + + // Retrieve the container + println!("\nRetrieving container..."); + let retrieved = client.containers().retrieve(&container.id).await?; + println!("Retrieved container: {}", retrieved.name); + + // Create a file in the container using in-memory content + println!("\nCreating a file in the container..."); + let file_content = b"Hello from the container!"; + let create_file_request = CreateContainerFileRequest { + file: Some(InputSource::VecU8 { + filename: "hello.txt".to_string(), + vec: file_content.to_vec(), + }), + file_id: None, + }; + + let container_file = client + .containers() + .files(&container.id) + .create(create_file_request) + .await?; + println!("Created file with ID: {}", container_file.id); + println!("File path: {}", container_file.path); + println!("File size: {} bytes", container_file.bytes); + + // List files in the container + println!("\nListing files in the container..."); + let files_query = [("limit", "10")]; + let files_list = client + .containers() + .files(&container.id) + .list(&files_query) + .await?; + println!("Found {} files", files_list.data.len()); + for f in &files_list.data { + println!(" - {} ({} bytes)", f.path, f.bytes); + } + + // Retrieve the file + println!("\nRetrieving the file..."); + let retrieved_file = client + .containers() + .files(&container.id) + .retrieve(&container_file.id) + .await?; + println!("Retrieved file: {}", retrieved_file.path); + + // Get file content + println!("\nRetrieving file content..."); + let content = client + .containers() + .files(&container.id) + .content(&container_file.id) + .await?; + println!( + "File content: {}", + String::from_utf8_lossy(content.as_ref()) + ); + + // Delete the file + println!("\nDeleting the file..."); + let delete_file_response = client + .containers() + .files(&container.id) + .delete(&container_file.id) + .await?; + println!("File deleted: {}", delete_file_response.deleted); + + // Delete the container + println!("\nDeleting the container..."); + let delete_response = client.containers().delete(&container.id).await?; + println!("Container deleted: {}", delete_response.deleted); + + println!("\nAll operations completed successfully!"); + Ok(()) +} From 84a537a0503d9f07aac2c0215b8149106d1d5f49 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Wed, 5 Nov 2025 17:51:44 -0800 Subject: [PATCH 3/5] updated readme --- async-openai/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/async-openai/README.md b/async-openai/README.md index 56e69d5c..b50b5e77 100644 --- a/async-openai/README.md +++ b/async-openai/README.md @@ -29,6 +29,7 @@ - [x] Chat - [x] Completions (Legacy) - [x] Conversations + - [x] Containers | Container Files - [x] Embeddings - [x] Files - [x] Fine-Tuning @@ -37,7 +38,7 @@ - [x] Moderations - [x] Organizations | Administration (partially implemented) - [x] Realtime GA (partially implemented) - - [x] Responses (partially implemented) + - [x] Responses - [x] Uploads - [x] Videos - Bring your own custom types for Request or Response objects. From eb63f34e20a3b013eedeab9b02b07ed7ea832a27 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Wed, 5 Nov 2025 17:55:34 -0800 Subject: [PATCH 4/5] cargo fmt --- async-openai/src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/async-openai/src/client.rs b/async-openai/src/client.rs index 4acd6f1e..e5db9a3d 100644 --- a/async-openai/src/client.rs +++ b/async-openai/src/client.rs @@ -13,9 +13,9 @@ use crate::{ image::Images, moderation::Moderations, traits::AsyncTryFrom, - Assistants, Audio, AuditLogs, Batches, Chat, Completions, Containers, Conversations, Embeddings, - FineTuning, Invites, Models, Projects, Responses, Threads, Uploads, Users, VectorStores, - Videos, + Assistants, Audio, AuditLogs, Batches, Chat, Completions, Containers, Conversations, + Embeddings, FineTuning, Invites, Models, Projects, Responses, Threads, Uploads, Users, + VectorStores, Videos, }; #[derive(Debug, Clone, Default)] From ab0addb5c558de848fbae581922876188013edd7 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Wed, 5 Nov 2025 17:58:15 -0800 Subject: [PATCH 5/5] clear docs on scope --- async-openai/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async-openai/README.md b/async-openai/README.md index b50b5e77..cede1991 100644 --- a/async-openai/README.md +++ b/async-openai/README.md @@ -174,7 +174,7 @@ To maintain quality of the project, a minimum of the following is a must for cod - **Names & Documentation**: All struct names, field names and doc comments are from OpenAPI spec. Nested objects in spec without names leaves room for making appropriate name. - **Tested**: For changes supporting test(s) and/or example is required. Existing examples, doc tests, unit tests, and integration tests should be made to work with the changes if applicable. -- **Scope**: Keep scope limited to APIs available in official documents such as [API Reference](https://platform.openai.com/docs/api-reference) or [OpenAPI spec](https://github.com/openai/openai-openapi/). Other LLMs or AI Providers offer OpenAI-compatible APIs, yet they may not always have full parity. In such cases, the OpenAI spec takes precedence. +- **Scope**: Keep scope limited to APIs available in official documents such as [API Reference](https://platform.openai.com/docs/api-reference) or [OpenAPI spec](https://github.com/openai/openai-openapi/). Other LLMs or AI Providers offer OpenAI-compatible APIs, yet they may not always have full parity - for those use `byot` feature. - **Consistency**: Keep code style consistent across all the "APIs" that library exposes; it creates a great developer experience. This project adheres to [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)