ai
Version:
AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript
1,859 lines (1,784 loc) • 74.3 kB
text/typescript
import { ModelMessage } from '@ai-sdk/provider-utils';
import { describe, expect, it } from 'vitest';
import { convertToModelMessages } from './convert-to-model-messages';
import { UIMessage } from './ui-messages';
describe('convertToModelMessages', () => {
describe('system message', () => {
it('should convert a simple system message', async () => {
const result = await convertToModelMessages([
{
role: 'system',
parts: [{ text: 'System message', type: 'text' }],
},
]);
expect(result).toEqual([{ role: 'system', content: 'System message' }]);
});
it('should convert a system message with provider metadata', async () => {
const result = await convertToModelMessages([
{
role: 'system',
parts: [
{
text: 'System message with metadata',
type: 'text',
providerMetadata: { testProvider: { systemSignature: 'abc123' } },
},
],
},
]);
expect(result).toEqual([
{
role: 'system',
content: 'System message with metadata',
providerOptions: { testProvider: { systemSignature: 'abc123' } },
},
]);
});
it('should merge provider metadata from multiple text parts in system message', async () => {
const result = await convertToModelMessages([
{
role: 'system',
parts: [
{
text: 'Part 1',
type: 'text',
providerMetadata: { provider1: { key1: 'value1' } },
},
{
text: ' Part 2',
type: 'text',
providerMetadata: { provider2: { key2: 'value2' } },
},
],
},
]);
expect(result).toEqual([
{
role: 'system',
content: 'Part 1 Part 2',
providerOptions: {
provider1: { key1: 'value1' },
provider2: { key2: 'value2' },
},
},
]);
});
it('should convert a system message with Anthropic cache control metadata', async () => {
const SYSTEM_PROMPT = 'You are a helpful assistant.';
const systemMessage = {
id: 'system',
role: 'system' as const,
parts: [
{
type: 'text' as const,
text: SYSTEM_PROMPT,
providerMetadata: {
anthropic: {
cacheControl: { type: 'ephemeral' },
},
},
},
],
};
const result = await convertToModelMessages([systemMessage]);
expect(result).toEqual([
{
role: 'system',
content: SYSTEM_PROMPT,
providerOptions: {
anthropic: {
cacheControl: { type: 'ephemeral' },
},
},
},
]);
});
});
describe('user message', () => {
it('should convert a simple user message', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [{ text: 'Hello, AI!', type: 'text' }],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Hello, AI!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
it('should convert a simple user message with provider metadata', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [
{
text: 'Hello, AI!',
type: 'text',
providerMetadata: { testProvider: { signature: '1234567890' } },
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"providerOptions": {
"testProvider": {
"signature": "1234567890",
},
},
"text": "Hello, AI!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
it('should handle user message file parts', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [
{
type: 'file',
mediaType: 'image/jpeg',
url: 'https://example.com/image.jpg',
},
{ type: 'text', text: 'Check this image' },
],
},
]);
expect(result).toEqual([
{
role: 'user',
content: [
{
type: 'file',
mediaType: 'image/jpeg',
data: 'https://example.com/image.jpg',
},
{ type: 'text', text: 'Check this image' },
],
},
]);
});
it('should handle user message file parts with provider metadata', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [
{
type: 'file',
mediaType: 'image/jpeg',
url: 'https://example.com/image.jpg',
providerMetadata: { testProvider: { signature: '1234567890' } },
},
{ type: 'text', text: 'Check this image' },
],
},
]);
expect(result).toEqual([
{
role: 'user',
content: [
{
type: 'file',
mediaType: 'image/jpeg',
data: 'https://example.com/image.jpg',
providerOptions: { testProvider: { signature: '1234567890' } },
},
{ type: 'text', text: 'Check this image' },
],
},
]);
});
it('should include filename for user file parts when provided', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [
{
type: 'file',
mediaType: 'image/jpeg',
url: 'https://example.com/image.jpg',
filename: 'image.jpg',
},
],
},
]);
expect(result).toEqual([
{
role: 'user',
content: [
{
type: 'file',
mediaType: 'image/jpeg',
data: 'https://example.com/image.jpg',
filename: 'image.jpg',
},
],
},
]);
});
});
it('should not include filename for user file parts when not provided', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [
{
type: 'file',
mediaType: 'image/jpeg',
url: 'https://example.com/image.jpg',
},
],
},
]);
expect(result).toEqual([
{
role: 'user',
content: [
{
type: 'file',
mediaType: 'image/jpeg',
data: 'https://example.com/image.jpg',
},
],
},
]);
});
describe('assistant message', () => {
it('should convert a simple assistant text message', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [{ type: 'text', text: 'Hello, human!', state: 'done' }],
},
]);
expect(result).toEqual([
{
role: 'assistant',
content: [{ type: 'text', text: 'Hello, human!' }],
},
]);
});
it('should convert a simple assistant text message with provider metadata', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{
type: 'text',
text: 'Hello, human!',
state: 'done',
providerMetadata: { testProvider: { signature: '1234567890' } },
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"providerOptions": {
"testProvider": {
"signature": "1234567890",
},
},
"text": "Hello, human!",
"type": "text",
},
],
"role": "assistant",
},
]
`);
});
it('should convert an assistant message with reasoning', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{
type: 'reasoning',
text: 'Thinking...',
providerMetadata: {
testProvider: {
signature: '1234567890',
},
},
state: 'done',
},
{
type: 'reasoning',
text: 'redacted-data',
providerMetadata: {
testProvider: { isRedacted: true },
},
state: 'done',
},
{ type: 'text', text: 'Hello, human!', state: 'done' },
],
},
]);
expect(result).toEqual([
{
role: 'assistant',
content: [
{
type: 'reasoning',
text: 'Thinking...',
providerOptions: { testProvider: { signature: '1234567890' } },
},
{
type: 'reasoning',
text: 'redacted-data',
providerOptions: { testProvider: { isRedacted: true } },
},
{ type: 'text', text: 'Hello, human!' },
],
},
] satisfies ModelMessage[]);
});
it('should convert an assistant message with file parts', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{
type: 'file',
mediaType: 'image/png',
url: 'data:image/png;base64,dGVzdA==',
},
],
},
]);
expect(result).toEqual([
{
role: 'assistant',
content: [
{
type: 'file',
mediaType: 'image/png',
data: 'data:image/png;base64,dGVzdA==',
},
],
},
] satisfies ModelMessage[]);
});
it('should include filename for assistant file parts when provided', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{
type: 'file',
mediaType: 'image/png',
url: 'data:image/png;base64,dGVzdA==',
filename: 'test.png',
},
],
},
]);
expect(result).toEqual([
{
role: 'assistant',
content: [
{
type: 'file',
mediaType: 'image/png',
data: 'data:image/png;base64,dGVzdA==',
filename: 'test.png',
},
],
},
] as unknown as ModelMessage[]);
});
it('should handle assistant message with tool output available', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-calculator',
state: 'output-available',
toolCallId: 'call1',
input: { operation: 'add', numbers: [1, 2] },
output: '3',
callProviderMetadata: {
testProvider: {
signature: '1234567890',
},
},
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Let me calculate that for you.",
"type": "text",
},
{
"input": {
"numbers": [
1,
2,
],
"operation": "add",
},
"providerExecuted": undefined,
"providerOptions": {
"testProvider": {
"signature": "1234567890",
},
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "3",
},
"providerOptions": {
"testProvider": {
"signature": "1234567890",
},
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "tool",
},
]
`);
});
describe('tool output error', () => {
it('should handle assistant message with tool output error that has raw input', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-calculator',
state: 'output-error',
toolCallId: 'call1',
errorText: 'Error: Invalid input',
input: undefined,
rawInput: { operation: 'add', numbers: [1, 2] },
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Let me calculate that for you.",
"type": "text",
},
{
"input": {
"numbers": [
1,
2,
],
"operation": "add",
},
"providerExecuted": undefined,
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "error-text",
"value": "Error: Invalid input",
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "tool",
},
]
`);
});
it('should handle assistant message with tool output error that has no raw input', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-calculator',
state: 'output-error',
toolCallId: 'call1',
input: { operation: 'add', numbers: [1, 2] },
errorText: 'Error: Invalid input',
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Let me calculate that for you.",
"type": "text",
},
{
"input": {
"numbers": [
1,
2,
],
"operation": "add",
},
"providerExecuted": undefined,
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "error-text",
"value": "Error: Invalid input",
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "tool",
},
]
`);
});
});
it('should handle assistant message with provider-executed tool output available', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-calculator',
state: 'output-available',
toolCallId: 'call1',
input: { operation: 'add', numbers: [1, 2] },
output: '3',
providerExecuted: true,
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Let me calculate that for you.",
"type": "text",
},
{
"input": {
"numbers": [
1,
2,
],
"operation": "add",
},
"providerExecuted": true,
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
{
"output": {
"type": "text",
"value": "3",
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "assistant",
},
]
`);
});
it('should handle assistant message with provider-executed tool output error', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-calculator',
state: 'output-error',
toolCallId: 'call1',
input: { operation: 'add', numbers: [1, 2] },
errorText: 'Error: Invalid input',
providerExecuted: true,
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "Let me calculate that for you.",
"type": "text",
},
{
"input": {
"numbers": [
1,
2,
],
"operation": "add",
},
"providerExecuted": true,
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
{
"output": {
"type": "error-json",
"value": "Error: Invalid input",
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "assistant",
},
]
`);
});
it('should propagate provider metadata to provider-executed tool-result', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'tool-calculator',
state: 'output-available',
toolCallId: 'call1',
input: { operation: 'multiply', numbers: [3, 4] },
output: '12',
providerExecuted: true,
callProviderMetadata: {
testProvider: {
executionTime: 75,
},
},
},
],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"input": {
"numbers": [
3,
4,
],
"operation": "multiply",
},
"providerExecuted": true,
"providerOptions": {
"testProvider": {
"executionTime": 75,
},
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-call",
},
{
"output": {
"type": "text",
"value": "12",
},
"providerOptions": {
"testProvider": {
"executionTime": 75,
},
},
"toolCallId": "call1",
"toolName": "calculator",
"type": "tool-result",
},
],
"role": "assistant",
},
]
`);
});
it('should handle assistant message with tool invocations that have multi-part responses', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'text',
text: 'Let me calculate that for you.',
state: 'done',
},
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call1',
input: {},
output: 'imgbase64',
},
],
},
]);
expect(result).toMatchSnapshot();
});
it('should handle conversation with an assistant message that has empty tool invocations', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [{ type: 'text', text: 'text1' }],
},
{
role: 'assistant',
parts: [{ type: 'text', text: 'text2', state: 'done' }],
},
]);
expect(result).toMatchSnapshot();
});
it('should handle conversation with multiple tool invocations that have step information', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{ type: 'text', text: 'response', state: 'done' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-2',
input: { value: 'value-2' },
output: 'result-2',
},
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-3',
input: { value: 'value-3' },
output: 'result-3',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-4',
input: { value: 'value-4' },
output: 'result-4',
},
],
},
]);
expect(result).toMatchSnapshot();
});
it('should handle conversation with mix of tool invocations and text', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{ type: 'text', text: 'i am gonna use tool1', state: 'done' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
},
{ type: 'step-start' },
{
type: 'text',
text: 'i am gonna use tool2 and tool3',
state: 'done',
},
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-2',
input: { value: 'value-2' },
output: 'result-2',
},
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-3',
input: { value: 'value-3' },
output: 'result-3',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-4',
input: { value: 'value-4' },
output: 'result-4',
},
{ type: 'step-start' },
{ type: 'text', text: 'final response', state: 'done' },
],
},
]);
expect(result).toMatchSnapshot();
});
});
describe('multiple messages', () => {
it('should handle a conversation with multiple messages', async () => {
const result = await convertToModelMessages([
{
role: 'user',
parts: [{ type: 'text', text: "What's the weather like?" }],
},
{
role: 'assistant',
parts: [
{ type: 'text', text: "I'll check that for you.", state: 'done' },
],
},
{
role: 'user',
parts: [{ type: 'text', text: 'Thanks!' }],
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What's the weather like?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"text": "I'll check that for you.",
"type": "text",
},
],
"role": "assistant",
},
{
"content": [
{
"text": "Thanks!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
it('should handle conversation with multiple tool invocations and user message at the end', async () => {
const result = await convertToModelMessages([
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-2',
input: { value: 'value-2' },
output: 'result-2',
},
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-3',
input: { value: 'value-3' },
output: 'result-3',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-4',
input: { value: 'value-4' },
output: 'result-4',
},
{ type: 'step-start' },
{ type: 'text', text: 'response', state: 'done' },
],
},
{
role: 'user',
parts: [{ type: 'text', text: 'Thanks!' }],
},
]);
expect(result).toMatchSnapshot();
});
});
describe('error handling', () => {
it('should throw an error for unhandled roles', async () => {
expect(async () => {
await convertToModelMessages([
{
role: 'unknown' as any,
parts: [{ text: 'unknown role message', type: 'text' }],
},
]);
}).rejects.toThrow('Unsupported role: unknown');
});
});
describe('when ignoring incomplete tool calls', () => {
it('should handle conversation with multiple tool invocations and user message at the end', async () => {
const result = await convertToModelMessages(
[
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
},
{ type: 'step-start' },
{
type: 'tool-screenshot',
state: 'input-streaming',
toolCallId: 'call-2',
input: { value: 'value-2' },
},
{
type: 'tool-screenshot',
state: 'input-available',
toolCallId: 'call-3',
input: { value: 'value-3' },
},
{
type: 'dynamic-tool',
toolName: 'tool-screenshot2',
state: 'input-available',
toolCallId: 'call-3',
input: { value: 'value-3' },
},
{ type: 'text', text: 'response', state: 'done' },
],
},
{
role: 'user',
parts: [{ type: 'text', text: 'Thanks!' }],
},
],
{ ignoreIncompleteToolCalls: true },
);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"input": {
"value": "value-1",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result-1",
},
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-result",
},
],
"role": "tool",
},
{
"content": [
{
"text": "response",
"type": "text",
},
],
"role": "assistant",
},
{
"content": [
{
"text": "Thanks!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
});
describe('when converting dynamic tool invocations', () => {
it('should convert a dynamic tool invocation', async () => {
const result = await convertToModelMessages(
[
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'dynamic-tool',
toolName: 'screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
},
],
},
{
role: 'user',
parts: [{ type: 'text', text: 'Thanks!' }],
},
],
{ ignoreIncompleteToolCalls: true },
);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"input": {
"value": "value-1",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-call",
},
],
"role": "assistant",
},
{
"content": [
{
"output": {
"type": "text",
"value": "result-1",
},
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-result",
},
],
"role": "tool",
},
{
"content": [
{
"text": "Thanks!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
});
describe('when converting provider-executed dynamic tool invocations', () => {
it('should convert a provider-executed dynamic tool invocation', async () => {
const result = await convertToModelMessages(
[
{
role: 'assistant',
parts: [
{ type: 'step-start' },
{
type: 'dynamic-tool',
toolName: 'screenshot',
state: 'output-available',
toolCallId: 'call-1',
input: { value: 'value-1' },
output: 'result-1',
providerExecuted: true,
callProviderMetadata: {
'test-provider': {
'key-a': 'test-value-1',
'key-b': 'test-value-2',
},
},
},
],
},
{
role: 'user',
parts: [{ type: 'text', text: 'Thanks!' }],
},
],
{ ignoreIncompleteToolCalls: true },
);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"input": {
"value": "value-1",
},
"providerExecuted": true,
"providerOptions": {
"test-provider": {
"key-a": "test-value-1",
"key-b": "test-value-2",
},
},
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-call",
},
{
"output": {
"type": "text",
"value": "result-1",
},
"providerOptions": {
"test-provider": {
"key-a": "test-value-1",
"key-b": "test-value-2",
},
},
"toolCallId": "call-1",
"toolName": "screenshot",
"type": "tool-result",
},
],
"role": "assistant",
},
{
"content": [
{
"text": "Thanks!",
"type": "text",
},
],
"role": "user",
},
]
`);
});
});
describe('when converting tool approval request responses', () => {
it('should convert an approved tool approval request (static tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: true,
id: 'approval-1',
reason: undefined,
},
input: {
city: 'Tokyo',
},
state: 'approval-responded',
toolCallId: 'call-1',
type: 'tool-weather',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": true,
"providerExecuted": undefined,
"reason": undefined,
"type": "tool-approval-response",
},
],
"role": "tool",
},
]
`);
});
it('should convert an approved tool approval request (dynamic tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: true,
id: 'approval-1',
reason: undefined,
},
input: {
city: 'Tokyo',
},
state: 'approval-responded',
toolCallId: 'call-1',
type: 'dynamic-tool',
toolName: 'weather',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": true,
"providerExecuted": undefined,
"reason": undefined,
"type": "tool-approval-response",
},
],
"role": "tool",
},
]
`);
});
it('should convert a denied tool approval request and follow up text (static tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: false,
id: 'approval-1',
reason: "I don't want to approve this",
},
input: {
city: 'Tokyo',
},
state: 'approval-responded',
toolCallId: 'call-1',
type: 'tool-weather',
},
{ type: 'step-start' },
{
type: 'text',
text: 'I was not able to retrieve the weather.',
state: 'done',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": false,
"providerExecuted": undefined,
"reason": "I don't want to approve this",
"type": "tool-approval-response",
},
],
"role": "tool",
},
{
"content": [
{
"text": "I was not able to retrieve the weather.",
"type": "text",
},
],
"role": "assistant",
},
]
`);
});
it('should convert a denied tool approval request and follow up text (dynamic tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: false,
id: 'approval-1',
reason: "I don't want to approve this",
},
input: {
city: 'Tokyo',
},
state: 'approval-responded',
toolCallId: 'call-1',
type: 'dynamic-tool',
toolName: 'weather',
},
{ type: 'step-start' },
{
type: 'text',
text: 'I was not able to retrieve the weather.',
state: 'done',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": false,
"providerExecuted": undefined,
"reason": "I don't want to approve this",
"type": "tool-approval-response",
},
],
"role": "tool",
},
{
"content": [
{
"text": "I was not able to retrieve the weather.",
"type": "text",
},
],
"role": "assistant",
},
]
`);
});
it('should convert tool output denied (static tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: false,
id: 'approval-1',
reason: "I don't want to approve this",
},
input: {
city: 'Tokyo',
},
state: 'output-denied',
toolCallId: 'call-1',
type: 'tool-weather',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": false,
"providerExecuted": undefined,
"reason": "I don't want to approve this",
"type": "tool-approval-response",
},
{
"output": {
"type": "error-text",
"value": "I don't want to approve this",
},
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-result",
},
],
"role": "tool",
},
]
`);
});
it('should convert tool output denied (dynamic tool)', async () => {
const result = await convertToModelMessages([
{
parts: [
{
text: 'What is the weather in Tokyo?',
type: 'text',
},
],
role: 'user',
},
{
parts: [
{
type: 'step-start',
},
{
approval: {
approved: false,
id: 'approval-1',
reason: "I don't want to approve this",
},
input: {
city: 'Tokyo',
},
state: 'output-denied',
toolCallId: 'call-1',
type: 'dynamic-tool',
toolName: 'weather',
},
],
role: 'assistant',
},
]);
expect(result).toMatchInlineSnapshot(`
[
{
"content": [
{
"text": "What is the weather in Tokyo?",
"type": "text",
},
],
"role": "user",
},
{
"content": [
{
"input": {
"city": "Tokyo",
},
"providerExecuted": undefined,
"toolCallId": "call-1",
"toolName": "weather",
"type": "tool-call",
},
{
"approvalId": "approval-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
},
],
"role": "assistant",
},
{
"content": [
{
"approvalId": "approval-1",
"approved": false,
"providerExecuted": undefined,
"reason": "I don't want to approve this",
"type": "tool-approval-respon