Skip to content

Commit 609f31b

Browse files
authored
Add base64 image support (#429)
1 parent 6de5b78 commit 609f31b

File tree

4 files changed

+189
-3
lines changed

4 files changed

+189
-3
lines changed

docs/core-concepts/image-generation.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ $image = $response->firstImage();
4343
if ($image->hasUrl()) {
4444
echo "Image URL: " . $image->url;
4545
}
46+
if ($image->hasBase64()) {
47+
echo "Base64 Image Data: " . $image->base64;
48+
}
4649
```
4750

4851
### Working with Responses
@@ -64,8 +67,12 @@ if ($response->hasImages()) {
6467
if ($image->hasUrl()) {
6568
echo "Image: {$image->url}\n";
6669
}
70+
71+
if ($image->hasBase64()) {
72+
echo "Base64 Image: " . substr($image->base64, 0, 50) . "...\n";
73+
}
6774

68-
if ($image->hasRevisedwithPrompt()) {
75+
if ($image->hasRevisedPrompt()) {
6976
echo "Revised prompt: {$image->revisedPrompt}\n";
7077
}
7178
}
@@ -102,6 +109,54 @@ $response = Prism::image()
102109
->generate();
103110
```
104111

112+
#### GPT-Image-1 (Base64 Only)
113+
114+
The GPT-Image-1 model always returns base64-encoded images, regardless of the `response_format` setting:
115+
116+
```php
117+
$response = Prism::image()
118+
->using('openai', 'gpt-image-1')
119+
->withPrompt('A cute baby sea otter floating on its back')
120+
->withProviderOptions([
121+
'size' => '1024x1024', // 1024x1024, 1536x1024, 1024x1536, auto
122+
'quality' => 'high', // auto, high, medium, low
123+
'background' => 'transparent', // transparent, opaque, auto
124+
'output_format' => 'png', // png, jpeg, webp
125+
'output_compression' => 90, // 0-100 (for jpeg/webp)
126+
])
127+
->generate();
128+
129+
$image = $response->firstImage();
130+
if ($image->hasBase64()) {
131+
// Save the base64 image to a file
132+
file_put_contents('generated-image.png', base64_decode($image->base64));
133+
echo "Base64 image saved to generated-image.png";
134+
}
135+
```
136+
137+
#### Base64 vs URL Responses
138+
139+
Different models return images in different formats:
140+
141+
- **GPT-Image-1**: Always returns base64-encoded images in the `base64` property
142+
- **DALL-E 2 & 3**: Return URLs by default, but can return base64 when `response_format` is set to `'b64_json'`
143+
144+
```php
145+
// Request base64 format from DALL-E 3
146+
$response = Prism::image()
147+
->using('openai', 'dall-e-3')
148+
->withPrompt('Abstract art')
149+
->withProviderOptions([
150+
'response_format' => 'b64_json',
151+
])
152+
->generate();
153+
154+
$image = $response->firstImage();
155+
if ($image->hasBase64()) {
156+
echo "Received base64 image data";
157+
}
158+
```
159+
105160
## Testing
106161

107162
Prism provides convenient fakes for testing image generation:

src/Providers/OpenAI/Handlers/Images.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public function handle(Request $request): Response
3737

3838
$responseBuilder = new ResponseBuilder(
3939
usage: new Usage(
40-
promptTokens: data_get($data, 'usage.prompt_tokens', 0),
41-
completionTokens: data_get($data, 'usage.completion_tokens', 0),
40+
promptTokens: data_get($data, 'usage.input_tokens', data_get($data, 'usage.prompt_tokens', 0)),
41+
completionTokens: data_get($data, 'usage.output_tokens', data_get($data, 'usage.completion_tokens', 0)),
4242
),
4343
meta: new Meta(
4444
id: data_get($data, 'id', 'img_'.bin2hex(random_bytes(8))),
@@ -71,6 +71,7 @@ protected function extractImages(array $data): array
7171
foreach (data_get($data, 'data', []) as $imageData) {
7272
$images[] = new GeneratedImage(
7373
url: data_get($imageData, 'url'),
74+
base64: data_get($imageData, 'b64_json'),
7475
revisedPrompt: data_get($imageData, 'revised_prompt'),
7576
);
7677
}

src/ValueObjects/GeneratedImage.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{
99
public function __construct(
1010
public ?string $url = null,
11+
public ?string $base64 = null,
1112
public ?string $revisedPrompt = null,
1213
) {}
1314

@@ -16,6 +17,11 @@ public function hasUrl(): bool
1617
return $this->url !== null;
1718
}
1819

20+
public function hasBase64(): bool
21+
{
22+
return $this->base64 !== null;
23+
}
24+
1925
public function hasRevisedPrompt(): bool
2026
{
2127
return $this->revisedPrompt !== null;

tests/Providers/OpenAI/ImagesTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,127 @@
223223
expect($response->meta->model)->toBe('dall-e-3');
224224
expect($response->meta->rateLimits)->not->toBeEmpty();
225225
});
226+
227+
it('can generate an image with gpt-image-1 returning base64', function (): void {
228+
Http::fake([
229+
'api.openai.com/v1/images/generations' => Http::response([
230+
'created' => 1713833628,
231+
'data' => [
232+
[
233+
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
234+
],
235+
],
236+
'usage' => [
237+
'total_tokens' => 100,
238+
'input_tokens' => 50,
239+
'output_tokens' => 50,
240+
'input_tokens_details' => [
241+
'text_tokens' => 10,
242+
'image_tokens' => 40,
243+
],
244+
],
245+
], 200),
246+
]);
247+
248+
$response = Prism::image()
249+
->using('openai', 'gpt-image-1')
250+
->withPrompt('A cute baby sea otter')
251+
->generate();
252+
253+
expect($response->firstImage())->not->toBeNull();
254+
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
255+
expect($response->firstImage()->hasBase64())->toBeTrue();
256+
expect($response->firstImage()->hasUrl())->toBeFalse();
257+
expect($response->firstImage()->url)->toBeNull();
258+
expect($response->usage->promptTokens)->toBe(50);
259+
expect($response->imageCount())->toBe(1);
260+
261+
Http::assertSent(function (Request $request): bool {
262+
$data = $request->data();
263+
264+
return $request->url() === 'https://api.openai.com/v1/images/generations' &&
265+
$data['model'] === 'gpt-image-1' &&
266+
$data['prompt'] === 'A cute baby sea otter';
267+
});
268+
});
269+
270+
it('can generate an image with dall-e-3 requesting base64 format', function (): void {
271+
Http::fake([
272+
'api.openai.com/v1/images/generations' => Http::response([
273+
'created' => 1713833628,
274+
'data' => [
275+
[
276+
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
277+
'revised_prompt' => 'A highly detailed mountain sunset scene',
278+
],
279+
],
280+
'usage' => [
281+
'prompt_tokens' => 20,
282+
'completion_tokens' => 0,
283+
],
284+
], 200),
285+
]);
286+
287+
$response = Prism::image()
288+
->using('openai', 'dall-e-3')
289+
->withPrompt('A mountain sunset')
290+
->withProviderOptions([
291+
'response_format' => 'b64_json',
292+
])
293+
->generate();
294+
295+
expect($response->firstImage())->not->toBeNull();
296+
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
297+
expect($response->firstImage()->hasBase64())->toBeTrue();
298+
expect($response->firstImage()->hasUrl())->toBeFalse();
299+
expect($response->firstImage()->hasRevisedPrompt())->toBeTrue();
300+
expect($response->firstImage()->revisedPrompt)->toBe('A highly detailed mountain sunset scene');
301+
302+
Http::assertSent(function (Request $request): bool {
303+
$data = $request->data();
304+
305+
return $data['model'] === 'dall-e-3' &&
306+
$data['prompt'] === 'A mountain sunset' &&
307+
$data['response_format'] === 'b64_json';
308+
});
309+
});
310+
311+
it('can generate an image with dall-e-2 requesting base64 format', function (): void {
312+
Http::fake([
313+
'api.openai.com/v1/images/generations' => Http::response([
314+
'created' => 1713833628,
315+
'data' => [
316+
[
317+
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
318+
],
319+
],
320+
'usage' => [
321+
'prompt_tokens' => 12,
322+
'completion_tokens' => 0,
323+
],
324+
], 200),
325+
]);
326+
327+
$response = Prism::image()
328+
->using('openai', 'dall-e-2')
329+
->withPrompt('Abstract geometric patterns')
330+
->withProviderOptions([
331+
'response_format' => 'b64_json',
332+
'size' => '256x256',
333+
])
334+
->generate();
335+
336+
expect($response->firstImage())->not->toBeNull();
337+
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
338+
expect($response->firstImage()->hasBase64())->toBeTrue();
339+
expect($response->firstImage()->hasUrl())->toBeFalse();
340+
341+
Http::assertSent(function (Request $request): bool {
342+
$data = $request->data();
343+
344+
return $data['model'] === 'dall-e-2' &&
345+
$data['prompt'] === 'Abstract geometric patterns' &&
346+
$data['response_format'] === 'b64_json' &&
347+
$data['size'] === '256x256';
348+
});
349+
});

0 commit comments

Comments
 (0)