diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java
index d17b2fc4773..5e3735afc68 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java
@@ -38,6 +38,18 @@
/**
* The name of the tool. If not provided, the method name will be used.
+ *
+ * For maximum compatibility across different LLMs, it is recommended to use only
+ * alphanumeric characters, underscores, hyphens, and dots in tool names. Using spaces
+ * or special characters may cause issues with some LLMs (e.g., OpenAI).
+ *
+ *
+ * Examples of recommended names: "get_weather", "search-docs", "tool.v1"
+ *
+ *
+ * Examples of names that may cause compatibility issues: "get weather" (contains
+ * space), "tool()" (contains parentheses)
+ *
*/
String name() default "";
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
index 4186d935acc..baa03b830cb 100644
--- a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
+++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java
@@ -20,8 +20,12 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
@@ -38,16 +42,30 @@
*/
public final class ToolUtils {
+ private static final Logger logger = LoggerFactory.getLogger(ToolUtils.class);
+
+ /**
+ * Regular expression pattern for recommended tool names. Tool names should contain
+ * only alphanumeric characters, underscores, hyphens, and dots for maximum
+ * compatibility across different LLMs.
+ */
+ private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\.-]+$");
+
private ToolUtils() {
}
public static String getToolName(Method method) {
Assert.notNull(method, "method cannot be null");
var tool = AnnotatedElementUtils.findMergedAnnotation(method, Tool.class);
+ String toolName;
if (tool == null) {
- return method.getName();
+ toolName = method.getName();
+ }
+ else {
+ toolName = StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
}
- return StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
+ validateToolName(toolName);
+ return toolName;
}
public static String getToolDescriptionFromName(String toolName) {
@@ -102,4 +120,17 @@ public static List getDuplicateToolNames(ToolCallback... toolCallbacks)
return getDuplicateToolNames(Arrays.asList(toolCallbacks));
}
+ /**
+ * Validates that a tool name follows recommended naming conventions. Logs a warning
+ * if the tool name contains characters that may not be compatible with some LLMs.
+ * @param toolName the tool name to validate
+ */
+ private static void validateToolName(String toolName) {
+ Assert.hasText(toolName, "Tool name cannot be null or empty");
+ if (!RECOMMENDED_NAME_PATTERN.matcher(toolName).matches()) {
+ logger.warn("Tool name '{}' may not be compatible with some LLMs (e.g., OpenAI). "
+ + "Consider using only alphanumeric characters, underscores, hyphens, and dots.", toolName);
+ }
+ }
+
}
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/support/ToolUtilsTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/support/ToolUtilsTests.java
new file mode 100644
index 00000000000..dbf432d4b7c
--- /dev/null
+++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/support/ToolUtilsTests.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.tool.support;
+
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.tool.annotation.Tool;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Unit tests for {@link ToolUtils}.
+ *
+ * @author Hyunjoon Park
+ * @since 1.0.0
+ */
+class ToolUtilsTests {
+
+ @Test
+ void getToolNameFromMethodWithoutAnnotation() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("simpleMethod");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("simpleMethod");
+ }
+
+ @Test
+ void getToolNameFromMethodWithAnnotationButNoName() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("annotatedMethodWithoutName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("annotatedMethodWithoutName");
+ }
+
+ @Test
+ void getToolNameFromMethodWithValidName() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("methodWithValidName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("valid_tool-name.v1");
+ }
+
+ @Test
+ void getToolNameFromMethodWithNameContainingSpaces() throws NoSuchMethodException {
+ // Tool names with spaces are now allowed but will generate a warning log
+ Method method = TestTools.class.getMethod("methodWithSpacesInName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("invalid tool name");
+ }
+
+ @Test
+ void getToolNameFromMethodWithNameContainingSpecialChars() throws NoSuchMethodException {
+ // Tool names with special characters are now allowed but will generate a warning
+ // log
+ Method method = TestTools.class.getMethod("methodWithSpecialCharsInName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("tool@name!");
+ }
+
+ @Test
+ void getToolNameFromMethodWithNameContainingParentheses() throws NoSuchMethodException {
+ // Tool names with parentheses are now allowed but will generate a warning log
+ Method method = TestTools.class.getMethod("methodWithParenthesesInName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("tool()");
+ }
+
+ @Test
+ void getToolNameFromMethodWithEmptyName() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("methodWithEmptyName");
+ // When name is empty, it falls back to method name which is valid
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("methodWithEmptyName");
+ }
+
+ @Test
+ void getToolDescriptionFromMethodWithoutAnnotation() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("simpleMethod");
+ String description = ToolUtils.getToolDescription(method);
+ assertThat(description).isEqualTo("simple method");
+ }
+
+ @Test
+ void getToolDescriptionFromMethodWithAnnotationButNoDescription() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("annotatedMethodWithoutName");
+ String description = ToolUtils.getToolDescription(method);
+ assertThat(description).isEqualTo("annotatedMethodWithoutName");
+ }
+
+ @Test
+ void getToolDescriptionFromMethodWithDescription() throws NoSuchMethodException {
+ Method method = TestTools.class.getMethod("methodWithDescription");
+ String description = ToolUtils.getToolDescription(method);
+ assertThat(description).isEqualTo("This is a tool description");
+ }
+
+ @Test
+ void getToolNameFromMethodWithUnicodeCharacters() throws NoSuchMethodException {
+ // Tool names with unicode characters should be allowed for non-English contexts
+ Method method = TestTools.class.getMethod("methodWithUnicodeName");
+ String toolName = ToolUtils.getToolName(method);
+ assertThat(toolName).isEqualTo("获取天气");
+ }
+
+ // Test helper class with various tool methods
+ public static class TestTools {
+
+ public void simpleMethod() {
+ // Method without @Tool annotation
+ }
+
+ @Tool
+ public void annotatedMethodWithoutName() {
+ // Method with @Tool but no name specified
+ }
+
+ @Tool(name = "valid_tool-name.v1")
+ public void methodWithValidName() {
+ // Method with valid tool name
+ }
+
+ @Tool(name = "invalid tool name")
+ public void methodWithSpacesInName() {
+ // Method with spaces in tool name (invalid)
+ }
+
+ @Tool(name = "tool@name!")
+ public void methodWithSpecialCharsInName() {
+ // Method with special characters in tool name (invalid)
+ }
+
+ @Tool(name = "tool()")
+ public void methodWithParenthesesInName() {
+ // Method with parentheses in tool name (invalid)
+ }
+
+ @Tool(name = "")
+ public void methodWithEmptyName() {
+ // Method with empty name (falls back to method name)
+ }
+
+ @Tool(description = "This is a tool description")
+ public void methodWithDescription() {
+ // Method with description
+ }
+
+ @Tool(name = "获取天气")
+ public void methodWithUnicodeName() {
+ // Method with unicode characters in tool name (Chinese: "get weather")
+ }
+
+ }
+
+}