Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
20a485c
migrate spring-ai to v2
vladimir-a-sap Aug 27, 2026
0c9fbe8
Merge branch 'main' into spring-ai-v2
vladimir-a-sap Aug 27, 2026
e168629
improve test coverage
vladimir-a-sap Aug 28, 2026
410a2b1
implement orchestration chat options builder and add tests
vladimir-a-sap Aug 28, 2026
196295d
remove unnecessary comments
vladimir-a-sap Aug 28, 2026
1c3f02a
close semantics gaps related to api migration
vladimir-a-sap Aug 28, 2026
0ae5a5f
Merge branch 'main' into spring-ai-v2
vladimir-a-sap Aug 28, 2026
373bff0
update release notes
vladimir-a-sap Aug 28, 2026
befaa00
Merge branch 'spring-ai-v2' of github.com:SAP/ai-sdk-java into spring…
vladimir-a-sap Aug 28, 2026
940d610
revert dependabot ignore for spring ai
vladimir-a-sap Aug 28, 2026
8ba7ab5
fix "since" version in docs
vladimir-a-sap Aug 28, 2026
3448b10
fix e2e tests
vladimir-a-sap Sep 2, 2026
f775900
fix e2e tests
vladimir-a-sap Sep 2, 2026
cce6719
polish the implementation
vladimir-a-sap Sep 3, 2026
27dfa3d
polish the implementation
vladimir-a-sap Sep 3, 2026
0811d9f
Merge branch 'main' into spring-ai-v2
vladimir-a-sap Sep 3, 2026
0217acd
fix pmd issue
vladimir-a-sap Sep 3, 2026
23d59fe
Formatting
bot-sdk-js Sep 3, 2026
a1d0ccd
refactor tool calling to comply with new spring ai approach
vladimir-a-sap Sep 3, 2026
0ce3c33
refactor tool calling to comply with new spring ai approach
vladimir-a-sap Sep 3, 2026
c1acf2e
add spring ai v2 migration guidelines
vladimir-a-sap Sep 4, 2026
38ec195
Merge branch 'main' into spring-ai-v2
vladimir-a-sap Sep 4, 2026
622f153
polish the code
vladimir-a-sap Sep 4, 2026
3816932
polish the code
vladimir-a-sap Sep 4, 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
11 changes: 0 additions & 11 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,6 @@ updates:
prefix: 'chore: [DevOps] '
cooldown:
default-days: 4
ignore:
- dependency-name: "com.fasterxml.jackson.*:*"
versions: [ ">=3.0.0" ]
- dependency-name: "tools.jackson.*:*"
versions: [ ">=3.0.0" ]
- dependency-name: "com.github.victools:jsonschema-generator"
versions: [ ">=5.0.0" ]
- dependency-name: "com.github.victools:jsonschema-module-jackson"
versions: [ ">=5.0.0" ]
- dependency-name: "org.springframework.ai:spring-ai-bom"
versions: [ ">=2.0.0" ]
groups:
production-minor-patch:
dependency-type: "production"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
run: wget -qO- -S localhost:8080

- name: "Slack Notification"
if: failure()
if: github.ref_name == 'main' && failure()
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
Expand Down
63 changes: 63 additions & 0 deletions docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,69 @@
### 🔧 Compatibility Notes

- [RPT] SAP-RPT was updated to the newer 1.6.0 API
- [Orchestration] Spring AI support was upgraded to version `2.0.1`

#### Spring AI 2.0.1 Migration Guide

If you use the Spring AI integration (`OrchestrationChatModel`, `OpenAiChatModel`) together with tool calling, the following changes are required:

**Tool execution — use `ChatClient` instead of `isInternalToolExecutionEnabled`**

The `isInternalToolExecutionEnabled` flag has been removed in Spring AI 2.0.1.
To execute tools automatically, route the call through `ChatClient` (which wires in `ToolCallingAdvisor`):

```java
// Before
options.setInternalToolExecutionEnabled(true);
chatModel.call(prompt);

// After
ChatClient.builder(chatModel).build().prompt(prompt).call().chatResponse();
```
Comment on lines +23 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
```java
// Before
options.setInternalToolExecutionEnabled(true);
chatModel.call(prompt);
// After
ChatClient.builder(chatModel).build().prompt(prompt).call().chatResponse();
```
```diff
-options.setInternalToolExecutionEnabled(true);
-chatModel.call(prompt);
+ChatClient.builder(chatModel).build().prompt(prompt).call().chatResponse();
Diff is the right markdown format for those code samples


To receive raw tool calls without execution (e.g. to forward them to a client), call the model directly as before — no change needed there.

**Immutable options — use `.mutate().build()` to configure per-request options**

`OrchestrationChatOptions` and `DefaultToolCallingChatOptions` are now immutable.
Use the builder to set per-request options such as tool callbacks:

```java
// Before
OrchestrationChatOptions options = new OrchestrationChatOptions(config);
options.setToolCallbacks(...); // no longer available

