spring-projects/spring-ai · error · IllegalStateException

Multiple tools with the same name (%s) found in ToolCallingC

Error message

Multiple tools with the same name (%s) found in ToolCallingChatOptions

What it means

ToolCallingChatOptions.validateToolCallbacks rejects a tool callback list containing two or more callbacks with the same tool name, since the model and manager could not disambiguate which implementation to invoke. It uses ToolUtils.getDuplicateToolNames and throws IllegalStateException listing the duplicated names.

Source

Thrown at spring-ai-model/src/main/java/org/springframework/ai/model/tool/ToolCallingChatOptions.java:98

			return defaultToolContext != null ? Map.copyOf(defaultToolContext) : null;
		}
		Assert.noNullElements(runtimeToolContext.keySet(), "runtimeToolContext keys cannot be null");
		if (CollectionUtils.isEmpty(defaultToolContext)) {
			return Map.copyOf(runtimeToolContext);
		}
		Assert.noNullElements(defaultToolContext.keySet(), "defaultToolContext keys cannot be null");
		var mergedToolContext = new java.util.HashMap<>(defaultToolContext);
		mergedToolContext.putAll(runtimeToolContext);
		return Map.copyOf(mergedToolContext);
	}

	static void validateToolCallbacks(@Nullable List<ToolCallback> toolCallbacks) {
		if (CollectionUtils.isEmpty(toolCallbacks)) {
			return;
		}
		List<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
		if (!duplicateToolNames.isEmpty()) {
			throw new IllegalStateException("Multiple tools with the same name (%s) found in ToolCallingChatOptions"
				.formatted(String.join(", ", duplicateToolNames)));
		}
	}

	/**
	 * A builder to create a {@link ToolCallingChatOptions} instance.
	 */
	interface Builder<B extends Builder<B>> extends ChatOptions.Builder<B> {

		/**
		 * ToolCallbacks to be registered with the ChatModel.
		 */
		B toolCallbacks(@Nullable List<ToolCallback> toolCallbacks);

		/**
		 * ToolCallbacks to be registered with the ChatModel.
		 */
		B toolCallbacks(ToolCallback... toolCallbacks);

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Deduplicate the callback list before setting it (filter by ToolCallback.getName())
  2. Rename one of the conflicting tools (unique @Tool name / FunctionCallback name)
  3. Inspect which providers contributed duplicates (log each callback name before building options)
  4. If both tools are wanted, wrap or prefix names so each is unique

Example fix

// before
options.setToolCallbacks(List.of(providerA.getToolCallbacks(), providerB.getToolCallbacks()).stream().flatMap(List::stream).toList());
// after
Set<String> seen = new HashSet<>();
List<ToolCallback> unique = all.stream().filter(cb -> seen.add(cb.getName())).toList();
options.setToolCallbacks(unique);
Defensive patterns

Strategy: validation

Validate before calling

List<ToolCallback> cbs = options.getToolCallbacks();
Set<String> names = new HashSet<>();
for (ToolCallback cb : cbs) {
    if (!names.add(cb.getName())) {
        throw new IllegalStateException("Duplicate tool name: " + cb.getName());
    }
}

Prevention

When it happens

Trigger: Building ToolCallingChatOptions (or calling a ChatModel with them) where getToolCallbacks() contains callbacks with identical names, e.g. after merging tool lists from multiple ToolCallbackProviders or registering the same @Tool method twice.

Common situations: Combining tool sources (MCP client + local tools) that both expose a tool named e.g. 'search', accidentally registering the same FunctionCallback twice, or version migrations registering defaults plus explicit duplicates.

Related errors


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