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

  1. Log and compare the tool names the model returned against registered callbacks; add the missing ToolCallback.
  2. Use sanitized, simple tool names (alphanumeric, underscore, hyphen, dot) so the LLM echoes them exactly.
  3. Ensure the same ToolCallback/tool registry instance is used for both request building and execution.
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/56f7c4130684a446. Report an issue: GitHub.