spring-projects/spring-ai · error · IllegalStateException
No ToolCallback found for tool name: ${toolName}
Error message
No ToolCallback found for tool name: ${toolName} What it means
DefaultToolCallingManager.executeToolCall throws IllegalStateException when the tool name sent back by the LLM does not match any registered ToolCallback. This happens when the model hallucinates or renames a tool, or when the callbacks resolved at execution time differ from the ones the request was built with. Execution aborts since there is no implementation to invoke.
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
- Log and compare the tool names the model returned against registered callbacks; add the missing ToolCallback.
- Use sanitized, simple tool names (alphanumeric, underscore, hyphen, dot) so the LLM echoes them exactly.
- Ensure the same ToolCallback/tool registry instance is used for both request building and execution.
- Retry the request; if the model intermittently mangles names, consider a different model or stricter tool descriptions.
Example fix
// before callbacks were registered only on a second request: ChatClient.create(chatModel).prompt().tools(weatherTool).call() // after — register all callbacks consistently on every call ToolCallbacks toolCallbacks = ToolCallbacks.from(weatherTool, stockTool); chatClient.prompt().tools(toolCallbacks).call()
Defensive patterns
Strategy: try-catch
Validate before calling
Set<String> names = toolCallbacks.stream().map(cb -> cb.getToolDefinition().name()).collect(Collectors.toSet());
if (!names.contains(expectedToolName)) throw new IllegalArgumentException("Tool not registered: " + expectedToolName); Try / catch
try {
return chatClient.prompt().tools(toolCallbacks).call().content();
} catch (IllegalStateException e) {
if (e.getMessage().startsWith("No ToolCallback found")) {
logger.error("Model returned unknown tool name; registered={}", registeredNames, e);
return retryWithStrictPrompt();
}
throw e;
} Prevention
- Use simple names matching [a-zA-Z0-9_-] to avoid LLM name mangling.
- Register all tools on every request from a single shared registry.
- Log registered tool names at startup to spot drift early.
- Keep tool descriptions precise so the model picks existing tools.
When it happens
Trigger: internalToolExecutionResult -> executeToolCall resolves toolCallbacks by name and finds none matching the ToolCall's name returned by the model.
Common situations: LLM name mangling (model alters the tool name); tool callbacks added/removed between request building and execution; using a ChatClient with different tools than the ones advertised; case-sensitivity or truncation by the provider.
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
- Currently only one tool call is supported per message!
- No tool call requested by the chat model
- Sampling not supported by the client:
- Tool execution results are not supported for Bedrock models
- Currently only one tool call is supported per message!
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/56f7c4130684a446.
Report an issue: GitHub.