Skip to content

Commit

Permalink
feat (ai/core): throw NoImageGeneratedError from generateImage when n…
Browse files Browse the repository at this point in the history
…o predictions are returned (#4395)

Co-authored-by: Lars Grammel <[email protected]>
  • Loading branch information
shaper and lgrammel authored Jan 23, 2025
1 parent 76e92a6 commit 3a58a2e
Show file tree
Hide file tree
Showing 21 changed files with 696 additions and 36 deletions.
10 changes: 10 additions & 0 deletions .changeset/rich-tigers-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'ai': patch
'@ai-sdk/fireworks': patch
'@ai-sdk/google-vertex': patch
'@ai-sdk/openai': patch
'@ai-sdk/provider': patch
'@ai-sdk/replicate': patch
---

feat (ai/core): throw NoImageGeneratedError from generateImage when no predictions are returned.
28 changes: 28 additions & 0 deletions content/docs/03-ai-sdk-core/35-image-generation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,34 @@ const { image, warnings } = await generateImage({
});
```

### Error Handling

When `generateImage` cannot generate a valid image, it throws a [`AI_NoImageGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-image-generated-error).

This error occurs when the AI provider fails to generate an image. It can arise due to the following reasons:

- The model failed to generate a response
- The model generated a response that could not be parsed

The error preserves the following information to help you log the issue:

- `responses`: Metadata about the image model responses, including timestamp, model, and headers.
- `cause`: The cause of the error. You can use this for more detailed error handling

```ts
import { generateImage, NoImageGeneratedError } from 'ai';

try {
await generateImage({ model, prompt });
} catch (error) {
if (NoImageGeneratedError.isInstance(error)) {
console.log('NoImageGeneratedError');
console.log('Cause:', error.cause);
console.log('Responses:', error.responses);
}
}
```

## Image Models

| Provider | Model | Support sizes (`width x height`) or aspect ratios (`width : height`) |
Expand Down
30 changes: 30 additions & 0 deletions content/docs/07-reference/01-ai-sdk-core/10-generate-image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,35 @@ console.log(images);
description:
'Warnings from the model provider (e.g. unsupported settings).',
},
{
name: 'responses',
type: 'Array<ImageModelResponseMetadata>',
description:
'Response metadata from the provider. There may be multiple responses if we made multiple calls to the model.',
properties: [
{
type: 'ImageModelResponseMetadata',
parameters: [
{
name: 'timestamp',
type: 'Date',
description: 'Timestamp for the start of the generated response.',
},
{
name: 'modelId',
type: 'string',
description:
'The ID of the response model that was used to generate the response.',
},
{
name: 'headers',
type: 'Record<string, string>',
isOptional: true,
description: 'Response headers.',
},
],
},
],
},
]}
/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
title: AI_NoImageGeneratedError
description: Learn how to fix AI_NoImageGeneratedError
---

# AI_NoImageGeneratedError

This error occurs when the AI provider fails to generate an image.
It can arise due to the following reasons:

- The model failed to generate a response.
- The model generated an invalid response.

## Properties

- `message`: The error message.
- `responses`: Metadata about the image model responses, including timestamp, model, and headers.
- `cause`: The cause of the error. You can use this for more detailed error handling.

## Checking for this Error

You can check if an error is an instance of `AI_NoImageGeneratedError` using:

```typescript
import { generateImage, NoImageGeneratedError } from 'ai';

try {
await generateImage({ model, prompt });
} catch (error) {
if (NoImageGeneratedError.isInstance(error)) {
console.log('NoImageGeneratedError');
console.log('Cause:', error.cause);
console.log('Responses:', error.responses);
}
}
```
1 change: 1 addition & 0 deletions content/docs/07-reference/05-ai-sdk-errors/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ collapsed: true
- [AI_LoadSettingError](/docs/reference/ai-sdk-errors/ai-load-setting-error)
- [AI_MessageConversionError](/docs/reference/ai-sdk-errors/ai-message-conversion-error)
- [AI_NoContentGeneratedError](/docs/reference/ai-sdk-errors/ai-no-content-generated-error)
- [AI_NoImageGeneratedError](/docs/reference/ai-sdk-errors/ai-no-image-generated-error)
- [AI_NoObjectGeneratedError](/docs/reference/ai-sdk-errors/ai-no-object-generated-error)
- [AI_NoOutputSpecifiedError](/docs/reference/ai-sdk-errors/ai-no-output-specified-error)
- [AI_NoSuchModelError](/docs/reference/ai-sdk-errors/ai-no-such-model-error)
Expand Down
6 changes: 6 additions & 0 deletions packages/ai/core/generate-image/generate-image-result.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ImageGenerationWarning } from '../types/image-model';
import { ImageModelResponseMetadata } from '../types/image-model-response-metadata';

/**
The result of a `generateImage` call.
Expand All @@ -19,6 +20,11 @@ The images that were generated.
Warnings for the call, e.g. unsupported settings.
*/
readonly warnings: Array<ImageGenerationWarning>;

/**
Response metadata from the provider. There may be multiple responses if we made multiple calls to the model.
*/
readonly responses: Array<ImageModelResponseMetadata>;
}

