@lobehub/chat 1.108.1 → 1.109.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.
Files changed (28) hide show
  1. package/.cursor/rules/testing-guide/testing-guide.mdc +18 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +18 -0
  4. package/package.json +2 -2
  5. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +15 -2
  6. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +30 -3
  7. package/src/components/Analytics/LobeAnalyticsProvider.tsx +10 -13
  8. package/src/components/Analytics/LobeAnalyticsProviderWrapper.tsx +16 -4
  9. package/src/config/aiModels/ollama.ts +27 -0
  10. package/src/libs/model-runtime/RouterRuntime/createRuntime.test.ts +538 -0
  11. package/src/libs/model-runtime/RouterRuntime/createRuntime.ts +50 -13
  12. package/src/libs/model-runtime/RouterRuntime/index.ts +1 -1
  13. package/src/libs/model-runtime/aihubmix/index.ts +10 -5
  14. package/src/libs/model-runtime/ppio/index.test.ts +3 -6
  15. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +8 -6
  16. package/src/server/globalConfig/genServerAiProviderConfig.test.ts +22 -25
  17. package/src/server/globalConfig/genServerAiProviderConfig.ts +34 -22
  18. package/src/server/globalConfig/index.ts +1 -1
  19. package/src/server/services/discover/index.ts +11 -2
  20. package/src/services/chat.ts +1 -1
  21. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +211 -0
  22. package/src/store/aiInfra/slices/aiProvider/action.ts +46 -35
  23. package/src/store/user/slices/modelList/action.test.ts +5 -5
  24. package/src/store/user/slices/modelList/action.ts +4 -4
  25. package/src/utils/getFallbackModelProperty.test.ts +52 -45
  26. package/src/utils/getFallbackModelProperty.ts +4 -3
  27. package/src/utils/parseModels.test.ts +107 -98
  28. package/src/utils/parseModels.ts +10 -8
