From 47ee4413462bbf4eaf6b3ae324656af2ce6e4079 Mon Sep 17 00:00:00 2001 From: Christian Soltenborn Date: Wed, 26 Jul 2023 23:13:45 +0200 Subject: [PATCH 01/19] added timeout duration to service instantiation for demonstration purposes (#341) Co-authored-by: Christian Soltenborn --- example/src/main/java/example/OpenAiApiExample.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/src/main/java/example/OpenAiApiExample.java b/example/src/main/java/example/OpenAiApiExample.java index e6e78b44..7a245ef9 100644 --- a/example/src/main/java/example/OpenAiApiExample.java +++ b/example/src/main/java/example/OpenAiApiExample.java @@ -7,6 +7,7 @@ import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.image.CreateImageRequest; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -14,7 +15,7 @@ class OpenAiApiExample { public static void main(String... args) { String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); + OpenAiService service = new OpenAiService(token, Duration.ofSeconds(30)); System.out.println("\nCreating completion..."); CompletionRequest completionRequest = CompletionRequest.builder() From cd2be868854fa0c883998ea8a8b548bee171270d Mon Sep 17 00:00:00 2001 From: YU ZHANG Date: Thu, 27 Jul 2023 05:17:29 +0800 Subject: [PATCH 02/19] New features: token calculation logic and account information query logic. (#311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tool for calculating token consumption quantity. API for querying account balance * feat: * feat: update jtokkit --------- Co-authored-by: 大名(张瑜) --- api/build.gradle | 1 + .../openai/billing/BillingUsage.java | 29 ++ .../openai/billing/CreditGrantsResponse.java | 35 +++ .../theokanning/openai/billing/DailyCost.java | 24 ++ .../com/theokanning/openai/billing/Datum.java | 36 +++ .../theokanning/openai/billing/Grants.java | 17 + .../theokanning/openai/billing/LineItem.java | 21 ++ .../com/theokanning/openai/billing/Plan.java | 13 + .../openai/billing/Subscription.java | 51 +++ .../openai/utils/TikTokensUtil.java | 297 ++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 24 ++ .../main/java/example/TikTokensExample.java | 21 ++ gradle/libs.versions.toml | 1 + .../openai/service/OpenAiService.java | 27 ++ 14 files changed, 597 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/billing/BillingUsage.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/DailyCost.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Datum.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Grants.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/LineItem.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Plan.java create mode 100644 api/src/main/java/com/theokanning/openai/billing/Subscription.java create mode 100644 api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java create mode 100644 example/src/main/java/example/TikTokensExample.java diff --git a/api/build.gradle b/api/build.gradle index b861a367..db97b833 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -4,6 +4,7 @@ apply plugin: "com.vanniktech.maven.publish" dependencies { api libs.jacksonAnnotations api libs.jacksonDatabind + api libs.jtokkit compileOnly libs.lombok annotationProcessor libs.lombok diff --git a/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java b/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java new file mode 100644 index 00000000..3c6db957 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/BillingUsage.java @@ -0,0 +1,29 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Amount consumption information + * + */ +@Data +public class BillingUsage { + + @JsonProperty("object") + private String object; + /** + * Account expenditure details + */ + @JsonProperty("daily_costs") + private List dailyCosts; + /** + * Total usage amount: cents + */ + @JsonProperty("total_usage") + private BigDecimal totalUsage; + +} diff --git a/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java b/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java new file mode 100644 index 00000000..71f681d1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/CreditGrantsResponse.java @@ -0,0 +1,35 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * Return value of balance inquiry interface + * + */ +@Data +public class CreditGrantsResponse implements Serializable { + private String object; + /** + * Total amount: US dollars + */ + @JsonProperty("total_granted") + private BigDecimal totalGranted; + /** + * Total usage amount: US dollars + */ + @JsonProperty("total_used") + private BigDecimal totalUsed; + /** + * Total remaining amount: US dollars + */ + @JsonProperty("total_available") + private BigDecimal totalAvailable; + /** + * Balance details + */ + private Grants grants; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/DailyCost.java b/api/src/main/java/com/theokanning/openai/billing/DailyCost.java new file mode 100644 index 00000000..6aede61c --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/DailyCost.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * List of amount consumption + * + */ +@Data +public class DailyCost { + /** + * + */ + @JsonProperty("timestamp") + private long timestamp; + /** + * Model consumption amount details + */ + @JsonProperty("line_items") + private List lineItems; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Datum.java b/api/src/main/java/com/theokanning/openai/billing/Datum.java new file mode 100644 index 00000000..2eee7b3f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Datum.java @@ -0,0 +1,36 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * + * + */ +@Data +public class Datum { + private String object; + private String id; + /** + * Gift amount: US dollars + */ + @JsonProperty("grant_amount") + private BigDecimal grantAmount; + /** + * Usage amount: US dollars + */ + @JsonProperty("used_amount") + private BigDecimal usedAmount; + /** + * Effective timestamp + */ + @JsonProperty("effective_at") + private Long effectiveAt; + /** + * Expiration timestamp + */ + @JsonProperty("expires_at") + private Long expiresAt; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Grants.java b/api/src/main/java/com/theokanning/openai/billing/Grants.java new file mode 100644 index 00000000..8826f04a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Grants.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * + * + */ +@Data +public class Grants { + private String object; + @JsonProperty("data") + private List data; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/LineItem.java b/api/src/main/java/com/theokanning/openai/billing/LineItem.java new file mode 100644 index 00000000..e12c4912 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/LineItem.java @@ -0,0 +1,21 @@ +package com.theokanning.openai.billing; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * List of amount consumption + * + */ +@Data +public class LineItem { + /** + * model name + */ + private String name; + /** + * Expenditure amount + */ + private BigDecimal cost; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Plan.java b/api/src/main/java/com/theokanning/openai/billing/Plan.java new file mode 100644 index 00000000..e3db3dd4 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Plan.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.billing; + +import lombok.Data; + +/** + * + * + */ +@Data +public class Plan { + private String title; + private String id; +} diff --git a/api/src/main/java/com/theokanning/openai/billing/Subscription.java b/api/src/main/java/com/theokanning/openai/billing/Subscription.java new file mode 100644 index 00000000..541e5af6 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/billing/Subscription.java @@ -0,0 +1,51 @@ +package com.theokanning.openai.billing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * Account information + * + * + */ +@Data +public class Subscription { + @JsonProperty("object") + private String object; + @JsonProperty("has_payment_method") + private boolean hasPaymentMethod; + @JsonProperty("canceled") + private boolean canceled; + @JsonProperty("canceled_at") + private Object canceledAt; + @JsonProperty("delinquent") + private Object delinquent; + @JsonProperty("access_until") + private long accessUntil; + @JsonProperty("soft_limit") + private long softLimit; + @JsonProperty("hard_limit") + private long hardLimit; + @JsonProperty("system_hard_limit") + private long systemHardLimit; + @JsonProperty("soft_limit_usd") + private double softLimitUsd; + @JsonProperty("hard_limit_usd") + private double hardLimitUsd; + @JsonProperty("system_hard_limit_usd") + private double systemHardLimitUsd; + @JsonProperty("plan") + private Plan plan; + @JsonProperty("account_name") + private String accountName; + @JsonProperty("po_number") + private Object poNumber; + @JsonProperty("billing_email") + private Object billingEmail; + @JsonProperty("tax_ids") + private Object taxIds; + @JsonProperty("billing_address") + private Object billingAddress; + @JsonProperty("business_address") + private Object businessAddress; +} diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java new file mode 100644 index 00000000..8250a018 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -0,0 +1,297 @@ +package com.theokanning.openai.utils; + +import com.knuddels.jtokkit.Encodings; +import com.knuddels.jtokkit.api.Encoding; +import com.knuddels.jtokkit.api.EncodingRegistry; +import com.knuddels.jtokkit.api.EncodingType; +import com.knuddels.jtokkit.api.ModelType; +import com.theokanning.openai.completion.chat.ChatMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +/** + * Token calculation tool class + */ +@Slf4j +public class TikTokensUtil { + /** + * Model name corresponds to Encoding + */ + private static final Map modelMap = new HashMap<>(); + /** + * Registry instance + */ + private static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); + + static { + for (ModelType modelType : ModelType.values()) { + modelMap.put(modelType.getName(), registry.getEncodingForModel(modelType)); + } + modelMap.put(ModelEnum.GPT_3_5_TURBO_0301.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO)); + modelMap.put(ModelEnum.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + modelMap.put(ModelEnum.GPT_4_32K_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + modelMap.put(ModelEnum.GPT_4_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + } + + /** + * Get encoding array through Encoding and text. + * + * @param enc Encoding type + * @param text Text information + * @return Encoding array + */ + public static List encode(Encoding enc, String text) { + return isBlank(text) ? new ArrayList<>() : enc.encode(text); + } + + /** + * Calculate tokens of text information through Encoding. + * + * @param enc Encoding type + * @param text Text information + * @return Number of tokens + */ + public static int tokens(Encoding enc, String text) { + return encode(enc, text).size(); + } + + + /** + * Reverse calculate text information through Encoding and encoded array + * + * @param enc Encoding + * @param encoded Encoding array + * @return Text information corresponding to the encoding array. + */ + public static String decode(Encoding enc, List encoded) { + return enc.decode(encoded); + } + + /** + * Get an Encoding object by Encoding type + * + * @param encodingType + * @return Encoding + */ + public static Encoding getEncoding(EncodingType encodingType) { + Encoding enc = registry.getEncoding(encodingType); + return enc; + } + + /** + * Obtain the encoding array by encoding; + * + * @param text + * @return Encoding array + */ + public static List encode(EncodingType encodingType, String text) { + if (isBlank(text)) { + return new ArrayList<>(); + } + Encoding enc = getEncoding(encodingType); + List encoded = enc.encode(text); + return encoded; + } + + /** + * Compute the tokens of the specified string through EncodingType. + * + * @param encodingType + * @param text + * @return Number of tokens + */ + public static int tokens(EncodingType encodingType, String text) { + return encode(encodingType, text).size(); + } + + + /** + * Reverse the encoded array to get the string text using EncodingType and the encoded array. + * + * @param encodingType + * @param encoded + * @return The string corresponding to the encoding array. + */ + public static String decode(EncodingType encodingType, List encoded) { + Encoding enc = getEncoding(encodingType); + return enc.decode(encoded); + } + + + /** + * Get an Encoding object by model name. + * + * @param modelName + * @return Encoding + */ + public static Encoding getEncoding(String modelName) { + return modelMap.get(modelName); + } + + /** + * Get the encoded array by model name using encode. + * + * @param text Text information + * @return Encoding array + */ + public static List encode(String modelName, String text) { + if (isBlank(text)) { + return new ArrayList<>(); + } + Encoding enc = getEncoding(modelName); + if (Objects.isNull(enc)) { + return new ArrayList<>(); + } + List encoded = enc.encode(text); + return encoded; + } + + /** + * Calculate the tokens of a specified string by model name. + * + * @param modelName + * @param text + * @return Number of tokens + */ + public static int tokens(String modelName, String text) { + return encode(modelName, text).size(); + } + + + /** + * Calculate the encoded array for messages by model name. + * Refer to the official processing logic: + * https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + * + * @param modelName + * @param messages + * @return Number of tokens + */ + public static int tokens(String modelName, List messages) { + Encoding encoding = getEncoding(modelName); + int tokensPerMessage = 0; + int tokensPerName = 0; + //3.5统一处理 + if (modelName.equals("gpt-3.5-turbo-0301") || modelName.equals("gpt-3.5-turbo")) { + tokensPerMessage = 4; + tokensPerName = -1; + } + //4.0统一处理 + if (modelName.equals("gpt-4") || modelName.equals("gpt-4-0314")) { + tokensPerMessage = 3; + tokensPerName = 1; + } + int sum = 0; + for (ChatMessage msg : messages) { + sum += tokensPerMessage; + sum += tokens(encoding, msg.getContent()); + sum += tokens(encoding, msg.getRole()); + sum += tokens(encoding, msg.getName()); + if (isNotBlank(msg.getName())) { + sum += tokensPerName; + } + } + sum += 3; + return sum; + } + + /** + * Reverse the string text through the model name and the encoded array. + * + * @param modelName + * @param encoded + * @return + */ + public static String decode(String modelName, List encoded) { + Encoding enc = getEncoding(modelName); + return enc.decode(encoded); + } + + + /** + * Obtain the modelType. + * + * @param name + * @return + */ + public static ModelType getModelTypeByName(String name) { + if (ModelEnum.GPT_3_5_TURBO_0301.getName().equals(name)) { + return ModelType.GPT_3_5_TURBO; + } + if (ModelEnum.GPT_4.getName().equals(name) + || ModelEnum.GPT_4_32K.getName().equals(name) + || ModelEnum.GPT_4_32K_0314.getName().equals(name) + || ModelEnum.GPT_4_0314.getName().equals(name)) { + return ModelType.GPT_4; + } + + for (ModelType modelType : ModelType.values()) { + if (modelType.getName().equals(name)) { + return modelType; + } + } + return null; + } + + @Getter + @AllArgsConstructor + public enum ModelEnum { + /** + * gpt-3.5-turbo + */ + GPT_3_5_TURBO("gpt-3.5-turbo"), + /** + * Temporary model, not recommended for use. + */ + GPT_3_5_TURBO_0301("gpt-3.5-turbo-0301"), + /** + * GPT4.0 + */ + GPT_4("gpt-4"), + /** + * Temporary model, not recommended for use. + */ + GPT_4_0314("gpt-4-0314"), + /** + * GPT4.0 超长上下文 + */ + GPT_4_32K("gpt-4-32k"), + /** + * Temporary model, not recommended for use. + */ + GPT_4_32K_0314("gpt-4-32k-0314"), + ; + private String name; + } + + public static boolean isBlankChar(int c) { + return Character.isWhitespace(c) || Character.isSpaceChar(c) || c == 65279 || c == 8234 || c == 0 || c == 12644 || c == 10240 || c == 6158; + } + + public static boolean isBlankChar(char c) { + return isBlankChar((int) c); + } + + public static boolean isNotBlank(CharSequence str) { + return !isBlank(str); + } + + public static boolean isBlank(CharSequence str) { + int length; + if (str != null && (length = str.length()) != 0) { + for (int i = 0; i < length; ++i) { + if (!isBlankChar(str.charAt(i))) { + return false; + } + } + + return true; + } else { + return true; + } + } + +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 72f5c9e2..1563b91b 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -4,6 +4,8 @@ import com.theokanning.openai.OpenAiResponse; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; +import com.theokanning.openai.billing.BillingUsage; +import com.theokanning.openai.billing.Subscription; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.CompletionResult; import com.theokanning.openai.completion.chat.ChatCompletionRequest; @@ -29,6 +31,8 @@ import retrofit2.Call; import retrofit2.http.*; +import java.time.LocalDate; + public interface OpenAiApi { @GET("v1/models") @@ -128,4 +132,24 @@ public interface OpenAiApi { @Deprecated @GET("/v1/engines/{engine_id}") Single getEngine(@Path("engine_id") String engineId); + + /** + * Account information inquiry: It contains total amount (in US dollars) and other information. + * + * @return + */ + @GET("v1/dashboard/billing/subscription") + Single subscription(); + + /** + * Account call interface consumption amount inquiry. + * totalUsage = Total amount used by the account (in US cents). + * + * @param starDate + * @param endDate + * @return Consumption amount information. + */ + @GET("v1/dashboard/billing/usage") + Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + } diff --git a/example/src/main/java/example/TikTokensExample.java b/example/src/main/java/example/TikTokensExample.java new file mode 100644 index 00000000..3da1a4d7 --- /dev/null +++ b/example/src/main/java/example/TikTokensExample.java @@ -0,0 +1,21 @@ +package example; + +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import com.theokanning.openai.utils.TikTokensUtil; + +import java.util.ArrayList; +import java.util.List; + +class TikTokensExample { + + public static void main(String... args) { + List messages = new ArrayList<>(); + messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), "Hello OpenAI 1.")); + messages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), "Hello OpenAI 2. ")); + + int tokens_1 = TikTokensUtil.tokens(TikTokensUtil.ModelEnum.GPT_3_5_TURBO.getName(), messages); + int tokens_2 = TikTokensUtil.tokens(TikTokensUtil.ModelEnum.GPT_3_5_TURBO.getName(), "Hello OpenAI 1."); + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1f0c605..ecccc347 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,3 +12,4 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit retrofitJackson = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } retrofitRxJava2 = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" } retrofitMock = { module = "com.squareup.retrofit2:retrofit-mock", version.ref = "retrofit" } +jtokkit = { module = "com.knuddels:jtokkit", version = "0.5.1" } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index e1cb856f..63da487c 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -12,6 +12,8 @@ import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; +import com.theokanning.openai.billing.BillingUsage; +import com.theokanning.openai.billing.Subscription; import com.theokanning.openai.client.OpenAiApi; import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; @@ -42,8 +44,10 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.jackson.JacksonConverterFactory; +import javax.validation.constraints.NotNull; import java.io.IOException; import java.time.Duration; +import java.time.LocalDate; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; @@ -440,4 +444,27 @@ public Flowable mapStreamToAccumulator(Flowable subscription = api.subscription(); + return subscription.blockingGet(); + } + + /** + * Account API consumption amount information inquiry. + * Up to 100 days of inquiry. + * + * @param starDate + * @param endDate + * @return Consumption amount information. + */ + public BillingUsage billingUsage(@NotNull LocalDate starDate, @NotNull LocalDate endDate) { + Single billingUsage = api.billingUsage(starDate, endDate); + return billingUsage.blockingGet(); + } + } From 332855d75cb71b2b1915491dfc7153e28ae499de Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 4 Aug 2023 17:52:14 -0500 Subject: [PATCH 03/19] Update to version 0.15.0 --- .../main/java/com/theokanning/openai/utils/TikTokensUtil.java | 2 -- gradle.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java index 8250a018..c30871f7 100644 --- a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -8,14 +8,12 @@ import com.theokanning.openai.completion.chat.ChatMessage; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.extern.slf4j.Slf4j; import java.util.*; /** * Token calculation tool class */ -@Slf4j public class TikTokensUtil { /** * Model name corresponds to Encoding diff --git a/gradle.properties b/gradle.properties index fe983327..de7ec97b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.14.0 +VERSION_NAME=0.15.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 9b28ab790c84b4539481af250010bc3943ceb576 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 4 Aug 2023 18:02:38 -0500 Subject: [PATCH 04/19] Fix tests Translation result changed, and for some reason my text was no longer flagged --- .../test/java/com/theokanning/openai/service/AudioTest.java | 4 ++-- .../java/com/theokanning/openai/service/ModerationTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java index d4f7b61b..d5a54a23 100644 --- a/service/src/test/java/com/theokanning/openai/service/AudioTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -52,7 +52,7 @@ void createTranslation() { .build(); String text = service.createTranslation(createTranslationRequest, koreanAudioFilePath).getText(); - assertEquals("Hello, my name is Yuna. I am Korean voice.", text); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", text); } @Test @@ -63,7 +63,7 @@ void createTranslationVerbose() { .build(); TranslationResult result = service.createTranslation(createTranslationRequest, koreanAudioFilePath); - assertEquals("Hello, my name is Yuna. I am Korean voice.", result.getText()); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", result.getText()); assertEquals("translate", result.getTask()); assertEquals("english", result.getLanguage()); assertTrue(result.getDuration() > 0); diff --git a/service/src/test/java/com/theokanning/openai/service/ModerationTest.java b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java index 9df3b447..f28bc083 100644 --- a/service/src/test/java/com/theokanning/openai/service/ModerationTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java @@ -15,7 +15,7 @@ public class ModerationTest { @Test void createModeration() { ModerationRequest moderationRequest = ModerationRequest.builder() - .input("I want to kill them") + .input("I want to kill him") .model("text-moderation-latest") .build(); From 4a9cfc247fdecca790cab77b905db566294d25c2 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 09:47:33 -0500 Subject: [PATCH 05/19] Add workflow to make sure code compiles on pull requests (#359) Pull requests can't run most tests because they require an OpenAI token secret, but they should at least compile. --- .github/workflows/pull_request.yml | 21 +++++++++++++++++++++ .github/workflows/test.yml | 2 -- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..77d94afb --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,21 @@ +name: Compile + +on: + pull_request: + branches: [ main ] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 8 + + - name: Compile + run: ./gradlew compileJava compileTestJava diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbf1af23..830e797f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,6 @@ name: Test on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: test: From 2a9abd0e2d2415ff338ff6f9780cb347562cdf06 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 25 Aug 2023 22:57:56 +0800 Subject: [PATCH 06/19] Fixes #123 (#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Tool for calculating token consumption quantity. API for querying account balance * feat: * feat: update jtokkit * Fixes #349 --------- Co-authored-by: 大名(张瑜) --- .../src/main/java/com/theokanning/openai/client/OpenAiApi.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 1563b91b..d841e8ad 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -138,6 +138,7 @@ public interface OpenAiApi { * * @return */ + @Deprecated @GET("v1/dashboard/billing/subscription") Single subscription(); @@ -149,6 +150,7 @@ public interface OpenAiApi { * @param endDate * @return Consumption amount information. */ + @Deprecated @GET("v1/dashboard/billing/usage") Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); From 253aff44758e077d68135d22b8d5e5a791349ec6 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 11:05:29 -0500 Subject: [PATCH 07/19] Add latest Fine Tuning API (#360) This api supports fine tuning chat models --- README.md | 3 +- .../openai/fine_tuning/FineTuningEvent.java | 38 ++++++++ .../openai/fine_tuning/FineTuningJob.java | 90 +++++++++++++++++++ .../fine_tuning/FineTuningJobRequest.java | 46 ++++++++++ .../openai/fine_tuning/Hyperparameters.java | 28 ++++++ .../openai/finetune/FineTuneEvent.java | 1 + .../openai/finetune/FineTuneRequest.java | 1 + .../openai/finetune/FineTuneResult.java | 1 + .../openai/finetune/HyperParameters.java | 1 + .../theokanning/openai/client/OpenAiApi.java | 23 +++++ .../openai/service/OpenAiService.java | 28 ++++++ .../openai/service/FineTuneTest.java | 1 + .../openai/service/FineTuningTest.java | 86 ++++++++++++++++++ .../resources/chat-fine-tuning-data.jsonl | 10 +++ 14 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java create mode 100644 service/src/test/java/com/theokanning/openai/service/FineTuningTest.java create mode 100644 service/src/test/resources/chat-fine-tuning-data.jsonl diff --git a/README.md b/README.md index 1824b5e5..20a5dcb8 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,13 @@ as well as an example project using the service. - [Embeddings](https://platform.openai.com/docs/api-reference/embeddings) - [Audio](https://platform.openai.com/docs/api-reference/audio) - [Files](https://platform.openai.com/docs/api-reference/files) -- [Fine-tunes](https://platform.openai.com/docs/api-reference/fine-tunes) +- [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning) - [Images](https://platform.openai.com/docs/api-reference/images) - [Moderations](https://platform.openai.com/docs/api-reference/moderations) #### Deprecated by OpenAI - [Engines](https://platform.openai.com/docs/api-reference/engines) +- [Legacy Fine-Tunes](https://platform.openai.com/docs/guides/legacy-fine-tuning) ## Importing diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java new file mode 100644 index 00000000..097bf7ce --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -0,0 +1,38 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * An object representing an event in the lifecycle of a fine-tuning job + * + * https://platform.openai.com/docs/api-reference/fine-tuning/list-events + */ +@Data +public class FineTuningEvent { + /** + * The type of object returned, should be "fine-tuneing.job.event". + */ + String object; + + /** + * The creation time in epoch seconds. + */ + @JsonProperty("created_at") + Long createdAt; + + /** + * The log level of this message. + */ + String level; + + /** + * The event message. + */ + String message; + + /** + * The type of event, i.e. "message" + */ + String type; +} diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java new file mode 100644 index 00000000..685d751e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJob.java @@ -0,0 +1,90 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * Fine-tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/object + */ +@Data +public class FineTuningJob { + /** + * The object identifier, which can be referenced in the API endpoints. + */ + String id; + + /** + * The object type, which is always "fine_tuning.job". + */ + String object; + + /** + * The unix timestamp for when the fine-tuning job was created. + */ + @JsonProperty("created_at") + Long createdAt; + + /** + * The unix timestamp for when the fine-tuning job was finished. + */ + @JsonProperty("finished_at") + Long finishedAt; + + /** + * The base model that is being fine-tuned. + */ + String model; + + /** + * The name of the fine-tuned model that is being created. + * Can be null if no fine-tuned model is created yet. + */ + @JsonProperty("fine_tuned_model") + String fineTunedModel; + + /** + * The organization that owns the fine-tuning job. + */ + @JsonProperty("organization_id") + String organizationId; + + /** + * The current status of the fine-tuning job. + * Can be either created, pending, running, succeeded, failed, or cancelled. + */ + String status; + + /** + * The hyperparameters used for the fine-tuning job. + * See the fine-tuning guide for more details. + */ + Hyperparameters hyperparameters; + + /** + * The file ID used for training. + */ + @JsonProperty("training_file") + String trainingFile; + + /** + * The file ID used for validation. + * Can be null if validation is not used. + */ + @JsonProperty("validation_file") + String validationFile; + + /** + * The compiled results files for the fine-tuning job. + */ + @JsonProperty("result_files") + List resultFiles; + + /** + * The total number of billable tokens processed by this fine-tuning job. + */ + @JsonProperty("trained_tokens") + Integer trainedTokens; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java new file mode 100644 index 00000000..44a501c3 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningJobRequest.java @@ -0,0 +1,46 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + + +/** + * Request to create a fine tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class FineTuningJobRequest { + + /** + * The ID of an uploaded file that contains training data. + */ + @NonNull + @JsonProperty("training_file") + String trainingFile; + + /** + * The ID of an uploaded file that contains validation data. + * Optional. + */ + @JsonProperty("validation_file") + String validationFile; + + /** + * The name of the model to fine-tune. + */ + @NonNull + String model; + + /** + * The hyperparameters used for the fine-tuning job. + */ + Hyperparameters hyperparameters; + + /** + * A string of up to 40 characters that will be added to your fine-tuned model name. + */ + String suffix; +} diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java b/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java new file mode 100644 index 00000000..3d59913d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/Hyperparameters.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.fine_tuning; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Hyperparameters for a fine-tuning job + * https://platform.openai.com/docs/api-reference/fine-tuning/object#hyperparameters + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Hyperparameters { + + /** + * The number of epochs to train the model for. + * An epoch refers to one full cycle through the training dataset. + * "Auto" decides the optimal number of epochs based on the size of the dataset. + * If setting the number manually, we support any number between 1 and 50 epochs. + */ + @JsonProperty("n_epochs") + Integer nEpochs; +} diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java index 855d774e..7b8f76c3 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java @@ -8,6 +8,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneEvent { /** diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java index fbf89db1..2d145aa0 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneRequest.java @@ -11,6 +11,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes/create */ +@Deprecated @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java b/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java index 49c0170c..6cbcce41 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java @@ -11,6 +11,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneResult { /** diff --git a/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java b/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java index e83af27c..d1d383ed 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java +++ b/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java @@ -8,6 +8,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class HyperParameters { diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index d841e8ad..60ffdbf6 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -16,6 +16,9 @@ import com.theokanning.openai.embedding.EmbeddingResult; import com.theokanning.openai.engine.Engine; import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneRequest; import com.theokanning.openai.finetune.FineTuneResult; @@ -86,21 +89,41 @@ public interface OpenAiApi { @GET("/v1/files/{file_id}") Single retrieveFile(@Path("file_id") String fileId); + @POST("/v1/fine_tuning/jobs") + Single createFineTuningJob(@Body FineTuningJobRequest request); + + @GET("/v1/fine_tuning/jobs") + Single> listFineTuningJobs(); + + @GET("/v1/fine_tuning/jobs/{fine_tuning_job_id}") + Single retrieveFineTuningJob(@Path("fine_tuning_job_id") String fineTuningJobId); + + @POST("/v1/fine_tuning/jobs/{fine_tuning_job_id}/cancel") + Single cancelFineTuningJob(@Path("fine_tuning_job_id") String fineTuningJobId); + + @GET("/v1/fine_tuning/jobs/{fine_tuning_job_id}/events") + Single> listFineTuningJobEvents(@Path("fine_tuning_job_id") String fineTuningJobId); + + @Deprecated @POST("/v1/fine-tunes") Single createFineTune(@Body FineTuneRequest request); @POST("/v1/completions") Single createFineTuneCompletion(@Body CompletionRequest request); + @Deprecated @GET("/v1/fine-tunes") Single> listFineTunes(); + @Deprecated @GET("/v1/fine-tunes/{fine_tune_id}") Single retrieveFineTune(@Path("fine_tune_id") String fineTuneId); + @Deprecated @POST("/v1/fine-tunes/{fine_tune_id}/cancel") Single cancelFineTune(@Path("fine_tune_id") String fineTuneId); + @Deprecated @GET("/v1/fine-tunes/{fine_tune_id}/events") Single> listFineTuneEvents(@Path("fine_tune_id") String fineTuneId); diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 63da487c..7114531b 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -24,6 +24,9 @@ import com.theokanning.openai.embedding.EmbeddingRequest; import com.theokanning.openai.embedding.EmbeddingResult; import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneRequest; import com.theokanning.openai.finetune.FineTuneResult; @@ -170,6 +173,27 @@ public File retrieveFile(String fileId) { return execute(api.retrieveFile(fileId)); } + public FineTuningJob createFineTuningJob(FineTuningJobRequest request) { + return execute(api.createFineTuningJob(request)); + } + + public List listFineTuningJobs() { + return execute(api.listFineTuningJobs()).data; + } + + public FineTuningJob retrieveFineTuningJob(String fineTuningJobId) { + return execute(api.retrieveFineTuningJob(fineTuningJobId)); + } + + public FineTuningJob cancelFineTuningJob(String fineTuningJobId) { + return execute(api.cancelFineTuningJob(fineTuningJobId)); + } + + public List listFineTuningJobEvents(String fineTuningJobId) { + return execute(api.listFineTuningJobEvents(fineTuningJobId)).data; + } + + @Deprecated public FineTuneResult createFineTune(FineTuneRequest request) { return execute(api.createFineTune(request)); } @@ -178,18 +202,22 @@ public CompletionResult createFineTuneCompletion(CompletionRequest request) { return execute(api.createFineTuneCompletion(request)); } + @Deprecated public List listFineTunes() { return execute(api.listFineTunes()).data; } + @Deprecated public FineTuneResult retrieveFineTune(String fineTuneId) { return execute(api.retrieveFineTune(fineTuneId)); } + @Deprecated public FineTuneResult cancelFineTune(String fineTuneId) { return execute(api.cancelFineTune(fineTuneId)); } + @Deprecated public List listFineTuneEvents(String fineTuneId) { return execute(api.listFineTuneEvents(fineTuneId)).data; } diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java index 0886b547..e47460d0 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.*; +@Deprecated @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FineTuneTest { static com.theokanning.openai.service.OpenAiService service; diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java new file mode 100644 index 00000000..288b1f41 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -0,0 +1,86 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; +import com.theokanning.openai.fine_tuning.Hyperparameters; +import com.theokanning.openai.finetune.FineTuneResult; +import org.junit.jupiter.api.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class FineTuningTest { + static OpenAiService service; + static String fileId; + static String fineTuningJobId; + + + @BeforeAll + static void setup() throws Exception { + String token = System.getenv("OPENAI_TOKEN"); + service = new OpenAiService(token); + fileId = service.uploadFile("fine-tune", "src/test/resources/chat-fine-tuning-data.jsonl").getId(); + + // wait for file to be processed + TimeUnit.SECONDS.sleep(10); + } + + @AfterAll + static void teardown() { + service.deleteFile(fileId); + } + + @Test + @Order(1) + void createFineTuningJob() { + Hyperparameters hyperparameters = Hyperparameters.builder() + .nEpochs(4) + .build(); + FineTuningJobRequest request = FineTuningJobRequest.builder() + .trainingFile(fileId) + .model("gpt-3.5-turbo") + .hyperparameters(hyperparameters) + .build(); + + FineTuningJob fineTuningJob = service.createFineTuningJob(request); + fineTuningJobId = fineTuningJob.getId(); + + assertEquals("created", fineTuningJob.getStatus()); + } + + @Test + @Order(2) + void listFineTuningJobs() { + List fineTuningJobs = service.listFineTuningJobs(); + + assertTrue(fineTuningJobs.stream().anyMatch(fineTuningJob -> fineTuningJob.getId().equals(fineTuningJobId))); + } + + @Test + @Order(2) + void listFineTuningEvents() { + List events = service.listFineTuningJobEvents(fineTuningJobId); + + assertFalse(events.isEmpty()); + } + + @Test + @Order(2) + void retrieveFineTuningJob() { + FineTuningJob fineTune = service.retrieveFineTuningJob(fineTuningJobId); + + assertTrue(fineTune.getModel().startsWith("gpt-3.5-turbo")); + } + + @Test + @Order(2) + void cancelFineTuningJob() { + FineTuningJob fineTuningJob = service.cancelFineTuningJob(fineTuningJobId); + + assertEquals("cancelled", fineTuningJob.getStatus()); + } +} diff --git a/service/src/test/resources/chat-fine-tuning-data.jsonl b/service/src/test/resources/chat-fine-tuning-data.jsonl new file mode 100644 index 00000000..31604bf2 --- /dev/null +++ b/service/src/test/resources/chat-fine-tuning-data.jsonl @@ -0,0 +1,10 @@ +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]} +{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]} \ No newline at end of file From 442cf130f6fbedbdcb70ce9d96ce572faf54c9e9 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Fri, 25 Aug 2023 11:06:56 -0500 Subject: [PATCH 08/19] Update to version 0.16.0 (#361) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index de7ec97b..84d38815 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.15.0 +VERSION_NAME=0.16.0 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From 44208305716435bab5e834a3601009f0f1f8c978 Mon Sep 17 00:00:00 2001 From: Andrei Costescu Date: Sun, 22 Oct 2023 17:36:03 +0300 Subject: [PATCH 09/19] Implemented missing API for issue #366: No Retrieve file content api (#367) --- .../java/com/theokanning/openai/client/OpenAiApi.java | 4 ++++ .../com/theokanning/openai/service/OpenAiService.java | 4 ++++ .../java/com/theokanning/openai/service/FileTest.java | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 60ffdbf6..497dacd5 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -89,6 +89,10 @@ public interface OpenAiApi { @GET("/v1/files/{file_id}") Single retrieveFile(@Path("file_id") String fileId); + @Streaming + @GET("/v1/files/{file_id}/content") + Single retrieveFileContent(@Path("file_id") String fileId); + @POST("/v1/fine_tuning/jobs") Single createFineTuningJob(@Body FineTuningJobRequest request); diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 7114531b..0296c15c 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -173,6 +173,10 @@ public File retrieveFile(String fileId) { return execute(api.retrieveFile(fileId)); } + public ResponseBody retrieveFileContent(String fileId) { + return execute(api.retrieveFileContent(fileId)); + } + public FineTuningJob createFineTuningJob(FineTuningJobRequest request) { return execute(api.createFineTuningJob(request)); } diff --git a/service/src/test/java/com/theokanning/openai/service/FileTest.java b/service/src/test/java/com/theokanning/openai/service/FileTest.java index 2b51aee3..74fe2084 100644 --- a/service/src/test/java/com/theokanning/openai/service/FileTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FileTest.java @@ -7,6 +7,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.concurrent.TimeUnit; @@ -52,6 +55,13 @@ void retrieveFile() { @Test @Order(4) + void retrieveFileContent() throws IOException { + String fileBytesToString = service.retrieveFileContent(fileId).string(); + assertEquals(Files.readString(Path.of(filePath)), fileBytesToString); + } + + @Test + @Order(5) void deleteFile() { DeleteResult result = service.deleteFile(fileId); assertTrue(result.isDeleted()); From a4f2df805d30e98cd6a74ed1a1bf43ef73df88cb Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 15:41:32 -0500 Subject: [PATCH 10/19] Fix java 8 compilation (#378) --- .../java/com/theokanning/openai/service/FileTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FileTest.java b/service/src/test/java/com/theokanning/openai/service/FileTest.java index 74fe2084..51dd9407 100644 --- a/service/src/test/java/com/theokanning/openai/service/FileTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FileTest.java @@ -8,8 +8,9 @@ import org.junit.jupiter.api.TestMethodOrder; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,7 +22,7 @@ public class FileTest { static String filePath = "src/test/resources/fine-tuning-data.jsonl"; String token = System.getenv("OPENAI_TOKEN"); - com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); + OpenAiService service = new OpenAiService(token); static String fileId; @Test @@ -57,7 +58,8 @@ void retrieveFile() { @Order(4) void retrieveFileContent() throws IOException { String fileBytesToString = service.retrieveFileContent(fileId).string(); - assertEquals(Files.readString(Path.of(filePath)), fileBytesToString); + String contents = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); + assertEquals(contents, fileBytesToString); } @Test From 44cba2e92757e7281ba4a3c45a91b89e56876683 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:02:15 -0500 Subject: [PATCH 11/19] Add fine-tuning api models to json test (#379) --- .../openai/fine_tuning/FineTuningEvent.java | 5 +++++ .../java/com/theokanning/openai/JsonTest.java | 8 ++++++-- .../resources/fixtures/FineTuningEvent.json | 8 ++++++++ .../resources/fixtures/FineTuningJob.json | 19 +++++++++++++++++++ .../fixtures/FineTuningJobRequest.json | 9 +++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 api/src/test/resources/fixtures/FineTuningEvent.json create mode 100644 api/src/test/resources/fixtures/FineTuningJob.json create mode 100644 api/src/test/resources/fixtures/FineTuningJobRequest.json diff --git a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java index 097bf7ce..c653c048 100644 --- a/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -15,6 +15,11 @@ public class FineTuningEvent { */ String object; + /** + * The ID of the fine-tuning event. + */ + String id; + /** * The creation time in epoch seconds. */ diff --git a/api/src/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java index 09d89f5b..68b98155 100644 --- a/api/src/test/java/com/theokanning/openai/JsonTest.java +++ b/api/src/test/java/com/theokanning/openai/JsonTest.java @@ -12,10 +12,11 @@ import com.theokanning.openai.embedding.EmbeddingResult; import com.theokanning.openai.engine.Engine; import com.theokanning.openai.file.File; +import com.theokanning.openai.fine_tuning.FineTuningEvent; +import com.theokanning.openai.fine_tuning.FineTuningJob; +import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.finetune.FineTuneEvent; import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.image.CreateImageEditRequest; -import com.theokanning.openai.image.CreateImageRequest; import com.theokanning.openai.image.ImageResult; import com.theokanning.openai.model.Model; import com.theokanning.openai.moderation.ModerationRequest; @@ -43,6 +44,9 @@ public class JsonTest { File.class, FineTuneEvent.class, FineTuneResult.class, + FineTuningEvent.class, + FineTuningJob.class, + FineTuningJobRequest.class, ImageResult.class, TranscriptionResult.class, TranslationResult.class, diff --git a/api/src/test/resources/fixtures/FineTuningEvent.json b/api/src/test/resources/fixtures/FineTuningEvent.json new file mode 100644 index 00000000..cd07faa5 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningEvent.json @@ -0,0 +1,8 @@ +{ + "object": "fine_tuning.job.event", + "id": "ft-event-ddTJfwuMVpfLXseO0Am0Gqjm", + "created_at": 1692407401, + "level": "info", + "message": "Fine tuning job successfully completed", + "type": "message" +} diff --git a/api/src/test/resources/fixtures/FineTuningJob.json b/api/src/test/resources/fixtures/FineTuningJob.json new file mode 100644 index 00000000..51eb64c1 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningJob.json @@ -0,0 +1,19 @@ +{ + "id": "ftjob-abc123", + "object": "fine_tuning.job", + "model": "davinci-002", + "status": "succeeded", + "hyperparameters": { + "n_epochs": 4 + }, + "created_at": 1692661014, + "finished_at": 1692661190, + "fine_tuned_model": "ft:davinci-002:my-org:custom_suffix:7q8mpxmy", + "organization_id": "org-123", + "training_file": "file-abc123", + "result_files": [ + "file-abc123" + ], + "validation_file": "validation-file", + "trained_tokens": 5768 +} diff --git a/api/src/test/resources/fixtures/FineTuningJobRequest.json b/api/src/test/resources/fixtures/FineTuningJobRequest.json new file mode 100644 index 00000000..45e79f73 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuningJobRequest.json @@ -0,0 +1,9 @@ +{ + "model": "davinci-002", + "validation_file": "file-abc123", + "training_file": "file-abc123", + "hyperparameters": { + "n_epochs": 4 + }, + "suffix": "test" +} From fe4295837a1bdbe68a8e2a13b7c72878956a02b4 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:06:56 -0500 Subject: [PATCH 12/19] Fix fine-tuning test (#380) The first status changed from created to validating_files Since this is working either way, just check that it has an id instead --- .../java/com/theokanning/openai/service/FineTuningTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java index 288b1f41..8bc3cd57 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -4,7 +4,6 @@ import com.theokanning.openai.fine_tuning.FineTuningJob; import com.theokanning.openai.fine_tuning.FineTuningJobRequest; import com.theokanning.openai.fine_tuning.Hyperparameters; -import com.theokanning.openai.finetune.FineTuneResult; import org.junit.jupiter.api.*; import java.util.List; @@ -49,7 +48,7 @@ void createFineTuningJob() { FineTuningJob fineTuningJob = service.createFineTuningJob(request); fineTuningJobId = fineTuningJob.getId(); - assertEquals("created", fineTuningJob.getStatus()); + assertNotNull(fineTuningJob); } @Test From 761350a89ff819828bb07852cbe13e7c077be0af Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:24:29 -0500 Subject: [PATCH 13/19] Add File status fields (#381) Fixes https://github.com/TheoKanning/openai-java/issues/355 --- .../main/java/com/theokanning/openai/file/File.java | 12 ++++++++++++ api/src/test/resources/fixtures/File.json | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/file/File.java b/api/src/main/java/com/theokanning/openai/file/File.java index 36107e73..07708f8e 100644 --- a/api/src/main/java/com/theokanning/openai/file/File.java +++ b/api/src/main/java/com/theokanning/openai/file/File.java @@ -41,4 +41,16 @@ public class File { * Description of the file's purpose. */ String purpose; + + /** + * The current status of the file, which can be either uploaded, processed, pending, error, deleting or deleted. + */ + String status; + + /** + * Additional details about the status of the file. + * If the file is in the error state, this will include a message describing the error. + */ + @JsonProperty("status_details") + String statusDetails; } diff --git a/api/src/test/resources/fixtures/File.json b/api/src/test/resources/fixtures/File.json index 8ede985d..9fa0c564 100644 --- a/api/src/test/resources/fixtures/File.json +++ b/api/src/test/resources/fixtures/File.json @@ -4,5 +4,7 @@ "bytes": 175, "created_at": 1613677385, "filename": "train.jsonl", - "purpose": "search" -} \ No newline at end of file + "purpose": "search", + "status": "error", + "status_details": "File is too large." +} From e7635c3cf07eb9a423d0bdaad5e41b65f0dbe9cd Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:27:54 -0500 Subject: [PATCH 14/19] Update version to 0.16.1 (#382) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 84d38815..85a94ec8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.16.0 +VERSION_NAME=0.16.1 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java From dc51a5bbf9d4ea311b48b652f7aff0ead7c27ec1 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 22 Oct 2023 16:50:25 -0500 Subject: [PATCH 15/19] Catch errors when deleting file in FineTuningTest (#383) Thsi is failing repeatedly and it's not a big deal if we don't clean this up. --- .../java/com/theokanning/openai/service/FineTuningTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java index 8bc3cd57..81a8e808 100644 --- a/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -30,7 +30,11 @@ static void setup() throws Exception { @AfterAll static void teardown() { - service.deleteFile(fileId); + try { + service.deleteFile(fileId); + } catch (Exception e) { + // ignore + } } @Test From f1e587d685419765e9ab81c763610847f8fa0317 Mon Sep 17 00:00:00 2001 From: vacuityv Date: Mon, 13 Nov 2023 05:17:36 +0800 Subject: [PATCH 16/19] feat(image): new feature for dalle api. (#393) You can set model, quality and style now. --- .../openai/image/CreateImageEditRequest.java | 5 +++++ .../openai/image/CreateImageRequest.java | 21 ++++++++++++++++--- .../image/CreateImageVariationRequest.java | 5 +++++ .../com/theokanning/openai/image/Image.java | 6 ++++++ .../openai/service/OpenAiService.java | 2 ++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java index 72046953..7d37f689 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java @@ -21,6 +21,11 @@ public class CreateImageEditRequest { @NonNull String prompt; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The number of images to generate. Must be between 1 and 10. Defaults to 1. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java index b8a1d05d..13672c24 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java @@ -17,18 +17,28 @@ public class CreateImageRequest { /** - * A text description of the desired image(s). The maximum length in 1000 characters. + * A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3. */ @NonNull String prompt; /** - * The number of images to generate. Must be between 1 and 10. Defaults to 1. + * The model to use for image generation. Defaults to "dall-e-2". + */ + String model; + + /** + * The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported. Defaults to 1. */ Integer n; /** - * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". + * The quality of the image that will be generated. "hd" creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3. Defaults to "standard". + */ + String quality; + + /** + * The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models. Defaults to 1024x1024. */ String size; @@ -38,6 +48,11 @@ public class CreateImageRequest { @JsonProperty("response_format") String responseFormat; + /** + * The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3. Defaults to vivid. + */ + String style; + /** * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. */ diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java index 2bc0c5d1..f16f613d 100644 --- a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java @@ -20,6 +20,11 @@ public class CreateImageVariationRequest { */ Integer n; + /** + * The model to use for image generation. Only dall-e-2 is supported at this time. Defaults to dall-e-2. + */ + String model; + /** * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". */ diff --git a/api/src/main/java/com/theokanning/openai/image/Image.java b/api/src/main/java/com/theokanning/openai/image/Image.java index e3214844..6b8391ed 100644 --- a/api/src/main/java/com/theokanning/openai/image/Image.java +++ b/api/src/main/java/com/theokanning/openai/image/Image.java @@ -21,4 +21,10 @@ public class Image { */ @JsonProperty("b64_json") String b64Json; + + /** + * The prompt that was used to generate the image, if there was any revision to the prompt. + */ + @JsonProperty("revised_prompt") + String revisedPrompt; } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0296c15c..cedbd805 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -250,6 +250,7 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -276,6 +277,7 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) + .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); From 0ec5a9ee303f91f6d66da9e91ab726ca27f28534 Mon Sep 17 00:00:00 2001 From: Daniel Faria Date: Sun, 12 Nov 2023 18:22:08 -0300 Subject: [PATCH 17/19] add support to audio/createSpeech API (#392) --- .../openai/audio/CreateSpeechRequest.java | 45 +++++++++++++++++++ .../theokanning/openai/client/OpenAiApi.java | 4 ++ .../openai/service/OpenAiService.java | 5 +++ .../theokanning/openai/service/AudioTest.java | 19 ++++++++ 4 files changed, 73 insertions(+) create mode 100644 api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java diff --git a/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java new file mode 100644 index 00000000..6d2e69ac --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateSpeechRequest.java @@ -0,0 +1,45 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateSpeechRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * The text to generate audio for. The maximum length is 4096 characters. + */ + @NonNull + String input; + + /** + * The voice to use when generating the audio. + */ + @NonNull + String voice; + + /** + * The format to audio in. Supported formats are mp3, opus, aac, and flac. Defaults to mp3. + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The speed of the generated audio. Select a value from 0.25 to 4.0. Defaults to 1.0. + */ + Double speed; +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index 497dacd5..f2665ee2 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import com.theokanning.openai.billing.BillingUsage; @@ -149,6 +150,9 @@ public interface OpenAiApi { @POST("/v1/audio/translations") Single createTranslation(@Body RequestBody requestBody); + @POST("/v1/audio/speech") + Single createSpeech(@Body CreateSpeechRequest requestBody); + @POST("/v1/moderations") Single createModeration(@Body ModerationRequest request); diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index cedbd805..ea59417e 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -8,6 +8,7 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; @@ -347,6 +348,10 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public ResponseBody createSpeech(CreateSpeechRequest request) { + return execute(api.createSpeech(request)); + } + /** * Calls the Open AI api, returns the response, and parses error messages if the request fails */ diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java index d5a54a23..9cb083de 100644 --- a/service/src/test/java/com/theokanning/openai/service/AudioTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -1,13 +1,18 @@ package com.theokanning.openai.service; +import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.time.Duration; +import okhttp3.MediaType; +import okhttp3.ResponseBody; + import static org.junit.jupiter.api.Assertions.*; @@ -69,4 +74,18 @@ void createTranslationVerbose() { assertTrue(result.getDuration() > 0); assertEquals(1, result.getSegments().size()); } + + @Test + void createSpeech() throws IOException { + CreateSpeechRequest createSpeechRequest = CreateSpeechRequest.builder() + .model("tts-1") + .input("Hello World.") + .voice("alloy") + .build(); + + final ResponseBody speech = service.createSpeech(createSpeechRequest); + assertNotNull(speech); + assertEquals(MediaType.get("audio/mpeg"), speech.contentType()); + assertTrue(speech.bytes().length > 0); + } } From fb62307a61f35075d198cd8298be8404ba2a0173 Mon Sep 17 00:00:00 2001 From: Remy Ohajinwa Date: Sun, 12 Nov 2023 21:32:56 +0000 Subject: [PATCH 18/19] Support Assistants (#395) * #390 Create Assistant #390 Create Assistant * #390 Retrieve Assistant * #390 Modify Assistant * RemyOhajinwa#390 Delete Assistant * RemyOhajinwa#390 List Assistants * RemyOhajinwa#390 Create Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * RemyOhajinwa#390 Assistant File * Remove DeleteAssistantResult import --------- Co-authored-by: Remy Ohajinwa Co-authored-by: Theo Kanning --- .../openai/assistants/Assistant.java | 24 ++ .../openai/assistants/AssistantBase.java | 55 +++++ .../openai/assistants/AssistantFile.java | 30 +++ .../assistants/AssistantFileRequest.java | 17 ++ .../openai/assistants/AssistantRequest.java | 6 + .../openai/assistants/AssistantSortOrder.java | 12 + .../openai/assistants/AssistantToolsEnum.java | 15 ++ .../openai/assistants/ListAssistant.java | 16 ++ .../assistants/ListAssistantQueryRequest.java | 39 ++++ .../theokanning/openai/assistants/Tool.java | 12 + .../openai/utils/TikTokensUtil.java | 7 +- .../theokanning/openai/client/OpenAiApi.java | 43 ++++ .../openai/service/OpenAiService.java | 46 ++++ .../openai/service/AssistantTest.java | 214 ++++++++++++++++++ .../test/resources/assistant-file-data.json | 1 + .../src/test/resources/assistants-data.html | 1 + 16 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Assistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java create mode 100644 api/src/main/java/com/theokanning/openai/assistants/Tool.java create mode 100644 service/src/test/java/com/theokanning/openai/service/AssistantTest.java create mode 100644 service/src/test/resources/assistant-file-data.json create mode 100644 service/src/test/resources/assistants-data.html diff --git a/api/src/main/java/com/theokanning/openai/assistants/Assistant.java b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java new file mode 100644 index 00000000..111ef169 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class Assistant extends AssistantBase { + + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type which is always 'assistant' + */ + String object; + + /** + * The Unix timestamp(in seconds) for when the assistant was created + */ + @JsonProperty("created_at") + Integer createdAt; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java new file mode 100644 index 00000000..d771bd30 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantBase.java @@ -0,0 +1,55 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantBase { + + /** + * ID of the model to use + */ + @NonNull + String model; + + /** + * The name of the assistant. The maximum length is 256 + */ + String name; + + /** + * The description of the assistant. + */ + String description; + + /** + * The system instructions that the assistant uses. + */ + String instructions; + + /** + * A list of tools enabled on the assistant. + */ + List tools; + + /** + * A list of file IDs attached to this assistant. + */ + @JsonProperty("file_ids") + List fields; + + /** + * Set of 16 key-value pairs that can be attached to an object. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java new file mode 100644 index 00000000..c5d551a9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFile.java @@ -0,0 +1,30 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class AssistantFile { + + /** + * The identifier of the Assistant File + */ + String id; + + /** + * The object type, which is always assistant.file. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the assistant file was created. + */ + @JsonProperty("created_at") + String createdAt; + + /** + * The assistant ID that the file is attached to + */ + @JsonProperty("assistant_id") + String assistantId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java new file mode 100644 index 00000000..98ee009f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFileRequest.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantFileRequest { + + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java new file mode 100644 index 00000000..dc0a66df --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -0,0 +1,6 @@ +package com.theokanning.openai.assistants; + + +public class AssistantRequest extends AssistantBase { + +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java new file mode 100644 index 00000000..9f784a66 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantSortOrder.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantSortOrder { + + @JsonProperty("asc") + ASC, + + @JsonProperty("desc") + DESC +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java new file mode 100644 index 00000000..f6b5021d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantToolsEnum.java @@ -0,0 +1,15 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AssistantToolsEnum { + + @JsonProperty("code_interpreter") + CODE_INTERPRETER, + + @JsonProperty("function") + FUNCTION, + + @JsonProperty("retrieval") + RETRIEVAL +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java new file mode 100644 index 00000000..8478a547 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistant.java @@ -0,0 +1,16 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.OpenAiResponse; + +public class ListAssistant extends OpenAiResponse { + + @JsonProperty("first_id") + String firstId; + + @JsonProperty("last_id") + String lastId; + + @JsonProperty("has_more") + boolean hasMore; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java new file mode 100644 index 00000000..3e5f3c68 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ListAssistantQueryRequest.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ListAssistantQueryRequest { + /** + * A limit on the number of objects to be returned. + * Limit can range between 1 and 100, and the default is 20 + */ + + Integer limit; + + /** + * Sort order by the 'created_at' timestamp of the objects. + * 'asc' for ascending order and 'desc' for descending order. + */ + AssistantSortOrder order; + + /** + * A cursor for use in pagination. after is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include after=obj_foo in order to fetch the next page of the list + */ + String after; + + /** + * A cursor for use in pagination. before is an object ID that defines your place in the list. + * For instance, if you make a list request and receive 100 objects, ending with obj_foo, + * your subsequent call can include before=obj_foo in order to fetch the previous page of the list. + */ + String before; +} diff --git a/api/src/main/java/com/theokanning/openai/assistants/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java new file mode 100644 index 00000000..00027d72 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -0,0 +1,12 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + AssistantToolsEnum type; +} diff --git a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java index c30871f7..0a50907e 100644 --- a/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -32,6 +32,7 @@ public class TikTokensUtil { modelMap.put(ModelEnum.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_32K_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); modelMap.put(ModelEnum.GPT_4_0314.getName(), registry.getEncodingForModel(ModelType.GPT_4)); + modelMap.put(ModelEnum.GPT_4_1106_preview.getName(), registry.getEncodingForModel(ModelType.GPT_4)); } /** @@ -261,7 +262,11 @@ public enum ModelEnum { * Temporary model, not recommended for use. */ GPT_4_32K_0314("gpt-4-32k-0314"), - ; + + /** + * Temporary model, not recommended for use. + */ + GPT_4_1106_preview("gpt-4-1106-preview"); private String name; } diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java index f2665ee2..0bfc03aa 100644 --- a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -2,6 +2,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.TranscriptionResult; import com.theokanning.openai.audio.TranslationResult; @@ -36,6 +42,7 @@ import retrofit2.http.*; import java.time.LocalDate; +import java.util.Map; public interface OpenAiApi { @@ -185,4 +192,40 @@ public interface OpenAiApi { @GET("v1/dashboard/billing/usage") Single billingUsage(@Query("start_date") LocalDate starDate, @Query("end_date") LocalDate endDate); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants") + Single createAssistant(@Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}") + Single retrieveAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}") + Single modifyAssistant(@Path("assistant_id") String assistantId, @Body AssistantBase request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}") + Single deleteAssistant(@Path("assistant_id") String assistantId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants") + Single> listAssistants(@QueryMap Map filterRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/assistants/{assistant_id}/files") + Single createAssistantFile(@Path("assistant_id") String assistantId, @Body AssistantFileRequest fileRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files/{file_id}") + Single retrieveAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/assistants/{assistant_id}/files/{file_id}") + Single deleteAssistantFile(@Path("assistant_id") String assistantId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/assistants/{assistant_id}/files") + Single> listAssistantFiles(@Path("assistant_id") String assistantId, @QueryMap Map filterRequest); } diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index ea59417e..0a7d9459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -1,6 +1,7 @@ package com.theokanning.openai.service; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; @@ -8,6 +9,12 @@ import com.theokanning.openai.DeleteResult; import com.theokanning.openai.OpenAiError; import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; import com.theokanning.openai.audio.CreateSpeechRequest; import com.theokanning.openai.audio.CreateTranscriptionRequest; import com.theokanning.openai.audio.CreateTranslationRequest; @@ -53,6 +60,7 @@ import java.time.Duration; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -348,6 +356,44 @@ public ModerationResult createModeration(ModerationRequest request) { return execute(api.createModeration(request)); } + public Assistant createAssistant(AssistantBase request) { + return execute(api.createAssistant(request)); + } + + public Assistant retrieveAssistant(String assistantId) { + return execute(api.retrieveAssistant(assistantId)); + } + + public Assistant modifyAssistant(String assistantId, AssistantBase request) { + return execute(api.modifyAssistant(assistantId, request)); + } + + public DeleteResult deleteAssistant(String assistantId) { + return execute(api.deleteAssistant(assistantId)); + } + + public ListAssistant listAssistants(ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistants(queryParameters)); + } + + public AssistantFile createAssistantFile(String assistantId, AssistantFileRequest fileRequest) { + return execute(api.createAssistantFile(assistantId, fileRequest)); + } + + public AssistantFile retrieveAssistantFile(String assistantId, String fileId) { + return execute(api.retrieveAssistantFile(assistantId, fileId)); + } + + public DeleteResult deleteAssistantFile(String assistantId, String fileId) { + return execute(api.deleteAssistantFile(assistantId, fileId)); + } + + public ListAssistant listAssistantFiles(String assistantId, ListAssistantQueryRequest filterRequest) { + Map queryParameters = mapper.convertValue(filterRequest, new TypeReference>() {}); + return execute(api.listAssistantFiles(assistantId, queryParameters)); + } + public ResponseBody createSpeech(CreateSpeechRequest request) { return execute(api.createSpeech(request)); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java new file mode 100644 index 00000000..4f21b739 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,214 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantBase; +import com.theokanning.openai.assistants.AssistantFile; +import com.theokanning.openai.assistants.AssistantFileRequest; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.assistants.AssistantSortOrder; +import com.theokanning.openai.assistants.AssistantToolsEnum; +import com.theokanning.openai.assistants.ListAssistant; +import com.theokanning.openai.assistants.ListAssistantQueryRequest; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.file.File; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class AssistantTest { + public static final String MATH_TUTOR = "Math Tutor"; + public static final String ASSISTANT_INSTRUCTION = "You are a personal Math Tutor."; + + static String token = System.getenv("OPENAI_TOKEN");; + + static OpenAiService service = new OpenAiService(token); + + + + @Test + void retrieveAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + Assistant retrieveAssistantResponse = service.retrieveAssistant(createAssistantResponse.getId()); + validateAssistantResponse(retrieveAssistantResponse); + } + + @Test + void modifyAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + String modifiedName = MATH_TUTOR + " Modified"; + createAssistantResponse.setName(modifiedName);//modify a field + + Assistant modifiedAssistantResponse = service.modifyAssistant(createAssistantResponse.getId(), createAssistantResponse); + assertNotNull(modifiedAssistantResponse); + assertEquals(modifiedName, modifiedAssistantResponse.getName()); + } + + @Test + void deleteAssistant() { + Assistant createAssistantResponse = createAndValidateAssistant(); + + DeleteResult deletedAssistant = service.deleteAssistant(createAssistantResponse.getId()); + + assertNotNull(deletedAssistant); + assertEquals(createAssistantResponse.getId(), deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); + } + + @Test + void listAssistants() { + ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); + + assertNotNull(assistants); + // this should be more than 2 depending on how many times createAndValidateAssistant method is called + assertTrue(assistants.getData().size() > 1); + } + + @Test + void listAssistants_returnsTwoAssistants() { + int expectedLimit = 2; + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + assertEquals(expectedLimit, data.size()); + } + + + + @Test + void listAssistants_returnsAscSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.ASC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); + boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); + assertTrue(firstTwoAscending && lastTwoAscending); + } + + @Test + void listAssistants_returnsDescSortedAssistants() { + int expectedLimit = 3; + + ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() + .limit(expectedLimit) + .order(AssistantSortOrder.DESC) + .build(); + + ListAssistant assistants = service.listAssistants(queryResult); + + List data = validateListAssistants(assistants); + + boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); + boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); + assertTrue(firstTwoDescending && lastTwoDescending); + } + + @Test + void createAssistantFile() { + File uploadedFile = uploadAssistantFile(); + + Assistant assistant = createAndValidateAssistant(); + + AssistantFile assistantFile = service.createAssistantFile(assistant.getId(), new AssistantFileRequest(uploadedFile.getId())); + + assertNotNull(assistantFile); + assertEquals(uploadedFile.getId(), assistantFile.getId()); + assertEquals(assistant.getId(), assistantFile.getAssistantId()); + } + + + + @Test + void retrieveAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void deleteAssistantFile() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @Test + void listAssistantFiles() { + //TODO + //There is a bug with uploading assistant files https://community.openai.com/t/possible-bug-with-agent-creation-php-file-upload/484490/5 + //So this would have to be done later + } + + @AfterAll + static void clean() { + //Clean up all data created during this test + ListAssistantQueryRequest queryFilter = ListAssistantQueryRequest.builder() + .limit(100) + .build(); + ListAssistant assistantListAssistant = service.listAssistants(queryFilter); + assistantListAssistant.getData().forEach(assistant ->{ + service.deleteAssistant(assistant.getId()); + }); + } + + private static File uploadAssistantFile() { + String filePath = "src/test/resources/assistants-data.html"; + return service.uploadFile("assistants", filePath); + } + + private static Assistant createAndValidateAssistant() { + AssistantBase assistantRequest = assistantStub(); + Assistant createAssistantResponse = service.createAssistant(assistantRequest); + validateAssistantResponse(createAssistantResponse); + + return createAssistantResponse; + } + + + private static AssistantBase assistantStub() { + return AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name(MATH_TUTOR) + .instructions(ASSISTANT_INSTRUCTION) + .tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER))) + .build(); + } + + private static void validateAssistantResponse(Assistant assistantResponse) { + assertNotNull(assistantResponse); + assertNotNull(assistantResponse.getId()); + assertNotNull(assistantResponse.getCreatedAt()); + assertNotNull(assistantResponse.getObject()); + assertEquals(assistantResponse.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); + assertEquals(MATH_TUTOR, assistantResponse.getName()); + } + + private static List validateListAssistants(ListAssistant assistants) { + assertNotNull(assistants); + List data = assistants.getData(); + assertNotNull(data); + return data; + } +} diff --git a/service/src/test/resources/assistant-file-data.json b/service/src/test/resources/assistant-file-data.json new file mode 100644 index 00000000..8b42bc07 --- /dev/null +++ b/service/src/test/resources/assistant-file-data.json @@ -0,0 +1 @@ +{"prompt": "prompt", "completion": "text"} \ No newline at end of file diff --git a/service/src/test/resources/assistants-data.html b/service/src/test/resources/assistants-data.html new file mode 100644 index 00000000..6c70bcfe --- /dev/null +++ b/service/src/test/resources/assistants-data.html @@ -0,0 +1 @@ + \ No newline at end of file From fee68bad25374d4553654d323652289ad9724696 Mon Sep 17 00:00:00 2001 From: Theo Kanning Date: Sun, 12 Nov 2023 16:23:26 -0600 Subject: [PATCH 19/19] Fix tests --- .../openai/service/OpenAiService.java | 10 +++- .../openai/service/AssistantTest.java | 60 +------------------ 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java index 0a7d9459..04f2e459 100644 --- a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -259,7 +259,6 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File .setType(MediaType.get("multipart/form-data")) .addFormDataPart("prompt", request.getPrompt()) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -272,6 +271,10 @@ public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File builder.addFormDataPart("mask", "mask", maskBody); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageEdit(builder.build())); } @@ -286,7 +289,6 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.get("multipart/form-data")) .addFormDataPart("size", request.getSize()) - .addFormDataPart("model", request.getModel()) .addFormDataPart("response_format", request.getResponseFormat()) .addFormDataPart("image", "image", imageBody); @@ -294,6 +296,10 @@ public ImageResult createImageVariation(CreateImageVariationRequest request, jav builder.addFormDataPart("n", request.getN().toString()); } + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + return execute(api.createImageVariation(builder.build())); } diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java index 4f21b739..781161b2 100644 --- a/service/src/test/java/com/theokanning/openai/service/AssistantTest.java +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -19,9 +19,7 @@ import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class AssistantTest { @@ -70,59 +68,7 @@ void listAssistants() { ListAssistant assistants = service.listAssistants(ListAssistantQueryRequest.builder().build()); assertNotNull(assistants); - // this should be more than 2 depending on how many times createAndValidateAssistant method is called - assertTrue(assistants.getData().size() > 1); - } - - @Test - void listAssistants_returnsTwoAssistants() { - int expectedLimit = 2; - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - assertEquals(expectedLimit, data.size()); - } - - - - @Test - void listAssistants_returnsAscSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.ASC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoAscending = data.get(0).getCreatedAt() <= data.get(1).getCreatedAt(); - boolean lastTwoAscending = data.get(1).getCreatedAt() <= data.get(2).getCreatedAt(); - assertTrue(firstTwoAscending && lastTwoAscending); - } - - @Test - void listAssistants_returnsDescSortedAssistants() { - int expectedLimit = 3; - - ListAssistantQueryRequest queryResult = ListAssistantQueryRequest.builder() - .limit(expectedLimit) - .order(AssistantSortOrder.DESC) - .build(); - - ListAssistant assistants = service.listAssistants(queryResult); - - List data = validateListAssistants(assistants); - - boolean firstTwoDescending = data.get(0).getCreatedAt() >= data.get(1).getCreatedAt(); - boolean lastTwoDescending = data.get(1).getCreatedAt() >= data.get(2).getCreatedAt(); - assertTrue(firstTwoDescending && lastTwoDescending); + assertFalse(assistants.getData().isEmpty()); } @Test @@ -138,8 +84,6 @@ void createAssistantFile() { assertEquals(assistant.getId(), assistantFile.getAssistantId()); } - - @Test void retrieveAssistantFile() { //TODO