diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml index 340832839..9bc1bde85 100644 --- a/foundation-models/openai/pom.xml +++ b/foundation-models/openai/pom.xml @@ -38,18 +38,22 @@ ${project.basedir}/../../ - 81% - 91% - 88% - 78% - 90% - 92% + 77% + 87% + 85% + 75% + 87% + 91% com.sap.cloud.sdk.cloudplatform cloudplatform-connectivity + + com.sap.cloud.sdk.cloudplatform + cloudplatform-core + com.sap.cloud.sdk.cloudplatform connectivity-apache-httpclient5 @@ -117,6 +121,11 @@ reactor-core true + + com.openai + openai-java-core + true + org.projectlombok @@ -202,13 +211,13 @@ /deployments/{deployment-id}/completions - /deployments/{deployment-id}/audio/transcriptions - /deployments/{deployment-id}/audio/translations - /deployments/{deployment-id}/images/generations + /deployments/{deployment-id}/audio/transcriptions + /deployments/{deployment-id}/audio/translations + /deployments/{deployment-id}/images/generations chatCompletionResponseMessage.context - createChatCompletionRequest.data_sources + createChatCompletionRequest.data_sources true diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java new file mode 100644 index 000000000..9bf5ff4d3 --- /dev/null +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClient.java @@ -0,0 +1,319 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import com.google.common.annotations.Beta; +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientImpl; +import com.openai.core.ClientOptions; +import com.openai.core.RequestOptions; +import com.openai.core.http.Headers; +import com.openai.core.http.HttpClient; +import com.openai.core.http.HttpRequest; +import com.openai.core.http.HttpResponse; +import com.openai.errors.OpenAIIoException; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.core.AiModel; +import com.sap.ai.sdk.core.DeploymentResolutionException; +import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.apache.hc.core5.net.URIBuilder; + +/** + * Factory for creating OpenAI SDK clients configured for SAP AI Core deployments. + * + *

This class provides factory methods that return fully configured OpenAI SDK clients using SAP + * Cloud SDK's Apache HttpClient with automatic OAuth token refresh. + * + * @since 1.18.0 + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Beta +public final class AiCoreOpenAiClient { + + private static final String DEFAULT_RESOURCE_GROUP = "default"; + + /** + * Create an OpenAI client for a deployment serving the specified model using the default resource + * group. + * + * @param model The AI model to target. + * @return A configured OpenAI client instance. + * @throws DeploymentResolutionException If no running deployment is found for the model. + */ + @Nonnull + public static OpenAIClient forModel(@Nonnull final AiModel model) { + return forModel(model, DEFAULT_RESOURCE_GROUP); + } + + /** + * Create an OpenAI client for a deployment serving the specified model in the given resource + * group. + * + * @param model The AI model to target. + * @param resourceGroup The resource group containing the deployment. + * @return A configured OpenAI client instance. + * @throws DeploymentResolutionException If no running deployment is found for the model. + */ + @Nonnull + public static OpenAIClient forModel( + @Nonnull final AiModel model, @Nonnull final String resourceGroup) { + final HttpDestination destination = + new AiCoreService().getInferenceDestination(resourceGroup).forModel(model); + + return fromDestination(destination); + } + + /** + * Create an OpenAI client from a pre-resolved destination. + * + * @param destination The destination to use. + * @return A configured OpenAI client instance. + */ + @Nonnull + @SuppressWarnings("PMD.CloseResource") + static OpenAIClient fromDestination(@Nonnull final HttpDestination destination) { + final var baseUrl = destination.getUri().toString(); + final var httpClient = new AiCoreHttpClientImpl(destination); + + final ClientOptions clientOptions = + ClientOptions.builder().baseUrl(baseUrl).httpClient(httpClient).apiKey("unused").build(); + + return new OpenAIClientImpl(clientOptions); + } + + /** + * Internal implementation of OpenAI SDK's HttpClient interface using Apache HttpClient from SAP + * Cloud SDK. + */ + @Slf4j + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static final class AiCoreHttpClientImpl implements HttpClient { + private final HttpDestination destination; + + private static final String SSE_MEDIA_TYPE = "text/event-stream"; + private static final Set ALLOWED_PATHS = + Set.of( + "/chat/completions", + "/responses", + "/responses/[^/]+", + "/responses/[^/]+/input_items", + "/responses/[^/]+/cancel"); + + @Override + @Nonnull + public HttpResponse execute( + @Nonnull final HttpRequest request, @Nonnull final RequestOptions requestOptions) { + validateAllowedEndpoint(request); + final var apacheClient = ApacheHttpClient5Accessor.getHttpClient(destination); + final var apacheRequest = toApacheRequest(request); + + try { + if (isStreaming(request)) { + final var apacheResponse = apacheClient.executeOpen(null, apacheRequest, null); + final int statusCode = apacheResponse.getCode(); + if (statusCode >= 200 && statusCode < 300) { + return createStreamingResponse(apacheResponse); + } + return createBufferedResponse(apacheResponse); + } else { + return apacheClient.execute(apacheRequest, this::createBufferedResponse); + } + } catch (final IOException e) { + throw new OpenAIIoException("HTTP request execution failed", e); + } + } + + @Override + @Nonnull + public CompletableFuture executeAsync( + @Nonnull final HttpRequest request, @Nonnull final RequestOptions requestOptions) { + return CompletableFuture.supplyAsync( + () -> execute(request, requestOptions), ThreadContextExecutors.getExecutor()); + } + + @Override + public void close() { + // Apache HttpClient lifecycle is managed by Cloud SDK's ApacheHttpClient5Cache + } + + private static void validateAllowedEndpoint(@Nonnull final HttpRequest request) { + final var endpoint = "/" + String.join("/", request.pathSegments()); + if (ALLOWED_PATHS.stream().noneMatch(endpoint::matches)) { + throw new UnsupportedOperationException( + String.format( + "Only requests to the following endpoints are allowed: %s.", ALLOWED_PATHS)); + } + } + + @Nonnull + private ClassicHttpRequest toApacheRequest(@Nonnull final HttpRequest request) { + final var fullUri = buildUrlWithQueryParams(request); + final var method = request.method(); + final var apacheRequest = new BasicClassicHttpRequest(method.name(), fullUri.toString()); + applyRequestHeaders(request, apacheRequest); + + try (var requestBody = request.body()) { + if (requestBody != null) { + try (var outputStream = new ByteArrayOutputStream()) { + requestBody.writeTo(outputStream); + final var bodyBytes = outputStream.toByteArray(); + + final var apacheContentType = + Optional.ofNullable(requestBody.contentType()) + .map(ContentType::parse) + .orElse(ContentType.APPLICATION_JSON); + + apacheRequest.setEntity(new ByteArrayEntity(bodyBytes, apacheContentType)); + return apacheRequest; + } catch (final IOException e) { + throw new OpenAIIoException("Failed to read request body", e); + } + } + } + + return apacheRequest; + } + + private static URI buildUrlWithQueryParams(@Nonnull final HttpRequest request) { + try { + final var uriBuilder = new URIBuilder(request.url()); + final var queryParams = request.queryParams(); + + for (final var key : queryParams.keys()) { + for (final var value : queryParams.values(key)) { + uriBuilder.addParameter(key, value); + } + } + + return uriBuilder.build(); + } catch (URISyntaxException e) { + throw new OpenAIIoException("Failed to build URI with query parameters", e); + } + } + + private static void applyRequestHeaders( + @Nonnull final HttpRequest request, @Nonnull final BasicClassicHttpRequest apacheRequest) { + final var headers = request.headers(); + for (final var name : headers.names()) { + if ("Authorization".equalsIgnoreCase(name)) { + continue; + } + for (final var value : headers.values(name)) { + apacheRequest.addHeader(name, value); + } + } + } + + private static boolean isStreaming(@Nonnull final HttpRequest request) { + return request.headers().values("Accept").stream() + .map(value -> Objects.toString(value, "").toLowerCase(Locale.ROOT)) + .anyMatch(value -> value.contains(SSE_MEDIA_TYPE)); + } + + @Nonnull + private static Headers extractResponseHeaders( + @Nonnull final ClassicHttpResponse apacheResponse) { + final var builder = Headers.builder(); + for (final var header : apacheResponse.getHeaders()) { + builder.put(header.getName(), header.getValue()); + } + return builder.build(); + } + + @Nonnull + private HttpResponse createStreamingResponse(@Nonnull final ClassicHttpResponse apacheResponse) + throws IOException { + + final var statusCode = apacheResponse.getCode(); + final var headers = extractResponseHeaders(apacheResponse); + + final var liveStream = + apacheResponse.getEntity() != null + ? apacheResponse.getEntity().getContent() + : InputStream.nullInputStream(); + + return new StreamingHttpResponse(statusCode, headers, liveStream, apacheResponse); + } + + @Nonnull + private HttpResponse createBufferedResponse(@Nonnull final ClassicHttpResponse apacheResponse) + throws IOException { + try (apacheResponse) { + final int statusCode = apacheResponse.getCode(); + final Headers headers = extractResponseHeaders(apacheResponse); + + final byte[] body = + apacheResponse.getEntity() != null + ? EntityUtils.toByteArray(apacheResponse.getEntity()) + : new byte[0]; + + return new BufferedHttpResponse(statusCode, headers, body); + } + } + + /** + * HTTP response for streaming requests. Keeps the connection open and provides a live stream. + * The stream must be closed by calling {@link #close()}. + */ + record StreamingHttpResponse( + int statusCode, + @Nonnull Headers headers, + @Nonnull InputStream body, + @Nonnull ClassicHttpResponse apacheResponse) + implements HttpResponse { + + @Override + public void close() { + try { + body.close(); + apacheResponse.close(); + log.debug("Closed streaming response connection"); + } catch (final IOException e) { + log.warn("Failed to close streaming response", e); + } + } + } + + /** + * HTTP response for buffered requests. The entire response body is loaded into memory. No + * resources need to be closed. + */ + record BufferedHttpResponse(int statusCode, @Nonnull Headers headers, @Nonnull byte[] bodyBytes) + implements HttpResponse { + + @Nonnull + @Override + public InputStream body() { + return new ByteArrayInputStream(bodyBytes); + } + + @Override + public void close() { + // Body already consumed and buffered, nothing to close + } + } + } +} diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java new file mode 100644 index 000000000..26687df52 --- /dev/null +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreOpenAiClientTest.java @@ -0,0 +1,57 @@ +package com.sap.ai.sdk.foundationmodels.openai; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.openai.client.OpenAIClient; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseStatus; +import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; +import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Cache; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@WireMockTest +class AiCoreOpenAiClientTest { + + private OpenAIClient client; + + @BeforeEach + void setup(@Nonnull final WireMockRuntimeInfo server) { + // Create destination pointing to WireMock server + final var destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); + + // Create OpenAI client using our custom implementation + client = AiCoreOpenAiClient.fromDestination(destination); + + // Disable HTTP client caching for tests to ensure fresh clients + ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); + } + + @AfterEach + void reset() { + ApacheHttpClient5Accessor.setHttpClientCache(null); + ApacheHttpClient5Accessor.setHttpClientFactory(null); + } + + @Test + void testResponseSuccess() { + final var params = + ResponseCreateParams.builder() + .input("What is the capital of France?") + .model("gpt-5") + .build(); + + final Response response = client.responses().create(params); + + assertThat(response).isNotNull(); + assertThat(response.id()).isEqualTo("resp_01a38d2783b385be0069bd43d260108193aef990678aa8a0af"); + assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED); + assertThat(response.output()).isNotEmpty(); + } +} diff --git a/foundation-models/openai/src/test/resources/mappings/createResponse.json b/foundation-models/openai/src/test/resources/mappings/createResponse.json new file mode 100644 index 000000000..bf013a069 --- /dev/null +++ b/foundation-models/openai/src/test/resources/mappings/createResponse.json @@ -0,0 +1,158 @@ +{ + "request": { + "method": "POST", + "urlPattern": "/responses", + "bodyPatterns": [ + { + "equalToJson": { + "input": "What is the capital of France?", + "model": "gpt-5" + } + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json", + "x-request-id": "a1c0c7e7-58a0-957c-bda8-1cf971154766", + "ai-inference-id": "a1c0c7e7-58a0-957c-bda8-1cf971154766", + "x-upstream-service-time": "1474" + }, + "jsonBody": { + "background": false, + "completed_at": 1774011347, + "content_filters": [ + { + "blocked": false, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 60, + "start_offset": 30 + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": false, + "severity": "safe" + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": false, + "severity": "safe" + } + }, + "source_type": "prompt" + }, + { + "blocked": false, + "content_filter_offsets": { + "check_offset": 0, + "end_offset": 221, + "start_offset": 0 + }, + "content_filter_raw": [], + "content_filter_results": { + "hate": { + "filtered": false, + "severity": "safe" + }, + "protected_material_code": { + "detected": false, + "filtered": false + }, + "self_harm": { + "filtered": false, + "severity": "safe" + }, + "sexual": { + "filtered": false, + "severity": "safe" + }, + "violence": { + "filtered": false, + "severity": "safe" + } + }, + "source_type": "completion" + } + ], + "created_at": 1774011346, + "error": null, + "frequency_penalty": 0.0, + "id": "resp_01a38d2783b385be0069bd43d260108193aef990678aa8a0af", + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "metadata": {}, + "model": "gpt-5-2025-08-07-dz-std", + "object": "response", + "output": [ + { + "id": "rs_01a38d2783b385be0069bd43d2de808193a34727b53738d2e1", + "summary": [], + "type": "reasoning" + }, + { + "content": [ + { + "annotations": [], + "logprobs": [], + "text": "Paris.", + "type": "output_text" + } + ], + "id": "msg_01a38d2783b385be0069bd43d354388193a870074cabde1603", + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "status": "completed", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 13, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 59, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 72 + }, + "user": null + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d5732ec82..36a2ceac1 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,8 @@ 4.38.0 2.21.1 1.5.32 + 1.9.10 + 4.29.0 1.16.4 20251224 @@ -261,6 +263,32 @@ spring-app ${project.version} + + com.openai + openai-java-client-okhttp + ${openai-java.version} + + + com.openai + openai-java-core + ${openai-java.version} + + + com.openai + openai-java + ${openai-java.version} + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin-stdlib-jdk8.version} + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin-stdlib-jdk8.version} + diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index 355da3f66..a5a8d21b6 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -58,6 +58,12 @@ tomcat-embed-websocket ${apache-tomcat-embed.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.19.4 + @@ -71,6 +77,10 @@ com.sap.ai.sdk.foundationmodels openai + + com.openai + openai-java-core + com.sap.ai.sdk orchestration diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/AiCoreOpenAiService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/AiCoreOpenAiService.java new file mode 100644 index 000000000..7b59d8bda --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/AiCoreOpenAiService.java @@ -0,0 +1,103 @@ +package com.sap.ai.sdk.app.services; + +import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.GPT_5; + +import com.openai.client.OpenAIClient; +import com.openai.core.http.QueryParams; +import com.openai.core.http.StreamResponse; +import com.openai.models.ChatModel; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.responses.Response; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseStreamEvent; +import com.sap.ai.sdk.foundationmodels.openai.AiCoreOpenAiClient; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.stereotype.Service; + +/** Service class for the OpenAI Responses API */ +@Service +@Slf4j +public class AiCoreOpenAiService { + + private static final OpenAIClient CLIENT = AiCoreOpenAiClient.forModel(GPT_5, "ai-sdk-java-e2e"); + + /** + * Create a simple response using the Responses API + * + * @param input the input text to send to the model + * @return the response object from the Responses API + */ + @Nonnull + public Response createResponse(@Nonnull final String input) { + val params = + ResponseCreateParams.builder().input(input).model(ChatModel.GPT_5).store(false).build(); + return CLIENT.responses().create(params); + } + + /** + * Create a response and immediately retrieve it using the Responses API. This demonstrates the + * two-step process of creating and then fetching a response. + * + * @param input the input text to send to the model + * @return the retrieved response object from the Responses API + */ + @Nonnull + public Response retrieveResponse(@Nonnull final String input) { + val params = ResponseCreateParams.builder().input(input).model(ChatModel.GPT_5).build(); + val createResponse = CLIENT.responses().create(params); + return CLIENT.responses().retrieve(createResponse.id()); + } + + /** + * Create a streaming response using the Responses API + * + * @param input the input text to send to the model + * @return the streaming response object from the Responses API + */ + @Nonnull + public StreamResponse createStreamingResponse(@Nonnull final String input) { + val params = + ResponseCreateParams.builder().input(input).model(ChatModel.GPT_5).store(false).build(); + return CLIENT.responses().createStreaming(params); + } + + /** + * Create a chat completion using the Chat Completions API. Note: This uses the legacy API version + * format via query parameters. + * + * @param input the input text to send to the model + * @return the chat completion response from the Chat Completions API + */ + @Nonnull + public ChatCompletion createChatCompletion(@Nonnull final String input) { + val params = + ChatCompletionCreateParams.builder() + .addUserMessage(input) + .model(ChatModel.GPT_5) + .additionalQueryParams(QueryParams.builder().put("api-version", "2024-02-01").build()) + .build(); + return CLIENT.chat().completions().create(params); + } + + /** + * Create a streaming chat completion using the Chat Completions API + * + * @param input the input text to send to the model + * @return the streaming chat completion response from the Chat Completions API + */ + @Nonnull + public StreamResponse createStreamingChatCompletion( + @Nonnull final String input) { + val params = + ChatCompletionCreateParams.builder() + .addUserMessage(input) + .model(ChatModel.GPT_5) + .additionalQueryParams(QueryParams.builder().put("api-version", "2024-02-01").build()) + .build(); + return CLIENT.chat().completions().createStreaming(params); + } +} diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/AiCoreOpenAiTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/AiCoreOpenAiTest.java new file mode 100644 index 000000000..99548e00f --- /dev/null +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/AiCoreOpenAiTest.java @@ -0,0 +1,78 @@ +package com.sap.ai.sdk.app.controllers; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.ai.sdk.app.services.AiCoreOpenAiService; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@Slf4j +class AiCoreOpenAiTest { + AiCoreOpenAiService service; + + @BeforeEach + void setUp() { + service = new AiCoreOpenAiService(); + } + + @Test + void testCreateResponse() { + final var response = service.createResponse("What is the capital of France?"); + assertThat(response).isNotNull(); + assertThat(response.output()).isNotNull(); + assertThat(response.output().get(1).message().get().content().get(0).asOutputText().text()) + .contains("Paris"); + } + + @Test + @Disabled("Flaky test") + void testGetResponse() { + final var response = service.retrieveResponse("What is the capital of France?"); + assertThat(response).isNotNull(); + assertThat(response.output()).isNotNull(); + assertThat(response.output().get(1).message().get().content().get(0).asOutputText().text()) + .contains("Paris"); + } + + @Test + @Disabled("Not yet enables and leads to Internal Server Error (500)") + void testCreateStreamingResponse() { + try (final var streamResponse = + service.createStreamingResponse("What is the capital of France?")) { + final var events = streamResponse.stream().collect(Collectors.toList()); + + assertThat(events).isNotEmpty(); + + final var hasTextDeltas = + events.stream().anyMatch(event -> event.outputTextDelta().isPresent()); + assertThat(hasTextDeltas).isTrue(); + } + } + + @Test + void testCreateChatCompletion() { + final var response = service.createChatCompletion("What is the capital of France?"); + assertThat(response).isNotNull(); + } + + @Test + void testCreateStreamingChatCompletion() { + try (final var streamResponse = + service.createStreamingChatCompletion("What is the capital of France?")) { + final var events = streamResponse.stream().collect(Collectors.toList()); + + assertThat(events).isNotEmpty(); + + final var hasContentDeltas = + events.stream() + .anyMatch( + event -> + !event.choices().isEmpty() + && event.choices().get(0).delta().content().isPresent()); + assertThat(hasContentDeltas).isTrue(); + } + } +}