diff --git a/foundation-models/openai-wrapper/pom.xml b/foundation-models/openai-wrapper/pom.xml new file mode 100644 index 000000000..1ab8455d0 --- /dev/null +++ b/foundation-models/openai-wrapper/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + com.sap.ai.sdk + sdk-parent + 1.18.0-SNAPSHOT + ../../pom.xml + + openai-wrapper + SAP AI SDK - OpenAI Wrapper + SAP AI SDK wrapper around the official OpenAI Java SDK for use with SAP AI Core. + + https://github.com/SAP/ai-sdk-java?tab=readme-ov-file#documentation + + SAP SE + https://www.sap.com + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + SAP + cloudsdk@sap.com + SAP SE + https://www.sap.com + + + + ${project.basedir}/../../ + 0% + 0% + 0% + 0% + 0% + 0% + + + + + + com.sap.ai.sdk + core + + + com.sap.cloud.sdk.cloudplatform + cloudplatform-connectivity + + + + com.openai + openai-java + 4.31.0 + + + org.springframework + spring-core + + + + + com.google.code.findbugs + jsr305 + + + org.slf4j + slf4j-api + + + + org.projectlombok + lombok + provided + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + com.openai:openai-java-core + com.openai:openai-java-client-okhttp + + com.sap.cloud.sdk.cloudplatform:connectivity-apache-httpclient5 + + + + com.openai:openai-java + + com.sap.cloud.sdk.cloudplatform:cloudplatform-connectivity + + + + + + diff --git a/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProvider.java b/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProvider.java new file mode 100644 index 000000000..ccf4ff170 --- /dev/null +++ b/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProvider.java @@ -0,0 +1,148 @@ +package com.sap.ai.sdk.foundationmodels.openaiwrapper; + +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.credential.BearerTokenCredential; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.core.AiModel; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.Header; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import java.util.Collection; +import java.util.Locale; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; + +/** + * Provider for the official OpenAI Java SDK client, configured to work with SAP AI Core. + * + *

This is a thin wrapper around the official + * OpenAI Java SDK that handles: + * + *

+ * + *

The returned {@link OpenAIClient} is the official SDK client and can be used with the full + * OpenAI API surface (chat completions, embeddings, streaming, function calling, etc.) as + * documented in the OpenAI API documentation. + * + *

No additional abstraction layers are added. Request and response types are the official + * SDK types. + * + *

Example usage: + * + *

