ai
Version:
AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript
591 lines (525 loc) • 17.8 kB
text/typescript
import {
JSONValue,
LanguageModelV3CallOptions,
TypeValidationError,
} from '@ai-sdk/provider';
import {
asSchema,
FlexibleSchema,
resolve,
safeParseJSON,
safeValidateTypes,
} from '@ai-sdk/provider-utils';
import { NoObjectGeneratedError } from '../error/no-object-generated-error';
import { FinishReason } from '../types/language-model';
import { LanguageModelResponseMetadata } from '../types/language-model-response-metadata';
import { LanguageModelUsage } from '../types/usage';
import { DeepPartial } from '../util/deep-partial';
import { parsePartialJson } from '../util/parse-partial-json';
import { EnrichedStreamPart } from './stream-text';
export interface Output<OUTPUT = any, PARTIAL = any, ELEMENT = any> {
/**
* The name of the output mode.
*/
name: string;
/**
* The response format to use for the model.
*/
responseFormat: PromiseLike<LanguageModelV3CallOptions['responseFormat']>;
/**
* Parses the complete output of the model.
*/
parseCompleteOutput(
options: { text: string },
context: {
response: LanguageModelResponseMetadata;
usage: LanguageModelUsage;
finishReason: FinishReason;
},
): Promise<OUTPUT>;
/**
* Parses the partial output of the model.
*/
parsePartialOutput(options: {
text: string;
}): Promise<{ partial: PARTIAL } | undefined>;
/**
* Creates a stream transform that emits individual elements as they complete.
*/
createElementStreamTransform():
| TransformStream<EnrichedStreamPart<any, PARTIAL>, ELEMENT>
| undefined;
}
/**
* Output specification for text generation.
* This is the default output mode that generates plain text.
*
* @returns An output specification for generating text.
*/
export const text = (): Output<string, string, never> => ({
name: 'text',
responseFormat: Promise.resolve({ type: 'text' }),
async parseCompleteOutput({ text }: { text: string }) {
return text;
},
async parsePartialOutput({ text }: { text: string }) {
return { partial: text };
},
createElementStreamTransform() {
return undefined;
},
});
/**
* Output specification for typed object generation using schemas.
* When the model generates a text response, it will return an object that matches the schema.
*
* @param schema - The schema of the object to generate.
* @param name - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name.
* @param description - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*
* @returns An output specification for generating objects with the specified schema.
*/
export const object = <OBJECT>({
schema: inputSchema,
name,
description,
}: {
schema: FlexibleSchema<OBJECT>;
/**
* Optional name of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema name.
*/
name?: string;
/**
* Optional description of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*/
description?: string;
}): Output<OBJECT, DeepPartial<OBJECT>, never> => {
const schema = asSchema(inputSchema);
return {
name: 'object',
responseFormat: resolve(schema.jsonSchema).then(jsonSchema => ({
type: 'json' as const,
schema: jsonSchema,
...(name != null && { name }),
...(description != null && { description }),
})),
async parseCompleteOutput(
{ text }: { text: string },
context: {
response: LanguageModelResponseMetadata;
usage: LanguageModelUsage;
finishReason: FinishReason;
},
) {
const parseResult = await safeParseJSON({ text });
if (!parseResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: could not parse the response.',
cause: parseResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
const validationResult = await safeValidateTypes({
value: parseResult.value,
schema,
});
if (!validationResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: response did not match schema.',
cause: validationResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
return validationResult.value;
},
async parsePartialOutput({ text }: { text: string }) {
const result = await parsePartialJson(text);
switch (result.state) {
case 'failed-parse':
case 'undefined-input': {
return undefined;
}
case 'repaired-parse':
case 'successful-parse': {
return {
// Note: currently no validation of partial results:
partial: result.value as DeepPartial<OBJECT>,
};
}
}
},
createElementStreamTransform() {
return undefined;
},
};
};
/**
* Output specification for array generation.
* When the model generates a text response, it will return an array of elements.
*
* @param element - The schema of the array elements to generate.
* @param name - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name.
* @param description - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*
* @returns An output specification for generating an array of elements.
*/
export const array = <ELEMENT>({
element: inputElementSchema,
name,
description,
}: {
element: FlexibleSchema<ELEMENT>;
/**
* Optional name of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema name.
*/
name?: string;
/**
* Optional description of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*/
description?: string;
}): Output<Array<ELEMENT>, Array<ELEMENT>, ELEMENT> => {
const elementSchema = asSchema(inputElementSchema);
return {
name: 'array',
// JSON schema that describes an array of elements:
responseFormat: resolve(elementSchema.jsonSchema).then(jsonSchema => {
// remove $schema from schema.jsonSchema:
const { $schema, ...itemSchema } = jsonSchema;
return {
type: 'json' as const,
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
elements: { type: 'array', items: itemSchema },
},
required: ['elements'],
additionalProperties: false,
},
...(name != null && { name }),
...(description != null && { description }),
};
}),
async parseCompleteOutput(
{ text }: { text: string },
context: {
response: LanguageModelResponseMetadata;
usage: LanguageModelUsage;
finishReason: FinishReason;
},
) {
const parseResult = await safeParseJSON({ text });
if (!parseResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: could not parse the response.',
cause: parseResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
const outerValue = parseResult.value;
if (
outerValue == null ||
typeof outerValue !== 'object' ||
!('elements' in outerValue) ||
!Array.isArray(outerValue.elements)
) {
throw new NoObjectGeneratedError({
message: 'No object generated: response did not match schema.',
cause: new TypeValidationError({
value: outerValue,
cause: 'response must be an object with an elements array',
}),
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
for (const element of outerValue.elements) {
const validationResult = await safeValidateTypes({
value: element,
schema: elementSchema,
});
if (!validationResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: response did not match schema.',
cause: validationResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
}
return outerValue.elements as Array<ELEMENT>;
},
async parsePartialOutput({ text }: { text: string }) {
const result = await parsePartialJson(text);
switch (result.state) {
case 'failed-parse':
case 'undefined-input': {
return undefined;
}
case 'repaired-parse':
case 'successful-parse': {
const outerValue = result.value;
// no parsable elements array
if (
outerValue == null ||
typeof outerValue !== 'object' ||
!('elements' in outerValue) ||
!Array.isArray(outerValue.elements)
) {
return undefined;
}
const rawElements =
result.state === 'repaired-parse' && outerValue.elements.length > 0
? outerValue.elements.slice(0, -1)
: outerValue.elements;
const parsedElements: Array<ELEMENT> = [];
for (const rawElement of rawElements) {
const validationResult = await safeValidateTypes({
value: rawElement,
schema: elementSchema,
});
if (validationResult.success) {
parsedElements.push(validationResult.value);
}
}
return { partial: parsedElements };
}
}
},
createElementStreamTransform() {
let publishedElements = 0;
return new TransformStream<
EnrichedStreamPart<any, Array<ELEMENT>>,
ELEMENT
>({
transform({ partialOutput }, controller) {
if (partialOutput != null) {
// Only enqueue new elements that haven't been published yet
for (
;
publishedElements < partialOutput.length;
publishedElements++
) {
controller.enqueue(partialOutput[publishedElements]);
}
}
},
});
},
};
};
/**
* Output specification for choice generation.
* When the model generates a text response, it will return a one of the choice options.
*
* @param options - The available choices.
* @param name - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name.
* @param description - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*
* @returns An output specification for generating a choice.
*/
export const choice = <CHOICE extends string>({
options: choiceOptions,
name,
description,
}: {
options: Array<CHOICE>;
/**
* Optional name of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema name.
*/
name?: string;
/**
* Optional description of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*/
description?: string;
}): Output<CHOICE, CHOICE, never> => {
return {
name: 'choice',
// JSON schema that describes an enumeration:
responseFormat: Promise.resolve({
type: 'json',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
result: { type: 'string', enum: choiceOptions },
},
required: ['result'],
additionalProperties: false,
},
...(name != null && { name }),
...(description != null && { description }),
} as const),
async parseCompleteOutput(
{ text }: { text: string },
context: {
response: LanguageModelResponseMetadata;
usage: LanguageModelUsage;
finishReason: FinishReason;
},
) {
const parseResult = await safeParseJSON({ text });
if (!parseResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: could not parse the response.',
cause: parseResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
const outerValue = parseResult.value;
if (
outerValue == null ||
typeof outerValue !== 'object' ||
!('result' in outerValue) ||
typeof outerValue.result !== 'string' ||
!choiceOptions.includes(outerValue.result as any)
) {
throw new NoObjectGeneratedError({
message: 'No object generated: response did not match schema.',
cause: new TypeValidationError({
value: outerValue,
cause: 'response must be an object that contains a choice value.',
}),
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
return outerValue.result as CHOICE;
},
async parsePartialOutput({ text }: { text: string }) {
const result = await parsePartialJson(text);
switch (result.state) {
case 'failed-parse':
case 'undefined-input': {
return undefined;
}
case 'repaired-parse':
case 'successful-parse': {
const outerValue = result.value;
if (
outerValue == null ||
typeof outerValue !== 'object' ||
!('result' in outerValue) ||
typeof outerValue.result !== 'string'
) {
return undefined;
}
// list of potential matches.
const potentialMatches = choiceOptions.filter(choiceOption =>
choiceOption.startsWith(outerValue.result as string),
);
if (result.state === 'successful-parse') {
// successful parse: exact choice value
return potentialMatches.includes(outerValue.result as any)
? { partial: outerValue.result as CHOICE }
: undefined;
} else {
// repaired parse: only return if not ambiguous
return potentialMatches.length === 1
? { partial: potentialMatches[0] as CHOICE }
: undefined;
}
}
}
},
createElementStreamTransform() {
return undefined;
},
};
};
/**
* Output specification for unstructured JSON generation.
* When the model generates a text response, it will return a JSON object.
*
* @param name - Optional name of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema name.
* @param description - Optional description of the output that should be generated. Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*
* @returns An output specification for generating JSON.
*/
export const json = ({
name,
description,
}: {
/**
* Optional name of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema name.
*/
name?: string;
/**
* Optional description of the output that should be generated.
* Used by some providers for additional LLM guidance, e.g. via tool or schema description.
*/
description?: string;
} = {}): Output<JSONValue, JSONValue, never> => {
return {
name: 'json',
responseFormat: Promise.resolve({
type: 'json' as const,
...(name != null && { name }),
...(description != null && { description }),
}),
async parseCompleteOutput(
{ text }: { text: string },
context: {
response: LanguageModelResponseMetadata;
usage: LanguageModelUsage;
finishReason: FinishReason;
},
) {
const parseResult = await safeParseJSON({ text });
if (!parseResult.success) {
throw new NoObjectGeneratedError({
message: 'No object generated: could not parse the response.',
cause: parseResult.error,
text,
response: context.response,
usage: context.usage,
finishReason: context.finishReason,
});
}
return parseResult.value;
},
async parsePartialOutput({ text }: { text: string }) {
const result = await parsePartialJson(text);
switch (result.state) {
case 'failed-parse':
case 'undefined-input': {
return undefined;
}
case 'repaired-parse':
case 'successful-parse': {
return result.value === undefined
? undefined
: { partial: result.value };
}
}
},
createElementStreamTransform() {
return undefined;
},
};
};