spring-projects/spring-ai · error · IllegalStateException
No ToolCallback found for tool name: %s
Error message
No ToolCallback found for tool name: %s
What it means
During tool execution, DefaultToolCallingManager.executeToolCall resolves the ToolCallback matching the tool name the model requested. If no registered callback has that name, it throws IllegalStateException. This typically means the model changed the tool name relative to what was registered, or the tool was never registered in the options.
Source
Thrown at spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java:296
+ ". Using empty JSON object as default.");
}
finalToolInputArguments = "{}";
}
else {
finalToolInputArguments = toolInputArguments;
}
ToolCallback toolCallback = toolCallbacks.stream()
.filter(tool -> toolName.equals(tool.getToolDefinition().name()))
.findFirst()
.orElseGet(() -> this.resolutionFallbackEnabled ? this.toolCallbackResolver.resolve(toolName) : null);
if (toolCallback == null) {
if (logger.isWarnEnabled()) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING_START + toolName
+ POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING_END);
}
throw new IllegalStateException("No ToolCallback found for tool name: " + toolName);
}
if (returnDirect == null) {
returnDirect = toolCallback.getToolMetadata().returnDirect();
}
else {
returnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect();
}
// In streaming/reactive mode the parent observation is propagated through the
// Reactor context captured in ToolCallReactiveContextHolder. In blocking mode
// that holder is never populated, so fall back to the observation currently
// in scope on the calling thread to keep the observation hierarchy intact.
Observation parent = ToolCallReactiveContextHolder.getContext()
.getOrDefault(ObservationThreadLocalAccessor.KEY, this.observationRegistry.getCurrentObservation());
ToolCallingObservationContext observationContext = ToolCallingObservationContext.builder()
.toolDefinition(toolCallback.getToolDefinition())View on GitHub (pinned to 98a7beda4f)
Solutions
- Check the manager's WARN log (POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING) and compare the requested name against your registered callback names
- Ensure all intended ToolCallbacks are set on ToolCallingChatOptions (toolCallbacks(...) / tools(...)) for the exact request
- Register a default/fallback ToolCallback or use a ToolCallbackResolver so unknown names resolve instead of throwing
- Simplify tool names (avoid special characters, keep them short) to prevent LLM name mutation
Example fix
// before
ChatOptions options = ToolCallingChatOptions.builder().build(); // tools omitted
// after
ChatOptions options = ToolCallingChatOptions.builder()
.toolCallbacks(weatherTool, stockTool)
.build(); Defensive patterns
Strategy: validation
Validate before calling
// Verify the tool names you register are what the model will see
Set<String> registered = options.getToolCallbacks().stream()
.map(ToolCallback::getName).collect(Collectors.toSet());
// After the model replies, before manual execution:
// assistantMessage.getToolCalls().stream().allMatch(tc -> registered.contains(tc.name())) Try / catch
try {
return chatModel.call(prompt);
} catch (IllegalStateException e) {
if (e.getMessage() != null && e.getMessage().startsWith("No ToolCallback found for tool name")) {
logger.warn("Model requested unknown tool; re-prompting with tool list");
return chatModel.call(rePromptWithExplicitToolList);
}
throw e;
} Prevention
- Always attach the same callback set to every request in a session
- Keep tool names short, lowercase, and free of special characters
- Use a ToolCallbackResolver/fallback callback for unknown names
- Log requested vs registered tool names on mismatch
When it happens
Trigger: Calling a ChatModel with tool callbacks where the model's assistant message contains a tool call whose name has no matching ToolCallback in ToolCallingChatOptions.getToolCallbacks(); executeToolCall's lookup (including callback-resolver fallback) returns null.
Common situations: Model mangling or inventing tool names (sanitization, truncation, special-character handling), registering tools on the request but the model hallucinating a different name, stale options that dropped callbacks after an upgrade, or misconfigured function names in prompt/context.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- Tool call limit exceeded (dynamic message: tool name and lim
- Multiple tools with the same name (%s) found in ToolCallingC
- Async complete methods should use McpAsyncRequestContext ins
- Method cannot have more than one McpMeta parameter: ${method
- Method cannot have more than one exchange parameter: ${metho
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/4c703a47d7c08e98.
Report an issue: GitHub.