Skip to content

Commit

Permalink
feat (provider/amazon-bedrock): add generate image support for Amazon…
Browse files Browse the repository at this point in the history
… Nova Canvas (#4974)

Co-authored-by: Andrea Amorosi <[email protected]>
  • Loading branch information
shaper and dreamorosi authored Feb 25, 2025
1 parent 88f6e01 commit 58c3411
Show file tree
Hide file tree
Showing 9 changed files with 579 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-ducks-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/amazon-bedrock': patch
---

feat (provider/amazon-bedrock): add generate image support for Amazon Nova Canvas
83 changes: 42 additions & 41 deletions content/docs/03-ai-sdk-core/35-image-generation.mdx

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions content/providers/01-ai-sdk-providers/08-amazon-bedrock.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,86 @@ The following optional settings are available for Bedrock Titan embedding models
| `amazon.titan-embed-text-v1` | 1536 | <Cross size={18} /> |
| `amazon.titan-embed-text-v2:0` | 1024 | <Check size={18} /> |

## Image Models

You can create models that call the Bedrock API [Bedrock API](https://docs.aws.amazon.com/nova/latest/userguide/image-generation.html)
using the `.image()` factory method.

For more on the Amazon Nova Canvas image model, see the [Nova Canvas
Overview](https://docs.aws.amazon.com/ai/responsible-ai/nova-canvas/overview.html).

<Note>
The `amazon.nova-canvas-v1:0` model is available in the `us-east-1` region.
</Note>

```ts
const model = bedrock.image('amazon.nova-canvas-v1:0');
```

You can then generate images with the `experimental_generateImage` function:

```ts
import { bedrock } from '@ai-sdk/amazon-bedrock';
import { experimental_generateImage as generateImage } from 'ai';

const { image } = await generateImage({
model: bedrock.imageModel('amazon.nova-canvas-v1:0'),
prompt: 'A beautiful sunset over a calm ocean',
size: '512x512',
seed: 42,
});
```

You can also pass the `providerOptions` object to the `generateImage` function to customize the generation behavior:

```ts
import { bedrock } from '@ai-sdk/amazon-bedrock';
import { experimental_generateImage as generateImage } from 'ai';

const { image } = await generateImage({
model: bedrock.imageModel('amazon.nova-canvas-v1:0'),
prompt: 'A beautiful sunset over a calm ocean',
size: '512x512',
seed: 42,
providerOptions: { bedrock: { quality: 'premium' } },
});
```

Documentation for additional settings can be found within the [Amazon Bedrock
User Guide for Amazon Nova
Documentation](https://docs.aws.amazon.com/nova/latest/userguide/image-gen-req-resp-structure.html).

### Image Model Settings

When creating an image model, you can customize the generation behavior with optional settings:

```ts
const model = bedrock.imageModel('amazon.nova-canvas-v1:0', {
maxImagesPerCall: 1, // Maximum number of images to generate per API call
});
```

- **maxImagesPerCall** _number_

Override the maximum number of images generated per API call. Default can vary
by model, with 5 as a common default.

### Model Capabilities

The Amazon Nova Canvas model supports custom sizes with constraints as follows:

- Each side must be between 320-4096 pixels, inclusive.
- Each side must be evenly divisible by 16.
- The aspect ratio must be between 1:4 and 4:1. That is, one side can't be more than 4 times longer than the other side.
- The total pixel count must be less than 4,194,304.

For more, see [Image generation access and
usage](https://docs.aws.amazon.com/nova/latest/userguide/image-gen-access.html).

| Model | Sizes |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `amazon.nova-canvas-v1:0` | Custom sizes: 320-4096px per side (must be divisible by 16), aspect ratio 1:4 to 4:1, max 4.2M pixels |

## Response Headers

The Amazon Bedrock provider will return the response headers associated with
Expand Down
23 changes: 23 additions & 0 deletions examples/ai-core/src/generate-image/amazon-bedrock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { bedrock } from '@ai-sdk/amazon-bedrock';
import { experimental_generateImage as generateImage } from 'ai';
import { presentImages } from '../lib/present-image';
import 'dotenv/config';

async function main() {
const result = await generateImage({
model: bedrock.imageModel('amazon.nova-canvas-v1:0'),
prompt:
'A salamander at dusk in a forest pond with fireflies in the background, in the style of anime',
size: '512x512',
seed: 42,
providerOptions: {
bedrock: {
quality: 'premium',
},
},
});

await presentImages(result.images);
}

main().catch(console.error);
223 changes: 223 additions & 0 deletions packages/amazon-bedrock/src/bedrock-image-model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { createTestServer } from '@ai-sdk/provider-utils/test';
import { createAmazonBedrock } from './bedrock-provider';
import { BedrockImageModel } from './bedrock-image-model';
import { injectFetchHeaders } from './inject-fetch-headers';

const prompt = 'A cute baby sea otter';

const provider = createAmazonBedrock();
const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' });

const invokeUrl = `https://bedrock-runtime.us-east-1.amazonaws.com/model/${encodeURIComponent(
'amazon.nova-canvas-v1:0',
)}/invoke`;

describe('doGenerate', () => {
const mockConfigHeaders = {
'config-header': 'config-value',
'shared-header': 'config-shared',
};

const server = createTestServer({
[invokeUrl]: {
response: {
type: 'binary',
headers: {
'content-type': 'application/json',
},
body: Buffer.from(
JSON.stringify({
images: ['base64-image-1', 'base64-image-2'],
}),
),
},
},
});

const model = new BedrockImageModel(
'amazon.nova-canvas-v1:0',
{},
{
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
headers: mockConfigHeaders,
fetch: fakeFetchWithAuth,
},
);

it('should pass the model and the settings', async () => {
await model.doGenerate({
prompt,
n: 1,
size: '1024x1024',
aspectRatio: undefined,
seed: 1234,
providerOptions: {
bedrock: {
negativeText: 'bad',
quality: 'premium',
cfgScale: 1.2,
},
},
});

const body = await server.calls[0].requestBody;
expect(body).toStrictEqual({
taskType: 'TEXT_IMAGE',
textToImageParams: {
text: prompt,
negativeText: 'bad',
},
imageGenerationConfig: {
numberOfImages: 1,
seed: 1234,
quality: 'premium',
cfgScale: 1.2,
width: 1024,
height: 1024,
},
});
});

it('should properly combine headers from all sources', async () => {
const optionsHeaders = {
'options-header': 'options-value',
'shared-header': 'options-shared',
};

const modelWithHeaders = new BedrockImageModel(
'amazon.nova-canvas-v1:0',
{},
{
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
headers: {
'model-header': 'model-value',
'shared-header': 'model-shared',
},
fetch: injectFetchHeaders({
'signed-header': 'signed-value',
authorization: 'AWS4-HMAC-SHA256...',
}),
},
);

await modelWithHeaders.doGenerate({
prompt,
n: 1,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
headers: optionsHeaders,
});

const requestHeaders = server.calls[0].requestHeaders;
expect(requestHeaders['options-header']).toBe('options-value');
expect(requestHeaders['model-header']).toBe('model-value');
expect(requestHeaders['signed-header']).toBe('signed-value');
expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
expect(requestHeaders['shared-header']).toBe('options-shared');
});

it('should respect maxImagesPerCall setting', async () => {
const customModel = provider.image('amazon.nova-canvas-v1:0', {
maxImagesPerCall: 2,
});
expect(customModel.maxImagesPerCall).toBe(2);

const defaultModel = provider.image('amazon.nova-canvas-v1:0');
expect(defaultModel.maxImagesPerCall).toBe(5); // 'amazon.nova-canvas-v1:0','s default from settings

const unknownModel = provider.image('unknown-model' as any);
expect(unknownModel.maxImagesPerCall).toBe(1); // fallback for unknown models
});

it('should return warnings for unsupported settings', async () => {
const result = await model.doGenerate({
prompt,
n: 1,
size: '1024x1024',
aspectRatio: '1:1',
seed: undefined,
providerOptions: {},
});

expect(result.warnings).toStrictEqual([
{
type: 'unsupported-setting',
setting: 'aspectRatio',
details:
'This model does not support aspect ratio. Use `size` instead.',
},
]);
});

it('should extract the generated images', async () => {
const result = await model.doGenerate({
prompt,
n: 1,
size: undefined,
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
});

expect(result.images).toStrictEqual(['base64-image-1', 'base64-image-2']);
});

it('should include response data with timestamp, modelId and headers', async () => {
const testDate = new Date('2024-03-15T12:00:00Z');

const customModel = new BedrockImageModel(
'amazon.nova-canvas-v1:0',
{},
{
baseUrl: () => 'https://bedrock-runtime.us-east-1.amazonaws.com',
headers: () => ({}),
_internal: {
currentDate: () => testDate,
},
},
);

const result = await customModel.doGenerate({
prompt,
n: 1,
size: '1024x1024',
aspectRatio: undefined,
seed: undefined,
providerOptions: {},
});

expect(result.response).toStrictEqual({
timestamp: testDate,
modelId: 'amazon.nova-canvas-v1:0',
headers: {
'content-length': '46',
'content-type': 'application/json',
},
});
});

it('should use real date when no custom date provider is specified', async () => {
const beforeDate = new Date();

const result = await model.doGenerate({
prompt,
n: 1,
size: undefined,
aspectRatio: undefined,
seed: 1234,
providerOptions: {},
});

const afterDate = new Date();

expect(result.response.timestamp.getTime()).toBeGreaterThanOrEqual(
beforeDate.getTime(),
);
expect(result.response.timestamp.getTime()).toBeLessThanOrEqual(
afterDate.getTime(),
);
expect(result.response.modelId).toBe('amazon.nova-canvas-v1:0');
});
});
Loading

0 comments on commit 58c3411

Please sign in to comment.