@lobehub/lobehub 2.0.0-next.310 → 2.0.0-next.311

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 (42) hide show
  1. package/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/docs/development/basic/chat-api.mdx +0 -1
  5. package/docs/development/basic/chat-api.zh-CN.mdx +0 -1
  6. package/package.json +1 -1
  7. package/packages/model-runtime/src/core/BaseAI.ts +0 -2
  8. package/packages/model-runtime/src/core/ModelRuntime.test.ts +0 -37
  9. package/packages/model-runtime/src/core/ModelRuntime.ts +0 -5
  10. package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +4 -0
  11. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.test.ts +325 -200
  12. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +205 -64
  13. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +0 -14
  14. package/packages/model-runtime/src/providers/aihubmix/index.test.ts +14 -20
  15. package/packages/model-runtime/src/types/index.ts +0 -1
  16. package/packages/model-runtime/src/utils/createError.test.ts +0 -20
  17. package/packages/model-runtime/src/utils/createError.ts +0 -1
  18. package/src/app/(backend)/market/agent/[[...segments]]/route.ts +3 -33
  19. package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +5 -6
  20. package/src/app/(backend)/market/social/[[...segments]]/route.ts +5 -52
  21. package/src/app/(backend)/market/user/[username]/route.ts +3 -9
  22. package/src/app/(backend)/market/user/me/route.ts +3 -34
  23. package/src/features/ChatMiniMap/useMinimapData.ts +1 -1
  24. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +20 -2
  25. package/src/features/Conversation/store/slices/virtuaList/action.ts +9 -0
  26. package/src/libs/trpc/lambda/middleware/marketSDK.ts +14 -23
  27. package/src/libs/trusted-client/index.ts +1 -1
  28. package/src/server/routers/lambda/market/index.ts +5 -0
  29. package/src/server/routers/lambda/market/oidc.ts +41 -61
  30. package/src/server/routers/tools/market.ts +12 -44
  31. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +7 -0
  32. package/src/server/services/agentRuntime/AgentRuntimeService.ts +1 -1
  33. package/src/server/services/aiAgent/__tests__/execAgent.threadId.test.ts +7 -0
  34. package/src/server/services/aiAgent/__tests__/execGroupSubAgentTask.test.ts +7 -0
  35. package/src/server/services/aiAgent/index.ts +9 -96
  36. package/src/server/services/discover/index.ts +11 -16
  37. package/src/server/services/market/index.ts +485 -0
  38. package/src/server/services/toolExecution/builtin.ts +11 -17
  39. package/src/server/services/toolExecution/index.ts +6 -2
  40. package/src/services/codeInterpreter.ts +0 -13
  41. package/packages/model-runtime/src/types/textToImage.ts +0 -36
  42. package/src/server/services/lobehubSkill/index.ts +0 -109
@@ -17,13 +17,14 @@ describe('createRouterRuntime', () => {
17
17
  const runtime = new Runtime();
18
18
 
19
19
  // 现在错误在使用时才抛出,因为是延迟创建
20
- await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
20
+ await expect(
21
+ runtime.chat({ model: 'test-model', messages: [], temperature: 0.7 }),
22
+ ).rejects.toThrow('empty providers');
21
23
  });
22
24
 
