spring-projects/spring-ai · error · IllegalStateException
Cannot set both delegate and toolObject
Error message
Cannot set both delegate and toolObject
What it means
build() enforces that delegate (a ToolCallback/ToolCallbackProvider delegate) and toolObject (an object with @Tool methods) are mutually exclusive sources for the provider. Setting both would create ambiguity about which source provides the tools, so the builder rejects it with an IllegalStateException.
Solutions
- Remove either .delegate(...) or .toolObject(...) from the builder chain, keeping only the intended source.
- If you need to combine both kinds of tools, create two AugmentedToolCallbackProvider instances and merge their ToolCallbacks.
- Audit shared builder helper methods so they only set one source.
Example fix
// before
var provider = AugmentedToolCallbackProvider.builder()
.delegate(wrappedCallbacks)
.toolObject(new MyTools())
.argumentType(Args.class)
.argumentConsumer(a -> a)
.build();
// after
var provider = AugmentedToolCallbackProvider.builder()
.toolObject(new MyTools())
.argumentType(Args.class)
.argumentConsumer(a -> a)
.build(); Defensive patterns
Strategy: validation
Validate before calling
boolean hasDelegate = delegate != null;
boolean hasToolObject = toolObject != null;
if (hasDelegate == hasToolObject && !hasDelegate) { /* neither */ }
if (hasDelegate && hasToolObject) {
throw new IllegalArgumentException("Set only one of delegate or toolObject");
} Try / catch
try { provider = builder.build(); } catch (IllegalStateException e) { throw new IllegalArgumentException("Invalid tool source configuration: " + e.getMessage(), e); } Prevention
- Pick one tool-source style (toolObject vs delegate) per codebase and enforce it in review.
- When migrating between styles, delete the old setter call in the same commit.
When it happens
Trigger: Calling builder().delegate(delegateProvider).toolObject(myToolObject)...build(); typically when migrating code from the toolObject style to the delegate style and forgetting to remove the old setter.
Common situations: Refactoring from @Tool-annotated objects to wrapping existing ToolCallbacks; copy-pasted builder chains where both setters appear; fluent chains reused across two provider configurations.
Related errors
- argumentConsumer is required
- Either delegate or toolObject must be set
- Anthropic cache breakpoint limit (4) reached. Additional…
- Anthropic Citations API requires all documents to have…
- argumentType is required
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/838c2234531a705a.
Report an issue: GitHub.
Appendix: source
Thrown at spring-ai-model/src/main/java/org/springframework/ai/tool/augment/AugmentedToolCallbackProvider.java:154
this.removeExtraArgumentsAfterProcessing = removeExtraArgumentsAfterProcessing;
return this;
}
/**
* Builds the {@link AugmentedToolCallbackProvider} instance.
* @return the built instance
* @throws IllegalStateException if required fields are not set
*/
public AugmentedToolCallbackProvider<T> build() {
if (this.argumentType == null) {
throw new IllegalStateException("argumentType is required");
}
if (this.argumentConsumer == null) {
throw new IllegalStateException("argumentConsumer is required");
}
if (this.delegate != null && this.toolObject != null) {
throw new IllegalStateException("Cannot set both delegate and toolObject");
}
if (this.delegate == null && this.toolObject == null) {
throw new IllegalStateException("Either delegate or toolObject must be set");
}
if (this.toolObject != null) {
return new AugmentedToolCallbackProvider<>(this.toolObject, this.argumentType, this.argumentConsumer,
this.removeExtraArgumentsAfterProcessing);
}
else if (this.delegate != null) { // Redundant if condition to please NullAway
return new AugmentedToolCallbackProvider<>(this.delegate, this.argumentType, this.argumentConsumer,
this.removeExtraArgumentsAfterProcessing);
}
else {
throw new IllegalStateException();
}
}View on GitHub (pinned to 98a7beda4f)