Skip to content

Commit

Permalink
Add Uploads support
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBratanov committed Jul 24, 2024
1 parent 71ffbbb commit 7d6bb0e
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 8 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ ChatCompletion chatCompletion = chatClient.createChatCompletion(createChatComple
| [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) | ✔️ |
| [Batch](https://platform.openai.com/docs/api-reference/batch) | ✔️ |
| [Files](https://platform.openai.com/docs/api-reference/files) | ✔️ |
| [Uploads](https://platform.openai.com/docs/api-reference/uploads) | ✔️ |
| [Images](https://platform.openai.com/docs/api-reference/images) | ✔️ |
| [Models](https://platform.openai.com/docs/api-reference/models) | ✔️ |
| [Moderations](https://platform.openai.com/docs/api-reference/moderations) | ✔️ |
Expand Down Expand Up @@ -203,6 +204,28 @@ Batch batch = batchClient.createBatch(request);
Batch retrievedBatch = batchClient.retrieveBatch(batch.id());
System.out.println(retrievedBatch.status());
```
- Upload large file in multiple parts
```java
UploadsClient uploadsClient = openAI.uploadsClient();
CreateUploadRequest createUploadRequest = CreateUploadRequest.newBuilder()
.filename("training_examples.jsonl")
.purpose(Purpose.FINE_TUNE)
.bytes(2147483648)
.mimeType("text/jsonl")
.build();
Upload upload = uploadsClient.createUpload(createUploadRequest);

UploadPart part1 = uploadsClient.addUploadPart(upload.id(), Paths.get("/tmp/part1.jsonl"));
UploadPart part2 = uploadsClient.addUploadPart(upload.id(), Paths.get("/tmp/part2.jsonl"));

CompleteUploadRequest completeUploadRequest = CompleteUploadRequest.newBuilder()
.partIds(List.of(part1.id(), part2.id()))
.build();

Upload completedUpload = uploadsClient.completeUpload(upload.id(), completeUploadRequest);
// the created usable File object
File file = completedUpload.file();
```
- Build AI Assistant
```java
AssistantsClient assistantsClient = openAI.assistantsClient();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.stefanbratanov.jvm.openai;

import java.util.List;
import java.util.Optional;

public record CompleteUploadRequest(List<String> partIds, Optional<String> md5) {

public static Builder newBuilder() {
return new Builder();
}

public static class Builder {

private List<String> partIds;

private Optional<String> md5 = Optional.empty();

/**
* @param partIds The ordered list of Part IDs.
*/
public Builder partIds(List<String> partIds) {
this.partIds = partIds;
return this;
}

/**
* @param md5 The optional md5 checksum for the file contents to verify if the bytes uploaded
* matches what you expect.
*/
public Builder md5(String md5) {
this.md5 = Optional.of(md5);
return this;
}

public CompleteUploadRequest build() {
return new CompleteUploadRequest(partIds, md5);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.github.stefanbratanov.jvm.openai;

public record CreateUploadRequest(String filename, String purpose, int bytes, String mimeType) {

public static Builder newBuilder() {
return new Builder();
}

public static class Builder {

private String filename;
private String purpose;
private int bytes;
private String mimeType;

/**
* @param filename The name of the file to upload.
*/
public Builder filename(String filename) {
this.filename = filename;
return this;
}

/**
* @param purpose The intended purpose of the uploaded file.
*/
public Builder purpose(String purpose) {
this.purpose = purpose;
return this;
}

/**
* @param purpose The intended purpose of the uploaded file.
*/
public Builder purpose(Purpose purpose) {
this.purpose = purpose.getId();
return this;
}

/**
* @param bytes The number of bytes in the file you are uploading.
*/
public Builder bytes(int bytes) {
this.bytes = bytes;
return this;
}

/**
* @param mimeType The MIME type of the file.
* <p>This must fall within the supported MIME types for your file purpose. See the
* supported MIME types for assistants and vision.
*/
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}

public CreateUploadRequest build() {
return new CreateUploadRequest(filename, purpose, bytes, mimeType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum Endpoint {
FILES("files"),
FINE_TUNING("fine_tuning/jobs"),
BATCHES("batches"),
UPLOADS("uploads"),
// Beta
THREADS("threads"),
ASSISTANTS("assistants"),
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public final class OpenAI {
private final FineTuningClient fineTuningClient;
private final BatchClient batchClient;
private final FilesClient filesClient;
private final UploadsClient uploadsClient;
private final ImagesClient imagesClient;
private final ModelsClient modelsClient;
private final ModerationsClient moderationsClient;
Expand Down Expand Up @@ -48,6 +49,7 @@ private OpenAI(
new FineTuningClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
batchClient = new BatchClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
filesClient = new FilesClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
uploadsClient = new UploadsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
imagesClient = new ImagesClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
modelsClient = new ModelsClient(baseUrl, authenticationHeaders, httpClient, requestTimeout);
moderationsClient =
Expand Down Expand Up @@ -115,6 +117,14 @@ public FilesClient filesClient() {
return filesClient;
}

/**
* @return a client based on <a
* href="https://platform.openai.com/docs/api-reference/uploads">Uploads</a>
*/
public UploadsClient uploadsClient() {
return uploadsClient;
}

/**
* @return a client based on <a
* href="https://platform.openai.com/docs/api-reference/images">Images</a>
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/Upload.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.stefanbratanov.jvm.openai;

public record Upload(
String id,
int createdAt,
String filename,
int bytes,
String purpose,
String status,
int expiresAt,
File file) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.stefanbratanov.jvm.openai;

public record UploadPart(String id, int createdAt, String uploadId) {}
119 changes: 119 additions & 0 deletions src/main/java/io/github/stefanbratanov/jvm/openai/UploadsClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.github.stefanbratanov.jvm.openai;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;

/**
* Allows you to upload large files in multiple parts.
*
* <p>Based on <a href="https://platform.openai.com/docs/api-reference/uploads">Uploads</a>
*/
public final class UploadsClient extends OpenAIClient {

private static final String PARTS_SEGMENT = "/parts";
private static final String COMPLETE_SEGMENT = "/complete";
private static final String CANCEL_SEGMENT = "/cancel";

private final URI baseUrl;

UploadsClient(
URI baseUrl,
String[] authenticationHeaders,
HttpClient httpClient,
Optional<Duration> requestTimeout) {
super(authenticationHeaders, httpClient, requestTimeout);
this.baseUrl = baseUrl;
}

/**
* Creates an intermediate Upload object that you can add Parts to. Currently, an Upload can
* accept at most 8 GB in total and expires after an hour after you create it.
*
* <p>Once you complete the Upload, we will create a File object that contains all the parts you
* uploaded. This File is usable in the rest of our platform as a regular File object.
*
* @throws OpenAIException in case of API errors
*/
public Upload createUpload(CreateUploadRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.UPLOADS.getPath()))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Upload.class);
}

/**
* Adds a Part to an Upload object. A Part represents a chunk of bytes from the file you are
* trying to upload.
*
* <p>Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8
* GB.
*
* <p>It is possible to add multiple Parts in parallel. You can decide the intended order of the
* Parts when you complete the Upload.
*
* @param uploadId The ID of the Upload.
* @param data The chunk of bytes for this Part.
* @throws OpenAIException in case of API errors
*/
public UploadPart addUploadPart(String uploadId, Path data) {
MultipartBodyPublisher multipartBodyPublisher =
MultipartBodyPublisher.newBuilder().filePart("data", data).build();
HttpRequest httpRequest =
newHttpRequestBuilder(
Constants.CONTENT_TYPE_HEADER, multipartBodyPublisher.getContentTypeHeader())
.uri(baseUrl.resolve(Endpoint.UPLOADS.getPath() + "/" + uploadId + PARTS_SEGMENT))
.POST(multipartBodyPublisher)
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), UploadPart.class);
}

/**
* Completes the Upload.
*
* <p>Within the returned Upload object, there is a nested File object that is ready to use in the
* rest of the platform.
*
* <p>You can specify the order of the Parts by passing in an ordered list of the Part IDs.
*
* <p>The number of bytes uploaded upon completion must match the number of bytes initially
* specified when creating the Upload object. No Parts may be added after an Upload is completed.
*
* @param uploadId The ID of the Upload.
* @throws OpenAIException in case of API errors
*/
public Upload completeUpload(String uploadId, CompleteUploadRequest request) {
HttpRequest httpRequest =
newHttpRequestBuilder(Constants.CONTENT_TYPE_HEADER, Constants.JSON_MEDIA_TYPE)
.uri(baseUrl.resolve(Endpoint.UPLOADS.getPath() + "/" + uploadId + COMPLETE_SEGMENT))
.POST(createBodyPublisher(request))
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Upload.class);
}

/**
* Cancels the Upload. No Parts may be added after an Upload is cancelled.
*
* @param uploadId The ID of the Upload.
* @throws OpenAIException in case of API errors
*/
public Upload cancelUpload(String uploadId) {
HttpRequest httpRequest =
newHttpRequestBuilder()
.uri(baseUrl.resolve(Endpoint.UPLOADS.getPath() + "/" + uploadId + CANCEL_SEGMENT))
.POST(BodyPublishers.noBody())
.build();
HttpResponse<byte[]> httpResponse = sendHttpRequest(httpRequest);
return deserializeResponse(httpResponse.body(), Upload.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

import io.github.stefanbratanov.jvm.openai.ContentPart.TextContentPart;
import io.github.stefanbratanov.jvm.openai.CreateChatCompletionRequest.StreamOptions;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.http.HttpTimeoutException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
Expand Down Expand Up @@ -371,6 +373,48 @@ void testFilesClient() {
assertThat(retrievedFile).isEqualTo(uploadedFile);
}

@Test
void testUploadsClient(@TempDir Path tempDir) throws IOException {
UploadsClient uploadsClient = openAI.uploadsClient();
FilesClient filesClient = openAI.filesClient();

CreateUploadRequest createUploadRequest =
CreateUploadRequest.newBuilder()
.filename("hello.txt")
.purpose(Purpose.BATCH)
.bytes(11)
.mimeType("text/plain")
.build();

Upload upload = uploadsClient.createUpload(createUploadRequest);

Path part1 = tempDir.resolve("part1.txt");
Path part2 = tempDir.resolve("part2.txt");

Files.writeString(part1, "Hello ");
Files.writeString(part2, "World");

UploadPart uploadPart = uploadsClient.addUploadPart(upload.id(), part1);
UploadPart uploadPart2 = uploadsClient.addUploadPart(upload.id(), part2);

CompleteUploadRequest completeUploadRequest =
CompleteUploadRequest.newBuilder()
.partIds(List.of(uploadPart.id(), uploadPart2.id()))
.build();

Upload completedUpload = uploadsClient.completeUpload(upload.id(), completeUploadRequest);

assertThat(completedUpload.status()).isEqualTo("completed");

File file = completedUpload.file();

assertThat(file).isNotNull();

byte[] retrievedContent = filesClient.retrieveFileContent(file.id());

assertThat(new String(retrievedContent)).isEqualTo("Hello World");
}

@Test // using mock server because fine-tuning models are costly
void testFineTuningClient() {
FineTuningClient fineTuningClient = openAIWithMockServer.fineTuningClient();
Expand Down
Loading

0 comments on commit 7d6bb0e

Please sign in to comment.