Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions foundation-models/openai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
</scm>
<properties>
<project.rootdir>${project.basedir}/../../</project.rootdir>
<coverage.complexity>77%</coverage.complexity>
<coverage.line>87%</coverage.line>
<coverage.instruction>85%</coverage.instruction>
<coverage.branch>75%</coverage.branch>
<coverage.method>87%</coverage.method>
<coverage.complexity>73%</coverage.complexity>
<coverage.line>85%</coverage.line>
<coverage.instruction>82%</coverage.instruction>
<coverage.branch>74%</coverage.branch>
<coverage.method>79%</coverage.method>
<coverage.class>91%</coverage.class>
</properties>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionChunk;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionDeleteParams;
import com.openai.models.chat.completions.ChatCompletionDeleted;
import com.openai.models.chat.completions.ChatCompletionListPage;
import com.openai.models.chat.completions.ChatCompletionListParams;
import com.openai.models.chat.completions.ChatCompletionRetrieveParams;
import com.openai.models.chat.completions.ChatCompletionUpdateParams;
import com.openai.models.chat.completions.StructuredChatCompletion;
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
import com.openai.services.blocking.chat.ChatCompletionService;
import com.openai.services.blocking.chat.completions.MessageService;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;

@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class AiCoreChatCompletionService implements ChatCompletionService {

@Delegate(types = PassThroughMethods.class)
private final ChatCompletionService delegate;

private final String deploymentModel;

@Override
@Nonnull
public ChatCompletionService withOptions(
@Nonnull final Consumer<ClientOptions.Builder> consumer) {
return new AiCoreChatCompletionService(delegate.withOptions(consumer), deploymentModel);
}

@Override
@Nonnull
public ChatCompletionService.WithRawResponse withRawResponse() {
throw new UnsupportedOperationException(
"withRawResponse() is not supported by AiCoreResponseService.");
}

@Override
@Nonnull
public ChatCompletion create(@Nonnull final ChatCompletionCreateParams params) {
return create(params, RequestOptions.none());
}

@Override
@Nonnull
public ChatCompletion create(
@Nonnull final ChatCompletionCreateParams params,
@Nonnull final RequestOptions requestOptions) {
throwOnModelMismatch(params.model().asString());
return delegate.create(params, requestOptions);
}
Comment on lines +53 to +60
Copy link
Copy Markdown
Member Author

@rpanackal rpanackal Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limitation 2:

In AiCoreResponseService, we are able to plug in the model ourselves, removing the burden from user having to mention it twice. Once for deployment resolution and once in ChatCompletionCreateParams (otherwise server returns 400).

client = AiCoreOpenAiClient.fromDestination(destination, OpenAiModel.GPT_5)
var params =
        ResponseCreateParams.builder()
            .input("What is the capital of France?")
            .model(ChatModel.GPT_5)                    // Users can leave this out
            .build();
Response response = client.responses().create(params);

The same is not true for AiCoreChatCompletionService. ChatCompletionCreateParams throws right away when model is not declared -> we can not provide any convenience to user by plugging in model downstream.

 final var params =
        ChatCompletionCreateParams.builder()
            .model(ChatModel.GPT_5)                      // -> build() throws without model
            .addUserMessage("Say this is a test")
            .build();

Why not switch to dynamic deployment resolution for model in params ?

The AI Core supports multiple operations for the following endpoints.

"/chat/completions": POST
"/responses": GET, POST
"/responses/{response_id}": GET, DELETE
"/responses/compact": POST

Except for the POST operations there is no model available in payload. So, it becomes unavoidable to not ask a model to resolve deployment url.


@Override
@Nonnull
public <T> StructuredChatCompletion<T> create(
@Nonnull final StructuredChatCompletionCreateParams<T> params) {
return create(params, RequestOptions.none());
}

@Override
@Nonnull
public <T> StructuredChatCompletion<T> create(
@Nonnull final StructuredChatCompletionCreateParams<T> params,
@Nonnull final RequestOptions requestOptions) {
throwOnModelMismatch(params.rawParams().model().asString());
return delegate.create(params, requestOptions);
}

@Override
@Nonnull
public StreamResponse<ChatCompletionChunk> createStreaming(
@Nonnull final ChatCompletionCreateParams params) {
return createStreaming(params, RequestOptions.none());
}

@Override
@Nonnull
public StreamResponse<ChatCompletionChunk> createStreaming(
@Nonnull final ChatCompletionCreateParams params,
@Nonnull final RequestOptions requestOptions) {
throwOnModelMismatch(params.model().asString());
return delegate.createStreaming(params, requestOptions);
}

@Override
@Nonnull
public StreamResponse<ChatCompletionChunk> createStreaming(
@Nonnull final StructuredChatCompletionCreateParams<?> params) {
return createStreaming(params, RequestOptions.none());
}

@Override
@Nonnull
public StreamResponse<ChatCompletionChunk> createStreaming(
@Nonnull final StructuredChatCompletionCreateParams<?> params,
@Nonnull final RequestOptions requestOptions) {
throwOnModelMismatch(params.rawParams().model().asString());
return delegate.createStreaming(params, requestOptions);
}

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 {
ChatCompletionDeleted delete(
ChatCompletionDeleteParams chatCompletionDeleteParams, RequestOptions requestOptions);

ChatCompletionDeleted delete(String completionId);

ChatCompletionDeleted delete(String completionId, ChatCompletionDeleteParams params);

ChatCompletionDeleted delete(
String completionId, ChatCompletionDeleteParams params, RequestOptions requestOptions);

ChatCompletionDeleted delete(String completionId, RequestOptions requestOptions);

ChatCompletionDeleted delete(ChatCompletionDeleteParams params);

ChatCompletionListPage list();

ChatCompletionListPage list(
ChatCompletionListParams chatCompletionListParams, RequestOptions requestOptions);

ChatCompletionListPage list(ChatCompletionListParams params);

ChatCompletionListPage list(RequestOptions requestOptions);

MessageService messages();

ChatCompletion retrieve(
ChatCompletionRetrieveParams chatCompletionRetrieveParams, RequestOptions requestOptions);

ChatCompletion retrieve(String completionId);

ChatCompletion retrieve(String completionId, ChatCompletionRetrieveParams params);

ChatCompletion retrieve(
String completionId, ChatCompletionRetrieveParams params, RequestOptions requestOptions);

ChatCompletion retrieve(String completionId, RequestOptions requestOptions);

ChatCompletion retrieve(ChatCompletionRetrieveParams params);

ChatCompletion update(
ChatCompletionUpdateParams chatCompletionUpdateParams, RequestOptions requestOptions);

ChatCompletion update(String completionId, ChatCompletionUpdateParams params);

ChatCompletion update(
String completionId, ChatCompletionUpdateParams params, RequestOptions requestOptions);

ChatCompletion update(ChatCompletionUpdateParams params);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.openai.core.ClientOptions;
import com.openai.core.http.QueryParams;
import com.openai.services.blocking.ChatService;
import com.openai.services.blocking.chat.ChatCompletionService;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Delegate;

@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class AiCoreChatService implements ChatService {

@Delegate private final ChatService delegate;
private final String deploymentModel;

@Override
@Nonnull
public ChatService withOptions(@Nonnull final Consumer<ClientOptions.Builder> consumer) {
return new AiCoreChatService(delegate.withOptions(consumer), deploymentModel);
}

@Override
@Nonnull
public WithRawResponse withRawResponse() {
throw new UnsupportedOperationException(
"withRawResponse() is not supported for AiCoreChatService");
}

@Override
@Nonnull
public ChatCompletionService completions() {
final var apiVersionQuery = QueryParams.builder().put("api-version", "2024-02-01").build();
final var completions =
delegate.completions().withOptions(builder -> builder.queryParams(apiVersionQuery));
return new AiCoreChatCompletionService(completions, deploymentModel);
}
}
Loading