Skip to content

Upgrade to Spring Boot 4.1 and Spring AI 2.0#50

Merged
trialiya merged 8 commits into
mainfrom
claude/clever-ramanujan-oucoiw
Jun 21, 2026
Merged

Upgrade to Spring Boot 4.1 and Spring AI 2.0#50
trialiya merged 8 commits into
mainfrom
claude/clever-ramanujan-oucoiw

Conversation

@trialiya

Copy link
Copy Markdown
Owner

Summary

Upgrades the backend from Spring Boot 3.5.15 + Spring AI 1.1.6 to Spring Boot 4.1.0 + Spring AI 2.0.0, along with related dependency updates (Tomcat 11, Micrometer 1.17, Jackson 3, etc.). Includes necessary configuration adjustments and test import fixes for the new framework versions.

Key Changes

  • Framework upgrades:

    • Spring Boot: 3.5.15 → 4.1.0
    • Spring AI: 1.1.6 → 2.0.0
    • Tomcat: 10.1.55 → 11.0.22
    • Micrometer: 1.15.12 → 1.17.0
    • Jackson: 2.21.4 (with dataformat-toml/yaml removed)
    • HikariCP: 6.3.3 → 7.0.2
    • Flyway: 11.7.2 → 12.4.0
    • JSON Schema validator: 2.0.0 → 3.0.1
    • victools jsonschema: 4.38.0 → 5.0.0
  • New dependencies:

    • com.openai:openai-java-core:4.39.1 (OpenAI SDK)
    • com.squareup.okhttp3:okhttp:4.12.0 + okio (HTTP client)
    • Netty 4.2.15 (reactor-netty-core/http 1.3.6)
    • org.jetbrains.kotlin:kotlin-reflect:2.3.21
    • commons-logging:commons-logging:1.3.6
  • Spring Boot 4 migration fixes:

    • Added spring-boot-flyway module (Spring Boot 4 extracted FlywayAutoConfiguration)
    • Updated test imports: @DataJdbcTest and @AutoConfigureTestDatabase moved to org.springframework.boot.data.jdbc.test.autoconfigure and org.springframework.boot.jdbc.test.autoconfigure
    • Added @ImportAutoConfiguration(FlywayAutoConfiguration.class) to test base classes to ensure migrations run in test slices
  • Tool preparing advisor:

    • Added ToolPreparingAdvisor (new StreamAdvisor at LOWEST_PRECEDENCE) to detect tool calls early and publish TOOL_PREPARING events
    • Integrated into ChatConfig as innermost advisor in the chain
    • Passes RUN_ID_PARAM through advisor context for event correlation
    • Removed preparing flag logic from ChatRunService (now handled by advisor)
  • Documentation:

    • Added docs/features/tool-preparing.md — explains the TOOL_PREPARING event, why it's currently disabled, and implementation options (silence detection on frontend vs. OpenAI SDK decorator)
    • Added docs/проект/диагностика-tool-preparing-стриминг.md — diagnostic guide for understanding tool call streaming behavior in Spring AI 2.0
  • Frontend:

    • Disabled TOOL_PREPARING event handler in chatEventReducer.js with detailed comment explaining why the signal arrives too late (OpenAI SDK buffers tool-call deltas internally)
  • Build & config:

    • Updated .gitignore to exclude Spring Boot 4 build artifacts (META-INF/)
    • Enabled root logging level in application.yaml (INFO)

Implementation Notes

The ToolPreparingAdvisor is registered as the innermost advisor (highest precedence toward the model) to observe tool calls as early as possible within the Spring AI advisor chain. However, due to OpenAI SDK's internal buffering of tool-call deltas via bufferUntil, the signal still arrives nearly simultaneously with TOOL_CALL. The documentation outlines why earlier detection requires either:

  1. Frontend-side silence detection (recommended, no backend changes)
    2

https://claude.ai/code/session_013neSEixYXJoMdozigeAex7

