UNPKG

ai

Version:

AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript

1,614 lines (1,537 loc) • 225 kB
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, },