@lobehub/chat 1.140.0 → 1.141.0
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 +43 -0
- package/changelog/v1.json +15 -0
- package/locales/ar/chat.json +13 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/components.json +4 -0
- package/locales/ar/file.json +2 -2
- package/locales/bg-BG/chat.json +13 -0
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/components.json +4 -0
- package/locales/bg-BG/file.json +2 -2
- package/locales/de-DE/chat.json +13 -0
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/components.json +4 -0
- package/locales/de-DE/file.json +2 -2
- package/locales/en-US/chat.json +13 -0
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/components.json +4 -0
- package/locales/en-US/file.json +2 -2
- package/locales/es-ES/chat.json +13 -0
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/components.json +4 -0
- package/locales/es-ES/file.json +2 -2
- package/locales/fa-IR/chat.json +13 -0
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/components.json +4 -0
- package/locales/fa-IR/file.json +2 -2
- package/locales/fr-FR/chat.json +13 -0
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/components.json +4 -0
- package/locales/fr-FR/file.json +2 -2
- package/locales/it-IT/chat.json +13 -0
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/components.json +4 -0
- package/locales/it-IT/file.json +2 -2
- package/locales/ja-JP/chat.json +13 -0
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/components.json +4 -0
- package/locales/ja-JP/file.json +2 -2
- package/locales/ko-KR/chat.json +13 -0
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/components.json +4 -0
- package/locales/ko-KR/file.json +2 -2
- package/locales/nl-NL/chat.json +13 -0
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/components.json +4 -0
- package/locales/nl-NL/file.json +2 -2
- package/locales/pl-PL/chat.json +13 -0
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/components.json +4 -0
- package/locales/pl-PL/file.json +2 -2
- package/locales/pt-BR/chat.json +13 -0
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/components.json +4 -0
- package/locales/pt-BR/file.json +2 -2
- package/locales/ru-RU/chat.json +13 -0
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/components.json +4 -0
- package/locales/ru-RU/file.json +2 -2
- package/locales/tr-TR/chat.json +13 -0
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/components.json +4 -0
- package/locales/tr-TR/file.json +2 -2
- package/locales/vi-VN/chat.json +13 -0
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/components.json +4 -0
- package/locales/vi-VN/file.json +2 -2
- package/locales/zh-CN/chat.json +13 -0
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/components.json +4 -0
- package/locales/zh-CN/file.json +2 -2
- package/locales/zh-TW/chat.json +13 -0
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/components.json +4 -0
- package/locales/zh-TW/file.json +2 -2
- package/next.config.ts +5 -6
- package/package.json +8 -2
- package/packages/database/src/models/__tests__/message.test.ts +200 -2
- package/packages/database/src/models/message.ts +13 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +313 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +21 -5
- package/packages/model-runtime/src/providers/azureai/index.test.ts +12 -2
- package/packages/model-runtime/src/providers/groq/index.test.ts +449 -0
- package/packages/model-runtime/src/providers/groq/index.ts +46 -0
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +3 -2
- package/src/app/[variants]/(main)/files/(content)/@menu/features/KnowledgeBase/Item/index.tsx +10 -2
- package/src/features/ChatInput/InputEditor/index.tsx +2 -0
- package/src/features/Conversation/Messages/User/index.tsx +7 -17
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/PdfPreview.tsx +361 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/index.tsx +119 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/style.ts +63 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/template.ts +24 -0
- package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/usePdfGeneration.ts +93 -0
- package/src/features/Conversation/components/ShareMessageModal/index.tsx +39 -14
- package/src/features/FileManager/FileList/MasonryFileItem/MasonryItemWrapper.tsx +44 -0
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +553 -0
- package/src/features/FileManager/FileList/MasonrySkeleton.tsx +57 -0
- package/src/features/FileManager/FileList/ToolBar/ViewSwitcher.tsx +45 -0
- package/src/features/FileManager/FileList/ToolBar/index.tsx +9 -1
- package/src/features/FileManager/FileList/index.tsx +83 -13
- package/src/features/FileManager/Header/FilesSearchBar.tsx +7 -2
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +361 -0
- package/src/features/ShareModal/SharePdf/index.tsx +194 -0
- package/src/features/ShareModal/SharePdf/usePdfGeneration.ts +90 -0
- package/src/features/ShareModal/index.tsx +40 -14
- package/src/features/ShareModal/style.ts +8 -5
- package/src/libs/trpc/client/lambda.ts +7 -1
- package/src/locales/default/chat.ts +13 -0
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/components.ts +4 -0
- package/src/locales/default/file.ts +2 -2
- package/src/server/globalConfig/parseSystemAgent.ts +4 -2
- package/src/server/routers/lambda/exporter.ts +173 -3
- package/src/server/routers/lambda/message.ts +11 -0
- package/src/store/global/initialState.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.141.0",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"@lobehub/charts": "^2.1.2",
|
|
165
165
|
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
|
166
166
|
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
|
167
|
-
"@lobehub/editor": "^1.
|
|
167
|
+
"@lobehub/editor": "^1.20.2",
|
|
168
168
|
"@lobehub/icons": "^2.42.0",
|
|
169
169
|
"@lobehub/market-sdk": "^0.22.7",
|
|
170
170
|
"@lobehub/tts": "^2.0.1",
|
|
@@ -174,6 +174,7 @@
|
|
|
174
174
|
"@next/third-parties": "^15.5.4",
|
|
175
175
|
"@opentelemetry/exporter-jaeger": "^2.1.0",
|
|
176
176
|
"@opentelemetry/winston-transport": "^0.17.0",
|
|
177
|
+
"@react-pdf/renderer": "^4.3.0",
|
|
177
178
|
"@react-spring/web": "^9.7.5",
|
|
178
179
|
"@saintno/comfyui-sdk": "^0.2.48",
|
|
179
180
|
"@serwist/next": "^9.2.1",
|
|
@@ -187,6 +188,7 @@
|
|
|
187
188
|
"@vercel/edge-config": "^1.4.0",
|
|
188
189
|
"@vercel/functions": "^3.1.3",
|
|
189
190
|
"@vercel/speed-insights": "^1.2.0",
|
|
191
|
+
"@virtuoso.dev/masonry": "^1.3.5",
|
|
190
192
|
"@xterm/xterm": "^5.5.0",
|
|
191
193
|
"ahooks": "^3.9.5",
|
|
192
194
|
"antd": "^5.27.4",
|
|
@@ -224,6 +226,7 @@
|
|
|
224
226
|
"lucide-react": "^0.544.0",
|
|
225
227
|
"mammoth": "^1.11.0",
|
|
226
228
|
"markdown-to-txt": "^2.0.1",
|
|
229
|
+
"marked": "^16.3.0",
|
|
227
230
|
"mdast-util-to-markdown": "^2.1.2",
|
|
228
231
|
"model-bank": "workspace:*",
|
|
229
232
|
"modern-screenshot": "^4.6.6",
|
|
@@ -244,6 +247,7 @@
|
|
|
244
247
|
"path-browserify-esm": "^1.0.6",
|
|
245
248
|
"pdf-parse": "^1.1.1",
|
|
246
249
|
"pdfjs-dist": "4.8.69",
|
|
250
|
+
"pdfkit": "^0.17.2",
|
|
247
251
|
"pg": "^8.16.3",
|
|
248
252
|
"pino": "^9.13.1",
|
|
249
253
|
"plaiceholder": "^3.0.0",
|
|
@@ -320,9 +324,11 @@
|
|
|
320
324
|
"@types/json-schema": "^7.0.15",
|
|
321
325
|
"@types/lodash": "^4.17.20",
|
|
322
326
|
"@types/lodash-es": "^4.17.12",
|
|
327
|
+
"@types/marked": "^6.0.0",
|
|
323
328
|
"@types/node": "^22.18.9",
|
|
324
329
|
"@types/numeral": "^2.0.5",
|
|
325
330
|
"@types/oidc-provider": "^9.5.0",
|
|
331
|
+
"@types/pdfkit": "^0.17.3",
|
|
326
332
|
"@types/pg": "^8.15.5",
|
|
327
333
|
"@types/react": "^19.2.2",
|
|
328
334
|
"@types/react-dom": "^19.2.1",
|
|
@@ -7,10 +7,10 @@ import { uuid } from '@/utils/uuid';
|
|
|
7
7
|
|
|
8
8
|
import { getTestDB } from '../../models/__tests__/_util';
|
|
9
9
|
import {
|
|
10
|
-
chunks,
|
|
11
|
-
embeddings,
|
|
12
10
|
agents,
|
|
13
11
|
chatGroups,
|
|
12
|
+
chunks,
|
|
13
|
+
embeddings,
|
|
14
14
|
fileChunks,
|
|
15
15
|
files,
|
|
16
16
|
messagePlugins,
|
|
@@ -1290,6 +1290,204 @@ describe('MessageModel', () => {
|
|
|
1290
1290
|
});
|
|
1291
1291
|
});
|
|
1292
1292
|
|
|
1293
|
+
describe('updateMetadata', () => {
|
|
1294
|
+
it('should update metadata for an existing message', async () => {
|
|
1295
|
+
// 创建测试数据
|
|
1296
|
+
await serverDB.insert(messages).values({
|
|
1297
|
+
id: 'msg-with-metadata',
|
|
1298
|
+
userId,
|
|
1299
|
+
role: 'user',
|
|
1300
|
+
content: 'test message',
|
|
1301
|
+
metadata: { existingKey: 'existingValue' },
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// 调用 updateMetadata 方法
|
|
1305
|
+
await messageModel.updateMetadata('msg-with-metadata', { newKey: 'newValue' });
|
|
1306
|
+
|
|
1307
|
+
// 断言结果
|
|
1308
|
+
const result = await serverDB
|
|
1309
|
+
.select()
|
|
1310
|
+
.from(messages)
|
|
1311
|
+
.where(eq(messages.id, 'msg-with-metadata'));
|
|
1312
|
+
|
|
1313
|
+
expect(result[0].metadata).toEqual({
|
|
1314
|
+
existingKey: 'existingValue',
|
|
1315
|
+
newKey: 'newValue',
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it('should merge new metadata with existing metadata using lodash merge behavior', async () => {
|
|
1320
|
+
// 创建测试数据
|
|
1321
|
+
await serverDB.insert(messages).values({
|
|
1322
|
+
id: 'msg-merge-metadata',
|
|
1323
|
+
userId,
|
|
1324
|
+
role: 'assistant',
|
|
1325
|
+
content: 'test message',
|
|
1326
|
+
metadata: {
|
|
1327
|
+
level1: {
|
|
1328
|
+
level2a: 'original',
|
|
1329
|
+
level2b: { level3: 'deep' },
|
|
1330
|
+
},
|
|
1331
|
+
array: [1, 2, 3],
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
// 调用 updateMetadata 方法
|
|
1336
|
+
await messageModel.updateMetadata('msg-merge-metadata', {
|
|
1337
|
+
level1: {
|
|
1338
|
+
level2a: 'updated',
|
|
1339
|
+
level2c: 'new',
|
|
1340
|
+
},
|
|
1341
|
+
newTopLevel: 'value',
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// 断言结果 - 应该使用 lodash merge 行为
|
|
1345
|
+
const result = await serverDB
|
|
1346
|
+
.select()
|
|
1347
|
+
.from(messages)
|
|
1348
|
+
.where(eq(messages.id, 'msg-merge-metadata'));
|
|
1349
|
+
|
|
1350
|
+
expect(result[0].metadata).toEqual({
|
|
1351
|
+
level1: {
|
|
1352
|
+
level2a: 'updated',
|
|
1353
|
+
level2b: { level3: 'deep' },
|
|
1354
|
+
level2c: 'new',
|
|
1355
|
+
},
|
|
1356
|
+
array: [1, 2, 3],
|
|
1357
|
+
newTopLevel: 'value',
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
it('should handle non-existent message IDs', async () => {
|
|
1362
|
+
// 调用 updateMetadata 方法,尝试更新不存在的消息
|
|
1363
|
+
const result = await messageModel.updateMetadata('non-existent-id', { key: 'value' });
|
|
1364
|
+
|
|
1365
|
+
// 断言结果 - 应该返回 undefined
|
|
1366
|
+
expect(result).toBeUndefined();
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('should handle empty metadata updates', async () => {
|
|
1370
|
+
// 创建测试数据
|
|
1371
|
+
await serverDB.insert(messages).values({
|
|
1372
|
+
id: 'msg-empty-metadata',
|
|
1373
|
+
userId,
|
|
1374
|
+
role: 'user',
|
|
1375
|
+
content: 'test message',
|
|
1376
|
+
metadata: { originalKey: 'originalValue' },
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// 调用 updateMetadata 方法,传递空对象
|
|
1380
|
+
await messageModel.updateMetadata('msg-empty-metadata', {});
|
|
1381
|
+
|
|
1382
|
+
// 断言结果 - 原始 metadata 应该保持不变
|
|
1383
|
+
const result = await serverDB
|
|
1384
|
+
.select()
|
|
1385
|
+
.from(messages)
|
|
1386
|
+
.where(eq(messages.id, 'msg-empty-metadata'));
|
|
1387
|
+
|
|
1388
|
+
expect(result[0].metadata).toEqual({ originalKey: 'originalValue' });
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
it('should handle message with null metadata', async () => {
|
|
1392
|
+
// 创建测试数据
|
|
1393
|
+
await serverDB.insert(messages).values({
|
|
1394
|
+
id: 'msg-null-metadata',
|
|
1395
|
+
userId,
|
|
1396
|
+
role: 'user',
|
|
1397
|
+
content: 'test message',
|
|
1398
|
+
metadata: null,
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
// 调用 updateMetadata 方法
|
|
1402
|
+
await messageModel.updateMetadata('msg-null-metadata', { key: 'value' });
|
|
1403
|
+
|
|
1404
|
+
// 断言结果 - 应该创建新的 metadata
|
|
1405
|
+
const result = await serverDB
|
|
1406
|
+
.select()
|
|
1407
|
+
.from(messages)
|
|
1408
|
+
.where(eq(messages.id, 'msg-null-metadata'));
|
|
1409
|
+
|
|
1410
|
+
expect(result[0].metadata).toEqual({ key: 'value' });
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it('should only update messages belonging to the current user', async () => {
|
|
1414
|
+
// 创建测试数据 - 其他用户的消息
|
|
1415
|
+
await serverDB.insert(messages).values({
|
|
1416
|
+
id: 'msg-other-user',
|
|
1417
|
+
userId: '456',
|
|
1418
|
+
role: 'user',
|
|
1419
|
+
content: 'test message',
|
|
1420
|
+
metadata: { originalKey: 'originalValue' },
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// 调用 updateMetadata 方法
|
|
1424
|
+
const result = await messageModel.updateMetadata('msg-other-user', {
|
|
1425
|
+
hackedKey: 'hackedValue',
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// 断言结果 - 应该返回 undefined
|
|
1429
|
+
expect(result).toBeUndefined();
|
|
1430
|
+
|
|
1431
|
+
// 验证原始 metadata 未被修改
|
|
1432
|
+
const dbResult = await serverDB
|
|
1433
|
+
.select()
|
|
1434
|
+
.from(messages)
|
|
1435
|
+
.where(eq(messages.id, 'msg-other-user'));
|
|
1436
|
+
|
|
1437
|
+
expect(dbResult[0].metadata).toEqual({ originalKey: 'originalValue' });
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it('should handle complex nested metadata updates', async () => {
|
|
1441
|
+
// 创建测试数据
|
|
1442
|
+
await serverDB.insert(messages).values({
|
|
1443
|
+
id: 'msg-complex-metadata',
|
|
1444
|
+
userId,
|
|
1445
|
+
role: 'assistant',
|
|
1446
|
+
content: 'test message',
|
|
1447
|
+
metadata: {
|
|
1448
|
+
config: {
|
|
1449
|
+
settings: {
|
|
1450
|
+
enabled: true,
|
|
1451
|
+
options: ['a', 'b'],
|
|
1452
|
+
},
|
|
1453
|
+
version: 1,
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
// 调用 updateMetadata 方法
|
|
1459
|
+
await messageModel.updateMetadata('msg-complex-metadata', {
|
|
1460
|
+
config: {
|
|
1461
|
+
settings: {
|
|
1462
|
+
enabled: false,
|
|
1463
|
+
timeout: 5000,
|
|
1464
|
+
},
|
|
1465
|
+
newField: 'value',
|
|
1466
|
+
},
|
|
1467
|
+
stats: { count: 10 },
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// 断言结果
|
|
1471
|
+
const result = await serverDB
|
|
1472
|
+
.select()
|
|
1473
|
+
.from(messages)
|
|
1474
|
+
.where(eq(messages.id, 'msg-complex-metadata'));
|
|
1475
|
+
|
|
1476
|
+
expect(result[0].metadata).toEqual({
|
|
1477
|
+
config: {
|
|
1478
|
+
settings: {
|
|
1479
|
+
enabled: false,
|
|
1480
|
+
options: ['a', 'b'],
|
|
1481
|
+
timeout: 5000,
|
|
1482
|
+
},
|
|
1483
|
+
version: 1,
|
|
1484
|
+
newField: 'value',
|
|
1485
|
+
},
|
|
1486
|
+
stats: { count: 10 },
|
|
1487
|
+
});
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1293
1491
|
describe('updateTranslate', () => {
|
|
1294
1492
|
it('should insert a new record if message does not exist in messageTranslates table', async () => {
|
|
1295
1493
|
// 创建测试数据
|
|
@@ -570,6 +570,19 @@ export class MessageModel {
|
|
|
570
570
|
});
|
|
571
571
|
};
|
|
572
572
|
|
|
573
|
+
updateMetadata = async (id: string, metadata: Record<string, any>) => {
|
|
574
|
+
const item = await this.db.query.messages.findFirst({
|
|
575
|
+
where: and(eq(messages.id, id), eq(messages.userId, this.userId)),
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
if (!item) return;
|
|
579
|
+
|
|
580
|
+
return this.db
|
|
581
|
+
.update(messages)
|
|
582
|
+
.set({ metadata: merge(item.metadata || {}, metadata) })
|
|
583
|
+
.where(and(eq(messages.userId, this.userId), eq(messages.id, id)));
|
|
584
|
+
};
|
|
585
|
+
|
|
573
586
|
updatePluginState = async (id: string, state: Record<string, any>) => {
|
|
574
587
|
const item = await this.db.query.messagePlugins.findFirst({
|
|
575
588
|
where: eq(messagePlugins.id, id),
|
|
@@ -2048,6 +2048,319 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
|
2048
2048
|
});
|
|
2049
2049
|
});
|
|
2050
2050
|
|
|
2051
|
+
describe('handleSchema option', () => {
|
|
2052
|
+
let instanceWithSchemaHandler: any;
|
|
2053
|
+
const mockSchemaHandler = vi.fn((schema: any) => {
|
|
2054
|
+
const filtered: any = {};
|
|
2055
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2056
|
+
if (key !== 'maxLength' && key !== 'pattern') {
|
|
2057
|
+
filtered[key] = value;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return filtered;
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
beforeEach(() => {
|
|
2064
|
+
mockSchemaHandler.mockClear();
|
|
2065
|
+
const RuntimeClass = createOpenAICompatibleRuntime({
|
|
2066
|
+
baseURL: 'https://api.test.com',
|
|
2067
|
+
generateObject: {
|
|
2068
|
+
handleSchema: mockSchemaHandler,
|
|
2069
|
+
},
|
|
2070
|
+
provider: 'test-provider',
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
instanceWithSchemaHandler = new RuntimeClass({ apiKey: 'test-key' });
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it('should apply schema transformation with Responses API', async () => {
|
|
2077
|
+
const mockResponse = {
|
|
2078
|
+
output_text: '{"name":"Alice","age":30}',
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
vi.spyOn(instanceWithSchemaHandler['client'].responses, 'create').mockResolvedValue(
|
|
2082
|
+
mockResponse as any,
|
|
2083
|
+
);
|
|
2084
|
+
|
|
2085
|
+
const payload = {
|
|
2086
|
+
messages: [{ content: 'Extract person', role: 'user' as const }],
|
|
2087
|
+
model: 'gpt-4o',
|
|
2088
|
+
responseApi: true,
|
|
2089
|
+
schema: {
|
|
2090
|
+
name: 'person',
|
|
2091
|
+
schema: {
|
|
2092
|
+
maxLength: 100,
|
|
2093
|
+
pattern: '^[a-z]+$',
|
|
2094
|
+
properties: {
|
|
2095
|
+
age: { type: 'number' },
|
|
2096
|
+
name: { type: 'string' },
|
|
2097
|
+
},
|
|
2098
|
+
type: 'object' as const,
|
|
2099
|
+
},
|
|
2100
|
+
},
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
await instanceWithSchemaHandler.generateObject(payload);
|
|
2104
|
+
|
|
2105
|
+
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
|
|
2106
|
+
expect(instanceWithSchemaHandler['client'].responses.create).toHaveBeenCalledWith(
|
|
2107
|
+
expect.objectContaining({
|
|
2108
|
+
text: expect.objectContaining({
|
|
2109
|
+
format: expect.objectContaining({
|
|
2110
|
+
schema: {
|
|
2111
|
+
properties: {
|
|
2112
|
+
age: { type: 'number' },
|
|
2113
|
+
name: { type: 'string' },
|
|
2114
|
+
},
|
|
2115
|
+
type: 'object',
|
|
2116
|
+
},
|
|
2117
|
+
}),
|
|
2118
|
+
}),
|
|
2119
|
+
}),
|
|
2120
|
+
expect.any(Object),
|
|
2121
|
+
);
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
it('should apply schema transformation with Chat Completions API', async () => {
|
|
2125
|
+
const mockResponse = {
|
|
2126
|
+
choices: [
|
|
2127
|
+
{
|
|
2128
|
+
message: {
|
|
2129
|
+
content: '{"name":"Bob","age":25}',
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2132
|
+
],
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
vi.spyOn(instanceWithSchemaHandler['client'].chat.completions, 'create').mockResolvedValue(
|
|
2136
|
+
mockResponse as any,
|
|
2137
|
+
);
|
|
2138
|
+
|
|
2139
|
+
const payload = {
|
|
2140
|
+
messages: [{ content: 'Extract person', role: 'user' as const }],
|
|
2141
|
+
model: 'test-model',
|
|
2142
|
+
schema: {
|
|
2143
|
+
name: 'person',
|
|
2144
|
+
schema: {
|
|
2145
|
+
maxLength: 100,
|
|
2146
|
+
pattern: '^[a-z]+$',
|
|
2147
|
+
properties: {
|
|
2148
|
+
age: { type: 'number' },
|
|
2149
|
+
name: { type: 'string' },
|
|
2150
|
+
},
|
|
2151
|
+
type: 'object' as const,
|
|
2152
|
+
},
|
|
2153
|
+
},
|
|
2154
|
+
};
|
|
2155
|
+
|
|
2156
|
+
await instanceWithSchemaHandler.generateObject(payload);
|
|
2157
|
+
|
|
2158
|
+
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
|
|
2159
|
+
expect(instanceWithSchemaHandler['client'].chat.completions.create).toHaveBeenCalledWith(
|
|
2160
|
+
expect.objectContaining({
|
|
2161
|
+
response_format: expect.objectContaining({
|
|
2162
|
+
json_schema: expect.objectContaining({
|
|
2163
|
+
schema: {
|
|
2164
|
+
properties: {
|
|
2165
|
+
age: { type: 'number' },
|
|
2166
|
+
name: { type: 'string' },
|
|
2167
|
+
},
|
|
2168
|
+
type: 'object',
|
|
2169
|
+
},
|
|
2170
|
+
}),
|
|
2171
|
+
}),
|
|
2172
|
+
}),
|
|
2173
|
+
expect.any(Object),
|
|
2174
|
+
);
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
it('should apply schema transformation with tool calling fallback', async () => {
|
|
2178
|
+
const RuntimeClass = createOpenAICompatibleRuntime({
|
|
2179
|
+
baseURL: 'https://api.test.com',
|
|
2180
|
+
generateObject: {
|
|
2181
|
+
handleSchema: mockSchemaHandler,
|
|
2182
|
+
useToolsCalling: true,
|
|
2183
|
+
},
|
|
2184
|
+
provider: 'test-provider',
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
const instance = new RuntimeClass({ apiKey: 'test-key' });
|
|
2188
|
+
|
|
2189
|
+
const mockResponse = {
|
|
2190
|
+
choices: [
|
|
2191
|
+
{
|
|
2192
|
+
message: {
|
|
2193
|
+
tool_calls: [
|
|
2194
|
+
{
|
|
2195
|
+
function: {
|
|
2196
|
+
arguments: '{"name":"Charlie","age":35}',
|
|
2197
|
+
name: 'person',
|
|
2198
|
+
},
|
|
2199
|
+
type: 'function' as const,
|
|
2200
|
+
},
|
|
2201
|
+
],
|
|
2202
|
+
},
|
|
2203
|
+
},
|
|
2204
|
+
],
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
|
2208
|
+
mockResponse as any,
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
const payload = {
|
|
2212
|
+
messages: [{ content: 'Extract person', role: 'user' as const }],
|
|
2213
|
+
model: 'test-model',
|
|
2214
|
+
schema: {
|
|
2215
|
+
name: 'person',
|
|
2216
|
+
schema: {
|
|
2217
|
+
maxLength: 100,
|
|
2218
|
+
pattern: '^[a-z]+$',
|
|
2219
|
+
properties: {
|
|
2220
|
+
age: { type: 'number' },
|
|
2221
|
+
name: { type: 'string' },
|
|
2222
|
+
},
|
|
2223
|
+
type: 'object' as const,
|
|
2224
|
+
},
|
|
2225
|
+
},
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
await instance.generateObject(payload);
|
|
2229
|
+
|
|
2230
|
+
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
|
|
2231
|
+
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
|
|
2232
|
+
expect.objectContaining({
|
|
2233
|
+
tools: [
|
|
2234
|
+
expect.objectContaining({
|
|
2235
|
+
function: expect.objectContaining({
|
|
2236
|
+
parameters: {
|
|
2237
|
+
properties: {
|
|
2238
|
+
age: { type: 'number' },
|
|
2239
|
+
name: { type: 'string' },
|
|
2240
|
+
},
|
|
2241
|
+
type: 'object',
|
|
2242
|
+
},
|
|
2243
|
+
}),
|
|
2244
|
+
}),
|
|
2245
|
+
],
|
|
2246
|
+
}),
|
|
2247
|
+
expect.any(Object),
|
|
2248
|
+
);
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
it('should not apply schema transformation when handleSchema is not configured', async () => {
|
|
2252
|
+
const RuntimeClass = createOpenAICompatibleRuntime({
|
|
2253
|
+
baseURL: 'https://api.test.com',
|
|
2254
|
+
provider: 'test-provider',
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
const instance = new RuntimeClass({ apiKey: 'test-key' });
|
|
2258
|
+
|
|
2259
|
+
const mockResponse = {
|
|
2260
|
+
choices: [
|
|
2261
|
+
{
|
|
2262
|
+
message: {
|
|
2263
|
+
content: '{"name":"Test"}',
|
|
2264
|
+
},
|
|
2265
|
+
},
|
|
2266
|
+
],
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
|
|
2270
|
+
mockResponse as any,
|
|
2271
|
+
);
|
|
2272
|
+
|
|
2273
|
+
const payload = {
|
|
2274
|
+
messages: [{ content: 'Extract data', role: 'user' as const }],
|
|
2275
|
+
model: 'test-model',
|
|
2276
|
+
schema: {
|
|
2277
|
+
name: 'test',
|
|
2278
|
+
schema: {
|
|
2279
|
+
maxLength: 100,
|
|
2280
|
+
properties: {
|
|
2281
|
+
name: { type: 'string' },
|
|
2282
|
+
},
|
|
2283
|
+
type: 'object' as const,
|
|
2284
|
+
},
|
|
2285
|
+
},
|
|
2286
|
+
};
|
|
2287
|
+
|
|
2288
|
+
await instance.generateObject(payload);
|
|
2289
|
+
|
|
2290
|
+
expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
|
|
2291
|
+
expect.objectContaining({
|
|
2292
|
+
response_format: expect.objectContaining({
|
|
2293
|
+
json_schema: expect.objectContaining({
|
|
2294
|
+
schema: {
|
|
2295
|
+
maxLength: 100,
|
|
2296
|
+
properties: {
|
|
2297
|
+
name: { type: 'string' },
|
|
2298
|
+
},
|
|
2299
|
+
type: 'object',
|
|
2300
|
+
},
|
|
2301
|
+
}),
|
|
2302
|
+
}),
|
|
2303
|
+
}),
|
|
2304
|
+
expect.any(Object),
|
|
2305
|
+
);
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
it('should preserve original schema properties while filtering', async () => {
|
|
2309
|
+
const mockResponse = {
|
|
2310
|
+
output_text: '{"result":"success"}',
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
vi.spyOn(instanceWithSchemaHandler['client'].responses, 'create').mockResolvedValue(
|
|
2314
|
+
mockResponse as any,
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
const payload = {
|
|
2318
|
+
messages: [{ content: 'Test', role: 'user' as const }],
|
|
2319
|
+
model: 'gpt-4o',
|
|
2320
|
+
responseApi: true,
|
|
2321
|
+
schema: {
|
|
2322
|
+
description: 'Test schema',
|
|
2323
|
+
name: 'test',
|
|
2324
|
+
schema: {
|
|
2325
|
+
description: 'Inner schema description',
|
|
2326
|
+
maxLength: 100,
|
|
2327
|
+
pattern: '^test$',
|
|
2328
|
+
properties: {
|
|
2329
|
+
result: { type: 'string' },
|
|
2330
|
+
},
|
|
2331
|
+
required: ['result'],
|
|
2332
|
+
type: 'object' as const,
|
|
2333
|
+
},
|
|
2334
|
+
strict: true,
|
|
2335
|
+
},
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
await instanceWithSchemaHandler.generateObject(payload);
|
|
2339
|
+
|
|
2340
|
+
expect(mockSchemaHandler).toHaveBeenCalledWith(payload.schema.schema);
|
|
2341
|
+
expect(instanceWithSchemaHandler['client'].responses.create).toHaveBeenCalledWith(
|
|
2342
|
+
expect.objectContaining({
|
|
2343
|
+
text: expect.objectContaining({
|
|
2344
|
+
format: expect.objectContaining({
|
|
2345
|
+
description: 'Test schema',
|
|
2346
|
+
name: 'test',
|
|
2347
|
+
schema: {
|
|
2348
|
+
description: 'Inner schema description',
|
|
2349
|
+
properties: {
|
|
2350
|
+
result: { type: 'string' },
|
|
2351
|
+
},
|
|
2352
|
+
required: ['result'],
|
|
2353
|
+
type: 'object',
|
|
2354
|
+
},
|
|
2355
|
+
strict: true,
|
|
2356
|
+
}),
|
|
2357
|
+
}),
|
|
2358
|
+
}),
|
|
2359
|
+
expect.any(Object),
|
|
2360
|
+
);
|
|
2361
|
+
});
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2051
2364
|
describe('tool calling fallback', () => {
|
|
2052
2365
|
let instanceWithToolCalling: any;
|
|
2053
2366
|
|
|
@@ -119,6 +119,10 @@ export interface OpenAICompatibleFactoryOptions<T extends Record<string, any> =
|
|
|
119
119
|
invalidAPIKey: ILobeAgentRuntimeErrorType;
|
|
120
120
|
};
|
|
121
121
|
generateObject?: {
|
|
122
|
+
/**
|
|
123
|
+
* Transform schema before sending to the provider (e.g., filter unsupported properties)
|
|
124
|
+
*/
|
|
125
|
+
handleSchema?: (schema: any) => any;
|
|
122
126
|
/**
|
|
123
127
|
* If true, route generateObject requests to Responses API path directly
|
|
124
128
|
*/
|
|
@@ -454,12 +458,19 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
454
458
|
// Use tool calling fallback if configured
|
|
455
459
|
if (generateObjectConfig?.useToolsCalling) {
|
|
456
460
|
log('using tool calling fallback for structured output');
|
|
461
|
+
|
|
462
|
+
// Apply schema transformation if configured
|
|
463
|
+
const processedSchema = generateObjectConfig.handleSchema
|
|
464
|
+
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
|
|
465
|
+
: schema;
|
|
466
|
+
|
|
457
467
|
const tool: ChatCompletionTool = {
|
|
458
468
|
function: {
|
|
459
469
|
description:
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
470
|
+
processedSchema.description ||
|
|
471
|
+
'Generate structured output according to the provided schema',
|
|
472
|
+
name: processedSchema.name || 'structured_output',
|
|
473
|
+
parameters: processedSchema.schema,
|
|
463
474
|
},
|
|
464
475
|
type: 'function',
|
|
465
476
|
};
|
|
@@ -531,13 +542,18 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
531
542
|
return false;
|
|
532
543
|
})();
|
|
533
544
|
|
|
545
|
+
// Apply schema transformation if configured
|
|
546
|
+
const processedSchema = generateObjectConfig?.handleSchema
|
|
547
|
+
? { ...schema, schema: generateObjectConfig.handleSchema(schema.schema) }
|
|
548
|
+
: schema;
|
|
549
|
+
|
|
534
550
|
if (shouldUseResponses) {
|
|
535
551
|
log('calling responses.create for structured output');
|
|
536
552
|
const res = await this.client!.responses.create(
|
|
537
553
|
{
|
|
538
554
|
input: messages,
|
|
539
555
|
model,
|
|
540
|
-
text: { format: { strict: true, type: 'json_schema', ...
|
|
556
|
+
text: { format: { strict: true, type: 'json_schema', ...processedSchema } },
|
|
541
557
|
user: options?.user,
|
|
542
558
|
},
|
|
543
559
|
{ headers: options?.headers, signal: options?.signal },
|
|
@@ -561,7 +577,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
561
577
|
{
|
|
562
578
|
messages,
|
|
563
579
|
model,
|
|
564
|
-
response_format: { json_schema:
|
|
580
|
+
response_format: { json_schema: processedSchema, type: 'json_schema' },
|
|
565
581
|
user: options?.user,
|
|
566
582
|
},
|
|
567
583
|
{ headers: options?.headers, signal: options?.signal },
|