ai
Version:
AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript
1,614 lines (1,537 loc) • 225 kB
text/typescript
import {
LanguageModelV3,
LanguageModelV3CallOptions,
LanguageModelV3FunctionTool,
LanguageModelV3Prompt,
LanguageModelV3ProviderTool,
LanguageModelV3Usage,
} from '@ai-sdk/provider';
import {
dynamicTool,
jsonSchema,
ModelMessage,
tool,
ToolExecuteFunction,
} from '@ai-sdk/provider-utils';
import { mockId } from '@ai-sdk/provider-utils/test';
import {
afterEach,
assert,
assertType,
beforeEach,
describe,
expect,
it,
vi,
vitest,
} from 'vitest';
import { z } from 'zod/v4';
import { Output } from '.';
import * as logWarningsModule from '../logger/log-warnings';
import { MockLanguageModelV3 } from '../test/mock-language-model-v3';
import { MockTracer } from '../test/mock-tracer';
import { generateText, GenerateTextOnFinishCallback } from './generate-text';
import { GenerateTextResult } from './generate-text-result';
import { StepResult } from './step-result';
import { stepCountIs } from './stop-condition';
vi.mock('../version', () => {
return {
VERSION: '0.0.0-test',
};
});
const testUsage: LanguageModelV3Usage = {
inputTokens: {
total: 3,
noCache: 3,
cacheRead: undefined,
cacheWrite: undefined,
},
outputTokens: {
total: 10,
text: 10,
reasoning: undefined,
},
};
const dummyResponseValues = {
finishReason: { unified: 'stop', raw: 'stop' } as const,
usage: testUsage,
warnings: [],
};
const modelWithSources = new MockLanguageModelV3({
doGenerate: {
...dummyResponseValues,
content: [
{ type: 'text', text: 'Hello, world!' },
{
type: 'source',
sourceType: 'url',
id: '123',
url: 'https://example.com',
title: 'Example',
providerMetadata: { provider: { custom: 'value' } },
},
{
type: 'source',
sourceType: 'url',
id: '456',
url: 'https://example.com/2',
title: 'Example 2',
providerMetadata: { provider: { custom: 'value2' } },
},
],
},
});
const modelWithFiles = new MockLanguageModelV3({
doGenerate: {
...dummyResponseValues,
content: [
{ type: 'text', text: 'Hello, world!' },
{
type: 'file',
data: new Uint8Array([1, 2, 3]),
mediaType: 'image/png',
},
{
type: 'file',
data: 'QkFVRw==',
mediaType: 'image/jpeg',
},
],
},
});
const modelWithReasoning = new MockLanguageModelV3({
doGenerate: {
...dummyResponseValues,
content: [
{
type: 'reasoning',
text: 'I will open the conversation with witty banter.',
providerMetadata: {
testProvider: {
signature: 'signature',
},
},
},
{
type: 'reasoning',
text: '',
providerMetadata: {
testProvider: {
redactedData: 'redacted-reasoning-data',
},
},
},
{ type: 'text', text: 'Hello, world!' },
],
},
});
describe('generateText', () => {
let logWarningsSpy: ReturnType<typeof vitest.spyOn>;
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
logWarningsSpy = vitest
.spyOn(logWarningsModule, 'logWarnings')
.mockImplementation(() => {});
});
afterEach(() => {
vi.useRealTimers();
logWarningsSpy.mockRestore();
});
describe('result.content', () => {
it('should generate content', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: {
...dummyResponseValues,
content: [
{ type: 'text', text: 'Hello, world!' },
{
type: 'source',
sourceType: 'url',
id: '123',
url: 'https://example.com',
title: 'Example',
providerMetadata: { provider: { custom: 'value' } },
},
{
type: 'file',
data: new Uint8Array([1, 2, 3]),
mediaType: 'image/png',
},
{
type: 'reasoning',
text: 'I will open the conversation with witty banter.',
},
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
{ type: 'text', text: 'More text' },
],
},
}),
prompt: 'prompt',
tools: {
tool1: {
inputSchema: z.object({ value: z.string() }),
execute: async args => {
expect(args).toStrictEqual({ value: 'value' });
return 'result1';
},
},
},
});
expect(result.content).toMatchInlineSnapshot(`
[
{
"text": "Hello, world!",
"type": "text",
},
{
"id": "123",
"providerMetadata": {
"provider": {
"custom": "value",
},
},
"sourceType": "url",
"title": "Example",
"type": "source",
"url": "https://example.com",
},
{
"file": DefaultGeneratedFile {
"base64Data": "AQID",
"mediaType": "image/png",
"uint8ArrayData": Uint8Array [
1,
2,
3,
],
},
"type": "file",
},
{
"text": "I will open the conversation with witty banter.",
"type": "reasoning",
},
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
{
"text": "More text",
"type": "text",
},
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "result1",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
]
`);
});
});
describe('result.text', () => {
it('should generate text', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: {
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
},
}),
prompt: 'prompt',
});
expect(modelWithSources.doGenerateCalls).toMatchSnapshot();
expect(result.text).toStrictEqual('Hello, world!');
});
});
describe('result.reasoningText', () => {
it('should contain reasoning string from model response', async () => {
const result = await generateText({
model: modelWithReasoning,
prompt: 'prompt',
});
expect(result.reasoningText).toStrictEqual(
'I will open the conversation with witty banter.',
);
});
});
describe('result.sources', () => {
it('should contain sources', async () => {
const result = await generateText({
model: modelWithSources,
prompt: 'prompt',
});
expect(result.sources).toMatchSnapshot();
});
});
describe('result.files', () => {
it('should contain files', async () => {
const result = await generateText({
model: modelWithFiles,
prompt: 'prompt',
});
expect(result.files).toMatchSnapshot();
});
});
describe('result.steps', () => {
it('should add the reasoning from the model response to the step result', async () => {
const result = await generateText({
model: modelWithReasoning,
prompt: 'prompt',
_internal: {
generateId: mockId({ prefix: 'id' }),
},
});
expect(result.steps).toMatchSnapshot();
});
it('should contain sources', async () => {
const result = await generateText({
model: modelWithSources,
prompt: 'prompt',
_internal: {
generateId: mockId({ prefix: 'id' }),
},
});
expect(result.steps).toMatchSnapshot();
});
it('should contain files', async () => {
const result = await generateText({
model: modelWithFiles,
prompt: 'prompt',
_internal: {
generateId: mockId({ prefix: 'id' }),
},
});
expect(result.steps).toMatchSnapshot();
});
});
describe('result.toolCalls', () => {
it('should contain tool calls', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async ({ prompt, tools, toolChoice }) => {
expect(tools).toStrictEqual([
{
type: 'function',
name: 'tool1',
description: undefined,
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: { value: { type: 'string' } },
required: ['value'],
type: 'object',
},
providerOptions: undefined,
},
{
type: 'function',
name: 'tool2',
description: undefined,
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: { somethingElse: { type: 'string' } },
required: ['somethingElse'],
type: 'object',
},
providerOptions: undefined,
},
]);
expect(toolChoice).toStrictEqual({ type: 'required' });
expect(prompt).toStrictEqual([
{
role: 'user',
content: [{ type: 'text', text: 'test-input' }],
providerOptions: undefined,
},
]);
return {
...dummyResponseValues,
content: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
};
},
}),
tools: {
tool1: {
inputSchema: z.object({ value: z.string() }),
},
// 2nd tool to show typing:
tool2: {
inputSchema: z.object({ somethingElse: z.string() }),
},
},
toolChoice: 'required',
prompt: 'test-input',
});
// test type inference
if (
result.toolCalls[0].toolName === 'tool1' &&
!result.toolCalls[0].dynamic
) {
assertType<string>(result.toolCalls[0].input.value);
}
expect(result.toolCalls).toMatchInlineSnapshot(`
[
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
]
`);
});
});
describe('result.toolResults', () => {
it('should contain tool results', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async ({ prompt, tools, toolChoice }) => {
expect(tools).toStrictEqual([
{
type: 'function',
name: 'tool1',
description: undefined,
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: { value: { type: 'string' } },
required: ['value'],
type: 'object',
},
providerOptions: undefined,
},
]);
expect(toolChoice).toStrictEqual({ type: 'auto' });
expect(prompt).toStrictEqual([
{
role: 'user',
content: [{ type: 'text', text: 'test-input' }],
providerOptions: undefined,
},
]);
return {
...dummyResponseValues,
content: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
};
},
}),
tools: {
tool1: {
inputSchema: z.object({ value: z.string() }),
execute: async args => {
expect(args).toStrictEqual({ value: 'value' });
return 'result1';
},
},
},
prompt: 'test-input',
});
// test type inference
if (
result.toolResults[0].toolName === 'tool1' &&
!result.toolResults[0].dynamic
) {
assertType<string>(result.toolResults[0].output);
}
expect(result.toolResults).toMatchInlineSnapshot(`
[
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "result1",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
]
`);
});
});
describe('result.providerMetadata', () => {
it('should contain provider metadata', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async () => ({
...dummyResponseValues,
content: [],
providerMetadata: {
exampleProvider: {
a: 10,
b: 20,
},
},
}),
}),
prompt: 'test-input',
});
expect(result.providerMetadata).toStrictEqual({
exampleProvider: {
a: 10,
b: 20,
},
});
});
});
describe('result.response.messages', () => {
it('should contain assistant response message when there are no tool calls', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async () => ({
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
}),
}),
prompt: 'test-input',
});
expect(result.response.messages).toMatchSnapshot();
});
it('should contain assistant response message and tool message when there are tool calls with results', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async () => ({
...dummyResponseValues,
content: [
{ type: 'text', text: 'Hello, world!' },
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
}),
}),
tools: {
tool1: {
inputSchema: z.object({ value: z.string() }),
execute: async (args, options) => {
expect(args).toStrictEqual({ value: 'value' });
expect(options.messages).toStrictEqual([
{ role: 'user', content: 'test-input' },
]);
return 'result1';
},
},
},
prompt: 'test-input',
});
expect(result.response.messages).toMatchSnapshot();
});
it('should contain reasoning', async () => {
const result = await generateText({
model: modelWithReasoning,
prompt: 'test-input',
});
expect(result.response.messages).toMatchSnapshot();
});
});
describe('result.request', () => {
it('should contain request body', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async ({}) => ({
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
request: {
body: 'test body',
},
}),
}),
prompt: 'prompt',
});
expect(result.request).toStrictEqual({
body: 'test body',
});
});
});
describe('result.response', () => {
it('should contain response body and headers', async () => {
const result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async ({}) => ({
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
response: {
id: 'test-id-from-model',
timestamp: new Date(10000),
modelId: 'test-response-model-id',
headers: {
'custom-response-header': 'response-header-value',
},
body: 'test body',
},
}),
}),
prompt: 'prompt',
});
expect(result.steps[0].response).toMatchSnapshot();
expect(result.response).toMatchSnapshot();
});
});
describe('options.onFinish', () => {
it('should send correct information', async () => {
let result!: Parameters<GenerateTextOnFinishCallback<any>>[0];
await generateText({
model: new MockLanguageModelV3({
doGenerate: async () => ({
content: [
{ type: 'text', text: 'Hello, World!' },
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
finishReason: { unified: 'stop', raw: 'stop' },
usage: testUsage,
response: {
id: 'id-0',
modelId: 'mock-model-id',
timestamp: new Date(0),
headers: { call: '2' },
providerMetadata: {
testProvider: { testKey: 'testValue' },
},
},
warnings: [],
}),
}),
tools: {
tool1: {
inputSchema: z.object({ value: z.string() }),
execute: async ({ value }) => `${value}-result`,
},
},
onFinish: async event => {
result = event as unknown as typeof result;
},
prompt: 'irrelevant',
});
expect(result).toMatchInlineSnapshot(`
{
"content": [
{
"text": "Hello, World!",
"type": "text",
},
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "value-result",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"dynamicToolCalls": [],
"dynamicToolResults": [],
"experimental_context": undefined,
"files": [],
"finishReason": "stop",
"providerMetadata": undefined,
"rawFinishReason": "stop",
"reasoning": [],
"reasoningText": undefined,
"request": {},
"response": {
"body": undefined,
"headers": {
"call": "2",
},
"id": "id-0",
"messages": [
{
"content": [
{
"providerOptions": undefined,
"text": "Hello, World!",
"type": "text",
},
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "value-result",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
],
"modelId": "mock-model-id",
"timestamp": 1970-01-01T00:00:00.000Z,
},
"sources": [],
"staticToolCalls": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"staticToolResults": [
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "value-result",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"steps": [
DefaultStepResult {
"content": [
{
"text": "Hello, World!",
"type": "text",
},
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "value-result",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"finishReason": "stop",
"providerMetadata": undefined,
"rawFinishReason": "stop",
"request": {},
"response": {
"body": undefined,
"headers": {
"call": "2",
},
"id": "id-0",
"messages": [
{
"content": [
{
"providerOptions": undefined,
"text": "Hello, World!",
"type": "text",
},
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "value-result",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
],
"modelId": "mock-model-id",
"timestamp": 1970-01-01T00:00:00.000Z,
},
"usage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 3,
},
"inputTokens": 3,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 10,
},
"outputTokens": 10,
"raw": undefined,
"reasoningTokens": undefined,
"totalTokens": 13,
},
"warnings": [],
},
],
"text": "Hello, World!",
"toolCalls": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"toolResults": [
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "value-result",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"totalUsage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 3,
},
"inputTokens": 3,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 10,
},
"outputTokens": 10,
"reasoningTokens": undefined,
"totalTokens": 13,
},
"usage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 3,
},
"inputTokens": 3,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 10,
},
"outputTokens": 10,
"raw": undefined,
"reasoningTokens": undefined,
"totalTokens": 13,
},
"warnings": [],
}
`);
});
});
describe('options.stopWhen', () => {
let result: GenerateTextResult<any, any>;
let onFinishResult: Parameters<GenerateTextOnFinishCallback<any>>[0];
let onStepFinishResults: StepResult<any>[];
beforeEach(() => {
result = undefined as any;
onFinishResult = undefined as any;
onStepFinishResults = [];
});
describe('2 steps: initial, tool-result', () => {
beforeEach(async () => {
let responseCount = 0;
result = await generateText({
model: new MockLanguageModelV3({
doGenerate: async ({ prompt, tools, toolChoice }) => {
switch (responseCount++) {
case 0:
expect(tools).toStrictEqual([
{
type: 'function',
name: 'tool1',
description: undefined,
inputSchema: {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: { value: { type: 'string' } },
required: ['value'],
type: 'object',
},
providerOptions: undefined,
},
]);
expect(toolChoice).toStrictEqual({ type: 'auto' });
expect(prompt).toStrictEqual([
{
role: 'user',
content: [{ type: 'text', text: 'test-input' }],
providerOptions: undefined,
},
]);
return {
...dummyResponseValues,
content: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
finishReason: { unified: 'tool-calls', raw: undefined },
usage: {
inputTokens: {
total: 10,
noCache: 10,
cacheRead: undefined,
cacheWrite: undefined,
},
outputTokens: {
total: 5,
text: 5,
reasoning: undefined,
},
},
response: {
id: 'test-id-1-from-model',
timestamp: new Date(0),
modelId: 'test-response-model-id',
},
};
case 1:
return {
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
response: {
id: 'test-id-2-from-model',
timestamp: new Date(10000),
modelId: 'test-response-model-id',
headers: {
'custom-response-header': 'response-header-value',
},
},
};
default:
throw new Error(
`Unexpected response count: ${responseCount}`,
);
}
},
}),
tools: {
tool1: tool({
title: 'Tool One',
inputSchema: z.object({ value: z.string() }),
execute: async (args, options) => {
expect(args).toStrictEqual({ value: 'value' });
expect(options.messages).toStrictEqual([
{ role: 'user', content: 'test-input' },
]);
return 'result1';
},
}),
},
prompt: 'test-input',
onFinish: async event => {
onFinishResult = event as unknown as typeof onFinishResult;
},
onStepFinish: async event => {
onStepFinishResults.push(event);
},
stopWhen: stepCountIs(3),
});
});
it('result.text should return text from last step', async () => {
assert.deepStrictEqual(result.text, 'Hello, world!');
});
it('result.toolCalls should return empty tool calls from last step', async () => {
assert.deepStrictEqual(result.toolCalls, []);
});
it('result.toolResults should return empty tool results from last step', async () => {
assert.deepStrictEqual(result.toolResults, []);
});
it('result.response.messages should contain response messages from all steps', () => {
expect(result.response.messages).toMatchSnapshot();
});
it('result.totalUsage should sum token usage', () => {
expect(result.totalUsage).toMatchInlineSnapshot(`
{
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 13,
},
"inputTokens": 13,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 15,
},
"outputTokens": 15,
"reasoningTokens": undefined,
"totalTokens": 28,
}
`);
});
it('result.usage should contain token usage from final step', async () => {
expect(result.usage).toMatchInlineSnapshot(`
{
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 3,
},
"inputTokens": 3,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 10,
},
"outputTokens": 10,
"raw": undefined,
"reasoningTokens": undefined,
"totalTokens": 13,
}
`);
});
it('result.steps should contain all steps', () => {
expect(result.steps).toMatchSnapshot();
});
describe('callbacks', () => {
it('onFinish should send correct information', async () => {
expect(onFinishResult).toMatchSnapshot();
});
it('onStepFinish should be called for each step', () => {
expect(onStepFinishResults).toMatchSnapshot();
});
});
});
describe('2 steps: initial, tool-result with prepareStep', () => {
let result: GenerateTextResult<any, any>;
let onStepFinishResults: StepResult<any>[];
let doGenerateCalls: Array<LanguageModelV3CallOptions>;
let prepareStepCalls: Array<{
modelId: string;
stepNumber: number;
steps: Array<StepResult<any>>;
messages: Array<ModelMessage>;
experimental_context: unknown;
}>;
beforeEach(async () => {
onStepFinishResults = [];
doGenerateCalls = [];
prepareStepCalls = [];
let responseCount = 0;
const trueModel = new MockLanguageModelV3({
doGenerate: async ({ prompt, tools, toolChoice }) => {
doGenerateCalls.push({ prompt, tools, toolChoice });
switch (responseCount++) {
case 0:
return {
...dummyResponseValues,
content: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'tool1',
input: `{ "value": "value" }`,
},
],
finishReason: { unified: 'tool-calls', raw: undefined },
usage: {
inputTokens: {
total: 10,
noCache: 10,
cacheRead: undefined,
cacheWrite: undefined,
},
outputTokens: {
total: 5,
text: 5,
reasoning: undefined,
},
},
response: {
id: 'test-id-1-from-model',
timestamp: new Date(0),
modelId: 'test-response-model-id',
},
};
case 1:
return {
...dummyResponseValues,
content: [{ type: 'text', text: 'Hello, world!' }],
response: {
id: 'test-id-2-from-model',
timestamp: new Date(10000),
modelId: 'test-response-model-id',
headers: {
'custom-response-header': 'response-header-value',
},
},
};
default:
throw new Error(`Unexpected response count: ${responseCount}`);
}
},
});
result = await generateText({
model: modelWithFiles,
tools: {
tool1: tool({
inputSchema: z.object({ value: z.string() }),
execute: async (args, options) => {
expect(args).toStrictEqual({ value: 'value' });
expect(options.messages).toStrictEqual([
{ role: 'user', content: 'test-input' },
]);
return 'result1';
},
}),
},
experimental_context: { context: 'state1' },
prompt: 'test-input',
stopWhen: stepCountIs(3),
onStepFinish: async event => {
onStepFinishResults.push(event);
},
prepareStep: async ({
model,
stepNumber,
steps,
messages,
experimental_context,
}) => {
prepareStepCalls.push({
modelId: typeof model === 'string' ? model : model.modelId,
stepNumber,
steps,
messages,
experimental_context,
});
if (stepNumber === 0) {
expect(steps).toStrictEqual([]);
return {
model: trueModel,
toolChoice: {
type: 'tool',
toolName: 'tool1' as const,
},
system: 'system-message-0',
messages: [
{
role: 'user',
content: 'new input from prepareStep',
},
],
experimental_context: { context: 'state2' },
};
}
if (stepNumber === 1) {
expect(steps.length).toStrictEqual(1);
return {
model: trueModel,
activeTools: [],
system: 'system-message-1',
experimental_context: { context: 'state3' },
};
}
},
});
});
it('should contain all prepareStep calls', async () => {
expect(prepareStepCalls).toMatchInlineSnapshot(`
[
{
"experimental_context": {
"context": "state1",
},
"messages": [
{
"content": "test-input",
"role": "user",
},
],
"modelId": "mock-model-id",
"stepNumber": 0,
"steps": [
DefaultStepResult {
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "result1",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"finishReason": "tool-calls",
"providerMetadata": undefined,
"rawFinishReason": undefined,
"request": {},
"response": {
"body": undefined,
"headers": undefined,
"id": "test-id-1-from-model",
"messages": [
{
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result1",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
],
"modelId": "test-response-model-id",
"timestamp": 1970-01-01T00:00:00.000Z,
},
"usage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 10,
},
"inputTokens": 10,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 5,
},
"outputTokens": 5,
"raw": undefined,
"reasoningTokens": undefined,
"totalTokens": 15,
},
"warnings": [],
},
DefaultStepResult {
"content": [
{
"text": "Hello, world!",
"type": "text",
},
],
"finishReason": "stop",
"providerMetadata": undefined,
"rawFinishReason": "stop",
"request": {},
"response": {
"body": undefined,
"headers": {
"custom-response-header": "response-header-value",
},
"id": "test-id-2-from-model",
"messages": [
{
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result1",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
{
"content": [
{
"providerOptions": undefined,
"text": "Hello, world!",
"type": "text",
},
],
"role": "assistant",
},
],
"modelId": "test-response-model-id",
"timestamp": 1970-01-01T00:00:10.000Z,
},
"usage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 3,
},
"inputTokens": 3,
"outputTokenDetails": {
"reasoningTokens": undefined,
"textTokens": 10,
},
"outputTokens": 10,
"raw": undefined,
"reasoningTokens": undefined,
"totalTokens": 13,
},
"warnings": [],
},
],
},
{
"experimental_context": {
"context": "state2",
},
"messages": [
{
"content": "test-input",
"role": "user",
},
{
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result1",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
],
"modelId": "mock-model-id",
"stepNumber": 1,
"steps": [
DefaultStepResult {
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerMetadata": undefined,
"title": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
{
"dynamic": false,
"input": {
"value": "value",
},
"output": "result1",
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"finishReason": "tool-calls",
"providerMetadata": undefined,
"rawFinishReason": undefined,
"request": {},
"response": {
"body": undefined,
"headers": undefined,
"id": "test-id-1-from-model",
"messages": [
{
"content": [
{
"input": {
"value": "value",
},
"providerExecuted": undefined,
"providerOptions": undefined,
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result1",
},
"toolCallId": "call-1",
"toolName": "tool1",
"type": "tool-result",
},
],
"role": "tool",
},
],
"modelId": "test-response-model-id",
"timestamp": 1970-01-01T00:00:00.000Z,
},
"usage": {
"cachedInputTokens": undefined,
"inputTokenDetails": {
"cacheReadTokens": undefined,
"cacheWriteTokens": undefined,
"noCacheTokens": 10,
},