// After
OrchestrationChatOptions options = new OrchestrationChatOptions(config)
.mutate()
.toolCallbacks(ToolCallbacks.from(new MyTool()))
.build();
```

**Dependency updates**

The `spring-ai-autoconfigure-mcp-client` artifact was split in Spring AI 2.0.1.
If you include MCP client autoconfiguration, replace:

```xml
<!-- Remove -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-client</artifactId>
</dependency>

<!-- Add -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-mcp-client-common</artifactId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You mention adding those dependencies but you didn't add them to the spring app, are they really needed?

<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-annotations</artifactId>
<version>2.0.1</version>
</dependency>
```

### ✨ New Functionality

Expand Down
4 changes: 4 additions & 0 deletions foundation-models/openai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@
name -> {
final Function<String, Object> exec =
s -> function.apply(deserializeArgument(inputClass, s));
final var schema = GENERATOR.generateSchema(inputClass);
final var jackson3Schema = GENERATOR.generateSchema(inputClass);
final ObjectNode schema;
try {
schema = (ObjectNode) JACKSON.readTree(jackson3Schema.toString());
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to parse generated JSON schema", e);
}
return new OpenAiTool(name, exec, schema, null, null);
};
}
Expand Down Expand Up @@ -145,7 +151,7 @@

private static SchemaGenerator createSchemaGenerator() {
final var module =
new JacksonModule(

Check warning on line 154 in foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java

View workflow job for this annotation

GitHub Actions / continuous-integration

com.github.victools.jsonschema.module.jackson.JacksonModule in com.github.victools.jsonschema.module.jackson has been deprecated and marked for removal

Check warning on line 154 in foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java

View workflow job for this annotation

GitHub Actions / end-to-end-tests (canary)

com.github.victools.jsonschema.module.jackson.JacksonModule in com.github.victools.jsonschema.module.jackson has been deprecated and marked for removal
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER);
return new SchemaGenerator(
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
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;
Expand Down Expand Up @@ -35,7 +33,7 @@
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.DefaultToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import reactor.core.publisher.Flux;

Expand All @@ -49,8 +47,10 @@ public class OpenAiChatModel implements ChatModel {
private final OpenAiClient client;

@Nonnull
private final DefaultToolCallingManager toolCallingManager =
DefaultToolCallingManager.builder().build();
@Override
public ChatOptions getOptions() {
return DefaultToolCallingChatOptions.builder().toolCallbacks(List.of()).build();
}

@Override
@Nonnull
Expand All @@ -66,18 +66,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) {
}

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

if (options != null && isInternalToolExecutionEnabled(options) && response.hasToolCalls()) {
val toolCalls =
response.getResult().getOutput().getToolCalls().stream().map(ToolCall::name).toList();
log.info("Executing {} tool call(s) - {}.", toolCalls.size(), toolCalls);
val toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response);
// Send the tool execution result back to the model.
log.debug("Re-invoking model with tool execution results.");
return call(new Prompt(toolExecutionResult.conversationHistory(), options));
}
return response;
return new ChatResponse(toGenerations(result));
Comment thread
vladimir-a-sap marked this conversation as resolved.
}

@Override
Expand Down Expand Up @@ -129,14 +118,15 @@ private static List<OpenAiMessage> extractMessages(final Prompt prompt) {

private static void addAssistantMessage(
final List<OpenAiMessage> result, final AssistantMessage message) {
if (message.getText() != null) {
result.add(OpenAiMessage.assistant(message.getText()));
final var toolCalls = message.getToolCalls();
if (toolCalls != null && !toolCalls.isEmpty()) {
final Function<ToolCall, OpenAiToolCall> callTranslate =
toolCall -> OpenAiToolCall.function(toolCall.id(), toolCall.name(), toolCall.arguments());
val calls = toolCalls.stream().map(callTranslate).toList();
result.add(OpenAiMessage.assistant(calls));
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));
Option.of(message.getText()).peek(t -> result.add(OpenAiMessage.assistant(t)));
}

private static void addToolMessages(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ void testToolCallsWithoutExecution() throws IOException {
.withHeader("Content-Type", "application/json")
.withBodyFile("weatherToolResponse.json")));

var options = new DefaultToolCallingChatOptions();
options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
options.setInternalToolExecutionEnabled(false);
var options =
DefaultToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
.build();
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
val result = client.call(prompt);

Expand Down Expand Up @@ -178,10 +179,16 @@ void testToolCallsWithExecution() throws IOException {
.withBodyFile("weatherToolResponse2.json")
.withHeader("Content-Type", "application/json")));

var options = new DefaultToolCallingChatOptions();
options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod())));
val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options);
val result = client.call(prompt);
var options =
DefaultToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new WeatherMethod()))
.build();
val chatClient = ChatClient.builder(client).build();
val result =
chatClient
.prompt(new Prompt("What is the weather in Potsdam and in Toulouse?", options))
.call()
.chatResponse();

assertThat(result.getResult().getOutput().getText())
.isEqualTo("The current temperature in Potsdam is 30°C and in Toulouse 30°C.");
Expand Down
4 changes: 4 additions & 0 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Comment thread
vladimir-a-sap marked this conversation as resolved.
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.sap.ai.sdk.orchestration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
import com.github.victools.jsonschema.module.jackson.JacksonSchemaModule;
import java.lang.reflect.Type;
import java.util.Map;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -63,7 +64,7 @@ public static ResponseJsonSchema fromMap(
@Nonnull
public static ResponseJsonSchema fromType(@Nonnull final Type classType) {
val module =
new JacksonModule(
new JacksonSchemaModule(
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER);
val generator =
new SchemaGenerator(
Expand All @@ -73,8 +74,12 @@ public static ResponseJsonSchema fromType(@Nonnull final Type classType) {
.with(module)
.build());
val jsonSchema = generator.generateSchema(classType);
val mapper = new ObjectMapper();
val schemaMap = mapper.convertValue(jsonSchema, new TypeReference<Map<String, Object>>() {});
final Map<String, Object> schemaMap;
try {
schemaMap = new ObjectMapper().readValue(jsonSchema.toString(), new TypeReference<>() {});
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to parse generated JSON schema", e);
}
val schemaName = ((Class<?>) classType).getSimpleName() + "-Schema";
return new ResponseJsonSchema(schemaMap, schemaName, null, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.sap.ai.sdk.orchestration.AssistantMessage;
import com.sap.ai.sdk.orchestration.OrchestrationChatCompletionDelta;
import com.sap.ai.sdk.orchestration.OrchestrationClient;
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
import com.sap.ai.sdk.orchestration.OrchestrationPrompt;
import com.sap.ai.sdk.orchestration.SystemMessage;
import com.sap.ai.sdk.orchestration.ToolMessage;
Expand All @@ -15,18 +16,18 @@
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
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.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
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;

/**
Expand All @@ -38,9 +39,7 @@
public class OrchestrationChatModel implements ChatModel {
@Nonnull private final OrchestrationClient client;

@Nonnull
private final DefaultToolCallingManager toolCallingManager =
DefaultToolCallingManager.builder().build();
@Setter @Nullable private OrchestrationChatOptions defaultOptions;

/**
* Default constructor.
Expand All @@ -61,6 +60,15 @@ public OrchestrationChatModel(@Nonnull final OrchestrationClient client) {
this.client = client;
}

@Nonnull
@Override
public ChatOptions getOptions() {
if (defaultOptions != null) {
return defaultOptions;
}
return new OrchestrationChatOptions(new OrchestrationModuleConfig());
}

@Nonnull
@Override
public ChatResponse call(@Nonnull final Prompt prompt) {
Expand All @@ -69,23 +77,8 @@ public ChatResponse call(@Nonnull final Prompt prompt) {
val orchestrationPrompt = toOrchestrationPrompt(prompt);
val response =
new OrchestrationSpringChatResponse(
client.chatCompletion(orchestrationPrompt, options.getConfig()));

if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions())
&& response.hasToolCalls()) {

if (log.isDebugEnabled()) {
val tools = response.getResult().getOutput().getToolCalls();
val toolsStr = tools.stream().map(ToolCall::name).collect(Collectors.joining(", "));
log.debug("Executing {} tool call(s) - {}.", tools.size(), toolsStr);
}

val toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response);
client.chatCompletion(orchestrationPrompt, options.getConfigWithCallbacks()));

// Send the tool execution result back to the model.
log.debug("Re-invoking LLM with tool execution results.");
return call(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()));
}
return response;
}
throw new IllegalArgumentException(
Expand Down
Loading