From 20a485c6357c79f5047edd317c25976ce3b230ed Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Thu, 27 Aug 2026 11:49:07 +0200 Subject: [PATCH 01/10] migrate spring-ai to v2 --- foundation-models/openai/pom.xml | 4 + .../foundationmodels/openai/OpenAiTool.java | 8 +- .../openai/spring/OpenAiChatModel.java | 23 +-- .../openai/spring/OpenAiChatModelTest.java | 21 ++- orchestration/pom.xml | 4 + .../sdk/orchestration/ResponseJsonSchema.java | 8 +- .../spring/OrchestrationChatModel.java | 17 +- .../spring/OrchestrationChatOptions.java | 174 ++++++++++++++++-- .../spring/OrchestrationChatModelTest.java | 11 +- pom.xml | 4 +- sample-code/spring-app/pom.xml | 12 ++ .../app/services/SpringAiOpenAiService.java | 10 +- 12 files changed, 245 insertions(+), 51 deletions(-) diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml index 68140484c..26e478f0e 100644 --- a/foundation-models/openai/pom.xml +++ b/foundation-models/openai/pom.xml @@ -94,6 +94,10 @@ com.github.victools jsonschema-module-jackson + + tools.jackson.core + jackson-databind + io.vavr vavr diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java index af14b1be5..84f7f9962 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java @@ -86,7 +86,13 @@ public static Builder1 forFunction(@Nonnull final Function { final Function 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); }; } diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java index a38d99ff7..51dac84ef 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java @@ -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; @@ -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; @@ -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 @@ -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)); } @Override diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java index 3f92e35ee..6fb1478c7 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModelTest.java @@ -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); @@ -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."); diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 0cca802c8..efe24d080 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -118,6 +118,10 @@ com.github.victools jsonschema-module-jackson + + tools.jackson.core + jackson-databind + com.fasterxml.jackson.dataformat jackson-dataformat-yaml diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 718db8755..a60f0759c 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -73,8 +73,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>() {}); + final Map schemaMap; + try { + schemaMap = new ObjectMapper().readValue(jsonSchema.toString(), new TypeReference<>() {}); + } catch (com.fasterxml.jackson.core.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); } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java index 44310e005..5ac33e11c 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java @@ -17,6 +17,8 @@ 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; @@ -24,9 +26,9 @@ 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; /** @@ -38,6 +40,8 @@ public class OrchestrationChatModel implements ChatModel { @Nonnull private final OrchestrationClient client; + @Setter @Nullable private OrchestrationChatOptions defaultOptions; + @Nonnull private final DefaultToolCallingManager toolCallingManager = DefaultToolCallingManager.builder().build(); @@ -61,6 +65,15 @@ public OrchestrationChatModel(@Nonnull final OrchestrationClient client) { this.client = client; } + @Nonnull + @Override + public ChatOptions getOptions() { + if (defaultOptions != null) { + return defaultOptions; + } + return ChatModel.super.getOptions(); + } + @Nonnull @Override public ChatResponse call(@Nonnull final Prompt prompt) { @@ -71,7 +84,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) { new OrchestrationSpringChatResponse( client.chatCompletion(orchestrationPrompt, options.getConfig())); - if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) + if (!Boolean.FALSE.equals(options.isInternalToolExecutionEnabled()) && response.hasToolCalls()) { if (log.isDebugEnabled()) { diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java index b6b32aa3a..0fd2ec78b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java @@ -7,6 +7,7 @@ import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TOP_P; import static com.sap.ai.sdk.orchestration.OrchestrationJacksonConfiguration.getOrchestrationObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; @@ -25,7 +26,6 @@ import lombok.Getter; import lombok.val; import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.tool.ToolCallback; @@ -153,11 +153,11 @@ public Double getTopP() { /** * Returns a copy of this {@link OrchestrationChatOptions}. * + * @param option subtype * @return a copy of this {@link OrchestrationChatOptions} */ @SuppressWarnings("unchecked") // The same suppress is in DefaultChatOptions @Nonnull - @Override public T copy() { // note: this is a shallow copy val copyConfig = @@ -179,7 +179,11 @@ private T getLlmConfigParam(@Nonnull final String param) { return ((Map) getLlmConfigNonNull().getParams()).get(param); } - @Override + /** + * Setter method + * + * @param toolCallbacks tool callbacks to set int template config + */ public void setToolCallbacks(@Nonnull final List toolCallbacks) { this.toolCallbacks = toolCallbacks; final Template template = @@ -189,12 +193,151 @@ public void setToolCallbacks(@Nonnull final List toolCallbacks) { config = config.withTemplateConfig(template.tools(tools)); } + /** + * Getter method + * + * @return if internal tool execution enabled + */ @Nullable - @Override - public Boolean getInternalToolExecutionEnabled() { + public Boolean isInternalToolExecutionEnabled() { return this.internalToolExecutionEnabled; } + @Nonnull + @Override + public Builder mutate() { + return new Builder(this); + } + + /** + * Builder that preserves {@link OrchestrationChatOptions} through the Spring AI advisor chain. + * Spring AI 2.x {@code ChatClient} calls {@code mutate().build()} to reconstruct the options + * after passing through advisors; returning {@code OrchestrationChatOptions} here ensures the + * type is not lost. + * + * @since 1.3.0 + */ + public static final class Builder implements ToolCallingChatOptions.Builder { + @Nonnull private final OrchestrationChatOptions source; + @Nonnull private List toolCallbacks; + @Nonnull private Map toolContext; + + private Builder(@Nonnull final OrchestrationChatOptions source) { + this.source = source; + this.toolCallbacks = source.getToolCallbacks(); + this.toolContext = source.getToolContext(); + } + + @Override + @Nonnull + public Builder clone() { + return new Builder(source); + } + + @Override + @Nonnull + public Builder combineWith(@Nonnull final ChatOptions.Builder other) { + if (other instanceof OrchestrationChatOptions.Builder that) { + // Per-request builder overrides model-level defaults + this.toolCallbacks = that.toolCallbacks; + this.toolContext = that.toolContext; + // Use the per-request source for all OrchestrationChatOptions-specific config + return new Builder(that.source) + .toolCallbacks(this.toolCallbacks) + .toolContext(this.toolContext); + } + return this; + } + + @Override + @Nonnull + public Builder toolCallbacks(@Nonnull final List callbacks) { + this.toolCallbacks = callbacks; + return this; + } + + @Override + @Nonnull + public Builder toolCallbacks(@Nonnull final ToolCallback... callbacks) { + this.toolCallbacks = List.of(callbacks); + return this; + } + + @Override + @Nonnull + public Builder toolContext(@Nonnull final Map ctx) { + this.toolContext = ctx; + return this; + } + + @Override + @Nonnull + public Builder toolContext(@Nonnull final String key, @Nonnull final Object value) { + val mutable = new java.util.HashMap<>(toolContext); + mutable.put(key, value); + this.toolContext = Map.copyOf(mutable); + return this; + } + + @Override + @Nonnull + public Builder model(@Nullable final String model) { + return this; + } + + @Override + @Nonnull + public Builder frequencyPenalty(@Nullable final Double v) { + return this; + } + + @Override + @Nonnull + public Builder maxTokens(@Nullable final Integer v) { + return this; + } + + @Override + @Nonnull + public Builder presencePenalty(@Nullable final Double v) { + return this; + } + + @Override + @Nonnull + public Builder stopSequences(@Nullable final List v) { + return this; + } + + @Override + @Nonnull + public Builder temperature(@Nullable final Double v) { + return this; + } + + @Override + @Nonnull + public Builder topK(@Nullable final Integer v) { + return this; + } + + @Override + @Nonnull + public Builder topP(@Nullable final Double v) { + return this; + } + + @Override + @Nonnull + public OrchestrationChatOptions build() { + @SuppressWarnings("unchecked") + final OrchestrationChatOptions result = source.copy(); + result.setToolCallbacks(toolCallbacks); + result.setToolContext(toolContext); + return result; + } + } + @Nonnull private LLMModelDetails getLlmConfigNonNull() { return Objects.requireNonNull( @@ -204,12 +347,19 @@ private LLMModelDetails getLlmConfigNonNull() { private static ChatCompletionTool toOrchestrationTool(@Nonnull final ToolCallback toolCallback) { val toolDef = toolCallback.getToolDefinition(); - return ChatCompletionTool.create() - .type(TypeEnum.FUNCTION) - .function( - FunctionObject.create() - .name(toolDef.name()) - .description(toolDef.description()) - .parameters(ModelOptionsUtils.jsonToMap(toolDef.inputSchema()))); + try { + final Map params = + JACKSON.readValue(toolDef.inputSchema(), new TypeReference<>() {}); + return ChatCompletionTool.create() + .type(TypeEnum.FUNCTION) + .function( + FunctionObject.create() + .name(toolDef.name()) + .description(toolDef.description()) + .parameters(params)); + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + throw new IllegalArgumentException( + "Failed to parse tool input schema for tool: " + toolDef.name(), e); + } } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModelTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModelTest.java index 17b1eaa82..b4a001407 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModelTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModelTest.java @@ -67,6 +67,7 @@ void setup(WireMockRuntimeInfo server) { client = new OrchestrationChatModel(new OrchestrationClient(destination)); defaultOptions = new OrchestrationChatOptions(new OrchestrationModuleConfig().withLlmConfig(GPT_4O)); + client.setDefaultOptions(defaultOptions); prompt = new Prompt("Hello World! Why is this phrase so famous?", defaultOptions); ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); } @@ -241,9 +242,13 @@ void testChatMemory() throws IOException { val repository = new InMemoryChatMemoryRepository(); val memory = MessageWindowChatMemory.builder().chatMemoryRepository(repository).build(); val advisor = MessageChatMemoryAdvisor.builder(memory).build(); - val cl = ChatClient.builder(client).defaultAdvisors(advisor).build(); - val prompt1 = new Prompt("What is the capital of France?", defaultOptions); - val prompt2 = new Prompt("And what is the typical food there?", defaultOptions); + val cl = + ChatClient.builder(client) + .defaultAdvisors(advisor) + .defaultOptions(defaultOptions.mutate()) + .build(); + val prompt1 = new Prompt("What is the capital of France?"); + val prompt2 = new Prompt("And what is the typical food there?"); cl.prompt(prompt1) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, "test-conversation")) diff --git a/pom.xml b/pom.xml index 058759de6..8a21a8c2d 100644 --- a/pom.xml +++ b/pom.xml @@ -65,12 +65,12 @@ 13.8.0 2.1.3 3.5.6 - 1.1.8 + 2.0.1 3.8.7 3.2.0 5.23.0 3.28.2 - 4.38.0 + 5.0.0 2.22.2 3.2.2 1.6.3 diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index f60b9bc58..e513fd908 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -64,6 +64,18 @@ mcp-core ${mcp-core.version} + + + com.github.victools + jsonschema-module-swagger-2 + 5.0.0 + + + + com.networknt + json-schema-validator + 3.0.1 + org.junit diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java index 48796e833..0d1070afb 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java @@ -91,11 +91,11 @@ public Flux streamChatCompletion() { */ @Nonnull public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) { - val options = new DefaultToolCallingChatOptions(); - options.setToolCallbacks(List.of(ToolCallbacks.from(new WeatherMethod()))); - options.setInternalToolExecutionEnabled(internalToolExecutionEnabled); - - val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options); + val builder = DefaultToolCallingChatOptions.builder(); + if (internalToolExecutionEnabled) { + builder.toolCallbacks(ToolCallbacks.from(new WeatherMethod())); + } + val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", builder.build()); return chatClient.call(prompt); } From e168629c3a97d435252c7b64e1bcb557610fba7e Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 11:36:44 +0200 Subject: [PATCH 02/10] improve test coverage --- .../ai/sdk/orchestration/TextItemTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 orchestration/src/test/java/com/sap/ai/sdk/orchestration/TextItemTest.java diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/TextItemTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/TextItemTest.java new file mode 100644 index 000000000..6cb7d7321 --- /dev/null +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/TextItemTest.java @@ -0,0 +1,20 @@ +package com.sap.ai.sdk.orchestration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class TextItemTest { + + @Test + void testEquals() { + assertThat(new TextItem("test").equals(null)).isFalse(); + } + + @Test + void testHashCode() { + assertThat(new TextItem("test").hashCode()).isEqualTo(new TextItem("test").hashCode()); + assertThat(new TextItem("test").hashCode()).isNotEqualTo(new TextItem("other").hashCode()); + } +} From 410a2b12caf11828dbaa6826bf19974414d25009 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 13:07:26 +0200 Subject: [PATCH 03/10] implement orchestration chat options builder and add tests --- .../spring/OrchestrationChatOptions.java | 35 +++- .../spring/OrchestrationChatOptionsTest.java | 184 ++++++++++++++++++ 2 files changed, 214 insertions(+), 5 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java index 0fd2ec78b..6f3dd5b7a 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java @@ -221,6 +221,8 @@ public static final class Builder implements ToolCallingChatOptions.Builder toolCallbacks; @Nonnull private Map toolContext; + @Nullable private String modelName; + @Nonnull private final Map paramOverrides = new java.util.LinkedHashMap<>(); private Builder(@Nonnull final OrchestrationChatOptions source) { this.source = source; @@ -242,9 +244,11 @@ public Builder combineWith(@Nonnull final ChatOptions.Builder other) { this.toolCallbacks = that.toolCallbacks; this.toolContext = that.toolContext; // Use the per-request source for all OrchestrationChatOptions-specific config - return new Builder(that.source) - .toolCallbacks(this.toolCallbacks) - .toolContext(this.toolContext); + final Builder result = new Builder(that.source); + result.toolCallbacks(this.toolCallbacks).toolContext(this.toolContext); + result.modelName = that.modelName; + result.paramOverrides.putAll(that.paramOverrides); + return result; } return this; } @@ -282,56 +286,77 @@ public Builder toolContext(@Nonnull final String key, @Nonnull final Object valu @Override @Nonnull public Builder model(@Nullable final String model) { + this.modelName = model; return this; } @Override @Nonnull public Builder frequencyPenalty(@Nullable final Double v) { + paramOverrides.put(FREQUENCY_PENALTY.getName(), v); return this; } @Override @Nonnull public Builder maxTokens(@Nullable final Integer v) { + paramOverrides.put(MAX_TOKENS.getName(), v); return this; } @Override @Nonnull public Builder presencePenalty(@Nullable final Double v) { + paramOverrides.put(PRESENCE_PENALTY.getName(), v); return this; } @Override @Nonnull public Builder stopSequences(@Nullable final List v) { + paramOverrides.put("stop_sequences", v); return this; } @Override @Nonnull public Builder temperature(@Nullable final Double v) { + paramOverrides.put(TEMPERATURE.getName(), v); return this; } @Override @Nonnull public Builder topK(@Nullable final Integer v) { + paramOverrides.put("top_k", v); return this; } @Override @Nonnull public Builder topP(@Nullable final Double v) { + paramOverrides.put(TOP_P.getName(), v); return this; } @Override @Nonnull public OrchestrationChatOptions build() { - @SuppressWarnings("unchecked") - final OrchestrationChatOptions result = source.copy(); + final OrchestrationChatOptions result = source.copy(); + if (modelName != null || !paramOverrides.isEmpty()) { + final LLMModelDetails existingLlm = result.getLlmConfigNonNull(); + final Map mergedParams = new java.util.LinkedHashMap<>(); + if (existingLlm.getParams() != null) { + mergedParams.putAll(existingLlm.getParams()); + } + mergedParams.putAll(paramOverrides); + final LLMModelDetails newLlm = + LLMModelDetails.create() + .name(modelName != null ? modelName : existingLlm.getName()) + .version(existingLlm.getVersion()) + .params(mergedParams); + result.setConfig(result.getConfig().withLlmConfig(newLlm)); + } result.setToolCallbacks(toolCallbacks); result.setToolContext(toolContext); return result; diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java index a9e7cf90a..ca82bcbb4 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java @@ -1,6 +1,7 @@ package com.sap.ai.sdk.orchestration.spring; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_2_5_FLASH; +import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.FREQUENCY_PENALTY; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.MAX_TOKENS; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.PRESENCE_PENALTY; @@ -11,7 +12,9 @@ import com.sap.ai.sdk.orchestration.OrchestrationAiModel; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.ai.support.ToolCallbacks; class OrchestrationChatOptionsTest { @@ -37,6 +40,11 @@ private static void assertCustomLLM(OrchestrationChatOptions opts) { assertThat(opts.getTopP()).isEqualTo(0.5); } + private static OrchestrationChatOptions baseOpts() { + return new OrchestrationChatOptions( + new OrchestrationModuleConfig().withLlmConfig(GEMINI_2_5_FLASH)); + } + @Test void testParametersAreInherited() { var opts = @@ -74,4 +82,180 @@ void testCustomCopy() { var copy = (OrchestrationChatOptions) opts.copy(); assertCustomLLM(copy); } + + // --- Builder scalar setters --- + + @Test + void testBuilderModelOverride() { + var built = baseOpts().mutate().model(GPT_4O.getName()).build(); + + assertThat(built.getModel()).isEqualTo(GPT_4O.getName()); + // other fields from source are preserved + assertThat(built.getModelVersion()).isEqualTo(GEMINI_2_5_FLASH.getVersion()); + } + + @Test + void testBuilderFrequencyPenalty() { + var built = baseOpts().mutate().frequencyPenalty(0.7).build(); + + assertThat(built.getFrequencyPenalty()).isEqualTo(0.7); + } + + @Test + void testBuilderMaxTokens() { + var built = baseOpts().mutate().maxTokens(200).build(); + + assertThat(built.getMaxTokens()).isEqualTo(200); + } + + @Test + void testBuilderPresencePenalty() { + var built = baseOpts().mutate().presencePenalty(0.3).build(); + + assertThat(built.getPresencePenalty()).isEqualTo(0.3); + } + + @Test + void testBuilderStopSequences() { + var built = baseOpts().mutate().stopSequences(List.of("stop", "end")).build(); + + assertThat(built.getStopSequences()).containsExactly("stop", "end"); + } + + @Test + void testBuilderTemperature() { + var built = baseOpts().mutate().temperature(0.9).build(); + + assertThat(built.getTemperature()).isEqualTo(0.9); + } + + @Test + void testBuilderTopK() { + var built = baseOpts().mutate().topK(40).build(); + + assertThat(built.getTopK()).isEqualTo(40); + } + + @Test + void testBuilderTopP() { + var built = baseOpts().mutate().topP(0.8).build(); + + assertThat(built.getTopP()).isEqualTo(0.8); + } + + @Test + void testBuilderAllScalarsAtOnce() { + var built = + baseOpts() + .mutate() + .model(GPT_4O.getName()) + .frequencyPenalty(0.1) + .maxTokens(50) + .presencePenalty(0.2) + .stopSequences(List.of("\n")) + .temperature(0.6) + .topK(10) + .topP(0.95) + .build(); + + assertThat(built.getModel()).isEqualTo(GPT_4O.getName()); + assertThat(built.getFrequencyPenalty()).isEqualTo(0.1); + assertThat(built.getMaxTokens()).isEqualTo(50); + assertThat(built.getPresencePenalty()).isEqualTo(0.2); + assertThat(built.getStopSequences()).containsExactly("\n"); + assertThat(built.getTemperature()).isEqualTo(0.6); + assertThat(built.getTopK()).isEqualTo(10); + assertThat(built.getTopP()).isEqualTo(0.95); + } + + @Test + void testBuilderOverridesPreserveExistingParams() { + // Source already has all params; builder should override only the ones specified + var source = + new OrchestrationChatOptions(new OrchestrationModuleConfig().withLlmConfig(CUSTOM_LLM)); + var built = source.mutate().temperature(0.99).build(); + + assertThat(built.getTemperature()).isEqualTo(0.99); + // other params unchanged from CUSTOM_LLM + assertThat(built.getMaxTokens()).isEqualTo(100); + assertThat(built.getFrequencyPenalty()).isEqualTo(0.5); + assertThat(built.getModel()).isEqualTo(GEMINI_2_5_FLASH.getName()); + } + + @Test + void testBuilderDoesNotMutateSource() { + var source = baseOpts(); + source.mutate().temperature(0.5).maxTokens(100).build(); + + // source must be unchanged + assertThat(source.getTemperature()).isNull(); + assertThat(source.getMaxTokens()).isNull(); + } + + // --- Builder tool callbacks and context --- + + @Test + void testBuilderToolCallbacks() { + var callbacks = ToolCallbacks.from(new WeatherMethod()); + var built = baseOpts().mutate().toolCallbacks(List.of(callbacks)).build(); + + // The built result has the tool callbacks set (setToolCallbacks wires them into template config + // too) + assertThat(built.getToolCallbacks()).hasSize(1); + } + + @Test + void testBuilderToolContext() { + var built = baseOpts().mutate().toolContext("key", "value").build(); + + assertThat(built.getToolContext()).containsEntry("key", "value"); + } + + @Test + void testBuilderToolContextMap() { + var built = baseOpts().mutate().toolContext(Map.of("a", 1, "b", 2)).build(); + + assertThat(built.getToolContext()).containsEntry("a", 1).containsEntry("b", 2); + } + + // --- combineWith --- + + @Test + void testCombineWithOrchestrationBuilder() { + var base = baseOpts(); + var perRequest = + new OrchestrationChatOptions(new OrchestrationModuleConfig().withLlmConfig(GPT_4O)); + + // Simulate what Spring AI does: starts from base.mutate(), then combines with + // per-request.mutate() + var combined = base.mutate().combineWith(perRequest.mutate().temperature(0.7)); + + var result = combined.build(); + // Per-request source (GPT_4O) wins for OrchestrationChatOptions-specific config + assertThat(result.getModel()).isEqualTo(GPT_4O.getName()); + // Per-request temperature override is carried through + assertThat(result.getTemperature()).isEqualTo(0.7); + } + + @Test + void testCombineWithNonOrchestrationBuilderIsNoOp() { + var base = baseOpts().mutate().temperature(0.4); + var unrelated = org.springframework.ai.chat.prompt.ChatOptions.builder().temperature(0.9); + + var result = base.combineWith(unrelated).build(); + + // combineWith a non-OrchestrationChatOptions.Builder is a no-op; base values survive + assertThat(result.getModel()).isEqualTo(GEMINI_2_5_FLASH.getName()); + assertThat(result.getTemperature()).isEqualTo(0.4); + } + + @Test + void testMutateProducesOrchestrationChatOptions() { + var opts = baseOpts(); + var builder = opts.mutate(); + var result = builder.build(); + + assertThat(result).isInstanceOf(OrchestrationChatOptions.class); + assertThat(result.getModel()).isEqualTo(GEMINI_2_5_FLASH.getName()); + } } From 196295d22411cd0761a1a4674c43160c2fbe1e76 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 13:14:22 +0200 Subject: [PATCH 04/10] remove unnecessary comments --- .../orchestration/spring/OrchestrationChatOptionsTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java index ca82bcbb4..a500641cb 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptionsTest.java @@ -83,8 +83,6 @@ void testCustomCopy() { assertCustomLLM(copy); } - // --- Builder scalar setters --- - @Test void testBuilderModelOverride() { var built = baseOpts().mutate().model(GPT_4O.getName()).build(); @@ -192,8 +190,6 @@ void testBuilderDoesNotMutateSource() { assertThat(source.getMaxTokens()).isNull(); } - // --- Builder tool callbacks and context --- - @Test void testBuilderToolCallbacks() { var callbacks = ToolCallbacks.from(new WeatherMethod()); @@ -218,8 +214,6 @@ void testBuilderToolContextMap() { assertThat(built.getToolContext()).containsEntry("a", 1).containsEntry("b", 2); } - // --- combineWith --- - @Test void testCombineWithOrchestrationBuilder() { var base = baseOpts(); From 1c3f02a3d6c84dbec1c7fd55de4c51ce32a945f1 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 14:57:09 +0200 Subject: [PATCH 05/10] close semantics gaps related to api migration --- .../openai/spring/OpenAiChatModel.java | 13 +++--- .../spring/OrchestrationChatModel.java | 7 +++ .../spring/OrchestrationChatOptions.java | 5 +++ .../spring/MockWeatherService.java | 44 ------------------- 4 files changed, 19 insertions(+), 50 deletions(-) delete mode 100644 orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/MockWeatherService.java diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java index 51dac84ef..22a998ad4 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiChatModel.java @@ -118,14 +118,15 @@ private static List extractMessages(final Prompt prompt) { private static void addAssistantMessage( final List 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 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 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( diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java index 5ac33e11c..6d0324770 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java @@ -95,6 +95,13 @@ public ChatResponse call(@Nonnull final Prompt prompt) { val toolExecutionResult = toolCallingManager.executeToolCalls(prompt, response); + if (toolExecutionResult.returnDirect()) { + log.debug("Returning tool execution result directly without re-invoking LLM."); + return new ChatResponse( + org.springframework.ai.model.tool.ToolExecutionResult.buildGenerations( + toolExecutionResult)); + } + // 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())); diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java index 6f3dd5b7a..c5775c04d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java @@ -169,6 +169,7 @@ public T copy() { .withGroundingConfig(config.getGroundingConfig()); val result = new OrchestrationChatOptions(copyConfig); result.setToolCallbacks(toolCallbacks); + result.setToolNames(toolNames); result.setInternalToolExecutionEnabled(internalToolExecutionEnabled); return (T) result; } @@ -220,6 +221,7 @@ public Builder mutate() { public static final class Builder implements ToolCallingChatOptions.Builder { @Nonnull private final OrchestrationChatOptions source; @Nonnull private List toolCallbacks; + @Nonnull private Set toolNames; @Nonnull private Map toolContext; @Nullable private String modelName; @Nonnull private final Map paramOverrides = new java.util.LinkedHashMap<>(); @@ -227,6 +229,7 @@ public static final class Builder implements ToolCallingChatOptions.Builder other) { // Use the per-request source for all OrchestrationChatOptions-specific config final Builder result = new Builder(that.source); result.toolCallbacks(this.toolCallbacks).toolContext(this.toolContext); + result.toolNames = that.toolNames; result.modelName = that.modelName; result.paramOverrides.putAll(that.paramOverrides); return result; @@ -358,6 +362,7 @@ public OrchestrationChatOptions build() { result.setConfig(result.getConfig().withLlmConfig(newLlm)); } result.setToolCallbacks(toolCallbacks); + result.setToolNames(toolNames); result.setToolContext(toolContext); return result; } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/MockWeatherService.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/MockWeatherService.java deleted file mode 100644 index 46c79bb3c..000000000 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/spring/MockWeatherService.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.sap.ai.sdk.orchestration.spring; - -import java.util.function.Function; -import javax.annotation.Nonnull; - -/** Function for tool calls in Spring AI */ -public class MockWeatherService - implements Function { - - /** Unit of temperature */ - public enum Unit { - /** Celsius */ - C, - /** Fahrenheit */ - F - } - - /** - * Request for the weather - * - * @param location the city - * @param unit the unit of temperature - */ - public record Request(String location, Unit unit) {} - - /** - * Response for the weather - * - * @param temp the temperature - * @param unit the unit of temperature - */ - public record Response(double temp, Unit unit) {} - - /** - * Apply the function - * - * @param request the request - * @return the response - */ - @Nonnull - public Response apply(@Nonnull Request request) { - return new Response(30.0, Unit.C); - } -} From 373bff0ae4864d947fe195b205e86d661afdf64a Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 15:11:41 +0200 Subject: [PATCH 06/10] update release notes --- docs/release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release_notes.md b/docs/release_notes.md index 1d60754f1..f2ea7d17e 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -8,7 +8,7 @@ ### 🔧 Compatibility Notes -- +-[Orchestration] Spring AI support was upgraded to version `2.0.1` ### ✨ New Functionality From 940d6101cebba9d1ca827899820379f006ea058e Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 15:13:23 +0200 Subject: [PATCH 07/10] revert dependabot ignore for spring ai --- .github/dependabot.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 0e7e61203..422d1887b 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -22,8 +22,6 @@ updates: 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" From 8ba7ab5761be7e37b2f67d3c27dee343f69a2279 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Fri, 28 Aug 2026 15:18:39 +0200 Subject: [PATCH 08/10] fix "since" version in docs --- .../ai/sdk/orchestration/spring/OrchestrationChatOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java index c5775c04d..506bca322 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatOptions.java @@ -216,7 +216,7 @@ public Builder mutate() { * after passing through advisors; returning {@code OrchestrationChatOptions} here ensures the * type is not lost. * - * @since 1.3.0 + * @since 1.25.0 */ public static final class Builder implements ToolCallingChatOptions.Builder { @Nonnull private final OrchestrationChatOptions source; From 3448b10f82bf3c6fc8fe8f658c54928a3499c2b5 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Wed, 2 Sep 2026 15:27:47 +0200 Subject: [PATCH 09/10] fix e2e tests --- .github/workflows/e2e-test.yaml | 2 +- .../orchestration/spring/OrchestrationChatModel.java | 10 +++++++--- .../ai/sdk/app/services/SpringAiOpenAiService.java | 11 ++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-test.yaml b/.github/workflows/e2e-test.yaml index 60592c9ae..769de1b19 100644 --- a/.github/workflows/e2e-test.yaml +++ b/.github/workflows/e2e-test.yaml @@ -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 }} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java index 6d0324770..24a9f7dd7 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java @@ -4,8 +4,10 @@ import static com.sap.ai.sdk.orchestration.model.MessageToolCall.TypeEnum.FUNCTION; import com.sap.ai.sdk.orchestration.AssistantMessage; +import com.sap.ai.sdk.orchestration.OrchestrationAiModel; 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; @@ -27,8 +29,10 @@ 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.DefaultChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.DefaultToolCallingManager; +import org.springframework.ai.model.tool.ToolExecutionResult; import reactor.core.publisher.Flux; /** @@ -71,7 +75,7 @@ public ChatOptions getOptions() { if (defaultOptions != null) { return defaultOptions; } - return ChatModel.super.getOptions(); + return new OrchestrationChatOptions( new OrchestrationModuleConfig()); } @Nonnull @@ -97,8 +101,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) { if (toolExecutionResult.returnDirect()) { log.debug("Returning tool execution result directly without re-invoking LLM."); - return new ChatResponse( - org.springframework.ai.model.tool.ToolExecutionResult.buildGenerations( + return new ChatResponse(ToolExecutionResult.buildGenerations( toolExecutionResult)); } @@ -108,6 +111,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) { } return response; } + log.warn("Options are: {}", prompt.getOptions().getClass().getCanonicalName()); throw new IllegalArgumentException( "Please add OrchestrationChatOptions to the Prompt: new Prompt(\"message\", new OrchestrationChatOptions(config))"); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java index 0d1070afb..94d912200 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java @@ -91,11 +91,16 @@ public Flux streamChatCompletion() { */ @Nonnull public ChatResponse toolCalling(final boolean internalToolExecutionEnabled) { - val builder = DefaultToolCallingChatOptions.builder(); + val options = + DefaultToolCallingChatOptions.builder() + .toolCallbacks(ToolCallbacks.from(new WeatherMethod())) + .build(); + val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", options); if (internalToolExecutionEnabled) { - builder.toolCallbacks(ToolCallbacks.from(new WeatherMethod())); + return Objects.requireNonNull( + ChatClient.builder(chatClient).build().prompt(prompt).call().chatResponse(), + "Chat response is null"); } - val prompt = new Prompt("What is the weather in Potsdam and in Toulouse?", builder.build()); return chatClient.call(prompt); } From f7759008b33b20a2a0207e38a3e296b2ff2f2367 Mon Sep 17 00:00:00 2001 From: Vladimir Aseev Date: Wed, 2 Sep 2026 16:56:45 +0200 Subject: [PATCH 10/10] fix e2e tests --- .../spring/OrchestrationChatModel.java | 8 ++------ sample-code/spring-app/pom.xml | 18 ++++++++---------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java index 24a9f7dd7..9dc1eaded 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/spring/OrchestrationChatModel.java @@ -4,7 +4,6 @@ import static com.sap.ai.sdk.orchestration.model.MessageToolCall.TypeEnum.FUNCTION; import com.sap.ai.sdk.orchestration.AssistantMessage; -import com.sap.ai.sdk.orchestration.OrchestrationAiModel; import com.sap.ai.sdk.orchestration.OrchestrationChatCompletionDelta; import com.sap.ai.sdk.orchestration.OrchestrationClient; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; @@ -29,7 +28,6 @@ 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.DefaultChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.DefaultToolCallingManager; import org.springframework.ai.model.tool.ToolExecutionResult; @@ -75,7 +73,7 @@ public ChatOptions getOptions() { if (defaultOptions != null) { return defaultOptions; } - return new OrchestrationChatOptions( new OrchestrationModuleConfig()); + return new OrchestrationChatOptions(new OrchestrationModuleConfig()); } @Nonnull @@ -101,8 +99,7 @@ public ChatResponse call(@Nonnull final Prompt prompt) { if (toolExecutionResult.returnDirect()) { log.debug("Returning tool execution result directly without re-invoking LLM."); - return new ChatResponse(ToolExecutionResult.buildGenerations( - toolExecutionResult)); + return new ChatResponse(ToolExecutionResult.buildGenerations(toolExecutionResult)); } // Send the tool execution result back to the model. @@ -111,7 +108,6 @@ public ChatResponse call(@Nonnull final Prompt prompt) { } return response; } - log.warn("Options are: {}", prompt.getOptions().getClass().getCanonicalName()); throw new IllegalArgumentException( "Please add OrchestrationChatOptions to the Prompt: new Prompt(\"message\", new OrchestrationChatOptions(config))"); } diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index e9046cc98..0bd6ad5f6 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -188,17 +188,15 @@ org.springframework.ai - spring-ai-autoconfigure-mcp-client - 1.0.9 + spring-ai-autoconfigure-mcp-client-common + 2.0.1 + runtime + + + org.springframework.ai + spring-ai-mcp-annotations + 2.0.1 runtime - - - - org.springframework.boot - spring-boot-starter - - org.springframework.boot