Skip to content

Commit

Permalink
feat (ai/core): add additional information to NoObjectGeneratedError (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
lgrammel authored Dec 13, 2024
1 parent 078db00 commit 304e6d3
Show file tree
Hide file tree
Showing 17 changed files with 808 additions and 113 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-poems-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ai': patch
---

feat (ai/core): standardize generateObject, streamObject, and output errors to NoObjectGeneratedError
5 changes: 5 additions & 0 deletions .changeset/orange-glasses-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'ai': patch
---

feat (ai/core): add additional information to NoObjectGeneratedError
65 changes: 24 additions & 41 deletions content/docs/03-ai-sdk-core/10-generating-structured-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,51 +175,34 @@ const { object } = await generateObject({

## Error Handling

When you use `generateObject`, errors are thrown when the model fails to generate proper JSON (`JSONParseError`)
or when the generated JSON does not match the schema (`TypeValidationError`).
Both error types contain additional information, e.g. the generated text or the invalid value.
When `generateObject` cannot generate a valid object, it throws a [`AI_NoObjectGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-object-generated-error).

You can use this to e.g. design a function that safely process the result object and also returns values in error cases:
This error occurs when the AI provider fails to generate a parsable object that conforms to the schema.
It can arise due to the following reasons:

```ts
import { openai } from '@ai-sdk/openai';
import { JSONParseError, TypeValidationError, generateObject } from 'ai';
import { z } from 'zod';
- The model failed to generate a response.
- The model generated a response that could not be parsed.
- The model generated a response that could not be validated against the schema.

const recipeSchema = z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
steps: z.array(z.string()),
}),
});
The error preserves the following information to help you log the issue:

type Recipe = z.infer<typeof recipeSchema>;

async function generateRecipe(
food: string,
): Promise<
| { type: 'success'; recipe: Recipe }
| { type: 'parse-error'; text: string }
| { type: 'validation-error'; value: unknown }
| { type: 'unknown-error'; error: unknown }
> {
try {
const result = await generateObject({
model: openai('gpt-4-turbo'),
schema: recipeSchema,
prompt: `Generate a ${food} recipe.`,
});

return { type: 'success', recipe: result.object };
} catch (error) {
if (TypeValidationError.isTypeValidationError(error)) {
return { type: 'validation-error', value: error.value };
} else if (JSONParseError.isJSONParseError(error)) {
return { type: 'parse-error', text: error.text };
} else {
return { type: 'unknown-error', error };
}
- `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the object generation mode.
- `response`: Metadata about the language model response, including response id, timestamp, and model.
- `usage`: Request token usage.
- `cause`: The cause of the error (e.g. a JSON parsing error). You can use this for more detailed error handling.

```ts
import { generateObject, NoObjectGeneratedError } from 'ai';

try {
await generateObject({ model, schema, prompt });
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.log('NoObjectGeneratedError');
console.log('Cause:', error.cause);
console.log('Text:', error.text);
console.log('Response:', error.response);
console.log('Usage:', error.usage);
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,37 @@ description: Learn how to fix AI_NoObjectGeneratedError

# AI_NoObjectGeneratedError

This error occurs when the AI provider fails to generate a parsable object.
This error occurs when the AI provider fails to generate a parsable object that conforms to the schema.
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 model generated a response that could not be validated against the schema.

## Properties

- `message`: The error message
- `message`: The error message.
- `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the object generation mode.
- `response`: Metadata about the language model response, including response id, timestamp, and model.
- `usage`: Request token usage.
- `cause`: The cause of the error (e.g. a JSON parsing 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_NoObjectGeneratedError` using:

```typescript
import { NoObjectGeneratedError } from 'ai';

if (NoObjectGeneratedError.isInstance(error)) {
// Handle the error
import { generateObject, NoObjectGeneratedError } from 'ai';

try {
await generateObject({ model, schema, prompt });
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.log('NoObjectGeneratedError');
console.log('Cause:', error.cause);
console.log('Text:', error.text);
console.log('Response:', error.response);
console.log('Usage:', error.usage);
}
}
```
37 changes: 37 additions & 0 deletions examples/ai-core/src/generate-object/mock-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { generateObject, NoObjectGeneratedError } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import 'dotenv/config';
import { z } from 'zod';

async function main() {
try {
await generateObject({
model: new MockLanguageModelV1({
defaultObjectGenerationMode: 'json',
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
response: {
id: 'id-1',
timestamp: new Date(123),
modelId: 'model-1',
},
finishReason: 'stop',
usage: { promptTokens: 10, completionTokens: 20 },
text: `{"content":"Hello broken json`,
}),
}),
schema: z.object({ content: z.string() }),
prompt: 'Hello, test!',
});
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.log('NoObjectGeneratedError');
console.log('Cause:', error.cause);
console.log('Text:', error.text);
console.log('Response:', error.response);
console.log('Usage:', error.usage);
}
}
}