{@code
+ * import com.openai.models.chat.completions.ChatCompletionCreateParams;
+ * import com.openai.models.ChatModel;
+ * import com.openai.models.chat.completions.ChatCompletion;
+ *
+ * OpenAIClient client = OpenAiClientProvider.forModel(OpenAiModel.GPT_4O);
+ *
+ * ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
+ *     .addUserMessage("Hello, world!")
+ *     .model(ChatModel.GPT_4O)
+ *     .build();
+ *
+ * ChatCompletion completion = client.chat().completions().create(params);
+ * }
+ * + * @see OpenAI Java SDK + * @see SAP AI Core + */ +@Slf4j +public final class OpenAiClientProvider { + + private OpenAiClientProvider() {} + + /** + * Creates an {@link OpenAIClient} for the given model using the default {@link AiCoreService}. + * + *

This resolves the deployment URL for the given model from SAP AI Core and configures the + * official OpenAI client with the correct base URL and authentication. + * + * @param model The {@link AiModel} to use (e.g. an {@code OpenAiModel} constant). + * @return A configured {@link OpenAIClient} ready to make requests via SAP AI Core. + */ + @Nonnull + public static OpenAIClient forModel(@Nonnull final AiModel model) { + return forModel(new AiCoreService(), model); + } + + /** + * Creates an {@link OpenAIClient} for the given model using the provided {@link AiCoreService}. + * + * @param aiCoreService The {@link AiCoreService} to use for deployment resolution. + * @param model The {@link AiModel} to use. + * @return A configured {@link OpenAIClient} ready to make requests via SAP AI Core. + */ + @Nonnull + public static OpenAIClient forModel( + @Nonnull final AiCoreService aiCoreService, @Nonnull final AiModel model) { + final HttpDestination destination = aiCoreService.getInferenceDestination().forModel(model); + return buildClient(destination); + } + + /** + * Creates an {@link OpenAIClient} using a custom {@link HttpDestination}. + * + *

This is useful for advanced scenarios where you need full control over the destination + * configuration. + * + * @param destination The destination to use. + * @return A configured {@link OpenAIClient}. + */ + @Nonnull + public static OpenAIClient withCustomDestination(@Nonnull final HttpDestination destination) { + return buildClient(destination); + } + + @Nonnull + private static OpenAIClient buildClient(@Nonnull final HttpDestination destination) { + final DefaultHttpDestination defaultDest = (DefaultHttpDestination) destination; + final String baseUrl = defaultDest.getUri().toString() + "v1/"; + log.debug("Building OpenAI client with base URL: {}", baseUrl); + + return OpenAIOkHttpClient.builder() + .baseUrl(baseUrl) + .credential(BearerTokenCredential.create(() -> extractBearerToken(defaultDest))) + .putHeader("AI-Resource-Group", getResourceGroupHeader(defaultDest)) + .build(); + } + + /** + * Extracts the bearer token from the destination's authorization headers. + * + *

The SAP Cloud SDK destination resolves the OAuth token from the service binding. This method + * extracts it so it can be passed to the OpenAI SDK's credential mechanism. + */ + @Nonnull + static String extractBearerToken(@Nonnull final DefaultHttpDestination destination) { + final Collection

headers = destination.getHeaders(destination.getUri()); + return headers.stream() + .filter(h -> "Authorization".equalsIgnoreCase(h.getName())) + .map(Header::getValue) + .filter(v -> v.toLowerCase(Locale.ROOT).startsWith("bearer ")) + .map(v -> v.substring("Bearer ".length()).trim()) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + "No Bearer token found in destination headers. " + + "Ensure the destination is configured with OAuth authentication.")); + } + + /** Extracts the AI-Resource-Group header value from the destination, defaulting to "default". */ + @Nonnull + static String getResourceGroupHeader(@Nonnull final DefaultHttpDestination destination) { + final Collection
headers = destination.getHeaders(destination.getUri()); + return headers.stream() + .filter(h -> "AI-Resource-Group".equalsIgnoreCase(h.getName())) + .map(Header::getValue) + .findFirst() + .orElse("default"); + } +} diff --git a/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiModel.java b/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiModel.java new file mode 100644 index 000000000..9b8555cf4 --- /dev/null +++ b/foundation-models/openai-wrapper/src/main/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiModel.java @@ -0,0 +1,143 @@ +package com.sap.ai.sdk.foundationmodels.openaiwrapper; + +import com.sap.ai.sdk.core.AiModel; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * OpenAI models that are available in AI Core. + * + *

