From 21750623a78315c5579ba56ad0df567f874afa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Lochm=C3=BCller?= Date: Mon, 6 Oct 2025 20:07:48 +0200 Subject: [PATCH] [Platform][Gemini][VertexAI] Handle `inlineData` --- .../Bridge/Gemini/Gemini/ResultConverter.php | 7 ++- .../VertexAi/Gemini/ResultConverter.php | 16 ++++- .../Gemini/Gemini/ResultConverterTest.php | 56 +++++++++++++++++ .../VertexAi/Gemini/ResultConverterTest.php | 62 +++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index b71249e69..755cb2590 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -15,6 +15,7 @@ use Symfony\AI\Platform\Exception\RateLimitExceededException; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -138,7 +139,7 @@ private function convertStream(HttpResponse $result): \Generator * } * } $choice */ - private function convertChoice(array $choice): ToolCallResult|TextResult + private function convertChoice(array $choice): ToolCallResult|TextResult|BinaryResult { $contentParts = $choice['content']['parts']; @@ -156,6 +157,10 @@ private function convertChoice(array $choice): ToolCallResult|TextResult return new TextResult($contentPart['text']); } + if (isset($contentPart['inlineData'])) { + return new BinaryResult($contentPart['inlineData']['data'], $contentPart['inlineData']['mimeType'] ?? null); + } + throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finishReason'])); } diff --git a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php index 161d70235..44c81b383 100644 --- a/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/VertexAi/Gemini/ResultConverter.php @@ -11,8 +11,10 @@ namespace Symfony\AI\Platform\Bridge\VertexAi\Gemini; +use Symfony\AI\Platform\Exception\RateLimitExceededException; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model as BaseModel; +use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\ChoiceResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -42,8 +44,14 @@ public function supports(BaseModel $model): bool public function convert(RawResultInterface|RawHttpResult $result, array $options = []): ResultInterface { + $response = $result->getObject(); + + if (429 === $response->getStatusCode()) { + throw new RateLimitExceededException(); + } + if ($options['stream'] ?? false) { - return new StreamResult($this->convertStream($result->getObject())); + return new StreamResult($this->convertStream($response)); } $data = $result->getData(); @@ -125,7 +133,7 @@ private function convertStream(HttpResponse $result): \Generator * } * } $choice */ - private function convertChoice(array $choice): ToolCallResult|TextResult + private function convertChoice(array $choice): ToolCallResult|TextResult|BinaryResult { $contentParts = $choice['content']['parts']; @@ -143,6 +151,10 @@ private function convertChoice(array $choice): ToolCallResult|TextResult return new TextResult($contentPart['text']); } + if (isset($contentPart['inlineData'])) { + return new BinaryResult($contentPart['inlineData']['data'], $contentPart['inlineData']['mimeType'] ?? null); + } + throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finishReason'])); } diff --git a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php index a2726a331..3b615ab88 100644 --- a/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php +++ b/src/platform/tests/Bridge/Gemini/Gemini/ResultConverterTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\Gemini\Gemini\ResultConverter; use Symfony\AI\Platform\Exception\RuntimeException; +use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\ToolCall; use Symfony\AI\Platform\Result\ToolCallResult; @@ -76,4 +77,59 @@ public function testReturnsToolCallEvenIfMultipleContentPartsAreGiven() $this->assertInstanceOf(ToolCall::class, $toolCall); $this->assertSame('1234', $toolCall->id); } + + public function testConvertsInlineDataToBinaryResult() + { + $converter = new ResultConverter(); + $httpResponse = self::createMock(ResponseInterface::class); + $httpResponse->method('getStatusCode')->willReturn(200); + $httpResponse->method('toArray')->willReturn([ + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ + 'inlineData' => [ + 'mimeType' => 'image/png', + 'data' => 'base64EncodedImageData', + ], + ], + ], + ], + ], + ], + ]); + + $result = $converter->convert(new RawHttpResult($httpResponse)); + $this->assertInstanceOf(BinaryResult::class, $result); + $this->assertSame('base64EncodedImageData', $result->getContent()); + $this->assertSame('image/png', $result->mimeType); + } + + public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() + { + $converter = new ResultConverter(); + $httpResponse = self::createMock(ResponseInterface::class); + $httpResponse->method('getStatusCode')->willReturn(200); + $httpResponse->method('toArray')->willReturn([ + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ + 'inlineData' => [ + 'data' => 'base64EncodedData', + ], + ], + ], + ], + ], + ], + ]); + + $result = $converter->convert(new RawHttpResult($httpResponse)); + $this->assertInstanceOf(BinaryResult::class, $result); + $this->assertSame('base64EncodedData', $result->getContent()); + $this->assertNull($result->mimeType); + } } diff --git a/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php b/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php index b93aad8a2..f43bcd0b2 100644 --- a/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php +++ b/src/platform/tests/Bridge/VertexAi/Gemini/ResultConverterTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\AI\Platform\Bridge\VertexAi\Gemini\ResultConverter; +use Symfony\AI\Platform\Result\BinaryResult; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\AI\Platform\Result\TextResult; use Symfony\AI\Platform\Result\ToolCall; @@ -124,4 +125,65 @@ public function testItThrowsExceptionOnTimeout() $this->expectException(\RuntimeException::class); $converter->convert(new RawHttpResult($response)); } + + public function testConvertsInlineDataToBinaryResult() + { + $response = $this->createStub(ResponseInterface::class); + $response + ->method('toArray') + ->willReturn([ + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ + 'inlineData' => [ + 'mimeType' => 'image/png', + 'data' => 'base64EncodedImageData', + ], + ], + ], + ], + ], + ], + ]); + + $resultConverter = new ResultConverter(); + + $result = $resultConverter->convert(new RawHttpResult($response)); + + $this->assertInstanceOf(BinaryResult::class, $result); + $this->assertSame('base64EncodedImageData', $result->getContent()); + $this->assertSame('image/png', $result->mimeType); + } + + public function testConvertsInlineDataWithoutMimeTypeToBinaryResult() + { + $response = $this->createStub(ResponseInterface::class); + $response + ->method('toArray') + ->willReturn([ + 'candidates' => [ + [ + 'content' => [ + 'parts' => [ + [ + 'inlineData' => [ + 'data' => 'base64EncodedData', + ], + ], + ], + ], + ], + ], + ]); + + $resultConverter = new ResultConverter(); + + $result = $resultConverter->convert(new RawHttpResult($response)); + + $this->assertInstanceOf(BinaryResult::class, $result); + $this->assertSame('base64EncodedData', $result->getContent()); + $this->assertNull($result->mimeType); + } }