main().catch(console.error);
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,7 @@ exports[`streamObject > output = "object" > options.onFinish > should be called

exports[`streamObject > output = "object" > options.onFinish > should be called when object doesn't match the schema 1`] = `
{
"error": [AI_TypeValidationError: Type validation failed: Value: {"invalid":"Hello, world!"}.
Error message: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"content"
],
"message": "Required"
}
]],
"error": [AI_NoObjectGeneratedError: No object generated: response did not match schema.],
"experimental_providerMetadata": undefined,
"object": undefined,
"response": {
Expand Down
171 changes: 170 additions & 1 deletion packages/ai/core/generate-object/generate-object.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test';
import { jsonSchema } from '@ai-sdk/ui-utils';
import assert from 'node:assert';
import assert, { fail } from 'node:assert';
import { z } from 'zod';
import { verifyNoObjectGeneratedError as originalVerifyNoObjectGeneratedError } from '../../errors/no-object-generated-error';
import { MockLanguageModelV1 } from '../test/mock-language-model-v1';
import { MockTracer } from '../test/mock-tracer';
import { generateObject } from './generate-object';
Expand All @@ -10,6 +11,7 @@ const dummyResponseValues = {
rawCall: { rawPrompt: 'prompt', rawSettings: {} },
finishReason: 'stop' as const,
usage: { promptTokens: 10, completionTokens: 20 },
response: { id: 'id-1', timestamp: new Date(123), modelId: 'm-1' },
};

describe('output = "object"', () => {
Expand Down Expand Up @@ -695,6 +697,173 @@ describe('output = "object"', () => {
});
});
});

describe('error handling', () => {
function verifyNoObjectGeneratedError(
error: unknown,
{ message }: { message: string },
) {
originalVerifyNoObjectGeneratedError(error, {
message,
response: {
id: 'id-1',
timestamp: new Date(123),
modelId: 'm-1',
},
usage: {
completionTokens: 20,
promptTokens: 10,
totalTokens: 30,
},
});
}

it('should throw NoObjectGeneratedError when schema validation fails in tool model', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'tool-call-1',
toolName: 'json',
args: `{ "content": 123 }`,
},
],
}),
}),
schema: z.object({ content: z.string() }),
mode: 'tool',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: response did not match schema.',
});
}
});

it('should throw NoObjectGeneratedError when schema validation fails in json model', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
text: `{ "content": 123 }`,
}),
}),
schema: z.object({ content: z.string() }),
mode: 'json',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: response did not match schema.',
});
}
});

it('should throw NoObjectGeneratedError when parsing fails in tool model', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'tool-call-1',
toolName: 'json',
args: `{ broken json`,
},
],
}),
}),
schema: z.object({ content: z.string() }),
mode: 'tool',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: could not parse the response.',
});
}
});

it('should throw NoObjectGeneratedError when parsing fails in json model', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
text: '{ broken json',
}),
}),
schema: z.object({ content: z.string() }),
mode: 'json',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: could not parse the response.',
});
}
});

it('should throw NoObjectGeneratedError when the tool was not called in tool mode', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
text: undefined,
}),
}),
schema: z.object({ content: z.string() }),
mode: 'tool',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: the tool was not called.',
});
}
});

it('should throw NoObjectGeneratedError when no text is available in json model', async () => {
try {
await generateObject({
model: new MockLanguageModelV1({
doGenerate: async ({}) => ({
...dummyResponseValues,
text: undefined,
}),
}),
schema: z.object({ content: z.string() }),
mode: 'json',
prompt: 'prompt',
});

fail('must throw error');
} catch (error) {
verifyNoObjectGeneratedError(error, {
message: 'No object generated: the model did not return a response.',
});
}
});
});
});

describe('output = "array"', () => {
Expand Down
Loading

0 comments on commit 304e6d3

Please sign in to comment.