@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.
199 lines (168 loc) • 5.63 kB
text/typescript
import {
Content,
FunctionDeclaration,
Tool as GoogleFunctionCallTool,
Part,
Type as SchemaType,
} from '@google/genai';
import { imageUrlToBase64 } from '@lobechat/utils';
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
import { safeParseJSON } from '../../utils/safeParseJSON';
import { parseDataUri } from '../../utils/uriParser';
/**
* Convert OpenAI content part to Google Part format
*/
export const buildGooglePart = async (
content: UserMessageContentPart,
): Promise<Part | undefined> => {
switch (content.type) {
default: {
return undefined;
}
case 'text': {
return { text: content.text };
}
case 'image_url': {
const { mimeType, base64, type } = parseDataUri(content.image_url.url);
if (type === 'base64') {
if (!base64) {
throw new TypeError("Image URL doesn't contain base64 data");
}
return {
inlineData: { data: base64, mimeType: mimeType || 'image/png' },
};
}
if (type === 'url') {
const { base64, mimeType } = await imageUrlToBase64(content.image_url.url);
return {
inlineData: { data: base64, mimeType },
};
}
throw new TypeError(`currently we don't support image url: ${content.image_url.url}`);
}
case 'video_url': {
const { mimeType, base64, type } = parseDataUri(content.video_url.url);
if (type === 'base64') {
if (!base64) {
throw new TypeError("Video URL doesn't contain base64 data");
}
return {
inlineData: { data: base64, mimeType: mimeType || 'video/mp4' },
};
}
if (type === 'url') {
// Use imageUrlToBase64 for SSRF protection (works for any binary data including videos)
// Note: This might need size/duration limits for practical use
const { base64, mimeType } = await imageUrlToBase64(content.video_url.url);
return {
inlineData: { data: base64, mimeType },
};
}
throw new TypeError(`currently we don't support video url: ${content.video_url.url}`);
}
}
};
/**
* Convert OpenAI message to Google Content format
*/
export const buildGoogleMessage = async (
message: OpenAIChatMessage,
toolCallNameMap?: Map<string, string>,
): Promise<Content> => {
const content = message.content as string | UserMessageContentPart[];
// Handle assistant messages with tool_calls
if (!!message.tool_calls) {
return {
parts: message.tool_calls.map<Part>((tool) => ({
functionCall: {
args: safeParseJSON(tool.function.arguments)!,
name: tool.function.name,
},
})),
role: 'model',
};
}
// Convert tool_call result to functionResponse part
if (message.role === 'tool' && toolCallNameMap && message.tool_call_id) {
const functionName = toolCallNameMap.get(message.tool_call_id);
if (functionName) {
return {
parts: [
{
functionResponse: {
name: functionName,
response: { result: message.content },
},
},
],
role: 'user',
};
}
}
const getParts = async () => {
if (typeof content === 'string') return [{ text: content }];
const parts = await Promise.all(content.map(async (c) => await buildGooglePart(c)));
return parts.filter(Boolean) as Part[];
};
return {
parts: await getParts(),
role: message.role === 'assistant' ? 'model' : 'user',
};
};
/**
* Convert messages from the OpenAI format to Google GenAI SDK format
*/
export const buildGoogleMessages = async (messages: OpenAIChatMessage[]): Promise<Content[]> => {
const toolCallNameMap = new Map<string, string>();
// Build tool call id to name mapping
messages.forEach((message) => {
if (message.role === 'assistant' && message.tool_calls) {
message.tool_calls.forEach((toolCall) => {
if (toolCall.type === 'function') {
toolCallNameMap.set(toolCall.id, toolCall.function.name);
}
});
}
});
const pools = messages
.filter((message) => message.role !== 'function')
.map(async (msg) => await buildGoogleMessage(msg, toolCallNameMap));
const contents = await Promise.all(pools);
// Filter out empty messages: contents.parts must not be empty.
return contents.filter((content: Content) => content.parts && content.parts.length > 0);
};
/**
* Convert ChatCompletionTool to Google FunctionDeclaration
*/
export const buildGoogleTool = (tool: ChatCompletionTool): FunctionDeclaration => {
const functionDeclaration = tool.function;
const parameters = functionDeclaration.parameters;
// refs: https://github.com/lobehub/lobe-chat/pull/5002
const properties =
parameters?.properties && Object.keys(parameters.properties).length > 0
? parameters.properties
: { dummy: { type: 'string' } }; // dummy property to avoid empty object
return {
description: functionDeclaration.description,
name: functionDeclaration.name,
parameters: {
description: parameters?.description,
properties: properties,
required: parameters?.required,
type: SchemaType.OBJECT,
},
};
};
/**
* Build Google function declarations from ChatCompletionTool array
*/
export const buildGoogleTools = (
tools: ChatCompletionTool[] | undefined,
): GoogleFunctionCallTool[] | undefined => {
if (!tools || tools.length === 0) return;
return [
{
functionDeclarations: tools.map((tool) => buildGoogleTool(tool)),
},
];
};