Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,17 +236,24 @@ function (Message $message): array {
'tool_call_id' => $functionResponse->getId(),
];
}
return [
$messageData = [
'role' => $this->getMessageRoleString($message->getRole()),
'content' => array_values(array_filter(array_map(
[$this, 'getMessagePartContentData'],
$messageParts
))),
'tool_calls' => array_values(array_filter(array_map(
[$this, 'getMessagePartToolCallData'],
$messageParts
))),
];

// Only include tool_calls if there are any (OpenAI rejects empty arrays).
$toolCalls = array_values(array_filter(array_map(
[$this, 'getMessagePartToolCallData'],
$messageParts
)));
if (!empty($toolCalls)) {
$messageData['tool_calls'] = $toolCalls;
}

return $messageData;
},
$messages
);
Expand Down Expand Up @@ -394,12 +401,17 @@ protected function getMessagePartToolCallData(MessagePart $part): ?array
'The function call typed message part must contain a function call.'
);
}
$args = $functionCall->getArgs();
// Ensure empty arrays become empty objects for JSON encoding.
if (is_array($args) && empty($args)) {
$args = (object) array();
Copy link

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The (object) array() conversion can be simplified to new \stdClass() for better readability and clarity of intent when creating an empty object.

Suggested change
if (is_array($args) && empty($args)) {
$args = (object) array();
$args = new \stdClass();

Copilot uses AI. Check for mistakes.
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is tricky. Technically speaking, for JSON-encoding this should still only be turned into stdClass when the expected input shape is object. While that's often the case, it only depends on the schema - it could also be array, in which case leaving it a PHP array would be correct.

What happens if the parameters JSON schema specified for the function expects an actual array (indexed/numeric)? Does the OpenAI API always fail if it's an empty array? If so, that would seem like a flaw in the API (although, if that's the case, of course we still need to fix / work around it in our SDK).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a bit more, and while my concern is still valid in theory, in practice the OpenAI API specification does not support arrays as the root value. So it's fair to assume the data should be an object in this scenario.

return [
'type' => 'function',
'id' => $functionCall->getId(),
'function' => [
'name' => $functionCall->getName(),
'arguments' => json_encode($functionCall->getArgs()),
'arguments' => json_encode($args),
],
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,26 @@ public function testPrepareMessagesParamModelMessageWithFunctionCall(): void
);
}

/**
* Tests prepareMessagesParam with message having no function calls (tool_calls should not be included).
*
* @return void
*/
public function testPrepareMessagesParamNoToolCalls(): void
{
$message = new Message(
MessageRoleEnum::model(),
[new MessagePart('Hello, I am a simple text response.')]
);

$model = $this->createModel();
$prepared = $model->exposePrepareMessagesParam([$message], null);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPCS errors here:

Suggested change
$model = $this->createModel();
$prepared = $model->exposePrepareMessagesParam([$message], null);
$model = $this->createModel();
$prepared = $model->exposePrepareMessagesParam([$message], null);

$this->assertCount(1, $prepared);
$this->assertEquals('assistant', $prepared[0]['role']);
$this->assertArrayNotHasKey('tool_calls', $prepared[0]); // Should not have tool_calls field at all
}

/**
* Tests prepareMessagesParam() with function response.
*
Expand Down Expand Up @@ -742,6 +762,32 @@ public function testGetMessagePartToolCallDataFunctionCallPart(): void
], $data);
}

/**
* Tests getMessagePartToolCallData() with empty arguments (should encode as empty object).
*
* @return void
*/
public function testGetMessagePartToolCallDataEmptyArguments(): void
{
$functionCall = new FunctionCall(
'call_1',
'list_capabilities',
[] // Empty arguments array
);
$part = new MessagePart($functionCall);
$model = $this->createModel();
$data = $model->exposeGetMessagePartToolCallData($part);

$this->assertEquals([
'type' => 'function',
'id' => 'call_1',
'function' => [
'name' => 'list_capabilities',
'arguments' => '{}', // Should be empty object, not empty array
],
], $data);
}

/**
* Tests getMessagePartToolCallData() with text part (should return null).
*
Expand Down
Loading