Please note that the template of models provided in this class might be outdated. To check the + * latest availability of OpenAI models in AI Core, please refer to SAP Availability of Generative AI Models . + * + * @param name The name of the model. + * @param version The version of the model (optional). + */ +public record OpenAiModel(@Nonnull String name, @Nullable String version) implements AiModel { + + /** + * internal [Azure OpenAI dall-e-3 model] + * + * @deprecated This model is deprecated on AI Core. + */ + @Deprecated public static final OpenAiModel DALL_E_3 = new OpenAiModel("dall-e-3", null); + + /** + * Azure OpenAI GPT-3.5 Turbo model + * + * @deprecated This model is deprecated on AI Core with a planned retirement on 2025-02-13. The + * suggested replacement model is {@link OpenAiModel#GPT_4O_MINI}. + */ + @Deprecated public static final OpenAiModel GPT_35_TURBO = new OpenAiModel("gpt-35-turbo", null); + + /** + * Azure OpenAI GPT-3.5 Turbo model + * + * @deprecated This model is deprecated on AI Core with a planned retirement on 2025-02-22. The + * suggested replacement model is {@link OpenAiModel#GPT_4O_MINI}. + */ + @Deprecated + public static final OpenAiModel GPT_35_TURBO_1025 = new OpenAiModel("gpt-35-turbo-0125", null); + + /** + * Azure OpenAI GPT-3.5 Turbo model + * + * @deprecated This model is deprecated on AI Core with a planned retirement on 2025-02-13. The + * suggested replacement model is {@link OpenAiModel#GPT_4O_MINI}. + */ + @Deprecated + public static final OpenAiModel GPT_35_TURBO_16K = new OpenAiModel("gpt-35-turbo-16k", null); + + /** + * Azure OpenAI GPT-4 model + * + * @deprecated This model is deprecated on AI Core with a planned retirement on 2025-09-01. The + * suggested replacement model is {@link OpenAiModel#GPT_4O} or {@link OpenAiModel#GPT_41}. + */ + @Deprecated public static final OpenAiModel GPT_4 = new OpenAiModel("gpt-4", null); + + /** + * Azure OpenAI GPT-4 model + * + * @deprecated This model is deprecated on AI Core. + */ + @Deprecated public static final OpenAiModel GPT_4_32K = new OpenAiModel("gpt-4-32k", null); + + /** Azure OpenAI GPT-4o model */ + public static final OpenAiModel GPT_4O = new OpenAiModel("gpt-4o", null); + + /** + * Azure OpenAI GPT-4o Mini model + * + * @deprecated This model is deprecated on AI Core. The suggested replacement model is {@link + * OpenAiModel#GPT_5_MINI}. + */ + @Deprecated public static final OpenAiModel GPT_4O_MINI = new OpenAiModel("gpt-4o-mini", null); + + /** Azure OpenAI GPT-o3 Mini model */ + public static final OpenAiModel O3_MINI = new OpenAiModel("o3-mini", null); + + /** Azure OpenAI GPT-o1 model */ + public static final OpenAiModel O1 = new OpenAiModel("o1", null); + + /** Azure OpenAI Text Embedding 3 Large model */ + public static final OpenAiModel TEXT_EMBEDDING_3_LARGE = + new OpenAiModel("text-embedding-3-large", null); + + /** Azure OpenAI Text Embedding 3 Small model */ + public static final OpenAiModel TEXT_EMBEDDING_3_SMALL = + new OpenAiModel("text-embedding-3-small", null); + + /** Azure OpenAI GPT-o4 Mini model */ + public static final OpenAiModel O4_MINI = new OpenAiModel("o4-mini", null); + + /** Azure OpenAI GPT-o3 model */ + public static final OpenAiModel O3 = new OpenAiModel("o3", null); + + /** Azure OpenAI GPT-4.1 model */ + public static final OpenAiModel GPT_41 = new OpenAiModel("gpt-4.1", null); + + /** Azure OpenAI GPT-4.1-nano model */ + public static final OpenAiModel GPT_41_NANO = new OpenAiModel("gpt-4.1-nano", null); + + /** Azure OpenAI GPT-4.1-mini model */ + public static final OpenAiModel GPT_41_MINI = new OpenAiModel("gpt-4.1-mini", null); + + /** Azure OpenAI GPT-5 model */ + public static final OpenAiModel GPT_5 = new OpenAiModel("gpt-5", null); + + /** Azure OpenAI GPT-5-mini model */ + public static final OpenAiModel GPT_5_MINI = new OpenAiModel("gpt-5-mini", null); + + /** Azure OpenAI GPT-5-nano model */ + public static final OpenAiModel GPT_5_NANO = new OpenAiModel("gpt-5-nano", null); + + /** Azure OpenAI GPT-5-nano model */ + public static final OpenAiModel GPT_REALTIME = new OpenAiModel("gpt-realtime", null); + + /** Azure OpenAI GPT-5.2 model */ + public static final OpenAiModel GPT_52 = new OpenAiModel("gpt-5.2", null); + + /** + * Azure OpenAI Text Embedding ADA 002 model + * + * @deprecated This model is deprecated on AI Core with a planned retirement on 2025-10-03. The + * suggested replacement models are {@link OpenAiModel#TEXT_EMBEDDING_3_SMALL} and {@link + * OpenAiModel#TEXT_EMBEDDING_3_LARGE}. + */ + @Deprecated + public static final OpenAiModel TEXT_EMBEDDING_ADA_002 = + new OpenAiModel("text-embedding-ada-002", null); + + /** + * Create a new instance of OpenAiModel with the provided version. + * + * @param version The version of the model. + * @return The new instance of OpenAiModel. + */ + @Nonnull + public OpenAiModel withVersion(@Nonnull final String version) { + return new OpenAiModel(name, version); + } +} diff --git a/foundation-models/openai-wrapper/src/test/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProviderTest.java b/foundation-models/openai-wrapper/src/test/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProviderTest.java new file mode 100644 index 000000000..589fa0623 --- /dev/null +++ b/foundation-models/openai-wrapper/src/test/java/com/sap/ai/sdk/foundationmodels/openaiwrapper/OpenAiClientProviderTest.java @@ -0,0 +1,93 @@ +package com.sap.ai.sdk.foundationmodels.openaiwrapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import com.openai.client.OpenAIClient; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.Header; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; + +class OpenAiClientProviderTest { + + @Test + void testWithCustomDestinationCreatesClient() { + final var destination = mock(DefaultHttpDestination.class); + + doReturn(URI.create("https://api.ai.sap/v2/inference/deployments/d123/")) + .when(destination) + .getUri(); + doReturn( + List.of( + new Header("Authorization", "Bearer test-token-123"), + new Header("AI-Resource-Group", "my-rg"))) + .when(destination) + .getHeaders(any(URI.class)); + doReturn(destination).when(destination).asHttp(); + + final OpenAIClient client = OpenAiClientProvider.withCustomDestination(destination); + + assertThat(client).isNotNull(); + } + + @Test + void testExtractBearerToken() { + final var destination = mock(DefaultHttpDestination.class); + + doReturn(URI.create("https://api.ai.sap/")).when(destination).getUri(); + doReturn(List.of(new Header("Authorization", "Bearer my-secret-token"))) + .when(destination) + .getHeaders(any(URI.class)); + + final String token = OpenAiClientProvider.extractBearerToken(destination); + + assertThat(token).isEqualTo("my-secret-token"); + } + + @Test + void testExtractBearerTokenThrowsWhenMissing() { + final var destination = mock(DefaultHttpDestination.class); + + doReturn(URI.create("https://api.ai.sap/")).when(destination).getUri(); + doReturn(List.of(new Header("AI-Resource-Group", "default"))) + .when(destination) + .getHeaders(any(URI.class)); + + assertThatThrownBy(() -> OpenAiClientProvider.extractBearerToken(destination)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("No Bearer token found"); + } + + @Test + void testGetResourceGroupHeader() { + final var destination = mock(DefaultHttpDestination.class); + + doReturn(URI.create("https://api.ai.sap/")).when(destination).getUri(); + doReturn(List.of(new Header("AI-Resource-Group", "custom-rg"))) + .when(destination) + .getHeaders(any(URI.class)); + + final String rg = OpenAiClientProvider.getResourceGroupHeader(destination); + + assertThat(rg).isEqualTo("custom-rg"); + } + + @Test + void testGetResourceGroupHeaderDefaultsToDefault() { + final var destination = mock(DefaultHttpDestination.class); + + doReturn(URI.create("https://api.ai.sap/")).when(destination).getUri(); + doReturn(List.of(new Header("Authorization", "Bearer token"))) + .when(destination) + .getHeaders(any(URI.class)); + + final String rg = OpenAiClientProvider.getResourceGroupHeader(destination); + + assertThat(rg).isEqualTo("default"); + } +} diff --git a/pom.xml b/pom.xml index 8baa46508..fa97c9089 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ core-services/document-grounding core-services/prompt-registry foundation-models/openai + foundation-models/openai-wrapper foundation-models/sap-rpt @@ -115,9 +116,9 @@ - org.springframework - spring-core - ${springframework.version} + com.sap.ai.sdk + openai-wrapper + ${project.version} org.springframework @@ -144,6 +145,11 @@ jackson-module-jsonSchema ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + com.github.victools jsonschema-generator @@ -160,6 +166,26 @@ ${jackson.version} + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.9.10 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.9.10 + + + org.jetbrains.kotlin + kotlin-stdlib-jdk7 + 1.9.10 + + + org.jetbrains.kotlin + kotlin-stdlib-common + 1.9.10 + io.micrometer micrometer-core @@ -170,6 +196,11 @@ micrometer-observation ${micrometer.version} + + org.springframework + spring-core + ${springframework.version} + org.springframework spring-beans diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index 610850109..c9f5cf58c 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -65,7 +65,7 @@ com.sap.ai.sdk - core + openai-wrapper com.sap.ai.sdk.foundationmodels @@ -148,6 +148,7 @@ org.springframework spring-core + ${springframework.version} org.springframework @@ -268,6 +269,18 @@ ${skipTests} + + org.apache.maven.plugins + maven-dependency-plugin + + + + com.openai:openai-java-core + + com.sap.ai.sdk:core + + + org.codehaus.mojo license-maven-plugin diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiWrapperController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiWrapperController.java new file mode 100644 index 000000000..352827184 --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OpenAiWrapperController.java @@ -0,0 +1,68 @@ +package com.sap.ai.sdk.app.controllers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.openai.core.ObjectMappers; +import com.sap.ai.sdk.app.services.OpenAiWrapperService; +import javax.annotation.Nonnull; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** Endpoints for OpenAI operations using the official OpenAI Java SDK wrapper. */ +@RestController +@RequestMapping("/openai-wrapper") +class OpenAiWrapperController { + + private final OpenAiWrapperService service; + + OpenAiWrapperController(@Nonnull final OpenAiWrapperService service) { + this.service = service; + } + + /** + * Chat completion using the official OpenAI SDK via SAP AI Core. + * + * @return a chat completion response as JSON string + * @throws JsonProcessingException if serialization fails + */ + @GetMapping(value = "/chatCompletion", produces = MediaType.APPLICATION_JSON_VALUE) + @Nonnull + String chatCompletion() throws JsonProcessingException { + return ObjectMappers.jsonMapper().writeValueAsString(service.chatCompletion()); + } + + /** + * Chat completion with a custom user message. + * + * @param message the user message + * @return a chat completion response as JSON string + * @throws JsonProcessingException if serialization fails + */ + @GetMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE) + @Nonnull + String chat(@RequestParam @Nonnull final String message) throws JsonProcessingException { + return ObjectMappers.jsonMapper().writeValueAsString(service.chatCompletion(message)); + } + + /** + * Chat completion filtering by resource group. + * + * @param resourceGroup the resource group to use + * @return a chat completion response as JSON string + * @throws JsonProcessingException if serialization fails + */ + @GetMapping( + value = "/chatCompletion/{resourceGroup}", + produces = MediaType.APPLICATION_JSON_VALUE) + @Nonnull + String chatCompletionWithResource( + @Nonnull @PathVariable("resourceGroup") final String resourceGroup) + throws JsonProcessingException { + return ObjectMappers.jsonMapper() + .writeValueAsString( + service.chatCompletionWithResource(resourceGroup, "Where is the nearest coffee shop?")); + } +} diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OpenAiWrapperService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OpenAiWrapperService.java new file mode 100644 index 000000000..346b6b11d --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OpenAiWrapperService.java @@ -0,0 +1,84 @@ +package com.sap.ai.sdk.app.services; + +import com.openai.client.OpenAIClient; +import com.openai.models.ChatModel; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; +import com.sap.ai.sdk.foundationmodels.openaiwrapper.OpenAiClientProvider; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * Service class demonstrating the official OpenAI Java SDK wrapper for SAP AI Core. + * + *

