@lobehub/chat 1.138.3 → 1.138.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/database/src/repositories/aiInfra/index.test.ts +656 -0
- package/packages/model-runtime/src/core/contextBuilders/google.test.ts +585 -0
- package/packages/model-runtime/src/core/contextBuilders/google.ts +201 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +191 -179
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +305 -47
- package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +93 -84
- package/packages/model-runtime/src/providers/anthropic/generateObject.ts +3 -3
- package/packages/model-runtime/src/providers/google/generateObject.test.ts +588 -83
- package/packages/model-runtime/src/providers/google/generateObject.ts +104 -6
- package/packages/model-runtime/src/providers/google/index.test.ts +0 -395
- package/packages/model-runtime/src/providers/google/index.ts +28 -194
- package/packages/model-runtime/src/providers/openai/index.test.ts +18 -17
- package/packages/model-runtime/src/types/structureOutput.ts +3 -4
- package/packages/types/src/aiChat.ts +0 -1
- package/src/app/(backend)/trpc/edge/[trpc]/route.ts +0 -2
- package/src/server/routers/edge/index.ts +2 -1
- package/src/server/routers/lambda/aiChat.ts +1 -2
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/upload.ts +16 -0
- package/src/services/__tests__/upload.test.ts +266 -18
- package/src/services/upload.ts +2 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Content,
|
|
3
|
+
FunctionDeclaration,
|
|
4
|
+
Tool as GoogleFunctionCallTool,
|
|
5
|
+
Part,
|
|
6
|
+
Type as SchemaType,
|
|
7
|
+
} from '@google/genai';
|
|
8
|
+
|
|
9
|
+
import { ChatCompletionTool, OpenAIChatMessage, UserMessageContentPart } from '../../types';
|
|
10
|
+
import { imageUrlToBase64 } from '../../utils/imageToBase64';
|
|
11
|
+
import { safeParseJSON } from '../../utils/safeParseJSON';
|
|
12
|
+
import { parseDataUri } from '../../utils/uriParser';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert OpenAI content part to Google Part format
|
|
16
|
+
*/
|
|
17
|
+
export const buildGooglePart = async (
|
|
18
|
+
content: UserMessageContentPart,
|
|
19
|
+
): Promise<Part | undefined> => {
|
|
20
|
+
switch (content.type) {
|
|
21
|
+
default: {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'text': {
|
|
26
|
+
return { text: content.text };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case 'image_url': {
|
|
30
|
+
const { mimeType, base64, type } = parseDataUri(content.image_url.url);
|
|
31
|
+
|
|
32
|
+
if (type === 'base64') {
|
|
33
|
+
if (!base64) {
|
|
34
|
+
throw new TypeError("Image URL doesn't contain base64 data");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
inlineData: { data: base64, mimeType: mimeType || 'image/png' },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (type === 'url') {
|
|
43
|
+
const { base64, mimeType } = await imageUrlToBase64(content.image_url.url);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
inlineData: { data: base64, mimeType },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new TypeError(`currently we don't support image url: ${content.image_url.url}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'video_url': {
|
|
54
|
+
const { mimeType, base64, type } = parseDataUri(content.video_url.url);
|
|
55
|
+
|
|
56
|
+
if (type === 'base64') {
|
|
57
|
+
if (!base64) {
|
|
58
|
+
throw new TypeError("Video URL doesn't contain base64 data");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
inlineData: { data: base64, mimeType: mimeType || 'video/mp4' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (type === 'url') {
|
|
67
|
+
// For video URLs, we need to fetch and convert to base64
|
|
68
|
+
// Note: This might need size/duration limits for practical use
|
|
69
|
+
const response = await fetch(content.video_url.url);
|
|
70
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
71
|
+
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
|
72
|
+
const mimeType = response.headers.get('content-type') || 'video/mp4';
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
inlineData: { data: base64, mimeType },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new TypeError(`currently we don't support video url: ${content.video_url.url}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert OpenAI message to Google Content format
|
|
86
|
+
*/
|
|
87
|
+
export const buildGoogleMessage = async (
|
|
88
|
+
message: OpenAIChatMessage,
|
|
89
|
+
toolCallNameMap?: Map<string, string>,
|
|
90
|
+
): Promise<Content> => {
|
|
91
|
+
const content = message.content as string | UserMessageContentPart[];
|
|
92
|
+
|
|
93
|
+
// Handle assistant messages with tool_calls
|
|
94
|
+
if (!!message.tool_calls) {
|
|
95
|
+
return {
|
|
96
|
+
parts: message.tool_calls.map<Part>((tool) => ({
|
|
97
|
+
functionCall: {
|
|
98
|
+
args: safeParseJSON(tool.function.arguments)!,
|
|
99
|
+
name: tool.function.name,
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
102
|
+
role: 'model',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Convert tool_call result to functionResponse part
|
|
107
|
+
if (message.role === 'tool' && toolCallNameMap && message.tool_call_id) {
|
|
108
|
+
const functionName = toolCallNameMap.get(message.tool_call_id);
|
|
109
|
+
if (functionName) {
|
|
110
|
+
return {
|
|
111
|
+
parts: [
|
|
112
|
+
{
|
|
113
|
+
functionResponse: {
|
|
114
|
+
name: functionName,
|
|
115
|
+
response: { result: message.content },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
role: 'user',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const getParts = async () => {
|
|
125
|
+
if (typeof content === 'string') return [{ text: content }];
|
|
126
|
+
|
|
127
|
+
const parts = await Promise.all(content.map(async (c) => await buildGooglePart(c)));
|
|
128
|
+
return parts.filter(Boolean) as Part[];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
parts: await getParts(),
|
|
133
|
+
role: message.role === 'assistant' ? 'model' : 'user',
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Convert messages from the OpenAI format to Google GenAI SDK format
|
|
139
|
+
*/
|
|
140
|
+
export const buildGoogleMessages = async (messages: OpenAIChatMessage[]): Promise<Content[]> => {
|
|
141
|
+
const toolCallNameMap = new Map<string, string>();
|
|
142
|
+
|
|
143
|
+
// Build tool call id to name mapping
|
|
144
|
+
messages.forEach((message) => {
|
|
145
|
+
if (message.role === 'assistant' && message.tool_calls) {
|
|
146
|
+
message.tool_calls.forEach((toolCall) => {
|
|
147
|
+
if (toolCall.type === 'function') {
|
|
148
|
+
toolCallNameMap.set(toolCall.id, toolCall.function.name);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const pools = messages
|
|
155
|
+
.filter((message) => message.role !== 'function')
|
|
156
|
+
.map(async (msg) => await buildGoogleMessage(msg, toolCallNameMap));
|
|
157
|
+
|
|
158
|
+
const contents = await Promise.all(pools);
|
|
159
|
+
|
|
160
|
+
// Filter out empty messages: contents.parts must not be empty.
|
|
161
|
+
return contents.filter((content: Content) => content.parts && content.parts.length > 0);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert ChatCompletionTool to Google FunctionDeclaration
|
|
166
|
+
*/
|
|
167
|
+
export const buildGoogleTool = (tool: ChatCompletionTool): FunctionDeclaration => {
|
|
168
|
+
const functionDeclaration = tool.function;
|
|
169
|
+
const parameters = functionDeclaration.parameters;
|
|
170
|
+
// refs: https://github.com/lobehub/lobe-chat/pull/5002
|
|
171
|
+
const properties =
|
|
172
|
+
parameters?.properties && Object.keys(parameters.properties).length > 0
|
|
173
|
+
? parameters.properties
|
|
174
|
+
: { dummy: { type: 'string' } }; // dummy property to avoid empty object
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
description: functionDeclaration.description,
|
|
178
|
+
name: functionDeclaration.name,
|
|
179
|
+
parameters: {
|
|
180
|
+
description: parameters?.description,
|
|
181
|
+
properties: properties,
|
|
182
|
+
required: parameters?.required,
|
|
183
|
+
type: SchemaType.OBJECT,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build Google function declarations from ChatCompletionTool array
|
|
190
|
+
*/
|
|
191
|
+
export const buildGoogleTools = (
|
|
192
|
+
tools: ChatCompletionTool[] | undefined,
|
|
193
|
+
): GoogleFunctionCallTool[] | undefined => {
|
|
194
|
+
if (!tools || tools.length === 0) return;
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
functionDeclarations: tools.map((tool) => buildGoogleTool(tool)),
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
};
|