diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml
index f08849fb2..d3b240bdf 100644
--- a/foundation-models/openai/pom.xml
+++ b/foundation-models/openai/pom.xml
@@ -38,11 +38,11 @@
${project.basedir}/../../
- 77%
- 87%
- 85%
+ 75%
+ 86%
+ 84%
75%
- 87%
+ 82%
91%
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
index ce4bdf487..efd62914c 100644
--- 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
@@ -1,8 +1,6 @@
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;
@@ -10,6 +8,8 @@
import com.openai.core.http.HttpRequest;
import com.openai.core.http.HttpResponse;
import com.openai.errors.OpenAIIoException;
+import com.openai.services.blocking.ResponseService;
+import com.openai.services.blocking.ResponseServiceImpl;
import com.sap.ai.sdk.core.AiCoreService;
import com.sap.ai.sdk.core.DeploymentResolutionException;
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
@@ -48,7 +48,7 @@
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Beta
-public final class AiCoreOpenAiClient {
+public class AiCoreOpenAiClient {
private static final String DEFAULT_RESOURCE_GROUP = "default";
@@ -61,8 +61,8 @@ public final class AiCoreOpenAiClient {
* @throws DeploymentResolutionException If no running deployment is found for the model.
*/
@Nonnull
- public static OpenAIClient forModel(@Nonnull final OpenAiModel model) {
- return forModel(model, DEFAULT_RESOURCE_GROUP);
+ public static ResponseService responses(@Nonnull final OpenAiModel model) {
+ return responses(model, DEFAULT_RESOURCE_GROUP);
}
/**
@@ -75,30 +75,27 @@ public static OpenAIClient forModel(@Nonnull final OpenAiModel model) {
* @throws DeploymentResolutionException If no running deployment is found for the model.
*/
@Nonnull
- public static OpenAIClient forModel(
+ public static ResponseService responses(
@Nonnull final OpenAiModel model, @Nonnull final String resourceGroup) {
final HttpDestination destination =
new AiCoreService().getInferenceDestination(resourceGroup).forModel(model);
-
- return fromDestination(destination);
+ return responses(model, 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);
+ static ResponseService responses(
+ @Nonnull final OpenAiModel model, @Nonnull final HttpDestination destination) {
+ final var clientOptions =
+ ClientOptions.builder()
+ .baseUrl(destination.getUri().toString())
+ .httpClient(new AiCoreHttpClientImpl(destination))
+ .apiKey("unused")
+ .build();
+ final var chatModel =
+ Optional.ofNullable(model.version())
+ .map(version -> model.name() + "-" + version)
+ .orElseGet(model::name);
+ return new AiCoreResponseService(new ResponseServiceImpl(clientOptions), chatModel);
}
/**
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java
new file mode 100644
index 000000000..090aa639a
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/AiCoreResponseService.java
@@ -0,0 +1,186 @@
+package com.sap.ai.sdk.foundationmodels.openai;
+
+import com.openai.core.ClientOptions;
+import com.openai.core.RequestOptions;
+import com.openai.core.http.StreamResponse;
+import com.openai.models.ChatModel;
+import com.openai.models.ResponsesModel;
+import com.openai.models.responses.CompactedResponse;
+import com.openai.models.responses.Response;
+import com.openai.models.responses.ResponseCancelParams;
+import com.openai.models.responses.ResponseCompactParams;
+import com.openai.models.responses.ResponseCreateParams;
+import com.openai.models.responses.ResponseDeleteParams;
+import com.openai.models.responses.ResponseRetrieveParams;
+import com.openai.models.responses.ResponseStreamEvent;
+import com.openai.models.responses.StructuredResponse;
+import com.openai.models.responses.StructuredResponseCreateParams;
+import com.openai.services.blocking.ResponseService;
+import com.openai.services.blocking.responses.InputItemService;
+import com.openai.services.blocking.responses.InputTokenService;
+import java.util.function.Consumer;
+import javax.annotation.Nonnull;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
+class AiCoreResponseService implements ResponseService {
+
+ @Delegate(types = PassThroughMethods.class)
+ private final ResponseService delegate;
+
+ private final String deploymentModel;
+
+ @Override
+ @Nonnull
+ public ResponseService withOptions(@Nonnull final Consumer modifier) {
+ return new AiCoreResponseService(delegate.withOptions(modifier), deploymentModel);
+ }
+
+ @Override
+ @Nonnull
+ public ResponseService.WithRawResponse withRawResponse() {
+ throw new UnsupportedOperationException(
+ "withRawResponse() is not supported by AiCoreResponseService.");
+ }
+
+ @Override
+ @Nonnull
+ public Response create(@Nonnull final ResponseCreateParams params) {
+ return create(params, RequestOptions.none());
+ }
+
+ @Override
+ @Nonnull
+ public Response create(
+ @Nonnull final ResponseCreateParams params, @Nonnull final RequestOptions requestOptions) {
+ return delegate.create(useDeploymentModel(params), requestOptions);
+ }
+
+ @Override
+ @Nonnull
+ public StructuredResponse create(@Nonnull final StructuredResponseCreateParams params) {
+ return create(params, RequestOptions.none());
+ }
+
+ @Override
+ @Nonnull
+ public StructuredResponse create(
+ @Nonnull final StructuredResponseCreateParams params,
+ @Nonnull final RequestOptions requestOptions) {
+ return delegate.create(useDeploymentModel(params), requestOptions);
+ }
+
+ @Override
+ @Nonnull
+ public StreamResponse createStreaming(
+ @Nonnull final ResponseCreateParams params) {
+ return createStreaming(params, RequestOptions.none());
+ }
+
+ @Override
+ @Nonnull
+ public StreamResponse createStreaming(
+ @Nonnull final ResponseCreateParams params, @Nonnull final RequestOptions requestOptions) {
+ return delegate.createStreaming(useDeploymentModel(params), requestOptions);
+ }
+
+ @Override
+ @Nonnull
+ public StreamResponse createStreaming(
+ @Nonnull final StructuredResponseCreateParams> params) {
+ return createStreaming(params, RequestOptions.none());
+ }
+
+ @Override
+ @Nonnull
+ public StreamResponse createStreaming(
+ @Nonnull final StructuredResponseCreateParams> params,
+ @Nonnull final RequestOptions requestOptions) {
+ return delegate.createStreaming(useDeploymentModel(params), requestOptions);
+ }
+
+ @Override
+ @Nonnull
+ public CompactedResponse compact(@Nonnull final ResponseCompactParams params) {
+ return compact(params, RequestOptions.none());
+ }
+
+ @Override
+ @Nonnull
+ public CompactedResponse compact(
+ @Nonnull final ResponseCompactParams params, @Nonnull final RequestOptions requestOptions) {
+ return delegate.compact(useDeploymentModel(params), requestOptions);
+ }
+
+ @Nonnull
+ private StructuredResponseCreateParams useDeploymentModel(
+ @Nonnull final StructuredResponseCreateParams params) {
+ final ResponseCreateParams validated = useDeploymentModel(params.rawParams());
+ return new StructuredResponseCreateParams<>(params.responseType(), validated);
+ }
+
+ @Nonnull
+ private ResponseCreateParams useDeploymentModel(@Nonnull final ResponseCreateParams params) {
+ final var givenModel =
+ params.model().map(ResponsesModel::asChat).map(ChatModel::asString).orElse(null);
+
+ if (givenModel == null) {
+ return params.toBuilder().model(deploymentModel).build();
+ }
+
+ throwOnModelMismatch(givenModel);
+ return params;
+ }
+
+ @Nonnull
+ private ResponseCompactParams useDeploymentModel(@Nonnull final ResponseCompactParams params) {
+ final String givenModel =
+ params.model().map(ResponseCompactParams.Model::asString).orElse(null);
+
+ if (givenModel == null) {
+ return params.toBuilder().model(deploymentModel).build();
+ }
+
+ throwOnModelMismatch(givenModel);
+ return params;
+ }
+
+ private void throwOnModelMismatch(@Nonnull final String givenModel) {
+ if (!deploymentModel.equals(givenModel)) {
+ throw new IllegalArgumentException(
+ """
+ Model mismatch:
+ Expected : '%s' (configured via forModel())
+ Actual : '%s' (set in request parameters)
+ Fix: Either remove the model from the request parameters, \
+ or use forModel("%s") when creating the client.\
+ """
+ .formatted(deploymentModel, givenModel, givenModel));
+ }
+ }
+
+ private interface PassThroughMethods {
+ InputItemService inputItems();
+
+ InputTokenService inputTokens();
+
+ Response retrieve(ResponseRetrieveParams params, RequestOptions requestOptions);
+
+ Response retrieve(ResponseRetrieveParams params);
+
+ StreamResponse retrieveStreaming(
+ ResponseRetrieveParams params, RequestOptions requestOptions);
+
+ StreamResponse retrieveStreaming(ResponseRetrieveParams params);
+
+ void delete(ResponseDeleteParams params, RequestOptions requestOptions);
+
+ void delete(ResponseDeleteParams params);
+
+ Response cancel(ResponseCancelParams params, RequestOptions requestOptions);
+
+ Response cancel(ResponseCancelParams params);
+ }
+}
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
index d2741ee34..b0a68342b 100644
--- 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
@@ -1,20 +1,19 @@
package com.sap.ai.sdk.foundationmodels.openai;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
-import com.openai.client.OpenAIClient;
-import com.openai.core.http.QueryParams;
import com.openai.models.ChatModel;
-import com.openai.models.chat.completions.ChatCompletion;
-import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStatus;
+import com.openai.services.blocking.ResponseService;
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 com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import javax.annotation.Nonnull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -23,13 +22,13 @@
@WireMockTest
class AiCoreOpenAiClientTest {
- private OpenAIClient client;
+ private ResponseService responseClient;
@BeforeEach
void setup(@Nonnull final WireMockRuntimeInfo server) {
- final var destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build();
- client = AiCoreOpenAiClient.fromDestination(destination);
-
+ final HttpDestination destination =
+ DefaultHttpDestination.builder(server.getHttpBaseUrl()).build();
+ responseClient = AiCoreOpenAiClient.responses(OpenAiModel.GPT_5, destination);
ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED);
}
@@ -47,22 +46,46 @@ void testResponseServiceSuccessWithMatchingModel() {
.model(ChatModel.GPT_5)
.build();
- final Response response = client.responses().create(params);
+ final Response response = responseClient.create(params);
assertThat(response).isNotNull();
assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED);
}
@Test
- void testChatCompletionServiceSuccessWithMatchingModel() {
+ void testResponseServiceSuccessWithoutModel() {
final var params =
- ChatCompletionCreateParams.builder()
- .model(ChatModel.GPT_5)
- .addUserMessage("Say this is a test")
- .additionalQueryParams(QueryParams.builder().put("api-version", "2024-02-01").build())
- .build();
+ ResponseCreateParams.builder().input("What is the capital of France?").build();
+ final Response response = responseClient.create(params);
- final ChatCompletion response = client.chat().completions().create(params);
assertThat(response).isNotNull();
+ assertThat(response.status().orElseThrow()).isEqualTo(ResponseStatus.COMPLETED);
+ }
+
+ @Test
+ void testResponseServiceFailsWithModelMismatch() {
+ final var params =
+ ResponseCreateParams.builder()
+ .input("What is the capital of France?")
+ .model(ChatModel.GPT_4) // Different from client's expected model "gpt-5"
+ .build();
+
+ assertThatThrownBy(() -> responseClient.create(params))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Model mismatch")
+ .hasMessageContaining("gpt-5")
+ .hasMessageContaining("gpt-4");
+ }
+
+ @Test
+ void testResponseServiceWithOptions() {
+ final var modifiedService =
+ responseClient.withOptions(
+ builder -> {
+ // Modify some option
+ builder.putHeader("X-Custom-Header", "test-value");
+ });
+
+ assertThat(modifiedService).isInstanceOf(AiCoreResponseService.class);
}
}
diff --git a/foundation-models/openai/src/test/resources/mappings/chatCompletion.json b/foundation-models/openai/src/test/resources/mappings/chatCompletion.json
deleted file mode 100644
index bb5358a84..000000000
--- a/foundation-models/openai/src/test/resources/mappings/chatCompletion.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "request": {
- "method": "POST",
- "urlPattern": "/chat/completions\\?api-version=2024-02-01",
- "bodyPatterns": [
- {
- "equalToJson": {
- "messages": [
- {
- "content": "Say this is a test",
- "role": "user"
- }
- ],
- "model": "gpt-5"
- }
- }
- ]
- },
- "response": {
- "status": 200,
- "headers": {
- "Content-Type": "application/json",
- "x-request-id": "f181d24e-f41e-9396-a195-6d1334bfe952",
- "ai-inference-id": "f181d24e-f41e-9396-a195-6d1334bfe952",
- "x-upstream-service-time": "3177"
- },
- "jsonBody": {
- "choices": [
- {
- "content_filter_results": {
- "hate": {
- "filtered": false,
- "severity": "safe"
- },
- "self_harm": {
- "filtered": false,
- "severity": "safe"
- },
- "sexual": {
- "filtered": false,
- "severity": "safe"
- },
- "violence": {
- "filtered": false,
- "severity": "safe"
- }
- },
- "finish_reason": "stop",
- "index": 0,
- "logprobs": null,
- "message": {
- "annotations": [],
- "content": "This is a test.",
- "refusal": null,
- "role": "assistant"
- }
- }
- ],
- "created": 1775053782,
- "id": "chatcmpl-DPqreavBOHfKfV0orguq4jK5Gbmmh",
- "model": "gpt-5-2025-08-07",
- "object": "chat.completion",
- "prompt_filter_results": [
- {
- "content_filter_results": {
- "hate": {
- "filtered": false,
- "severity": "safe"
- },
- "self_harm": {
- "filtered": false,
- "severity": "safe"
- },
- "sexual": {
- "filtered": false,
- "severity": "safe"
- },
- "violence": {
- "filtered": false,
- "severity": "safe"
- }
- },
- "prompt_index": 0
- }
- ],
- "system_fingerprint": null,
- "usage": {
- "completion_tokens": 271,
- "completion_tokens_details": {
- "accepted_prediction_tokens": 0,
- "audio_tokens": 0,
- "reasoning_tokens": 256,
- "rejected_prediction_tokens": 0
- },
- "prompt_tokens": 11,
- "prompt_tokens_details": {
- "audio_tokens": 0,
- "cached_tokens": 0
- },
- "total_tokens": 282
- }
- }
- }
-}
\ No newline at end of file
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
index 243055d67..0c538e7b3 100644
--- 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
@@ -2,16 +2,12 @@
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.openai.services.blocking.ResponseService;
import com.sap.ai.sdk.foundationmodels.openai.AiCoreOpenAiClient;
import javax.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;
@@ -23,7 +19,8 @@
@Slf4j
public class AiCoreOpenAiService {
- private static final OpenAIClient CLIENT = AiCoreOpenAiClient.forModel(GPT_5, "ai-sdk-java-e2e");
+ private static final ResponseService RESPONSE_CLIENT =
+ AiCoreOpenAiClient.responses(GPT_5, "ai-sdk-java-e2e");
/**
* Create a simple response using the Responses API
@@ -35,7 +32,7 @@ public class AiCoreOpenAiService {
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);
+ return RESPONSE_CLIENT.create(params);
}
/**
@@ -49,8 +46,8 @@ public Response createResponse(@Nonnull final String input) {
public Response retrieveResponse(@Nonnull final String input) {
// Create a non-persistent response with store=false
val params = ResponseCreateParams.builder().input(input).model(ChatModel.GPT_5).build();
- val createResponse = CLIENT.responses().create(params);
- return CLIENT.responses().retrieve(createResponse.id());
+ val createResponse = RESPONSE_CLIENT.create(params);
+ return RESPONSE_CLIENT.retrieve(createResponse.id());
}
/**
@@ -64,42 +61,6 @@ public StreamResponse createStreamingResponse(@Nonnull fina
// Create a non-persistent response with store=false
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);
+ return RESPONSE_CLIENT.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
index 99548e00f..d541f4bd7 100644
--- 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
@@ -51,28 +51,4 @@ void testCreateStreamingResponse() {
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();
- }
- }
}