diff --git a/src/Bridge/OpenAI/GPT/ResponseConverter.php b/src/Bridge/OpenAI/GPT/ResponseConverter.php index c3daa655..11b9584a 100644 --- a/src/Bridge/OpenAI/GPT/ResponseConverter.php +++ b/src/Bridge/OpenAI/GPT/ResponseConverter.php @@ -165,7 +165,7 @@ private function convertChoice(array $choice): Choice return new Choice(toolCalls: array_map([$this, 'convertToolCall'], $choice['message']['tool_calls'])); } - if ('stop' === $choice['finish_reason']) { + if (in_array($choice['finish_reason'], ['stop', 'length'], true)) { return new Choice($choice['message']['content']); } diff --git a/tests/Bridge/OpenAI/GPT/ResponseConverterTest.php b/tests/Bridge/OpenAI/GPT/ResponseConverterTest.php new file mode 100644 index 00000000..c64867e7 --- /dev/null +++ b/tests/Bridge/OpenAI/GPT/ResponseConverterTest.php @@ -0,0 +1,177 @@ +createMock(ResponseInterface::class); + $httpResponse->method('toArray')->willReturn([ + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello world', + ], + 'finish_reason' => 'stop', + ], + ], + ]); + + $response = $converter->convert($httpResponse); + + $this->assertInstanceOf(TextResponse::class, $response); + $this->assertEquals('Hello world', $response->getContent()); + } + + public function testConvertToolCallResponse(): void + { + $converter = new ResponseConverter(); + $httpResponse = $this->createMock(ResponseInterface::class); + $httpResponse->method('toArray')->willReturn([ + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => null, + 'tool_calls' => [ + [ + 'id' => 'call_123', + 'type' => 'function', + 'function' => [ + 'name' => 'test_function', + 'arguments' => '{"arg1": "value1"}', + ], + ], + ], + ], + 'finish_reason' => 'tool_calls', + ], + ], + ]); + + $response = $converter->convert($httpResponse); + + $this->assertInstanceOf(ToolCallResponse::class, $response); + $toolCalls = $response->getContent(); + $this->assertCount(1, $toolCalls); + $this->assertEquals('call_123', $toolCalls[0]->id); + $this->assertEquals('test_function', $toolCalls[0]->name); + $this->assertEquals(['arg1' => 'value1'], $toolCalls[0]->arguments); + } + + public function testConvertMultipleChoices(): void + { + $converter = new ResponseConverter(); + $httpResponse = $this->createMock(ResponseInterface::class); + $httpResponse->method('toArray')->willReturn([ + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Choice 1', + ], + 'finish_reason' => 'stop', + ], + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Choice 2', + ], + 'finish_reason' => 'stop', + ], + ], + ]); + + $response = $converter->convert($httpResponse); + + $this->assertInstanceOf(ChoiceResponse::class, $response); + $choices = $response->getContent(); + $this->assertCount(2, $choices); + $this->assertEquals('Choice 1', $choices[0]->getContent()); + $this->assertEquals('Choice 2', $choices[1]->getContent()); + } + + public function testContentFilterException(): void + { + $converter = new ResponseConverter(); + $httpResponse = $this->createMock(ResponseInterface::class); + + $httpResponse->expects($this->exactly(2)) + ->method('toArray') + ->willReturnCallback(function ($throw = true) { + if ($throw) { + throw new class extends \Exception implements ClientExceptionInterface { + public function getResponse(): ResponseInterface + { + throw new RuntimeException('Not implemented'); + } + }; + } + + return [ + 'error' => [ + 'code' => 'content_filter', + 'message' => 'Content was filtered', + ], + ]; + }); + + $this->expectException(ContentFilterException::class); + $this->expectExceptionMessage('Content was filtered'); + + $converter->convert($httpResponse); + } + + public function testThrowsExceptionWhenNoChoices(): void + { + $converter = new ResponseConverter(); + $httpResponse = $this->createMock(ResponseInterface::class); + $httpResponse->method('toArray')->willReturn([]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Response does not contain choices'); + + $converter->convert($httpResponse); + } + + public function testThrowsExceptionForUnsupportedFinishReason(): void + { + $converter = new ResponseConverter(); + $httpResponse = $this->createMock(ResponseInterface::class); + $httpResponse->method('toArray')->willReturn([ + 'choices' => [ + [ + 'message' => [ + 'role' => 'assistant', + 'content' => 'Test content', + ], + 'finish_reason' => 'unsupported_reason', + ], + ], + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported finish reason "unsupported_reason"'); + + $converter->convert($httpResponse); + } +}