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(