{"record":{"id":"4c703a47d7c08e98","repo":"spring-projects/spring-ai","slug":"no-toolcallback-found-for-tool-name-s","errorCode":null,"errorMessage":"No ToolCallback found for tool name: %s","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":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nChatOptions options = ToolCallingChatOptions.builder().build(); // tools omitted\n// after\nChatOptions options = ToolCallingChatOptions.builder()\n    .toolCallbacks(weatherTool, stockTool)\n    .build();","handlingStrategy":"validation","validationCode":"// Verify the tool names you register are what the model will see\nSet<String> registered = options.getToolCallbacks().stream()\n    .map(ToolCallback::getName).collect(Collectors.toSet());\n// After the model replies, before manual execution:\n// assistantMessage.getToolCalls().stream().allMatch(tc -> registered.contains(tc.name()))","typeGuard":null,"tryCatchPattern":"try {\n    return chatModel.call(prompt);\n} catch (IllegalStateException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"No ToolCallback found for tool name\")) {\n        logger.warn(\"Model requested unknown tool; re-prompting with tool list\");\n        return chatModel.call(rePromptWithExplicitToolList);\n    }\n    throw e;\n}","preventionTips":["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"],"tags":["spring-ai","tool-calling","lookup-failure"],"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-14T05:17:10.506Z"}