diff --git a/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicModelBindingTest.kt b/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicModelBindingTest.kt new file mode 100644 index 000000000..4bf120736 --- /dev/null +++ b/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicModelBindingTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2024-2026 Embabel Pty Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.embabel.agent.anthropic + +import com.embabel.agent.spi.support.springai.SpringAiLlmService +import com.embabel.common.ai.model.LlmOptions +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.ai.anthropic.AnthropicChatOptions + +/** + * Regression for the Spring AI 2.0 model-binding bug affecting Anthropic: + * [AnthropicOptionsConverter] never calls .model(), so its [AnthropicChatOptions] carry + * DEFAULT_MODEL ("claude-haiku-4-5"). Spring AI 2.0 no longer merges the ChatModel bean's + * configured model into per-request options, so haiku would go on the wire silently. + * [SpringAiLlmService.convertOptions] now stamps the service name explicitly. + */ +class AnthropicModelBindingTest { + + @Test + fun `convertOptions stamps configured model not AnthropicChatOptions haiku default`() { + val service = SpringAiLlmService( + name = "claude-sonnet-4-5", + provider = "Anthropic", + chatModel = mockk(relaxed = true), + optionsConverter = AnthropicOptionsConverter, + ) + + val result = service.convertOptions(LlmOptions()) + + assertThat(result).isInstanceOf(AnthropicChatOptions::class.java) + assertThat(result.model).isEqualTo("claude-sonnet-4-5") + assertThat(result.model).isNotEqualTo(AnthropicChatOptions.DEFAULT_MODEL) + } + + @Test + fun `convertOptions preserves AnthropicChatOptions fields alongside model`() { + val service = SpringAiLlmService( + name = "claude-sonnet-4-5", + provider = "Anthropic", + chatModel = mockk(relaxed = true), + optionsConverter = AnthropicOptionsConverter, + ) + + val result = service.convertOptions(LlmOptions().withMaxTokens(500)) + + assertThat(result).isInstanceOf(AnthropicChatOptions::class.java) + assertThat(result.model).isEqualTo("claude-sonnet-4-5") + assertThat(result.maxTokens).isEqualTo(500) + } + + @Test + fun `convertOptions result type is AnthropicChatOptions not generic fallback`() { + val service = SpringAiLlmService( + name = "claude-haiku-4-5", + provider = "Anthropic", + chatModel = mockk(relaxed = true), + optionsConverter = AnthropicOptionsConverter, + ) + + val result = service.convertOptions(LlmOptions()) + + assertThat(result).isInstanceOf(AnthropicChatOptions::class.java) + assertThat(result.model).isEqualTo("claude-haiku-4-5") + } +} diff --git a/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicOptionsConverterTest.kt b/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicOptionsConverterTest.kt index 5b15bfd00..e7da35d3a 100644 --- a/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicOptionsConverterTest.kt +++ b/embabel-agent-anthropic/src/test/kotlin/com/embabel/agent/anthropic/AnthropicOptionsConverterTest.kt @@ -28,6 +28,8 @@ import org.springframework.ai.anthropic.AnthropicCacheStrategy import org.springframework.ai.anthropic.AnthropicCacheTtl import org.springframework.ai.anthropic.AnthropicChatOptions +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class AnthropicOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = AnthropicOptionsConverter ) { diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/ChatClientLlmOperations.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/ChatClientLlmOperations.kt index 4b0b59e3c..d5151f6ba 100644 --- a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/ChatClientLlmOperations.kt +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/ChatClientLlmOperations.kt @@ -189,7 +189,7 @@ internal class ChatClientLlmOperations( ): LlmMessageSender { if (llmRequestEvent != null) { val springAiLlm = requireSpringAiLlm(llm) - val chatOptions = springAiLlm.optionsConverter.convertOptions(options) + val chatOptions = springAiLlm.convertOptions(options) val instrumentedModel = InstrumentedChatModel(springAiLlm.chatModel, llmRequestEvent) return SpringAiLlmMessageSender( chatModel = instrumentedModel, @@ -348,7 +348,7 @@ internal class ChatClientLlmOperations( val schemaFormat = converter?.getFormat() - val chatOptions = requireSpringAiLlm(llm).optionsConverter.convertOptions(interaction.llm) + val chatOptions = requireSpringAiLlm(llm).convertOptions(interaction.llm) val timeoutMillis = getTimeoutMillis(interaction.llm) val basePrompt = if (schemaFormat != null) { @@ -507,7 +507,7 @@ internal class ChatClientLlmOperations( // Get the complete format (examples + JSON schema) val schemaFormat = converter.getFormat() - val chatOptions = requireSpringAiLlm(llm).optionsConverter.convertOptions(interaction.llm) + val chatOptions = requireSpringAiLlm(llm).convertOptions(interaction.llm) val timeoutMillis = getTimeoutMillis(interaction.llm) val basePrompt = buildPromptWithMaybeReturnAndSchema( diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmService.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmService.kt index 8f5fe2e41..3444f0539 100644 --- a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmService.kt +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmService.kt @@ -28,6 +28,7 @@ import org.springframework.ai.chat.client.ChatClient import org.springframework.ai.chat.messages.UserMessage 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 reactor.core.publisher.Flux import java.time.Duration @@ -85,7 +86,9 @@ private object StreamingCapabilityVerifier { * @param name Name of the LLM * @param provider Name of the provider (e.g., "OpenAI", "Anthropic") * @param chatModel The Spring AI ChatModel to use for LLM calls - * @param optionsConverter Function to convert [LlmOptions] to Spring AI ChatOptions + * @param optionsConverter Function to convert [LlmOptions] to Spring AI ChatOptions. + * Do not call [OptionsConverter.convertOptions] directly — use [SpringAiLlmService.convertOptions] + * which also stamps the configured model name. * @param knowledgeCutoffDate Model's knowledge cutoff date, if known * @param promptContributors List of prompt contributors for this model. * Knowledge cutoff is automatically included if knowledgeCutoffDate is set. @@ -123,11 +126,13 @@ data class SpringAiLlmService @JvmOverloads constructor( */ override val model: ChatModel get() = chatModel + fun convertOptions(llmOptions: LlmOptions): ChatOptions = + optionsConverter.convertOptions(llmOptions, name) + override fun createMessageSender(options: LlmOptions): LlmMessageSender { - val chatOptions = optionsConverter.convertOptions(options) return SpringAiLlmMessageSender( chatModel = chatModel, - chatOptions = chatOptions, + chatOptions = convertOptions(options), toolResponseContentAdapter = toolResponseContentAdapter, nativeStructuredOutputConfigurer = nativeStructuredOutputConfigurer, nativeSupport = nativeSupport, @@ -136,9 +141,8 @@ data class SpringAiLlmService @JvmOverloads constructor( } override fun createMessageStreamer(options: LlmOptions): LlmMessageStreamer { - val chatOptions = optionsConverter.convertOptions(options) val chatClient = ChatClient.create(chatModel) - return SpringAiLlmMessageStreamer(chatClient, chatOptions) + return SpringAiLlmMessageStreamer(chatClient, convertOptions(options)) } override fun supportsStreaming(): Boolean = StreamingCapabilityVerifier.supportsStreaming(chatModel) diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperations.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperations.kt index 3ada4bf91..734edbe86 100644 --- a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperations.kt +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/streaming/StreamingChatClientOperations.kt @@ -174,7 +174,7 @@ internal class StreamingChatClientOperations( val userMessages = messages.filterIsInstance() validateUserInput(userMessages, interaction, llmRequestEvent?.agentProcess?.blackboard) - val chatOptions = requireSpringAiLlm(llm).optionsConverter.convertOptions(interaction.llm) + val chatOptions = requireSpringAiLlm(llm).convertOptions(interaction.llm) // Resolve tool groups and decorate tools val tools = chatClientLlmOperations.resolveAndDecorateTools(interaction, agentProcess, action) @@ -346,7 +346,7 @@ internal class StreamingChatClientOperations( // Chat Client val chatClient = chatClientLlmOperations.createChatClient(llm) // Chat Options, additional potential option "streaming" - val chatOptions = requireSpringAiLlm(llm).optionsConverter.convertOptions(interaction.llm) + val chatOptions = requireSpringAiLlm(llm).convertOptions(interaction.llm) // Spring AI 2.0's StreamingJacksonOutputConverter requires T : Any; // erase O via Class for the construction, cast result back at use sites. diff --git a/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/ChatClientLlmOperationsTest.kt b/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/ChatClientLlmOperationsTest.kt index 9d25b1a4a..a2b27f3f4 100644 --- a/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/ChatClientLlmOperationsTest.kt +++ b/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/ChatClientLlmOperationsTest.kt @@ -49,6 +49,7 @@ import io.mockk.mockk import io.mockk.slot import jakarta.validation.Validation import jakarta.validation.constraints.Pattern +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -1323,4 +1324,26 @@ class ChatClientLlmOperationsTest { } } + @Nested + inner class ModelBinding { + + @Test + fun `model name from SpringAiLlmService reaches ChatModel call`() { + val duke = Dog("Duke") + val fakeChatModel = FakeChatModel(jacksonObjectMapper().writeValueAsString(duke)) + val setup = createChatClientLlmOperations(fakeChatModel) + + setup.llmOperations.createObject( + messages = listOf(UserMessage("prompt")), + interaction = LlmInteraction(id = InteractionId("id"), llm = LlmOptions()), + outputClass = Dog::class.java, + action = SimpleTestAgent.actions.first(), + agentProcess = setup.mockAgentProcess, + ) + + assertThat(fakeChatModel.optionsPassed).isNotEmpty() + assertThat(fakeChatModel.optionsPassed[0].model).isEqualTo("fake") + } + } + } diff --git a/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmServiceTest.kt b/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmServiceTest.kt index 5eccb4de9..90849c229 100644 --- a/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmServiceTest.kt +++ b/embabel-agent-api/src/test/kotlin/com/embabel/agent/spi/support/springai/SpringAiLlmServiceTest.kt @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.ai.chat.model.ChatModel import org.springframework.ai.chat.prompt.ChatOptions +import org.springframework.ai.model.tool.ToolCallingChatOptions import java.time.LocalDate class SpringAiLlmServiceTest { @@ -249,6 +250,45 @@ class SpringAiLlmServiceTest { } } + @Nested + inner class ConvertOptionsTests { + + @Test + fun `convertOptions stamps service name as model`() { + val service = SpringAiLlmService( + name = "my-model", + provider = "Provider", + chatModel = mockChatModel, + ) + assertThat(service.convertOptions(LlmOptions()).model).isEqualTo("my-model") + } + + @Test + fun `convertOptions overrides converter default model with service name`() { + val service = SpringAiLlmService( + name = "my-model", + provider = "Provider", + chatModel = mockChatModel, + ) + val result = service.convertOptions(LlmOptions()) + assertThat(result.model).isEqualTo("my-model") + assertThat(result.model).isNotEqualTo("some-default-model") + } + + @Test + fun `convertOptions preserves converter fields alongside model`() { + val service = SpringAiLlmService( + name = "my-model", + provider = "Provider", + chatModel = mockChatModel, + ) + val result = service.convertOptions(LlmOptions().withTemperature(0.5).withMaxTokens(100)) + assertThat(result.model).isEqualTo("my-model") + assertThat(result.temperature).isEqualTo(0.5) + assertThat(result.maxTokens).isEqualTo(100) + } + } + @Nested inner class CreateMessageSenderTests { @@ -272,7 +312,7 @@ class SpringAiLlmServiceTest { val customConverter = object : OptionsConverter { override fun convertOptions(options: LlmOptions): ChatOptions { converterCalled = true - return mockk() + return ToolCallingChatOptions.builder().build() } } val service = SpringAiLlmService( @@ -314,7 +354,7 @@ class SpringAiLlmServiceTest { val customConverter = object : OptionsConverter { override fun convertOptions(options: LlmOptions): ChatOptions { converterCalled = true - return mockk() + return ToolCallingChatOptions.builder().build() } } val service = SpringAiLlmService( diff --git a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/java/com/embabel/agent/config/models/anthropic/LlmAnthropicCachingIT.java b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/java/com/embabel/agent/config/models/anthropic/LlmAnthropicCachingIT.java index 79db9197e..511dede5f 100644 --- a/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/java/com/embabel/agent/config/models/anthropic/LlmAnthropicCachingIT.java +++ b/embabel-agent-autoconfigure/models/embabel-agent-anthropic-autoconfigure/src/test/java/com/embabel/agent/config/models/anthropic/LlmAnthropicCachingIT.java @@ -308,7 +308,7 @@ public Integer calculateDewPoint(Integer temperatureFahrenheit, Integer relative @Test void testSystemPromptCaching() { - logger.info("Testing system prompt caching"); + logger.info("Testing system prompt caching with thinking enabled"); AnthropicCachingConfig cachingConfig = new AnthropicCachingConfig(); cachingConfig.setSystemPrompt(true); diff --git a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekOptionsConverterTest.kt index 271955322..6377465b5 100644 --- a/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekOptionsConverterTest.kt +++ b/embabel-agent-autoconfigure/models/embabel-agent-deepseek-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/deepseek/DeepSeekOptionsConverterTest.kt @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.ai.deepseek.DeepSeekChatOptions +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class DeepSeekOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = DeepSeekOptionsConverter ) { diff --git a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiOptionsConverterTest.kt index 3339a4ff7..884374cd4 100644 --- a/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiOptionsConverterTest.kt +++ b/embabel-agent-autoconfigure/models/embabel-agent-google-genai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/googlegenai/GoogleGenAiOptionsConverterTest.kt @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.springframework.ai.google.genai.GoogleGenAiChatOptions +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class GoogleGenAiOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = GoogleGenAiOptionsConverter ) { 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 955f08e91..9fb5095d7 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 @@ -22,6 +22,8 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.springframework.ai.openai.OpenAiChatOptions +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class MiniMaxOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = MiniMaxOptionsConverter ) { diff --git a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaOptionsConverterTest.kt index 332ce91e1..f18344a4a 100644 --- a/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaOptionsConverterTest.kt +++ b/embabel-agent-autoconfigure/models/embabel-agent-ollama-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/ollama/OllamaOptionsConverterTest.kt @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test import org.springframework.ai.ollama.api.OllamaChatOptions import org.springframework.ai.ollama.api.ThinkOption +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class OllamaOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = OllamaOptionsConverter ) { diff --git a/embabel-agent-autoconfigure/models/embabel-agent-zai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/zai/ZaiOptionsConverterTest.kt b/embabel-agent-autoconfigure/models/embabel-agent-zai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/zai/ZaiOptionsConverterTest.kt index 6cac281a1..430ad30f7 100644 --- a/embabel-agent-autoconfigure/models/embabel-agent-zai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/zai/ZaiOptionsConverterTest.kt +++ b/embabel-agent-autoconfigure/models/embabel-agent-zai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/zai/ZaiOptionsConverterTest.kt @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.springframework.ai.openai.OpenAiChatOptions +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). class ZaiOptionsConverterTest : OptionsConverterTestSupport( optionsConverter = ZaiOptionsConverter ) { diff --git a/embabel-agent-common/embabel-agent-ai/src/main/kotlin/com/embabel/common/ai/model/OptionsConverter.kt b/embabel-agent-common/embabel-agent-ai/src/main/kotlin/com/embabel/common/ai/model/OptionsConverter.kt index ff344ec21..0f5be9fa2 100644 --- a/embabel-agent-common/embabel-agent-ai/src/main/kotlin/com/embabel/common/ai/model/OptionsConverter.kt +++ b/embabel-agent-common/embabel-agent-ai/src/main/kotlin/com/embabel/common/ai/model/OptionsConverter.kt @@ -19,10 +19,25 @@ import org.springframework.ai.chat.prompt.ChatOptions import org.springframework.ai.model.tool.ToolCallingChatOptions /** - * Convert our LLM options to Spring AI ChatOptions + * Convert our LLM options to Spring AI ChatOptions. + * + * Prefer [convertOptions] with an explicit model over the no-model overload. + * The no-model form is deprecated because provider converters carry hardcoded + * default models that will silently go to the wire if not overridden. */ +// TODO: update all converter implementations to override convertOptions(options, model) directly, +// then remove the deprecated 1-arg form and the @Suppress below. fun interface OptionsConverter { + + @Deprecated( + message = "Provide the model explicitly — provider converters carry hardcoded defaults that bypass the configured model.", + replaceWith = ReplaceWith("convertOptions(options, model)"), + ) fun convertOptions(options: LlmOptions): O + + @Suppress("DEPRECATION") + fun convertOptions(options: LlmOptions, model: String): ChatOptions = + convertOptions(options).mutate().model(model).build() } /** diff --git a/embabel-agent-test-support/embabel-agent-test-internal/src/main/kotlin/com/embabel/agent/test/models/optionsConverterTestUtils.kt b/embabel-agent-test-support/embabel-agent-test-internal/src/main/kotlin/com/embabel/agent/test/models/optionsConverterTestUtils.kt index 6677a3721..794966a20 100644 --- a/embabel-agent-test-support/embabel-agent-test-internal/src/main/kotlin/com/embabel/agent/test/models/optionsConverterTestUtils.kt +++ b/embabel-agent-test-support/embabel-agent-test-internal/src/main/kotlin/com/embabel/agent/test/models/optionsConverterTestUtils.kt @@ -19,6 +19,8 @@ import com.embabel.common.ai.model.LlmOptions import com.embabel.common.ai.model.OptionsConverter import org.junit.jupiter.api.Assertions.assertEquals +// Calls the deprecated 1-arg convertOptions() directly to verify field mapping in isolation. +// Model stamping is not tested here — it is covered by OptionsConverter.convertOptions(options, model). fun checkOptionsConverterPreservesCoreValues(optionsConverter: OptionsConverter<*>) { val llmo = LlmOptions().withTemperature(temperature = 0.5).withTopK(10).withTopP(.2).withFrequencyPenalty(.2) val options = optionsConverter.convertOptions(llmo)