From 6dedf511dcd69113478bd838b89959f27dc809ed Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 9 May 2026 16:35:13 -0400 Subject: [PATCH 1/2] Preserve empty JSON schema object maps --- ...actOpenAiCompatibleTextGenerationModel.php | 8 +- src/Tools/DTO/FunctionDeclaration.php | 85 +++++++++++++++++++ ...penAiCompatibleTextGenerationModelTest.php | 29 +++++++ .../Tools/DTO/FunctionDeclarationTest.php | 27 ++++++ 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php b/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php index e0e2e71b..26ddaaff 100644 --- a/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php +++ b/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php @@ -492,9 +492,15 @@ protected function prepareToolsParam(array $functionDeclarations): array { $tools = []; foreach ($functionDeclarations as $functionDeclaration) { + $function = $functionDeclaration->toArray(); + $parameters = $functionDeclaration->getJsonSerializableParameters(); + if ($parameters !== null) { + $function[FunctionDeclaration::KEY_PARAMETERS] = $parameters; + } + $tools[] = [ 'type' => 'function', - 'function' => $functionDeclaration->toArray(), + 'function' => $function, ]; } diff --git a/src/Tools/DTO/FunctionDeclaration.php b/src/Tools/DTO/FunctionDeclaration.php index 5b08ce80..ab4a921c 100644 --- a/src/Tools/DTO/FunctionDeclaration.php +++ b/src/Tools/DTO/FunctionDeclaration.php @@ -94,6 +94,28 @@ public function getParameters(): ?array return $this->parameters; } + /** + * Gets the function parameters schema in a JSON-serializable form. + * + * JSON Schema object-map fields such as properties must encode as JSON + * objects even when empty. PHP arrays cannot preserve that distinction + * without casting the empty map before serialization. + * + * @since 0.1.0 + * + * @return array|\stdClass|null The JSON-serializable parameters schema. + */ + public function getJsonSerializableParameters() + { + if ($this->parameters === null) { + return null; + } + + /** @var array|\stdClass $parameters */ + $parameters = $this->prepareJsonSchemaObjectMaps($this->parameters, self::KEY_PARAMETERS); + return $parameters; + } + /** * {@inheritDoc} * @@ -143,6 +165,69 @@ public function toArray(): array return $data; } + /** + * {@inheritDoc} + * + * @since 0.1.0 + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $data = $this->toArray(); + + if ($this->parameters !== null) { + $data[self::KEY_PARAMETERS] = $this->getJsonSerializableParameters(); + } + + return $data; + } + + /** + * Recursively prepares JSON Schema object-map fields for JSON serialization. + * + * @since 0.1.0 + * + * @param mixed $value The value to prepare. + * @param string|null $key The current JSON Schema key, if available. + * @return mixed The prepared value. + */ + private function prepareJsonSchemaObjectMaps($value, ?string $key = null) + { + if (!is_array($value)) { + return $value; + } + + if ($value === [] && $this->isJsonSchemaObjectMapKey($key)) { + return new \stdClass(); + } + + foreach ($value as $childKey => $childValue) { + $value[$childKey] = $this->prepareJsonSchemaObjectMaps( + $childValue, + is_string($childKey) ? $childKey : null + ); + } + + return $value; + } + + /** + * Checks whether the given JSON Schema key represents an object map. + * + * @since 0.1.0 + * + * @param string|null $key The JSON Schema key. + * @return bool True if the key represents an object map, false otherwise. + */ + private function isJsonSchemaObjectMapKey(?string $key): bool + { + return in_array( + $key, + [self::KEY_PARAMETERS, 'properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'], + true + ); + } + /** * {@inheritDoc} * diff --git a/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php b/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php index 6c99c75b..9c441e81 100644 --- a/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php +++ b/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php @@ -926,6 +926,35 @@ public function testPrepareToolsParam(): void $this->assertEquals($functionDeclaration2->toArray(), $prepared[1]['function']); } + /** + * Tests prepareToolsParam() preserves empty JSON Schema object maps. + * + * @return void + */ + public function testPrepareToolsParamPreservesEmptySchemaObjectMaps(): void + { + $functionDeclaration = new FunctionDeclaration( + 'inspect_object', + 'Inspects an object with optional nested metadata', + [ + 'type' => 'object', + 'properties' => [ + 'metadata' => [ + 'type' => 'object', + 'properties' => [], + ], + ], + ] + ); + $model = $this->createModel(); + + $prepared = $model->exposePrepareToolsParam([$functionDeclaration]); + $json = json_encode($prepared, JSON_THROW_ON_ERROR); + + $this->assertStringContainsString('"properties":{', $json); + $this->assertStringNotContainsString('"properties":[]', $json); + } + /** * Tests prepareResponseFormatParam() with null schema. * diff --git a/tests/unit/Tools/DTO/FunctionDeclarationTest.php b/tests/unit/Tools/DTO/FunctionDeclarationTest.php index b2782bdb..53f6825b 100644 --- a/tests/unit/Tools/DTO/FunctionDeclarationTest.php +++ b/tests/unit/Tools/DTO/FunctionDeclarationTest.php @@ -206,6 +206,33 @@ public function testToArrayWithParameters(): void ); } + /** + * Tests JSON serialization with empty JSON Schema object maps. + * + * @return void + */ + public function testJsonSerializationPreservesEmptySchemaObjectMaps(): void + { + $declaration = new FunctionDeclaration( + 'inspectObject', + 'Inspects an object with optional nested metadata', + [ + 'type' => 'object', + 'properties' => [ + 'metadata' => [ + 'type' => 'object', + 'properties' => [], + ], + ], + ] + ); + + $json = json_encode($declaration, JSON_THROW_ON_ERROR); + + $this->assertStringContainsString('"properties":{', $json); + $this->assertStringNotContainsString('"properties":[]', $json); + } + /** * Tests array transformation without parameters. * From 824774d6537e34695d177047494ef30d4e8582b8 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 9 May 2026 16:39:36 -0400 Subject: [PATCH 2/2] Cover null function parameters serialization --- tests/unit/Tools/DTO/FunctionDeclarationTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/Tools/DTO/FunctionDeclarationTest.php b/tests/unit/Tools/DTO/FunctionDeclarationTest.php index 53f6825b..45759ccb 100644 --- a/tests/unit/Tools/DTO/FunctionDeclarationTest.php +++ b/tests/unit/Tools/DTO/FunctionDeclarationTest.php @@ -55,6 +55,7 @@ public function testCreateWithoutParameters(): void $this->assertEquals($name, $declaration->getName()); $this->assertEquals($description, $declaration->getDescription()); $this->assertNull($declaration->getParameters()); + $this->assertNull($declaration->getJsonSerializableParameters()); } /**