From 94dd3440e16320fbdc7d21a57a23484c2e09cb91 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sun, 27 Sep 2026 01:12:55 +0530 Subject: [PATCH] fix(prompt): keep requiredProperties of nested objects in OpenAI-compatible tool schema OpenAICompatibleToolDescriptorSchemaGenerator wrote only the top-level required list. The ToolParameterType.Object branch never wrote requiredProperties, so every property of a nested object became optional in the schema sent to the model. Write required after properties when requiredProperties is not empty, so objects without required properties produce the same schema as before. closes #2255 --- ...CompatibleToolDescriptorSchemaGenerator.kt | 5 ++ ...AICompatibleToolDescriptorConverterTest.kt | 71 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonMain/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorSchemaGenerator.kt b/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonMain/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorSchemaGenerator.kt index a868dfe3d4..1d6cb105d4 100644 --- a/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonMain/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorSchemaGenerator.kt +++ b/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonMain/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorSchemaGenerator.kt @@ -67,6 +67,11 @@ public open class OpenAICompatibleToolDescriptorSchemaGenerator : ToolDescriptor } } } + if (type.requiredProperties.isNotEmpty()) { + putJsonArray("required") { + type.requiredProperties.forEach { add(it) } + } + } } is ToolParameterType.AnyOf -> { diff --git a/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonTest/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorConverterTest.kt b/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonTest/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorConverterTest.kt index 1660b65221..c9c903a925 100644 --- a/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonTest/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorConverterTest.kt +++ b/prompt/prompt-executor/prompt-executor-clients/prompt-executor-openai-client-base/src/commonTest/kotlin/ai/koog/prompt/executor/clients/openai/base/OpenAICompatibleToolDescriptorConverterTest.kt @@ -158,7 +158,7 @@ class OpenAICompatibleToolDescriptorConverterTest { val actual = json.encodeToString(OpenAICompatibleToolDescriptorSchemaGenerator().generate(descriptor)) - // Note: OpenAI converter does not emit "required" inside nested object; it preserves order: type, additionalProperties, properties + // Note: the converter preserves order: type, additionalProperties, properties, required val expected = """ { "type": "object", @@ -176,7 +176,10 @@ class OpenAICompatibleToolDescriptorConverterTest { "type": "number", "description": "Threshold value" } - } + }, + "required": [ + "enabled" + ] } }, "required": [ @@ -188,6 +191,70 @@ class OpenAICompatibleToolDescriptorConverterTest { assertEquals(expected, actual) } + @Test + fun testNestedObjectInListKeepsRequiredProperties() { + val descriptor = ToolDescriptor( + name = "add_events", + description = "Add events", + requiredParameters = listOf( + ToolParameterDescriptor( + name = "events", + description = "Events to add", + type = ToolParameterType.List( + ToolParameterType.Object( + properties = listOf( + ToolParameterDescriptor("type", "Event type", ToolParameterType.String), + ToolParameterDescriptor("at", "YYYY-MM", ToolParameterType.String), + ToolParameterDescriptor("note", "Optional note", ToolParameterType.String), + ), + requiredProperties = listOf("type", "at"), + ) + ) + ) + ) + ) + + val actual = json.encodeToString(OpenAICompatibleToolDescriptorSchemaGenerator().generate(descriptor)) + + val expected = """ + { + "type": "object", + "properties": { + "events": { + "description": "Events to add", + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Event type" + }, + "at": { + "type": "string", + "description": "YYYY-MM" + }, + "note": { + "type": "string", + "description": "Optional note" + } + }, + "required": [ + "type", + "at" + ] + } + } + }, + "required": [ + "events" + ] + } + """.trimIndent() + + assertEquals(expected, actual) + } + @Test fun anyOf_mapping() { val descriptor = ToolDescriptor(