@@ -0,0 +1,538 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { LobeRuntimeAI } from '../BaseAI';
4
+ import { createRouterRuntime } from './createRuntime';
5
+
6
+ describe('createRouterRuntime', () => {
7
+ beforeEach(() => {
8
+ vi.resetModules();
9
+ });
10
+
11
+ describe('initialization', () => {
12
+ it('should throw error when routers array is empty', () => {
13
+ expect(() => {
14
+ const Runtime = createRouterRuntime({
15
+ id: 'test-runtime',
16
+ routers: [],
17
+ });
18
+ new Runtime();
19
+ }).toThrow('empty providers');
20
+ });
21
+
22
+ it('should create UniformRuntime class with valid routers', () => {
23
+ class MockRuntime implements LobeRuntimeAI {
24
+ chat = vi.fn();
25
+ textToImage = vi.fn();
26
+ models = vi.fn();
27
+ embeddings = vi.fn();
28
+ textToSpeech = vi.fn();
29
+ }
30
+
31
+ const Runtime = createRouterRuntime({
32
+ id: 'test-runtime',
33
+ routers: [
34
+ {
35
+ apiType: 'openai',
36
+ options: { apiKey: 'test-key' },
37
+ runtime: MockRuntime as any,
38
+ models: ['gpt-4', 'gpt-3.5-turbo'],
39
+ },
40
+ ],
41
+ });
42
+
43
+ const runtime = new Runtime();
44
+ expect(runtime).toBeDefined();
45
+ });
46
+
47
+ it('should merge router options with constructor options', () => {
48
+ const mockConstructor = vi.fn();
49
+
50
+ class MockRuntime implements LobeRuntimeAI {
51
+ constructor(options: any) {
52
+ mockConstructor(options);
53
+ }
54
+ chat = vi.fn();
55
+ }
56
+
57
+ const Runtime = createRouterRuntime({
58
+ id: 'test-runtime',
59
+ routers: [
60
+ {
61
+ apiType: 'openai',
62
+ options: { baseURL: 'https://api.example.com' },
63
+ runtime: MockRuntime as any,
64
+ },
65
+ ],
66
+ });
67
+
68
+ new Runtime({ apiKey: 'constructor-key' });
69
+
70
+ expect(mockConstructor).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ baseURL: 'https://api.example.com',
73
+ apiKey: 'constructor-key',
74
+ id: 'test-runtime',
75
+ }),
76
+ );
77
+ });
78
+ });
79
+
80
+ describe('getModels', () => {
81
+ it('should return synchronous models array directly', async () => {
82
+ const mockRuntime = {
83
+ chat: vi.fn(),
84
+ } as unknown as LobeRuntimeAI;
85
+
86
+ const Runtime = createRouterRuntime({
87
+ id: 'test-runtime',
88
+ routers: [
89
+ {
90
+ apiType: 'openai',
91
+ options: {},
92
+ runtime: mockRuntime.constructor as any,
93
+ models: ['model-1', 'model-2'],
94
+ },
95
+ ],
96
+ });
97
+
98
+ const runtime = new Runtime();
99
+ const models = await runtime['getModels']({
100
+ id: 'test',
101
+ models: ['model-1', 'model-2'],
102
+ runtime: mockRuntime,
103
+ });
104
+
105
+ expect(models).toEqual(['model-1', 'model-2']);
106
+ });
107
+
108
+ it('should call and cache asynchronous models function', async () => {
109
+ const mockRuntime = {
110
+ chat: vi.fn(),
111
+ } as unknown as LobeRuntimeAI;
112
+
113
+ const mockModelsFunction = vi.fn().mockResolvedValue(['async-model-1', 'async-model-2']);
114
+
115
+ const Runtime = createRouterRuntime({
116
+ id: 'test-runtime',
117
+ routers: [
118
+ {
119
+ apiType: 'openai',
120
+ options: {},
121
+ runtime: mockRuntime.constructor as any,
122
+ models: mockModelsFunction,
123
+ },
124
+ ],
125
+ });
126
+
127
+ const runtime = new Runtime();
128
+ const runtimeItem = {
129
+ id: 'test',
130
+ models: mockModelsFunction,
131
+ runtime: mockRuntime,
132
+ };
133
+
134
+ // First call
135
+ const models1 = await runtime['getModels'](runtimeItem);
136
+ expect(models1).toEqual(['async-model-1', 'async-model-2']);
137
+ expect(mockModelsFunction).toHaveBeenCalledTimes(1);
138
+
139
+ // Second call should use cache
140
+ const models2 = await runtime['getModels'](runtimeItem);
141
+ expect(models2).toEqual(['async-model-1', 'async-model-2']);
142
+ expect(mockModelsFunction).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ it('should return empty array when models is undefined', async () => {
146
+ const mockRuntime = {
147
+ chat: vi.fn(),
148
+ } as unknown as LobeRuntimeAI;
149
+
150
+ const Runtime = createRouterRuntime({
151
+ id: 'test-runtime',
152
+ routers: [
153
+ {
154
+ apiType: 'openai',
155
+ options: {},
156
+ runtime: mockRuntime.constructor as any,
157
+ },
158
+ ],
159
+ });
160
+
161
+ const runtime = new Runtime();
162
+ const models = await runtime['getModels']({
163
+ id: 'test',
164
+ runtime: mockRuntime,
165
+ });
166
+
167
+ expect(models).toEqual([]);
168
+ });
169
+ });
170
+
171
+ describe('getRuntimeByModel', () => {
172
+ it('should return runtime that supports the model', async () => {
173
+ class MockRuntime1 implements LobeRuntimeAI {
174
+ chat = vi.fn();
175
+ }
176
+
177
+ class MockRuntime2 implements LobeRuntimeAI {
178
+ chat = vi.fn();
179
+ }
180
+
181
+ const Runtime = createRouterRuntime({
182
+ id: 'test-runtime',
183
+ routers: [
184
+ {
185
+ apiType: 'openai',
186
+ options: {},
187
+ runtime: MockRuntime1 as any,
188
+ models: ['gpt-4'],
189
+ },
190
+ {
191
+ apiType: 'anthropic',
192
+ options: {},
193
+ runtime: MockRuntime2 as any,
194
+ models: ['claude-3'],
195
+ },
196
+ ],
197
+ });
198
+
199
+ const runtime = new Runtime();
200
+
201
+ const result1 = await runtime.getRuntimeByModel('gpt-4');
202
+ expect(result1).toBe(runtime['_runtimes'][0].runtime);
203
+
204
+ const result2 = await runtime.getRuntimeByModel('claude-3');
205
+ expect(result2).toBe(runtime['_runtimes'][1].runtime);
206
+ });
207
+
208
+ it('should return last runtime when no model matches', async () => {
209
+ class MockRuntime1 implements LobeRuntimeAI {
210
+ chat = vi.fn();
211
+ }
212
+
213
+ class MockRuntime2 implements LobeRuntimeAI {
214
+ chat = vi.fn();
215
+ }
216
+
217
+ const Runtime = createRouterRuntime({
218
+ id: 'test-runtime',
219
+ routers: [
220
+ {
221
+ apiType: 'openai',
222
+ options: {},
223
+ runtime: MockRuntime1 as any,
224
+ models: ['gpt-4'],
225
+ },
226
+ {
227
+ apiType: 'anthropic',
228
+ options: {},
229
+ runtime: MockRuntime2 as any,
230
+ models: ['claude-3'],
231
+ },
232
+ ],
233
+ });
234
+
235
+ const runtime = new Runtime();
236
+ const result = await runtime.getRuntimeByModel('unknown-model');
237
+
238
+ expect(result).toBe(runtime['_runtimes'][1].runtime);
239
+ });
240
+ });
241
+
242
+ describe('chat method', () => {
243
+ it('should call chat on the correct runtime based on model', async () => {
244
+ const mockChat = vi.fn().mockResolvedValue('chat-response');
245
+
246
+ class MockRuntime implements LobeRuntimeAI {
247
+ chat = mockChat;
248
+ }
249
+
250
+ const Runtime = createRouterRuntime({
251
+ id: 'test-runtime',
252
+ routers: [
253
+ {
254
+ apiType: 'openai',
255
+ options: {},
256
+ runtime: MockRuntime as any,
257
+ models: ['gpt-4'],
258
+ },
259
+ ],
260
+ });
261
+
262
+ const runtime = new Runtime();
263
+ const payload = { model: 'gpt-4', messages: [], temperature: 0.7 };
264
+
265
+ const result = await runtime.chat(payload);
266
+ expect(result).toBe('chat-response');
267
+ expect(mockChat).toHaveBeenCalledWith(payload, undefined);
268
+ });
269
+
270
+ it('should handle errors when provided with handleError', async () => {
271
+ const mockError = new Error('API Error');
272
+ const mockChat = vi.fn().mockRejectedValue(mockError);
273
+
274
+ class MockRuntime implements LobeRuntimeAI {
275
+ chat = mockChat;
276
+ }
277
+
278
+ const handleError = vi.fn().mockReturnValue({
279
+ errorType: 'APIError',
280
+ message: 'Handled error',
281
+ });
282
+
283
+ const Runtime = createRouterRuntime({
284
+ id: 'test-runtime',
285
+ routers: [
286
+ {
287
+ apiType: 'openai',
288
+ options: {},
289
+ runtime: MockRuntime as any,
290
+ models: ['gpt-4'],
291
+ },
292
+ ],
293
+ });
294
+
295
+ const runtime = new Runtime({
296
+ chat: {
297
+ handleError,
298
+ },
299
+ });
300
+
301
+ await expect(
302
+ runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 }),
303
+ ).rejects.toEqual({
304
+ errorType: 'APIError',
305
+ message: 'Handled error',
306
+ });
307
+ });
308
+
309
+ it('should re-throw original error when handleError returns undefined', async () => {
310
+ const mockError = new Error('API Error');
311
+ const mockChat = vi.fn().mockRejectedValue(mockError);
312
+
313
+ class MockRuntime implements LobeRuntimeAI {
314
+ chat = mockChat;
315
+ }
316
+
317
+ const handleError = vi.fn().mockReturnValue(undefined);
318
+
319
+ const Runtime = createRouterRuntime({
320
+ id: 'test-runtime',
321
+ routers: [
322
+ {
323
+ apiType: 'openai',
324
+ options: {},
325
+ runtime: MockRuntime as any,
326
+ models: ['gpt-4'],
327
+ },
328
+ ],
329
+ });
330
+
331
+ const runtime = new Runtime({
332
+ chat: {
333
+ handleError,
334
+ },
335
+ });
336
+
337
+ await expect(runtime.chat({ model: 'gpt-4', messages: [], temperature: 0.7 })).rejects.toBe(
338
+ mockError,
339
+ );
340
+ });
341
+ });
342
+
343
+ describe('textToImage method', () => {
344
+ it('should call textToImage on the correct runtime based on model', async () => {
345
+ const mockTextToImage = vi.fn().mockResolvedValue('image-response');
346
+
347
+ class MockRuntime implements LobeRuntimeAI {
348
+ textToImage = mockTextToImage;
349
+ }
350
+
351
+ const Runtime = createRouterRuntime({
352
+ id: 'test-runtime',
353
+ routers: [
354
+ {
355
+ apiType: 'openai',
356
+ options: {},
357
+ runtime: MockRuntime as any,
358
+ models: ['dall-e-3'],
359
+ },
360
+ ],
361
+ });
362
+
363
+ const runtime = new Runtime();
364
+ const payload = { model: 'dall-e-3', prompt: 'test prompt' };
365
+
366
+ const result = await runtime.textToImage(payload);
367
+ expect(result).toBe('image-response');
368
+ expect(mockTextToImage).toHaveBeenCalledWith(payload);
369
+ });
370
+ });
371
+
372
+ describe('models method', () => {
373
+ it('should call models method on first runtime', async () => {
374
+ const mockModels = vi.fn().mockResolvedValue(['model-1', 'model-2']);
375
+
376
+ class MockRuntime implements LobeRuntimeAI {
377
+ models = mockModels;
378
+ }
379
+
380
+ const Runtime = createRouterRuntime({
381
+ id: 'test-runtime',
382
+ routers: [
383
+ {
384
+ apiType: 'openai',
385
+ options: {},
386
+ runtime: MockRuntime as any,
387
+ },
388
+ ],
389
+ });
390
+
391
+ const runtime = new Runtime();
392
+ const result = await runtime.models();
393
+
394
+ expect(result).toEqual(['model-1', 'model-2']);
395
+ expect(mockModels).toHaveBeenCalled();
396
+ });
397
+ });
398
+
399
+ describe('embeddings method', () => {
400
+ it('should call embeddings on the correct runtime based on model', async () => {
401
+ const mockEmbeddings = vi.fn().mockResolvedValue('embeddings-response');
402
+
403
+ class MockRuntime implements LobeRuntimeAI {
404
+ embeddings = mockEmbeddings;
405
+ }
406
+
407
+ const Runtime = createRouterRuntime({
408
+ id: 'test-runtime',
409
+ routers: [
410
+ {
411
+ apiType: 'openai',
412
+ options: {},
413
+ runtime: MockRuntime as any,
414
+ models: ['text-embedding-ada-002'],
415
+ },
416
+ ],
417
+ });
418
+
419
+ const runtime = new Runtime();
420
+ const payload = { model: 'text-embedding-ada-002', input: 'test input' };
421
+ const options = {} as any;
422
+
423
+ const result = await runtime.embeddings(payload, options);
424
+ expect(result).toBe('embeddings-response');
425
+ expect(mockEmbeddings).toHaveBeenCalledWith(payload, options);
426
+ });
427
+ });
428
+
429
+ describe('textToSpeech method', () => {
430
+ it('should call textToSpeech on the correct runtime based on model', async () => {
431
+ const mockTextToSpeech = vi.fn().mockResolvedValue('speech-response');
432
+
433
+ class MockRuntime implements LobeRuntimeAI {
434
+ textToSpeech = mockTextToSpeech;
435
+ }
436
+
437
+ const Runtime = createRouterRuntime({
438
+ id: 'test-runtime',
439
+ routers: [
440
+ {
441
+ apiType: 'openai',
442
+ options: {},
443
+ runtime: MockRuntime as any,
444
+ models: ['tts-1'],
445
+ },
446
+ ],
447
+ });
448
+
449
+ const runtime = new Runtime();
450
+ const payload = { model: 'tts-1', input: 'Hello world', voice: 'alloy' };
451
+ const options = {} as any;
452
+
453
+ const result = await runtime.textToSpeech(payload, options);
454
+ expect(result).toBe('speech-response');
455
+ expect(mockTextToSpeech).toHaveBeenCalledWith(payload, options);
456
+ });
457
+ });
458
+
459
+ describe('clearModelCache method', () => {
460
+ it('should clear specific runtime cache when runtimeId provided', async () => {
461
+ const mockModelsFunction = vi.fn().mockResolvedValue(['model-1']);
462
+
463
+ const Runtime = createRouterRuntime({
464
+ id: 'test-runtime',
465
+ routers: [
466
+ {
467
+ apiType: 'openai',
468
+ options: {},
469
+ runtime: vi.fn() as any,
470
+ models: mockModelsFunction,
471
+ },
472
+ ],
473
+ });
474
+
475
+ const runtime = new Runtime();
476
+ const runtimeItem = {
477
+ id: 'test-id',
478
+ models: mockModelsFunction,
479
+ runtime: {} as any,
480
+ };
481
+
482
+ // Build cache
483
+ await runtime['getModels'](runtimeItem);
484
+ expect(mockModelsFunction).toHaveBeenCalledTimes(1);
485
+
486
+ // Clear specific cache
487
+ runtime.clearModelCache('test-id');
488
+
489
+ // Should call function again
490
+ await runtime['getModels'](runtimeItem);
491
+ expect(mockModelsFunction).toHaveBeenCalledTimes(2);
492
+ });
493
+
494
+ it('should clear all cache when no runtimeId provided', async () => {
495
+ const mockModelsFunction1 = vi.fn().mockResolvedValue(['model-1']);
496
+ const mockModelsFunction2 = vi.fn().mockResolvedValue(['model-2']);
497
+
498
+ const Runtime = createRouterRuntime({
499
+ id: 'test-runtime',
500
+ routers: [
501
+ {
502
+ apiType: 'openai',
503
+ options: {},
504
+ runtime: vi.fn() as any,
505
+ models: mockModelsFunction1,
506
+ },
507
+ ],
508
+ });
509
+
510
+ const runtime = new Runtime();
511
+ const runtimeItem1 = {
512
+ id: 'test-id-1',
513
+ models: mockModelsFunction1,
514
+ runtime: {} as any,
515
+ };
516
+ const runtimeItem2 = {
517
+ id: 'test-id-2',
518
+ models: mockModelsFunction2,
519
+ runtime: {} as any,
520
+ };
521
+
522
+ // Build cache for both items
523
+ await runtime['getModels'](runtimeItem1);
524
+ await runtime['getModels'](runtimeItem2);
525
+ expect(mockModelsFunction1).toHaveBeenCalledTimes(1);
526
+ expect(mockModelsFunction2).toHaveBeenCalledTimes(1);
527
+
528
+ // Clear all cache
529
+ runtime.clearModelCache();
530
+
531
+ // Should call functions again
532
+ await runtime['getModels'](runtimeItem1);
533
+ await runtime['getModels'](runtimeItem2);
534
+ expect(mockModelsFunction1).toHaveBeenCalledTimes(2);
535
+ expect(mockModelsFunction2).toHaveBeenCalledTimes(2);
536
+ });
537
+ });
538
+ });
@@ -28,7 +28,7 @@ import { baseRuntimeMap } from './baseRuntimeMap';
28
28
 
