UNPKG

ai

Version:

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

1,708 lines (1,657 loc) • 640 kB
import { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3FunctionTool, LanguageModelV3Prompt, LanguageModelV3ProviderTool, LanguageModelV3StreamPart, LanguageModelV3Usage, SharedV3ProviderMetadata, SharedV3Warning, } from '@ai-sdk/provider'; import { delay, DelayedPromise, dynamicTool, jsonSchema, ModelMessage, tool, Tool, ToolExecuteFunction, } from '@ai-sdk/provider-utils'; import { convertArrayToReadableStream, convertAsyncIterableToArray, convertReadableStreamToArray, convertResponseStreamToArray, mockId, } from '@ai-sdk/provider-utils/test'; import assert from 'node:assert'; import { afterEach, 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 { createMockServerResponse } from '../test/mock-server-response'; import { MockTracer } from '../test/mock-tracer'; import { mockValues } from '../test/mock-values'; import { asLanguageModelUsage, createNullLanguageModelUsage, } from '../types/usage'; import { StepResult } from './step-result'; import { stepCountIs } from './stop-condition'; import { streamText, StreamTextOnFinishCallback } from './stream-text'; import { StreamTextResult, TextStreamPart } from './stream-text-result'; import { ToolSet } from './tool-set'; const defaultSettings = () => ({ prompt: 'prompt', experimental_generateMessageId: mockId({ prefix: 'msg' }), _internal: { generateId: mockId({ prefix: 'id' }), }, onError: () => {}, }) as const; const testUsage: LanguageModelV3Usage = { inputTokens: { total: 3, noCache: 3, cacheRead: undefined, cacheWrite: undefined, }, outputTokens: { total: 10, text: 10, reasoning: undefined, }, }; const testUsage2: LanguageModelV3Usage = { inputTokens: { total: 3, noCache: 3, cacheRead: 0, cacheWrite: 0, }, outputTokens: { total: 10, text: 10, reasoning: 10, }, }; function createTestModel({ warnings = [], stream = convertArrayToReadableStream([ { type: 'stream-start', warnings, }, { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, providerMetadata: { testProvider: { testKey: 'testValue' }, }, }, ]), request = undefined, response = undefined, }: { stream?: ReadableStream<LanguageModelV3StreamPart>; request?: { body: string }; response?: { headers: Record<string, string> }; warnings?: SharedV3Warning[]; } = {}): LanguageModelV3 { return new MockLanguageModelV3({ doStream: async () => ({ stream, request, response, warnings }), }); } const modelWithSources = new MockLanguageModelV3({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'source', sourceType: 'url', id: '123', url: 'https://example.com', title: 'Example', providerMetadata: { provider: { custom: 'value' } }, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello!' }, { type: 'text-end', id: '1' }, { type: 'source', sourceType: 'url', id: '456', url: 'https://example.com/2', title: 'Example 2', providerMetadata: { provider: { custom: 'value2' } }, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), }); const modelWithDocumentSources = new MockLanguageModelV3({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'source', sourceType: 'document', id: 'doc-123', mediaType: 'application/pdf', title: 'Document Example', filename: 'example.pdf', providerMetadata: { provider: { custom: 'doc-value' } }, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello from document!' }, { type: 'text-end', id: '1' }, { type: 'source', sourceType: 'document', id: 'doc-456', mediaType: 'text/plain', title: 'Text Document', providerMetadata: { provider: { custom: 'doc-value2' } }, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), }); const modelWithFiles = new MockLanguageModelV3({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'file', data: 'Hello World', mediaType: 'text/plain', }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello!' }, { type: 'text-end', id: '1' }, { type: 'file', data: 'QkFVRw==', mediaType: 'image/jpeg', }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), }); const modelWithReasoning = new MockLanguageModelV3({ doStream: async () => ({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'reasoning-start', id: '1' }, { type: 'reasoning-delta', id: '1', delta: 'I will open the conversation', }, { type: 'reasoning-delta', id: '1', delta: ' with witty banter.', }, { type: 'reasoning-delta', id: '1', delta: '', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV3ProviderMetadata, }, { type: 'reasoning-end', id: '1' }, { type: 'reasoning-start', id: '2', providerMetadata: { testProvider: { redactedData: 'redacted-reasoning-data' }, }, }, { type: 'reasoning-end', id: '2' }, { type: 'reasoning-start', id: '3' }, { type: 'reasoning-delta', id: '3', delta: ' Once the user has relaxed,', }, { type: 'reasoning-delta', id: '3', delta: ' I will pry for valuable information.', }, { type: 'reasoning-end', id: '3', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV3ProviderMetadata, }, { type: 'reasoning-start', id: '4', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV3ProviderMetadata, }, { type: 'reasoning-delta', id: '4', delta: ' I need to think about', }, { type: 'reasoning-delta', id: '4', delta: ' this problem carefully.', }, { type: 'reasoning-start', id: '5', providerMetadata: { testProvider: { signature: '1234567890' }, } as SharedV3ProviderMetadata, }, { type: 'reasoning-delta', id: '5', delta: ' The best solution', }, { type: 'reasoning-delta', id: '5', delta: ' requires careful', }, { type: 'reasoning-delta', id: '5', delta: ' consideration of all factors.', }, { type: 'reasoning-end', id: '4', providerMetadata: { testProvider: { signature: '0987654321' }, } as SharedV3ProviderMetadata, }, { type: 'reasoning-end', id: '5', providerMetadata: { testProvider: { signature: '0987654321' }, } as SharedV3ProviderMetadata, }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hi' }, { type: 'text-delta', id: '1', delta: ' there!' }, { type: 'text-end', id: '1', providerMetadata: { testProvider: { signature: '0987654321' }, } as SharedV3ProviderMetadata, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), }); describe('streamText', () => { let logWarningsSpy: ReturnType<typeof vitest.spyOn>; beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }); vi.setSystemTime(new Date(0)); logWarningsSpy = vitest .spyOn(logWarningsModule, 'logWarnings') .mockImplementation(() => {}); }); afterEach(() => { vi.useRealTimers(); logWarningsSpy.mockRestore(); }); describe('result.textStream', () => { it('should send text deltas', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }; }, }), prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toStrictEqual(['Hello', ', ', 'world!']); }); it('should filter out empty text deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); it('should not include reasoning content in textStream', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); }); describe('result.fullStream', () => { it('should send text deltas', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'response-id', modelId: 'response-model-id', timestamp: new Date(5000), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }; }, }), prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "response-id", "modelId": "response-model-id", "timestamp": 1970-01-01T00:00:05.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send reasoning deltas', async () => { const result = streamText({ model: modelWithReasoning, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "reasoning-start", }, { "id": "1", "providerMetadata": undefined, "text": "I will open the conversation", "type": "reasoning-delta", }, { "id": "1", "providerMetadata": undefined, "text": " with witty banter.", "type": "reasoning-delta", }, { "id": "1", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "text": "", "type": "reasoning-delta", }, { "id": "1", "type": "reasoning-end", }, { "id": "2", "providerMetadata": { "testProvider": { "redactedData": "redacted-reasoning-data", }, }, "type": "reasoning-start", }, { "id": "2", "type": "reasoning-end", }, { "id": "3", "type": "reasoning-start", }, { "id": "3", "providerMetadata": undefined, "text": " Once the user has relaxed,", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": undefined, "text": " I will pry for valuable information.", "type": "reasoning-delta", }, { "id": "3", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-end", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "id": "4", "providerMetadata": undefined, "text": " I need to think about", "type": "reasoning-delta", }, { "id": "4", "providerMetadata": undefined, "text": " this problem carefully.", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "1234567890", }, }, "type": "reasoning-start", }, { "id": "5", "providerMetadata": undefined, "text": " The best solution", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": undefined, "text": " requires careful", "type": "reasoning-delta", }, { "id": "5", "providerMetadata": undefined, "text": " consideration of all factors.", "type": "reasoning-delta", }, { "id": "4", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "5", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "reasoning-end", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hi", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": " there!", "type": "text-delta", }, { "id": "1", "providerMetadata": { "testProvider": { "signature": "0987654321", }, }, "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send sources', async () => { const result = streamText({ model: modelWithSources, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "123", "providerMetadata": { "provider": { "custom": "value", }, }, "sourceType": "url", "title": "Example", "type": "source", "url": "https://example.com", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "id": "456", "providerMetadata": { "provider": { "custom": "value2", }, }, "sourceType": "url", "title": "Example 2", "type": "source", "url": "https://example.com/2", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send files', async () => { const result = streamText({ model: modelWithFiles, ...defaultSettings(), }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "file": DefaultGeneratedFileWithType { "base64Data": "Hello World", "mediaType": "text/plain", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "file": DefaultGeneratedFileWithType { "base64Data": "QkFVRw==", "mediaType": "image/jpeg", "type": "file", "uint8ArrayData": undefined, }, "type": "file", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should use fallback response metadata when response metadata is not provided', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: async ({ prompt }) => { expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: `world!` }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }; }, }), prompt: 'test-input', _internal: { generateId: mockValues('id-2000'), }, }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "id-2000", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should send tool calls', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: 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: 'required' }); expect(prompt).toStrictEqual([ { role: 'user', content: [{ type: 'text', text: 'test-input' }], providerOptions: undefined, }, ]); return { stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, providerMetadata: { testProvider: { signature: 'sig', }, }, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }; }, }), tools: { tool1: tool({ title: 'Tool 1', inputSchema: z.object({ value: z.string() }), }), }, toolChoice: 'required', prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should send tool call deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-input-start', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '{"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'value', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '":"', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'Spark', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: 'le', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: ' Day', }, { type: 'tool-input-delta', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', delta: '"}', }, { type: 'tool-input-end', id: 'call_O17Uplv4lJvD6DVdIvFFeRMw', }, { type: 'tool-call', toolCallId: 'call_O17Uplv4lJvD6DVdIvFFeRMw', toolName: 'test-tool', input: '{"value":"Sparkle Day"}', }, { type: 'finish', finishReason: { unified: 'tool-calls', raw: undefined }, usage: testUsage2, }, ]), }), tools: { 'test-tool': tool({ inputSchema: z.object({ value: z.string() }), }), }, toolChoice: 'required', prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "dynamic": false, "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "title": undefined, "toolName": "test-tool", "type": "tool-input-start", }, { "delta": "{"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "value", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "":"", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "Spark", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": "le", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": " Day", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "delta": ""}", "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-delta", }, { "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", "type": "tool-input-end", }, { "input": { "value": "Sparkle Day", }, "providerExecuted": undefined, "providerMetadata": undefined, "title": undefined, "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", "toolName": "test-tool", "type": "tool-call", }, { "finishReason": "tool-calls", "providerMetadata": undefined, "rawFinishReason": undefined, "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "usage": { "cachedInputTokens": 0, "inputTokenDetails": { "cacheReadTokens": 0, "cacheWriteTokens": 0, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": 10, "textTokens": 10, }, "outputTokens": 10, "raw": undefined, "reasoningTokens": 10, "totalTokens": 13, }, }, { "finishReason": "tool-calls", "rawFinishReason": undefined, "totalUsage": { "cachedInputTokens": 0, "inputTokenDetails": { "cacheReadTokens": 0, "cacheWriteTokens": 0, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": 10, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": 10, "totalTokens": 13, }, "type": "finish", }, ] `); }); it('should pass through providerMetadata on tool-input-start', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-input-start', id: 'call-1', toolName: 'test-tool', providerMetadata: { testProvider: { someKey: 'someValue' }, }, }, { type: 'tool-input-delta', id: 'call-1', delta: '{"value":"test"}', }, { type: 'tool-input-end', id: 'call-1', }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'test-tool', input: '{"value":"test"}', }, { type: 'finish', finishReason: { unified: 'tool-calls', raw: undefined }, usage: testUsage2, }, ]), }), tools: { 'test-tool': tool({ inputSchema: z.object({ value: z.string() }), }), }, toolChoice: 'required', prompt: 'test-input', }); const chunks = await convertAsyncIterableToArray(result.fullStream); const toolInputStart = chunks.find( (c): c is Extract<typeof c, { type: 'tool-input-start' }> => c.type === 'tool-input-start', ); expect(toolInputStart?.providerMetadata).toEqual({ testProvider: { someKey: 'someValue' }, }); }); it('should send tool results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), tools: { tool1: tool({ title: 'Tool 1', inputSchema: z.object({ value: z.string() }), execute: async (input, options) => { expect(input).toStrictEqual({ value: 'value' }); expect(options.messages).toStrictEqual([ { role: 'user', content: 'test-input' }, ]); return `${input.value}-result`; }, }), }, prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should send delayed asynchronous tool results', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'tool-call', toolCallId: 'call-1', toolName: 'tool1', input: `{ "value": "value" }`, }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), tools: { tool1: { title: 'Tool 1', inputSchema: z.object({ value: z.string() }), execute: async ({ value }) => { await delay(50); // delay to show bug where step finish is sent before tool result return `${value}-result`; }, }, }, prompt: 'test-input', }); expect( await convertAsyncIterableToArray(result.fullStream), ).toMatchSnapshot(); }); it('should filter out empty text deltas', async () => { const result = streamText({ model: createTestModel({ stream: convertArrayToReadableStream([ { type: 'response-metadata', id: 'id-0', modelId: 'mock-model-id', timestamp: new Date(0), }, { type: 'text-start', id: '1' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'Hello' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: ', ' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-delta', id: '1', delta: 'world!' }, { type: 'text-delta', id: '1', delta: '' }, { type: 'text-end', id: '1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage: testUsage, }, ]), }), prompt: 'test-input', }); expect(await convertAsyncIterableToArray(result.fullStream)) .toMatchInlineSnapshot(` [ { "type": "start", }, { "request": {}, "type": "start-step", "warnings": [], }, { "id": "1", "type": "text-start", }, { "id": "1", "providerMetadata": undefined, "text": "Hello", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": ", ", "type": "text-delta", }, { "id": "1", "providerMetadata": undefined, "text": "world!", "type": "text-delta", }, { "id": "1", "type": "text-end", }, { "finishReason": "stop", "providerMetadata": undefined, "rawFinishReason": "stop", "response": { "headers": undefined, "id": "id-0", "modelId": "mock-model-id", "timestamp": 1970-01-01T00:00:00.000Z, }, "type": "finish-step", "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, }, }, { "finishReason": "stop", "rawFinishReason": "stop", "totalUsage": { "cachedInputTokens": undefined, "inputTokenDetails": { "cacheReadTokens": undefined, "cacheWriteTokens": undefined, "noCacheTokens": 3, }, "inputTokens": 3, "outputTokenDetails": { "reasoningTokens": undefined, "textTokens": 10, }, "outputTokens": 10, "reasoningTokens": undefined, "totalTokens": 13, }, "type": "finish", }, ] `); }); }); describe('errors', () => { it('should swallow error to prevent server crash', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError: () => {}, }); expect( await convertAsyncIterableToArray(result.textStream), ).toMatchSnapshot(); }); it('should forward error in doStream as error stream part', async () => { const result = streamText({ model: new MockLanguageModelV3({ doStream: async () => { throw new Error('test error'); }, }), prompt: 'test-input', onError: () => {}, }); expect( await convertAsyncIterableToArray(result.fullStream), ).toStrictEqual([ { type: 'start',