Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.annotation.concurrent.ThreadSafe
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.chat.client.ChatClientCustomizer
import org.springframework.ai.chat.client.ChatClientBuilderCustomizer
import org.springframework.ai.chat.client.ResponseEntity
import org.springframework.ai.chat.client.advisor.observation.DefaultAdvisorObservationConvention
import org.springframework.ai.chat.client.observation.DefaultChatClientObservationConvention
Expand Down Expand Up @@ -128,7 +128,7 @@ internal class ChatClientLlmOperations(
// chat-client spans intact, since this registry stays injected with the real bean.
private val observationRegistry: ObservationRegistry = ObservationRegistry.NOOP,
instrumentation: AgentInstrumentation = NoOpAgentInstrumentation,
private val customizers: List<ChatClientCustomizer> = emptyList(),
private val customizers: List<ChatClientBuilderCustomizer> = emptyList(),
asyncer: Asyncer,
toolLoopFactory: ToolLoopFactory = ToolLoopFactory.create(ToolLoopConfiguration(), asyncer, AutoCorrectionPolicy()),
@Value("\${embabel.agent.platform.streaming.use-legacy-streaming:false}")
Expand Down Expand Up @@ -363,7 +363,7 @@ internal class ChatClientLlmOperations(
// Resolve tool groups and decorate tools
val tools = resolveAndDecorateTools(interaction, agentProcess, action)

// Spring AI 2.0: ChatClient merges chatModel.getDefaultOptions() with prompt.options
// Spring AI 2.0: ChatClient merges chatModel.getOptions() with prompt.options
// and adds spec-level toolCallbacks last. We bake toolCallbacks into the ToolCallingChatOptions
// (preserving the subtype through the merge) AND also pass them via .toolCallbacks() on the
// request spec — the latter survives Spring AI's options merge that would otherwise reset
Expand All @@ -385,7 +385,7 @@ internal class ChatClientLlmOperations(
val future = asyncer.async {
chatClient
.prompt(springAiPrompt)
.toolCallbacks(springAiToolCallbacks)
.tools(springAiToolCallbacks)
.call()
}

Expand Down Expand Up @@ -543,7 +543,7 @@ internal class ChatClientLlmOperations(
val future = asyncer.async {
chatClient
.prompt(springAiPrompt)
.toolCallbacks(springAiToolCallbacks)
.tools(springAiToolCallbacks)
.call()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal class InstrumentedChatModel(
// it does NOT delegate Java default methods to the delegate instance.
// Instead it calls the interface's own default implementation, which
// would silently break:
// - getDefaultOptions() would return a bare ChatOptions instead of
// - getOptions() would return a bare ChatOptions instead of
// the delegate's model-specific options (e.g. OpenAiChatOptions)
// - stream(Prompt) would throw UnsupportedOperationException
// instead of using the delegate's streaming implementation
Expand All @@ -92,7 +92,7 @@ internal class InstrumentedChatModel(
// decision about whether to delegate or instrument.
// -------------------------------------------------------------------

override fun getDefaultOptions(): ChatOptions = delegate.defaultOptions
override fun getOptions(): ChatOptions = delegate.options

override fun stream(prompt: Prompt): Flux<ChatResponse> = delegate.stream(prompt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ internal class SpringAiLlmMessageStreamer(

return chatClient
.prompt(prompt)
.toolCallbacks(toolCallbacks)
.tools(toolCallbacks)
.stream()
.content()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ internal class StreamingChatClientOperations(
val promptWithOptions = Prompt(springAiPrompt.instructions, effectiveOptions)
chatClient
.prompt(promptWithOptions)
.toolCallbacks(springAiToolCallbacks)
.tools(springAiToolCallbacks)
.stream()
.content()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ import reactor.core.publisher.Flux
class FakeStreamingChatModel(
private val response: String,
// Spring AI 2.0 ToolCallAdvisor requires the ChatClientRequest options to be
// ToolCallingChatOptions; the merge is rooted on ChatModel.getDefaultOptions().
// ToolCallingChatOptions; the merge is rooted on ChatModel.getOptions().
private val options: ChatOptions = ToolCallingChatOptions.builder().build(),
) : ChatModel {

override fun getDefaultOptions(): ChatOptions = options
override fun getOptions(): ChatOptions = options

override fun call(prompt: Prompt): ChatResponse {
return ChatResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import org.springframework.ai.chat.messages.AssistantMessage as SpringAssistantM
*/
class GuardRailTestFakeChatModel(
val responses: List<String>,
// Spring AI 2.0: ChatClient merges via getDefaultOptions; use ToolCallingChatOptions
// Spring AI 2.0: ChatClient merges via getOptions; use ToolCallingChatOptions
// so the subtype survives the merge into the final Prompt.
private val options: ChatOptions = ToolCallingChatOptions.builder().build(),
) : ChatModel {
Expand All @@ -86,7 +86,7 @@ class GuardRailTestFakeChatModel(
val promptsPassed = mutableListOf<Prompt>()
val optionsPassed = mutableListOf<ToolCallingChatOptions>()

override fun getDefaultOptions(): ChatOptions = options
override fun getOptions(): ChatOptions = options

override fun call(prompt: Prompt): ChatResponse {
promptsPassed.add(prompt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import kotlin.test.assertEquals
*/
class FakeChatModel(
val responses: List<String>,
// Spring AI 2.0: ChatClient merges options via ChatModel.getDefaultOptions(); the merged
// Spring AI 2.0: ChatClient merges options via ChatModel.getOptions(); the merged
// result inherits the default's runtime type. Default to ToolCallingChatOptions so tool
// callbacks and the subtype survive the merge — even when the prompt sets its own options.
private val options: ChatOptions = ToolCallingChatOptions.builder().build(),
Expand All @@ -94,7 +94,7 @@ class FakeChatModel(
val promptsPassed = mutableListOf<Prompt>()
val optionsPassed = mutableListOf<ChatOptions>()

override fun getDefaultOptions(): ChatOptions = options
override fun getOptions(): ChatOptions = options

override fun call(prompt: Prompt): ChatResponse {
promptsPassed.add(prompt)
Expand Down Expand Up @@ -604,12 +604,11 @@ class ChatClientLlmOperationsTest {
inner class DelayingFakeChatModel(
private val response: String,
private val delayMillis: Long,
options: ChatOptions = ToolCallingChatOptions.builder().build(),
private val options: ChatOptions = ToolCallingChatOptions.builder().build(),
) : ChatModel {
private val defaultOptions = options
val callCount = java.util.concurrent.atomic.AtomicInteger(0)

override fun getDefaultOptions(): ChatOptions = defaultOptions
override fun getOptions(): ChatOptions = options

override fun call(prompt: Prompt): ChatResponse {
callCount.incrementAndGet()
Expand Down Expand Up @@ -937,7 +936,7 @@ class ChatClientLlmOperationsTest {
inner class ErrorThrowingChatModel(
private val exception: RuntimeException = RuntimeException("401 Unauthorized: Invalid API key")
) : ChatModel {
override fun getDefaultOptions(): ChatOptions = ToolCallingChatOptions.builder().build()
override fun getOptions(): ChatOptions = ToolCallingChatOptions.builder().build()
override fun call(prompt: Prompt): ChatResponse = throw exception
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,20 @@ class InstrumentedChatModelTest {
inner class DefaultOptionsTests {

@Test
fun `delegates getDefaultOptions to underlying ChatModel`() {
fun `delegates getOptions to underlying ChatModel`() {
val expectedOptions: ChatOptions = mockk()
every { delegate.defaultOptions } returns expectedOptions
every { delegate.options } returns expectedOptions

val result = instrumentedModel.defaultOptions
val result = instrumentedModel.options

assertThat(result).isSameAs(expectedOptions)
}

@Test
fun `does not emit any event`() {
every { delegate.defaultOptions } returns mockk()
every { delegate.options } returns mockk()

instrumentedModel.defaultOptions
instrumentedModel.options

verify(exactly = 0) { processContext.onProcessEvent(any()) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ class SpringAiLlmServiceTest {
@Nested
inner class CreateMessageStreamerTests {

// Relaxed mock needed because ChatClient.create() calls chatModel.getDefaultOptions()
// Relaxed mock needed because ChatClient.create() calls chatModel.getOptions()
private val relaxedChatModel: ChatModel = mockk(relaxed = true)

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class SpringAiLlmMessageStreamerTest {
// survives Spring AI's options merge that would otherwise reset the toolCallbacks
// list to the chat model's empty default.
every { mockChatClient.prompt(capture(capturedPrompt)) } returns mockRequestSpec
every { mockRequestSpec.toolCallbacks(any<List<ToolCallback>>()) } returns mockRequestSpec
every { mockRequestSpec.tools(any<List<ToolCallback>>()) } returns mockRequestSpec
every { mockRequestSpec.stream() } returns mockStreamSpec
}

Expand All @@ -87,7 +87,7 @@ class SpringAiLlmMessageStreamerTest {

// Then
verify { mockChatClient.prompt(any<Prompt>()) }
verify { mockRequestSpec.toolCallbacks(any<List<ToolCallback>>()) }
verify { mockRequestSpec.tools(any<List<ToolCallback>>()) }
verify { mockRequestSpec.stream() }
// Spring AI 2.0: prompt.options is the rebuilt ToolCallingChatOptions.
assertTrue(capturedPrompt.captured.options is ToolCallingChatOptions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import org.springframework.ai.chat.messages.AssistantMessage as SpringAssistantM
*/
class StreamingGuardRailTestFakeChatModel(
val responses: List<String>,
// Spring AI 2.0: ChatClient merges via getDefaultOptions; use ToolCallingChatOptions so
// Spring AI 2.0: ChatClient merges via getOptions; use ToolCallingChatOptions so
// the subtype survives the merge.
private val options: ChatOptions = ToolCallingChatOptions.builder().build(),
) : ChatModel {
Expand All @@ -88,7 +88,7 @@ class StreamingGuardRailTestFakeChatModel(
val promptsPassed = mutableListOf<Prompt>()
val optionsPassed = mutableListOf<ChatOptions>()

override fun getDefaultOptions(): ChatOptions = options
override fun getOptions(): ChatOptions = options

override fun call(prompt: Prompt): ChatResponse {
promptsPassed.add(prompt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ class StreamingChatClientOperationsTest {
val mockContentStreamSpec = mockk<ChatClient.StreamResponseSpec>(relaxed = true)

every { mockChatClient.prompt(any<Prompt>()) } returns mockRequestSpec
every { mockRequestSpec.toolCallbacks(any<List<ToolCallback>>()) } returns mockRequestSpec
every { mockRequestSpec.tools(any<List<ToolCallback>>()) } returns mockRequestSpec
every { mockRequestSpec.options(any()) } returns mockRequestSpec
every { mockRequestSpec.stream() } returns mockContentStreamSpec
every { mockContentStreamSpec.content() } returns chunkFlux
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,14 @@ class DockerLocalModelsConfig(
}

private fun dockerEmbeddingServiceOf(model: Model): SpringAiEmbeddingService {
val springEmbeddingModel = OpenAiEmbeddingModel(
openAiClient,
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
val springEmbeddingModel = OpenAiEmbeddingModel.builder()
.openAiClient(openAiClient)
.metadataMode(MetadataMode.EMBED)
.options(OpenAiEmbeddingOptions.builder()
.model(model.id)
.build(),
observationRegistry.getIfUnique { ObservationRegistry.NOOP },
)
.build())
.observationRegistry(observationRegistry.getIfUnique { ObservationRegistry.NOOP })
.build()

return SpringAiEmbeddingService(
name = model.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class OciGenAiChatModel(
return toSpringChatResponse(response.chatResult, options)
}

override fun getDefaultOptions(): ChatOptions = defaultOptions.copy()
override fun getOptions(): ChatOptions = defaultOptions.copy()

internal fun chatRequest(prompt: Prompt, options: OciGenAiChatOptions): BaseChatRequest =
when (options.apiFormat) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,14 @@ open class OpenAiCompatibleModelFactory(
configuredDimensions: Int? = null,
pricingModel: PricingModel? = null,
): EmbeddingService {
val embeddingModel = OpenAiEmbeddingModel(
openAiClient,
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
val embeddingModel = OpenAiEmbeddingModel.builder()
.openAiClient(openAiClient)
.metadataMode(MetadataMode.EMBED)
.options(OpenAiEmbeddingOptions.builder()
.model(model)
.build(),
observationRegistry,
)
.build())
.observationRegistry(observationRegistry)
.build()
return SpringAiEmbeddingService(
name = model,
model = embeddingModel,
Expand Down