23
25
  it('should create UniformRuntime class with valid routers', () => {
24
26
  class MockRuntime implements LobeRuntimeAI {
25
27
  chat = vi.fn();
26
- textToImage = vi.fn();
27
28
  models = vi.fn();
28
29
  embeddings = vi.fn();
29
30
  textToSpeech = vi.fn();
@@ -53,7 +54,6 @@ describe('createRouterRuntime', () => {
53
54
  mockConstructor(options);
54
55
  }
55
56
  chat = vi.fn();
56
- textToImage = vi.fn();
57
57
  models = vi.fn();
58
58
  embeddings = vi.fn();
59
59
  textToSpeech = vi.fn();
@@ -74,7 +74,7 @@ describe('createRouterRuntime', () => {
74
74
  const runtime = new Runtime({ apiKey: 'constructor-key' });
75
75
 
76
76
  // 触发 runtime 创建
77
- await runtime.getRuntimeByModel('test-model');
77
+ await runtime.chat({ model: 'test-model', messages: [], temperature: 0.7 });
78
78
 
79
79
  expect(mockConstructor).toHaveBeenCalledWith(
80
80
  expect.objectContaining({
@@ -86,169 +86,6 @@ describe('createRouterRuntime', () => {
86
86
  });
87
87
  });
88
88
 
89
- describe('model matching', () => {
90
- it('should return correct runtime for matching model', async () => {
91
- const mockRuntime = {
92
- chat: vi.fn(),
93
- textToImage: vi.fn(),
94
- models: vi.fn(),
95
- embeddings: vi.fn(),
96
- textToSpeech: vi.fn(),
97
- } as unknown as LobeRuntimeAI;
98
-
99
- const Runtime = createRouterRuntime({
100
- id: 'test-runtime',
101
- routers: [
102
- {
103
- apiType: 'openai',
104
- options: {},
105
- runtime: mockRuntime.constructor as any,
106
- models: ['model-1', 'model-2'],
107
- },
108
- ],
109
- });
110
-
111
- const runtime = new Runtime();
112
- const selectedRuntime = await runtime.getRuntimeByModel('model-1');
113
-
114
- expect(selectedRuntime).toBeDefined();
115
- });
116
-
117
- it('should support dynamic routers with asynchronous model fetching', async () => {
118
- const mockRuntime = {
119
- chat: vi.fn(),
120
- textToImage: vi.fn(),
121
- models: vi.fn(),
122
- embeddings: vi.fn(),
123
- textToSpeech: vi.fn(),
124
- } as unknown as LobeRuntimeAI;
125
-
126
- const mockModelsFunction = vi.fn().mockResolvedValue(['async-model-1', 'async-model-2']);
127
-
128
- const Runtime = createRouterRuntime({
129
- id: 'test-runtime',
130
- routers: async () => {
131
- // 异步获取模型列表
132
- const models = await mockModelsFunction();
133
- return [
134
- {
135
- apiType: 'openai',
136
- options: {},
137
- runtime: mockRuntime.constructor as any,
138
- models, // 静态数组
139
- },
140
- ];
141
- },
142
- });
143
-
144
- const runtime = new Runtime();
145
-
146
- // 触发 routers 函数调用
147
- const selectedRuntime = await runtime.getRuntimeByModel('async-model-1');
148
-
149
- expect(selectedRuntime).toBeDefined();
150
- expect(mockModelsFunction).toHaveBeenCalled();
151
- });
152
-
153
- it('should return fallback runtime when model not found', async () => {
154
- const mockRuntime = {
155
- chat: vi.fn(),
156
- textToImage: vi.fn(),
157
- models: vi.fn(),
158
- embeddings: vi.fn(),
159
- textToSpeech: vi.fn(),
160
- } as unknown as LobeRuntimeAI;
161
-
162
- const Runtime = createRouterRuntime({
163
- id: 'test-runtime',
164
- routers: [
165
- {
166
- apiType: 'openai',
167
- options: {},
168
- runtime: mockRuntime.constructor as any,
169
- models: ['known-model'],
170
- },
171
- ],
172
- });
173
-
174
- const runtime = new Runtime();
175
- const selectedRuntime = await runtime.getRuntimeByModel('unknown-model');
176
-
177
- expect(selectedRuntime).toBeDefined();
178
- });
179
- });
180
-
181
- describe('getRuntimeByModel', () => {
182
- it('should return runtime that supports the model', async () => {
183
- class MockRuntime1 implements LobeRuntimeAI {
184
- chat = vi.fn();
185
- }
186
-
187
- class MockRuntime2 implements LobeRuntimeAI {
188
- chat = vi.fn();
189
- }
190
-
191
- const Runtime = createRouterRuntime({
192
- id: 'test-runtime',
193
- routers: [
194
- {
195
- apiType: 'openai',
196
- options: {},
197
- runtime: MockRuntime1 as any,
198
- models: ['gpt-4'],
199
- },
200
- {
201
- apiType: 'anthropic',
202
- options: {},
203
- runtime: MockRuntime2 as any,
204
- models: ['claude-3'],
205
- },
206
- ],
207
- });
208
-
209
- const runtime = new Runtime();
210
-
211
- const result1 = await runtime.getRuntimeByModel('gpt-4');
212
- expect(result1).toBeInstanceOf(MockRuntime1);
213
-
214
- const result2 = await runtime.getRuntimeByModel('claude-3');
215
- expect(result2).toBeInstanceOf(MockRuntime2);
216
- });
217
-
218
- it('should return last runtime when no model matches', async () => {
219
- class MockRuntime1 implements LobeRuntimeAI {
220
- chat = vi.fn();
221
- }
222
-
223
- class MockRuntime2 implements LobeRuntimeAI {
224
- chat = vi.fn();
225
- }
226
-
227
- const Runtime = createRouterRuntime({
228
- id: 'test-runtime',
229
- routers: [
230
- {
231
- apiType: 'openai',
232
- options: {},
233
- runtime: MockRuntime1 as any,
234
- models: ['gpt-4'],
235
- },
236
- {
237
- apiType: 'anthropic',
238
- options: {},
239
- runtime: MockRuntime2 as any,
240
- models: ['claude-3'],
241
- },
242
- ],
243
- });
244
-
245
- const runtime = new Runtime();
246
- const result = await runtime.getRuntimeByModel('unknown-model');
247
-
248
- expect(result).toBeInstanceOf(MockRuntime2);
249
- });
250
- });
251
-
252
89
  describe('chat method', () => {
253
90
  it('should call chat on the correct runtime based on model', async () => {
254
91
  const mockChat = vi.fn().mockResolvedValue('chat-response');
@@ -349,35 +186,6 @@ describe('createRouterRuntime', () => {
349
186
  });
350
187
  });
351
188
 
352
- describe('textToImage method', () => {
353
- it('should call textToImage on the correct runtime based on model', async () => {
354
- const mockTextToImage = vi.fn().mockResolvedValue('image-response');
355
-
356
- class MockRuntime implements LobeRuntimeAI {
357
- textToImage = mockTextToImage;
358
- }
359
-
360
- const Runtime = createRouterRuntime({
361
- id: 'test-runtime',
362
- routers: [
363
- {
364
- apiType: 'openai',
365
- options: {},
366
- runtime: MockRuntime as any,
367
- models: ['dall-e-3'],
368
- },
369
- ],
370
- });
371
-
372
- const runtime = new Runtime();
373
- const payload = { model: 'dall-e-3', prompt: 'test prompt' };
374
-
375
- const result = await runtime.textToImage(payload);
376
- expect(result).toBe('image-response');
377
- expect(mockTextToImage).toHaveBeenCalledWith(payload);
378
- });
379
- });
380
-
381
189
  describe('models method', () => {
382
190
  it('should call models method on first runtime', async () => {
383
191
  const mockModels = vi.fn().mockResolvedValue(['model-1', 'model-2']);
@@ -468,8 +276,7 @@ describe('createRouterRuntime', () => {
468
276
  describe('dynamic routers configuration', () => {
469
277
  it('should support function-based routers configuration', async () => {
470
278
  class MockRuntime implements LobeRuntimeAI {
471
- chat = vi.fn();
472
- textToImage = vi.fn();
279
+ chat = vi.fn().mockResolvedValue('chat-response');
473
280
  models = vi.fn();
474
281
  embeddings = vi.fn();
475
282
  textToSpeech = vi.fn();
@@ -509,7 +316,7 @@ describe('createRouterRuntime', () => {
509
316
  expect(runtime).toBeDefined();
510
317
 
511
318
  // 测试动态 routers 是否能正确工作
512
- const result = await runtime.getRuntimeByModel('gpt-4');
319
+ const result = await runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 });
513
320
  expect(result).toBeDefined();
514
321
 
515
322
  // 验证动态函数被调用时传入了正确的参数
@@ -532,7 +339,325 @@ describe('createRouterRuntime', () => {
532
339
  const runtime = new Runtime();
533
340
 
534
341
  // 现在错误在使用时才抛出,因为是延迟创建
535
- await expect(runtime.getRuntimeByModel('test-model')).rejects.toThrow('empty providers');
342
+ await expect(
343
+ runtime.chat({ model: 'test-model', messages: [], temperature: 0.7 }),
344
+ ).rejects.toThrow('empty providers');
345
+ });
346
+
347
+ it('should support async function-based routers configuration', async () => {
348
+ const mockChat = vi.fn().mockResolvedValue('async-chat-response');
349
+
350
+ class MockRuntime implements LobeRuntimeAI {
351
+ chat = mockChat;
352
+ }
353
+
354
+ const asyncRoutersFunction = vi.fn(async () => [
355
+ {
356
+ apiType: 'openai' as const,
357
+ options: { apiKey: 'async-key' },
358
+ runtime: MockRuntime as any,
359
+ models: ['gpt-4'],
360
+ },
361
+ ]);
362
+
363
+ const Runtime = createRouterRuntime({
364
+ id: 'test-runtime',
365
+ routers: asyncRoutersFunction,
366
+ });
367
+
368
+ const runtime = new Runtime();
369
+ const result = await runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 });
370
+
371
+ expect(result).toBe('async-chat-response');
372
+ expect(asyncRoutersFunction).toHaveBeenCalled();
373
+ });
374
+ });
375
+
376
+ describe('fallback mechanism', () => {
377
+ it('should fallback to next option when first option fails', async () => {
378
+ // Test that errors are caught and re-thrown when all options fail
379
+ const mockChatAlwaysFail = vi.fn().mockRejectedValue(new Error('All failed'));
380
+
381
+ class AlwaysFailRuntime implements LobeRuntimeAI {
382
+ chat = mockChatAlwaysFail;
383
+ }
384
+
385
+ const Runtime = createRouterRuntime({
386
+ id: 'test-runtime',
387
+ routers: [
388
+ {
389
+ apiType: 'openai',
390
+ options: [{ apiKey: 'key-1' }, { apiKey: 'key-2' }],
391
+ runtime: AlwaysFailRuntime as any,
392
+ models: ['gpt-4'],
393
+ },
394
+ ],
395
+ });
396
+
397
+ const runtime = new Runtime();
398
+ await expect(
399
+ runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 }),
400
+ ).rejects.toThrow('All failed');
401
+
402
+ // Verify chat was called twice (once per option)
403
+ expect(mockChatAlwaysFail).toHaveBeenCalledTimes(2);
404
+ });
405
+
406
+ it('should throw error when options array is empty', async () => {
407
+ const Runtime = createRouterRuntime({
408
+ id: 'test-runtime',
409
+ routers: [
410
+ {
411
+ apiType: 'openai',
412
+ options: [] as any,
413
+ models: ['gpt-4'],
414
+ },
415
+ ],
416
+ });
417
+
418
+ const runtime = new Runtime();
419
+ await expect(
420
+ runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 }),
421
+ ).rejects.toThrow('empty provider options');
422
+ });
423
+
424
+ it('should use apiType from option item when specified for fallback', async () => {
425
+ const constructorCalls: any[] = [];
426
+
427
+ class MockRuntime implements LobeRuntimeAI {
428
+ constructor(options: any) {
429
+ constructorCalls.push(options);
430
+ }
431
+ chat = vi.fn().mockResolvedValue('response');
432
+ }
433
+
434
+ const Runtime = createRouterRuntime({
435
+ id: 'test-runtime',
436
+ routers: [
437
+ {
438
+ apiType: 'openai',
439
+ options: [{ apiKey: 'openai-key' }, { apiKey: 'anthropic-key', apiType: 'anthropic' }],
440
+ runtime: MockRuntime as any,
441
+ models: ['gpt-4'],
442
+ },
443
+ ],
444
+ });
445
+
446
+ const runtime = new Runtime();
447
+ await runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 });
448
+
449
+ // First option should be tried
450
+ expect(constructorCalls.length).toBeGreaterThanOrEqual(1);
451
+ });
452
+ });
453
+
454
+ describe('router matching', () => {
455
+ it('should fallback to last router when model does not match any', async () => {
456
+ const mockChatFirst = vi.fn().mockResolvedValue('first-response');
457
+ const mockChatLast = vi.fn().mockResolvedValue('last-response');
458
+
459
+ class FirstRuntime implements LobeRuntimeAI {
460
+ chat = mockChatFirst;
461
+ }
462
+
463
+ class LastRuntime implements LobeRuntimeAI {
464
+ chat = mockChatLast;
465
+ }
466
+
467
+ const Runtime = createRouterRuntime({
468
+ id: 'test-runtime',
469
+ routers: [
470
+ {
471
+ apiType: 'openai',
472
+ options: { apiKey: 'first-key' },
473
+ runtime: FirstRuntime as any,
474
+ models: ['gpt-4'],
475
+ },
476
+ {
477
+ apiType: 'anthropic',
478
+ options: { apiKey: 'last-key' },
479
+ runtime: LastRuntime as any,
480
+ models: ['claude-3'],
481
+ },
482
+ ],
483
+ });
484
+
485
+ const runtime = new Runtime();
486
+ // Use a model that doesn't match any router
487
+ const result = await runtime.chat({
488
+ model: 'unknown-model',
489
+ messages: [],
490
+ temperature: 0.7,
491
+ });
492
+
493
+ expect(result).toBe('last-response');
494
+ expect(mockChatLast).toHaveBeenCalled();
495
+ expect(mockChatFirst).not.toHaveBeenCalled();
496
+ });
497
+
498
+ it('should match router with empty models array as fallback', async () => {
499
+ const mockChatSpecific = vi.fn().mockResolvedValue('specific-response');
500
+ const mockChatFallback = vi.fn().mockResolvedValue('fallback-response');
501
+
502
+ class SpecificRuntime implements LobeRuntimeAI {
503
+ chat = mockChatSpecific;
504
+ }
505
+
506
+ class FallbackRuntime implements LobeRuntimeAI {
507
+ chat = mockChatFallback;
508
+ }
509
+
510
+ const Runtime = createRouterRuntime({
511
+ id: 'test-runtime',
512
+ routers: [
513
+ {
514
+ apiType: 'openai',
515
+ options: { apiKey: 'specific-key' },
516
+ runtime: SpecificRuntime as any,
517
+ models: ['gpt-4'],
518
+ },
519
+ {
520
+ apiType: 'openai',
521
+ options: { apiKey: 'fallback-key' },
522
+ runtime: FallbackRuntime as any,
523
+ models: [], // Empty models array acts as catch-all
524
+ },
525
+ ],
526
+ });
527
+
528
+ const runtime = new Runtime();
529
+ const result = await runtime.chat({
530
+ model: 'any-model',
531
+ messages: [],
532
+ temperature: 0.7,
533
+ });
534
+
535
+ expect(result).toBe('fallback-response');
536
+ });
537
+ });
538
+
539
+ describe('createImage method', () => {
540
+ it('should call createImage on the correct runtime', async () => {
541
+ const mockCreateImage = vi
542
+ .fn()
543
+ .mockResolvedValue({ imageUrl: 'https://example.com/image.png' });
544
+
545
+ class MockRuntime implements LobeRuntimeAI {
546
+ createImage = mockCreateImage;
547
+ }
548
+
549
+ const Runtime = createRouterRuntime({
550
+ id: 'test-runtime',
551
+ routers: [
552
+ {
553
+ apiType: 'openai',
554
+ options: {},
555
+ runtime: MockRuntime as any,
556
+ models: ['gpt-image-1'],
557
+ },
558
+ ],
559
+ });
560
+
561
+ const runtime = new Runtime();
562
+ const payload = { model: 'gpt-image-1', params: { prompt: 'a cat' } };
563
+
564
+ const result = await runtime.createImage(payload);
565
+ expect(result).toEqual({ imageUrl: 'https://example.com/image.png' });
566
+ expect(mockCreateImage).toHaveBeenCalledWith(payload);
567
+ });
568
+ });
569
+
570
+ describe('generateObject method', () => {
571
+ it('should call generateObject on the correct runtime', async () => {
572
+ const mockGenerateObject = vi.fn().mockResolvedValue({ name: 'test' });
573
+
574
+ class MockRuntime implements LobeRuntimeAI {
575
+ generateObject = mockGenerateObject;
576
+ }
577
+
578
+ const Runtime = createRouterRuntime({
579
+ id: 'test-runtime',
580
+ routers: [
581
+ {
582
+ apiType: 'openai',
583
+ options: {},
584
+ runtime: MockRuntime as any,
585
+ models: ['gpt-4'],
586
+ },
587
+ ],
588
+ });
589
+
590
+ const runtime = new Runtime();
591
+ const payload = { model: 'gpt-4', messages: [{ role: 'user' as const, content: 'test' }] };
592
+ const options = { user: 'test-user' };
593
+
594
+ const result = await runtime.generateObject(payload, options);
595
+ expect(result).toEqual({ name: 'test' });
596
+ expect(mockGenerateObject).toHaveBeenCalledWith(payload, options);
597
+ });
598
+ });
599
+
600
+ describe('constructor options handling', () => {
601
+ it('should trim apiKey and baseURL', async () => {
602
+ const constructorOptions: any[] = [];
603
+
604
+ class MockRuntime implements LobeRuntimeAI {
605
+ constructor(options: any) {
606
+ constructorOptions.push(options);
607
+ }
608
+ chat = vi.fn().mockResolvedValue('response');
609
+ }
610
+
611
+ const Runtime = createRouterRuntime({
612
+ id: 'test-runtime',
613
+ routers: [
614
+ {
615
+ apiType: 'openai',
616
+ options: {},
617
+ runtime: MockRuntime as any,
618
+ models: ['gpt-4'],
619
+ },
620
+ ],
621
+ });
622
+
623
+ const runtime = new Runtime({
624
+ apiKey: ' trimmed-key ',
625
+ baseURL: ' https://api.example.com ',
626
+ });
627
+
628
+ await runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 });
629
+
630
+ expect(constructorOptions[0].apiKey).toBe('trimmed-key');
631
+ expect(constructorOptions[0].baseURL).toBe('https://api.example.com');
632
+ });
633
+
634
+ it('should use default apiKey when not provided', async () => {
635
+ const constructorOptions: any[] = [];
636
+
637
+ class MockRuntime implements LobeRuntimeAI {
638
+ constructor(options: any) {
639
+ constructorOptions.push(options);
640
+ }
641
+ chat = vi.fn().mockResolvedValue('response');
642
+ }
643
+
644
+ const Runtime = createRouterRuntime({
645
+ id: 'test-runtime',
646
+ apiKey: 'default-api-key',
647
+ routers: [
648
+ {
649
+ apiType: 'openai',
650
+ options: {},
651
+ runtime: MockRuntime as any,
652
+ models: ['gpt-4'],
653
+ },
654
+ ],
655
+ });
656
+
657
+ const runtime = new Runtime();
658
+ await runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 });
659
+
660
+ expect(constructorOptions[0].apiKey).toBe('default-api-key');
536
661
  });
537
662
  });
538
663
  });