29
29
  export interface RuntimeItem {
30
30
  id: string;
31
- models?: string[];
31
+ models?: string[] | (() => Promise<string[]>);
32
32
  runtime: LobeRuntimeAI;
33
33
  }
34
34
 
@@ -44,11 +44,13 @@ interface ProviderIniOptions extends Record<string, any> {
44
44
  sessionToken?: string;
45
45
  }
46
46
 
47
+ export type RuntimeClass = typeof LobeOpenAI;
48
+
47
49
  interface RouterInstance {
48
50
  apiType: keyof typeof baseRuntimeMap;
49
- models?: string[];
51
+ models?: string[] | (() => Promise<string[]>);
50
52
  options: ProviderIniOptions;
51
- runtime?: typeof LobeOpenAI;
53
+ runtime?: RuntimeClass;
52
54
  }
53
55
 
54
56
  type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
@@ -117,6 +119,7 @@ export const createRouterRuntime = ({
117
119
  return class UniformRuntime implements LobeRuntimeAI {
118
120
  private _runtimes: RuntimeItem[];
119
121
  private _options: ClientOptions & Record<string, any>;
122
+ private _modelCache = new Map<string, string[]>();
120
123
 
121
124
  constructor(options: ClientOptions & Record<string, any> = {}) {
122
125
  const _options = {
@@ -142,18 +145,40 @@ export const createRouterRuntime = ({
142
145
  this._options = _options;
143
146
  }
144
147
 
145
- // 检查下是否能匹配到特定模型,否则默认使用最后一个 runtime
146
- getRuntimeByModel(model: string) {
147
- const runtimeItem =
148
- this._runtimes.find((runtime) => runtime.models && runtime.models.includes(model)) ||
149
- this._runtimes.at(-1)!;
148
+ // 获取 runtime 的 models 列表,支持同步数组和异步函数,带缓存机制
149
+ private async getModels(runtimeItem: RuntimeItem): Promise<string[]> {
150
+ const cacheKey = runtimeItem.id;
151
+
152
+ // 如果是同步数组,直接返回不需要缓存
153
+ if (typeof runtimeItem.models !== 'function') {
154
+ return runtimeItem.models || [];
155
+ }
156
+
157
+ // 检查缓存
158
+ if (this._modelCache.has(cacheKey)) {
159
+ return this._modelCache.get(cacheKey)!;
160
+ }
161
+
162
+ // 获取模型列表并缓存结果
163
+ const models = await runtimeItem.models();
164
+ this._modelCache.set(cacheKey, models);
165
+ return models;
166
+ }
150
167
 
151
- return runtimeItem.runtime;
168
+ // 检查下是否能匹配到特定模型,否则默认使用最后一个 runtime
169
+ async getRuntimeByModel(model: string) {
170
+ for (const runtimeItem of this._runtimes) {
171
+ const models = await this.getModels(runtimeItem);
172
+ if (models.includes(model)) {
173
+ return runtimeItem.runtime;
174
+ }
175
+ }
176
+ return this._runtimes.at(-1)!.runtime;
152
177
  }
153
178
 
154
179
  async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
155
180
  try {
156
- const runtime = this.getRuntimeByModel(payload.model);
181
+ const runtime = await this.getRuntimeByModel(payload.model);
157
182
 
158
183
  return await runtime.chat!(payload, options);
159
184
  } catch (e) {
@@ -170,7 +195,7 @@ export const createRouterRuntime = ({
170
195
  }
171
196
 
172
197
  async textToImage(payload: TextToImagePayload) {
173
- const runtime = this.getRuntimeByModel(payload.model);
198
+ const runtime = await this.getRuntimeByModel(payload.model);
174
199
 
175
200
  return runtime.textToImage!(payload);
176
201
  }
@@ -180,15 +205,27 @@ export const createRouterRuntime = ({
180
205
  }
181
206
 
182
207
  async embeddings(payload: EmbeddingsPayload, options?: EmbeddingsOptions) {
183
- const runtime = this.getRuntimeByModel(payload.model);
208
+ const runtime = await this.getRuntimeByModel(payload.model);
184
209
 
185
210
  return runtime.embeddings!(payload, options);
186
211
  }
187
212
 
188
213
  async textToSpeech(payload: TextToSpeechPayload, options?: EmbeddingsOptions) {
189
- const runtime = this.getRuntimeByModel(payload.model);
214
+ const runtime = await this.getRuntimeByModel(payload.model);
190
215
 
191
216
  return runtime.textToSpeech!(payload, options);
192
217
  }
218
+
219
+ /**
220
+ * 清除模型列表缓存,强制下次获取时重新加载
221
+ * @param runtimeId - 可选,指定清除特定 runtime 的缓存,不传则清除所有缓存
222
+ */
223
+ clearModelCache(runtimeId?: string) {
224
+ if (runtimeId) {
225
+ this._modelCache.delete(runtimeId);
226
+ } else {
227
+ this._modelCache.clear();
228
+ }
229
+ }
193
230
  };
194
231
  };
@@ -2,7 +2,7 @@ import { LobeRuntimeAI } from '../BaseAI';
2
2
 
3
3
  export interface RuntimeItem {
4
4
  id: string;
5
- models?: string[];
5
+ models?: string[] | (() => Promise<string[]>);
6
6
  runtime: LobeRuntimeAI;
7
7
  }
8
8
 
@@ -1,6 +1,5 @@
1
1
  import urlJoin from 'url-join';
2
2
 
3
- import { LOBE_DEFAULT_MODEL_LIST } from '@/config/aiModels';
4
3
  import AiHubMixModels from '@/config/aiModels/aihubmix';
5
4
  import type { ChatModelCard } from '@/types/llm';
6
5
 
@@ -98,14 +97,20 @@ export const LobeAiHubMixAI = createRouterRuntime({
98
97
  routers: [
99
98
  {
100
99
  apiType: 'anthropic',
101
- models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
102
- (id) => id.startsWith('claude') || id.startsWith('kimi-k2'),
103
- ),
100
+ models: async () => {
101
+ const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
102
+ return LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
103
+ (id) => id.startsWith('claude') || id.startsWith('kimi-k2'),
104
+ );
105
+ },
104
106
  options: { baseURL },
105
107
  },
106
108
  {
107
109
  apiType: 'google',
108
- models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter((id) => id.startsWith('gemini')),
110
+ models: async () => {
111
+ const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
112
+ return LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter((id) => id.startsWith('gemini'));
113
+ },
109
114
  options: { baseURL: urlJoin(baseURL, '/gemini') },
110
115
  },
111
116
  {