Customization Options:
*
- * - Extensible through {@link McpSyncClientCustomizer} and {@link McpAsyncClientCustomizer}
+ *
- Extensible through {@link McpClientCustomizer} (Spring AI 2.0 unified the
+ * previous separate {@code McpSyncClientCustomizer} / {@code McpAsyncClientCustomizer} types)
*
- Configurable timeouts and client information
*
- Support for custom transport implementations
*
@@ -103,8 +103,7 @@
* @see McpSyncClient
* @see McpAsyncClient
* @see McpClientCommonProperties
- * @see McpSyncClientCustomizer
- * @see McpAsyncClientCustomizer
+ * @see McpClientCustomizer
* @see StdioTransportAutoConfiguration
*/
@AutoConfiguration(afterName = {
diff --git a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/pom.xml b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/pom.xml
index 29d9ff933..ef3ce3158 100644
--- a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
embabel-agent-shell-autoconfigure
jar
diff --git a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/shell/AgentShellProperties.java b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/shell/AgentShellProperties.java
index 4e03fdf37..408d30e5c 100644
--- a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/shell/AgentShellProperties.java
+++ b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/shell/AgentShellProperties.java
@@ -18,9 +18,9 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Pattern;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
/**
* Configuration properties for Embabel Agent Shell Starter.
diff --git a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/shell/AgentShellAutoConfigurationTest.java b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/shell/AgentShellAutoConfigurationTest.java
index 1c38ff1fd..1af07ba21 100644
--- a/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/shell/AgentShellAutoConfigurationTest.java
+++ b/embabel-agent-autoconfigure/embabel-agent-shell-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/shell/AgentShellAutoConfigurationTest.java
@@ -24,7 +24,7 @@
import com.embabel.agent.spi.logging.ColorPalette;
import com.embabel.agent.spi.logging.LoggingPersonality;
import com.embabel.common.ai.model.ModelProvider;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectMapper;
import org.jline.terminal.Terminal;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/pom.xml
index 5b3915f6e..8f363a686 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-anthropic-autoconfigure
@@ -36,6 +36,16 @@
spring-ai-anthropic
+
+
+ com.anthropic
+ anthropic-java-client-okhttp
+
+
com.embabel.agent
embabel-agent-test-internal
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfig.kt
index 5920a1f54..2049dc25e 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfig.kt
@@ -18,7 +18,7 @@ package com.embabel.agent.config.models.anthropic
import com.embabel.chat.MessageRole
import com.embabel.common.ai.model.LlmOptions
import com.embabel.common.ai.model.spi.InternalExtensionApi
-import org.springframework.ai.anthropic.api.AnthropicCacheTtl
+import org.springframework.ai.anthropic.AnthropicCacheTtl
/**
* Extension key for Anthropic caching configuration in LlmOptions.extensions.
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelFactory.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelFactory.kt
index 2fa9cf1ed..0408d8b87 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelFactory.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelFactory.kt
@@ -15,6 +15,8 @@
*/
package com.embabel.agent.config.models.anthropic
+import com.anthropic.client.AnthropicClient
+import com.anthropic.client.okhttp.AnthropicOkHttpClient
import com.embabel.agent.api.models.AnthropicModels
import com.embabel.agent.spi.LlmService
import com.embabel.agent.spi.support.springai.SpringAiLlmService
@@ -27,14 +29,11 @@ import io.micrometer.observation.ObservationRegistry
import org.slf4j.LoggerFactory
import org.springframework.ai.anthropic.AnthropicChatModel
import org.springframework.ai.anthropic.AnthropicChatOptions
-import org.springframework.ai.anthropic.api.AnthropicApi
import org.springframework.ai.model.tool.ToolCallingManager
-import org.springframework.ai.retry.RetryUtils
import org.springframework.beans.factory.ObjectProvider
-import org.springframework.http.client.SimpleClientHttpRequestFactory
import org.springframework.retry.support.RetryTemplate
import org.springframework.web.client.RestClient
-import org.springframework.web.reactive.function.client.WebClient
+import java.time.Duration
/**
* Builds Anthropic [LlmService] instances from a raw API key.
@@ -56,18 +55,25 @@ import org.springframework.web.reactive.function.client.WebClient
* AnthropicModelFactory(apiKey = userKey, validationModel = AnthropicModels.CLAUDE_SONNET_4_5)
* ```
*
+ * Spring AI 2.0 swapped its hand-rolled `AnthropicApi` for the official anthropic-java SDK
+ * ([AnthropicClient]). The migration removes Spring's `RestClient`/`WebClient` from the
+ * HTTP path entirely — the SDK uses OkHttp internally. The [restClientBuilder] parameter
+ * is kept for source compatibility with the previous constructor signature but is no
+ * longer wired into HTTP calls.
+ *
* @param apiKey Anthropic API key.
* @param baseUrl Optional base URL override; defaults to the standard Anthropic endpoint.
* @param validationModel Model used for the key-validation probe. Defaults to [VALIDATION_MODEL].
- * @param observationRegistry Micrometer registry for HTTP client instrumentation.
- * @param requestFactory Optional HTTP request factory (e.g. for custom timeouts).
+ * @param observationRegistry Micrometer registry for Spring AI's chat model instrumentation.
+ * @param restClientBuilder Unused since Spring AI 2.0; retained for source compatibility.
*/
open class AnthropicModelFactory(
private val apiKey: String,
private val baseUrl: String? = null,
private val validationModel: String = VALIDATION_MODEL,
protected val observationRegistry: ObservationRegistry = ObservationRegistry.NOOP,
- private val restClientBuilder: ObjectProvider = ObjectProviders.empty(),
+ @Suppress("UNUSED_PARAMETER")
+ restClientBuilder: ObjectProvider = ObjectProviders.empty(),
) : ByokFactory> {
protected val logger = LoggerFactory.getLogger(javaClass)
@@ -76,57 +82,50 @@ open class AnthropicModelFactory(
/** Default model used for key validation probes — cheapest available. */
const val VALIDATION_MODEL = AnthropicModels.CLAUDE_HAIKU_4_5
- private const val CONNECT_TIMEOUT_MS = 5000
- private const val READ_TIMEOUT_MS = 600000
-
- private val PASS_THROUGH_RETRY_TEMPLATE: RetryTemplate =
- RetryTemplate.builder().maxAttempts(1).build()
+ private val READ_TIMEOUT: Duration = Duration.ofMillis(600_000)
}
/**
- * Builds an [AnthropicApi] client from the configured credentials.
- * Protected so that [AnthropicModelsConfig] can reuse it for its own model wiring.
+ * Builds an [AnthropicClient] (from anthropic-java, the SDK Spring AI 2.0 delegates to)
+ * from the configured credentials. Protected so [AnthropicModelsConfig] can reuse it.
+ *
+ * Uses [AnthropicOkHttpClient.builder] directly rather than `AnthropicSetup` so we don't
+ * inherit Spring AI's `ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY` environment-variable
+ * precedence — the factory's caller has already resolved env vs. properties at construction
+ * time.
*/
- protected fun createAnthropicApi(): AnthropicApi {
- val builder = AnthropicApi.builder().apiKey(apiKey)
+ protected fun createAnthropicClient(): AnthropicClient {
+ val builder = AnthropicOkHttpClient.builder()
+ .apiKey(apiKey)
+ .timeout(READ_TIMEOUT)
if (!baseUrl.isNullOrBlank()) {
logger.info("Using custom Anthropic base URL: {}", baseUrl)
builder.baseUrl(baseUrl)
}
- builder.restClientBuilder(
- restClientBuilder.getIfAvailable {
- RestClient.builder()
- .requestFactory(
- SimpleClientHttpRequestFactory().apply {
- setConnectTimeout(CONNECT_TIMEOUT_MS)
- setReadTimeout(READ_TIMEOUT_MS)
- })
- }
- .observationRegistry(observationRegistry)
- )
- builder.webClientBuilder(
- WebClient.builder().observationRegistry(observationRegistry)
- )
return builder.build()
}
/**
* Builds an [LlmService] for the given Anthropic model.
*
+ * Spring AI 2.0 dropped the spring-retry `RetryTemplate` parameter on the chat model
+ * builder. The [retryTemplate] param is kept here so existing call sites compile, but
+ * is ignored — retries are handled at the ChatClientLlmOperations layer via spring-retry.
+ *
* @param model Model identifier, e.g. [AnthropicModels.CLAUDE_HAIKU_4_5].
- * @param retryTemplate Retry policy; defaults to Spring AI's standard retry template.
*/
+ @JvmOverloads
fun build(
model: String,
- retryTemplate: RetryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE,
+ @Suppress("UNUSED_PARAMETER")
+ retryTemplate: RetryTemplate? = null,
): LlmService<*> {
val chatModel = AnthropicChatModel.builder()
- .defaultOptions(AnthropicChatOptions.builder().model(model).build())
- .anthropicApi(createAnthropicApi())
+ .options(AnthropicChatOptions.builder().model(model).build())
+ .anthropicClient(createAnthropicClient())
.toolCallingManager(
ToolCallingManager.builder().observationRegistry(observationRegistry).build()
)
- .retryTemplate(retryTemplate)
.observationRegistry(observationRegistry)
.build()
@@ -151,15 +150,16 @@ open class AnthropicModelFactory(
* Validates the API key with a probe call on the given [model], then returns a production
* [LlmService] if successful.
*
- * The probe uses a single-attempt retry template so an invalid key fails fast without
- * retries or noisy stack traces. On any exception the provider-specific error is
- * translated to [InvalidApiKeyException], keeping Spring AI types out of the caller.
+ * Spring AI 2.0 no longer accepts a spring-retry [RetryTemplate] on the model builder,
+ * so the probe relies on the anthropic-java SDK's own no-retry default (any 401 fails fast).
+ * On any exception the provider-specific error is translated to [InvalidApiKeyException],
+ * keeping Spring AI types out of the caller.
*
* @param model Model to use for the probe.
* @throws InvalidApiKeyException if the key is invalid.
*/
fun buildValidated(model: String): LlmService<*> {
- val probe = build(model, PASS_THROUGH_RETRY_TEMPLATE)
+ val probe = build(model)
try {
probe.createMessageSender(LlmOptions()).call(listOf(UserMessage("Hi")), emptyList())
} catch (e: Exception) {
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelsConfig.kt
index aa6a33552..6d9f882b2 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/AnthropicModelsConfig.kt
@@ -30,11 +30,10 @@ import com.embabel.common.ai.model.OptionsConverter
import com.embabel.common.ai.model.PerTokenPricingModel
import com.embabel.common.util.ExcludeFromJacocoGeneratedReport
import io.micrometer.observation.ObservationRegistry
+import org.springframework.ai.anthropic.AnthropicCacheOptions
+import org.springframework.ai.anthropic.AnthropicCacheStrategy
import org.springframework.ai.anthropic.AnthropicChatModel
import org.springframework.ai.anthropic.AnthropicChatOptions
-import org.springframework.ai.anthropic.api.AnthropicApi
-import org.springframework.ai.anthropic.api.AnthropicCacheOptions
-import org.springframework.ai.anthropic.api.AnthropicCacheStrategy
import org.springframework.ai.chat.messages.MessageType
import org.springframework.ai.model.tool.ToolCallingManager
import org.springframework.beans.factory.ObjectProvider
@@ -166,18 +165,20 @@ class AnthropicModelsConfig(
* Creates an individual Anthropic model from configuration, applying full model
* definition settings (thinking mode, token budgets, pricing, etc.) that are not
* needed in the BYOK path.
+ *
+ * Spring AI 2.0 dropped the spring-retry `RetryTemplate` parameter on the chat model
+ * builder; retries are wrapped at the ChatClientLlmOperations layer instead.
*/
private fun createAnthropicLlm(modelDef: AnthropicModelDefinition): LlmService<*> {
val chatModel = AnthropicChatModel
.builder()
- .defaultOptions(createDefaultOptions(modelDef))
- .anthropicApi(createAnthropicApi())
+ .options(createDefaultOptions(modelDef))
+ .anthropicClient(createAnthropicClient())
.toolCallingManager(
ToolCallingManager.builder()
.observationRegistry(observationRegistry)
.build()
)
- .retryTemplate(properties.retryTemplate("anthropic-${modelDef.modelId}"))
.observationRegistry(observationRegistry)
.build()
@@ -201,6 +202,10 @@ class AnthropicModelsConfig(
/**
* Creates default options for a model based on YAML configuration.
+ *
+ * Spring AI 2.0 replaced the `AnthropicApi.ChatCompletionRequest.ThinkingConfig`
+ * constructor with first-class `thinkingEnabled(tokenBudget)` / `thinkingDisabled()`
+ * methods on the options builder.
*/
private fun createDefaultOptions(modelDef: AnthropicModelDefinition): AnthropicChatOptions {
return AnthropicChatOptions.builder()
@@ -212,19 +217,12 @@ class AnthropicModelsConfig(
modelDef.topK?.let { topK(it) }
// Configure thinking mode if specified
- modelDef.thinking?.let { thinkingConfig ->
- thinking(
- AnthropicApi.ChatCompletionRequest.ThinkingConfig(
- AnthropicApi.ThinkingType.ENABLED,
- thinkingConfig.tokenBudget
- )
- )
- } ?: thinking(
- AnthropicApi.ChatCompletionRequest.ThinkingConfig(
- AnthropicApi.ThinkingType.DISABLED,
- null
- )
- )
+ val thinkingBudget = modelDef.thinking?.tokenBudget
+ if (thinkingBudget != null && thinkingBudget > 0) {
+ thinkingEnabled(thinkingBudget.toLong())
+ } else {
+ thinkingDisabled()
+ }
}
.build()
}
@@ -244,15 +242,14 @@ object AnthropicOptionsConverter : OptionsConverter {
.temperature(options.temperature)
.topP(options.topP)
.maxTokens(options.maxTokens ?: DEFAULT_MAX_TOKENS)
- .thinking(
- if (options.thinking?.enabled == true) AnthropicApi.ChatCompletionRequest.ThinkingConfig(
- AnthropicApi.ThinkingType.ENABLED,
- options.thinking!!.tokenBudget,
- ) else AnthropicApi.ChatCompletionRequest.ThinkingConfig(
- AnthropicApi.ThinkingType.DISABLED,
- null,
- )
- )
+ .apply {
+ val thinkingBudget = options.thinking?.tokenBudget
+ if (options.thinking?.enabled == true && thinkingBudget != null) {
+ thinkingEnabled(thinkingBudget.toLong())
+ } else {
+ thinkingDisabled()
+ }
+ }
.topK(options.topK)
// Apply Anthropic caching if configured
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/UsageExtensions.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/UsageExtensions.kt
index d9bf606cd..435cd3789 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/UsageExtensions.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/anthropic/UsageExtensions.kt
@@ -14,14 +14,20 @@
* limitations under the License.
*/
@file:JvmName("AnthropicUsage")
+
package com.embabel.agent.config.models.anthropic
+import com.anthropic.models.messages.Usage as AnthropicSdkUsage
import com.embabel.agent.core.Usage
-import org.springframework.ai.anthropic.api.AnthropicApi
/**
* Extension functions for convenient access to Anthropic-specific usage metrics,
* particularly prompt caching information.
+ *
+ * Spring AI 2.0 swapped its hand-rolled `AnthropicApi.Usage` (with `int` accessors) for
+ * the anthropic-java SDK's [AnthropicSdkUsage] (with `Optional` accessors). The
+ * extensions below preserve the previous return type (`Int?`) for API stability —
+ * callers see the same shape, with absent values surfaced as null.
*/
/**
@@ -32,7 +38,8 @@ import org.springframework.ai.anthropic.api.AnthropicApi
* over regular input tokens.
*/
fun Usage.anthropicCacheCreationTokens(): Int? {
- return (nativeUsage as? AnthropicApi.Usage)?.cacheCreationInputTokens()
+ val sdkUsage = nativeUsage as? AnthropicSdkUsage ?: return null
+ return sdkUsage.cacheCreationInputTokens().orElse(null)?.toInt()
}
/**
@@ -42,7 +49,8 @@ fun Usage.anthropicCacheCreationTokens(): Int? {
* Cache read tokens cost 90% less than regular input tokens, providing significant savings.
*/
fun Usage.anthropicCacheReadTokens(): Int? {
- return (nativeUsage as? AnthropicApi.Usage)?.cacheReadInputTokens()
+ val sdkUsage = nativeUsage as? AnthropicSdkUsage ?: return null
+ return sdkUsage.cacheReadInputTokens().orElse(null)?.toInt()
}
/**
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfigTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfigTest.kt
index 18109a952..542e7f1e5 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfigTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicCachingConfigTest.kt
@@ -18,7 +18,7 @@ package com.embabel.agent.config.models.anthropic
import com.embabel.chat.MessageRole
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
-import org.springframework.ai.anthropic.api.AnthropicCacheTtl
+import org.springframework.ai.anthropic.AnthropicCacheTtl
class AnthropicCachingConfigTest {
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicOptionsConverterTest.kt
index b1abb673c..8fa7239cb 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicOptionsConverterTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/anthropic/AnthropicOptionsConverterTest.kt
@@ -21,12 +21,12 @@ import com.embabel.common.ai.model.LlmOptions
import com.embabel.common.ai.model.Thinking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
+import org.springframework.ai.anthropic.AnthropicCacheStrategy
+import org.springframework.ai.anthropic.AnthropicCacheTtl
import org.springframework.ai.anthropic.AnthropicChatOptions
-import org.springframework.ai.anthropic.api.AnthropicApi
-import org.springframework.ai.anthropic.api.AnthropicCacheStrategy
-import org.springframework.ai.anthropic.api.AnthropicCacheTtl
class AnthropicOptionsConverterTest : OptionsConverterTestSupport(
optionsConverter = AnthropicOptionsConverter
@@ -35,14 +35,17 @@ class AnthropicOptionsConverterTest : OptionsConverterTestSupport
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-bedrock-autoconfigure
@@ -56,6 +56,14 @@
embabel-agent-test-internal
test
+
+
+
+ org.springframework.boot
+ spring-boot-jackson
+ test
+
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfiguration.java b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfiguration.java
index 893d5daea..789b7eb6d 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfiguration.java
+++ b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfiguration.java
@@ -16,8 +16,13 @@
package com.embabel.agent.autoconfigure.models.bedrock;
import com.embabel.agent.config.models.bedrock.BedrockModelsConfig;
+import io.micrometer.observation.ObservationRegistry;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
/**
@@ -25,10 +30,25 @@
*
* This class serves as a Spring Boot autoconfiguration entry point that:
* - Imports the [BedrockModelsConfig] configuration to dynamically register Bedrock model beans
+ *
+ *
Also exposes a default {@link ToolCallingManager} bean. Spring AI 2.0's
+ * {@code BedrockConverseProxyChatAutoConfiguration#bedrockProxyChatModel(...)} factory method
+ * autowires a {@code ToolCallingManager} (parameter 4), but Spring AI 2.0 ships no auto-config
+ * that creates one — embabel factories build them inline per model. We register a primary
+ * shared {@code ToolCallingManager} here (gated with {@link ConditionalOnMissingBean}) so the
+ * Spring AI Bedrock proxy-chat auto-config can wire up alongside embabel's own model beans
+ * without crashing context refresh.
*/
@AutoConfiguration
@AutoConfigureBefore(name = {"com.embabel.agent.autoconfigure.platform.AgentPlatformAutoConfiguration"})
@Import(BedrockModelsConfig.class)
public class AgentBedrockAutoConfiguration {
+ @Bean
+ @ConditionalOnMissingBean
+ public ToolCallingManager toolCallingManager(ObjectProvider observationRegistry) {
+ return ToolCallingManager.builder()
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .build();
+ }
}
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/bedrock/BedrockModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/bedrock/BedrockModelsConfig.kt
index ca4fca453..22f034878 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/bedrock/BedrockModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/bedrock/BedrockModelsConfig.kt
@@ -27,6 +27,7 @@ import com.embabel.common.util.ExcludeFromJacocoGeneratedReport
import io.micrometer.observation.ObservationRegistry
import org.slf4j.LoggerFactory
import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel
+import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingOptions
import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi
import org.springframework.ai.bedrock.converse.BedrockChatOptions
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel
@@ -34,15 +35,12 @@ import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel
import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi
import org.springframework.ai.chat.model.ChatModel
import org.springframework.ai.chat.observation.ChatModelObservationConvention
-import org.springframework.ai.model.ModelOptionsUtils
import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration
import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties
import org.springframework.ai.model.bedrock.cohere.autoconfigure.BedrockCohereEmbeddingProperties
import org.springframework.ai.model.bedrock.titan.autoconfigure.BedrockTitanEmbeddingProperties
-import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate
import org.springframework.ai.model.tool.ToolCallingChatOptions
import org.springframework.ai.model.tool.ToolCallingManager
-import org.springframework.ai.model.tool.ToolExecutionEligibilityPredicate
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.boot.context.properties.ConfigurationProperties
@@ -238,7 +236,7 @@ class BedrockModelsConfig(
embeddingDef.modelId,
credentialsProvider,
regionProvider.region,
- ModelOptionsUtils.OBJECT_MAPPER,
+ tools.jackson.databind.json.JsonMapper.builder().build(),
connectionProperties.timeout,
), observationRegistry.getIfUnique { ObservationRegistry.NOOP }
).withInputType(bedrockTitanEmbeddingProperties.inputType),
@@ -254,10 +252,13 @@ class BedrockModelsConfig(
embeddingDef.modelId,
credentialsProvider,
regionProvider.region,
- ModelOptionsUtils.OBJECT_MAPPER,
+ tools.jackson.databind.json.JsonMapper.builder().build(),
connectionProperties.timeout
),
- bedrockCohereEmbeddingProperties.options
+ BedrockCohereEmbeddingOptions.builder()
+ .inputType(bedrockCohereEmbeddingProperties.options.inputType)
+ .truncate(bedrockCohereEmbeddingProperties.options.truncate)
+ .build()
),
provider = PROVIDER,
)
@@ -314,8 +315,6 @@ class EmbabelBedrockProxyChatModelBuilder internal constructor() {
private var region: Region? = Region.US_EAST_1
private var timeout: java.time.Duration? = java.time.Duration.ofMinutes(10)
private var toolCallingManager: ToolCallingManager? = null
- private var toolExecutionEligibilityPredicate: ToolExecutionEligibilityPredicate =
- DefaultToolExecutionEligibilityPredicate()
private var defaultOptions = BedrockChatOptions.builder().build()
private var observationRegistry = ObservationRegistry.NOOP
private var customObservationConvention: ChatModelObservationConvention? = null
@@ -328,12 +327,6 @@ class EmbabelBedrockProxyChatModelBuilder internal constructor() {
return this
}
- fun toolExecutionEligibilityPredicate(toolExecutionEligibilityPredicate: ToolExecutionEligibilityPredicate):
- EmbabelBedrockProxyChatModelBuilder {
- this.toolExecutionEligibilityPredicate = toolExecutionEligibilityPredicate
- return this
- }
-
fun credentialsProvider(credentialsProvider: AwsCredentialsProvider): EmbabelBedrockProxyChatModelBuilder {
this.credentialsProvider = credentialsProvider
return this
@@ -403,8 +396,7 @@ class EmbabelBedrockProxyChatModelBuilder internal constructor() {
bedrockRuntimeAsyncClient,
defaultOptions,
observationRegistry,
- toolCallingManager ?: defaultToolCallingManager,
- toolExecutionEligibilityPredicate
+ toolCallingManager ?: defaultToolCallingManager
).apply {
if (customObservationConvention != null) {
setObservationConvention(customObservationConvention)
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfigurationIT.java b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfigurationIT.java
index d07822e4e..ee8c0aa7b 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfigurationIT.java
+++ b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/java/com/embabel/agent/autoconfigure/models/bedrock/AgentBedrockAutoConfigurationIT.java
@@ -20,7 +20,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
-import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/bedrock/EmbabelBedrockProxyChatModelBuilderTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/bedrock/EmbabelBedrockProxyChatModelBuilderTest.kt
index 7c21f3995..74757bfab 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/bedrock/EmbabelBedrockProxyChatModelBuilderTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-bedrock-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/bedrock/EmbabelBedrockProxyChatModelBuilderTest.kt
@@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.ai.bedrock.converse.BedrockChatOptions
import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention
-import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate
import org.springframework.ai.model.tool.ToolCallingManager
import org.springframework.boot.test.system.CapturedOutput
import org.springframework.boot.test.system.OutputCaptureExtension
@@ -62,7 +61,6 @@ class EmbabelBedrockProxyChatModelBuilderTest {
val chatModel = EmbabelBedrockProxyChatModelBuilder()
.toolCallingManager(toolCallingManager)
- .toolExecutionEligibilityPredicate(DefaultToolExecutionEligibilityPredicate())
.customObservationConvention(observationConvention)
.credentialsProvider(credentialsProvider)
.region(region)
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/pom.xml
index f3952bf9e..0e5abc6c6 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekModelsConfig.kt
index d370a5e93..00db79df0 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekModelsConfig.kt
@@ -152,13 +152,16 @@ class DeepSeekModelsConfig(
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build()
)
- .defaultOptions(
+ .options(
DeepSeekChatOptions.builder()
.model(name)
.build()
)
.deepSeekApi(createDeepSeekApi())
- .retryTemplate(properties.retryTemplate(name))
+ // Spring AI 2.0 builder now expects org.springframework.core.retry.RetryTemplate;
+ // we already wrap calls with spring-retry at the ChatClientLlmOperations layer,
+ // so the model-internal retry is redundant. Dropping the call falls back to
+ // Spring AI's default retry (no-op if not configured).
.build()
return SpringAiLlmService(
name = name,
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/pom.xml
index 4dc9862eb..b3ba5f25c 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-dockermodels-autoconfigure
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/docker/DockerLocalModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/docker/DockerLocalModelsConfig.kt
index 276ea30ca..a2787f667 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/docker/DockerLocalModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-dockermodels-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/docker/DockerLocalModelsConfig.kt
@@ -24,16 +24,16 @@ import com.embabel.common.ai.autoconfig.ProviderInitialization
import com.embabel.common.ai.autoconfig.RegisteredModel
import com.embabel.common.ai.model.*
import com.embabel.common.util.ExcludeFromJacocoGeneratedReport
+import com.openai.client.OpenAIClient
+import com.openai.client.okhttp.OpenAIOkHttpClient
import io.micrometer.observation.ObservationRegistry
import org.slf4j.LoggerFactory
import org.springframework.ai.document.MetadataMode
-import org.springframework.ai.model.NoopApiKey
import org.springframework.ai.model.tool.ToolCallingManager
import org.springframework.ai.openai.OpenAiChatModel
import org.springframework.ai.openai.OpenAiChatOptions
import org.springframework.ai.openai.OpenAiEmbeddingModel
import org.springframework.ai.openai.OpenAiEmbeddingOptions
-import org.springframework.ai.openai.api.OpenAiApi
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.boot.context.properties.ConfigurationProperties
@@ -43,7 +43,6 @@ import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.client.RestClient
import org.springframework.web.client.body
-import org.springframework.web.reactive.function.client.WebClient
@ConfigurationProperties(prefix = PREFIX)
@@ -90,6 +89,12 @@ class DockerConnectionProperties {
* discovery fails silently and no models are registered.
* Model names will be precisely as reported from
* http://localhost:12434/engines/v1/models (assuming default port).
+ *
+ * Spring AI 2.0 swapped its hand-rolled `OpenAiApi` for the openai-java SDK
+ * ([OpenAIClient]). The migration removes Spring's `RestClient`/`WebClient` from
+ * the OpenAI HTTP path entirely — the SDK uses OkHttp internally. Spring AI 2.0
+ * also dropped the spring-retry `RetryTemplate` parameter on `OpenAiChatModel.Builder`;
+ * retries are now wrapped at the ChatClientLlmOperations layer instead.
*/
@ExcludeFromJacocoGeneratedReport(reason = "Docker model configuration can't be unit tested")
@Configuration(proxyBeanMethods = false)
@@ -99,7 +104,8 @@ class DockerConnectionProperties {
ConfigurableModelProviderProperties::class
)
class DockerLocalModelsConfig(
- private val dockerRetryProperties: DockerRetryProperties,
+ @Suppress("UNUSED_PARAMETER")
+ dockerRetryProperties: DockerRetryProperties,
private val dockerConnectionProperties: DockerConnectionProperties,
private val configurableBeanFactory: ConfigurableBeanFactory,
private val properties: ConfigurableModelProviderProperties,
@@ -120,6 +126,20 @@ class DockerLocalModelsConfig(
val id: String,
)
+ /**
+ * Shared OpenAI-compatible client pointing at the local Docker endpoint.
+ * Built lazily on first model creation so a missing endpoint doesn't crash
+ * Spring startup (loadModels already swallows the failure).
+ */
+ private val openAiClient: OpenAIClient by lazy {
+ OpenAIOkHttpClient.builder()
+ .baseUrl(dockerConnectionProperties.baseUrl)
+ // The openai-java SDK rejects null/blank API keys even when the
+ // backing server doesn't require auth. Placeholder is fine.
+ .apiKey("no-auth")
+ .build()
+ }
+
private fun loadModels(): List =
try {
val restClient = RestClient.create()
@@ -192,14 +212,12 @@ class DockerLocalModelsConfig(
private fun dockerEmbeddingServiceOf(model: Model): SpringAiEmbeddingService {
val springEmbeddingModel = OpenAiEmbeddingModel(
- OpenAiApi.Builder()
- .baseUrl(dockerConnectionProperties.baseUrl)
- .apiKey(NoopApiKey())
- .build(),
+ openAiClient,
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
.model(model.id)
.build(),
+ observationRegistry.getIfUnique { ObservationRegistry.NOOP },
)
return SpringAiEmbeddingService(
@@ -211,32 +229,18 @@ class DockerLocalModelsConfig(
private fun dockerLlmOf(model: Model): SpringAiLlmService {
val chatModel = OpenAiChatModel.builder()
- .openAiApi(
- OpenAiApi.Builder()
- .baseUrl(dockerConnectionProperties.baseUrl)
- .apiKey(NoopApiKey())
- .restClientBuilder(
- RestClient.builder()
- .observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
- )
- .webClientBuilder(
- WebClient.builder()
- .observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
- )
- .build()
- )
+ .openAiClient(openAiClient)
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.toolCallingManager(
ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build()
)
- .defaultOptions(
+ .options(
OpenAiChatOptions.builder()
.model(model.id)
.build()
)
- .retryTemplate(dockerRetryProperties.retryTemplate("docker-${model.id}"))
.build()
return SpringAiLlmService(
name = model.id,
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-gemini-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-gemini-autoconfigure/pom.xml
index a42b6dcf5..890432819 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-gemini-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-gemini-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/pom.xml
index ce7a5d206..266e1e2a3 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiModelsConfig.kt
index c46fa1f71..91f017b21 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiModelsConfig.kt
@@ -38,7 +38,7 @@ import io.micrometer.observation.ObservationRegistry
import org.slf4j.LoggerFactory
import org.springframework.ai.google.genai.GoogleGenAiChatModel
import org.springframework.ai.google.genai.GoogleGenAiChatOptions
-import org.springframework.ai.google.genai.GoogleGenAiEmbeddingConnectionDetails
+import org.springframework.ai.google.genai.embedding.GoogleGenAiEmbeddingConnectionDetails
import org.springframework.ai.google.genai.text.GoogleGenAiTextEmbeddingModel
import org.springframework.ai.google.genai.text.GoogleGenAiTextEmbeddingOptions
import org.springframework.ai.model.tool.ToolCallingManager
@@ -215,7 +215,10 @@ class GoogleGenAiModelsConfig(
ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build(),
- properties.retryTemplate("googlegenai-${modelDef.modelId}"),
+ // Spring AI 2.0 now requires org.springframework.core.retry.RetryTemplate here;
+ // we wrap calls with spring-retry at the ChatClientLlmOperations layer, so this
+ // model-internal retry is redundant — pass an empty core.retry instance.
+ org.springframework.core.retry.RetryTemplate(),
observationRegistry.getIfUnique { ObservationRegistry.NOOP }
)
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/pom.xml
index 079415c07..6f6bd97d7 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-lmstudio-autoconfigure
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/lmstudio/LmStudioModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/lmstudio/LmStudioModelsConfig.kt
index 661683173..750900ace 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/lmstudio/LmStudioModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-lmstudio-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/lmstudio/LmStudioModelsConfig.kt
@@ -24,7 +24,7 @@ import com.embabel.common.ai.autoconfig.RegisteredModel
import com.embabel.common.ai.model.PricingModel
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
-import com.fasterxml.jackson.databind.ObjectMapper
+import tools.jackson.databind.ObjectMapper
import io.micrometer.observation.ObservationRegistry
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.ObjectProvider
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/pom.xml
index c067bbd1b..789cb9355 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/minimax/MiniMaxOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/minimax/MiniMaxOptionsConverterTest.kt
index c87f52aee..955f08e91 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/minimax/MiniMaxOptionsConverterTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-minimax-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/minimax/MiniMaxOptionsConverterTest.kt
@@ -53,7 +53,11 @@ class MiniMaxOptionsConverterTest : OptionsConverterTestSupport
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-mistral-ai-autoconfigure
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-mistral-ai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/mistralai/MistralAiModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-mistral-ai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/mistralai/MistralAiModelsConfig.kt
index 75296d73f..9c1df8e0a 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-mistral-ai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/mistralai/MistralAiModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-mistral-ai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/mistralai/MistralAiModelsConfig.kt
@@ -150,14 +150,16 @@ class MistralAiModelsConfig(
private fun createMistralAiLlm(modelDef: MistralAiModelDefinition): LlmService<*> {
val mistralChatModel = MistralAiChatModel
.builder()
- .defaultOptions(createDefaultOptions(modelDef))
+ .options(createDefaultOptions(modelDef))
.mistralAiApi(createMistralAiApi())
.toolCallingManager(
ToolCallingManager.builder()
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build()
)
- .retryTemplate(properties.retryTemplate("mistral-ai-${modelDef.modelId}"))
+ // Spring AI 2.0 builder now expects org.springframework.core.retry.RetryTemplate;
+ // we wrap with spring-retry at ChatClientLlmOperations, so omitted here (defaults).
+ //.retryTemplate(properties.retryTemplate("mistral-ai-${modelDef.modelId}"))
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build()
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/pom.xml
index 630504770..2a99787c9 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiChatOptions.kt b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiChatOptions.kt
index 0869b7a38..d9dab1d3e 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiChatOptions.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiChatOptions.kt
@@ -106,28 +106,37 @@ class OciGenAiChatOptions internal constructor(
override fun getToolCallbacks(): List = toolCallbacks
- override fun setToolCallbacks(toolCallbacks: MutableList) {
+ fun setToolCallbacks(toolCallbacks: List) {
this.toolCallbacks = toolCallbacks
}
- override fun getToolNames(): Set = toolNames
+ // toolNames removed from Spring AI 2.0 ToolCallingChatOptions; kept as an OCI-internal property.
+ fun getToolNames(): Set = toolNames
- override fun setToolNames(toolNames: MutableSet) {
+ fun setToolNames(toolNames: Set) {
this.toolNames = toolNames
}
- override fun getInternalToolExecutionEnabled(): Boolean? = internalToolExecutionEnabled
+ // internalToolExecutionEnabled removed from Spring AI 2.0 ToolCallingChatOptions; kept as an OCI-internal property.
+ fun getInternalToolExecutionEnabled(): Boolean? = internalToolExecutionEnabled
- override fun setInternalToolExecutionEnabled(internalToolExecutionEnabled: Boolean?) {
+ fun setInternalToolExecutionEnabled(internalToolExecutionEnabled: Boolean?) {
this.internalToolExecutionEnabled = internalToolExecutionEnabled
}
override fun getToolContext(): Map = toolContext
- override fun setToolContext(toolContext: MutableMap) {
+ fun setToolContext(toolContext: Map) {
this.toolContext = toolContext
}
+ /**
+ * Spring AI 2.0 contract: return a builder seeded with this instance's state.
+ * Preserves OCI-specific fields (compartmentId, servingMode, endpointId, apiFormat)
+ * in addition to the standard chat/tool-calling options.
+ */
+ override fun mutate(): ToolCallingChatOptions.Builder<*> = Builder(copy())
+
fun merge(runtimeOptions: ChatOptions?): OciGenAiChatOptions {
val merged = copy()
if (runtimeOptions == null) {
@@ -154,6 +163,10 @@ class OciGenAiChatOptions internal constructor(
if (runtimeOptions.apiFormatExplicit) {
merged.apiFormat = runtimeOptions.apiFormat
}
+ if (runtimeOptions.toolNames.isNotEmpty()) {
+ merged.toolNames = runtimeOptions.toolNames
+ }
+ runtimeOptions.internalToolExecutionEnabled?.let { merged.internalToolExecutionEnabled = it }
}
return merged
}
@@ -165,17 +178,16 @@ class OciGenAiChatOptions internal constructor(
if (runtimeOptions.toolCallbacks.isNotEmpty()) {
merged.toolCallbacks = runtimeOptions.toolCallbacks
}
- if (runtimeOptions.toolNames.isNotEmpty()) {
- merged.toolNames = runtimeOptions.toolNames
- }
if (runtimeOptions.toolContext.isNotEmpty()) {
merged.toolContext = merged.toolContext + runtimeOptions.toolContext
}
- runtimeOptions.internalToolExecutionEnabled?.let { merged.internalToolExecutionEnabled = it }
+ // toolNames / internalToolExecutionEnabled were removed from Spring AI 2.0 ToolCallingChatOptions;
+ // they are OCI-internal now and merged in merge() when both sides are OciGenAiChatOptions.
}
+ // ChatOptions.copy() was removed in Spring AI 2.0; kept as an OCI-internal deep-copy helper.
@Suppress("UNCHECKED_CAST")
- override fun copy(): T =
+ fun copy(): T =
OciGenAiChatOptions(
model = model,
compartmentId = compartmentId,
@@ -199,40 +211,71 @@ class OciGenAiChatOptions internal constructor(
fun builder() = Builder()
}
- class Builder {
- private val options = OciGenAiChatOptions()
+ class Builder internal constructor(
+ private var options: OciGenAiChatOptions = OciGenAiChatOptions(),
+ ) : ToolCallingChatOptions.Builder {
- fun model(model: String?) = apply { options.setModel(model) }
+ // OCI-specific options (not part of the Spring AI builder contract)
- fun compartmentId(compartmentId: String?) = apply { options.compartmentId = compartmentId }
+ fun compartmentId(compartmentId: String?): Builder = apply { options.compartmentId = compartmentId }
- fun servingMode(servingMode: OciGenAiServingMode) = apply { options.servingMode = servingMode }
+ fun servingMode(servingMode: OciGenAiServingMode): Builder = apply { options.servingMode = servingMode }
- fun endpointId(endpointId: String?) = apply { options.endpointId = endpointId }
+ fun endpointId(endpointId: String?): Builder = apply { options.endpointId = endpointId }
- fun apiFormat(apiFormat: OciGenAiApiFormat) = apply { options.apiFormat = apiFormat }
+ fun apiFormat(apiFormat: OciGenAiApiFormat): Builder = apply { options.apiFormat = apiFormat }
- fun maxTokens(maxTokens: Int?) = apply { options.setMaxTokens(maxTokens) }
+ // ChatOptions.Builder contract
- fun temperature(temperature: Double?) = apply { options.setTemperature(temperature) }
+ override fun model(model: String?): Builder = apply { options.setModel(model) }
- fun topP(topP: Double?) = apply { options.setTopP(topP) }
+ override fun frequencyPenalty(frequencyPenalty: Double?): Builder =
+ apply { options.setFrequencyPenalty(frequencyPenalty) }
- fun topK(topK: Int?) = apply { options.setTopK(topK) }
+ override fun maxTokens(maxTokens: Int?): Builder = apply { options.setMaxTokens(maxTokens) }
- fun frequencyPenalty(frequencyPenalty: Double?) = apply { options.setFrequencyPenalty(frequencyPenalty) }
+ override fun presencePenalty(presencePenalty: Double?): Builder =
+ apply { options.setPresencePenalty(presencePenalty) }
- fun presencePenalty(presencePenalty: Double?) = apply { options.setPresencePenalty(presencePenalty) }
+ override fun stopSequences(stopSequences: List?): Builder =
+ apply { options.setStopSequences(stopSequences) }
- fun stopSequences(stopSequences: List?) = apply { options.setStopSequences(stopSequences) }
+ override fun temperature(temperature: Double?): Builder = apply { options.setTemperature(temperature) }
- fun toolCallbacks(toolCallbacks: List?) = apply {
- options.setToolCallbacks(toolCallbacks?.toMutableList() ?: mutableListOf())
- }
+ override fun topK(topK: Int?): Builder = apply { options.setTopK(topK) }
+
+ override fun topP(topP: Double?): Builder = apply { options.setTopP(topP) }
+
+ override fun combineWith(other: ChatOptions.Builder<*>): Builder =
+ apply { options = options.merge(other.build()) }
+
+ override fun clone(): Builder = Builder(options.copy())
- fun internalToolExecutionEnabled(internalToolExecutionEnabled: Boolean?) =
+ // ToolCallingChatOptions.Builder contract
+
+ override fun toolCallbacks(toolCallbacks: List): Builder =
+ apply { options.setToolCallbacks(toolCallbacks) }
+
+ override fun toolCallbacks(vararg toolCallbacks: ToolCallback): Builder =
+ apply { options.setToolCallbacks(toolCallbacks.toList()) }
+
+ // toolNames / internalToolExecutionEnabled removed from Spring AI 2.0 ToolCallingChatOptions.Builder;
+ // kept as OCI-internal builder methods.
+ fun toolNames(toolNames: Set): Builder =
+ apply { options.setToolNames(toolNames) }
+
+ fun toolNames(vararg toolNames: String): Builder =
+ apply { options.setToolNames(toolNames.toSet()) }
+
+ fun internalToolExecutionEnabled(internalToolExecutionEnabled: Boolean?): Builder =
apply { options.setInternalToolExecutionEnabled(internalToolExecutionEnabled) }
- fun build(): OciGenAiChatOptions = options.copy()
+ override fun toolContext(toolContext: Map): Builder =
+ apply { options.setToolContext(toolContext) }
+
+ override fun toolContext(key: String, value: Any): Builder =
+ apply { options.setToolContext(options.toolContext + (key to value)) }
+
+ override fun build(): OciGenAiChatOptions = options.copy()
}
}
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiOptionsConverterTest.kt
index d83a7646f..460ce0c2b 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiOptionsConverterTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-oci-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ocigenai/OciGenAiOptionsConverterTest.kt
@@ -48,7 +48,7 @@ class OciGenAiOptionsConverterTest : OptionsConverterTestSupport
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-ollama-autoconfigure
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfig.kt b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfig.kt
index 93ca5d25c..53d702be8 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfig.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfig.kt
@@ -161,7 +161,7 @@ class OllamaModelsConfig(
)
.build()
)
- .defaultOptions(
+ .options(
OllamaChatOptions.builder()
.model(uniqueModelName)
.build()
@@ -206,7 +206,7 @@ class OllamaModelsConfig(
)
.build()
)
- .defaultOptions(
+ .options(
OllamaEmbeddingOptions.builder()
.model(uniqueModelName)
.build()
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfigTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfigTest.kt
index 85c9caf70..ec34e1372 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfigTest.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaModelsConfigTest.kt
@@ -89,6 +89,12 @@ class OllamaModelsConfigTest {
every { mockRequestHeadersSpec.accept(MediaType.APPLICATION_JSON) } returns mockRequestHeadersSpec
every { mockRequestHeadersSpec.retrieve() } returns mockResponseSpec
+ // Spring 7's `org.springframework.web.client.body` reified Kotlin extension now
+ // invokes `.hint(KType, T)` on the ResponseSpec before `.body(...)` so converters
+ // can pick up the Kotlin reified type. Stub it to return the same mock so the
+ // chain proceeds to `.body(...)`.
+ every { mockResponseSpec.hint(any(), any()) } returns mockResponseSpec
+
// Mock the body method using any() matcher to avoid type issues
every { mockResponseSpec.body(any>()) } returns testModels
}
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-onnx-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-onnx-autoconfigure/pom.xml
index 2691e2a5d..f9ff193df 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-onnx-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-onnx-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-onnx-autoconfigure
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/pom.xml b/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/pom.xml
index e91b6fd79..5e056bbef 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/pom.xml
+++ b/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/pom.xml
@@ -4,7 +4,7 @@
com.embabel.agent
embabel-agent-autoconfigure
- 1.0.0-RC1-SNAPSHOT
+ 2.0.0-SNAPSHOT
../../pom.xml
embabel-agent-openai-autoconfigure
@@ -55,6 +55,14 @@
embabel-agent-platform-autoconfigure
test
+
+
+
+ org.springframework.boot
+ spring-boot-jackson
+ test
+
diff --git a/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/openai/OpenAiNativeStructuredOutputConfigurer.kt b/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/openai/OpenAiNativeStructuredOutputConfigurer.kt
index ef879a3b0..85b66d0b1 100644
--- a/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/openai/OpenAiNativeStructuredOutputConfigurer.kt
+++ b/embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/openai/OpenAiNativeStructuredOutputConfigurer.kt
@@ -19,11 +19,9 @@ import com.embabel.agent.spi.loop.StructuredOutputRequest
import com.embabel.agent.spi.support.springai.SpringAiNativeStructuredOutputConfigurer
import com.embabel.common.ai.autoconfig.NativeSupport
import com.embabel.common.ai.model.LlmMetadata
-import com.fasterxml.jackson.core.type.TypeReference
-import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.ai.chat.prompt.ChatOptions
+import org.springframework.ai.openai.OpenAiChatModel.ResponseFormat
import org.springframework.ai.openai.OpenAiChatOptions
-import org.springframework.ai.openai.api.ResponseFormat
/**
* OpenAI-specific native structured-output bridge.
@@ -38,8 +36,6 @@ import org.springframework.ai.openai.api.ResponseFormat
*/
internal object OpenAiNativeStructuredOutputConfigurer : SpringAiNativeStructuredOutputConfigurer {
- private val objectMapper = jacksonObjectMapper()
-
@Suppress("UNUSED_PARAMETER")
override fun configure(
options: ChatOptions,
@@ -60,21 +56,12 @@ internal object OpenAiNativeStructuredOutputConfigurer : SpringAiNativeStructure
val responseFormat = ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
- .jsonSchema(
- ResponseFormat.JsonSchema.builder()
- .name(structuredOutput.name)
- .schema(parseSchema(structuredOutput.schema))
- .strict(structuredOutput.strict)
- .build()
- )
+ .jsonSchema(structuredOutput.schema)
.build()
- return options.copy().also { copied ->
- copied.responseFormat = responseFormat
- copied.outputSchema = structuredOutput.schema
- }
+ return options.mutate()
+ .responseFormat(responseFormat)
+ .outputSchema(structuredOutput.schema)
+ .build()
}
-
- private fun parseSchema(schema: String): Map =
- objectMapper.readValue(schema, object : TypeReference