@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.
- package/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/docs/development/basic/chat-api.mdx +0 -1
- package/docs/development/basic/chat-api.zh-CN.mdx +0 -1
- package/package.json +1 -1
- package/packages/model-runtime/src/core/BaseAI.ts +0 -2
- package/packages/model-runtime/src/core/ModelRuntime.test.ts +0 -37
- package/packages/model-runtime/src/core/ModelRuntime.ts +0 -5
- package/packages/model-runtime/src/core/RouterRuntime/baseRuntimeMap.ts +4 -0
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.test.ts +325 -200
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +205 -64
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +0 -14
- package/packages/model-runtime/src/providers/aihubmix/index.test.ts +14 -20
- package/packages/model-runtime/src/types/index.ts +0 -1
- package/packages/model-runtime/src/utils/createError.test.ts +0 -20
- package/packages/model-runtime/src/utils/createError.ts +0 -1
- package/src/app/(backend)/market/agent/[[...segments]]/route.ts +3 -33
- package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +5 -6
- package/src/app/(backend)/market/social/[[...segments]]/route.ts +5 -52
- package/src/app/(backend)/market/user/[username]/route.ts +3 -9
- package/src/app/(backend)/market/user/me/route.ts +3 -34
- package/src/features/ChatMiniMap/useMinimapData.ts +1 -1
- package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +20 -2
- package/src/features/Conversation/store/slices/virtuaList/action.ts +9 -0
- package/src/libs/trpc/lambda/middleware/marketSDK.ts +14 -23
- package/src/libs/trusted-client/index.ts +1 -1
- package/src/server/routers/lambda/market/index.ts +5 -0
- package/src/server/routers/lambda/market/oidc.ts +41 -61
- package/src/server/routers/tools/market.ts +12 -44
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +7 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +1 -1
- package/src/server/services/aiAgent/__tests__/execAgent.threadId.test.ts +7 -0
- package/src/server/services/aiAgent/__tests__/execGroupSubAgentTask.test.ts +7 -0
- package/src/server/services/aiAgent/index.ts +9 -96
- package/src/server/services/discover/index.ts +11 -16
- package/src/server/services/market/index.ts +485 -0
- package/src/server/services/toolExecution/builtin.ts +11 -17
- package/src/server/services/toolExecution/index.ts +6 -2
- package/src/services/codeInterpreter.ts +0 -13
- package/packages/model-runtime/src/types/textToImage.ts +0 -36
- 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(
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
});
|