@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
586 lines (500 loc) • 16.5 kB
text/typescript
// @vitest-environment node
import { Type as SchemaType } from '@google/genai';
import * as imageToBase64Module from '@lobechat/utils';
import { describe, expect, it, vi } from 'vitest';
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
import { parseDataUri } from '../../utils/uriParser';
import {
buildGoogleMessage,
buildGoogleMessages,
buildGooglePart,
buildGoogleTool,
buildGoogleTools,
} from './google';
// Mock the utils
vi.mock('../../utils/uriParser', () => ({
parseDataUri: vi.fn(),
}));
vi.mock('../../utils/imageToBase64', () => ({
imageUrlToBase64: vi.fn(),
}));
describe('google contextBuilders', () => {
describe('buildGooglePart', () => {
it('should handle text type messages', async () => {
const content: UserMessageContentPart = {
text: 'Hello',
type: 'text',
};
const result = await buildGooglePart(content);
expect(result).toEqual({ text: 'Hello' });
});
it('should handle thinking type messages', async () => {
const content: UserMessageContentPart = {
signature: 'abc',
thinking: 'Hello',
type: 'thinking',
};
const result = await buildGooglePart(content);
expect(result).toEqual(undefined);
});
it('should handle base64 type images', async () => {
const base64Image =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
vi.mocked(parseDataUri).mockReturnValueOnce({
base64:
'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
mimeType: 'image/png',
type: 'base64',
});
const content: UserMessageContentPart = {
image_url: { url: base64Image },
type: 'image_url',
};
const result = await buildGooglePart(content);
expect(result).toEqual({
inlineData: {
data: 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
mimeType: 'image/png',
},
});
});
it('should handle URL type images', async () => {
const imageUrl = 'http://example.com/image.png';
const mockBase64 = 'mockBase64Data';
vi.mocked(parseDataUri).mockReturnValueOnce({
base64: null,
mimeType: 'image/png',
type: 'url',
});
vi.spyOn(imageToBase64Module, 'imageUrlToBase64').mockResolvedValueOnce({
base64: mockBase64,
mimeType: 'image/png',
});
const content: UserMessageContentPart = {
image_url: { url: imageUrl },
type: 'image_url',
};
const result = await buildGooglePart(content);
expect(result).toEqual({
inlineData: {
data: mockBase64,
mimeType: 'image/png',
},
});
expect(imageToBase64Module.imageUrlToBase64).toHaveBeenCalledWith(imageUrl);
});
it('should throw TypeError for unsupported image URL types', async () => {
const unsupportedImageUrl = 'unsupported://example.com/image.png';
vi.mocked(parseDataUri).mockReturnValueOnce({
base64: null,
mimeType: null,
type: 'unknown' as any,
});
const content: UserMessageContentPart = {
image_url: { url: unsupportedImageUrl },
type: 'image_url',
};
await expect(buildGooglePart(content)).rejects.toThrow(TypeError);
});
it('should handle base64 video', async () => {
const base64Video = 'data:video/mp4;base64,mockVideoBase64Data';
vi.mocked(parseDataUri).mockReturnValueOnce({
base64: 'mockVideoBase64Data',
mimeType: 'video/mp4',
type: 'base64',
});
const content: UserMessageContentPart = {
type: 'video_url',
video_url: { url: base64Video },
};
const result = await buildGooglePart(content);
expect(result).toEqual({
inlineData: {
data: 'mockVideoBase64Data',
mimeType: 'video/mp4',
},
});
});
});
describe('buildGoogleMessage', () => {
it('should correctly convert assistant message', async () => {
const message: OpenAIChatMessage = {
content: 'Hello',
role: 'assistant',
};
const converted = await buildGoogleMessage(message);
expect(converted).toEqual({
parts: [{ text: 'Hello' }],
role: 'model',
});
});
it('should correctly convert user message', async () => {
const message: OpenAIChatMessage = {
content: 'Hi',
role: 'user',
};
const converted = await buildGoogleMessage(message);
expect(converted).toEqual({
parts: [{ text: 'Hi' }],
role: 'user',
});
});
it('should correctly convert message with inline base64 image parts', async () => {
vi.mocked(parseDataUri).mockReturnValueOnce({
base64: '...',
mimeType: 'image/png',
type: 'base64',
});
const message: OpenAIChatMessage = {
content: [
{ text: 'Check this image:', type: 'text' },
{ image_url: { url: 'data:image/png;base64,...' }, type: 'image_url' },
],
role: 'user',
};
const converted = await buildGoogleMessage(message);
expect(converted).toEqual({
parts: [
{ text: 'Check this image:' },
{ inlineData: { data: '...', mimeType: 'image/png' } },
],
role: 'user',
});
});
it('should correctly convert function call message', async () => {
const message = {
role: 'assistant',
tool_calls: [
{
function: {
arguments: JSON.stringify({ location: 'London', unit: 'celsius' }),
name: 'get_current_weather',
},
id: 'call_1',
type: 'function',
},
],
} as OpenAIChatMessage;
const converted = await buildGoogleMessage(message);
expect(converted).toEqual({
parts: [
{
functionCall: {
args: { location: 'London', unit: 'celsius' },
name: 'get_current_weather',
},
},
],
role: 'model',
});
});
it('should correctly handle empty content', async () => {
const message: OpenAIChatMessage = {
content: '' as any, // explicitly set as empty string
role: 'user',
};
const converted = await buildGoogleMessage(message);
expect(converted).toEqual({
parts: [{ text: '' }],
role: 'user',
});
});
it('should correctly convert tool response message', async () => {
const toolCallNameMap = new Map<string, string>();
toolCallNameMap.set('call_1', 'get_current_weather');
const message: OpenAIChatMessage = {
content: '{"success":true,"data":{"temperature":"14°C"}}',
name: 'get_current_weather',
role: 'tool',
tool_call_id: 'call_1',
};
const converted = await buildGoogleMessage(message, toolCallNameMap);
expect(converted).toEqual({
parts: [
{
functionResponse: {
name: 'get_current_weather',
response: { result: '{"success":true,"data":{"temperature":"14°C"}}' },
},
},
],
role: 'user',
});
});
});
describe('buildGoogleMessages', () => {
it('get default result with gemini-pro', async () => {
const messages: OpenAIChatMessage[] = [{ content: 'Hello', role: 'user' }];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(1);
expect(contents).toEqual([{ parts: [{ text: 'Hello' }], role: 'user' }]);
});
it('should not modify the length if model is gemini-1.5-pro', async () => {
const messages: OpenAIChatMessage[] = [
{ content: 'Hello', role: 'user' },
{ content: 'Hi', role: 'assistant' },
];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(2);
expect(contents).toEqual([
{ parts: [{ text: 'Hello' }], role: 'user' },
{ parts: [{ text: 'Hi' }], role: 'model' },
]);
});
it('should use specified model when images are included in messages', async () => {
vi.mocked(parseDataUri).mockReturnValueOnce({
base64: '...',
mimeType: 'image/png',
type: 'base64',
});
const messages: OpenAIChatMessage[] = [
{
content: [
{ text: 'Hello', type: 'text' },
{ image_url: { url: 'data:image/png;base64,...' }, type: 'image_url' },
],
role: 'user',
},
];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(1);
expect(contents).toEqual([
{
parts: [{ text: 'Hello' }, { inlineData: { data: '...', mimeType: 'image/png' } }],
role: 'user',
},
]);
});
it('should correctly convert function response message', async () => {
const messages: OpenAIChatMessage[] = [
{
content: '',
role: 'assistant',
tool_calls: [
{
function: {
arguments: JSON.stringify({ location: 'London', unit: 'celsius' }),
name: 'get_current_weather',
},
id: 'call_1',
type: 'function',
},
],
},
{
content: '{"success":true,"data":{"temperature":"14°C"}}',
name: 'get_current_weather',
role: 'tool',
tool_call_id: 'call_1',
},
];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(2);
expect(contents).toEqual([
{
parts: [
{
functionCall: {
args: { location: 'London', unit: 'celsius' },
name: 'get_current_weather',
},
},
],
role: 'model',
},
{
parts: [
{
functionResponse: {
name: 'get_current_weather',
response: { result: '{"success":true,"data":{"temperature":"14°C"}}' },
},
},
],
role: 'user',
},
]);
});
it('should filter out function role messages', async () => {
const messages: OpenAIChatMessage[] = [
{ content: 'Hello', role: 'user' },
{ content: 'function result', name: 'test_func', role: 'function' },
{ content: 'Hi', role: 'assistant' },
];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(2);
expect(contents).toEqual([
{ parts: [{ text: 'Hello' }], role: 'user' },
{ parts: [{ text: 'Hi' }], role: 'model' },
]);
});
it('should filter out empty messages', async () => {
const messages: OpenAIChatMessage[] = [
{ content: 'Hello', role: 'user' },
{ content: [], role: 'user' },
{ content: 'Hi', role: 'assistant' },
];
const contents = await buildGoogleMessages(messages);
expect(contents).toHaveLength(2);
expect(contents).toEqual([
{ parts: [{ text: 'Hello' }], role: 'user' },
{ parts: [{ text: 'Hi' }], role: 'model' },
]);
});
});
describe('buildGoogleTool', () => {
it('should correctly convert ChatCompletionTool to FunctionDeclaration', () => {
const tool: ChatCompletionTool = {
function: {
description: 'A test tool',
name: 'testTool',
parameters: {
properties: {
param1: { type: 'string' },
param2: { type: 'number' },
},
required: ['param1'],
type: 'object',
},
},
type: 'function',
};
const result = buildGoogleTool(tool);
expect(result).toEqual({
description: 'A test tool',
name: 'testTool',
parameters: {
description: undefined,
properties: {
param1: { type: 'string' },
param2: { type: 'number' },
},
required: ['param1'],
type: SchemaType.OBJECT,
},
});
});
it('should handle tools with empty parameters', () => {
const tool: ChatCompletionTool = {
function: {
description: 'A simple function with no parameters',
name: 'simple_function',
parameters: {
properties: {},
type: 'object',
},
},
type: 'function',
};
const result = buildGoogleTool(tool);
// Should use dummy property for empty parameters
expect(result).toEqual({
description: 'A simple function with no parameters',
name: 'simple_function',
parameters: {
description: undefined,
properties: { dummy: { type: 'string' } },
required: undefined,
type: SchemaType.OBJECT,
},
});
});
it('should preserve parameter description', () => {
const tool: ChatCompletionTool = {
function: {
description: 'A test tool',
name: 'testTool',
parameters: {
description: 'Test parameters',
properties: {
param1: { type: 'string' },
},
type: 'object',
},
},
type: 'function',
};
const result = buildGoogleTool(tool);
expect(result.parameters?.description).toBe('Test parameters');
});
});
describe('buildGoogleTools', () => {
it('should return undefined when tools is undefined or empty', () => {
expect(buildGoogleTools(undefined)).toBeUndefined();
expect(buildGoogleTools([])).toBeUndefined();
});
it('should correctly convert ChatCompletionTool array to GoogleFunctionCallTool', () => {
const tools: ChatCompletionTool[] = [
{
function: {
description: 'A test tool',
name: 'testTool',
parameters: {
properties: {
param1: { type: 'string' },
param2: { type: 'number' },
},
required: ['param1'],
type: 'object',
},
},
type: 'function',
},
];
const googleTools = buildGoogleTools(tools);
expect(googleTools).toHaveLength(1);
expect(googleTools![0].functionDeclarations).toHaveLength(1);
expect(googleTools![0].functionDeclarations![0]).toEqual({
description: 'A test tool',
name: 'testTool',
parameters: {
description: undefined,
properties: {
param1: { type: 'string' },
param2: { type: 'number' },
},
required: ['param1'],
type: SchemaType.OBJECT,
},
});
});
it('should handle multiple tools', () => {
const tools: ChatCompletionTool[] = [
{
function: {
description: 'Get weather information',
name: 'get_weather',
parameters: {
properties: {
city: { type: 'string' },
unit: { type: 'string' },
},
required: ['city'],
type: 'object',
},
},
type: 'function',
},
{
function: {
description: 'Get current time',
name: 'get_time',
parameters: {
properties: {
timezone: { type: 'string' },
},
required: ['timezone'],
type: 'object',
},
},
type: 'function',
},
];
const googleTools = buildGoogleTools(tools);
expect(googleTools).toHaveLength(1);
expect(googleTools![0].functionDeclarations).toHaveLength(2);
expect(googleTools![0].functionDeclarations![0].name).toBe('get_weather');
expect(googleTools![0].functionDeclarations![1].name).toBe('get_time');
});
});
});