export interface GeneratedImage {
Expand Down
167 changes: 144 additions & 23 deletions packages/ai/core/generate-image/generate-image.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ImageModelV1 } from '@ai-sdk/provider';
import { ImageModelV1, ImageModelV1CallWarning } from '@ai-sdk/provider';
import { MockImageModelV1 } from '../test/mock-image-model-v1';
import { generateImage } from './generate-image';
import {
Expand All @@ -7,8 +7,29 @@ import {
} from '@ai-sdk/provider-utils';

const prompt = 'sunny day at the beach';
const testDate = new Date(2024, 0, 1);

const createMockResponse = (options: {
images: string[] | Uint8Array[];
warnings?: ImageModelV1CallWarning[];
timestamp?: Date;
modelId?: string;
headers?: Record<string, string>;
}) => ({
images: options.images,
warnings: options.warnings ?? [],
response: {
timestamp: options.timestamp ?? new Date(),
modelId: options.modelId ?? 'test-model-id',
headers: options.headers ?? {},
},
});

describe('generateImage', () => {
// 1x1 transparent PNG
const mockBase64Image =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';

it('should send args to doGenerate', async () => {
const abortController = new AbortController();
const abortSignal = abortController.signal;
Expand All @@ -19,7 +40,9 @@ describe('generateImage', () => {
model: new MockImageModelV1({
doGenerate: async args => {
capturedArgs = args;
return { images: [], warnings: [] };
return createMockResponse({
images: [mockBase64Image],
});
},
}),
prompt,
Expand All @@ -46,15 +69,16 @@ describe('generateImage', () => {
it('should return warnings', async () => {
const result = await generateImage({
model: new MockImageModelV1({
doGenerate: async () => ({
images: [],
warnings: [
{
type: 'other',
message: 'Setting is not supported',
},
],
}),
doGenerate: async () =>
createMockResponse({
images: [mockBase64Image],
warnings: [
{
type: 'other',
message: 'Setting is not supported',
},
],
}),
}),
prompt,
});
Expand All @@ -76,7 +100,10 @@ describe('generateImage', () => {

const result = await generateImage({
model: new MockImageModelV1({
doGenerate: async () => ({ images: base64Images, warnings: [] }),
doGenerate: async () =>
createMockResponse({
images: base64Images,
}),
}),
prompt,
});
Expand All @@ -103,10 +130,10 @@ describe('generateImage', () => {

const result = await generateImage({
model: new MockImageModelV1({
doGenerate: async () => ({
images: [base64Image, 'base64-image-2'],
warnings: [],
}),
doGenerate: async () =>
createMockResponse({
images: [base64Image, 'base64-image-2'],
}),
}),
prompt,
});
Expand All @@ -130,7 +157,10 @@ describe('generateImage', () => {

const result = await generateImage({
model: new MockImageModelV1({
doGenerate: async () => ({ images: uint8ArrayImages, warnings: [] }),
doGenerate: async () =>
createMockResponse({
images: uint8ArrayImages,
}),
}),
prompt,
});
Expand Down Expand Up @@ -179,7 +209,9 @@ describe('generateImage', () => {
headers: { 'custom-request-header': 'request-header-value' },
abortSignal: undefined,
});
return { images: base64Images.slice(0, 2), warnings: [] };
return createMockResponse({
images: base64Images.slice(0, 2),
});
case 1:
expect(options).toStrictEqual({
prompt,
Expand All @@ -191,7 +223,9 @@ describe('generateImage', () => {
headers: { 'custom-request-header': 'request-header-value' },
abortSignal: undefined,
});
return { images: base64Images.slice(2), warnings: [] };
return createMockResponse({
images: base64Images.slice(2),
});
default:
throw new Error('Unexpected call');
}
Expand Down Expand Up @@ -236,10 +270,10 @@ describe('generateImage', () => {
headers: { 'custom-request-header': 'request-header-value' },
abortSignal: undefined,
});
return {
return createMockResponse({
images: base64Images.slice(0, 2),
warnings: [{ type: 'other', message: '1' }],
};
});
case 1:
expect(options).toStrictEqual({
prompt,
Expand All @@ -251,10 +285,10 @@ describe('generateImage', () => {
headers: { 'custom-request-header': 'request-header-value' },
abortSignal: undefined,
});
return {
return createMockResponse({
images: base64Images.slice(2),
warnings: [{ type: 'other', message: '2' }],
};
});
default:
throw new Error('Unexpected call');
}
Expand All @@ -275,4 +309,91 @@ describe('generateImage', () => {
]);
});
});

describe('error handling', () => {
it('should throw NoImageGeneratedError when no images are returned', async () => {
await expect(
generateImage({
model: new MockImageModelV1({
doGenerate: async () =>
createMockResponse({
images: [],
timestamp: testDate,
}),
}),
prompt,
_internal: {
currentDate: () => testDate,
},
}),
).rejects.toMatchObject({
name: 'AI_NoImageGeneratedError',
message: 'No image generated.',
responses: [
{
timestamp: testDate,
modelId: expect.any(String),
},
],
});
});

it('should include response headers in error when no images generated', async () => {
await expect(
generateImage({
model: new MockImageModelV1({
doGenerate: async () =>
createMockResponse({
images: [],
timestamp: testDate,
headers: {
'custom-response-header': 'response-header-value',
},
}),
}),
prompt,
_internal: {
currentDate: () => testDate,
},
}),
).rejects.toMatchObject({
name: 'AI_NoImageGeneratedError',
message: 'No image generated.',
responses: [
{
timestamp: testDate,
modelId: expect.any(String),
headers: {
'custom-response-header': 'response-header-value',
},
},
],
});
});
});

it('should return response metadata', async () => {
const testHeaders = { 'x-test': 'value' };

const result = await generateImage({
model: new MockImageModelV1({
doGenerate: async () =>
createMockResponse({
images: [mockBase64Image],
timestamp: testDate,
modelId: 'test-model',
headers: testHeaders,
}),
}),
prompt,
});

expect(result.responses).toStrictEqual([
{
timestamp: testDate,
modelId: 'test-model',
headers: testHeaders,
},
]);
});
});
Loading

0 comments on commit 3a58a2e

Please sign in to comment.