Files
spring-ai/advisors
Claude 527f4ba7ff Add advisors module: custom logging, PII redaction and token-budget advisors
Tests pin down chain ordering (including ties), BaseAdvisor stream behaviour, redaction order versus memory and logging, the tool loop, and how a refusal surfaces on calls, streams and over HTTP.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ja4jkzrbQ4LQZBNrb5mkZE
2026-09-24 16:21:19 +00:00
..

advisors

Companion code for Writing Custom Advisors in Spring AI 2.0: Logging, PII Redaction and Token Budgets, part of the Spring AI series on ankurm.com.

Three advisors of our own -- a logger, a PII redactor and a token budget -- plus the tests that pin down how the advisor chain orders them, what happens on a stream, and what a caller sees when one of them refuses a request.

There is no live model anywhere. RecordingModel records every prompt it is sent and answers with a fixed sentence, which is exactly what an advisor test needs: what reached the model, and what came back. No database, no Docker, no API key.

Versions

Component Version
Spring Boot 4.1.1
Spring AI 2.0.1 (spring-ai-client-chat 2.0.1)
Reactor Core 3.8.7
JTokkit (token estimates) 1.1.0
Java 25 (LTS)

Quickstart

scripts/run-all.sh     # runs all 21 tests and regenerates output/01 .. 21

Two consecutive runs produce byte-identical files. To run the app itself: OPENAI_API_KEY=... mvn spring-boot:run, then POST /chat with an X-User header and {"text": "..."}.

What's here

File What it shows
advisor/Orders.java The order numbers, and the layers they sit between (memory +200, tool loop +300, model last)
advisor/LoggingAdvisor.java Request / response / stream / failure lines; content off by default
advisor/PiiRedactor.java Email, Luhn-checked card number and phone patterns; <EMAIL_1> placeholders
advisor/PiiRedactionAdvisor.java Redact on the way in, restore on the way out, on calls and on streams (buffers a split placeholder)
advisor/TokenBudgetAdvisor.java Per-request and per-user limits, checked before the model is called
advisor/Texts.java Rewriting request and response text with the mutate() copies
config/AdvisorConfig.java The advisors as beans and one ChatClient that uses them with Spring AI's memory advisor
web/ChatController.java POST /chat; the user comes from a header only so the tests need no login

Output files

Every file is written by the test named in the right column, and every console block in the article is quoted from one of them.

File Written by
01-chain-order.txt, 02-chain-order-ties.txt, 03-chain-contents.txt ChainOrderTest
04-base-advisor-stream.txt BaseAdvisorStreamTest
05-logging-advisor.txt, 06-logging-failure.txt LoggingAdvisorTest
07-pii-redaction.txt, 08-pii-limits.txt, 09-pii-stream-boundary.txt PiiRedactionTest
10-pii-order.txt PiiOrderTest
11-pii-multi-turn.txt PiiMultiTurnTest
12-token-budget.txt, 13-token-budget-stream.txt, 14-token-budget-stream-refusal.txt TokenBudgetTest
15-tool-loop-order.txt ToolLoopOrderTest
16-error-propagation.txt ErrorPropagationTest
17-context.txt ContextAndImmutabilityTest
18-unit-test-no-model.txt UnitTestingWithoutModelTest
19-web-429.txt AdvisorWebTest
20-built-in-orders.txt BuiltInOrdersTest
21-tool-loop-budget.txt ToolLoopBudgetTest

TokenBudgetTest.aStreamIsRefusedAsAnErrorSignal makes Spring AI's MessageAggregator log an ERROR ... Aggregation Error stack trace on the console. That is the refusal being reported, not a test failure.

Requirements

JDK 25 and Maven.