diff --git a/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php b/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php index e0e2e71b..e3cc9063 100644 --- a/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php +++ b/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php @@ -206,45 +206,55 @@ protected function prepareGenerateTextParams(array $prompt): array */ protected function prepareMessagesParam(array $messages, ?string $systemInstruction = null): array { - $messagesParam = array_map( - function (Message $message): array { - // Special case: Function response. - $messageParts = $message->getParts(); - if (count($messageParts) === 1 && $messageParts[0]->getType()->isFunctionResponse()) { - $functionResponse = $messageParts[0]->getFunctionResponse(); + $messagesParam = []; + foreach ($messages as $message) { + // Special case: Function responses must be sent as separate tool messages. + $messageParts = $message->getParts(); + if ( + !empty($messageParts) + && count(array_filter( + $messageParts, + static function (MessagePart $part): bool { + return $part->getType()->isFunctionResponse(); + } + )) === count($messageParts) + ) { + foreach ($messageParts as $messagePart) { + $functionResponse = $messagePart->getFunctionResponse(); if (!$functionResponse) { // This should be impossible due to class internals, but still needs to be checked. throw new RuntimeException( 'The function response typed message part must contain a function response.' ); } - return [ + $messagesParam[] = [ 'role' => 'tool', 'content' => json_encode($functionResponse->getResponse()), 'tool_call_id' => $functionResponse->getId(), ]; } - $messageData = [ - 'role' => $this->getMessageRoleString($message->getRole()), - 'content' => array_values(array_filter(array_map( - [$this, 'getMessagePartContentData'], - $messageParts - ))), - ]; + continue; + } - // Only include tool_calls if there are any (OpenAI rejects empty arrays). - $toolCalls = array_values(array_filter(array_map( - [$this, 'getMessagePartToolCallData'], + $messageData = [ + 'role' => $this->getMessageRoleString($message->getRole()), + 'content' => array_values(array_filter(array_map( + [$this, 'getMessagePartContentData'], $messageParts - ))); - if (!empty($toolCalls)) { - $messageData['tool_calls'] = $toolCalls; - } + ))), + ]; - return $messageData; - }, - $messages - ); + // 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; + } + + $messagesParam[] = $messageData; + } if ($systemInstruction) { array_unshift( diff --git a/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php b/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php index 6c99c75b..37d23f9f 100644 --- a/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php +++ b/tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php @@ -548,6 +548,33 @@ public function testPrepareMessagesParamFunctionResponse(): void $this->assertEquals('call_1', $prepared[0]['tool_call_id']); } + /** + * Tests prepareMessagesParam() with multiple function responses in one message. + * + * @return void + */ + public function testPrepareMessagesParamMultipleFunctionResponses(): void + { + $message = new Message( + MessageRoleEnum::user(), + [ + new MessagePart(new FunctionResponse('call_1', 'first_function', ['result' => 'first'])), + new MessagePart(new FunctionResponse('call_2', 'second_function', ['result' => 'second'])), + ] + ); + $model = $this->createModel(); + + $prepared = $model->exposePrepareMessagesParam([$message]); + + $this->assertCount(2, $prepared); + $this->assertSame('tool', $prepared[0]['role']); + $this->assertSame(json_encode(['result' => 'first']), $prepared[0]['content']); + $this->assertSame('call_1', $prepared[0]['tool_call_id']); + $this->assertSame('tool', $prepared[1]['role']); + $this->assertSame(json_encode(['result' => 'second']), $prepared[1]['content']); + $this->assertSame('call_2', $prepared[1]['tool_call_id']); + } + /** * Tests getMessageRoleString() method. *