@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.
Files changed (24) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/database/src/repositories/aiInfra/index.test.ts +656 -0
  5. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +585 -0
  6. package/packages/model-runtime/src/core/contextBuilders/google.ts +201 -0
  7. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +191 -179
  8. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +305 -47
  9. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +93 -84
  10. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +3 -3
  11. package/packages/model-runtime/src/providers/google/generateObject.test.ts +588 -83
  12. package/packages/model-runtime/src/providers/google/generateObject.ts +104 -6
  13. package/packages/model-runtime/src/providers/google/index.test.ts +0 -395
  14. package/packages/model-runtime/src/providers/google/index.ts +28 -194
  15. package/packages/model-runtime/src/providers/openai/index.test.ts +18 -17
  16. package/packages/model-runtime/src/types/structureOutput.ts +3 -4
  17. package/packages/types/src/aiChat.ts +0 -1
  18. package/src/app/(backend)/trpc/edge/[trpc]/route.ts +0 -2
  19. package/src/server/routers/edge/index.ts +2 -1
  20. package/src/server/routers/lambda/aiChat.ts +1 -2
  21. package/src/server/routers/lambda/index.ts +2 -0
  22. package/src/server/routers/lambda/upload.ts +16 -0
  23. package/src/services/__tests__/upload.test.ts +266 -18
  24. 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
+ };