diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..fc0a8d1f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +Thanks for submitting a pull request! Please check CONTRIBUTING.md for style guidelines. + +### Changes +Describe your changes here + +### New API Checklist +See CONTRIBUTING.md for more info. + +1. [ ] Documentation for every variable +2. [ ] Class-level documentation +3. [ ] POJO JSON parsing tests +4. [ ] Service integration tests \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c6650f4..6e978294 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,12 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: - java-version: 1.8 + distribution: temurin + java-version: 8 - name: Test run: ./gradlew test @@ -23,7 +24,7 @@ jobs: OPENAI_TOKEN: ${{ secrets.OPENAI_TOKEN }} - name: Publish - run: ./gradlew build publish + run: ./gradlew build publish --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 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 818151d7..830e797f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,20 +3,19 @@ name: Test on: push: branches: [ main ] - pull_request: - branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: - java-version: 1.8 + distribution: temurin + java-version: 8 - name: Test run: ./gradlew test --stacktrace diff --git a/.gitignore b/.gitignore index ff142adc..4b473169 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,13 @@ hs_err_pid* # Ignore Gradle build output directory build + +# VSCode +.vscode + +# Ignore any files in /bin and /obj Folders +**/bin/* +**/obj/* + +# Ignore the macOS folder attribute file +**/.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3a930669 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# How to Contribute + +## How to Add a New API + +### Add POJOs to API library +I usually have ChatGPT write them for me by copying and pasting from teh OpenAI API reference ([example chat](https://chat.openai.com/share/af48ef11-0354-40b2-a8e2-3bf8e93a94a3)), but double check everything because Chat always makes mistakes, especially around adding `@JsonProperty` annotations. + +- Make all java variables camel case, and use `@JsonProperty` for fields that OpenAI returns as snake case +- Include comments for each variable, I take these directly from the OpenAI website +- Include `@Data` on every response class, and `@Builder @NoArgsConstructor @AllArgsConstructor @Data` on every request +- Include basic class-level documentation and a link to the OpenAI reference page, [example](api/src/main/java/com/theokanning/openai/threads/Thread.java) +- Add a JSON test for every new java object, this ensures that your definition and variable name overrides are correct. + - Copy the sample response from OpenAI into an api test [fixture](api/src/test/resources/fixtures) + - Add any missing fields to the JSON file (OpenAI doesn't always include everything) + - Add the class name to the test cases here [JSON test](api/src/test/java/com/theokanning/openai/JsonTest.java) + +### Add to [OpenAiApi](client/src/main/java/com/theokanning/openai/client/OpenAiApi.java) +This is usually straightforward, use [OpenAiResponse](api/src/main/java/com/theokanning/openai/OpenAiResponse.java) for endpoints that return lists. + +### Add to [OpenAiService](service/src/main/java/com/theokanning/openai/OpenAiService.java) + +### Add an Integration Test +Since 99% of the work of this library is done on OpenAI's servers, the objective of these tests is to call each endpoint at least once. +Specify every available parameter to make sure that OpenAI accepts everything, but don't create extra test cases unless a parameter drastically affects the results. +For example, [CompletionTest](service/src/test/java/com/theokanning/openai/service/CompletionTest.java) has one test for normal completions, and one for streaming. + +If your test relies on creating and retrieving external resources, [FineTuningTest](service/src/test/java/com/theokanning/openai/service/FineTuningTest.java) is a good example of how to share resources between tests and clean up afterwards. \ No newline at end of file diff --git a/README.md b/README.md index 87081965..3698db58 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,208 @@ ![Maven Central](https://img.shields.io/maven-central/v/com.theokanning.openai-gpt3-java/client?color=blue) -> ⚠️The [Answers](https://help.openai.com/en/articles/6233728-answers-transition-guide), ->[Classifications](https://help.openai.com/en/articles/6272941-classifications-transition-guide), ->and [Searches](https://help.openai.com/en/articles/6272952-search-transition-guide) APIs are deprecated, ->and will stop working on December 3rd, 2022. +> ⚠️ Notice: This project is no longer maintained and has been archived as of June 6th, 2024. +Thank you to everyone who has contributed and supported this project. While the repository will remain available in its current state, no further updates or support will be provided. Please feel free to fork and modify the code as needed. > ⚠️OpenAI has deprecated all Engine-based APIs. See [Deprecated Endpoints](https://github.com/TheoKanning/openai-java#deprecated-endpoints) below for more info. # OpenAI-Java -Java libraries for using OpenAI's GPT-3 api. +Java libraries for using OpenAI's GPT apis. Supports GPT-3, ChatGPT, and GPT-4. Includes the following artifacts: -- `api` : request/response POJOs for the GPT-3 engine, completion, and search APIs. -- `client` : a basic retrofit client for the GPT-3 endpoints, includes the `api` module +- `api` : request/response POJOs for the GPT APIs. +- `client` : a basic retrofit client for the GPT endpoints, includes the `api` module +- `service` : A basic service class that creates and calls the client. This is the easiest way to get started. -as well as an example project using the client. +as well as an example project using the service. ## Supported APIs -- [Models](https://beta.openai.com/docs/api-reference/models) -- [Completions](https://beta.openai.com/docs/api-reference/completions) -- [Edits](https://beta.openai.com/docs/api-reference/edits) -- [Embeddings](https://beta.openai.com/docs/api-reference/embeddings) -- [Files](https://beta.openai.com/docs/api-reference/files) -- [Fine-tunes](https://beta.openai.com/docs/api-reference/fine-tunes) -- [Moderations](https://beta.openai.com/docs/api-reference/moderations) +- [Models](https://platform.openai.com/docs/api-reference/models) +- [Completions](https://platform.openai.com/docs/api-reference/completions) +- [Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) +- [Edits](https://platform.openai.com/docs/api-reference/edits) +- [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-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) +- [Assistants](https://platform.openai.com/docs/api-reference/assistants) #### Deprecated by OpenAI -- [Searches](https://beta.openai.com/docs/api-reference/searches) -- [Classifications](https://beta.openai.com/docs/api-reference/classifications) -- [Answers](https://beta.openai.com/docs/api-reference/answers) -- [Engines](https://beta.openai.com/docs/api-reference/engines) +- [Engines](https://platform.openai.com/docs/api-reference/engines) +- [Legacy Fine-Tunes](https://platform.openai.com/docs/guides/legacy-fine-tuning) -## Usage +## Importing -### Importing into a gradle project -`implementation 'com.theokanning.openai-gpt3-java:api:'` -or -`implementation 'com.theokanning.openai-gpt3-java:client:'` +### Gradle +`implementation 'com.theokanning.openai-gpt3-java::'` -### Using OpenAiService -If you're looking for the fastest solution, import the `client` and use [OpenAiService](client/src/main/java/com/theokanning/openai/OpenAiService.java). +### Maven +```xml + + com.theokanning.openai-gpt3-java + {api|client|service} + version + ``` + +## Usage +### Data classes only +If you want to make your own client, just import the POJOs from the `api` module. +Your client will need to use snake case to work with the OpenAI API. + +### Retrofit client +If you're using retrofit, you can import the `client` module and use the [OpenAiApi](client/src/main/java/com/theokanning/openai/OpenAiApi.java). +You'll have to add your auth token as a header (see [AuthenticationInterceptor](client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java)) +and set your converter factory to use snake case and only include non-null fields. + +### OpenAiService +If you're looking for the fastest solution, import the `service` module and use [OpenAiService](service/src/main/java/com/theokanning/openai/service/OpenAiService.java). + +> ⚠️The OpenAiService in the client module is deprecated, please switch to the new version in the service module. +```java OpenAiService service = new OpenAiService("your_token"); CompletionRequest completionRequest = CompletionRequest.builder() .prompt("Somebody once told me the world is gonna roll me") - .model("ada") + .model("babbage-002"") .echo(true) .build(); service.createCompletion(completionRequest).getChoices().forEach(System.out::println); ``` -### Using OpenAiApi Retrofit client -If you're using retrofit, you can import the `client` module and use the [OpenAiApi](client/src/main/java/com/theokanning/openai/OpenAiApi.java). -You'll have to add your auth token as a header (see [AuthenticationInterceptor](client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java)) -and set your converter factory to use snake case and only include non-null fields. +### Customizing OpenAiService +If you need to customize OpenAiService, create your own Retrofit client and pass it in to the constructor. +For example, do the following to add request logging (after adding the logging gradle dependency): -### Using data classes only -If you want to make your own client, just import the POJOs from the `api` module. -Your client will need to use snake case to work with the OpenAI API. +```java +ObjectMapper mapper = defaultObjectMapper(); +OkHttpClient client = defaultClient(token, timeout) + .newBuilder() + .interceptor(HttpLoggingInterceptor()) + .build(); +Retrofit retrofit = defaultRetrofit(client, mapper); + +OpenAiApi api = retrofit.create(OpenAiApi.class); +OpenAiService service = new OpenAiService(api); +``` + +### Adding a Proxy +To use a proxy, modify the OkHttp client as shown below: +```java +ObjectMapper mapper = defaultObjectMapper(); +Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)); +OkHttpClient client = defaultClient(token, timeout) + .newBuilder() + .proxy(proxy) + .build(); +Retrofit retrofit = defaultRetrofit(client, mapper); +OpenAiApi api = retrofit.create(OpenAiApi.class); +OpenAiService service = new OpenAiService(api); +``` + +### Functions +You can create your functions and define their executors easily using the ChatFunction class, along with any of your custom classes that will serve to define their available parameters. You can also process the functions with ease, with the help of an executor called FunctionExecutor. + +First we declare our function parameters: +```java +public class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; +} +public enum WeatherUnit { + CELSIUS, FAHRENHEIT; +} +public static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + // constructor +} +``` + +Next, we declare the function itself and associate it with an executor, in this example we will fake a response from some API: +```java +ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .build() +``` + +Then, we employ the FunctionExecutor object from the 'service' module to assist with execution and transformation into an object that is ready for the conversation: +```java +List functionList = // list with functions +FunctionExecutor functionExecutor = new FunctionExecutor(functionList); + +List messages = new ArrayList<>(); +ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "Tell me the weather in Barcelona."); +messages.add(userMessage); +ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall(new ChatCompletionRequestFunctionCall("auto")) + .maxTokens(256) + .build(); + +ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); +ChatFunctionCall functionCall = responseMessage.getFunctionCall(); // might be null, but in this case it is certainly a call to our 'get_weather' function. + +ChatMessage functionResponseMessage = functionExecutor.executeAndConvertToMessageHandlingExceptions(functionCall); +messages.add(response); +``` +> **Note:** The `FunctionExecutor` class is part of the 'service' module. + +You can also create your own function executor. The return object of `ChatFunctionCall.getArguments()` is a JsonNode for simplicity and should be able to help you with that. + +For a more in-depth look, refer to a conversational example that employs functions in: [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java). +Or for an example using functions and stream: [OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java) + +### Streaming thread shutdown +If you want to shut down your process immediately after streaming responses, call `OpenAiService.shutdownExecutor()`. +This is not necessary for non-streaming calls. ## Running the example project All the [example](example/src/main/java/example/OpenAiApiExample.java) project requires is your OpenAI api token -``` +```bash export OPENAI_TOKEN="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" -./gradlew example:run ``` +You can try all the capabilities of this project using: +```bash +./gradlew runExampleOne +``` +And you can also try the new capability of using functions: +```bash +./gradlew runExampleTwo +``` +Or functions with 'stream' mode enabled: +```bash +./gradlew runExampleThree +``` + +## FAQ +### Does this support GPT-4? +Yes! GPT-4 uses the ChatCompletion Api, and you can see the latest model options [here](https://platform.openai.com/docs/models/gpt-4). +GPT-4 is currently in a limited beta (as of 4/1/23), so make sure you have access before trying to use it. + +### Does this support functions? +Absolutely! It is very easy to use your own functions without worrying about doing the dirty work. +As mentioned above, you can refer to [OpenAiApiFunctionsExample.java](example/src/main/java/example/OpenAiApiFunctionsExample.java) or +[OpenAiApiFunctionsWithStreamExample.java](example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java) projects for an example. + +### Why am I getting connection timeouts? +Make sure that OpenAI is available in your country. + +### Why doesn't OpenAiService support x configuration option? +Many projects use OpenAiService, and in order to support them best I've kept it extremely simple. +You can create your own OpenAiApi instance to customize headers, timeouts, base urls etc. +If you want features like retry logic and async calls, you'll have to make an `OpenAiApi` instance and call it directly instead of using `OpenAiService` ## Deprecated Endpoints OpenAI has deprecated engine-based endpoints in favor of model-based endpoints. diff --git a/api/build.gradle b/api/build.gradle index 40a78842..db97b833 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -2,12 +2,22 @@ apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" dependencies { - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.0' - compileOnly 'org.projectlombok:lombok:1.18.24' - annotationProcessor 'org.projectlombok:lombok:1.18.24' + api libs.jacksonAnnotations + api libs.jacksonDatabind + api libs.jtokkit + compileOnly libs.lombok + annotationProcessor libs.lombok + + testImplementation libs.jacksonDatabind + testImplementation(platform(libs.junitBom)) + testImplementation('org.junit.jupiter:junit-jupiter') } compileJava { sourceCompatibility = '1.8' targetCompatibility = '1.8' } + +test { + useJUnitPlatform() +} diff --git a/api/gradle.properties b/api/gradle.properties index 15f5771f..0f2a9893 100644 --- a/api/gradle.properties +++ b/api/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=api POM_NAME=api -POM_DESCRIPTION=Basic java objects for the OpenAI GPT-3 API +POM_DESCRIPTION=Basic java objects for the OpenAI GPT APIs diff --git a/api/src/main/java/com/theokanning/openai/ListSearchParameters.java b/api/src/main/java/com/theokanning/openai/ListSearchParameters.java new file mode 100644 index 00000000..53fd484f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/ListSearchParameters.java @@ -0,0 +1,51 @@ +package com.theokanning.openai; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Common options when getting a list of objects + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ListSearchParameters { + /** + * 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. + */ + Order 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; + + public enum Order { + @JsonProperty("asc") + ASCENDING, + + @JsonProperty("desc") + DESCENDING + } +} diff --git a/api/src/main/java/com/theokanning/openai/OpenAiError.java b/api/src/main/java/com/theokanning/openai/OpenAiError.java new file mode 100644 index 00000000..9992037d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/OpenAiError.java @@ -0,0 +1,39 @@ +package com.theokanning.openai; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the error body when an OpenAI request fails + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OpenAiError { + + public OpenAiErrorDetails error; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class OpenAiErrorDetails { + /** + * Human-readable error message + */ + String message; + + /** + * OpenAI error type, for example "invalid_request_error" + * https://platform.openai.com/docs/guides/error-codes/python-library-error-types + */ + String type; + + String param; + + /** + * OpenAI error code, for example "invalid_api_key" + */ + String code; + } +} diff --git a/api/src/main/java/com/theokanning/openai/OpenAiHttpException.java b/api/src/main/java/com/theokanning/openai/OpenAiHttpException.java new file mode 100644 index 00000000..85533570 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/OpenAiHttpException.java @@ -0,0 +1,31 @@ +package com.theokanning.openai; + +public class OpenAiHttpException extends RuntimeException { + + /** + * HTTP status code + */ + public final int statusCode; + + /** + * OpenAI error code, for example "invalid_api_key" + */ + public final String code; + + + public final String param; + + /** + * OpenAI error type, for example "invalid_request_error" + * https://platform.openai.com/docs/guides/error-codes/python-library-error-types + */ + public final String type; + + public OpenAiHttpException(OpenAiError error, Exception parent, int statusCode) { + super(error.error.message, parent); + this.statusCode = statusCode; + this.code = error.error.code; + this.param = error.error.param; + this.type = error.error.type; + } +} diff --git a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java index 4b718aad..f062fc77 100644 --- a/api/src/main/java/com/theokanning/openai/OpenAiResponse.java +++ b/api/src/main/java/com/theokanning/openai/OpenAiResponse.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -18,4 +19,22 @@ public class OpenAiResponse { * The type of object returned, should be "list" */ public String object; + + /** + * The first id included + */ + @JsonProperty("first_id") + public String firstId; + + /** + * The last id included + */ + @JsonProperty("last_id") + public String lastId; + + /** + * True if there are objects after lastId + */ + @JsonProperty("has_more") + public boolean hasMore; } diff --git a/api/src/main/java/com/theokanning/openai/Usage.java b/api/src/main/java/com/theokanning/openai/Usage.java index 448dc18c..1e7eaa58 100644 --- a/api/src/main/java/com/theokanning/openai/Usage.java +++ b/api/src/main/java/com/theokanning/openai/Usage.java @@ -1,5 +1,6 @@ package com.theokanning.openai; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** @@ -10,15 +11,18 @@ public class Usage { /** * The number of prompt tokens used. */ + @JsonProperty("prompt_tokens") long promptTokens; /** * The number of completion tokens used. */ + @JsonProperty("completion_tokens") long completionTokens; /** * The number of total tokens used */ + @JsonProperty("total_tokens") long totalTokens; } diff --git a/api/src/main/java/com/theokanning/openai/answer/AnswerRequest.java b/api/src/main/java/com/theokanning/openai/answer/AnswerRequest.java deleted file mode 100644 index c939a4c3..00000000 --- a/api/src/main/java/com/theokanning/openai/answer/AnswerRequest.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.theokanning.openai.answer; - -import lombok.*; - -import java.util.List; -import java.util.Map; - -/** - * Given a question, a set of documents, and some examples, the API generates an answer to the question based - * on the information in the set of documents. This is useful for question-answering applications on sources of truth, - * like company documentation or a knowledge base. - * - * Documentation taken from - * https://beta.openai.com/docs/api-reference/answers/create - */ -@Deprecated -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Data -public class AnswerRequest { - - /** - * ID of the engine to use for completion. - */ - @NonNull - String model; - - /** - * Question to get answered. - */ - @NonNull - String question; - - /** - * List of (question, answer) pairs that will help steer the model towards the tone and answer format you'd like. - * We recommend adding 2 to 3 examples. - */ - @NonNull - List> examples; - - /** - * A text snippet containing the contextual information used to generate the answers for the examples you provide. - */ - @NonNull - String examplesContext; - - /** - * List of documents from which the answer for the input question should be derived. - * If this is an empty list, the question will be answered based on the question-answer examples. - * - * You should specify either documents or a file, but not both. - */ - List documents; - - /** - * The ID of an uploaded file that contains documents to search over. - * See upload file for how to upload a file of the desired format and purpose. - * - * You should specify either documents or file, but not both. - */ - String file; - - /** - * ID of the engine to use for Search. You can select one of ada, babbage, curie, or davinci. - */ - String searchModel; - - /** - * The maximum number of documents to be ranked by Search when using file. - * Setting it to a higher value leads to improved accuracy but with increased latency and cost. - */ - Integer maxRerank; - - /** - * What sampling temperature to use. Higher values means the model will take more risks. - * Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. - * - * We generally recommend using this or {@link top_p} but not both. - */ - Double temperature; - - /** - * Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. - * For example, if logprobs is 10, the API will return a list of the 10 most likely tokens. - * The API will always return the logprob of the sampled token, - * so there may be up to logprobs+1 elements in the response. - */ - Integer logprobs; - - /** - * The maximum number of tokens allowed for the generated answer. - */ - Integer maxTokens; - - /** - * Up to 4 sequences where the API will stop generating further tokens. - * The returned text will not contain the stop sequence. - */ - List stop; - - /** - * How many answers to generate for each question. - */ - Integer n; - - /** - * Modify the likelihood of specified tokens appearing in the completion. - * - * Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an - * associated bias value from -100 to 100. - */ - Map logitBias; - - /** - * A special boolean flag for showing metadata. - * If set to true, each document entry in the returned JSON will contain a "metadata" field. - * - * This flag only takes effect when file is set. - */ - Boolean returnMetadata; - - /** - * If set to true, the returned JSON will include a "prompt" field containing the final prompt that was - * used to request a completion. This is mainly useful for debugging purposes. - */ - Boolean returnPrompt; - - /** - * If an object name is in the list, we provide the full information of the object; - * otherwise, we only provide the object ID. - * - * Currently we support completion and file objects for expansion. - */ - List expand; - - /** - * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. - */ - String user; -} diff --git a/api/src/main/java/com/theokanning/openai/answer/AnswerResult.java b/api/src/main/java/com/theokanning/openai/answer/AnswerResult.java deleted file mode 100644 index e37f6f22..00000000 --- a/api/src/main/java/com/theokanning/openai/answer/AnswerResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.theokanning.openai.answer; - -import lombok.Data; - -import java.util.List; - -/** - * An object containing a response from the answer api - * - * https://beta.openai.com/docs/api-reference/answers/create - */ -@Deprecated -@Data -public class AnswerResult { - /** - * A list of generated answers to the provided question/ - */ - List answers; - - /** - * A unique id assigned to this completion - */ - String completion; - - /** - * The GPT-3 model used for completion - */ - String model; - - /** - * The type of object returned, should be "answer" - */ - String object; - - /** - * The GPT-3 model used for search - */ - String searchModel; - - /** - * A list of the most relevant documents for the question. - */ - List selectedDocuments; -} diff --git a/api/src/main/java/com/theokanning/openai/answer/Document.java b/api/src/main/java/com/theokanning/openai/answer/Document.java deleted file mode 100644 index ae6c1bff..00000000 --- a/api/src/main/java/com/theokanning/openai/answer/Document.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.theokanning.openai.answer; - -import lombok.Data; - -/** - * Represents an example returned by the classification api - * - * https://beta.openai.com/docs/api-reference/classifications/create - */ -@Deprecated -@Data -public class Document { - /** - * The position of this example in the example list - */ - Integer document; - - /** - * The text of the example - */ - String text; -} 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..49cf17e9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Assistant.java @@ -0,0 +1,67 @@ +package com.theokanning.openai.assistants; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Assistant { + + /** + * 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; + + /** + * The name of the assistant. The maximum length is 256 + */ + String name; + + /** + * The description of the assistant. + */ + String description; + + /** + * ID of the model to use + */ + @NonNull + String model; + + /** + * 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 fileIds; + + /** + * 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/AssistantFunction.java b/api/src/main/java/com/theokanning/openai/assistants/AssistantFunction.java new file mode 100644 index 00000000..3abee5ae --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantFunction.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-20 10:09 + **/ + + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantFunction { + + private String description; + + private String name; + + private Map parameters; +} 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..bf38ff0b --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/AssistantRequest.java @@ -0,0 +1,52 @@ +package com.theokanning.openai.assistants; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class AssistantRequest { + + /** + * 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 fileIds; + + /** + * 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/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/ModifyAssistantRequest.java b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java new file mode 100644 index 00000000..0fcc4f85 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/ModifyAssistantRequest.java @@ -0,0 +1,51 @@ +package com.theokanning.openai.assistants; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ModifyAssistantRequest { + + /** + * ID of the model to use + */ + 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 fileIds; + + /** + * 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/Tool.java b/api/src/main/java/com/theokanning/openai/assistants/Tool.java new file mode 100644 index 00000000..f35af0ef --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/assistants/Tool.java @@ -0,0 +1,20 @@ +package com.theokanning.openai.assistants; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Tool { + /** + * The type of tool being defined + */ + AssistantToolsEnum type; + + /** + * Function definition, only used if type is "function" + */ + AssistantFunction function; +} 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/api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java new file mode 100644 index 00000000..5c50480f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateTranscriptionRequest.java @@ -0,0 +1,46 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to create transcription based on an audio file + * All fields except model are optional + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateTranscriptionRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. + */ + String prompt; + + /** + * The format of the transcript output, in one of these options: json or verbose_json + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The sampling temperature, between 0 and 1. + * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + * If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + */ + Double temperature; + + /** + * The language of the input audio. Supplying the input language in ISO-639-1 format will improve accuracy and latency. + */ + String language; +} diff --git a/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java b/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java new file mode 100644 index 00000000..ace5cc36 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/CreateTranslationRequest.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to create English translation based on an audio file + * All fields except model are optional + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateTranslationRequest { + + /** + * The name of the model to use. + */ + @NonNull + String model; + + /** + * An optional text to guide the model's style or continue a previous audio segment. The prompt should be in English. + */ + String prompt; + + /** + * The format of the translated output, in one of these options: json or verbose_json + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * The sampling temperature, between 0 and 1. + * Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + * If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + */ + Double temperature; +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java b/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java new file mode 100644 index 00000000..2794a346 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranscriptionResult.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.audio; + +import lombok.Data; + +import java.util.List; + +/** + * An object with the text transcription + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranscriptionResult { + + /** + * The text transcription. + */ + String text; + + /** + * Task name + * @apiNote verbose_json response format only + */ + String task; + + /** + * Speech language + * @apiNote verbose_json response format only + */ + String language; + + /** + * Speech duration + * @apiNote verbose_json response format only + */ + Double duration; + + /** + * List of segments + * @apiNote verbose_json response format only + */ + List segments; + +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java b/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java new file mode 100644 index 00000000..7f382073 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranscriptionSegment.java @@ -0,0 +1,32 @@ +package com.theokanning.openai.audio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * An object represents transcription segment + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranscriptionSegment { + + Integer id; + Integer seek; + Double start; + Double end; + String text; + List tokens; + Double temperature; + @JsonProperty("avg_logprob") + Double averageLogProb; + @JsonProperty("compression_ratio") + Double compressionRatio; + @JsonProperty("no_speech_prob") + Double noSpeechProb; + @JsonProperty("transient") + Boolean transientFlag; + +} diff --git a/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java b/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java new file mode 100644 index 00000000..f1f1c446 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/audio/TranslationResult.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.audio; + +import lombok.Data; + +import java.util.List; + +/** + * An object with the English transcription + * + * https://platform.openai.com/docs/api-reference/audio/create + */ +@Data +public class TranslationResult { + + /** + * Translated text. + */ + String text; + + /** + * Task name + * @apiNote verbose_json response format only + */ + String task; + + /** + * Translated language + * @apiNote verbose_json response format only + */ + String language; + + /** + * Speech duration + * @apiNote verbose_json response format only + */ + Double duration; + + /** + * List of segments + * @apiNote verbose_json response format only + */ + List segments; + +} 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/classification/ClassificationRequest.java b/api/src/main/java/com/theokanning/openai/classification/ClassificationRequest.java deleted file mode 100644 index 7613cb46..00000000 --- a/api/src/main/java/com/theokanning/openai/classification/ClassificationRequest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.theokanning.openai.classification; - -import lombok.*; - -import java.util.List; -import java.util.Map; - -/** - * A request for OpenAi to classify text based on provided examples - * All fields are nullable. - * - * Documentation taken from - * https://beta.openai.com/docs/api-reference/classifications/create - */ -@Deprecated -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Data -public class ClassificationRequest { - - /** - * ID of the engine to use for completion - */ - @NonNull - String model; - - /** - * Query to be classified - */ - @NonNull - String query; - - /** - * A list of examples with labels, in the following format: - * - * [["The movie is so interesting.", "Positive"], ["It is quite boring.", "Negative"], ...] - * - * All the label strings will be normalized to be capitalized. - * - * You should specify either examples or file, but not both. - */ - List> examples; - - /** - * The ID of the uploaded file that contains training examples. - * See upload file for how to upload a file of the desired format and purpose. - * - * You should specify either examples or file, but not both. - */ - String file; - - /** - * The set of categories being classified. - * If not specified, candidate labels will be automatically collected from the examples you provide. - * All the label strings will be normalized to be capitalized. - */ - List labels; - - /** - * ID of the engine to use for Search. You can select one of ada, babbage, curie, or davinci. - */ - String searchModel; - - /** - * What sampling temperature to use. Higher values means the model will take more risks. - * Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. - * - * We generally recommend using this or {@link top_p} but not both. - */ - Double temperature; - - /** - * Include the log probabilities on the logprobs most likely tokens, as well the chosen tokens. - * For example, if logprobs is 10, the API will return a list of the 10 most likely tokens. - * The API will always return the logprob of the sampled token, - * so there may be up to logprobs+1 elements in the response. - */ - Integer logprobs; - - /** - * The maximum number of examples to be ranked by Search when using file. - * Setting it to a higher value leads to improved accuracy but with increased latency and cost. - */ - Integer maxExamples; - - /** - * Modify the likelihood of specified tokens appearing in the completion. - * - * Accepts a json object that maps tokens (specified by their token ID in the GPT tokenizer) to an - * associated bias value from -100 to 100. - */ - Map logitBias; - - /** - * If set to true, the returned JSON will include a "prompt" field containing the final prompt that was - * used to request a completion. This is mainly useful for debugging purposes. - */ - Boolean returnPrompt; - - /** - * A special boolean flag for showing metadata. - * If set to true, each document entry in the returned JSON will contain a "metadata" field. - * - * This flag only takes effect when file is set. - */ - Boolean returnMetadata; - - /** - * If an object name is in the list, we provide the full information of the object; - * otherwise, we only provide the object ID. - * - * Currently we support completion and file objects for expansion. - */ - List expand; - - /** - * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. - */ - String user; -} diff --git a/api/src/main/java/com/theokanning/openai/classification/ClassificationResult.java b/api/src/main/java/com/theokanning/openai/classification/ClassificationResult.java deleted file mode 100644 index 64a19ce5..00000000 --- a/api/src/main/java/com/theokanning/openai/classification/ClassificationResult.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.theokanning.openai.classification; - -import lombok.Data; - -import java.util.List; - -/** - * An object containing a response from the classification api - * < - * https://beta.openai.com/docs/api-reference/classifications/create - */ -@Deprecated -@Data -public class ClassificationResult { - - /** - * A unique id assigned to this completion - */ - String completion; - - /** - * The predicted label for the query text. - */ - String label; - - /** - * The GPT-3 model used for completion - */ - String model; - - /** - * The type of object returned, should be "classification" - */ - String object; - - /** - * The GPT-3 model used for search - */ - String searchModel; - - /** - * A list of the most relevant examples for the query text. - */ - List selectedExamples; -} diff --git a/api/src/main/java/com/theokanning/openai/classification/Example.java b/api/src/main/java/com/theokanning/openai/classification/Example.java deleted file mode 100644 index 7923777e..00000000 --- a/api/src/main/java/com/theokanning/openai/classification/Example.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.theokanning.openai.classification; - -import lombok.Data; - -/** - * Represents an example returned by the classification api - * - * https://beta.openai.com/docs/api-reference/classifications/create - */ -@Deprecated -@Data -public class Example { - /** - * The position of this example in the example list - */ - Integer document; - - /** - * The label of the example - */ - String label; - - /** - * The text of the example - */ - String text; -} diff --git a/api/src/main/java/com/theokanning/openai/common/LastError.java b/api/src/main/java/com/theokanning/openai/common/LastError.java new file mode 100644 index 00000000..6a9f99de --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/common/LastError.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:27 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LastError { + + private String code; + + private String message; +} diff --git a/api/src/main/java/com/theokanning/openai/completion/CompletionChoice.java b/api/src/main/java/com/theokanning/openai/completion/CompletionChoice.java index f46f30e2..c24ef7b4 100644 --- a/api/src/main/java/com/theokanning/openai/completion/CompletionChoice.java +++ b/api/src/main/java/com/theokanning/openai/completion/CompletionChoice.java @@ -3,7 +3,7 @@ import lombok.Data; /** - * A completion generated by GPT-3 + * A completion generated by OpenAI * * https://beta.openai.com/docs/api-reference/completions/create */ @@ -25,7 +25,7 @@ public class CompletionChoice { LogProbResult logprobs; /** - * The reason why GPT-3 stopped generating, for example "length". + * The reason why GPT stopped generating, for example "length". */ String finish_reason; } diff --git a/api/src/main/java/com/theokanning/openai/completion/CompletionChunk.java b/api/src/main/java/com/theokanning/openai/completion/CompletionChunk.java new file mode 100644 index 00000000..2fa6fd09 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/CompletionChunk.java @@ -0,0 +1,37 @@ +package com.theokanning.openai.completion; + +import lombok.Data; +import java.util.List; + +/** + * Object containing a response chunk from the completions streaming api. + * + * https://beta.openai.com/docs/api-reference/completions/create + */ +@Data +public class CompletionChunk { + /** + * A unique id assigned to this completion. + */ + String id; + + /**https://beta.openai.com/docs/api-reference/create-completion + * The type of object returned, should be "text_completion" + */ + String object; + + /** + * The creation time in epoch seconds. + */ + long created; + + /** + * The model used. + */ + String model; + + /** + * A list of generated completions. + */ + List choices; +} diff --git a/api/src/main/java/com/theokanning/openai/completion/CompletionRequest.java b/api/src/main/java/com/theokanning/openai/completion/CompletionRequest.java index dc8dc098..f7bca93c 100644 --- a/api/src/main/java/com/theokanning/openai/completion/CompletionRequest.java +++ b/api/src/main/java/com/theokanning/openai/completion/CompletionRequest.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * A request for OpenAi to generate a predicted completion for a prompt. * All fields are nullable. @@ -22,7 +24,7 @@ public class CompletionRequest { /** * The name of the model to use. - * Required if specifying a fine tuned model or if using the new v1/completions endpoint. + * Required if specifying a fine-tuned model or if using the new v1/completions endpoint. */ String model; @@ -31,11 +33,17 @@ public class CompletionRequest { */ String prompt; + /** + * The suffix that comes after a completion of inserted text. + */ + String suffix; + /** * The maximum number of tokens to generate. * Requests can use up to 2048 tokens shared between prompt and completion. * (One token is roughly 4 characters for normal English text) */ + @JsonProperty("max_tokens") Integer maxTokens; /** @@ -53,6 +61,7 @@ public class CompletionRequest { * * We generally recommend using this or {@link CompletionRequest#temperature} but not both. */ + @JsonProperty("top_p") Double topP; /** @@ -93,12 +102,14 @@ public class CompletionRequest { * Number between 0 and 1 (default 0) that penalizes new tokens based on whether they appear in the text so far. * Increases the model's likelihood to talk about new topics. */ + @JsonProperty("presence_penalty") Double presencePenalty; /** * Number between 0 and 1 (default 0) that penalizes new tokens based on their existing frequency in the text so far. * Decreases the model's likelihood to repeat the same line verbatim. */ + @JsonProperty("frequency_penalty") Double frequencyPenalty; /** @@ -109,6 +120,7 @@ public class CompletionRequest { * When used with {@link CompletionRequest#n}, best_of controls the number of candidate completions and n specifies how many to return, * best_of must be greater than n. */ + @JsonProperty("best_of") Integer bestOf; /** @@ -118,6 +130,7 @@ public class CompletionRequest { * * https://beta.openai.com/docs/api-reference/completions/create#completions/create-logit_bias */ + @JsonProperty("logit_bias") Map logitBias; /** diff --git a/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java b/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java index b43e4204..d63d5c73 100644 --- a/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java +++ b/api/src/main/java/com/theokanning/openai/completion/CompletionResult.java @@ -17,7 +17,7 @@ public class CompletionResult { */ String id; - /** + /**https://beta.openai.com/docs/api-reference/create-completion * The type of object returned, should be "text_completion" */ String object; @@ -28,7 +28,7 @@ public class CompletionResult { long created; /** - * The GPT-3 model used. + * The GPT model used. */ String model; diff --git a/api/src/main/java/com/theokanning/openai/completion/LogProbResult.java b/api/src/main/java/com/theokanning/openai/completion/LogProbResult.java index 90ff87a7..a631dada 100644 --- a/api/src/main/java/com/theokanning/openai/completion/LogProbResult.java +++ b/api/src/main/java/com/theokanning/openai/completion/LogProbResult.java @@ -1,5 +1,6 @@ package com.theokanning.openai.completion; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -22,12 +23,14 @@ public class LogProbResult { /** * The log probability of each token in {@link tokens} */ + @JsonProperty("token_logprobs") List tokenLogprobs; /** * A map for each index in the completion result. * The map contains the top {@link CompletionRequest#logprobs} tokens and their probabilities */ + @JsonProperty("top_logprobs") List> topLogprobs; /** diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChoice.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChoice.java new file mode 100644 index 00000000..7bb88698 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChoice.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.completion.chat; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * A chat completion generated by OpenAI + */ +@Data +public class ChatCompletionChoice { + + /** + * This index of this completion in the returned list. + */ + Integer index; + + /** + * The {@link ChatMessageRole#assistant} message or delta (when streaming) which was generated + */ + @JsonAlias("delta") + ChatMessage message; + + /** + * The reason why GPT stopped generating, for example "length". + */ + @JsonProperty("finish_reason") + String finishReason; +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChunk.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChunk.java new file mode 100644 index 00000000..4bbab2b9 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionChunk.java @@ -0,0 +1,35 @@ +package com.theokanning.openai.completion.chat; +import lombok.Data; + +import java.util.List; + +/** + * Object containing a response chunk from the chat completions streaming api. + */ +@Data +public class ChatCompletionChunk { + /** + * Unique id assigned to this chat completion. + */ + String id; + + /** + * The type of object returned, should be "chat.completion.chunk" + */ + String object; + + /** + * The creation time in epoch seconds. + */ + long created; + + /** + * The model used. + */ + String model; + + /** + * A list of all generated completions. + */ + List choices; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java new file mode 100644 index 00000000..e4479ff3 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionRequest.java @@ -0,0 +1,121 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatCompletionRequest { + + /** + * ID of the model to use. + */ + String model; + + /** + * The messages to generate chat completions for, in the chat format.
+ * see {@link ChatMessage} + */ + List messages; + + /** + * What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower + * values like 0.2 will make it more focused and deterministic.
+ * We generally recommend altering this or top_p but not both. + */ + Double temperature; + + /** + * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens + * with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.
+ * We generally recommend altering this or temperature but not both. + */ + @JsonProperty("top_p") + Double topP; + + /** + * How many chat completion chatCompletionChoices to generate for each input message. + */ + Integer n; + + /** + * If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent + * events as they become available, with the stream terminated by a data: [DONE] message. + */ + Boolean stream; + + /** + * Up to 4 sequences where the API will stop generating further tokens. + */ + List stop; + + /** + * The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will + * be (4096 - prompt tokens). + */ + @JsonProperty("max_tokens") + Integer maxTokens; + + /** + * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, + * increasing the model's likelihood to talk about new topics. + */ + @JsonProperty("presence_penalty") + Double presencePenalty; + + /** + * Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, + * decreasing the model's likelihood to repeat the same line verbatim. + */ + @JsonProperty("frequency_penalty") + Double frequencyPenalty; + + /** + * Accepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 + * to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will + * vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 + * should result in a ban or exclusive selection of the relevant token. + */ + @JsonProperty("logit_bias") + Map logitBias; + + + /** + * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. + */ + String user; + + /** + * A list of the available functions. + */ + List functions; + + /** + * Controls how the model responds to function calls, as specified in the OpenAI documentation. + */ + @JsonProperty("function_call") + ChatCompletionRequestFunctionCall functionCall; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ChatCompletionRequestFunctionCall { + String name; + + public static ChatCompletionRequestFunctionCall of(String name) { + return new ChatCompletionRequestFunctionCall(name); + } + + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionResult.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionResult.java new file mode 100644 index 00000000..11b4071a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatCompletionResult.java @@ -0,0 +1,43 @@ +package com.theokanning.openai.completion.chat; +import com.theokanning.openai.Usage; +import lombok.Data; + +import java.util.List; + +/** + * Object containing a response from the chat completions api. + */ +@Data +public class ChatCompletionResult { + + /** + * Unique id assigned to this chat completion. + */ + String id; + + /** + * The type of object returned, should be "chat.completion" + */ + String object; + + /** + * The creation time in epoch seconds. + */ + long created; + + /** + * The GPT model used. + */ + String model; + + /** + * A list of all generated completions. + */ + List choices; + + /** + * The API usage for this request. + */ + Usage usage; + +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java new file mode 100644 index 00000000..820f4bd6 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunction.java @@ -0,0 +1,70 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +import java.util.function.Function; + +@Data +@NoArgsConstructor +public class ChatFunction { + + /** + * The name of the function being called. + */ + @NonNull + private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ + private String description; + + /** + * The parameters the functions accepts. + */ + @JsonProperty("parameters") + private Class parametersClass; + + @JsonIgnore + private Function executor; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String description; + private Class parameters; + private Function executor; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder executor(Class requestClass, Function executor) { + this.parameters = requestClass; + this.executor = (Function) executor; + return this; + } + + public ChatFunction build() { + ChatFunction chatFunction = new ChatFunction(); + chatFunction.setName(name); + chatFunction.setDescription(description); + chatFunction.setParametersClass(parameters); + chatFunction.setExecutor(executor); + return chatFunction; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java new file mode 100644 index 00000000..962fbe12 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionCall.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatFunctionCall { + + /** + * The name of the function being called + */ + String name; + + /** + * The arguments of the call produced by the model, represented as a JsonNode for easy manipulation. + */ + JsonNode arguments; + +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java new file mode 100644 index 00000000..9b4f2070 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionDynamic.java @@ -0,0 +1,62 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; +import lombok.NonNull; + + +@Data +public class ChatFunctionDynamic { + + /** + * The name of the function being called. + */ + @NonNull + private String name; + + /** + * A description of what the function does, used by the model to choose when and how to call the function. + */ + private String description; + + /** + * The parameters the functions accepts. + */ + private ChatFunctionParameters parameters; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String name; + private String description; + private ChatFunctionParameters parameters = new ChatFunctionParameters(); + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder parameters(ChatFunctionParameters parameters) { + this.parameters = parameters; + return this; + } + + public Builder addProperty(ChatFunctionProperty property) { + this.parameters.addProperty(property); + return this; + } + + public ChatFunctionDynamic build() { + ChatFunctionDynamic chatFunction = new ChatFunctionDynamic(name); + chatFunction.setDescription(description); + chatFunction.setParameters(parameters); + return chatFunction; + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java new file mode 100644 index 00000000..fee71e8f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionParameters.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.completion.chat; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Data +public class ChatFunctionParameters { + + private final String type = "object"; + + private final HashMap properties = new HashMap<>(); + + private List required; + + public void addProperty(ChatFunctionProperty property) { + properties.put(property.getName(), property); + if (Boolean.TRUE.equals(property.getRequired())) { + if (this.required == null) { + this.required = new ArrayList<>(); + } + this.required.add(property.getName()); + } + } +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java new file mode 100644 index 00000000..3e695933 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatFunctionProperty.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +import java.util.Set; + +@Data +@Builder +public class ChatFunctionProperty { + @NonNull + @JsonIgnore + private String name; + @NonNull + private String type; + @JsonIgnore + private Boolean required; + private String description; + private ChatFunctionProperty items; + @JsonProperty("enum") + private Set enumValues; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java new file mode 100644 index 00000000..912a71f0 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessage.java @@ -0,0 +1,47 @@ +package com.theokanning.openai.completion.chat; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + *

Each object has a role (either "system", "user", or "assistant") and content (the content of the message). Conversations can be as short as 1 message or fill many pages.

+ *

Typically, a conversation is formatted with a system message first, followed by alternating user and assistant messages.

+ *

The system message helps set the behavior of the assistant. In the example above, the assistant was instructed with "You are a helpful assistant."
+ * The user messages help instruct the assistant. They can be generated by the end users of an application, or set by a developer as an instruction.
+ * The assistant messages help store prior responses. They can also be written by a developer to help give examples of desired behavior. + *

+ * + * see OpenAi documentation + */ +@Data +@NoArgsConstructor(force = true) +@RequiredArgsConstructor +@AllArgsConstructor +public class ChatMessage { + + /** + * Must be either 'system', 'user', 'assistant' or 'function'.
+ * You may use {@link ChatMessageRole} enum. + */ + @NonNull + String role; + @JsonInclude() // content should always exist in the call, even if it is null + String content; + //name is optional, The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters. + String name; + @JsonProperty("function_call") + ChatFunctionCall functionCall; + + public ChatMessage(String role, String content) { + this.role = role; + this.content = content; + } + + public ChatMessage(String role, String content, String name) { + this.role = role; + this.content = content; + this.name = name; + } + +} diff --git a/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java new file mode 100644 index 00000000..255641e0 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/completion/chat/ChatMessageRole.java @@ -0,0 +1,21 @@ +package com.theokanning.openai.completion.chat; + +/** + * see {@link ChatMessage} documentation. + */ +public enum ChatMessageRole { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"), + FUNCTION("function"); + + private final String value; + + ChatMessageRole(final String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/api/src/main/java/com/theokanning/openai/edit/EditChoice.java b/api/src/main/java/com/theokanning/openai/edit/EditChoice.java index bde78928..98c7c5a9 100644 --- a/api/src/main/java/com/theokanning/openai/edit/EditChoice.java +++ b/api/src/main/java/com/theokanning/openai/edit/EditChoice.java @@ -3,7 +3,7 @@ import lombok.Data; /** - * An edit generated by GPT-3 + * An edit generated by OpenAi * * https://beta.openai.com/docs/api-reference/edits/create */ diff --git a/api/src/main/java/com/theokanning/openai/edit/EditResult.java b/api/src/main/java/com/theokanning/openai/edit/EditResult.java index 415ba749..7816ba0e 100644 --- a/api/src/main/java/com/theokanning/openai/edit/EditResult.java +++ b/api/src/main/java/com/theokanning/openai/edit/EditResult.java @@ -6,7 +6,7 @@ import java.util.List; /** - * A list of edits generated by GPT-3 + * A list of edits generated by OpenAI * * https://beta.openai.com/docs/api-reference/edits/create */ diff --git a/api/src/main/java/com/theokanning/openai/embedding/EmbeddingRequest.java b/api/src/main/java/com/theokanning/openai/embedding/EmbeddingRequest.java index d52014dc..33a5d196 100644 --- a/api/src/main/java/com/theokanning/openai/embedding/EmbeddingRequest.java +++ b/api/src/main/java/com/theokanning/openai/embedding/EmbeddingRequest.java @@ -26,7 +26,7 @@ public class EmbeddingRequest { * To get embeddings for multiple inputs in a single request, pass an array of strings or array of token arrays. * Each input must not exceed 2048 tokens in length. *

- * Unless your are embedding code, we suggest replacing newlines (\n) in your input with a single space, + * Unless you are embedding code, we suggest replacing newlines (\n) in your input with a single space, * as we have observed inferior results when newlines are present. */ @NonNull diff --git a/api/src/main/java/com/theokanning/openai/embedding/EmbeddingResult.java b/api/src/main/java/com/theokanning/openai/embedding/EmbeddingResult.java index 58d9b9c3..41f9208e 100644 --- a/api/src/main/java/com/theokanning/openai/embedding/EmbeddingResult.java +++ b/api/src/main/java/com/theokanning/openai/embedding/EmbeddingResult.java @@ -14,7 +14,7 @@ public class EmbeddingResult { /** - * The GPT-3 model used for generating embeddings + * The GPTmodel used for generating embeddings */ String model; 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 2882e0b2..07708f8e 100644 --- a/api/src/main/java/com/theokanning/openai/file/File.java +++ b/api/src/main/java/com/theokanning/openai/file/File.java @@ -1,5 +1,6 @@ package com.theokanning.openai.file; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** @@ -28,6 +29,7 @@ public class File { /** * The creation time in epoch seconds. */ + @JsonProperty("created_at") Long createdAt; /** @@ -39,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/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..c653c048 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/fine_tuning/FineTuningEvent.java @@ -0,0 +1,43 @@ +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 ID of the fine-tuning event. + */ + String id; + + /** + * 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 ccf6d37f..7b8f76c3 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneEvent.java @@ -1,5 +1,6 @@ package com.theokanning.openai.finetune; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** @@ -7,6 +8,7 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneEvent { /** @@ -17,6 +19,7 @@ public class FineTuneEvent { /** * The creation time in epoch seconds. */ + @JsonProperty("created_at") Long createdAt; /** 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 a992cf19..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 @@ -21,11 +22,13 @@ public class FineTuneRequest { * 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. */ + @JsonProperty("validation_file") String validationFile; /** @@ -37,6 +40,7 @@ public class FineTuneRequest { /** * The number of epochs to train the model for. An epoch refers to one full cycle through the training dataset. */ + @JsonProperty("n_epochs") Integer nEpochs; /** @@ -46,6 +50,7 @@ public class FineTuneRequest { * By default, the batch size will be dynamically configured to be ~0.2% of the number of examples in the training * set, capped at 256 - in general, we've found that larger batch sizes tend to work better for larger datasets. */ + @JsonProperty("batch_size") Integer batchSize; /** @@ -56,6 +61,7 @@ public class FineTuneRequest { * (larger learning rates tend to perform better with larger batch sizes). * We recommend experimenting with values in the range 0.02 to 0.2 to see what produces the best results. */ + @JsonProperty("learning_rate_multiplier") Double learningRateMultiplier; /** @@ -67,6 +73,7 @@ public class FineTuneRequest { * If prompts are extremely long (relative to completions), it may make sense to reduce this weight so as to * avoid over-prioritizing learning the prompt. */ + @JsonProperty("prompt_loss_weight") Double promptLossWeight; /** @@ -77,6 +84,7 @@ public class FineTuneRequest { * Additionally, you must specify {@link FineTuneRequest#classificationNClasses} for multiclass * classification or {@link FineTuneRequest#classificationPositiveClass} for binary classification. */ + @JsonProperty("compute_classification_metrics") Boolean computeClassificationMetrics; /** @@ -92,6 +100,7 @@ public class FineTuneRequest { * * This parameter is needed to generate precision, recall, and F1 metrics when doing binary classification. */ + @JsonProperty("classification_positive_class") String classificationPositiveClass; /** @@ -102,6 +111,7 @@ public class FineTuneRequest { * A larger beta score puts more weight on recall and less on precision. * A smaller beta score puts more weight on precision and less on recall. */ + @JsonProperty("classification_betas") List classificationBetas; /** 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 d964c9ff..6cbcce41 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java +++ b/api/src/main/java/com/theokanning/openai/finetune/FineTuneResult.java @@ -1,15 +1,17 @@ package com.theokanning.openai.finetune; +import com.fasterxml.jackson.annotation.JsonProperty; import com.theokanning.openai.file.File; import lombok.Data; import java.util.List; /** - * An object describing an fine-tuned model. Returned by multiple fine-tune requests. + * An object describing a fine-tuned model. Returned by multiple fine-tune requests. * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class FineTuneResult { /** @@ -30,6 +32,7 @@ public class FineTuneResult { /** * The creation time in epoch seconds. */ + @JsonProperty("created_at") Long createdAt; /** @@ -41,6 +44,7 @@ public class FineTuneResult { * The ID of the fine-tuned model, null if tuning job is not finished. * This is the id used to call the model. */ + @JsonProperty("fine_tuned_model") String fineTunedModel; /** @@ -51,11 +55,13 @@ public class FineTuneResult { /** * The ID of the organization this model belongs to. */ + @JsonProperty("organization_id") String organizationId; /** * Result files for this fine-tune job. */ + @JsonProperty("result_files") List resultFiles; /** @@ -66,15 +72,18 @@ public class FineTuneResult { /** * Training files for this fine-tune job. */ + @JsonProperty("training_files") List trainingFiles; /** * The last update time in epoch seconds. */ + @JsonProperty("updated_at") Long updatedAt; /** * Validation files for this fine-tune job. */ + @JsonProperty("validation_files") List validationFiles; } 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 94e82e91..d1d383ed 100644 --- a/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java +++ b/api/src/main/java/com/theokanning/openai/finetune/HyperParameters.java @@ -1,5 +1,6 @@ package com.theokanning.openai.finetune; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** @@ -7,26 +8,31 @@ * * https://beta.openai.com/docs/api-reference/fine-tunes */ +@Deprecated @Data public class HyperParameters { /** * The batch size to use for training. */ - String batchSize; + @JsonProperty("batch_size") + Integer batchSize; /** * The learning rate multiplier to use for training. */ + @JsonProperty("learning_rate_multiplier") Double learningRateMultiplier; /** * The number of epochs to train the model for. */ + @JsonProperty("n_epochs") Integer nEpochs; /** * The weight to use for loss on the prompt tokens. */ + @JsonProperty("prompt_loss_weight") Double promptLossWeight; } diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java new file mode 100644 index 00000000..7d37f689 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageEditRequest.java @@ -0,0 +1,49 @@ +package com.theokanning.openai.image; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to edit an image based on a prompt + * All fields except prompt are optional + * + * https://beta.openai.com/docs/api-reference/images/create-edit + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateImageEditRequest { + + /** + * A text description of the desired image(s). The maximum length in 1000 characters. + */ + @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. + */ + Integer n; + + /** + * The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024". Defaults to "1024x1024". + */ + String size; + + /** + * The format in which the generated images are returned. Must be one of url or b64_json. Defaults to url. + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. + */ + String user; +} diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java new file mode 100644 index 00000000..13672c24 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageRequest.java @@ -0,0 +1,60 @@ +package com.theokanning.openai.image; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.*; + +/** + * A request for OpenAi to create an image based on a prompt + * All fields except prompt are optional + * + * https://beta.openai.com/docs/api-reference/images/create + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateImageRequest { + + /** + * 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 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 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; + + /** + * The format in which the generated images are returned. Must be one of url or b64_json. Defaults to url. + */ + @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. + */ + String user; +} diff --git a/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java new file mode 100644 index 00000000..f16f613d --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/image/CreateImageVariationRequest.java @@ -0,0 +1,43 @@ +package com.theokanning.openai.image; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +/** + * A request for OpenAi to create a variation of an image + * All fields are optional + * + * https://beta.openai.com/docs/api-reference/images/create-variation + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class CreateImageVariationRequest { + + /** + * The number of images to generate. Must be between 1 and 10. Defaults to 1. + */ + 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". + */ + String size; + + /** + * The format in which the generated images are returned. Must be one of url or b64_json. Defaults to url. + */ + @JsonProperty("response_format") + String responseFormat; + + /** + * A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse. + */ + String user; +} diff --git a/api/src/main/java/com/theokanning/openai/image/Image.java b/api/src/main/java/com/theokanning/openai/image/Image.java new file mode 100644 index 00000000..6b8391ed --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/image/Image.java @@ -0,0 +1,30 @@ +package com.theokanning.openai.image; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * An object containing either a URL or a base 64 encoded image. + * + * https://beta.openai.com/docs/api-reference/images + */ +@Data +public class Image { + /** + * The URL where the image can be accessed. + */ + String url; + + + /** + * Base64 encoded image string. + */ + @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/api/src/main/java/com/theokanning/openai/image/ImageResult.java b/api/src/main/java/com/theokanning/openai/image/ImageResult.java new file mode 100644 index 00000000..9576dbfd --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/image/ImageResult.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.image; + +import lombok.Data; + +import java.util.List; + +/** + * An object with a list of image results. + * + * https://beta.openai.com/docs/api-reference/images + */ +@Data +public class ImageResult { + + /** + * The creation time in epoch seconds. + */ + Long created; + + /** + * List of image results. + */ + List data; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/Message.java b/api/src/main/java/com/theokanning/openai/messages/Message.java new file mode 100644 index 00000000..44144780 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/Message.java @@ -0,0 +1,81 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + + +/** + * Represents a Message within a thread. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Message { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread.message. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the message was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * The thread ID that this message belongs to. + */ + @JsonProperty("thread_id") + String threadId; + + /** + * The entity that produced the message. One of user or assistant. + */ + String role; + + /** + * The content of the message in an array of text and/or images. + */ + List content; + + /** + * If applicable, the ID of the assistant that authored this message. + */ + @JsonProperty("assistant_id") + String assistantId; + + /** + * If applicable, the ID of the run associated with the authoring of this message. + */ + @JsonProperty("run_id") + String runId; + + /** + * A list of file IDs that the assistant should use. + * Useful for tools like retrieval and code_interpreter that can access files. + * A maximum of 10 files can be attached to a message. + */ + @JsonProperty("file_ids") + List fileIds; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} \ No newline at end of file diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageContent.java b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java new file mode 100644 index 00000000..a9ff489e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageContent.java @@ -0,0 +1,31 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.messages.content.ImageFile; +import com.theokanning.openai.messages.content.Text; +import lombok.Data; + + +/** + * Represents the content of a message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +public class MessageContent { + /** + * The content type, either "text" or "image_file" + */ + String type; + + /** + * Text content of the message. Only present if type == text + */ + Text text; + + /** + * The image content of a message. Only present if type == image_file + */ + @JsonProperty("image_file") + ImageFile imageFile; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageFile.java b/api/src/main/java/com/theokanning/openai/messages/MessageFile.java new file mode 100644 index 00000000..7a0dd3bd --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageFile.java @@ -0,0 +1,38 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A list of files attached to a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/file-object + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MessageFile { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread.message.file. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the message file was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * The ID of the message that the File is attached to. + */ + @JsonProperty("message_id") + String messageId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java b/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java new file mode 100644 index 00000000..6c49a110 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/MessageRequest.java @@ -0,0 +1,48 @@ +package com.theokanning.openai.messages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +/** + * Creates a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/createMessage + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class MessageRequest { + /** + * The role of the entity that is creating the message. + * Currently only "user" is supported. + */ + @NonNull + @Builder.Default + String role = "user"; + + /** + * The content of the message. + */ + @NonNull + String content; + + /** + * A list of File IDs that the message should use. + * Defaults to an empty list. + * There can be a maximum of 10 files attached to a message. + * Useful for tools like retrieval and code_interpreter that can access and use files. + */ + @JsonProperty("file_ids") + List fileIds; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java b/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java new file mode 100644 index 00000000..32344c96 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/ModifyMessageRequest.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.messages; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Modifies a Message + *

+ * https://platform.openai.com/docs/api-reference/messages/modifyMessage + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ModifyMessageRequest { + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java b/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java new file mode 100644 index 00000000..473a5dfc --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/Annotation.java @@ -0,0 +1,44 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * An annotation for a text Message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Annotation { + /** + * The type of annotation, either file_citation or file_path + */ + String type; + + /** + * The text in the message content that needs to be replaced + */ + String text; + + /** + * File citation details, only present when type == file_citation + */ + @JsonProperty("file_citation") + FileCitation fileCitation; + + /** + * File path details, only present when type == file_path + */ + @JsonProperty("file_path") + FilePath filePath; + + @JsonProperty("start_index") + int startIndex; + + @JsonProperty("end_index") + int endIndex; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java b/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java new file mode 100644 index 00000000..ff486d55 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/FileCitation.java @@ -0,0 +1,29 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A citation within the message that points to a specific quote from a specific File associated with the + * assistant or the message. Generated when the assistant uses the "retrieval" tool to search files. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FileCitation { + + /** + * The ID of the specific File the citation is from. + */ + @JsonProperty("file_id") + String fileId; + + /** + * The specific quote in the file. + */ + String quote; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java b/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java new file mode 100644 index 00000000..b11cc1af --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/FilePath.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A URL for the file that's generated when the assistant used the code_interpreter tool to generate a file. + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FilePath { + + /** + * The ID of the file that was generated + */ + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java b/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java new file mode 100644 index 00000000..2a43fa02 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/ImageFile.java @@ -0,0 +1,23 @@ +package com.theokanning.openai.messages.content; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * References an image File int eh content of a message. + *

+ * /https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ImageFile { + + /** + * The File ID of the image in the message content. + */ + @JsonProperty("file_id") + String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/messages/content/Text.java b/api/src/main/java/com/theokanning/openai/messages/content/Text.java new file mode 100644 index 00000000..6efa28bf --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/messages/content/Text.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.messages.content; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The text content that is part of a message + *

+ * https://platform.openai.com/docs/api-reference/messages/object + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Text { + + /** + * The data that makes up the text. + */ + String value; + + /** + * Text annotations that show additional details + */ + List annotations; +} diff --git a/api/src/main/java/com/theokanning/openai/model/Model.java b/api/src/main/java/com/theokanning/openai/model/Model.java index 829ac5ff..2add679d 100644 --- a/api/src/main/java/com/theokanning/openai/model/Model.java +++ b/api/src/main/java/com/theokanning/openai/model/Model.java @@ -1,11 +1,12 @@ package com.theokanning.openai.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; /** - * GPT-3 model details + * GPT model details * * https://beta.openai.com/docs/api-reference/models */ @@ -22,13 +23,15 @@ public class Model { public String object; /** - * The owner of the GPT-3 model, typically "openai" + * The owner of the model, typically "openai" */ + @JsonProperty("owned_by") public String ownedBy; /** - * List of permissions for this model + * List of permissions for this model. No longer returned by OpenAI */ + @Deprecated public List permission; /** diff --git a/api/src/main/java/com/theokanning/openai/model/Permission.java b/api/src/main/java/com/theokanning/openai/model/Permission.java index 416df7f3..475f053f 100644 --- a/api/src/main/java/com/theokanning/openai/model/Permission.java +++ b/api/src/main/java/com/theokanning/openai/model/Permission.java @@ -1,9 +1,10 @@ package com.theokanning.openai.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** - * GPT-3 model permissions + * Model permissions * I couldn't find documentation for the specific permissions, and I've elected to leave them undocumented rather than * write something incorrect. * @@ -26,22 +27,29 @@ public class Permission { */ public long created; + @JsonProperty("allow_create_engine") public boolean allowCreateEngine; + @JsonProperty("allow_sampling") public boolean allowSampling; + @JsonProperty("allow_log_probs") public boolean allowLogProbs; + @JsonProperty("allow_search_indices") public boolean allowSearchIndices; + @JsonProperty("allow_view") public boolean allowView; + @JsonProperty("allow_fine_tuning") public boolean allowFineTuning; public String organization; public String group; + @JsonProperty("is_blocking") public boolean isBlocking; } diff --git a/api/src/main/java/com/theokanning/openai/moderation/Moderation.java b/api/src/main/java/com/theokanning/openai/moderation/Moderation.java index b638c824..4d7de8b5 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/Moderation.java +++ b/api/src/main/java/com/theokanning/openai/moderation/Moderation.java @@ -1,5 +1,6 @@ package com.theokanning.openai.moderation; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; /** @@ -26,5 +27,6 @@ public class Moderation { * The value is between 0 and 1, where higher values denote higher confidence. * The scores should not be interpreted as probabilities. */ + @JsonProperty("category_scores") public ModerationCategoryScores categoryScores; } diff --git a/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java b/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java index 585109b1..238d0a05 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java +++ b/api/src/main/java/com/theokanning/openai/moderation/ModerationCategories.java @@ -1,7 +1,6 @@ package com.theokanning.openai.moderation; import com.fasterxml.jackson.annotation.JsonProperty; -import com.theokanning.openai.completion.CompletionChoice; import lombok.Data; import java.util.List; diff --git a/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java b/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java index 6e51e466..08513420 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java +++ b/api/src/main/java/com/theokanning/openai/moderation/ModerationRequest.java @@ -2,8 +2,6 @@ import lombok.*; -import java.util.List; - /** * A request for OpenAi to detect if text violates OpenAi's content policy. * diff --git a/api/src/main/java/com/theokanning/openai/moderation/ModerationResult.java b/api/src/main/java/com/theokanning/openai/moderation/ModerationResult.java index 3b9190c6..f8b432c2 100644 --- a/api/src/main/java/com/theokanning/openai/moderation/ModerationResult.java +++ b/api/src/main/java/com/theokanning/openai/moderation/ModerationResult.java @@ -17,7 +17,7 @@ public class ModerationResult { public String id; /** - * The GPT-3 model used. + * The model used. */ public String model; diff --git a/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java b/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java new file mode 100644 index 00000000..b27de696 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/CreateThreadAndRunRequest.java @@ -0,0 +1,39 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.threads.ThreadRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 23:08 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateThreadAndRunRequest { + + @JsonProperty("assistant_id") + private String assistantId; + + private ThreadRequest thread; + + private String model; + + private String instructions; + + private List tools; + + private Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java new file mode 100644 index 00000000..535fd7e2 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/MessageCreation.java @@ -0,0 +1,17 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MessageCreation { + + @JsonProperty("message_id") + String messageId; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java b/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java new file mode 100644 index 00000000..959b89dc --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RequiredAction.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:44 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RequiredAction { + + private String type; + + @JsonProperty("submit_tool_outputs") + private SubmitToolOutputs submitToolOutputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/Run.java b/api/src/main/java/com/theokanning/openai/runs/Run.java new file mode 100644 index 00000000..1da9ec95 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/Run.java @@ -0,0 +1,66 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.common.LastError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Run { + + private String id; + + private String object; + + @JsonProperty("created_at") + private Integer createdAt; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("assistant_id") + private String assistantId; + + private String status; + + @JsonProperty("required_action") + private RequiredAction requiredAction; + + @JsonProperty("last_error") + private LastError lastError; + + @JsonProperty("expires_at") + private Integer expiresAt; + + @JsonProperty("started_at") + private Integer startedAt; + + @JsonProperty("cancelled_at") + private Integer cancelledAt; + + @JsonProperty("failed_at") + private Integer failedAt; + + @JsonProperty("completed_at") + private Integer completedAt; + + private String model; + + private String instructions; + + private List tools; + + @JsonProperty("file_ids") + private List fileIds; + + private Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java new file mode 100644 index 00000000..cba5f283 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunCreateRequest.java @@ -0,0 +1,27 @@ +package com.theokanning.openai.runs; + +import com.theokanning.openai.assistants.Tool; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class RunCreateRequest { + String assistantId; + + // Optional + String model; + + String instructions; + + List tools; + + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunImage.java b/api/src/main/java/com/theokanning/openai/runs/RunImage.java new file mode 100644 index 00000000..18135187 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunImage.java @@ -0,0 +1,24 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:33 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RunImage { + + @JsonProperty("file_id") + private String fileId; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/RunStep.java b/api/src/main/java/com/theokanning/openai/runs/RunStep.java new file mode 100644 index 00000000..7cd39342 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/RunStep.java @@ -0,0 +1,58 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.common.LastError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class RunStep { + + private String id; + + private String object; + + @JsonProperty("created_at") + private Integer createdAt; + + @JsonProperty("assistant_id") + private String assistantId; + + @JsonProperty("thread_id") + private String threadId; + + @JsonProperty("run_id") + private String runId; + + private String type; + + private String status; + + @JsonProperty("step_details") + private StepDetails stepDetails; + + @JsonProperty("last_error") + private LastError lastError; + + @JsonProperty("expired_at") + private Integer expiredAt; + + @JsonProperty("cancelled_at") + private Integer cancelledAt; + + @JsonProperty("failed_at") + private Integer failedAt; + + @JsonProperty("completed_at") + private Integer completedAt; + + private Map metadata; + +} diff --git a/api/src/main/java/com/theokanning/openai/runs/StepDetails.java b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java new file mode 100644 index 00000000..08972623 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/StepDetails.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StepDetails { + + + private String type; + + @JsonProperty("message_creation") + private MessageCreation messageCreation; + + @JsonProperty("tool_calls") + private List toolCalls; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java new file mode 100644 index 00000000..ec2b346a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputRequestItem.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputRequestItem { + + @JsonProperty("tool_call_id") + private String toolCallId; + + private String output; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java new file mode 100644 index 00000000..e0aca757 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputs.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputs { + + @JsonProperty("tool_calls") + List toolCalls; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java new file mode 100644 index 00000000..f892f168 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/SubmitToolOutputsRequest.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:45 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SubmitToolOutputsRequest { + + @JsonProperty("tool_outputs") + private List toolOutputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCall.java b/api/src/main/java/com/theokanning/openai/runs/ToolCall.java new file mode 100644 index 00000000..8aa6c26c --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCall.java @@ -0,0 +1,34 @@ +package com.theokanning.openai.runs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:32 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCall { + + private String id; + + private String type; + + @JsonProperty("code_interpreter") + private ToolCallCodeInterpreter codeInterpreter; + + private Map retrieval; + + private ToolCallFunction function; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java new file mode 100644 index 00000000..c2a3a446 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreter.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:34 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallCodeInterpreter { + + private String input; + + private List outputs; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java new file mode 100644 index 00000000..179ef2f1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallCodeInterpreterOutput.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:34 + **/ + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallCodeInterpreterOutput { + + private String type; + + private String logs; + + private RunImage image; +} diff --git a/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java new file mode 100644 index 00000000..34de58d1 --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/runs/ToolCallFunction.java @@ -0,0 +1,25 @@ +package com.theokanning.openai.runs; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @description: + * @author: vacuity + * @create: 2023-11-16 22:38 + **/ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ToolCallFunction { + + private String name; + + private String arguments; + + private String output; +} diff --git a/api/src/main/java/com/theokanning/openai/search/SearchRequest.java b/api/src/main/java/com/theokanning/openai/search/SearchRequest.java deleted file mode 100644 index 4d5c4b26..00000000 --- a/api/src/main/java/com/theokanning/openai/search/SearchRequest.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.theokanning.openai.search; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * A request to the document search api. - * GPT-3 will perform a semantic search over the documents and score them based on how related they are to the query. - * Higher scores indicate a stronger relation. - * - * https://beta.openai.com/docs/api-reference/searches - */ -@Deprecated -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Data -public class SearchRequest { - - /** - * Documents to search over - */ - List documents; - - /** - * Search query - */ - String query; -} diff --git a/api/src/main/java/com/theokanning/openai/search/SearchResult.java b/api/src/main/java/com/theokanning/openai/search/SearchResult.java deleted file mode 100644 index ee35128b..00000000 --- a/api/src/main/java/com/theokanning/openai/search/SearchResult.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.theokanning.openai.search; - -import lombok.Data; - -/** - * A search result for a single document. - * - * https://beta.openai.com/docs/api-reference/searches - */ -@Deprecated -@Data -public class SearchResult { - /** - * The position of this document in the request list - */ - Integer document; - - /** - * The type of object returned, should be "search_result" - */ - String object; - - /** - * A number measuring the document's correlation with the query. - * A higher score means a stronger relationship. - */ - Double score; -} diff --git a/api/src/main/java/com/theokanning/openai/threads/Thread.java b/api/src/main/java/com/theokanning/openai/threads/Thread.java new file mode 100644 index 00000000..24adb73f --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/threads/Thread.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.threads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Represents a Thread with an assistant + *

+ * https://platform.openai.com/docs/api-reference/threads/object + */ +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Thread { + /** + * The identifier, which can be referenced in API endpoints. + */ + String id; + + /** + * The object type, which is always thread. + */ + String object; + + /** + * The Unix timestamp (in seconds) for when the thread was created. + */ + @JsonProperty("created_at") + int createdAt; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} diff --git a/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java new file mode 100644 index 00000000..35fd888a --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/threads/ThreadRequest.java @@ -0,0 +1,34 @@ +package com.theokanning.openai.threads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.theokanning.openai.messages.MessageRequest; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Creates a thread + *

+ * https://platform.openai.com/docs/api-reference/threads/createThread + */ +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ThreadRequest { + /** + * A list of messages to start the thread with. Optional. + */ + List messages; + + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object in a structured format. + * Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + */ + Map metadata; +} 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..0a50907e --- /dev/null +++ b/api/src/main/java/com/theokanning/openai/utils/TikTokensUtil.java @@ -0,0 +1,300 @@ +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 java.util.*; + +/** + * Token calculation tool class + */ +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)); + modelMap.put(ModelEnum.GPT_4_1106_preview.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"), + + /** + * Temporary model, not recommended for use. + */ + GPT_4_1106_preview("gpt-4-1106-preview"); + 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/api/src/test/java/com/theokanning/openai/JsonTest.java b/api/src/test/java/com/theokanning/openai/JsonTest.java new file mode 100644 index 00000000..58b5c5f2 --- /dev/null +++ b/api/src/test/java/com/theokanning/openai/JsonTest.java @@ -0,0 +1,72 @@ +package com.theokanning.openai; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.*; +import com.theokanning.openai.audio.TranscriptionResult; +import com.theokanning.openai.audio.TranslationResult; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.edit.EditRequest; +import com.theokanning.openai.edit.EditResult; +import com.theokanning.openai.embedding.EmbeddingRequest; +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.ImageResult; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.model.Model; +import com.theokanning.openai.moderation.ModerationRequest; +import com.theokanning.openai.moderation.ModerationResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.*; + +public class JsonTest { + + @ParameterizedTest + @ValueSource(classes = { + ChatCompletionRequest.class, + ChatCompletionResult.class, + DeleteResult.class, + EditRequest.class, + EditResult.class, + EmbeddingRequest.class, + EmbeddingResult.class, + Engine.class, + File.class, + FineTuneEvent.class, + FineTuneResult.class, + FineTuningEvent.class, + FineTuningJob.class, + FineTuningJobRequest.class, + ImageResult.class, + TranscriptionResult.class, + TranslationResult.class, + Message.class, + Model.class, + ModerationRequest.class, + ModerationResult.class + }) + void objectMatchesJson(Class clazz) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + String path = "src/test/resources/fixtures/" + clazz.getSimpleName() + ".json"; + byte[] bytes = Files.readAllBytes(Paths.get(path)); + String json = new String(bytes); + + String actual = mapper.writeValueAsString(mapper.readValue(json, clazz)); + + // Convert to JsonNodes to avoid any json formatting differences + assertEquals(mapper.readTree(json), mapper.readTree(actual)); + } +} diff --git a/api/src/test/resources/fixtures/ChatCompletionRequest.json b/api/src/test/resources/fixtures/ChatCompletionRequest.json new file mode 100644 index 00000000..b5cfd390 --- /dev/null +++ b/api/src/test/resources/fixtures/ChatCompletionRequest.json @@ -0,0 +1,4 @@ +{ + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello!"}] +} diff --git a/api/src/test/resources/fixtures/ChatCompletionResult.json b/api/src/test/resources/fixtures/ChatCompletionResult.json new file mode 100644 index 00000000..6dfcf5c8 --- /dev/null +++ b/api/src/test/resources/fixtures/ChatCompletionResult.json @@ -0,0 +1,18 @@ +{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "\n\nHello there, how may I assist you today?" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } +} diff --git a/api/src/test/resources/fixtures/CreateImageRequest.json b/api/src/test/resources/fixtures/CreateImageRequest.json new file mode 100644 index 00000000..8625e938 --- /dev/null +++ b/api/src/test/resources/fixtures/CreateImageRequest.json @@ -0,0 +1,5 @@ +{ + "prompt": "A cute baby sea otter", + "n": 2, + "size": "1024x1024" +} diff --git a/api/src/test/resources/fixtures/DeleteResult.json b/api/src/test/resources/fixtures/DeleteResult.json new file mode 100644 index 00000000..f0207713 --- /dev/null +++ b/api/src/test/resources/fixtures/DeleteResult.json @@ -0,0 +1,5 @@ +{ + "id": "file-XjGxS3KTG0uNmNOK362iJua3", + "object": "file", + "deleted": true +} diff --git a/api/src/test/resources/fixtures/EditRequest.json b/api/src/test/resources/fixtures/EditRequest.json new file mode 100644 index 00000000..c47cc379 --- /dev/null +++ b/api/src/test/resources/fixtures/EditRequest.json @@ -0,0 +1,5 @@ +{ + "model": "text-davinci-edit-001", + "input": "What day of the wek is it?", + "instruction": "Fix the spelling mistakes" +} diff --git a/api/src/test/resources/fixtures/EditResult.json b/api/src/test/resources/fixtures/EditResult.json new file mode 100644 index 00000000..63cdc6e1 --- /dev/null +++ b/api/src/test/resources/fixtures/EditResult.json @@ -0,0 +1,15 @@ +{ + "object": "edit", + "created": 1589478378, + "choices": [ + { + "text": "What day of the week is it?", + "index": 0 + } + ], + "usage": { + "prompt_tokens": 25, + "completion_tokens": 32, + "total_tokens": 57 + } +} diff --git a/api/src/test/resources/fixtures/EmbeddingRequest.json b/api/src/test/resources/fixtures/EmbeddingRequest.json new file mode 100644 index 00000000..ae49108f --- /dev/null +++ b/api/src/test/resources/fixtures/EmbeddingRequest.json @@ -0,0 +1,4 @@ +{ + "model": "text-embedding-ada-002", + "input": ["The food was delicious and the waiter..."] +} diff --git a/api/src/test/resources/fixtures/EmbeddingResult.json b/api/src/test/resources/fixtures/EmbeddingResult.json new file mode 100644 index 00000000..97cc6442 --- /dev/null +++ b/api/src/test/resources/fixtures/EmbeddingResult.json @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "object": "embedding", + "embedding": [ + 0.0023064255, + -0.009327292, + -0.0028842222 + ], + "index": 0 + } + ], + "model": "text-embedding-ada-002", + "usage": { + "completion_tokens": 8, + "prompt_tokens": 8, + "total_tokens": 8 + } +} diff --git a/api/src/test/resources/fixtures/Engine.json b/api/src/test/resources/fixtures/Engine.json new file mode 100644 index 00000000..ed9828ea --- /dev/null +++ b/api/src/test/resources/fixtures/Engine.json @@ -0,0 +1,6 @@ +{ + "id": "engine-id-0", + "object": "engine", + "owner": "organization-owner", + "ready": true +} diff --git a/api/src/test/resources/fixtures/File.json b/api/src/test/resources/fixtures/File.json new file mode 100644 index 00000000..9fa0c564 --- /dev/null +++ b/api/src/test/resources/fixtures/File.json @@ -0,0 +1,10 @@ +{ + "id": "file-ccdDZrC3iZVNiQVeEA6Z66wf", + "object": "file", + "bytes": 175, + "created_at": 1613677385, + "filename": "train.jsonl", + "purpose": "search", + "status": "error", + "status_details": "File is too large." +} diff --git a/api/src/test/resources/fixtures/FineTuneEvent.json b/api/src/test/resources/fixtures/FineTuneEvent.json new file mode 100644 index 00000000..429f3e62 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuneEvent.json @@ -0,0 +1,6 @@ +{ + "object": "fine-tune-event", + "created_at": 1614807352, + "level": "info", + "message": "Job enqueued. Waiting for jobs ahead to complete. Queue number: 0." +} diff --git a/api/src/test/resources/fixtures/FineTuneResult.json b/api/src/test/resources/fixtures/FineTuneResult.json new file mode 100644 index 00000000..119d4273 --- /dev/null +++ b/api/src/test/resources/fixtures/FineTuneResult.json @@ -0,0 +1,36 @@ +{ + "id": "ft-AF1WoRqd3aJAHsqc9NY7iL8F", + "object": "fine-tune", + "model": "curie", + "created_at": 1614807352, + "events": [ + { + "object": "fine-tune-event", + "created_at": 1614807352, + "level": "info", + "message": "Job enqueued. Waiting for jobs ahead to complete. Queue number: 0." + } + ], + "fine_tuned_model": "model", + "hyperparams": { + "batch_size": 4, + "learning_rate_multiplier": 0.1, + "n_epochs": 4, + "prompt_loss_weight": 0.1 + }, + "organization_id": "org-...", + "result_files": [], + "status": "pending", + "validation_files": [], + "training_files": [ + { + "id": "file-XGinujblHPwGLSztz8cPS8XY", + "object": "file", + "bytes": 1547276, + "created_at": 1610062281, + "filename": "my-data-train.jsonl", + "purpose": "fine-tune-train" + } + ], + "updated_at": 1614807352 +} 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" +} diff --git a/api/src/test/resources/fixtures/ImageResult.json b/api/src/test/resources/fixtures/ImageResult.json new file mode 100644 index 00000000..6cbd7e54 --- /dev/null +++ b/api/src/test/resources/fixtures/ImageResult.json @@ -0,0 +1,11 @@ +{ + "created": 1589478378, + "data": [ + { + "url": "https://..." + }, + { + "url": "https://..." + } + ] +} diff --git a/api/src/test/resources/fixtures/Message.json b/api/src/test/resources/fixtures/Message.json new file mode 100644 index 00000000..878e7a47 --- /dev/null +++ b/api/src/test/resources/fixtures/Message.json @@ -0,0 +1,46 @@ +{ + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1698983503, + "thread_id": "thread_abc123", + "role": "assistant", + "content": [ + { + "type": "text", + "text": { + "value": "Hi! How can I help you today?", + "annotations": [ + { + "type": "file_citation", + "text": "file citation text", + "file_citation": { + "file_id": "file citation id", + "quote": "Enough, Reggie" + }, + "start_index": 0, + "end_index": 1 + }, + { + "type": "file_path", + "text": "file path text", + "file_path": { + "file_id": "file id" + }, + "start_index": 1, + "end_index": 2 + } + ] + } + }, + { + "type": "image_file", + "image_file": { + "file_id": "image file id" + } + } + ], + "file_ids": [], + "assistant_id": "asst_abc123", + "run_id": "run_abc123", + "metadata": {} +} diff --git a/api/src/test/resources/fixtures/Model.json b/api/src/test/resources/fixtures/Model.json new file mode 100644 index 00000000..3aacb2a4 --- /dev/null +++ b/api/src/test/resources/fixtures/Model.json @@ -0,0 +1,6 @@ +{ + "id": "model-id-0", + "object": "model", + "owned_by": "organization-owner", + "permission": [] +} diff --git a/api/src/test/resources/fixtures/ModerationRequest.json b/api/src/test/resources/fixtures/ModerationRequest.json new file mode 100644 index 00000000..bf1beb80 --- /dev/null +++ b/api/src/test/resources/fixtures/ModerationRequest.json @@ -0,0 +1,3 @@ +{ + "input": "I want to kill them." +} diff --git a/api/src/test/resources/fixtures/ModerationResult.json b/api/src/test/resources/fixtures/ModerationResult.json new file mode 100644 index 00000000..97081528 --- /dev/null +++ b/api/src/test/resources/fixtures/ModerationResult.json @@ -0,0 +1,27 @@ +{ + "id": "modr-5MWoLO", + "model": "text-moderation-001", + "results": [ + { + "categories": { + "hate": false, + "hate/threatening": true, + "self-harm": false, + "sexual": false, + "sexual/minors": false, + "violence": true, + "violence/graphic": false + }, + "category_scores": { + "hate": 0.22714105248451233, + "hate/threatening": 0.4132447838783264, + "self-harm": 0.005232391878962517, + "sexual": 0.01407341007143259, + "sexual/minors": 0.0038522258400917053, + "violence": 0.9223177433013916, + "violence/graphic": 0.036865197122097015 + }, + "flagged": true + } + ] +} diff --git a/api/src/test/resources/fixtures/TranscriptionResult.json b/api/src/test/resources/fixtures/TranscriptionResult.json new file mode 100644 index 00000000..769a139e --- /dev/null +++ b/api/src/test/resources/fixtures/TranscriptionResult.json @@ -0,0 +1,27 @@ +{ + "task": "transcribe", + "language": "english", + "duration": 1.1, + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 0.96, + "text": " Hello World.", + "tokens": [ + 50364, + 2425, + 3937, + 13, + 50412 + ], + "temperature": 0.0, + "avg_logprob": -0.7308251063028971, + "compression_ratio": 0.6, + "no_speech_prob": 0.015335720032453537, + "transient": false + } + ], + "text": "Hello World." +} \ No newline at end of file diff --git a/api/src/test/resources/fixtures/TranslationResult.json b/api/src/test/resources/fixtures/TranslationResult.json new file mode 100644 index 00000000..3e211cab --- /dev/null +++ b/api/src/test/resources/fixtures/TranslationResult.json @@ -0,0 +1,37 @@ +{ + "task": "translate", + "language": "english", + "duration": 4.38, + "segments": [ + { + "id": 0, + "seek": 0, + "start": 0.0, + "end": 4.32, + "text": " Hello, my name is Yuna. I am Korean voice.", + "tokens": [ + 50364, + 2425, + 11, + 452, + 1315, + 307, + 398, + 5051, + 13, + 286, + 669, + 6933, + 3177, + 13, + 50580 + ], + "temperature": 0.0, + "avg_logprob": -0.6644304394721985, + "compression_ratio": 0.84, + "no_speech_prob": 0.006824055220931768, + "transient": false + } + ], + "text": "Hello, my name is Yuna. I am Korean voice." +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 97f4c918..23e4f934 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,8 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.19.0' - } +plugins { + id "com.vanniktech.maven.publish" version "0.19.0" apply false } allprojects { - repositories { - mavenCentral() - } - plugins.withId("com.vanniktech.maven.publish") { mavenPublish { sonatypeHost = "S01" diff --git a/client/build.gradle b/client/build.gradle index 538632af..6bc08d7d 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -3,11 +3,11 @@ apply plugin: "com.vanniktech.maven.publish" dependencies { api project(":api") - api 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' - implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' + api libs.retrofit + api libs.retrofitRxJava2 + implementation libs.retrofitJackson - testImplementation(platform('org.junit:junit-bom:5.8.2')) + testImplementation(platform(libs.junitBom)) testImplementation('org.junit.jupiter:junit-jupiter') } diff --git a/client/gradle.properties b/client/gradle.properties index 94fab2a7..f8641e7b 100644 --- a/client/gradle.properties +++ b/client/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=client POM_NAME=client -POM_DESCRIPTION=Basic retrofit client for OpenAI's GPT-3 API +POM_DESCRIPTION=Basic retrofit client for OpenAI's GPT APIs diff --git a/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java b/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java index 7e53c6a4..fbe9a5b4 100644 --- a/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java +++ b/client/src/main/java/com/theokanning/openai/AuthenticationInterceptor.java @@ -1,28 +1,15 @@ package com.theokanning.openai; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -import java.io.IOException; - /** * OkHttp Interceptor that adds an authorization token header + * + * @deprecated Use {@link com.theokanning.openai.client.AuthenticationInterceptor} */ -public class AuthenticationInterceptor implements Interceptor { - - private final String token; +@Deprecated +public class AuthenticationInterceptor extends com.theokanning.openai.client.AuthenticationInterceptor { AuthenticationInterceptor(String token) { - this.token = token; + super(token); } - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request() - .newBuilder() - .header("Authorization", "Bearer " + token) - .build(); - return chain.proceed(request); - } } diff --git a/client/src/main/java/com/theokanning/openai/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/OpenAiApi.java index a3cc0d64..feb2f6fc 100644 --- a/client/src/main/java/com/theokanning/openai/OpenAiApi.java +++ b/client/src/main/java/com/theokanning/openai/OpenAiApi.java @@ -1,113 +1,9 @@ package com.theokanning.openai; -import com.theokanning.openai.answer.AnswerRequest; -import com.theokanning.openai.answer.AnswerResult; -import com.theokanning.openai.classification.ClassificationRequest; -import com.theokanning.openai.classification.ClassificationResult; -import com.theokanning.openai.completion.CompletionRequest; -import com.theokanning.openai.completion.CompletionResult; -import com.theokanning.openai.edit.EditRequest; -import com.theokanning.openai.edit.EditResult; -import com.theokanning.openai.embedding.EmbeddingRequest; -import com.theokanning.openai.embedding.EmbeddingResult; -import com.theokanning.openai.engine.Engine; -import com.theokanning.openai.file.File; -import com.theokanning.openai.finetune.FineTuneEvent; -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.model.Model; -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.moderation.ModerationResult; -import com.theokanning.openai.search.SearchRequest; -import com.theokanning.openai.search.SearchResult; -import io.reactivex.Single; -import okhttp3.MultipartBody; -import okhttp3.RequestBody; -import retrofit2.http.*; - -public interface OpenAiApi { - - @GET("v1/models") - Single> listModels(); - - @GET("/v1/models/{model_id}") - Single getModel(@Path("model_id") String modelId); - - @POST("/v1/completions") - Single createCompletion(@Body CompletionRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/completions") - Single createCompletion(@Path("engine_id") String engineId, @Body CompletionRequest request); - - @POST("/v1/edits") - Single createEdit(@Body EditRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/edits") - Single createEdit(@Path("engine_id") String engineId, @Body EditRequest request); - - @POST("/v1/embeddings") - Single createEmbeddings(@Body EmbeddingRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/embeddings") - Single createEmbeddings(@Path("engine_id") String engineId, @Body EmbeddingRequest request); - - @GET("/v1/files") - Single> listFiles(); - - @Multipart - @POST("/v1/files") - Single uploadFile(@Part("purpose") RequestBody purpose, @Part MultipartBody.Part file); - - @DELETE("/v1/files/{file_id}") - Single deleteFile(@Path("file_id") String fileId); - - @GET("/v1/files/{file_id}") - Single retrieveFile(@Path("file_id") String fileId); - - @POST("/v1/fine-tunes") - Single createFineTune(@Body FineTuneRequest request); - - @POST("/v1/completions") - Single createFineTuneCompletion(@Body CompletionRequest request); - - @GET("/v1/fine-tunes") - Single> listFineTunes(); - - @GET("/v1/fine-tunes/{fine_tune_id}") - Single retrieveFineTune(@Path("fine_tune_id") String fineTuneId); - - @POST("/v1/fine-tunes/{fine_tune_id}/cancel") - Single cancelFineTune(@Path("fine_tune_id") String fineTuneId); - - @GET("/v1/fine-tunes/{fine_tune_id}/events") - Single> listFineTuneEvents(@Path("fine_tune_id") String fineTuneId); - - @DELETE("/v1/models/{fine_tune_id}") - Single deleteFineTune(@Path("fine_tune_id") String fineTuneId); - - @POST("/v1/moderations") - Single createModeration(@Body ModerationRequest request); - - @Deprecated - @GET("v1/engines") - Single> getEngines(); - - @Deprecated - @GET("/v1/engines/{engine_id}") - Single getEngine(@Path("engine_id") String engineId); - - @Deprecated - @POST("v1/answers") - Single createAnswer(@Body AnswerRequest request); - - @Deprecated - @POST("v1/classifications") - Single createClassification(@Body ClassificationRequest request); - - @Deprecated - @POST("/v1/engines/{engine_id}/search") - Single> search(@Path("engine_id") String engineId, @Body SearchRequest request); +/** + * @deprecated Use {@link com.theokanning.openai.client.OpenAiApi} + */ +@Deprecated +public interface OpenAiApi extends com.theokanning.openai.client.OpenAiApi { + // For legacy compatibility only. } diff --git a/client/src/main/java/com/theokanning/openai/OpenAiService.java b/client/src/main/java/com/theokanning/openai/OpenAiService.java deleted file mode 100644 index 43b99e42..00000000 --- a/client/src/main/java/com/theokanning/openai/OpenAiService.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.theokanning.openai; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.theokanning.openai.answer.AnswerRequest; -import com.theokanning.openai.answer.AnswerResult; -import com.theokanning.openai.classification.ClassificationRequest; -import com.theokanning.openai.classification.ClassificationResult; -import com.theokanning.openai.completion.CompletionRequest; -import com.theokanning.openai.completion.CompletionResult; -import com.theokanning.openai.edit.EditRequest; -import com.theokanning.openai.edit.EditResult; -import com.theokanning.openai.embedding.EmbeddingRequest; -import com.theokanning.openai.embedding.EmbeddingResult; -import com.theokanning.openai.engine.Engine; -import com.theokanning.openai.file.File; -import com.theokanning.openai.finetune.FineTuneEvent; -import com.theokanning.openai.finetune.FineTuneRequest; -import com.theokanning.openai.finetune.FineTuneResult; -import com.theokanning.openai.model.Model; -import com.theokanning.openai.moderation.ModerationRequest; -import com.theokanning.openai.moderation.ModerationResult; -import com.theokanning.openai.search.SearchRequest; -import com.theokanning.openai.search.SearchResult; -import okhttp3.*; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.jackson.JacksonConverterFactory; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class OpenAiService { - - OpenAiApi api; - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - */ - public OpenAiService(String token) { - this(token, 10); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - * @param timeout http read timeout in seconds, 0 means no timeout - */ - public OpenAiService(String token, int timeout) { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); - - OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(new AuthenticationInterceptor(token)) - .connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS)) - .readTimeout(timeout, TimeUnit.SECONDS) - .build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl("https://api.openai.com/") - .client(client) - .addConverterFactory(JacksonConverterFactory.create(mapper)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .build(); - - this.api = retrofit.create(OpenAiApi.class); - } - - /** - * Creates a new OpenAiService that wraps OpenAiApi - * @param api OpenAiApi instance to use for all methods - */ - public OpenAiService(OpenAiApi api) { - this.api = api; - } - - public List listModels() { - return api.listModels().blockingGet().data; - } - - public Model getModel(String modelId) { - return api.getModel(modelId).blockingGet(); - } - - public CompletionResult createCompletion(CompletionRequest request) { - return api.createCompletion(request).blockingGet(); - } - - /** Use {@link OpenAiService#createCompletion(CompletionRequest)} and {@link CompletionRequest#model}instead */ - @Deprecated - public CompletionResult createCompletion(String engineId, CompletionRequest request) { - return api.createCompletion(engineId, request).blockingGet(); - } - - public EditResult createEdit(EditRequest request) { - return api.createEdit(request).blockingGet(); - } - - /** Use {@link OpenAiService#createEdit(EditRequest)} and {@link EditRequest#model}instead */ - @Deprecated - public EditResult createEdit(String engineId, EditRequest request) { - return api.createEdit(engineId, request).blockingGet(); - } - - public EmbeddingResult createEmbeddings(EmbeddingRequest request) { - return api.createEmbeddings(request).blockingGet(); - } - - /** Use {@link OpenAiService#createEmbeddings(EmbeddingRequest)} and {@link EmbeddingRequest#model}instead */ - @Deprecated - public EmbeddingResult createEmbeddings(String engineId, EmbeddingRequest request) { - return api.createEmbeddings(engineId, request).blockingGet(); - } - - public List listFiles() { - return api.listFiles().blockingGet().data; - } - - public File uploadFile(String purpose, String filepath) { - java.io.File file = new java.io.File(filepath); - RequestBody purposeBody = RequestBody.create(okhttp3.MultipartBody.FORM, purpose); - RequestBody fileBody = RequestBody.create(MediaType.parse("text"), file); - MultipartBody.Part body = MultipartBody.Part.createFormData("file", filepath, fileBody); - - return api.uploadFile(purposeBody, body).blockingGet(); - } - - public DeleteResult deleteFile(String fileId) { - return api.deleteFile(fileId).blockingGet(); - } - - public File retrieveFile(String fileId) { - return api.retrieveFile(fileId).blockingGet(); - } - - public FineTuneResult createFineTune(FineTuneRequest request) { - return api.createFineTune(request).blockingGet(); - } - - public CompletionResult createFineTuneCompletion(CompletionRequest request) { - return api.createFineTuneCompletion(request).blockingGet(); - } - - public List listFineTunes() { - return api.listFineTunes().blockingGet().data; - } - - public FineTuneResult retrieveFineTune(String fineTuneId) { - return api.retrieveFineTune(fineTuneId).blockingGet(); - } - - public FineTuneResult cancelFineTune(String fineTuneId) { - return api.cancelFineTune(fineTuneId).blockingGet(); - } - - public List listFineTuneEvents(String fineTuneId) { - return api.listFineTuneEvents(fineTuneId).blockingGet().data; - } - - public DeleteResult deleteFineTune(String fineTuneId) { - return api.deleteFineTune(fineTuneId).blockingGet(); - } - - public ModerationResult createModeration(ModerationRequest request) { - return api.createModeration(request).blockingGet(); - } - - @Deprecated - public List getEngines() { - return api.getEngines().blockingGet().data; - } - - @Deprecated - public Engine getEngine(String engineId) { - return api.getEngine(engineId).blockingGet(); - } - - @Deprecated - public AnswerResult createAnswer(AnswerRequest request) { - return api.createAnswer(request).blockingGet(); - } - - @Deprecated - public ClassificationResult createClassification(ClassificationRequest request) { - return api.createClassification(request).blockingGet(); - } - - @Deprecated - public List search(String engineId, SearchRequest request) { - return api.search(engineId, request).blockingGet().data; - } -} diff --git a/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java b/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java new file mode 100644 index 00000000..d89c36f1 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/AuthenticationInterceptor.java @@ -0,0 +1,28 @@ +package com.theokanning.openai.client; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +/** + * OkHttp Interceptor that adds an authorization token header + */ +public class AuthenticationInterceptor implements Interceptor { + + private final String token; + + protected AuthenticationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request() + .newBuilder() + .header("Authorization", "Bearer " + token) + .build(); + return chain.proceed(request); + } +} diff --git a/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java new file mode 100644 index 00000000..7342e953 --- /dev/null +++ b/client/src/main/java/com/theokanning/openai/client/OpenAiApi.java @@ -0,0 +1,324 @@ +package com.theokanning.openai.client; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.audio.CreateSpeechRequest; +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; +import com.theokanning.openai.completion.chat.ChatCompletionResult; +import com.theokanning.openai.edit.EditRequest; +import com.theokanning.openai.edit.EditResult; +import com.theokanning.openai.embedding.EmbeddingRequest; +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; +import com.theokanning.openai.image.CreateImageRequest; +import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; +import com.theokanning.openai.model.Model; +import com.theokanning.openai.moderation.ModerationRequest; +import com.theokanning.openai.moderation.ModerationResult; +import com.theokanning.openai.runs.CreateThreadAndRunRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import io.reactivex.Single; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.*; + +import java.time.LocalDate; +import java.util.Map; + +public interface OpenAiApi { + + @GET("v1/models") + Single> listModels(); + + @GET("/v1/models/{model_id}") + Single getModel(@Path("model_id") String modelId); + + @POST("/v1/completions") + Single createCompletion(@Body CompletionRequest request); + + @Streaming + @POST("/v1/completions") + Call createCompletionStream(@Body CompletionRequest request); + + @POST("/v1/chat/completions") + Single createChatCompletion(@Body ChatCompletionRequest request); + + @Streaming + @POST("/v1/chat/completions") + Call createChatCompletionStream(@Body ChatCompletionRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/completions") + Single createCompletion(@Path("engine_id") String engineId, @Body CompletionRequest request); + + @POST("/v1/edits") + Single createEdit(@Body EditRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/edits") + Single createEdit(@Path("engine_id") String engineId, @Body EditRequest request); + + @POST("/v1/embeddings") + Single createEmbeddings(@Body EmbeddingRequest request); + + @Deprecated + @POST("/v1/engines/{engine_id}/embeddings") + Single createEmbeddings(@Path("engine_id") String engineId, @Body EmbeddingRequest request); + + @GET("/v1/files") + Single> listFiles(); + + @Multipart + @POST("/v1/files") + Single uploadFile(@Part("purpose") RequestBody purpose, @Part MultipartBody.Part file); + + @DELETE("/v1/files/{file_id}") + Single deleteFile(@Path("file_id") String fileId); + + @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); + + @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); + + @DELETE("/v1/models/{fine_tune_id}") + Single deleteFineTune(@Path("fine_tune_id") String fineTuneId); + + @POST("/v1/images/generations") + Single createImage(@Body CreateImageRequest request); + + @POST("/v1/images/edits") + Single createImageEdit(@Body RequestBody requestBody); + + @POST("/v1/images/variations") + Single createImageVariation(@Body RequestBody requestBody); + + @POST("/v1/audio/transcriptions") + Single createTranscription(@Body RequestBody requestBody); + + @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); + + @Deprecated + @GET("v1/engines") + Single> getEngines(); + + @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 + */ + @Deprecated + @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. + */ + @Deprecated + @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 AssistantRequest 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 ModifyAssistantRequest 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); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads") + Single createThread(@Body ThreadRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}") + Single retrieveThread(@Path("thread_id") String threadId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}") + Single modifyThread(@Path("thread_id") String threadId, @Body ThreadRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @DELETE("/v1/threads/{thread_id}") + Single deleteThread(@Path("thread_id") String threadId); + + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}/messages") + Single createMessage(@Path("thread_id") String threadId, @Body MessageRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}") + Single retrieveMessage(@Path("thread_id") String threadId, @Path("message_id") String messageId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @POST("/v1/threads/{thread_id}/messages/{message_id}") + Single modifyMessage(@Path("thread_id") String threadId, @Path("message_id") String messageId, @Body ModifyMessageRequest request); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages") + Single> listMessages(@Path("thread_id") String threadId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages") + Single> listMessages(@Path("thread_id") String threadId, @QueryMap Map filterRequest); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files/{file_id}") + Single retrieveMessageFile(@Path("thread_id") String threadId, @Path("message_id") String messageId, @Path("file_id") String fileId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files") + Single> listMessageFiles(@Path("thread_id") String threadId, @Path("message_id") String messageId); + + @Headers({"OpenAI-Beta: assistants=v1"}) + @GET("/v1/threads/{thread_id}/messages/{message_id}/files") + Single> listMessageFiles(@Path("thread_id") String threadId, @Path("message_id") String messageId, @QueryMap Map filterRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs") + Single createRun(@Path("thread_id") String threadId, @Body RunCreateRequest runCreateRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}") + Single retrieveRun(@Path("thread_id") String threadId, @Path("run_id") String runId); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}") + Single modifyRun(@Path("thread_id") String threadId, @Path("run_id") String runId, @Body Map metadata); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs") + Single> listRuns(@Path("thread_id") String threadId, @QueryMap Map listSearchParameters); + + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}/submit_tool_outputs") + Single submitToolOutputs(@Path("thread_id") String threadId, @Path("run_id") String runId, @Body SubmitToolOutputsRequest submitToolOutputsRequest); + + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/{thread_id}/runs/{run_id}/cancel") + Single cancelRun(@Path("thread_id") String threadId, @Path("run_id") String runId); + + @Headers("OpenAI-Beta: assistants=v1") + @POST("/v1/threads/runs") + Single createThreadAndRun(@Body CreateThreadAndRunRequest createThreadAndRunRequest); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}/steps/{step_id}") + Single retrieveRunStep(@Path("thread_id") String threadId, @Path("run_id") String runId, @Path("step_id") String stepId); + + @Headers("OpenAI-Beta: assistants=v1") + @GET("/v1/threads/{thread_id}/runs/{run_id}/steps") + Single> listRunSteps(@Path("thread_id") String threadId, @Path("run_id") String runId, @QueryMap Map listSearchParameters); +} diff --git a/client/src/test/java/com/theokanning/openai/AnswerTest.java b/client/src/test/java/com/theokanning/openai/AnswerTest.java deleted file mode 100644 index 499e42d4..00000000 --- a/client/src/test/java/com/theokanning/openai/AnswerTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.answer.AnswerRequest; -import com.theokanning.openai.answer.AnswerResult; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - - -public class AnswerTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createAnswer() { - AnswerRequest answerRequest = AnswerRequest.builder() - .documents(Arrays.asList("Puppy A is happy.", "Puppy B is sad.")) - .question("which puppy is happy?") - .searchModel("ada") - .model("curie") - .examplesContext("In 2017, U.S. life expectancy was 78.6 years.") - .examples(Collections.singletonList( - Arrays.asList("What is human life expectancy in the United States?", "78 years.") - )) - .maxTokens(5) - .stop(Arrays.asList("\n", "<|endoftext|>")) - .build(); - - AnswerResult result = service.createAnswer(answerRequest); - - assertNotNull(result.getAnswers().get(0)); - } -} diff --git a/client/src/test/java/com/theokanning/openai/ClassificationTest.java b/client/src/test/java/com/theokanning/openai/ClassificationTest.java deleted file mode 100644 index e7e024ad..00000000 --- a/client/src/test/java/com/theokanning/openai/ClassificationTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.classification.ClassificationRequest; -import com.theokanning.openai.classification.ClassificationResult; -import com.theokanning.openai.completion.CompletionChoice; -import com.theokanning.openai.completion.CompletionRequest; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - - -public class ClassificationTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void createCompletion() { - ClassificationRequest classificationRequest = ClassificationRequest.builder() - .examples(Arrays.asList( - Arrays.asList("A happy moment", "Positive"), - Arrays.asList("I am sad.", "Negative"), - Arrays.asList("I am feeling awesome", "Positive") - )) - .query("It is a raining day :(") - .model("curie") - .searchModel("ada") - .labels(Arrays.asList("Positive", "Negative", "Neutral")) - .build(); - - ClassificationResult result = service.createClassification(classificationRequest); - - assertNotNull(result.getCompletion()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/EngineTest.java b/client/src/test/java/com/theokanning/openai/EngineTest.java deleted file mode 100644 index 5e522731..00000000 --- a/client/src/test/java/com/theokanning/openai/EngineTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.engine.Engine; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - - -public class EngineTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void getEngines() { - List engines = service.getEngines(); - - assertFalse(engines.isEmpty()); - } - - @Test - void getEngine() { - Engine ada = service.getEngine("ada"); - - assertEquals("ada", ada.id); - } -} diff --git a/client/src/test/java/com/theokanning/openai/ModelTest.java b/client/src/test/java/com/theokanning/openai/ModelTest.java deleted file mode 100644 index be7f573e..00000000 --- a/client/src/test/java/com/theokanning/openai/ModelTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.model.Model; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - - -public class ModelTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void listModels() { - List models = service.listModels(); - - assertFalse(models.isEmpty()); - } - - @Test - void getModel() { - Model ada = service.getModel("ada"); - - assertEquals("ada", ada.id); - assertEquals("openai", ada.ownedBy); - assertFalse(ada.permission.isEmpty()); - } -} diff --git a/client/src/test/java/com/theokanning/openai/SearchTest.java b/client/src/test/java/com/theokanning/openai/SearchTest.java deleted file mode 100644 index 657bfb39..00000000 --- a/client/src/test/java/com/theokanning/openai/SearchTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.theokanning.openai; - -import com.theokanning.openai.search.SearchRequest; -import com.theokanning.openai.search.SearchResult; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.List; - - -import static org.junit.jupiter.api.Assertions.assertFalse; - -public class SearchTest { - - String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); - - @Test - void search() { - SearchRequest searchRequest = SearchRequest.builder() - .documents(Arrays.asList("Water", "Earth", "Electricity", "Fire")) - .query("Pikachu") - .build(); - - List results = service.search("ada", searchRequest); - assertFalse(results.isEmpty()); - } -} diff --git a/example/build.gradle b/example/build.gradle index f7f95e53..f64639e9 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,9 +2,29 @@ apply plugin: 'java' apply plugin: 'application' application { - mainClassName = 'example.OpenAiApiExample' + mainClass.set('example.OpenAiApiExample') } dependencies { - implementation project(":client") + implementation project(":service") +} + +task runExampleOne(type: JavaExec) { + mainClass.set('example.OpenAiApiExample') + classpath = sourceSets.main.runtimeClasspath + args = [] +} + +task runExampleTwo(type: JavaExec) { + mainClass.set('example.OpenAiApiFunctionsExample') + classpath = sourceSets.main.runtimeClasspath + args = [] + standardInput = System.in +} + +task runExampleThree(type: JavaExec) { + mainClass.set('example.OpenAiApiFunctionsWIthStreamExample') + classpath = sourceSets.main.runtimeClasspath + args = [] + standardInput = System.in } \ No newline at end of file diff --git a/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java new file mode 100644 index 00000000..75f9b8e2 --- /dev/null +++ b/example/src/main/java/example/OpenAiApiDynamicFunctionExample.java @@ -0,0 +1,90 @@ +package example; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.service.OpenAiService; + +import java.util.*; + +public class OpenAiApiDynamicFunctionExample { + + private static JsonNode getWeather(String location, String unit) { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode response = mapper.createObjectNode(); + response.put("location", location); + response.put("unit", unit); + response.put("temperature", new Random().nextInt(50)); + response.put("description", "sunny"); + return response; + } + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + ChatFunctionDynamic weatherFunction = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) + .required(true) + .build()) + .build(); + + + List messages = new ArrayList<>(); + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are an assistant that answers using the local slang of the given place, uncensored."); + messages.add(systemMessage); + + System.out.print("First Query: "); + Scanner scanner = new Scanner(System.in); + ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine()); + messages.add(firstMsg); + + while (true) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(weatherFunction)) + .functionCall(ChatCompletionRequest.ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); + messages.add(responseMessage); // don't forget to update the conversation with the latest response + + ChatFunctionCall functionCall = responseMessage.getFunctionCall(); + if (functionCall != null) { + if (functionCall.getName().equals("get_weather")) { + String location = functionCall.getArguments().get("location").asText(); + String unit = functionCall.getArguments().get("unit").asText(); + JsonNode weather = getWeather(location, unit); + ChatMessage weatherMessage = new ChatMessage(ChatMessageRole.FUNCTION.value(), weather.toString(), "get_weather"); + messages.add(weatherMessage); + continue; + } + } + + System.out.println("Response: " + responseMessage.getContent()); + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} diff --git a/example/src/main/java/example/OpenAiApiExample.java b/example/src/main/java/example/OpenAiApiExample.java index 27b6671f..52ae1ccf 100644 --- a/example/src/main/java/example/OpenAiApiExample.java +++ b/example/src/main/java/example/OpenAiApiExample.java @@ -1,20 +1,57 @@ package example; -import com.theokanning.openai.OpenAiService; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; +import com.theokanning.openai.service.OpenAiService; 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; 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() - .model("ada") + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) .user("testing") + .n(3) .build(); service.createCompletion(completionRequest).getChoices().forEach(System.out::println); + + System.out.println("\nCreating Image..."); + CreateImageRequest request = CreateImageRequest.builder() + .prompt("A cow breakdancing with a turtle") + .build(); + + System.out.println("\nImage is located at:"); + System.out.println(service.createImage(request).getData().get(0).getUrl()); + + System.out.println("Streaming chat completion..."); + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a dog and will speak as such."); + messages.add(systemMessage); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo") + .messages(messages) + .n(1) + .maxTokens(50) + .logitBias(new HashMap<>()) + .build(); + + service.streamChatCompletion(chatCompletionRequest) + .doOnError(Throwable::printStackTrace) + .blockingForEach(System.out::println); + + service.shutdownExecutor(); } } diff --git a/example/src/main/java/example/OpenAiApiFunctionsExample.java b/example/src/main/java/example/OpenAiApiFunctionsExample.java new file mode 100644 index 00000000..954b9104 --- /dev/null +++ b/example/src/main/java/example/OpenAiApiFunctionsExample.java @@ -0,0 +1,109 @@ +package example; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.completion.chat.ChatCompletionRequest.ChatCompletionRequestFunctionCall; +import com.theokanning.openai.service.FunctionExecutor; +import com.theokanning.openai.service.OpenAiService; + +import java.util.*; + +class OpenAiApiFunctionsExample { + + public static class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; + } + + public enum WeatherUnit { + CELSIUS, FAHRENHEIT; + } + + public static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + public WeatherResponse(String location, WeatherUnit unit, int temperature, String description) { + this.location = location; + this.unit = unit; + this.temperature = temperature; + this.description = description; + } + } + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + FunctionExecutor functionExecutor = new FunctionExecutor(Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .build())); + + + List messages = new ArrayList<>(); + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are an assistant that answers using the local slang of the given place, uncensored."); + messages.add(systemMessage); + + System.out.print("First Query: "); + Scanner scanner = new Scanner(System.in); + ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine()); + messages.add(firstMsg); + + while (true) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall(ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + ChatMessage responseMessage = service.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage(); + messages.add(responseMessage); // don't forget to update the conversation with the latest response + + ChatFunctionCall functionCall = responseMessage.getFunctionCall(); + if (functionCall != null) { + System.out.println("Trying to execute " + functionCall.getName() + "..."); + Optional message = functionExecutor.executeAndConvertToMessageSafely(functionCall); + /* You can also try 'executeAndConvertToMessage' inside a try-catch block, and add the following line inside the catch: + "message = executor.handleException(exception);" + The content of the message will be the exception itself, so the flow of the conversation will not be interrupted, and you will still be able to log the issue. */ + + if (message.isPresent()) { + /* At this point: + 1. The function requested was found + 2. The request was converted to its specified object for execution (Weather.class in this case) + 3. It was executed + 4. The response was finally converted to a ChatMessage object. */ + + System.out.println("Executed " + functionCall.getName() + "."); + messages.add(message.get()); + continue; + } else { + System.out.println("Something went wrong with the execution of " + functionCall.getName() + "..."); + break; + } + } + + System.out.println("Response: " + responseMessage.getContent()); + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} diff --git a/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java new file mode 100644 index 00000000..e6de65b6 --- /dev/null +++ b/example/src/main/java/example/OpenAiApiFunctionsWithStreamExample.java @@ -0,0 +1,86 @@ +package example; + +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.service.FunctionExecutor; +import com.theokanning.openai.service.OpenAiService; +import example.OpenAiApiFunctionsExample.Weather; +import example.OpenAiApiFunctionsExample.WeatherResponse; +import io.reactivex.Flowable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +public class OpenAiApiFunctionsWithStreamExample { + + public static void main(String... args) { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + FunctionExecutor functionExecutor = new FunctionExecutor(Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather of a location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, new Random().nextInt(50), "sunny")) + .build())); + + List messages = new ArrayList<>(); + ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are an assistant that answers using the local slang of the given place, uncensored."); + messages.add(systemMessage); + + System.out.print("First Query: "); + Scanner scanner = new Scanner(System.in); + ChatMessage firstMsg = new ChatMessage(ChatMessageRole.USER.value(), scanner.nextLine()); + messages.add(firstMsg); + + while (true) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .functionCall(ChatCompletionRequest.ChatCompletionRequestFunctionCall.of("auto")) + .n(1) + .maxTokens(256) + .logitBias(new HashMap<>()) + .build(); + Flowable flowable = service.streamChatCompletion(chatCompletionRequest); + + AtomicBoolean isFirst = new AtomicBoolean(true); + ChatMessage chatMessage = service.mapStreamToAccumulator(flowable) + .doOnNext(accumulator -> { + if (accumulator.isFunctionCall()) { + if (isFirst.getAndSet(false)) { + System.out.println("Executing function " + accumulator.getAccumulatedChatFunctionCall().getName() + "..."); + } + } else { + if (isFirst.getAndSet(false)) { + System.out.print("Response: "); + } + if (accumulator.getMessageChunk().getContent() != null) { + System.out.print(accumulator.getMessageChunk().getContent()); + } + } + }) + .doOnComplete(System.out::println) + .lastElement() + .blockingGet() + .getAccumulatedMessage(); + messages.add(chatMessage); // don't forget to update the conversation with the latest response + + if (chatMessage.getFunctionCall() != null) { + System.out.println("Trying to execute " + chatMessage.getFunctionCall().getName() + "..."); + ChatMessage functionResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(chatMessage.getFunctionCall()); + System.out.println("Executed " + chatMessage.getFunctionCall().getName() + "."); + messages.add(functionResponse); + continue; + } + + System.out.print("Next Query: "); + String nextLine = scanner.nextLine(); + if (nextLine.equalsIgnoreCase("exit")) { + System.exit(0); + } + messages.add(new ChatMessage(ChatMessageRole.USER.value(), nextLine)); + } + } + +} \ No newline at end of file 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.properties b/gradle.properties index 05f82acc..a4f840ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.theokanning.openai-gpt3-java -VERSION_NAME=0.8.1 +VERSION_NAME=0.18.2 POM_URL=https://github.com/theokanning/openai-java POM_SCM_URL=https://github.com/theokanning/openai-java @@ -12,3 +12,7 @@ POM_LICENSE_DIST=repo POM_DEVELOPER_ID=theokanning POM_DEVELOPER_NAME=Theo Kanning + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ecccc347 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +[versions] +jackson = "2.14.2" +retrofit = "2.9.0" + +[libraries] +jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jacksonAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } +jacksonJsonSchema = { module = "com.kjetland:mbknor-jackson-jsonschema_2.12", version = "1.0.34" } +lombok = { module = "org.projectlombok:lombok", version = "1.18.24" } +junitBom = { module = "org.junit:junit-bom", version = "5.8.2" } +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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053..943f0cbf 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fce..bdc9a83b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c515..65dcd68d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 5093609d..93e3f59f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +65,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,17 +72,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/service/build.gradle b/service/build.gradle new file mode 100644 index 00000000..6c242724 --- /dev/null +++ b/service/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'java-library' +apply plugin: "com.vanniktech.maven.publish" + +dependencies { + api project(":client") + api libs.retrofit + implementation libs.retrofitRxJava2 + implementation libs.retrofitJackson + implementation libs.jacksonJsonSchema + + testImplementation(platform(libs.junitBom)) + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation libs.retrofitMock +} + +compileJava { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +test { + useJUnitPlatform() +} diff --git a/service/gradle.properties b/service/gradle.properties new file mode 100644 index 00000000..4bd45061 --- /dev/null +++ b/service/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=service +POM_NAME=service +POM_DESCRIPTION=Basic service to create and use an OpenAI retrofit client diff --git a/service/src/main/java/com/theokanning/openai/service/AuthenticationInterceptor.java b/service/src/main/java/com/theokanning/openai/service/AuthenticationInterceptor.java new file mode 100644 index 00000000..798eccb7 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/AuthenticationInterceptor.java @@ -0,0 +1,30 @@ +package com.theokanning.openai.service; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.util.Objects; + +/** + * OkHttp Interceptor that adds an authorization token header + */ +public class AuthenticationInterceptor implements Interceptor { + + private final String token; + + AuthenticationInterceptor(String token) { + Objects.requireNonNull(token, "OpenAI token required"); + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request() + .newBuilder() + .header("Authorization", "Bearer " + token) + .build(); + return chain.proceed(request); + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java new file mode 100644 index 00000000..e2f24ffb --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestMixIn.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +public abstract class ChatCompletionRequestMixIn { + + @JsonSerialize(using = ChatCompletionRequestSerializerAndDeserializer.Serializer.class) + @JsonDeserialize(using = ChatCompletionRequestSerializerAndDeserializer.Deserializer.class) + abstract ChatCompletionRequest.ChatCompletionRequestFunctionCall getFunctionCall(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java new file mode 100644 index 00000000..e9a8b023 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatCompletionRequestSerializerAndDeserializer.java @@ -0,0 +1,41 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; + +import java.io.IOException; + +public class ChatCompletionRequestSerializerAndDeserializer { + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(ChatCompletionRequest.ChatCompletionRequestFunctionCall value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null || value.getName() == null) { + gen.writeNull(); + } else if ("none".equals(value.getName()) || "auto".equals(value.getName())) { + gen.writeString(value.getName()); + } else { + gen.writeStartObject(); + gen.writeFieldName("name"); + gen.writeString(value.getName()); + gen.writeEndObject(); + } + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public ChatCompletionRequest.ChatCompletionRequestFunctionCall deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.getCurrentToken().isStructStart()) { + p.nextToken(); //key + p.nextToken(); //value + } + return new ChatCompletionRequest.ChatCompletionRequestFunctionCall(p.getValueAsString()); + } + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java new file mode 100644 index 00000000..9b7be0f9 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallArgumentsSerializerAndDeserializer.java @@ -0,0 +1,64 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.TextNode; + +import java.io.IOException; + +public class ChatFunctionCallArgumentsSerializerAndDeserializer { + + private final static ObjectMapper MAPPER = new ObjectMapper(); + + private ChatFunctionCallArgumentsSerializerAndDeserializer() { + } + + public static class Serializer extends JsonSerializer { + + private Serializer() { + } + + @Override + public void serialize(JsonNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeString(value instanceof TextNode ? value.asText() : value.toPrettyString()); + } + } + } + + public static class Deserializer extends JsonDeserializer { + + private Deserializer() { + } + + @Override + public JsonNode deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String json = p.getValueAsString(); + if (json == null || p.currentToken() == JsonToken.VALUE_NULL) { + return null; + } + + try { + JsonNode node = null; + try { + node = MAPPER.readTree(json); + } catch (JsonParseException ignored) { + } + if (node == null || node.getNodeType() == JsonNodeType.MISSING) { + node = MAPPER.readTree(p); + } + return node; + } catch (Exception ex) { + ex.printStackTrace(); + return null; + } + } + } + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java new file mode 100644 index 00000000..7b32e051 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionCallMixIn.java @@ -0,0 +1,13 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +public abstract class ChatFunctionCallMixIn { + + @JsonSerialize(using = ChatFunctionCallArgumentsSerializerAndDeserializer.Serializer.class) + @JsonDeserialize(using = ChatFunctionCallArgumentsSerializerAndDeserializer.Deserializer.class) + abstract JsonNode getArguments(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java new file mode 100644 index 00000000..d94a9179 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionMixIn.java @@ -0,0 +1,10 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +public abstract class ChatFunctionMixIn { + + @JsonSerialize(using = ChatFunctionParametersSerializer.class) + abstract Class getParametersClass(); + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java b/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java new file mode 100644 index 00000000..c0273916 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatFunctionParametersSerializer.java @@ -0,0 +1,37 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.kjetland.jackson.jsonSchema.JsonSchemaConfig; +import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator; + +import java.io.IOException; + +public class ChatFunctionParametersSerializer extends JsonSerializer> { + + private final ObjectMapper mapper = new ObjectMapper(); + private final JsonSchemaConfig config = JsonSchemaConfig.vanillaJsonSchemaDraft4(); + private final JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(mapper, config); + + @Override + public void serialize(Class value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + try { + JsonNode schema = jsonSchemaGenerator.generateJsonSchema(value); + gen.writeObject(schema); + } catch (Exception e) { + throw new RuntimeException("Failed to generate JSON Schema", e); + } + } + } +} + + + + + diff --git a/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java b/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java new file mode 100644 index 00000000..a55173cc --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ChatMessageAccumulator.java @@ -0,0 +1,84 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.completion.chat.ChatMessage; + +/** + * Class that accumulates chat messages and provides utility methods for + * handling message chunks and function calls within a chat stream. This + * class is immutable. + * + * @author [Your Name] + */ +public class ChatMessageAccumulator { + + private final ChatMessage messageChunk; + private final ChatMessage accumulatedMessage; + + /** + * Constructor that initializes the message chunk and accumulated message. + * + * @param messageChunk The message chunk. + * @param accumulatedMessage The accumulated message. + */ + public ChatMessageAccumulator(ChatMessage messageChunk, ChatMessage accumulatedMessage) { + this.messageChunk = messageChunk; + this.accumulatedMessage = accumulatedMessage; + } + + /** + * Checks if the accumulated message contains a function call. + * + * @return true if the accumulated message contains a function call, false otherwise. + */ + public boolean isFunctionCall() { + return getAccumulatedMessage().getFunctionCall() != null && getAccumulatedMessage().getFunctionCall().getName() != null; + } + + /** + * Checks if the accumulated message contains a chat message. + * + * @return true if the accumulated message contains a chat message, false otherwise. + */ + public boolean isChatMessage() { + return !isFunctionCall(); + } + + /** + * Retrieves the message chunk. + * + * @return the message chunk. + */ + public ChatMessage getMessageChunk() { + return messageChunk; + } + + /** + * Retrieves the accumulated message. + * + * @return the accumulated message. + */ + public ChatMessage getAccumulatedMessage() { + return accumulatedMessage; + } + + /** + * Retrieves the function call from the message chunk. + * This is equivalent to getMessageChunk().getFunctionCall(). + * + * @return the function call from the message chunk. + */ + public ChatFunctionCall getChatFunctionCallChunk() { + return getMessageChunk().getFunctionCall(); + } + + /** + * Retrieves the function call from the accumulated message. + * This is equivalent to getAccumulatedMessage().getFunctionCall(). + * + * @return the function call from the accumulated message. + */ + public ChatFunctionCall getAccumulatedChatFunctionCall() { + return getAccumulatedMessage().getFunctionCall(); + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java b/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java new file mode 100644 index 00000000..5d143a95 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/FunctionExecutor.java @@ -0,0 +1,105 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.theokanning.openai.completion.chat.ChatFunction; +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.completion.chat.ChatMessageRole; + +import java.util.*; + +public class FunctionExecutor { + + private ObjectMapper MAPPER = new ObjectMapper(); + private final Map FUNCTIONS = new HashMap<>(); + + public FunctionExecutor(List functions) { + setFunctions(functions); + } + + public FunctionExecutor(List functions, ObjectMapper objectMapper) { + setFunctions(functions); + setObjectMapper(objectMapper); + } + + public Optional executeAndConvertToMessageSafely(ChatFunctionCall call) { + try { + return Optional.ofNullable(executeAndConvertToMessage(call)); + } catch (Exception ignored) { + return Optional.empty(); + } + } + + public ChatMessage executeAndConvertToMessageHandlingExceptions(ChatFunctionCall call) { + try { + return executeAndConvertToMessage(call); + } catch (Exception exception) { + exception.printStackTrace(); + return convertExceptionToMessage(exception); + } + } + + public ChatMessage convertExceptionToMessage(Exception exception) { + String error = exception.getMessage() == null ? exception.toString() : exception.getMessage(); + return new ChatMessage(ChatMessageRole.FUNCTION.value(), "{\"error\": \"" + error + "\"}", "error"); + } + + public ChatMessage executeAndConvertToMessage(ChatFunctionCall call) { + return new ChatMessage(ChatMessageRole.FUNCTION.value(), executeAndConvertToJson(call).toPrettyString(), call.getName()); + } + + public JsonNode executeAndConvertToJson(ChatFunctionCall call) { + try { + Object execution = execute(call); + if (execution instanceof TextNode) { + JsonNode objectNode = MAPPER.readTree(((TextNode) execution).asText()); + if (objectNode.isMissingNode()) + return (JsonNode) execution; + return objectNode; + } + if (execution instanceof ObjectNode) { + return (JsonNode) execution; + } + if (execution instanceof String) { + JsonNode objectNode = MAPPER.readTree((String) execution); + if (objectNode.isMissingNode()) + throw new RuntimeException("Parsing exception"); + return objectNode; + } + return MAPPER.readValue(MAPPER.writeValueAsString(execution), JsonNode.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public T execute(ChatFunctionCall call) { + ChatFunction function = FUNCTIONS.get(call.getName()); + Object obj; + try { + JsonNode arguments = call.getArguments(); + obj = MAPPER.readValue(arguments instanceof TextNode ? arguments.asText() : arguments.toPrettyString(), function.getParametersClass()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return (T) function.getExecutor().apply(obj); + } + + public List getFunctions() { + return new ArrayList<>(FUNCTIONS.values()); + } + + public void setFunctions(List functions) { + this.FUNCTIONS.clear(); + functions.forEach(f -> this.FUNCTIONS.put(f.getName(), f)); + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.MAPPER = objectMapper; + } + +} diff --git a/service/src/main/java/com/theokanning/openai/service/OpenAiService.java b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java new file mode 100644 index 00000000..52ab6b0f --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/OpenAiService.java @@ -0,0 +1,662 @@ +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; +import com.fasterxml.jackson.databind.node.TextNode; +import com.theokanning.openai.*; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.audio.*; +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; +import com.theokanning.openai.completion.CompletionResult; +import com.theokanning.openai.completion.chat.*; +import com.theokanning.openai.edit.EditRequest; +import com.theokanning.openai.edit.EditResult; +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; +import com.theokanning.openai.image.CreateImageEditRequest; +import com.theokanning.openai.image.CreateImageRequest; +import com.theokanning.openai.image.CreateImageVariationRequest; +import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; +import com.theokanning.openai.model.Model; +import com.theokanning.openai.moderation.ModerationRequest; +import com.theokanning.openai.moderation.ModerationResult; +import com.theokanning.openai.runs.CreateThreadAndRunRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.Single; +import okhttp3.*; +import retrofit2.Call; +import retrofit2.HttpException; +import retrofit2.Retrofit; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +public class OpenAiService { + + private static final String BASE_URL = "https://api.openai.com/"; + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10); + private static final ObjectMapper mapper = defaultObjectMapper(); + + private final OpenAiApi api; + private final ExecutorService executorService; + + /** + * Creates a new OpenAiService that wraps OpenAiApi + * + * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + */ + public OpenAiService(final String token) { + this(token, DEFAULT_TIMEOUT); + } + + /** + * Creates a new OpenAiService that wraps OpenAiApi + * + * @param token OpenAi token string "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + * @param timeout http read timeout, Duration.ZERO means no timeout + */ + public OpenAiService(final String token, final Duration timeout) { + ObjectMapper mapper = defaultObjectMapper(); + OkHttpClient client = defaultClient(token, timeout); + Retrofit retrofit = defaultRetrofit(client, mapper); + + this.api = retrofit.create(OpenAiApi.class); + this.executorService = client.dispatcher().executorService(); + } + + /** + * Creates a new OpenAiService that wraps OpenAiApi. + * Use this if you need more customization, but use OpenAiService(api, executorService) if you use streaming and + * want to shut down instantly + * + * @param api OpenAiApi instance to use for all methods + */ + public OpenAiService(final OpenAiApi api) { + this.api = api; + this.executorService = null; + } + + /** + * Creates a new OpenAiService that wraps OpenAiApi. + * The ExecutorService must be the one you get from the client you created the api with + * otherwise shutdownExecutor() won't work. + *

+ * Use this if you need more customization. + * + * @param api OpenAiApi instance to use for all methods + * @param executorService the ExecutorService from client.dispatcher().executorService() + */ + public OpenAiService(final OpenAiApi api, final ExecutorService executorService) { + this.api = api; + this.executorService = executorService; + } + + public List listModels() { + return execute(api.listModels()).data; + } + + public Model getModel(String modelId) { + return execute(api.getModel(modelId)); + } + + public CompletionResult createCompletion(CompletionRequest request) { + return execute(api.createCompletion(request)); + } + + public Flowable streamCompletion(CompletionRequest request) { + request.setStream(true); + + return stream(api.createCompletionStream(request), CompletionChunk.class); + } + + public ChatCompletionResult createChatCompletion(ChatCompletionRequest request) { + return execute(api.createChatCompletion(request)); + } + + public Flowable streamChatCompletion(ChatCompletionRequest request) { + request.setStream(true); + + return stream(api.createChatCompletionStream(request), ChatCompletionChunk.class); + } + + public EditResult createEdit(EditRequest request) { + return execute(api.createEdit(request)); + } + + public EmbeddingResult createEmbeddings(EmbeddingRequest request) { + return execute(api.createEmbeddings(request)); + } + + public List listFiles() { + return execute(api.listFiles()).data; + } + + public File uploadFile(String purpose, String filepath) { + java.io.File file = new java.io.File(filepath); + RequestBody purposeBody = RequestBody.create(MultipartBody.FORM, purpose); + RequestBody fileBody = RequestBody.create(MediaType.parse("text"), file); + MultipartBody.Part body = MultipartBody.Part.createFormData("file", filepath, fileBody); + + return execute(api.uploadFile(purposeBody, body)); + } + + public DeleteResult deleteFile(String fileId) { + return execute(api.deleteFile(fileId)); + } + + 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)); + } + + 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)); + } + + 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; + } + + public DeleteResult deleteFineTune(String fineTuneId) { + return execute(api.deleteFineTune(fineTuneId)); + } + + public ImageResult createImage(CreateImageRequest request) { + return execute(api.createImage(request)); + } + + public ImageResult createImageEdit(CreateImageEditRequest request, String imagePath, String maskPath) { + java.io.File image = new java.io.File(imagePath); + java.io.File mask = null; + if (maskPath != null) { + mask = new java.io.File(maskPath); + } + return createImageEdit(request, image, mask); + } + + public ImageResult createImageEdit(CreateImageEditRequest request, java.io.File image, java.io.File mask) { + RequestBody imageBody = RequestBody.create(MediaType.parse("image"), image); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("prompt", request.getPrompt()) + .addFormDataPart("size", request.getSize()) + .addFormDataPart("response_format", request.getResponseFormat()) + .addFormDataPart("image", "image", imageBody); + + if (request.getN() != null) { + builder.addFormDataPart("n", request.getN().toString()); + } + + if (mask != null) { + RequestBody maskBody = RequestBody.create(MediaType.parse("image"), mask); + builder.addFormDataPart("mask", "mask", maskBody); + } + + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + + return execute(api.createImageEdit(builder.build())); + } + + public ImageResult createImageVariation(CreateImageVariationRequest request, String imagePath) { + java.io.File image = new java.io.File(imagePath); + return createImageVariation(request, image); + } + + public ImageResult createImageVariation(CreateImageVariationRequest request, java.io.File image) { + RequestBody imageBody = RequestBody.create(MediaType.parse("image"), image); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("size", request.getSize()) + .addFormDataPart("response_format", request.getResponseFormat()) + .addFormDataPart("image", "image", imageBody); + + if (request.getN() != null) { + builder.addFormDataPart("n", request.getN().toString()); + } + + if (request.getModel() != null) { + builder.addFormDataPart("model", request.getModel()); + } + + return execute(api.createImageVariation(builder.build())); + } + + public TranscriptionResult createTranscription(CreateTranscriptionRequest request, String audioPath) { + java.io.File audio = new java.io.File(audioPath); + return createTranscription(request, audio); + } + + public TranscriptionResult createTranscription(CreateTranscriptionRequest request, java.io.File audio) { + RequestBody audioBody = RequestBody.create(MediaType.parse("audio"), audio); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("model", request.getModel()) + .addFormDataPart("file", audio.getName(), audioBody); + + if (request.getPrompt() != null) { + builder.addFormDataPart("prompt", request.getPrompt()); + } + if (request.getResponseFormat() != null) { + builder.addFormDataPart("response_format", request.getResponseFormat()); + } + if (request.getTemperature() != null) { + builder.addFormDataPart("temperature", request.getTemperature().toString()); + } + if (request.getLanguage() != null) { + builder.addFormDataPart("language", request.getLanguage()); + } + + return execute(api.createTranscription(builder.build())); + } + + public TranslationResult createTranslation(CreateTranslationRequest request, String audioPath) { + java.io.File audio = new java.io.File(audioPath); + return createTranslation(request, audio); + } + + public TranslationResult createTranslation(CreateTranslationRequest request, java.io.File audio) { + RequestBody audioBody = RequestBody.create(MediaType.parse("audio"), audio); + + MultipartBody.Builder builder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart("model", request.getModel()) + .addFormDataPart("file", audio.getName(), audioBody); + + if (request.getPrompt() != null) { + builder.addFormDataPart("prompt", request.getPrompt()); + } + if (request.getResponseFormat() != null) { + builder.addFormDataPart("response_format", request.getResponseFormat()); + } + if (request.getTemperature() != null) { + builder.addFormDataPart("temperature", request.getTemperature().toString()); + } + + return execute(api.createTranslation(builder.build())); + } + + public ModerationResult createModeration(ModerationRequest request) { + return execute(api.createModeration(request)); + } + + public ResponseBody createSpeech(CreateSpeechRequest request) { + return execute(api.createSpeech(request)); + } + + public Assistant createAssistant(AssistantRequest request) { + return execute(api.createAssistant(request)); + } + + public Assistant retrieveAssistant(String assistantId) { + return execute(api.retrieveAssistant(assistantId)); + } + + public Assistant modifyAssistant(String assistantId, ModifyAssistantRequest request) { + return execute(api.modifyAssistant(assistantId, request)); + } + + public DeleteResult deleteAssistant(String assistantId) { + return execute(api.deleteAssistant(assistantId)); + } + + public OpenAiResponse listAssistants(ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, 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 OpenAiResponse listAssistantFiles(String assistantId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listAssistantFiles(assistantId, queryParameters)); + } + + public Thread createThread(ThreadRequest request) { + return execute(api.createThread(request)); + } + + public Thread retrieveThread(String threadId) { + return execute(api.retrieveThread(threadId)); + } + + public Thread modifyThread(String threadId, ThreadRequest request) { + return execute(api.modifyThread(threadId, request)); + } + + public DeleteResult deleteThread(String threadId) { + return execute(api.deleteThread(threadId)); + } + + public Message createMessage(String threadId, MessageRequest request) { + return execute(api.createMessage(threadId, request)); + } + + public Message retrieveMessage(String threadId, String messageId) { + return execute(api.retrieveMessage(threadId, messageId)); + } + + public Message modifyMessage(String threadId, String messageId, ModifyMessageRequest request) { + return execute(api.modifyMessage(threadId, messageId, request)); + } + + public OpenAiResponse listMessages(String threadId) { + return execute(api.listMessages(threadId)); + } + + public OpenAiResponse listMessages(String threadId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listMessages(threadId, queryParameters)); + } + + public MessageFile retrieveMessageFile(String threadId, String messageId, String fileId) { + return execute(api.retrieveMessageFile(threadId, messageId, fileId)); + } + + public OpenAiResponse listMessageFiles(String threadId, String messageId) { + return execute(api.listMessageFiles(threadId, messageId)); + } + + public OpenAiResponse listMessageFiles(String threadId, String messageId, ListSearchParameters params) { + Map queryParameters = mapper.convertValue(params, new TypeReference>() { + }); + return execute(api.listMessageFiles(threadId, messageId, queryParameters)); + } + + public Run createRun(String threadId, RunCreateRequest runCreateRequest) { + return execute(api.createRun(threadId, runCreateRequest)); + } + + public Run retrieveRun(String threadId, String runId) { + return execute(api.retrieveRun(threadId, runId)); + } + + public Run modifyRun(String threadId, String runId, Map metadata) { + return execute(api.modifyRun(threadId, runId, metadata)); + } + + public OpenAiResponse listRuns(String threadId, ListSearchParameters listSearchParameters) { + Map search = new HashMap<>(); + if (listSearchParameters != null) { + ObjectMapper mapper = defaultObjectMapper(); + search = mapper.convertValue(listSearchParameters, Map.class); + } + return execute(api.listRuns(threadId, search)); + } + + public Run submitToolOutputs(String threadId, String runId, SubmitToolOutputsRequest submitToolOutputsRequest) { + return execute(api.submitToolOutputs(threadId, runId, submitToolOutputsRequest)); + } + + public Run cancelRun(String threadId, String runId) { + return execute(api.cancelRun(threadId, runId)); + } + + public Run createThreadAndRun(CreateThreadAndRunRequest createThreadAndRunRequest) { + return execute(api.createThreadAndRun(createThreadAndRunRequest)); + } + + public RunStep retrieveRunStep(String threadId, String runId, String stepId) { + return execute(api.retrieveRunStep(threadId, runId, stepId)); + } + + public OpenAiResponse listRunSteps(String threadId, String runId, ListSearchParameters listSearchParameters) { + Map search = new HashMap<>(); + if (listSearchParameters != null) { + ObjectMapper mapper = defaultObjectMapper(); + search = mapper.convertValue(listSearchParameters, Map.class); + } + return execute(api.listRunSteps(threadId, runId, search)); + } + + /** + * Calls the Open AI api, returns the response, and parses error messages if the request fails + */ + public static T execute(Single apiCall) { + try { + return apiCall.blockingGet(); + } catch (HttpException e) { + try { + if (e.response() == null || e.response().errorBody() == null) { + throw e; + } + String errorBody = e.response().errorBody().string(); + + OpenAiError error = mapper.readValue(errorBody, OpenAiError.class); + throw new OpenAiHttpException(error, e, e.code()); + } catch (IOException ex) { + // couldn't parse OpenAI error + throw e; + } + } + } + + /** + * Calls the Open AI api and returns a Flowable of SSE for streaming + * omitting the last message. + * + * @param apiCall The api call + */ + public static Flowable stream(Call apiCall) { + return stream(apiCall, false); + } + + /** + * Calls the Open AI api and returns a Flowable of SSE for streaming. + * + * @param apiCall The api call + * @param emitDone If true the last message ([DONE]) is emitted + */ + public static Flowable stream(Call apiCall, boolean emitDone) { + return Flowable.create(emitter -> apiCall.enqueue(new ResponseBodyCallback(emitter, emitDone)), BackpressureStrategy.BUFFER); + } + + /** + * Calls the Open AI api and returns a Flowable of type T for streaming + * omitting the last message. + * + * @param apiCall The api call + * @param cl Class of type T to return + */ + public static Flowable stream(Call apiCall, Class cl) { + return stream(apiCall).map(sse -> mapper.readValue(sse.getData(), cl)); + } + + /** + * Shuts down the OkHttp ExecutorService. + * The default behaviour of OkHttp's ExecutorService (ConnectionPool) + * is to shut down after an idle timeout of 60s. + * Call this method to shut down the ExecutorService immediately. + */ + public void shutdownExecutor() { + Objects.requireNonNull(this.executorService, "executorService must be set in order to shut down"); + this.executorService.shutdown(); + } + + public static OpenAiApi buildApi(String token, Duration timeout) { + ObjectMapper mapper = defaultObjectMapper(); + OkHttpClient client = defaultClient(token, timeout); + Retrofit retrofit = defaultRetrofit(client, mapper); + + return retrofit.create(OpenAiApi.class); + } + + public static ObjectMapper defaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); + return mapper; + } + + public static OkHttpClient defaultClient(String token, Duration timeout) { + return new OkHttpClient.Builder() + .addInterceptor(new AuthenticationInterceptor(token)) + .connectionPool(new ConnectionPool(5, 1, TimeUnit.SECONDS)) + .readTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS) + .build(); + } + + public static Retrofit defaultRetrofit(OkHttpClient client, ObjectMapper mapper) { + return new Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(JacksonConverterFactory.create(mapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build(); + } + + public Flowable mapStreamToAccumulator(Flowable flowable) { + ChatFunctionCall functionCall = new ChatFunctionCall(null, null); + ChatMessage accumulatedMessage = new ChatMessage(ChatMessageRole.ASSISTANT.value(), null); + + return flowable.map(chunk -> { + ChatMessage messageChunk = chunk.getChoices().get(0).getMessage(); + if (messageChunk.getFunctionCall() != null) { + if (messageChunk.getFunctionCall().getName() != null) { + String namePart = messageChunk.getFunctionCall().getName(); + functionCall.setName((functionCall.getName() == null ? "" : functionCall.getName()) + namePart); + } + if (messageChunk.getFunctionCall().getArguments() != null) { + String argumentsPart = messageChunk.getFunctionCall().getArguments() == null ? "" : messageChunk.getFunctionCall().getArguments().asText(); + functionCall.setArguments(new TextNode((functionCall.getArguments() == null ? "" : functionCall.getArguments().asText()) + argumentsPart)); + } + accumulatedMessage.setFunctionCall(functionCall); + } else { + accumulatedMessage.setContent((accumulatedMessage.getContent() == null ? "" : accumulatedMessage.getContent()) + (messageChunk.getContent() == null ? "" : messageChunk.getContent())); + } + + if (chunk.getChoices().get(0).getFinishReason() != null) { // last + if (functionCall.getArguments() != null) { + functionCall.setArguments(mapper.readTree(functionCall.getArguments().asText())); + accumulatedMessage.setFunctionCall(functionCall); + } + } + + return new ChatMessageAccumulator(messageChunk, accumulatedMessage); + }); + } + + /** + * Account information inquiry: including total amount and other information. + * + * @return Account information. + */ + public Subscription subscription() { + Single 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(); + } + +} diff --git a/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java b/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java new file mode 100644 index 00000000..c5404e0f --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/ResponseBodyCallback.java @@ -0,0 +1,100 @@ +package com.theokanning.openai.service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.theokanning.openai.OpenAiError; +import com.theokanning.openai.OpenAiHttpException; + +import io.reactivex.FlowableEmitter; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.HttpException; +import retrofit2.Response; + +/** + * Callback to parse Server Sent Events (SSE) from raw InputStream and + * emit the events with io.reactivex.FlowableEmitter to allow streaming of + * SSE. + */ +public class ResponseBodyCallback implements Callback { + private static final ObjectMapper mapper = OpenAiService.defaultObjectMapper(); + + private FlowableEmitter emitter; + private boolean emitDone; + + public ResponseBodyCallback(FlowableEmitter emitter, boolean emitDone) { + this.emitter = emitter; + this.emitDone = emitDone; + } + + @Override + public void onResponse(Call call, Response response) { + BufferedReader reader = null; + + try { + if (!response.isSuccessful()) { + HttpException e = new HttpException(response); + ResponseBody errorBody = response.errorBody(); + + if (errorBody == null) { + throw e; + } else { + OpenAiError error = mapper.readValue( + errorBody.string(), + OpenAiError.class + ); + throw new OpenAiHttpException(error, e, e.code()); + } + } + + InputStream in = response.body().byteStream(); + reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + String line; + SSE sse = null; + + while (!emitter.isCancelled() && (line = reader.readLine()) != null) { + if (line.startsWith("data:")) { + String data = line.substring(5).trim(); + sse = new SSE(data); + } else if (line.equals("") && sse != null) { + if (sse.isDone()) { + if (emitDone) { + emitter.onNext(sse); + } + break; + } + + emitter.onNext(sse); + sse = null; + } else { + throw new SSEFormatException("Invalid sse format! " + line); + } + } + + emitter.onComplete(); + + } catch (Throwable t) { + onFailure(call, t); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // do nothing + } + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + emitter.onError(t); + } +} diff --git a/service/src/main/java/com/theokanning/openai/service/SSE.java b/service/src/main/java/com/theokanning/openai/service/SSE.java new file mode 100644 index 00000000..c3024fac --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/SSE.java @@ -0,0 +1,26 @@ +package com.theokanning.openai.service; + +/** + * Simple Server Sent Event representation + */ +public class SSE { + private static final String DONE_DATA = "[DONE]"; + + private final String data; + + public SSE(String data){ + this.data = data; + } + + public String getData(){ + return this.data; + } + + public byte[] toBytes(){ + return String.format("data: %s\n\n", this.data).getBytes(); + } + + public boolean isDone(){ + return DONE_DATA.equalsIgnoreCase(this.data); + } +} \ No newline at end of file diff --git a/service/src/main/java/com/theokanning/openai/service/SSEFormatException.java b/service/src/main/java/com/theokanning/openai/service/SSEFormatException.java new file mode 100644 index 00000000..f99aeaf3 --- /dev/null +++ b/service/src/main/java/com/theokanning/openai/service/SSEFormatException.java @@ -0,0 +1,10 @@ +package com.theokanning.openai.service; + +/** + * Exception indicating a SSE format error + */ +public class SSEFormatException extends Throwable{ + public SSEFormatException(String msg){ + super(msg); + } +} \ No newline at end of file diff --git a/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java b/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java new file mode 100644 index 00000000..9ad819a7 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantFunctionTest.java @@ -0,0 +1,148 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +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; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantFunction; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.assistants.AssistantToolsEnum; +import com.theokanning.openai.assistants.Tool; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatFunction; +import com.theokanning.openai.completion.chat.ChatFunctionCall; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.runs.RequiredAction; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.runs.RunStep; +import com.theokanning.openai.runs.SubmitToolOutputRequestItem; +import com.theokanning.openai.runs.SubmitToolOutputs; +import com.theokanning.openai.runs.SubmitToolOutputsRequest; +import com.theokanning.openai.runs.ToolCall; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AssistantFunctionTest { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token, Duration.ofMinutes(1)); + + @Test + void createRetrieveRun() throws JsonProcessingException { + + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class); + mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class); + mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class); + + String funcDef = "{\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"location\": {\n" + + " \"type\": \"string\",\n" + + " \"description\": \"The city and state, e.g. San Francisco, CA\"\n" + + " },\n" + + " \"unit\": {\n" + + " \"type\": \"string\",\n" + + " \"enum\": [\"celsius\", \"fahrenheit\"]\n" + + " }\n" + + " },\n" + + " \"required\": [\"location\"]\n" + + "}"; + Map funcParameters = mapper.readValue(funcDef, new TypeReference>() {}); + AssistantFunction function = AssistantFunction.builder() + .name("weather_reporter") + .description("Get the current weather of a location") + .parameters(funcParameters) + .build(); + + List toolList = new ArrayList<>(); + Tool funcTool = new Tool(AssistantToolsEnum.FUNCTION, function); + toolList.add(funcTool); + + + AssistantRequest assistantRequest = AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name("MATH_TUTOR") + .instructions("You are a personal Math Tutor.") + .tools(toolList) + .build(); + Assistant assistant = service.createAssistant(assistantRequest); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + Thread thread = service.createThread(threadRequest); + + MessageRequest messageRequest = MessageRequest.builder() + .content("What's the weather of Xiamen?") + .build(); + + Message message = service.createMessage(thread.getId(), messageRequest); + + RunCreateRequest runCreateRequest = RunCreateRequest.builder() + .assistantId(assistant.getId()) + .build(); + + Run run = service.createRun(thread.getId(), runCreateRequest); + assertNotNull(run); + + Run retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + while (!(retrievedRun.getStatus().equals("completed")) + && !(retrievedRun.getStatus().equals("failed")) + && !(retrievedRun.getStatus().equals("requires_action"))){ + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + } + if (retrievedRun.getStatus().equals("requires_action")) { + RequiredAction requiredAction = retrievedRun.getRequiredAction(); + System.out.println("requiredAction"); + System.out.println(mapper.writeValueAsString(requiredAction)); + List toolCalls = requiredAction.getSubmitToolOutputs().getToolCalls(); + ToolCall toolCall = toolCalls.get(0); + String toolCallId = toolCall.getId(); + + SubmitToolOutputRequestItem toolOutputRequestItem = SubmitToolOutputRequestItem.builder() + .toolCallId(toolCallId) + .output("sunny") + .build(); + List toolOutputRequestItems = new ArrayList<>(); + toolOutputRequestItems.add(toolOutputRequestItem); + SubmitToolOutputsRequest submitToolOutputsRequest = SubmitToolOutputsRequest.builder() + .toolOutputs(toolOutputRequestItems) + .build(); + retrievedRun = service.submitToolOutputs(retrievedRun.getThreadId(), retrievedRun.getId(), submitToolOutputsRequest); + + while (!(retrievedRun.getStatus().equals("completed")) + && !(retrievedRun.getStatus().equals("failed")) + && !(retrievedRun.getStatus().equals("requires_action"))){ + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + } + + OpenAiResponse response = service.listMessages(thread.getId()); + + List messages = response.getData(); + + System.out.println(mapper.writeValueAsString(messages)); + + } + } +} 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..8b687e34 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AssistantTest.java @@ -0,0 +1,128 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.*; +import com.theokanning.openai.file.File; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.*; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AssistantTest { + + static OpenAiService service = new OpenAiService(System.getenv("OPENAI_TOKEN")); + static String assistantId; + static String fileId; + + + @AfterAll + static void teardown() { + try { + service.deleteAssistantFile(assistantId, fileId); + } catch (Exception e) { + // do nothing + } + try { + service.deleteAssistant(assistantId); + } catch (Exception e) { + // do nothing + } + } + + @Test + @Order(1) + void createAssistant() { + AssistantRequest assistantRequest = AssistantRequest.builder().model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()).name("Math Tutor").instructions("You are a personal Math Tutor.").tools(Collections.singletonList(new Tool(AssistantToolsEnum.CODE_INTERPRETER, null))).build(); + Assistant assistant = service.createAssistant(assistantRequest); + + assistantId = assistant.getId(); + + assertEquals(assistant.getName(), "Math Tutor"); + assertEquals(assistant.getTools().get(0).getType(), AssistantToolsEnum.CODE_INTERPRETER); + } + + @Test + @Order(2) + void retrieveAssistant() { + Assistant assistant = service.retrieveAssistant(assistantId); + + assertEquals(assistant.getName(), "Math Tutor"); + } + + @Test + @Order(3) + void modifyAssistant() { + String modifiedName = "Science Tutor"; + ModifyAssistantRequest modifyRequest = ModifyAssistantRequest.builder().name(modifiedName).build(); + + Assistant modifiedAssistant = service.modifyAssistant(assistantId, modifyRequest); + assertEquals(modifiedName, modifiedAssistant.getName()); + } + + @Test + @Order(4) + void listAssistants() { + OpenAiResponse assistants = service.listAssistants(ListSearchParameters.builder().build()); + + assertNotNull(assistants); + assertFalse(assistants.getData().isEmpty()); + } + + @Test + @Order(5) + void createAssistantFile() { + String filePath = "src/test/resources/assistants-data.html"; + File uploadedFile = service.uploadFile("assistants", filePath); + + AssistantFile assistantFile = service.createAssistantFile(assistantId, new AssistantFileRequest(uploadedFile.getId())); + + fileId = assistantFile.getId(); + assertNotNull(assistantFile); + assertEquals(uploadedFile.getId(), assistantFile.getId()); + assertEquals(assistantId, assistantFile.getAssistantId()); + } + + @Test + @Order(6) + void retrieveAssistantFile() { + AssistantFile file = service.retrieveAssistantFile(assistantId, fileId); + + assertEquals(file.getId(), fileId); + } + + + @Test + @Order(7) + void listAssistantFiles() { + List files = service.listAssistantFiles(assistantId, new ListSearchParameters()).data; + + assertFalse(files.isEmpty()); + assertEquals(files.get(0).getId(), fileId); + assertEquals(files.get(0).getObject(), "assistant.file"); + } + + @Test + @Order(8) + void deleteAssistantFile() { + DeleteResult deletedFile = service.deleteAssistantFile(assistantId, fileId); + + assertEquals(deletedFile.getId(), fileId); + assertTrue(deletedFile.isDeleted()); + } + + @Test + @Order(9) + void deleteAssistant() { + DeleteResult deletedAssistant = service.deleteAssistant(assistantId); + + assertEquals(assistantId, deletedAssistant.getId()); + assertTrue(deletedAssistant.isDeleted()); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/AudioTest.java b/service/src/test/java/com/theokanning/openai/service/AudioTest.java new file mode 100644 index 00000000..9cb083de --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/AudioTest.java @@ -0,0 +1,91 @@ +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.*; + + +public class AudioTest { + + static String englishAudioFilePath = "src/test/resources/hello-world.mp3"; + + static String koreanAudioFilePath = "src/test/resources/korean-hello.mp3"; + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token, Duration.ofSeconds(30)); + + @Test + void createTranscription() { + CreateTranscriptionRequest createTranscriptionRequest = CreateTranscriptionRequest.builder() + .model("whisper-1") + .build(); + + String text = service.createTranscription(createTranscriptionRequest, englishAudioFilePath).getText(); + assertEquals("Hello World.", text); + } + + @Test + void createTranscriptionVerbose() { + CreateTranscriptionRequest createTranscriptionRequest = CreateTranscriptionRequest.builder() + .model("whisper-1") + .responseFormat("verbose_json") + .build(); + + TranscriptionResult result = service.createTranscription(createTranscriptionRequest, englishAudioFilePath); + assertEquals("Hello World.", result.getText()); + assertEquals("transcribe", result.getTask()); + assertEquals("english", result.getLanguage()); + assertTrue(result.getDuration() > 0); + assertEquals(1, result.getSegments().size()); + } + + @Test + void createTranslation() { + CreateTranslationRequest createTranslationRequest = CreateTranslationRequest.builder() + .model("whisper-1") + .build(); + + String text = service.createTranslation(createTranslationRequest, koreanAudioFilePath).getText(); + assertEquals("Hello, my name is Yoona. I am a Korean native speaker.", text); + } + + @Test + void createTranslationVerbose() { + CreateTranslationRequest createTranslationRequest = CreateTranslationRequest.builder() + .model("whisper-1") + .responseFormat("verbose_json") + .build(); + + TranslationResult result = service.createTranslation(createTranslationRequest, koreanAudioFilePath); + 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); + 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); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java new file mode 100644 index 00000000..25f0defb --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ChatCompletionTest.java @@ -0,0 +1,303 @@ +package com.theokanning.openai.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.theokanning.openai.completion.chat.*; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +class ChatCompletionTest { + + static class Weather { + @JsonPropertyDescription("City and state, for example: León, Guanajuato") + public String location; + + @JsonPropertyDescription("The temperature unit, can be 'celsius' or 'fahrenheit'") + @JsonProperty(required = true) + public WeatherUnit unit; + } + + enum WeatherUnit { + CELSIUS, FAHRENHEIT; + } + + static class WeatherResponse { + public String location; + public WeatherUnit unit; + public int temperature; + public String description; + + public WeatherResponse(String location, WeatherUnit unit, int temperature, String description) { + this.location = location; + this.unit = unit; + this.temperature = temperature; + this.description = description; + } + } + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + @Test + void createChatCompletion() { + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a dog and will speak as such."); + messages.add(systemMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo") + .messages(messages) + .n(5) + .maxTokens(50) + .logitBias(new HashMap<>()) + .build(); + + List choices = service.createChatCompletion(chatCompletionRequest).getChoices(); + assertEquals(5, choices.size()); + } + + @Test + void streamChatCompletion() { + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a dog and will speak as such."); + messages.add(systemMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo") + .messages(messages) + .n(1) + .maxTokens(50) + .logitBias(new HashMap<>()) + .stream(true) + .build(); + + List chunks = new ArrayList<>(); + service.streamChatCompletion(chatCompletionRequest).blockingForEach(chunks::add); + assertTrue(chunks.size() > 0); + assertNotNull(chunks.get(0).getChoices().get(0)); + } + + @Test + void createChatCompletionWithFunctions() { + final List functions = Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather in a given location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, 25, "sunny")) + .build()); + final FunctionExecutor functionExecutor = new FunctionExecutor(functions); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice = service.createChatCompletion(chatCompletionRequest).getChoices().get(0); + assertEquals("function_call", choice.getFinishReason()); + assertNotNull(choice.getMessage().getFunctionCall()); + assertEquals("get_weather", choice.getMessage().getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, choice.getMessage().getFunctionCall().getArguments()); + + ChatMessage callResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(choice.getMessage().getFunctionCall()); + assertNotEquals("error", callResponse.getName()); + + // this performs an unchecked cast + WeatherResponse functionExecutionResponse = functionExecutor.execute(choice.getMessage().getFunctionCall()); + assertInstanceOf(WeatherResponse.class, functionExecutionResponse); + assertEquals(25, functionExecutionResponse.temperature); + + JsonNode jsonFunctionExecutionResponse = functionExecutor.executeAndConvertToJson(choice.getMessage().getFunctionCall()); + assertInstanceOf(ObjectNode.class, jsonFunctionExecutionResponse); + assertEquals("25", jsonFunctionExecutionResponse.get("temperature").asText()); + + messages.add(choice.getMessage()); + messages.add(callResponse); + + ChatCompletionRequest chatCompletionRequest2 = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice2 = service.createChatCompletion(chatCompletionRequest2).getChoices().get(0); + assertNotEquals("function_call", choice2.getFinishReason()); // could be stop or length, but should not be function_call + assertNull(choice2.getMessage().getFunctionCall()); + assertNotNull(choice2.getMessage().getContent()); + } + + @Test + void createChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatCompletionChoice choice = service.createChatCompletion(chatCompletionRequest).getChoices().get(0); + assertEquals("function_call", choice.getFinishReason()); + assertNotNull(choice.getMessage().getFunctionCall()); + assertEquals("get_weather", choice.getMessage().getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, choice.getMessage().getFunctionCall().getArguments()); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("location")); + assertNotNull(choice.getMessage().getFunctionCall().getArguments().get("unit")); + } + + @Test + void streamChatCompletionWithFunctions() { + final List functions = Collections.singletonList(ChatFunction.builder() + .name("get_weather") + .description("Get the current weather in a given location") + .executor(Weather.class, w -> new WeatherResponse(w.location, w.unit, 25, "sunny")) + .build()); + final FunctionExecutor functionExecutor = new FunctionExecutor(functions); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest)) + .blockingLast() + .getAccumulatedMessage(); + assertNotNull(accumulatedMessage.getFunctionCall()); + assertEquals("get_weather", accumulatedMessage.getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, accumulatedMessage.getFunctionCall().getArguments()); + + ChatMessage callResponse = functionExecutor.executeAndConvertToMessageHandlingExceptions(accumulatedMessage.getFunctionCall()); + assertNotEquals("error", callResponse.getName()); + + // this performs an unchecked cast + WeatherResponse functionExecutionResponse = functionExecutor.execute(accumulatedMessage.getFunctionCall()); + assertInstanceOf(WeatherResponse.class, functionExecutionResponse); + assertEquals(25, functionExecutionResponse.temperature); + + JsonNode jsonFunctionExecutionResponse = functionExecutor.executeAndConvertToJson(accumulatedMessage.getFunctionCall()); + assertInstanceOf(ObjectNode.class, jsonFunctionExecutionResponse); + assertEquals("25", jsonFunctionExecutionResponse.get("temperature").asText()); + + + messages.add(accumulatedMessage); + messages.add(callResponse); + + ChatCompletionRequest chatCompletionRequest2 = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(functionExecutor.getFunctions()) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage2 = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest2)) + .blockingLast() + .getAccumulatedMessage(); + assertNull(accumulatedMessage2.getFunctionCall()); + assertNotNull(accumulatedMessage2.getContent()); + } + + @Test + void streamChatCompletionWithDynamicFunctions() { + ChatFunctionDynamic function = ChatFunctionDynamic.builder() + .name("get_weather") + .description("Get the current weather of a location") + .addProperty(ChatFunctionProperty.builder() + .name("location") + .type("string") + .description("City and state, for example: León, Guanajuato") + .build()) + .addProperty(ChatFunctionProperty.builder() + .name("unit") + .type("string") + .description("The temperature unit, can be 'celsius' or 'fahrenheit'") + .enumValues(new HashSet<>(Arrays.asList("celsius", "fahrenheit"))) + .required(true) + .build()) + .build(); + + final List messages = new ArrayList<>(); + final ChatMessage systemMessage = new ChatMessage(ChatMessageRole.SYSTEM.value(), "You are a helpful assistant."); + final ChatMessage userMessage = new ChatMessage(ChatMessageRole.USER.value(), "What is the weather in Monterrey, Nuevo León?"); + messages.add(systemMessage); + messages.add(userMessage); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest + .builder() + .model("gpt-3.5-turbo-0613") + .messages(messages) + .functions(Collections.singletonList(function)) + .n(1) + .maxTokens(100) + .logitBias(new HashMap<>()) + .build(); + + ChatMessage accumulatedMessage = service.mapStreamToAccumulator(service.streamChatCompletion(chatCompletionRequest)) + .blockingLast() + .getAccumulatedMessage(); + assertNotNull(accumulatedMessage.getFunctionCall()); + assertEquals("get_weather", accumulatedMessage.getFunctionCall().getName()); + assertInstanceOf(ObjectNode.class, accumulatedMessage.getFunctionCall().getArguments()); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("location")); + assertNotNull(accumulatedMessage.getFunctionCall().getArguments().get("unit")); + } + +} diff --git a/client/src/test/java/com/theokanning/openai/CompletionTest.java b/service/src/test/java/com/theokanning/openai/service/CompletionTest.java similarity index 53% rename from client/src/test/java/com/theokanning/openai/CompletionTest.java rename to service/src/test/java/com/theokanning/openai/service/CompletionTest.java index 7833f1e6..69284c3b 100644 --- a/client/src/test/java/com/theokanning/openai/CompletionTest.java +++ b/service/src/test/java/com/theokanning/openai/service/CompletionTest.java @@ -1,13 +1,16 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; import com.theokanning.openai.completion.CompletionChoice; +import com.theokanning.openai.completion.CompletionChunk; import com.theokanning.openai.completion.CompletionRequest; +import com.theokanning.openai.service.OpenAiService; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.*; public class CompletionTest { @@ -18,26 +21,38 @@ public class CompletionTest { @Test void createCompletion() { CompletionRequest completionRequest = CompletionRequest.builder() - .model("ada") + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) + .n(5) + .maxTokens(50) .user("testing") .logitBias(new HashMap<>()) + .logprobs(5) .build(); List choices = service.createCompletion(completionRequest).getChoices(); - assertFalse(choices.isEmpty()); + assertEquals(5, choices.size()); + assertNotNull(choices.get(0).getLogprobs()); } @Test - void createCompletionDeprecated() { + void streamCompletion() { CompletionRequest completionRequest = CompletionRequest.builder() + .model("babbage-002") .prompt("Somebody once told me the world is gonna roll me") .echo(true) + .n(1) + .maxTokens(25) .user("testing") + .logitBias(new HashMap<>()) + .logprobs(5) + .stream(true) .build(); - List choices = service.createCompletion("ada", completionRequest).getChoices(); - assertFalse(choices.isEmpty()); + List chunks = new ArrayList<>(); + service.streamCompletion(completionRequest).blockingForEach(chunks::add); + assertTrue(chunks.size() > 0); + assertNotNull(chunks.get(0).getChoices().get(0)); } } diff --git a/client/src/test/java/com/theokanning/openai/EditTest.java b/service/src/test/java/com/theokanning/openai/service/EditTest.java similarity index 52% rename from client/src/test/java/com/theokanning/openai/EditTest.java rename to service/src/test/java/com/theokanning/openai/service/EditTest.java index 61be016c..64277bdd 100644 --- a/client/src/test/java/com/theokanning/openai/EditTest.java +++ b/service/src/test/java/com/theokanning/openai/service/EditTest.java @@ -1,5 +1,6 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; +import com.theokanning.openai.OpenAiHttpException; import com.theokanning.openai.edit.EditRequest; import com.theokanning.openai.edit.EditResult; import org.junit.jupiter.api.Test; @@ -9,30 +10,17 @@ public class EditTest { String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); + com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); @Test - void edit() { + void edit() throws OpenAiHttpException { EditRequest request = EditRequest.builder() .model("text-davinci-edit-001") .input("What day of the wek is it?") .instruction("Fix the spelling mistakes") .build(); - EditResult result = service.createEdit( request); - - assertNotNull(result.getChoices().get(0).getText()); - } - - @Test - void editDeprecated() { - EditRequest request = EditRequest.builder() - .input("What day of the wek is it?") - .instruction("Fix the spelling mistakes") - .build(); - - EditResult result = service.createEdit("text-davinci-edit-001", request); - + EditResult result = service.createEdit(request); assertNotNull(result.getChoices().get(0).getText()); } } diff --git a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java b/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java similarity index 55% rename from client/src/test/java/com/theokanning/openai/EmbeddingTest.java rename to service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java index 16628e0c..3320dcea 100644 --- a/client/src/test/java/com/theokanning/openai/EmbeddingTest.java +++ b/service/src/test/java/com/theokanning/openai/service/EmbeddingTest.java @@ -1,4 +1,4 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; import com.theokanning.openai.embedding.Embedding; import com.theokanning.openai.embedding.EmbeddingRequest; @@ -13,12 +13,12 @@ public class EmbeddingTest { String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); + com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); @Test void createEmbeddings() { EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() - .model("text-similarity-babbage-001") + .model("text-embedding-ada-002") .input(Collections.singletonList("The food was delicious and the waiter...")) .build(); @@ -27,16 +27,4 @@ void createEmbeddings() { assertFalse(embeddings.isEmpty()); assertFalse(embeddings.get(0).getEmbedding().isEmpty()); } - - @Test - void createEmbeddingsDeprecated() { - EmbeddingRequest embeddingRequest = EmbeddingRequest.builder() - .input(Collections.singletonList("The food was delicious and the waiter...")) - .build(); - - List embeddings = service.createEmbeddings("text-similarity-babbage-001", embeddingRequest).getData(); - - assertFalse(embeddings.isEmpty()); - assertFalse(embeddings.get(0).getEmbedding().isEmpty()); - } } diff --git a/client/src/test/java/com/theokanning/openai/FileTest.java b/service/src/test/java/com/theokanning/openai/service/FileTest.java similarity index 66% rename from client/src/test/java/com/theokanning/openai/FileTest.java rename to service/src/test/java/com/theokanning/openai/service/FileTest.java index 9bfe5aeb..51dd9407 100644 --- a/client/src/test/java/com/theokanning/openai/FileTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FileTest.java @@ -1,8 +1,16 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; +import com.theokanning.openai.DeleteResult; import com.theokanning.openai.file.File; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; import java.util.concurrent.TimeUnit; @@ -48,6 +56,14 @@ void retrieveFile() { @Test @Order(4) + void retrieveFileContent() throws IOException { + String fileBytesToString = service.retrieveFileContent(fileId).string(); + String contents = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); + assertEquals(contents, fileBytesToString); + } + + @Test + @Order(5) void deleteFile() { DeleteResult result = service.deleteFile(fileId); assertTrue(result.isDeleted()); diff --git a/client/src/test/java/com/theokanning/openai/FineTuneTest.java b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java similarity index 93% rename from client/src/test/java/com/theokanning/openai/FineTuneTest.java rename to service/src/test/java/com/theokanning/openai/service/FineTuneTest.java index 245da09a..e47460d0 100644 --- a/client/src/test/java/com/theokanning/openai/FineTuneTest.java +++ b/service/src/test/java/com/theokanning/openai/service/FineTuneTest.java @@ -1,7 +1,7 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; -import com.theokanning.openai.finetune.FineTuneRequest; import com.theokanning.openai.finetune.FineTuneEvent; +import com.theokanning.openai.finetune.FineTuneRequest; import com.theokanning.openai.finetune.FineTuneResult; import org.junit.jupiter.api.*; @@ -10,9 +10,10 @@ import static org.junit.jupiter.api.Assertions.*; +@Deprecated @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class FineTuneTest { - static OpenAiService service; + static com.theokanning.openai.service.OpenAiService service; static String fileId; static String fineTuneId; @@ -38,6 +39,7 @@ void createFineTune() { FineTuneRequest request = FineTuneRequest.builder() .trainingFile(fileId) .model("ada") + .nEpochs(4) .build(); FineTuneResult fineTune = service.createFineTune(request); 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..c0b98f6f --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/FineTuningTest.java @@ -0,0 +1,92 @@ +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 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() { + try { + service.deleteFile(fileId); + } catch (Exception e) { + // ignore + } + } + + @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(); + + assertNotNull(fineTuningJob); + } + + @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(3) + void cancelFineTuningJob() throws Exception { + FineTuningJob fineTuningJob = service.cancelFineTuningJob(fineTuningJobId); + + assertEquals("cancelled", fineTuningJob.getStatus()); + + // wait before cleaning up to prevent job failure emails + TimeUnit.SECONDS.sleep(3); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/ImageTest.java b/service/src/test/java/com/theokanning/openai/service/ImageTest.java new file mode 100644 index 00000000..e56b7907 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ImageTest.java @@ -0,0 +1,95 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.image.CreateImageEditRequest; +import com.theokanning.openai.image.CreateImageRequest; +import com.theokanning.openai.image.CreateImageVariationRequest; +import com.theokanning.openai.image.Image; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +public class ImageTest { + + static String filePath = "src/test/resources/penguin.png"; + static String fileWithAlphaPath = "src/test/resources/penguin_with_alpha.png"; + static String maskPath = "src/test/resources/mask.png"; + + String token = System.getenv("OPENAI_TOKEN"); + com.theokanning.openai.service.OpenAiService service = new OpenAiService(token, Duration.ofSeconds(30)); + + @Test + void createImageUrl() { + CreateImageRequest createImageRequest = CreateImageRequest.builder() + .prompt("penguin") + .n(3) + .size("256x256") + .user("testing") + .build(); + + List images = service.createImage(createImageRequest).getData(); + assertEquals(3, images.size()); + assertNotNull(images.get(0).getUrl()); + } + + @Test + void createImageBase64() { + CreateImageRequest createImageRequest = CreateImageRequest.builder() + .prompt("penguin") + .responseFormat("b64_json") + .user("testing") + .build(); + + List images = service.createImage(createImageRequest).getData(); + assertEquals(1, images.size()); + assertNotNull(images.get(0).getB64Json()); + } + + @Test + void createImageEdit() { + CreateImageEditRequest createImageRequest = CreateImageEditRequest.builder() + .prompt("a penguin with a red background") + .responseFormat("url") + .size("256x256") + .user("testing") + .n(2) + .build(); + + List images = service.createImageEdit(createImageRequest, fileWithAlphaPath, null).getData(); + assertEquals(2, images.size()); + assertNotNull(images.get(0).getUrl()); + } + + @Test + void createImageEditWithMask() { + CreateImageEditRequest createImageRequest = CreateImageEditRequest.builder() + .prompt("a penguin with a red hat") + .responseFormat("url") + .size("256x256") + .user("testing") + .n(2) + .build(); + + List images = service.createImageEdit(createImageRequest, filePath, maskPath).getData(); + assertEquals(2, images.size()); + assertNotNull(images.get(0).getUrl()); + } + + @Test + void createImageVariation() { + CreateImageVariationRequest createImageVariationRequest = CreateImageVariationRequest.builder() + .responseFormat("url") + .size("256x256") + .user("testing") + .n(2) + .build(); + + List images = service.createImageVariation(createImageVariationRequest, filePath).getData(); + assertEquals(2, images.size()); + assertNotNull(images.get(0).getUrl()); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/MessageTest.java b/service/src/test/java/com/theokanning/openai/service/MessageTest.java new file mode 100644 index 00000000..06a903c0 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/MessageTest.java @@ -0,0 +1,137 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.ListSearchParameters; +import com.theokanning.openai.file.File; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageFile; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.messages.ModifyMessageRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +public class MessageTest { + + static OpenAiService service; + + static String threadId; + + @BeforeAll + static void setup() { + String token = System.getenv("OPENAI_TOKEN"); + service = new OpenAiService(token); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + threadId = service.createThread(threadRequest).getId(); + } + + @AfterAll + static void teardown() { + try { + service.deleteThread(threadId); + } catch (Exception e) { + // ignore + } + } + + @Test + void createMessage() { + File file = service.uploadFile("assistants", "src/test/resources/penguin.png"); + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .fileIds(Collections.singletonList(file.getId())) + .metadata(metadata) + .build(); + + Message message = service.createMessage(threadId, messageRequest); + + assertNotNull(message.getId()); + assertEquals("thread.message", message.getObject()); + assertEquals(1, message.getFileIds().size()); + } + + @Test + void retrieveMessage() { + String messageId = createTestMessage().getId(); + + Message message = service.retrieveMessage(threadId, messageId); + + assertEquals(messageId, message.getId()); + } + + @Test + void modifyMessage() { + String messageId = createTestMessage().getId(); + + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + ModifyMessageRequest request = ModifyMessageRequest.builder() + .metadata(metadata) + .build(); + Message message = service.modifyMessage(threadId, messageId, request); + + assertEquals(messageId, message.getId()); + assertEquals("value", message.getMetadata().get("key")); + } + + @Test + void listMessages() { + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + String separateThreadId = service.createThread(threadRequest).getId(); + createTestMessage(separateThreadId); + createTestMessage(separateThreadId); + createTestMessage(separateThreadId); + + List messages = service.listMessages(separateThreadId).getData(); + + assertEquals(3, messages.size()); + } + + @Test + void retrieveAndListMessageFile() { + File file = service.uploadFile("assistants", "src/test/resources/penguin.png"); + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .fileIds(Collections.singletonList(file.getId())) + .build(); + + Message message = service.createMessage(threadId, messageRequest); + + MessageFile messageFile = service.retrieveMessageFile(threadId, message.getId(), file.getId()); + + assertEquals(file.getId(), messageFile.getId()); + assertEquals(message.getId(), messageFile.getMessageId()); + + List messageFiles = service.listMessageFiles(threadId, message.getId(), new ListSearchParameters()).getData(); + assertEquals(1, messageFiles.size()); + } + + Message createTestMessage() { + return createTestMessage(threadId); + } + + Message createTestMessage(String threadId) { + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + return service.createMessage(threadId, messageRequest); + } +} \ No newline at end of file diff --git a/service/src/test/java/com/theokanning/openai/service/ModelTest.java b/service/src/test/java/com/theokanning/openai/service/ModelTest.java new file mode 100644 index 00000000..637ed5e6 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ModelTest.java @@ -0,0 +1,31 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.model.Model; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +public class ModelTest { + + String token = System.getenv("OPENAI_TOKEN"); + com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); + + @Test + void listModels() { + List models = service.listModels(); + + assertFalse(models.isEmpty()); + } + + @Test + void getModel() { + Model model = service.getModel("babbage-002"); + + assertEquals("babbage-002", model.id); + assertEquals("system", model.ownedBy); + } +} diff --git a/client/src/test/java/com/theokanning/openai/ModerationTest.java b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java similarity index 79% rename from client/src/test/java/com/theokanning/openai/ModerationTest.java rename to service/src/test/java/com/theokanning/openai/service/ModerationTest.java index c6024c08..f28bc083 100644 --- a/client/src/test/java/com/theokanning/openai/ModerationTest.java +++ b/service/src/test/java/com/theokanning/openai/service/ModerationTest.java @@ -1,7 +1,7 @@ -package com.theokanning.openai; +package com.theokanning.openai.service; -import com.theokanning.openai.moderation.ModerationRequest; import com.theokanning.openai.moderation.Moderation; +import com.theokanning.openai.moderation.ModerationRequest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,12 +10,12 @@ public class ModerationTest { String token = System.getenv("OPENAI_TOKEN"); - OpenAiService service = new OpenAiService(token); + com.theokanning.openai.service.OpenAiService service = new OpenAiService(token); @Test void createModeration() { ModerationRequest moderationRequest = ModerationRequest.builder() - .input("I want to kill them") + .input("I want to kill him") .model("text-moderation-latest") .build(); diff --git a/service/src/test/java/com/theokanning/openai/service/OpenAiServiceTest.java b/service/src/test/java/com/theokanning/openai/service/OpenAiServiceTest.java new file mode 100644 index 00000000..25902486 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/OpenAiServiceTest.java @@ -0,0 +1,75 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.OpenAiHttpException; +import com.theokanning.openai.completion.CompletionResult; +import io.reactivex.Single; +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; +import retrofit2.HttpException; +import retrofit2.Response; + +import static org.junit.jupiter.api.Assertions.*; + +public class OpenAiServiceTest { + + @Test + void assertTokenNotNull() { + String token = null; + assertThrows(NullPointerException.class, () -> new OpenAiService(token)); + } + + @Test + void executeHappyPath() { + CompletionResult expected = new CompletionResult(); + Single single = Single.just(expected); + + CompletionResult actual = OpenAiService.execute(single); + assertEquals(expected, actual); + } + + @Test + void executeParseHttpError() { + String errorBody = "{\"error\":{\"message\":\"Invalid auth token\",\"type\":\"type\",\"param\":\"param\",\"code\":\"code\"}}"; + HttpException httpException = createException(errorBody, 401); + Single single = Single.error(httpException); + + OpenAiHttpException exception = assertThrows(OpenAiHttpException.class, () -> OpenAiService.execute(single)); + + assertEquals("Invalid auth token", exception.getMessage()); + assertEquals("type", exception.type); + assertEquals("param", exception.param); + assertEquals("code", exception.code); + assertEquals(401, exception.statusCode); + } + + @Test + void executeParseUnknownProperties() { + // error body contains one unknown property and no message + String errorBody = "{\"error\":{\"unknown\":\"Invalid auth token\",\"type\":\"type\",\"param\":\"param\",\"code\":\"code\"}}"; + HttpException httpException = createException(errorBody, 401); + Single single = Single.error(httpException); + + OpenAiHttpException exception = assertThrows(OpenAiHttpException.class, () -> OpenAiService.execute(single)); + assertNull(exception.getMessage()); + assertEquals("type", exception.type); + assertEquals("param", exception.param); + assertEquals("code", exception.code); + assertEquals(401, exception.statusCode); + } + + @Test + void executeNullErrorBodyThrowOriginalError() { + // exception with a successful response creates an error without an error body + HttpException httpException = new HttpException(Response.success(new CompletionResult())); + Single single = Single.error(httpException); + + HttpException exception = assertThrows(HttpException.class, () -> OpenAiService.execute(single)); + } + + private HttpException createException(String errorBody, int code) { + ResponseBody body = ResponseBody.create(MediaType.get("application/json"), errorBody); + Response response = Response.error(code, body); + return new HttpException(response); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/ResponseBodyCallbackTest.java b/service/src/test/java/com/theokanning/openai/service/ResponseBodyCallbackTest.java new file mode 100644 index 00000000..7e22c59c --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ResponseBodyCallbackTest.java @@ -0,0 +1,81 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.OpenAiHttpException; +import io.reactivex.BackpressureStrategy; +import io.reactivex.Flowable; +import io.reactivex.subscribers.TestSubscriber; +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.Test; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.mock.Calls; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ResponseBodyCallbackTest { + + @Test + void testHappyPath() { + ResponseBody body = ResponseBody.create(MediaType.get("application/json"), "data: line 1\n\ndata: line 2\n\ndata: [DONE]\n\n"); + Call call = Calls.response(body); + + Flowable flowable = Flowable.create(emitter -> call.enqueue(new ResponseBodyCallback(emitter, false)), BackpressureStrategy.BUFFER); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + flowable.subscribe(testSubscriber); + + testSubscriber.assertComplete(); + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(2); + assertEquals("line 1", testSubscriber.values().get(0).getData()); + assertEquals("line 2", testSubscriber.values().get(1).getData()); + } + + @Test + void testEmitDone() { + ResponseBody body = ResponseBody.create(MediaType.get("application/json"), "data: line 1\n\ndata: line 2\n\ndata: [DONE]\n\n"); + Call call = Calls.response(body); + + Flowable flowable = Flowable.create(emitter -> call.enqueue(new ResponseBodyCallback(emitter, true)), BackpressureStrategy.BUFFER); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + flowable.subscribe(testSubscriber); + + testSubscriber.assertComplete(); + testSubscriber.assertNoErrors(); + testSubscriber.assertValueCount(3); + assertEquals("[DONE]", testSubscriber.values().get(2).getData()); + } + + @Test + void testSseFormatException() { + ResponseBody body = ResponseBody.create(MediaType.get("application/json"), "bad: line 1\n\ndata: line 2\n\ndata: [DONE]\n\n"); + Call call = Calls.response(body); + + Flowable flowable = Flowable.create(emitter -> call.enqueue(new ResponseBodyCallback(emitter, true)), BackpressureStrategy.BUFFER); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + flowable.subscribe(testSubscriber); + + testSubscriber.assertError(SSEFormatException.class); + } + + @Test + void testServerError() { + String errorBody = "{\"error\":{\"message\":\"Invalid auth token\",\"type\":\"type\",\"param\":\"param\",\"code\":\"code\"}}"; + ResponseBody body = ResponseBody.create(MediaType.get("application/json"), errorBody); + Call call = Calls.response(Response.error(401, body)); + + Flowable flowable = Flowable.create(emitter -> call.enqueue(new ResponseBodyCallback(emitter, true)), BackpressureStrategy.BUFFER); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + flowable.subscribe(testSubscriber); + + testSubscriber.assertError(OpenAiHttpException.class); + + assertEquals("Invalid auth token", testSubscriber.errors().get(0).getMessage()); + } + +} diff --git a/service/src/test/java/com/theokanning/openai/service/RunTest.java b/service/src/test/java/com/theokanning/openai/service/RunTest.java new file mode 100644 index 00000000..2bd0c166 --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/RunTest.java @@ -0,0 +1,69 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.OpenAiResponse; +import com.theokanning.openai.assistants.Assistant; +import com.theokanning.openai.assistants.AssistantRequest; +import com.theokanning.openai.messages.Message; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.runs.Run; +import com.theokanning.openai.runs.RunCreateRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import com.theokanning.openai.utils.TikTokensUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RunTest { + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + @Test + @Timeout(10) + void createRetrieveRun() { + AssistantRequest assistantRequest = AssistantRequest.builder() + .model(TikTokensUtil.ModelEnum.GPT_4_1106_preview.getName()) + .name("MATH_TUTOR") + .instructions("You are a personal Math Tutor.") + .build(); + Assistant assistant = service.createAssistant(assistantRequest); + + ThreadRequest threadRequest = ThreadRequest.builder() + .build(); + Thread thread = service.createThread(threadRequest); + + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + Message message = service.createMessage(thread.getId(), messageRequest); + + RunCreateRequest runCreateRequest = RunCreateRequest.builder() + .assistantId(assistant.getId()) + .build(); + + Run run = service.createRun(thread.getId(), runCreateRequest); + assertNotNull(run); + + Run retrievedRun; + do { + retrievedRun = service.retrieveRun(thread.getId(), run.getId()); + assertEquals(run.getId(), retrievedRun.getId()); + } + while (!(retrievedRun.getStatus().equals("completed")) && !(retrievedRun.getStatus().equals("failed"))); + + + assertNotNull(retrievedRun); + + OpenAiResponse response = service.listMessages(thread.getId()); + + List messages = response.getData(); + assertEquals(2, messages.size()); + assertEquals("user", messages.get(1).getRole()); + assertEquals("assistant", messages.get(0).getRole()); + } +} diff --git a/service/src/test/java/com/theokanning/openai/service/ThreadTest.java b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java new file mode 100644 index 00000000..212653af --- /dev/null +++ b/service/src/test/java/com/theokanning/openai/service/ThreadTest.java @@ -0,0 +1,67 @@ +package com.theokanning.openai.service; + +import com.theokanning.openai.DeleteResult; +import com.theokanning.openai.messages.MessageRequest; +import com.theokanning.openai.threads.Thread; +import com.theokanning.openai.threads.ThreadRequest; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ThreadTest { + + String token = System.getenv("OPENAI_TOKEN"); + OpenAiService service = new OpenAiService(token); + + static String threadId; + + @Test + @Order(1) + void createThread() { + MessageRequest messageRequest = MessageRequest.builder() + .content("Hello") + .build(); + + ThreadRequest threadRequest = ThreadRequest.builder() + .messages(Collections.singletonList(messageRequest)) + .build(); + + Thread thread = service.createThread(threadRequest); + threadId = thread.getId(); + assertEquals("thread", thread.getObject()); + } + + @Test + @Order(2) + void retrieveThread() { + Thread thread = service.retrieveThread(threadId); + System.out.println(thread.getMetadata()); + assertEquals("thread", thread.getObject()); + } + + @Test + @Order(3) + void modifyThread() { + Map metadata = new HashMap<>(); + metadata.put("action", "modify"); + ThreadRequest threadRequest = ThreadRequest.builder() + .metadata(metadata) + .build(); + Thread thread = service.modifyThread(threadId, threadRequest); + assertEquals("thread", thread.getObject()); + assertEquals("modify", thread.getMetadata().get("action")); + } + + @Test + @Order(4) + void deleteThread() { + DeleteResult deleteResult = service.deleteThread(threadId); + assertEquals("thread.deleted", deleteResult.getObject()); + } +} \ No newline at end of file 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 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 diff --git a/client/src/test/resources/fine-tuning-data.jsonl b/service/src/test/resources/fine-tuning-data.jsonl similarity index 100% rename from client/src/test/resources/fine-tuning-data.jsonl rename to service/src/test/resources/fine-tuning-data.jsonl diff --git a/service/src/test/resources/hello-world.mp3 b/service/src/test/resources/hello-world.mp3 new file mode 100644 index 00000000..b5bd8594 Binary files /dev/null and b/service/src/test/resources/hello-world.mp3 differ diff --git a/service/src/test/resources/korean-hello.mp3 b/service/src/test/resources/korean-hello.mp3 new file mode 100644 index 00000000..d1e65b60 Binary files /dev/null and b/service/src/test/resources/korean-hello.mp3 differ diff --git a/service/src/test/resources/mask.png b/service/src/test/resources/mask.png new file mode 100644 index 00000000..84fcfb35 Binary files /dev/null and b/service/src/test/resources/mask.png differ diff --git a/service/src/test/resources/penguin.png b/service/src/test/resources/penguin.png new file mode 100644 index 00000000..91b023c3 Binary files /dev/null and b/service/src/test/resources/penguin.png differ diff --git a/service/src/test/resources/penguin_with_alpha.png b/service/src/test/resources/penguin_with_alpha.png new file mode 100644 index 00000000..4636c4f4 Binary files /dev/null and b/service/src/test/resources/penguin_with_alpha.png differ diff --git a/settings.gradle b/settings.gradle index 969e3691..f0b1618d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,10 @@ rootProject.name = 'openai-java' include 'api' include 'client' include 'example' +include 'service' + +dependencyResolutionManagement { + repositories { + mavenCentral() + } +}