Skip to content

feat: SpringAI integration in OpenAI #538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 82 commits into from
Aug 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
9ff262b
first draft
Jonas-Isr Jun 16, 2025
b14d70c
Align with docs
Jonas-Isr Jun 17, 2025
b32b999
Codestyle
Jonas-Isr Jun 17, 2025
32d1bb2
Merge branch 'main' into agent-workflow-examples
Jonas-Isr Jun 17, 2025
5720581
Merge branch 'main' into agent-workflow-examples
Jonas-Isr Jun 19, 2025
64c8b0e
Merge branch 'main' into agent-workflow-examples
Jonas-Isr Jun 24, 2025
98ab1af
Merge branch 'main' into agent-workflow-examples
CharlesDuboisSAP Jul 3, 2025
0e59667
feat: [OpenAI] Spring AI integration
CharlesDuboisSAP Jul 3, 2025
71898c5
test
CharlesDuboisSAP Jul 3, 2025
b056414
Merge branch 'agent-workflow-examples' into openai-springai
CharlesDuboisSAP Jul 3, 2025
025eba1
test
CharlesDuboisSAP Jul 4, 2025
d402c64
Merge branch 'agent-workflow-examples' into openai-springai
CharlesDuboisSAP Jul 4, 2025
7ed6baa
Merge branch 'main' into openai-springai
CharlesDuboisSAP Jul 24, 2025
3b0273b
Fixing errors in OpenAiChatOptions according to "Upgrade to Spring AI…
n-o-u-r-h-a-n Jul 25, 2025
8c2a638
Fixing errors in OpenAiChatOptions according to "Upgrade to Spring AI…
n-o-u-r-h-a-n Jul 25, 2025
8eb5459
Fixing SpringAiAgenticWorkflowService according to "Upgrade to Spring…
n-o-u-r-h-a-n Jul 25, 2025
ab1f795
Implementation of completion and streamChatCompletion in SpringAiOpen…
n-o-u-r-h-a-n Jul 29, 2025
9d4bdcd
Removing a comment
n-o-u-r-h-a-n Jul 29, 2025
72198d9
Removing a comment
n-o-u-r-h-a-n Jul 31, 2025
88904c8
Chat Memory test working.
n-o-u-r-h-a-n Aug 1, 2025
47d1bc0
Formatting for SpringAiOpenAiService.
n-o-u-r-h-a-n Aug 3, 2025
443e464
Removing unneccessary imports in SpringAiOpenAiService.
n-o-u-r-h-a-n Aug 3, 2025
00ba4ad
Updating the toOpenAiRequest in OpenAiChatModel.java.
n-o-u-r-h-a-n Aug 3, 2025
55c85d9
Implementing the new approach
n-o-u-r-h-a-n Aug 4, 2025
56cda93
Editing the approach.
n-o-u-r-h-a-n Aug 5, 2025
0183c83
Fix compilation and format and annotations and javadoc
newtork Aug 5, 2025
02d5f54
Merge remote-tracking branch 'origin/main' into chatcompletion-for-sp…
newtork Aug 5, 2025
ee94941
Remove unrelated code
newtork Aug 5, 2025
dec85d6
implementation hint
newtork Aug 5, 2025
d99dc0e
Formatting
bot-sdk-js Aug 5, 2025
c073c8d
Updating OpenAiChatOptions.java with our Config Object.
n-o-u-r-h-a-n Aug 6, 2025
6ed3a13
Formatting
bot-sdk-js Aug 6, 2025
9f4a403
Passing our Config Object as an input parameter for OpenAiChatOptions()
n-o-u-r-h-a-n Aug 6, 2025
687fe2e
Fixing NullPointerException in toOpenAiRequest method for ToolCallng …
n-o-u-r-h-a-n Aug 6, 2025
efa3831
Adding topK for the Config Class ??
n-o-u-r-h-a-n Aug 6, 2025
817d3cb
Formatting
bot-sdk-js Aug 6, 2025
ad4241a
Update foundation-models/openai/src/main/java/com/sap/ai/sdk/foundati…
n-o-u-r-h-a-n Aug 7, 2025
203eba6
Failing Test of testToolCallingWithoutExecution() in SpringAiOpenAiTe…
n-o-u-r-h-a-n Aug 7, 2025
f7c7ece
Resolving Reviewed Issues.
n-o-u-r-h-a-n Aug 7, 2025
84546c0
Resolving Reviewed Issues.
n-o-u-r-h-a-n Aug 7, 2025
c52720e
--> still having testToolCallingWithoutExecution() in SpringAiOpenAiT…
n-o-u-r-h-a-n Aug 7, 2025
cd3501c
format
n-o-u-r-h-a-n Aug 8, 2025
7f447d7
format
n-o-u-r-h-a-n Aug 8, 2025
9e1760c
Removing wild cards imports
n-o-u-r-h-a-n Aug 8, 2025
222924c
Sucessful build of OpenAi
n-o-u-r-h-a-n Aug 8, 2025
de9ef56
Sucessful build of Spring Boot app.
n-o-u-r-h-a-n Aug 8, 2025
9fb6321
Merge branch 'main' into chatcompletion-for-springopenai
n-o-u-r-h-a-n Aug 8, 2025
9284fe3
Formatting
bot-sdk-js Aug 8, 2025
8d40dfa
Removing this test for now.
n-o-u-r-h-a-n Aug 8, 2025
be51dd3
Fix nullcheck
newtork Aug 8, 2025
d0ea158
Merge remote-tracking branch 'origin/chatcompletion-for-springopenai'…
newtork Aug 8, 2025
3c161c0
Fix unit test
newtork Aug 8, 2025
35d89cb
Merge branch 'main' into chatcompletion-for-springopenai
n-o-u-r-h-a-n Aug 11, 2025
c186aea
chore: Reduce constructor visibility in OpenAI / SpringAI PR (#531)
newtork Aug 12, 2025
75a2361
Replacing config.toolsExecutable with getter-usage + adding tolerate …
n-o-u-r-h-a-n Aug 12, 2025
ddcb88c
Merge remote-tracking branch 'origin/chatcompletion-for-springopenai'…
n-o-u-r-h-a-n Aug 12, 2025
9f27cb4
Merge branch 'main' into chatcompletion-for-springopenai
n-o-u-r-h-a-n Aug 13, 2025
5e4b131
2
n-o-u-r-h-a-n Aug 13, 2025
3cfbd5d
Assistant message wiht tools calls
Aug 13, 2025
ba3f337
unit test
CharlesDuboisSAP Aug 13, 2025
05f75ea
Creating OpenAiChatModelTest.java and updating the controller and ind…
n-o-u-r-h-a-n Aug 13, 2025
e1f8e46
Merge branch 'main' into chatcompletion-for-springopenai2
n-o-u-r-h-a-n Aug 13, 2025
2cda676
Formatting
bot-sdk-js Aug 13, 2025
c427c17
formatting
n-o-u-r-h-a-n Aug 14, 2025
3c02160
Merge remote-tracking branch 'origin/chatcompletion-for-springopenai2…
n-o-u-r-h-a-n Aug 14, 2025
b1a68cc
Finishing the tests
n-o-u-r-h-a-n Aug 14, 2025
9b2b5bf
Merge branch 'main' into chatcompletion-for-springopenai2
CharlesDuboisSAP Aug 15, 2025
f9a719e
Added more options and metadata
CharlesDuboisSAP Aug 15, 2025
074d794
Formatting
bot-sdk-js Aug 15, 2025
554e45a
Fixing Format/Style (Minor).
n-o-u-r-h-a-n Aug 15, 2025
47ca557
Merge remote-tracking branch 'origin/chatcompletion-for-springopenai2…
n-o-u-r-h-a-n Aug 15, 2025
43bc1fc
Merge branch 'main' into chatcompletion-for-springopenai2
n-o-u-r-h-a-n Aug 15, 2025
7ee82a7
Fixing Format/Style (Minor).
n-o-u-r-h-a-n Aug 15, 2025
50043e0
Updating the Release notes according to integrating SpringAI with our…
n-o-u-r-h-a-n Aug 15, 2025
c9fb23f
Update docs/release_notes.md
n-o-u-r-h-a-n Aug 18, 2025
6abcdce
Handling JsonProcessingException
n-o-u-r-h-a-n Aug 18, 2025
2e7860c
Merge branch 'main' into chatcompletion-for-springopenai2
n-o-u-r-h-a-n Aug 18, 2025
613d0df
Handling JsonProcessingException
n-o-u-r-h-a-n Aug 18, 2025
1154537
Handling JsonProcessingException
n-o-u-r-h-a-n Aug 18, 2025
bfe6202
Formatting
bot-sdk-js Aug 18, 2025
f91d32b
Merge branch 'main' into chatcompletion-for-springopenai2
CharlesDuboisSAP Aug 19, 2025
3b5bad8
protected extractOptions
CharlesDuboisSAP Aug 19, 2025
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
5 changes: 4 additions & 1 deletion docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

### ✨ New Functionality

- Extend `OpenAiClientException` and `OrchestrationClientException` to retrieve error diagnostics information received from remote service.
- Extend `OpenAiClientException` and `OrchestrationClientException` to retrieve error diagnostics information received
from remote service.
New available accessors for troubleshooting: `getErrorResponse()`, `getHttpResponse()` and, `getHttpRequest()`.
Please note: depending on the error response, these methods may return `null` if the information is not available.
- [OpenAI] Added new models for `OpenAiModel`: `GPT_5`, `GPT_5_MINI` and `GPT_5_NANO`.
Expand All @@ -22,6 +23,8 @@
`OrchestrationAiModel.GEMINI_1_5_FLASH`
- Replacement are `GEMINI_2_5_PRO` and `GEMINI_2_5_FLASH`.
- [Orchestration] Deprecated `OrchestrationAiModel.IBM_GRANITE_13B_CHAT` with no replacement.
- [OpenAI] [Introduced SpringAI integration with our OpenAI client.](https://sap.github.io/ai-sdk/docs/java/spring-ai/openai)
- Added `OpenAiChatModel`

### 📈 Improvements

Expand Down
23 changes: 17 additions & 6 deletions foundation-models/openai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@
</scm>
<properties>
<project.rootdir>${project.basedir}/../../</project.rootdir>
<coverage.complexity>72%</coverage.complexity>
<coverage.line>80%</coverage.line>
<coverage.instruction>76%</coverage.instruction>
<coverage.branch>70%</coverage.branch>
<coverage.method>83%</coverage.method>
<coverage.class>84%</coverage.class>
<coverage.complexity>81%</coverage.complexity>
<coverage.line>91%</coverage.line>
<coverage.instruction>88%</coverage.instruction>
<coverage.branch>79%</coverage.branch>
<coverage.method>90%</coverage.method>
<coverage.class>92%</coverage.class>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -112,6 +112,11 @@
<artifactId>spring-ai-model</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<!-- scope "provided" -->
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down Expand Up @@ -149,6 +154,12 @@
<artifactId>javaparser-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;

Expand Down Expand Up @@ -46,6 +47,18 @@ static OpenAiAssistantMessage assistant(@Nonnull final String message) {
return new OpenAiAssistantMessage(message);
}

/**
* A convenience method to create an assistant message.
*
* @param toolCalls tool calls to associate with the message.
* @return the assistant message.
*/
@Nonnull
static OpenAiAssistantMessage assistant(@Nonnull final List<OpenAiToolCall> toolCalls) {
return new OpenAiAssistantMessage(
new OpenAiMessageContent(List.of()), new ArrayList<>(toolCalls));
}

/**
* A convenience method to create a system message.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;
import javax.annotation.Nonnull;

/**
* Represents a tool called by an OpenAI model.
*
* @since 1.6.0
*/
@Beta
public sealed interface OpenAiToolCall permits OpenAiFunctionCall {}
public sealed interface OpenAiToolCall permits OpenAiFunctionCall {
/**
* Creates a new instance of {@link OpenAiToolCall}.
*
* @param id The unique identifier for the tool call.
* @param name The name of the tool to be called.
* @param arguments The arguments for the tool call, encoded as a JSON string.
* @return A new instance of {@link OpenAiToolCall}.
* @since 1.10.0
*/
@Nonnull
static OpenAiToolCall function(
@Nonnull final String id, @Nonnull final String name, @Nonnull final String arguments) {
return new OpenAiFunctionCall(id, name, arguments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.sap.ai.sdk.foundationmodels.openai.spring;

import static org.springframework.ai.model.tool.ToolCallingChatOptions.isInternalToolExecutionEnabled;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionDelta;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionRequest;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionResponse;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiMessage;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiToolCall;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionMessageToolCall;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionResponseChoicesInner;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.FunctionObject;
import io.vavr.control.Option;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.AssistantMessage.ToolCall;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.ToolResponseMessage;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.DefaultToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import reactor.core.publisher.Flux;

/**
* OpenAI Chat Model implementation that interacts with the OpenAI API to generate chat completions.
*/
@Slf4j
@RequiredArgsConstructor
public class OpenAiChatModel implements ChatModel {

private final OpenAiClient client;

@Nonnull
private final DefaultToolCallingManager toolCallingManager =
DefaultToolCallingManager.builder().build();

@Override
@Nonnull
public ChatResponse call(@Nonnull final Prompt prompt) {
val options = prompt.getOptions();
var request = new OpenAiChatCompletionRequest(extractMessages(prompt));

if (options != null) {
request = extractOptions(request, options);
}
if ((options instanceof ToolCallingChatOptions toolOptions)) {
request = request.withTools(extractTools(toolOptions));
}

val result = client.chatCompletion(request);
val response = new ChatResponse(toGenerations(result));

if (options != null && isInternalToolExecutionEnabled(options) && response.hasToolCalls()) {
val toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response);
// Send the tool execution result back to the model.
return call(new Prompt(toolExecutionResult.conversationHistory(), options));
}
return response;
}

@Override
@Nonnull
public Flux<ChatResponse> stream(@Nonnull final Prompt prompt) {
val options = prompt.getOptions();
var request = new OpenAiChatCompletionRequest(extractMessages(prompt));

if (options != null) {
request = extractOptions(request, options);
}
if ((options instanceof ToolCallingChatOptions toolOptions)) {
request = request.withTools(extractTools(toolOptions));
}

val stream = client.streamChatCompletionDeltas(request);
final Flux<OpenAiChatCompletionDelta> flux =
Flux.generate(
stream::iterator,
(iterator, sink) -> {
if (iterator.hasNext()) {
sink.next(iterator.next());
} else {
sink.complete();
}
return iterator;
});
return flux.map(
delta -> {
val assistantMessage = new AssistantMessage(delta.getDeltaContent(), Map.of());
val metadata =
ChatGenerationMetadata.builder().finishReason(delta.getFinishReason()).build();
return new ChatResponse(List.of(new Generation(assistantMessage, metadata)));
});
}

private static List<OpenAiMessage> extractMessages(final Prompt prompt) {
final List<OpenAiMessage> result = new ArrayList<>();
for (final Message message : prompt.getInstructions()) {
switch (message.getMessageType()) {
case USER -> Option.of(message.getText()).peek(t -> result.add(OpenAiMessage.user(t)));
case SYSTEM -> Option.of(message.getText()).peek(t -> result.add(OpenAiMessage.system(t)));
case ASSISTANT -> addAssistantMessage(result, (AssistantMessage) message);
case TOOL -> addToolMessages(result, (ToolResponseMessage) message);
}
}
return result;
}

private static void addAssistantMessage(
final List<OpenAiMessage> result, final AssistantMessage message) {
if (message.getText() != null) {
result.add(OpenAiMessage.assistant(message.getText()));
return;
}
final Function<ToolCall, OpenAiToolCall> callTranslate =
toolCall -> OpenAiToolCall.function(toolCall.id(), toolCall.name(), toolCall.arguments());
val calls = message.getToolCalls().stream().map(callTranslate).toList();
result.add(OpenAiMessage.assistant(calls));
}

private static void addToolMessages(
final List<OpenAiMessage> result, final ToolResponseMessage message) {
for (final ToolResponseMessage.ToolResponse response : message.getResponses()) {
result.add(OpenAiMessage.tool(response.responseData(), response.id()));
}
}

@Nonnull
private static List<Generation> toGenerations(
@Nonnull final OpenAiChatCompletionResponse result) {
return result.getOriginalResponse().getChoices().stream()
.map(OpenAiChatModel::toGeneration)
.toList();
}

@Nonnull
private static Generation toGeneration(
@Nonnull final CreateChatCompletionResponseChoicesInner choice) {
val metadata =
ChatGenerationMetadata.builder().finishReason(choice.getFinishReason().getValue());
metadata.metadata("index", choice.getIndex());
if (choice.getLogprobs() != null && !choice.getLogprobs().getContent().isEmpty()) {
metadata.metadata("logprobs", choice.getLogprobs().getContent());
}
val message = choice.getMessage();
val calls = new ArrayList<ToolCall>();
if (message.getToolCalls() != null) {
for (final ChatCompletionMessageToolCall c : message.getToolCalls()) {
val fnc = c.getFunction();
calls.add(
new ToolCall(c.getId(), c.getType().getValue(), fnc.getName(), fnc.getArguments()));
}
}

val assistantMessage = new AssistantMessage(message.getContent(), Map.of(), calls);
return new Generation(assistantMessage, metadata.build());
}

/**
* Adds options to the request.
*
* @param request the request to modify
* @param options the options to extract
* @return the modified request with options applied
*/
@Nonnull
protected static OpenAiChatCompletionRequest extractOptions(
@Nonnull OpenAiChatCompletionRequest request, @Nonnull final ChatOptions options) {
request = request.withStop(options.getStopSequences()).withMaxTokens(options.getMaxTokens());
if (options.getTemperature() != null) {
request = request.withTemperature(BigDecimal.valueOf(options.getTemperature()));
}
if (options.getTopP() != null) {
request = request.withTopP(BigDecimal.valueOf(options.getTopP()));
}
if (options.getPresencePenalty() != null) {
request = request.withPresencePenalty(BigDecimal.valueOf(options.getPresencePenalty()));
}
if (options.getFrequencyPenalty() != null) {
request = request.withFrequencyPenalty(BigDecimal.valueOf(options.getFrequencyPenalty()));
}
return request;
}

private static List<ChatCompletionTool> extractTools(final ToolCallingChatOptions options) {
val tools = new ArrayList<ChatCompletionTool>();
for (val toolCallback : options.getToolCallbacks()) {
val toolDefinition = toolCallback.getToolDefinition();
try {
final Map<String, Object> params =
new ObjectMapper().readValue(toolDefinition.inputSchema(), new TypeReference<>() {});
val toolType = ChatCompletionTool.TypeEnum.FUNCTION;
val toolFunction =
new FunctionObject()
.name(toolDefinition.name())
.description(toolDefinition.description())
.parameters(params);
val tool = new ChatCompletionTool().type(toolType).function(toolFunction);
tools.add(tool);
} catch (JsonProcessingException e) {
log.warn("Failed to add tool to the chat request: {}", e.getMessage());
}
}
return tools;
}
}
Loading