{"record":{"id":"56f7c4130684a446","repo":"spring-projects/spring-ai","slug":"no-toolcallback-found-for-tool-name-toolname","errorCode":null,"errorMessage":"No ToolCallback found for tool name: ${toolName}","messagePattern":"No ToolCallback found for tool name: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java","lineNumber":296,"sourceCode":"\t\t\t\t\t\t\t+ \". Using empty JSON object as default.\");\n\t\t\t\t}\n\t\t\t\tfinalToolInputArguments = \"{}\";\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfinalToolInputArguments = toolInputArguments;\n\t\t\t}\n\n\t\t\tToolCallback toolCallback = toolCallbacks.stream()\n\t\t\t\t.filter(tool -> toolName.equals(tool.getToolDefinition().name()))\n\t\t\t\t.findFirst()\n\t\t\t\t.orElseGet(() -> this.resolutionFallbackEnabled ? this.toolCallbackResolver.resolve(toolName) : null);\n\n\t\t\tif (toolCallback == null) {\n\t\t\t\tif (logger.isWarnEnabled()) {\n\t\t\t\t\tlogger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING_START + toolName\n\t\t\t\t\t\t\t+ POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING_END);\n\t\t\t\t}\n\t\t\t\tthrow new IllegalStateException(\"No ToolCallback found for tool name: \" + toolName);\n\t\t\t}\n\n\t\t\tif (returnDirect == null) {\n\t\t\t\treturnDirect = toolCallback.getToolMetadata().returnDirect();\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect();\n\t\t\t}\n\n\t\t\t// In streaming/reactive mode the parent observation is propagated through the\n\t\t\t// Reactor context captured in ToolCallReactiveContextHolder. In blocking mode\n\t\t\t// that holder is never populated, so fall back to the observation currently\n\t\t\t// in scope on the calling thread to keep the observation hierarchy intact.\n\t\t\tObservation parent = ToolCallReactiveContextHolder.getContext()\n\t\t\t\t.getOrDefault(ObservationThreadLocalAccessor.KEY, this.observationRegistry.getCurrentObservation());\n\n\t\t\tToolCallingObservationContext observationContext = ToolCallingObservationContext.builder()\n\t\t\t\t.toolDefinition(toolCallback.getToolDefinition())","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java#L278-L314","documentation":"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.","triggerScenarios":"internalToolExecutionResult -> executeToolCall resolves toolCallbacks by name and finds none matching the ToolCall's name returned by the model.","commonSituations":"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.","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."],"exampleFix":"// before\ncallbacks were registered only on a second request:\nChatClient.create(chatModel).prompt().tools(weatherTool).call()\n\n// after — register all callbacks consistently on every call\nToolCallbacks toolCallbacks = ToolCallbacks.from(weatherTool, stockTool);\nchatClient.prompt().tools(toolCallbacks).call()","handlingStrategy":"try-catch","validationCode":"Set<String> names = toolCallbacks.stream().map(cb -> cb.getToolDefinition().name()).collect(Collectors.toSet());\nif (!names.contains(expectedToolName)) throw new IllegalArgumentException(\"Tool not registered: \" + expectedToolName);","typeGuard":null,"tryCatchPattern":"try {\n    return chatClient.prompt().tools(toolCallbacks).call().content();\n} catch (IllegalStateException e) {\n    if (e.getMessage().startsWith(\"No ToolCallback found\")) {\n        logger.error(\"Model returned unknown tool name; registered={}\", registeredNames, e);\n        return retryWithStrictPrompt();\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["tool-calling","illegal-state","llm"],"backgroundTag":"resource-not-found","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}