diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/AssistantsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/AssistantsClient.java index 1bcf570..fd58680 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/AssistantsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/AssistantsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -19,8 +20,12 @@ public final class AssistantsClient extends OpenAIAssistantsClient { private final URI baseUrl; AssistantsClient( - URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/AudioClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/AudioClient.java index 719b591..b620f91 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/AudioClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/AudioClient.java @@ -9,6 +9,7 @@ import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -21,8 +22,13 @@ public final class AudioClient extends OpenAIClient { private final URI baseUrl; - AudioClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + AudioClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ChatClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ChatClient.java index 1421e69..bd47a9e 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ChatClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ChatClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; @@ -19,8 +20,13 @@ public final class ChatClient extends OpenAIClient { private final URI endpoint; - ChatClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + ChatClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); endpoint = baseUrl.resolve(Endpoint.CHAT.getPath()); } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/EmbeddingsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/EmbeddingsClient.java index 5cc406d..05abcb3 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/EmbeddingsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/EmbeddingsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Optional; /** @@ -17,8 +18,12 @@ public final class EmbeddingsClient extends OpenAIClient { private final URI endpoint; EmbeddingsClient( - URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); endpoint = baseUrl.resolve(Endpoint.EMBEDDINCS.getPath()); } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/FilesClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/FilesClient.java index 905573a..ae0b82c 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/FilesClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/FilesClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -17,8 +18,13 @@ public final class FilesClient extends OpenAIClient { private final URI baseUrl; - FilesClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + FilesClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/FineTuningClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/FineTuningClient.java index 501852e..4b37e39 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/FineTuningClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/FineTuningClient.java @@ -5,6 +5,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; @@ -19,8 +20,12 @@ public final class FineTuningClient extends OpenAIClient { private final URI baseUrl; FineTuningClient( - URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ImagesClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ImagesClient.java index 31b3f14..62b450e 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ImagesClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ImagesClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -16,8 +17,13 @@ public final class ImagesClient extends OpenAIClient { private final URI baseUrl; - ImagesClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + ImagesClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/MessagesClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/MessagesClient.java index 3f01fd6..0c8b18c 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/MessagesClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/MessagesClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -19,8 +20,13 @@ public final class MessagesClient extends OpenAIAssistantsClient { private final URI baseUrl; - MessagesClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + MessagesClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ModelsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ModelsClient.java index dfd26bc..9daa582 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ModelsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ModelsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -17,8 +18,13 @@ public final class ModelsClient extends OpenAIClient { private final URI baseUrl; - ModelsClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + ModelsClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ModerationsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ModerationsClient.java index 6fb13c8..b632bdd 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ModerationsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ModerationsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Optional; /** @@ -16,8 +17,12 @@ public final class ModerationsClient extends OpenAIClient { private final URI endpoint; ModerationsClient( - URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); endpoint = baseUrl.resolve(Endpoint.MODERATIONS.getPath()); } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java index 64bd88a..c4e4bd4 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAI.java @@ -2,6 +2,7 @@ import java.net.URI; import java.net.http.HttpClient; +import java.time.Duration; import java.util.Optional; /** @@ -24,19 +25,28 @@ public final class OpenAI { private final MessagesClient messagesClient; private final RunsClient runsClient; - private OpenAI(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - audioClient = new AudioClient(baseUrl, apiKey, organization, httpClient); - chatClient = new ChatClient(baseUrl, apiKey, organization, httpClient); - embeddingsClient = new EmbeddingsClient(baseUrl, apiKey, organization, httpClient); - fineTuningClient = new FineTuningClient(baseUrl, apiKey, organization, httpClient); - filesClient = new FilesClient(baseUrl, apiKey, organization, httpClient); - imagesClient = new ImagesClient(baseUrl, apiKey, organization, httpClient); - modelsClient = new ModelsClient(baseUrl, apiKey, organization, httpClient); - moderationsClient = new ModerationsClient(baseUrl, apiKey, organization, httpClient); - assistantsClient = new AssistantsClient(baseUrl, apiKey, organization, httpClient); - threadsClient = new ThreadsClient(baseUrl, apiKey, organization, httpClient); - messagesClient = new MessagesClient(baseUrl, apiKey, organization, httpClient); - runsClient = new RunsClient(baseUrl, apiKey, organization, httpClient); + private OpenAI( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + audioClient = new AudioClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + chatClient = new ChatClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + embeddingsClient = + new EmbeddingsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + fineTuningClient = + new FineTuningClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + filesClient = new FilesClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + imagesClient = new ImagesClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + modelsClient = new ModelsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + moderationsClient = + new ModerationsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + assistantsClient = + new AssistantsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + threadsClient = new ThreadsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + messagesClient = new MessagesClient(baseUrl, apiKey, organization, httpClient, requestTimeout); + runsClient = new RunsClient(baseUrl, apiKey, organization, httpClient, requestTimeout); } /** @@ -152,6 +162,7 @@ public static class Builder { private Optional organization = Optional.empty(); private Optional httpClient = Optional.empty(); + private Optional requestTimeout = Optional.empty(); public Builder(String apiKey) { this.apiKey = apiKey; @@ -182,6 +193,15 @@ public Builder httpClient(HttpClient httpClient) { return this; } + /** + * @param requestTimeout a timeout in the form of a {@link Duration} which will be set for all + * API requests. If none is set, there will be no timeout. + */ + public Builder requestTimeout(Duration requestTimeout) { + this.requestTimeout = Optional.of(requestTimeout); + return this; + } + public OpenAI build() { if (!baseUrl.endsWith("/")) { baseUrl += "/"; @@ -190,7 +210,8 @@ public OpenAI build() { URI.create(baseUrl), apiKey, organization, - httpClient.orElseGet(HttpClient::newHttpClient)); + httpClient.orElseGet(HttpClient::newHttpClient), + requestTimeout); } } } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIAssistantsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIAssistantsClient.java index e82b3f5..a2c9553 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIAssistantsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIAssistantsClient.java @@ -2,6 +2,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; +import java.time.Duration; import java.util.Map; import java.util.Optional; @@ -11,8 +12,12 @@ */ class OpenAIAssistantsClient extends OpenAIClient { - OpenAIAssistantsClient(String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + OpenAIAssistantsClient( + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); } @Override diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIClient.java index b0f720f..2d8ef25 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/OpenAIClient.java @@ -10,6 +10,7 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -21,14 +22,20 @@ */ abstract class OpenAIClient { - private final String[] authenticationHeaders; - - protected final HttpClient httpClient; - protected final ObjectMapper objectMapper = ObjectMapperSingleton.getInstance(); + private final ObjectMapper objectMapper = ObjectMapperSingleton.getInstance(); - OpenAIClient(String apiKey, Optional organization, HttpClient httpClient) { + private final String[] authenticationHeaders; + private final HttpClient httpClient; + private final Optional requestTimeout; + + OpenAIClient( + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { this.authenticationHeaders = getAuthenticationHeaders(apiKey, organization); this.httpClient = httpClient; + this.requestTimeout = requestTimeout; } HttpRequest.Builder newHttpRequestBuilder(String... headers) { @@ -37,6 +44,7 @@ HttpRequest.Builder newHttpRequestBuilder(String... headers) { if (headers.length > 0) { httpRequestBuilder.headers(headers); } + requestTimeout.ifPresent(httpRequestBuilder::timeout); return httpRequestBuilder; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/RunsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/RunsClient.java index 7c272ba..85c6ceb 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/RunsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/RunsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.List; import java.util.Optional; @@ -19,8 +20,13 @@ public final class RunsClient extends OpenAIAssistantsClient { private final URI baseUrl; - RunsClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + RunsClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/main/java/io/github/stefanbratanov/jvm/openai/ThreadsClient.java b/src/main/java/io/github/stefanbratanov/jvm/openai/ThreadsClient.java index 17ec585..6147da4 100644 --- a/src/main/java/io/github/stefanbratanov/jvm/openai/ThreadsClient.java +++ b/src/main/java/io/github/stefanbratanov/jvm/openai/ThreadsClient.java @@ -4,6 +4,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Optional; /** @@ -15,8 +16,13 @@ public final class ThreadsClient extends OpenAIAssistantsClient { private final URI baseUrl; - ThreadsClient(URI baseUrl, String apiKey, Optional organization, HttpClient httpClient) { - super(apiKey, organization, httpClient); + ThreadsClient( + URI baseUrl, + String apiKey, + Optional organization, + HttpClient httpClient, + Optional requestTimeout) { + super(apiKey, organization, httpClient, requestTimeout); this.baseUrl = baseUrl; } diff --git a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIIntegrationTest.java b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIIntegrationTest.java index ffec810..ab747db 100644 --- a/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIIntegrationTest.java +++ b/src/test/java/io/github/stefanbratanov/jvm/openai/OpenAIIntegrationTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import io.github.stefanbratanov.jvm.openai.ChatMessage.UserMessage.UserMessageWithContentParts.ContentPart.TextContentPart; +import java.io.UncheckedIOException; +import java.net.http.HttpTimeoutException; import java.nio.file.Path; import java.time.Duration; import java.util.List; @@ -14,6 +16,8 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.*; class OpenAIIntegrationTest extends OpenAIIntegrationTestBase { @@ -44,6 +48,40 @@ void testUnauthorizedRequest() { assertThat(exception.errorMessage()).startsWith("Incorrect API key provided: foobar"); } + @Test + void testConfiguringRequestTimeout() { + try (ClientAndServer mockServer = ClientAndServer.startClientAndServer()) { + mockServer + .when(HttpRequest.request()) + .respond( + HttpResponse.response() + .withStatusCode(200) + .withBody( + "{\"id\":\"chatcmpl-123\",\"object\":\"chat.completion\",\"created\":1677652288,\"model\":\"gpt-3.5-turbo-0613\",\"system_fingerprint\":\"fp_44709d6fcb\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Hello there, how may I assist you today?\"},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":9,\"completion_tokens\":12,\"total_tokens\":21}}") + // simulate a backend delay + .withDelay(Delay.seconds(1))); + + OpenAI openAI = + OpenAI.newBuilder("foobar") + .baseUrl("http://localhost:" + mockServer.getPort()) + // set the request timeout to less than the backend delay + .requestTimeout(Duration.ofMillis(500)) + .build(); + + ChatClient chatClient = openAI.chatClient(); + + CreateChatCompletionRequest request = + CreateChatCompletionRequest.newBuilder() + .message(ChatMessage.userMessage("This is a timeout test")) + .build(); + + UncheckedIOException exception = + assertThrows(UncheckedIOException.class, () -> chatClient.createChatCompletion(request)); + + assertThat(exception).hasCauseInstanceOf(HttpTimeoutException.class); + } + } + @Test void testChatClient() { ChatClient chatClient = openAI.chatClient();