Uses {@link OpenAiClientProvider} to get an {@link OpenAIClient} backed by SAP AI Core, then + * interacts with the official SDK types directly. + */ +@Service +@Slf4j +public class OpenAiWrapperService { + + /** + * Chat completion request using the official OpenAI Java SDK via SAP AI Core. + * + * @return the chat completion response + */ + @Nonnull + public ChatCompletion chatCompletion() { + final OpenAIClient client = OpenAiClientProvider.forModel(OpenAiModel.GPT_5); + + final ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_5) + .addSystemMessage("Be a helpful assistant") + .addUserMessage("Hello World! Why is this phrase so famous?") + .build(); + + return client.chat().completions().create(params); + } + + /** + * Chat completion request with streaming disabled, using structured output. + * + * @param userMessage the user message to send + * @return the chat completion response + */ + @Nonnull + public ChatCompletion chatCompletion(@Nonnull final String userMessage) { + final OpenAIClient client = OpenAiClientProvider.forModel(OpenAiModel.GPT_5); + + final ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .model(ChatModel.GPT_5) + .addUserMessage(userMessage) + .build(); + + return client.chat().completions().create(params); + } + + /** + * Chat completion request using the official OpenAI Java SDK via SAP AI Core, filtering by + * resource group. + * + * @param resourceGroup the resource group to use + * @param prompt the prompt to send to the assistant + * @return the chat completion response + */ + @Nonnull + public ChatCompletion chatCompletionWithResource( + @Nonnull final String resourceGroup, @Nonnull final String prompt) { + + final var destination = + new AiCoreService().getInferenceDestination(resourceGroup).forModel(OpenAiModel.GPT_5); + + final OpenAIClient client = OpenAiClientProvider.withCustomDestination(destination); + + final ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder().model(ChatModel.GPT_5).addUserMessage(prompt).build(); + + return client.chat().completions().create(params); + } +}