claude and others added 8 commits June 21, 2026 14:13
The early "preparing a tool call" signal (944e1ac) never fires. In Spring
AI 1.1.x MessageAggregator strips toolCalls from the streamed
AssistantMessage (spring-ai#3366 / #5167), so hasToolCallDelta() — which
reads getOutput().getToolCalls() — is always false at the subscriber, and
with internal tool execution the finishReason=tool_calls boundary may not
reach the subscriber at all.

Add opt-in diagnostics (kb.diag.stream-tool-calls, default true on this
branch) that log the exact per-chunk stream shape at three vantage points
so the real behavior can be confirmed empirically:
- subscriber: what reaches ChatRunService.onNext (where the detector runs)
- advisor:    innermost StreamAdvisor (can a ChatConfig advisor catch it?)
- TOOL STARTED: real tool start, to measure the silent gap before a call

Each chunk logs textLen/empty/finishReason/hasToolCalls/outToolCalls.
INFO = interesting chunks (empty text / finishReason / tool calls),
DEBUG = every chunk (logger io.github.trialiya.kb.diag).

No production behavior change when disabled. Doc with how-to-read and the
solution options (1.1.x manual loop vs Spring AI 2.0 ToolCallingAdvisor)
in docs/проект/диагностика-tool-preparing-стриминг.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
First live run proved the subscriber-layer conclusions: on every chunk
outToolCalls=[] and hasToolCalls=false (incl. empty chunks), and the only
finishReason ever seen is STOP — TOOL_CALLS never reaches the subscriber.
So both hasToolCallDelta() and the "TOOL_CALLS".equals(finishReason)
fallback are dead in streaming (spring-ai#3366 + internal tool loop
swallows the boundary).

Two probe fixes:
- "interesting" gate now ignores empty-string finishReason (intermediate
  stream chunks carry "" not null), so INFO is the genuinely interesting
  subset (empty text / real finishReason / tool calls) and text flow drops
  to DEBUG — much less noise.
- The advisor produced zero lines, so make it unmistakable: log
  registration at startup, add SUBSCRIBED / finally markers + a chunk
  counter, and log advisor chunks unconditionally at INFO. This settles
  "registered but bypassed" vs "not registered" on the next run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
…visor

Root cause: In Spring AI 1.x, tool calls were processed entirely inside ChatModel
internals. StreamAdvisors (even at LOWEST_PRECEDENCE) never saw tool-call chunks
or finish_reason=TOOL_CALLS, making TOOL_PREPARING impossible to signal reliably.

Spring AI 2.0.0 moves the tool-call loop into ToolCallingAdvisor (outermost
advisor). A new ToolPreparingAdvisor at LOWEST_PRECEDENCE (innermost) now sits
inside the loop and sees raw model streaming chunks on each iteration — it
detects tool-call intent and publishes the TOOL_PREPARING event before execution.

Changes:
- Bump springAiVersion 1.1.6 → 2.0.0 in build.gradle; regenerate lockfile
- Exclude spring-boot-reactor/spring-boot-jackson (Spring Boot 4.x-only modules
  pulled transitively by spring-ai-starter-model-openai) that break Boot 3.x
  context loading with ClassNotFoundException on EnvironmentPostProcessor
- Add ToolPreparingAdvisor (new): innermost StreamAdvisor that publishes
  TOOL_PREPARING once per tool-call round before the tool executes
- Rewire ChatConfig: ToolCallingAdvisor → MessageChatMemoryAdvisor →
  ToolPreparingAdvisor (innermost); remove StreamDiagnosticsAdvisor
- Fix ChatClientRequestSpec.options() call sites (ChatRunService, ChatController,
  ChatModelClientIT): 2.0.0 API now accepts Builder, not built ChatOptions
- Remove SearchAgentService.internalToolExecutionEnabled(false) — method removed
  in 2.0.0 (chatModel.call() no longer auto-executes tools)
- Delete StreamDiagnostics and StreamDiagnosticsAdvisor (diagnostic code)
- Remove kb.diag config from application.yaml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
Gradle extracts spring.factories from Spring Boot 4.x transitive deps
into the project root during dependency resolution. This is a build
artifact (not project source) and should not be tracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
The previous comment had the chain backwards — ToolCallingAdvisor was
listed as outermost, but MessageChatMemoryAdvisor (order MIN+200) is
actually more outermost than ToolCallingAdvisor (order MIN+300).

Actual chain, outermost → innermost:
  1. MessageChatMemoryAdvisor (MIN+200) — OUTSIDE the tool loop:
     loads history once, saves user+assistant only. No tool messages
     are written — our JDBC repository does not support them, and this
     matches Spring AI 1.x behaviour.
  2. ToolCallingAdvisor (MIN+300) — drives the tool-call loop.
     Memory is outside the loop so internal conversation accumulation
     is correct and .disableInternalConversationHistory() is not needed.
  3. ToolPreparingAdvisor (MAX) — INSIDE the loop: emits TOOL_PREPARING
     before each tool execution round.

No functional change — Spring AI sorts advisors by getOrder() regardless
of insertion order, so behaviour was already correct. List order and
comment now match the actual execution order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
Spring AI 2.0.0 is built against Spring Boot 4 / Spring Framework 7; its
autoconfiguration classes reference Boot 4-only APIs (e.g. the chat-memory-jdbc
autoconfig needs org.springframework.boot.jdbc.init.DatabaseInitializationProperties),
so the app could not boot on Boot 3.5.15. Bump to Boot 4.1.0 and fix the fallout.

build.gradle:
- Spring Boot plugin 3.5.15 -> 4.1.0; drop the spring-boot-reactor/-jackson
  excludes (those are legitimate Boot 4 modules now).
- Boot 4 split many autoconfigurations out of spring-boot-autoconfigure into
  per-technology modules that aren't pulled transitively:
  * add spring-boot-flyway — FlywayAutoConfiguration moved here; flyway-core no
    longer brings it, so without it migrations never run (app startup AND
    @DataJdbcTest slices).
- Boot 4 makes Jackson 3 (tools.jackson) the auto-configured mapper and demotes
  Jackson 2 to runtime-only. Our internal serialization still targets Jackson 2,
  so add jackson-datatype-jsr310 back onto the compile classpath. Web layer uses
  Jackson 3; both coexist (supported migration state).
- Boot 4 no longer manages Testcontainers versions: import testcontainers-bom.
- Boot 4 moved test slices into spring-boot-<module>-test artifacts: add
  spring-boot-data-jdbc-test (@DataJdbcTest) and spring-boot-jdbc-test
  (@AutoConfigureTestDatabase).
- Regenerate gradle.lockfile (Framework 7, Security 7, Data 4, Jackson 2+3,
  JUnit 6, Tomcat 11, H2 2.4.240, Flyway 12).

tests:
- Update relocated slice imports: ...test.autoconfigure.data.jdbc.DataJdbcTest ->
  ...data.jdbc.test.autoconfigure.DataJdbcTest, and the jdbc AutoConfigureTestDatabase.
- @DataJdbcTest no longer imports Flyway (and @OverrideAutoConfiguration disables
  the rest), so @ImportAutoConfiguration(FlywayAutoConfiguration.class) on the
  Postgres IT base class and on DocumentServiceUnitTest.
- ChatModelClientIT: Spring AI 2.0.0 reads ChatModel.getOptions() in
  DefaultChatClientUtils — stub it (was NPEing on null).
- DocumentServiceUnitTest: Spring 7 canonicalizes 422 as UNPROCESSABLE_CONTENT
  (HttpStatus.valueOf(422)); update expectations.
- SearchAgentServiceIT: 2.0.0 leaves an unset tool-callback list null (1.x: empty)
  — assert isNullOrEmpty for the tool-less summarization call.

Verified on JDK 21: spotlessCheck, all *Test unit tests, and SearchAgentServiceIT
pass. Docker-backed *IT tests and full app boot to be verified locally on JDK 25.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
…able

TOOL_PREPARING arrives immediately before TOOL_CALL because
OpenAiChatModel.internalStream buffers all tool-call deltas via
bufferUntil/ChunkMerger before emitting — every public extension
point (StreamAdvisor, observation) sits above that buffer.

chatEventReducer: handler reduced to no-op with explanation.
docs/features/tool-preparing.md: documents the buffer mechanism,
all investigated hooks, and the two viable alternatives
(frontend silence-timer / AsyncStreamResponse decorator).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013neSEixYXJoMdozigeAex7
@trialiya trialiya merged commit 10aaf73 into main Jun 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants