From 9dac36a78b809d1552a3f1b592a317b2dccf6453 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sun, 9 Nov 2025 22:35:40 +0800 Subject: [PATCH 1/4] fix: ensure valid parameters schema for parameter-less functions in OpenAiApi Signed-off-by: liugddx --- .../ai/openai/api/OpenAiApi.java | 36 +++- .../api/FunctionToolParametersTest.java | 156 ++++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index e364def5e07..9aa323ec71c 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -935,7 +935,7 @@ private Function() { public Function(String description, String name, Map parameters, Boolean strict) { this.description = description; this.name = name; - this.parameters = parameters; + this.parameters = ensureValidParametersSchema(parameters); this.strict = strict; } @@ -946,7 +946,39 @@ public Function(String description, String name, Map parameters, * @param jsonSchema tool function schema as json. */ public Function(String description, String name, String jsonSchema) { - this(description, name, ModelOptionsUtils.jsonToMap(jsonSchema), null); + this.description = description; + this.name = name; + this.parameters = ensureValidParametersSchema( + jsonSchema != null ? ModelOptionsUtils.jsonToMap(jsonSchema) : null); + this.strict = null; + } + + /** + * Ensures that the parameters schema is valid for OpenAI API. OpenAI requires + * that the parameters object must have a "properties" field, even if it's + * empty. + * @param parameters the parameters map from JSON schema + * @return a valid parameters map with required fields + */ + private static Map ensureValidParametersSchema(Map parameters) { + if (parameters == null) { + parameters = new java.util.HashMap<>(); + parameters.put("type", "object"); + parameters.put("properties", new java.util.HashMap<>()); + return parameters; + } + + // Ensure "type" field exists + if (!parameters.containsKey("type")) { + parameters.put("type", "object"); + } + + // Ensure "properties" field exists for object types + if ("object".equals(parameters.get("type")) && !parameters.containsKey("properties")) { + parameters.put("properties", new java.util.HashMap<>()); + } + + return parameters; } public String getDescription() { diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java new file mode 100644 index 00000000000..2dec8b86a08 --- /dev/null +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java @@ -0,0 +1,156 @@ +/* + * 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.openai.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for FunctionTool.Function parameters validation. + * + * @author Liu Guodong + */ +class FunctionToolParametersTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testFunctionWithNoParameters() throws Exception { + // Test case 1: JSON schema with no properties field + String jsonSchemaNoProperties = """ + { + "type": "object" + } + """; + + OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function( + "Test function with no parameters", "test_function", jsonSchemaNoProperties); + + assertThat(function.getParameters()).isNotNull(); + assertThat(function.getParameters().get("type")).isEqualTo("object"); + assertThat(function.getParameters().get("properties")).isNotNull(); + assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class); + + // Verify serialization produces valid JSON + String json = objectMapper.writeValueAsString(function); + assertThat(json).contains("\"properties\""); + } + + @Test + void testFunctionWithEmptyProperties() throws Exception { + // Test case 2: JSON schema with empty properties + String jsonSchemaEmptyProperties = """ + { + "type": "object", + "properties": {} + } + """; + + OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function( + "Test function with empty properties", "test_function", jsonSchemaEmptyProperties); + + assertThat(function.getParameters()).isNotNull(); + assertThat(function.getParameters().get("type")).isEqualTo("object"); + assertThat(function.getParameters().get("properties")).isNotNull(); + + // Verify serialization produces valid JSON + String json = objectMapper.writeValueAsString(function); + assertThat(json).contains("\"properties\""); + } + + @Test + void testFunctionWithParameters() throws Exception { + // Test case 3: JSON schema with actual parameters + String jsonSchemaWithParams = """ + { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + } + }, + "required": ["param1"] + } + """; + + OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function with parameters", + "test_function", jsonSchemaWithParams); + + assertThat(function.getParameters()).isNotNull(); + assertThat(function.getParameters().get("type")).isEqualTo("object"); + assertThat(function.getParameters().get("properties")).isNotNull(); + assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class); + + @SuppressWarnings("unchecked") + Map properties = (Map) function.getParameters().get("properties"); + assertThat(properties).containsKey("param1"); + + // Verify serialization produces valid JSON + String json = objectMapper.writeValueAsString(function); + assertThat(json).contains("\"properties\""); + assertThat(json).contains("\"param1\""); + } + + @Test + void testFunctionWithNullSchema() throws Exception { + // Test case 4: null JSON schema (edge case) + String nullSchema = null; + OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function with null schema", + "test_function", nullSchema); + + // Should create a valid empty parameters object + assertThat(function.getParameters()).isNotNull(); + assertThat(function.getParameters().get("type")).isEqualTo("object"); + assertThat(function.getParameters().get("properties")).isNotNull(); + + // Verify serialization produces valid JSON + String json = objectMapper.writeValueAsString(function); + assertThat(json).contains("\"properties\""); + } + + @Test + void testFunctionWithVoidTypeSchema() throws Exception { + // Test case 5: Schema generated for Void.class (common case for no-param + // functions) + // This simulates what JsonSchemaGenerator would produce + String voidSchema = """ + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false + } + """; + + OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function for Void type", + "test_void_function", voidSchema); + + assertThat(function.getParameters()).isNotNull(); + assertThat(function.getParameters().get("type")).isEqualTo("object"); + assertThat(function.getParameters().get("properties")).isNotNull(); + assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class); + + // Verify serialization produces valid JSON for OpenAI API + String json = objectMapper.writeValueAsString(function); + assertThat(json).contains("\"properties\""); + } + +} From 3657f435b648ff7267cd13175c610f7c946a18d7 Mon Sep 17 00:00:00 2001 From: liugddx Date: Mon, 10 Nov 2025 08:03:19 +0800 Subject: [PATCH 2/4] fix: ensure valid parameters schema for parameter-less functions in OpenAiApi Signed-off-by: liugddx --- .../ai/openai/api/FunctionToolParametersTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java index 2dec8b86a08..05b2db88654 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java @@ -154,3 +154,4 @@ void testFunctionWithVoidTypeSchema() throws Exception { } } + From d7cfdb99b60bee05621f50fdc80379925e44c06b Mon Sep 17 00:00:00 2001 From: liugddx Date: Mon, 10 Nov 2025 08:29:24 +0800 Subject: [PATCH 3/4] fix: ensure valid parameters schema for parameter-less functions in OpenAiApi Signed-off-by: liugddx --- .../ai/openai/api/FunctionToolParametersTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java index 05b2db88654..256032fc177 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java @@ -16,11 +16,11 @@ package org.springframework.ai.openai.api; +import java.util.Map; + import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -50,7 +50,7 @@ void testFunctionWithNoParameters() throws Exception { assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class); // Verify serialization produces valid JSON - String json = objectMapper.writeValueAsString(function); + String json = this.objectMapper.writeValueAsString(function); assertThat(json).contains("\"properties\""); } @@ -72,7 +72,7 @@ void testFunctionWithEmptyProperties() throws Exception { assertThat(function.getParameters().get("properties")).isNotNull(); // Verify serialization produces valid JSON - String json = objectMapper.writeValueAsString(function); + String json = this.objectMapper.writeValueAsString(function); assertThat(json).contains("\"properties\""); } @@ -105,7 +105,7 @@ void testFunctionWithParameters() throws Exception { assertThat(properties).containsKey("param1"); // Verify serialization produces valid JSON - String json = objectMapper.writeValueAsString(function); + String json = this.objectMapper.writeValueAsString(function); assertThat(json).contains("\"properties\""); assertThat(json).contains("\"param1\""); } @@ -123,7 +123,7 @@ void testFunctionWithNullSchema() throws Exception { assertThat(function.getParameters().get("properties")).isNotNull(); // Verify serialization produces valid JSON - String json = objectMapper.writeValueAsString(function); + String json = this.objectMapper.writeValueAsString(function); assertThat(json).contains("\"properties\""); } @@ -149,7 +149,7 @@ void testFunctionWithVoidTypeSchema() throws Exception { assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class); // Verify serialization produces valid JSON for OpenAI API - String json = objectMapper.writeValueAsString(function); + String json = this.objectMapper.writeValueAsString(function); assertThat(json).contains("\"properties\""); } From e7ea0b1cb99fad646edc5a4716cbafd7f49ed510 Mon Sep 17 00:00:00 2001 From: liugddx Date: Mon, 10 Nov 2025 08:29:42 +0800 Subject: [PATCH 4/4] fix: ensure valid parameters schema for parameter-less functions in OpenAiApi Signed-off-by: liugddx --- .../ai/openai/api/FunctionToolParametersTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java index 256032fc177..dda9592d899 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/api/FunctionToolParametersTest.java @@ -154,4 +154,3 @@ void testFunctionWithVoidTypeSchema() throws Exception { } } -