Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
57 changes: 56 additions & 1 deletion docs/core-concepts/image-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ $image = $response->firstImage();
if ($image->hasUrl()) {
echo "Image URL: " . $image->url;
}
if ($image->hasBase64()) {
echo "Base64 Image Data: " . $image->base64;
}
```

### Working with Responses
Expand All @@ -64,8 +67,12 @@ if ($response->hasImages()) {
if ($image->hasUrl()) {
echo "Image: {$image->url}\n";
}

if ($image->hasBase64()) {
echo "Base64 Image: " . substr($image->base64, 0, 50) . "...\n";
}

if ($image->hasRevisedwithPrompt()) {
if ($image->hasRevisedPrompt()) {
echo "Revised prompt: {$image->revisedPrompt}\n";
}
}
Expand Down Expand Up @@ -102,6 +109,54 @@ $response = Prism::image()
->generate();
```

#### GPT-Image-1 (Base64 Only)

The GPT-Image-1 model always returns base64-encoded images, regardless of the `response_format` setting:

```php
$response = Prism::image()
->using('openai', 'gpt-image-1')
->withPrompt('A cute baby sea otter floating on its back')
->withProviderOptions([
'size' => '1024x1024', // 1024x1024, 1536x1024, 1024x1536, auto
'quality' => 'high', // auto, high, medium, low
'background' => 'transparent', // transparent, opaque, auto
'output_format' => 'png', // png, jpeg, webp
'output_compression' => 90, // 0-100 (for jpeg/webp)
])
->generate();

$image = $response->firstImage();
if ($image->hasBase64()) {
// Save the base64 image to a file
file_put_contents('generated-image.png', base64_decode($image->base64));
echo "Base64 image saved to generated-image.png";
}
```

#### Base64 vs URL Responses

Different models return images in different formats:

- **GPT-Image-1**: Always returns base64-encoded images in the `base64` property
- **DALL-E 2 & 3**: Return URLs by default, but can return base64 when `response_format` is set to `'b64_json'`

```php
// Request base64 format from DALL-E 3
$response = Prism::image()
->using('openai', 'dall-e-3')
->withPrompt('Abstract art')
->withProviderOptions([
'response_format' => 'b64_json',
])
->generate();

$image = $response->firstImage();
if ($image->hasBase64()) {
echo "Received base64 image data";
}
```

## Testing

Prism provides convenient fakes for testing image generation:
Expand Down
5 changes: 3 additions & 2 deletions src/Providers/OpenAI/Handlers/Images.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public function handle(Request $request): Response

$responseBuilder = new ResponseBuilder(
usage: new Usage(
promptTokens: data_get($data, 'usage.prompt_tokens', 0),
completionTokens: data_get($data, 'usage.completion_tokens', 0),
promptTokens: data_get($data, 'usage.input_tokens', data_get($data, 'usage.prompt_tokens', 0)),
completionTokens: data_get($data, 'usage.output_tokens', data_get($data, 'usage.completion_tokens', 0)),
),
meta: new Meta(
id: data_get($data, 'id', 'img_'.bin2hex(random_bytes(8))),
Expand Down Expand Up @@ -71,6 +71,7 @@ protected function extractImages(array $data): array
foreach (data_get($data, 'data', []) as $imageData) {
$images[] = new GeneratedImage(
url: data_get($imageData, 'url'),
base64: data_get($imageData, 'b64_json'),
revisedPrompt: data_get($imageData, 'revised_prompt'),
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/ValueObjects/GeneratedImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{
public function __construct(
public ?string $url = null,
public ?string $base64 = null,
public ?string $revisedPrompt = null,
) {}

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

public function hasBase64(): bool
{
return $this->base64 !== null;
}

public function hasRevisedPrompt(): bool
{
return $this->revisedPrompt !== null;
Expand Down
124 changes: 124 additions & 0 deletions tests/Providers/OpenAI/ImagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,127 @@
expect($response->meta->model)->toBe('dall-e-3');
expect($response->meta->rateLimits)->not->toBeEmpty();
});

it('can generate an image with gpt-image-1 returning base64', function (): void {
Http::fake([
'api.openai.com/v1/images/generations' => Http::response([
'created' => 1713833628,
'data' => [
[
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
],
],
'usage' => [
'total_tokens' => 100,
'input_tokens' => 50,
'output_tokens' => 50,
'input_tokens_details' => [
'text_tokens' => 10,
'image_tokens' => 40,
],
],
], 200),
]);

$response = Prism::image()
->using('openai', 'gpt-image-1')
->withPrompt('A cute baby sea otter')
->generate();

expect($response->firstImage())->not->toBeNull();
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
expect($response->firstImage()->hasBase64())->toBeTrue();
expect($response->firstImage()->hasUrl())->toBeFalse();
expect($response->firstImage()->url)->toBeNull();
expect($response->usage->promptTokens)->toBe(50);
expect($response->imageCount())->toBe(1);

Http::assertSent(function (Request $request): bool {
$data = $request->data();

return $request->url() === 'https://api.openai.com/v1/images/generations' &&
$data['model'] === 'gpt-image-1' &&
$data['prompt'] === 'A cute baby sea otter';
});
});

it('can generate an image with dall-e-3 requesting base64 format', function (): void {
Http::fake([
'api.openai.com/v1/images/generations' => Http::response([
'created' => 1713833628,
'data' => [
[
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'revised_prompt' => 'A highly detailed mountain sunset scene',
],
],
'usage' => [
'prompt_tokens' => 20,
'completion_tokens' => 0,
],
], 200),
]);

$response = Prism::image()
->using('openai', 'dall-e-3')
->withPrompt('A mountain sunset')
->withProviderOptions([
'response_format' => 'b64_json',
])
->generate();

expect($response->firstImage())->not->toBeNull();
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
expect($response->firstImage()->hasBase64())->toBeTrue();
expect($response->firstImage()->hasUrl())->toBeFalse();
expect($response->firstImage()->hasRevisedPrompt())->toBeTrue();
expect($response->firstImage()->revisedPrompt)->toBe('A highly detailed mountain sunset scene');

Http::assertSent(function (Request $request): bool {
$data = $request->data();

return $data['model'] === 'dall-e-3' &&
$data['prompt'] === 'A mountain sunset' &&
$data['response_format'] === 'b64_json';
});
});

it('can generate an image with dall-e-2 requesting base64 format', function (): void {
Http::fake([
'api.openai.com/v1/images/generations' => Http::response([
'created' => 1713833628,
'data' => [
[
'b64_json' => 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
],
],
'usage' => [
'prompt_tokens' => 12,
'completion_tokens' => 0,
],
], 200),
]);

$response = Prism::image()
->using('openai', 'dall-e-2')
->withPrompt('Abstract geometric patterns')
->withProviderOptions([
'response_format' => 'b64_json',
'size' => '256x256',
])
->generate();

expect($response->firstImage())->not->toBeNull();
expect($response->firstImage()->base64)->toBe('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
expect($response->firstImage()->hasBase64())->toBeTrue();
expect($response->firstImage()->hasUrl())->toBeFalse();

Http::assertSent(function (Request $request): bool {
$data = $request->data();

return $data['model'] === 'dall-e-2' &&
$data['prompt'] === 'Abstract geometric patterns' &&
$data['response_format'] === 'b64_json' &&
$data['size'] === '256x256';
});
});