Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
27b7e3f
Minimal new module setup including spec
rpanackal Mar 3, 2026
a2c9456
Generation partial-success
rpanackal Mar 3, 2026
aed34ae
Remove examples
rpanackal Mar 6, 2026
9e7c90d
Successfully filter by path
rpanackal Mar 10, 2026
a9a299a
Attach spec filter command
rpanackal Mar 10, 2026
eaf74ba
Initial setup
rpanackal Mar 17, 2026
372769e
Successful PoC with OpenAI Models
rpanackal Mar 18, 2026
3726089
Version 1
rpanackal Mar 20, 2026
01c7f61
Stable api
rpanackal Mar 20, 2026
c921a86
Change class name
rpanackal Mar 20, 2026
2491d07
Add tests
rpanackal Mar 20, 2026
c004481
fix dependency analyse issues
rpanackal Mar 20, 2026
fbff9b0
Initial draft untested
rpanackal Mar 21, 2026
68524b2
Second draft
rpanackal Mar 21, 2026
37ea275
Successful E2E
rpanackal Mar 23, 2026
3bda22a
Streaming initial draft
rpanackal Mar 23, 2026
229db60
Streaming E2E with chat completion
rpanackal Mar 24, 2026
7ffe122
isStreaming check simplified
rpanackal Mar 24, 2026
5553e1b
Cleanup PoC and rename module
rpanackal Mar 24, 2026
2a55080
Reduce Javadoc verbosity
rpanackal Mar 24, 2026
4ca8af1
Restrict to `/responses` api
rpanackal Mar 24, 2026
7061cbc
Cleanup comments
rpanackal Mar 24, 2026
6eb57fc
Charles review suggestions
rpanackal Mar 26, 2026
38ced86
Charles review - round 2 suggestions
rpanackal Mar 26, 2026
d8e1902
Add dependency
rpanackal Mar 26, 2026
dae1906
Mark openai dependency optional and new client `@Beta`
rpanackal Mar 26, 2026
88ae43c
Cleanup and no throw on missing model
rpanackal Mar 30, 2026
c995bee
pmd
rpanackal Mar 30, 2026
145f162
Responses API complete
rpanackal Mar 31, 2026
bf29f13
ChatCompletionCreateParams throws without model. Needs rethink client…
rpanackal Apr 1, 2026
96feb25
Cleanup and close with test documenting limitation
rpanackal Apr 1, 2026
2bf90df
Merge branch 'feat/poc-openai-responses-apache' into feat/poc-aicore-…
rpanackal Apr 2, 2026
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
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.openai.core.http.HttpRequest;
import com.openai.core.http.HttpResponse;
import com.openai.errors.OpenAIIoException;
import com.openai.models.ChatModel;
import com.sap.ai.sdk.core.AiCoreService;
import com.sap.ai.sdk.core.DeploymentResolutionException;
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
Expand Down Expand Up @@ -79,26 +80,31 @@ public static OpenAIClient forModel(
@Nonnull final OpenAiModel model, @Nonnull final String resourceGroup) {
final HttpDestination destination =
new AiCoreService().getInferenceDestination(resourceGroup).forModel(model);

return fromDestination(destination);
return fromDestination(destination, model);
}

/**
* Create an OpenAI client from a pre-resolved destination.
*
* @param destination The destination to use.
* @param model The model name for validation.
* @return A configured OpenAI client instance.
*/
@Nonnull
@SuppressWarnings("PMD.CloseResource")
static OpenAIClient fromDestination(@Nonnull final HttpDestination destination) {
static OpenAIClient fromDestination(
@Nonnull final HttpDestination destination, @Nonnull final OpenAiModel model) {
final var baseUrl = destination.getUri().toString();
final var httpClient = new AiCoreHttpClientImpl(destination);

final ClientOptions clientOptions =
final var clientOptions =
ClientOptions.builder().baseUrl(baseUrl).httpClient(httpClient).apiKey("unused").build();

return new OpenAIClientImpl(clientOptions);
final var chatModel =
Optional.ofNullable(model.version())
.map(version -> model.name() + "-" + version)
.orElseGet(model::name);
return new OpenAIClientImplWrapper(
new OpenAIClientImpl(clientOptions), ChatModel.of(chatModel).asString());
}

/**
Expand Down
Loading