@lobehub/chat 1.124.0 → 1.124.1

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 (48) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +33 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/Dockerfile.pglite +2 -0
  6. package/changelog/v1.json +12 -0
  7. package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
  8. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
  9. package/locales/ar/chat.json +2 -0
  10. package/locales/bg-BG/chat.json +2 -0
  11. package/locales/de-DE/chat.json +2 -0
  12. package/locales/en-US/chat.json +2 -0
  13. package/locales/es-ES/chat.json +2 -0
  14. package/locales/fa-IR/chat.json +2 -0
  15. package/locales/fr-FR/chat.json +2 -0
  16. package/locales/it-IT/chat.json +2 -0
  17. package/locales/ja-JP/chat.json +2 -0
  18. package/locales/ko-KR/chat.json +2 -0
  19. package/locales/nl-NL/chat.json +2 -0
  20. package/locales/pl-PL/chat.json +2 -0
  21. package/locales/pt-BR/chat.json +2 -0
  22. package/locales/ru-RU/chat.json +2 -0
  23. package/locales/tr-TR/chat.json +2 -0
  24. package/locales/vi-VN/chat.json +2 -0
  25. package/locales/zh-CN/chat.json +2 -0
  26. package/locales/zh-CN/modelProvider.json +1 -1
  27. package/locales/zh-TW/chat.json +2 -0
  28. package/package.json +1 -1
  29. package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
  30. package/packages/model-bank/src/aiModels/groq.ts +26 -8
  31. package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
  32. package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
  33. package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
  34. package/packages/model-bank/src/aiModels/novita.ts +40 -9
  35. package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
  36. package/packages/model-bank/src/aiModels/qwen.ts +62 -1
  37. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  38. package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
  39. package/packages/model-runtime/src/newapi/index.test.ts +49 -42
  40. package/packages/model-runtime/src/newapi/index.ts +124 -143
  41. package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
  42. package/src/config/llm.ts +8 -0
  43. package/src/features/ChatInput/Desktop/index.tsx +16 -4
  44. package/src/locales/default/chat.ts +1 -0
  45. package/src/locales/default/modelProvider.ts +1 -1
  46. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
  47. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +352 -7
  48. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +2 -1
@@ -62,7 +62,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
62
62
  describe('HandlePayload Function Branch Coverage - Direct Testing', () => {
63
63
  // Create a mock Set for testing
64
64
  let testResponsesAPIModels: Set<string>;
65
-
65
+
66
66
  const testHandlePayload = (payload: ChatStreamPayload) => {
67
67
  // This replicates the exact handlePayload logic from the source
68
68
  if (
@@ -85,7 +85,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
85
85
  };
86
86
 
87
87
  const result = testHandlePayload(payload);
88
-
88
+
89
89
  expect(result).toEqual({ ...payload, apiMode: 'responses' });
90
90
  });
91
91
 
@@ -99,7 +99,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
99
99
  };
100
100
 
101
101
  const result = testHandlePayload(payload);
102
-
102
+
103
103
  expect(result).toEqual({ ...payload, apiMode: 'responses' });
104
104
  });
105
105
 
@@ -113,7 +113,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
113
113
  };
114
114
 
115
115
  const result = testHandlePayload(payload);
116
-
116
+
117
117
  expect(result).toEqual({ ...payload, apiMode: 'responses' });
118
118
  });
119
119
 
@@ -127,7 +127,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
127
127
  };
128
128
 
129
129
  const result = testHandlePayload(payload);
130
-
130
+
131
131
  expect(result).toEqual({ ...payload, apiMode: 'responses' });
132
132
  });
133
133
 
@@ -141,7 +141,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
141
141
  };
142
142
 
143
143
  const result = testHandlePayload(payload);
144
-
144
+
145
145
  expect(result).toEqual(payload);
146
146
  });
147
147
  });
@@ -207,7 +207,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
207
207
 
208
208
  describe('Models Function Branch Coverage - Logical Testing', () => {
209
209
  // Test the complex models function logic by replicating its branching behavior
210
-
210
+
211
211
  describe('Data Handling Branches', () => {
212
212
  it('should handle undefined data from models.list (Branch 3.1: data = undefined)', () => {
213
213
  const data = undefined;
@@ -293,63 +293,63 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
293
293
  it('should use model_price when > 0 (Branch 3.8: model_price && model_price > 0 = true)', () => {
294
294
  const pricing = { model_price: 15, model_ratio: 10 };
295
295
  let inputPrice;
296
-
296
+
297
297
  if (pricing.model_price && pricing.model_price > 0) {
298
298
  inputPrice = pricing.model_price * 2;
299
299
  } else if (pricing.model_ratio) {
300
300
  inputPrice = pricing.model_ratio * 2;
301
301
  }
302
-
302
+
303
303
  expect(inputPrice).toBe(30); // model_price * 2
304
304
  });
305
305
 
306
306
  it('should fallback to model_ratio when model_price = 0 (Branch 3.8: model_price > 0 = false, Branch 3.9: model_ratio = true)', () => {
307
307
  const pricing = { model_price: 0, model_ratio: 12 };
308
308
  let inputPrice;
309
-
309
+
310
310
  if (pricing.model_price && pricing.model_price > 0) {
311
311
  inputPrice = pricing.model_price * 2;
312
312
  } else if (pricing.model_ratio) {
313
313
  inputPrice = pricing.model_ratio * 2;
314
314
  }
315
-
315
+
316
316
  expect(inputPrice).toBe(24); // model_ratio * 2
317
317
  });
318
318
 
319
319
  it('should handle missing model_ratio (Branch 3.9: model_ratio = undefined)', () => {
320
320
  const pricing: Partial<NewAPIPricing> = { quota_type: 0 }; // No model_price and no model_ratio
321
321
  let inputPrice: number | undefined;
322
-
322
+
323
323
  if (pricing.model_price && pricing.model_price > 0) {
324
324
  inputPrice = pricing.model_price * 2;
325
325
  } else if (pricing.model_ratio) {
326
326
  inputPrice = pricing.model_ratio * 2;
327
327
  }
328
-
328
+
329
329
  expect(inputPrice).toBeUndefined();
330
330
  });
331
331
 
332
332
  it('should calculate output price when inputPrice is defined (Branch 3.10: inputPrice !== undefined = true)', () => {
333
333
  const inputPrice = 20;
334
334
  const completionRatio = 1.5;
335
-
335
+
336
336
  let outputPrice;
337
337
  if (inputPrice !== undefined) {
338
338
  outputPrice = inputPrice * (completionRatio || 1);
339
339
  }
340
-
340
+
341
341
  expect(outputPrice).toBe(30);
342
342
  });
343
343
 
344
344
  it('should use default completion_ratio when not provided', () => {
345
345
  const inputPrice = 16;
346
346
  const completionRatio = undefined;
347
-
347
+
348
348
  let outputPrice;
349
349
  if (inputPrice !== undefined) {
350
350
  outputPrice = inputPrice * (completionRatio || 1);
351
351
  }
352
-
352
+
353
353
  expect(outputPrice).toBe(16); // input * 1 (default)
354
354
  });
355
355
  });
@@ -358,74 +358,77 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
358
358
  it('should use supported_endpoint_types with anthropic (Branch 3.11: length > 0 = true, Branch 3.12: includes anthropic = true)', () => {
359
359
  const model = { supported_endpoint_types: ['anthropic'] };
360
360
  let detectedProvider = 'openai';
361
-
361
+
362
362
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
363
363
  if (model.supported_endpoint_types.includes('anthropic')) {
364
364
  detectedProvider = 'anthropic';
365
365
  }
366
366
  }
367
-
367
+
368
368
  expect(detectedProvider).toBe('anthropic');
369
369
  });
370
370
 
371
371
  it('should use supported_endpoint_types with gemini (Branch 3.13: includes gemini = true)', () => {
372
372
  const model = { supported_endpoint_types: ['gemini'] };
373
373
  let detectedProvider = 'openai';
374
-
374
+
375
375
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
376
376
  if (model.supported_endpoint_types.includes('gemini')) {
377
377
  detectedProvider = 'google';
378
378
  }
379
379
  }
380
-
380
+
381
381
  expect(detectedProvider).toBe('google');
382
382
  });
383
383
 
384
384
  it('should use supported_endpoint_types with xai (Branch 3.14: includes xai = true)', () => {
385
385
  const model = { supported_endpoint_types: ['xai'] };
386
386
  let detectedProvider = 'openai';
387
-
387
+
388
388
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
389
389
  if (model.supported_endpoint_types.includes('xai')) {
390
390
  detectedProvider = 'xai';
391
391
  }
392
392
  }
393
-
393
+
394
394
  expect(detectedProvider).toBe('xai');
395
395
  });
396
396
 
397
397
  it('should fallback to owned_by when supported_endpoint_types is empty (Branch 3.11: length > 0 = false, Branch 3.15: owned_by = true)', () => {
398
- const model: Partial<NewAPIModelCard> = { supported_endpoint_types: [], owned_by: 'anthropic' };
398
+ const model: Partial<NewAPIModelCard> = {
399
+ supported_endpoint_types: [],
400
+ owned_by: 'anthropic',
401
+ };
399
402
  let detectedProvider = 'openai';
400
-
403
+
401
404
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
402
405
  // Skip - empty array
403
406
  } else if (model.owned_by) {
404
407
  detectedProvider = 'anthropic'; // Simplified for test
405
408
  }
406
-
409
+
407
410
  expect(detectedProvider).toBe('anthropic');
408
411
  });
409
412
 
410
413
  it('should fallback to owned_by when no supported_endpoint_types (Branch 3.15: owned_by = true)', () => {
411
414
  const model: Partial<NewAPIModelCard> = { owned_by: 'google' };
412
415
  let detectedProvider = 'openai';
413
-
416
+
414
417
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
415
418
  // Skip - no supported_endpoint_types
416
419
  } else if (model.owned_by) {
417
420
  detectedProvider = 'google'; // Simplified for test
418
421
  }
419
-
422
+
420
423
  expect(detectedProvider).toBe('google');
421
424
  });
422
425
 
423
426
  it('should use detectModelProvider fallback when no owned_by (Branch 3.15: owned_by = false, Branch 3.17)', () => {
424
427
  const model: Partial<NewAPIModelCard> = { id: 'claude-3-sonnet', owned_by: '' };
425
428
  mockDetectModelProvider.mockReturnValue('anthropic');
426
-
429
+
427
430
  let detectedProvider = 'openai';
428
-
431
+
429
432
  if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
430
433
  // Skip - no supported_endpoint_types
431
434
  } else if (model.owned_by) {
@@ -433,7 +436,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
433
436
  } else {
434
437
  detectedProvider = mockDetectModelProvider(model.id || '');
435
438
  }
436
-
439
+
437
440
  expect(detectedProvider).toBe('anthropic');
438
441
  expect(mockDetectModelProvider).toHaveBeenCalledWith('claude-3-sonnet');
439
442
  });
@@ -444,11 +447,11 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
444
447
  displayName: 'Test Model',
445
448
  _detectedProvider: 'openai',
446
449
  };
447
-
450
+
448
451
  if (model._detectedProvider) {
449
452
  delete model._detectedProvider;
450
453
  }
451
-
454
+
452
455
  expect(model).not.toHaveProperty('_detectedProvider');
453
456
  });
454
457
 
@@ -457,27 +460,31 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
457
460
  id: 'test-model',
458
461
  displayName: 'Test Model',
459
462
  };
460
-
463
+
461
464
  const hadDetectedProvider = '_detectedProvider' in model;
462
-
465
+
463
466
  if (model._detectedProvider) {
464
467
  delete model._detectedProvider;
465
468
  }
466
-
469
+
467
470
  expect(hadDetectedProvider).toBe(false);
468
471
  });
469
472
  });
470
473
 
471
474
  describe('URL Processing Branch Coverage', () => {
472
- it('should remove trailing /v1 from baseURL', () => {
475
+ it('should remove trailing API version paths from baseURL', () => {
473
476
  const testURLs = [
474
477
  { input: 'https://api.newapi.com/v1', expected: 'https://api.newapi.com' },
475
478
  { input: 'https://api.newapi.com/v1/', expected: 'https://api.newapi.com' },
479
+ { input: 'https://api.newapi.com/v1beta', expected: 'https://api.newapi.com' },
480
+ { input: 'https://api.newapi.com/v1beta/', expected: 'https://api.newapi.com' },
481
+ { input: 'https://api.newapi.com/v2', expected: 'https://api.newapi.com' },
482
+ { input: 'https://api.newapi.com/v1alpha', expected: 'https://api.newapi.com' },
476
483
  { input: 'https://api.newapi.com', expected: 'https://api.newapi.com' },
477
484
  ];
478
485
 
479
486
  testURLs.forEach(({ input, expected }) => {
480
- const result = input.replace(/\/v1\/?$/, '');
487
+ const result = input.replace(/\/v\d+[a-z]*\/?$/, '');
481
488
  expect(result).toBe(expected);
482
489
  });
483
490
  });
@@ -538,7 +545,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
538
545
  { model_name: 'openai-gpt4', quota_type: 1, model_price: 30 }, // Should be skipped
539
546
  ];
540
547
 
541
- const pricingMap = new Map(pricingData.map(p => [p.model_name, p]));
548
+ const pricingMap = new Map(pricingData.map((p) => [p.model_name, p]));
542
549
 
543
550
  const enrichedModels = models.map((model) => {
544
551
  let enhancedModel: any = { ...model };
@@ -601,7 +608,7 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
601
608
  // Test the dynamic routers configuration
602
609
  const testOptions = {
603
610
  apiKey: 'test-key',
604
- baseURL: 'https://yourapi.cn/v1'
611
+ baseURL: 'https://yourapi.cn/v1',
605
612
  };
606
613
 
607
614
  // Create instance to test dynamic routers
@@ -611,8 +618,8 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
611
618
  // The dynamic routers should be configured with user's baseURL
612
619
  // This is tested indirectly through successful instantiation
613
620
  // since the routers function processes the options.baseURL
614
- const expectedBaseURL = testOptions.baseURL.replace(/\/v1\/?$/, '');
621
+ const expectedBaseURL = testOptions.baseURL.replace(/\/v\d+[a-z]*\/?$/, '');
615
622
  expect(expectedBaseURL).toBe('https://yourapi.cn');
616
623
  });
617
624
  });
618
- });
625
+ });
@@ -1,3 +1,4 @@
1
+ import { LOBE_DEFAULT_MODEL_LIST } from 'model-bank';
1
2
  import urlJoin from 'url-join';
2
3
 
3
4
  import { createRouterRuntime } from '../RouterRuntime';
@@ -54,9 +55,6 @@ const getProviderFromOwnedBy = (ownedBy: string): string => {
54
55
  return 'openai';
55
56
  };
56
57
 
57
- // 全局的模型路由映射,在 models 函数执行后被填充
58
- let globalModelRouteMap: Map<string, string> = new Map();
59
-
60
58
  export const LobeNewAPIAI = createRouterRuntime({
61
59
  debug: {
62
60
  chatCompletion: () => process.env.DEBUG_NEWAPI_CHAT_COMPLETION === '1',
@@ -66,180 +64,163 @@ export const LobeNewAPIAI = createRouterRuntime({
66
64
  },
67
65
  id: ModelProvider.NewAPI,
68
66
  models: async ({ client: openAIClient }) => {
69
- // 每次调用 models 时清空并重建路由映射
70
- globalModelRouteMap.clear();
71
-
72
- // 获取基础 URL(移除末尾的 /v1)
73
- const baseURL = openAIClient.baseURL.replace(/\/v1\/?$/, '');
74
-
75
- const modelsPage = (await openAIClient.models.list()) as any;
76
- const modelList: NewAPIModelCard[] = modelsPage.data || [];
77
-
78
- // 尝试获取 pricing 信息以补充模型详细信息
79
- let pricingMap: Map<string, NewAPIPricing> = new Map();
80
- try {
81
- // 使用保存的 baseURL
82
- const pricingResponse = await fetch(`${baseURL}/api/pricing`, {
83
- headers: {
84
- Authorization: `Bearer ${openAIClient.apiKey}`,
85
- },
86
- });
87
-
88
- if (pricingResponse.ok) {
89
- const pricingData = await pricingResponse.json();
90
- if (pricingData.success && pricingData.data) {
91
- (pricingData.data as NewAPIPricing[]).forEach((pricing) => {
92
- pricingMap.set(pricing.model_name, pricing);
93
- });
94
- }
67
+ // 获取基础 URL(移除末尾的 API 版本路径如 /v1、/v1beta 等)
68
+ const baseURL = openAIClient.baseURL.replace(/\/v\d+[a-z]*\/?$/, '');
69
+
70
+ const modelsPage = (await openAIClient.models.list()) as any;
71
+ const modelList: NewAPIModelCard[] = modelsPage.data || [];
72
+
73
+ // 尝试获取 pricing 信息以补充模型详细信息
74
+ let pricingMap: Map<string, NewAPIPricing> = new Map();
75
+ try {
76
+ // 使用保存的 baseURL
77
+ const pricingResponse = await fetch(`${baseURL}/api/pricing`, {
78
+ headers: {
79
+ Authorization: `Bearer ${openAIClient.apiKey}`,
80
+ },
81
+ });
82
+
83
+ if (pricingResponse.ok) {
84
+ const pricingData = await pricingResponse.json();
85
+ if (pricingData.success && pricingData.data) {
86
+ (pricingData.data as NewAPIPricing[]).forEach((pricing) => {
87
+ pricingMap.set(pricing.model_name, pricing);
88
+ });
95
89
  }
96
- } catch (error) {
97
- // If fetching pricing information fails, continue using the basic model information
98
- console.debug('Failed to fetch NewAPI pricing info:', error);
99
90
  }
100
-
101
- // Process the model list: determine the provider for each model based on priority rules
102
- const enrichedModelList = modelList.map((model) => {
103
- let enhancedModel: any = { ...model };
104
-
105
- // 1. 添加 pricing 信息
106
- const pricing = pricingMap.get(model.id);
107
- if (pricing) {
108
- // NewAPI 的价格计算逻辑:
109
- // - quota_type: 0 表示按量计费(按 token),1 表示按次计费
110
- // - model_ratio: 相对于基础价格的倍率(基础价格 = $0.002/1K tokens)
111
- // - model_price: 直接指定的价格(优先使用)
112
- // - completion_ratio: 输出价格相对于输入价格的倍率
113
- //
114
- // LobeChat 需要的格式:美元/百万 token
115
-
116
- let inputPrice: number | undefined;
117
- let outputPrice: number | undefined;
118
-
119
- if (pricing.quota_type === 0) {
120
- // 按量计费
121
- if (pricing.model_price && pricing.model_price > 0) {
122
- // model_price is a direct price value; need to confirm its unit.
123
- // Assumption: model_price is the price per 1,000 tokens (i.e., $/1K tokens).
124
- // To convert to price per 1,000,000 tokens ($/1M tokens), multiply by 1,000,000 / 1,000 = 1,000.
125
- // Since the base price is $0.002/1K tokens, multiplying by 2 gives $2/1M tokens.
126
- // Therefore, inputPrice = model_price * 2 converts the price to $/1M tokens for LobeChat.
127
- inputPrice = pricing.model_price * 2;
128
- } else if (pricing.model_ratio) {
129
- // model_ratio × $0.002/1K = model_ratio × $2/1M
130
- inputPrice = pricing.model_ratio * 2; // 转换为 $/1M tokens
131
- }
132
-
133
- if (inputPrice !== undefined) {
134
- // 计算输出价格
135
- outputPrice = inputPrice * (pricing.completion_ratio || 1);
136
-
137
- enhancedModel.pricing = {
138
- input: inputPrice,
139
- output: outputPrice,
140
- };
141
- }
91
+ } catch (error) {
92
+ // If fetching pricing information fails, continue using the basic model information
93
+ console.debug('Failed to fetch NewAPI pricing info:', error);
94
+ }
95
+
96
+ // Process the model list: determine the provider for each model based on priority rules
97
+ const enrichedModelList = modelList.map((model) => {
98
+ let enhancedModel: any = { ...model };
99
+
100
+ // 1. 添加 pricing 信息
101
+ const pricing = pricingMap.get(model.id);
102
+ if (pricing) {
103
+ // NewAPI 的价格计算逻辑:
104
+ // - quota_type: 0 表示按量计费(按 token),1 表示按次计费
105
+ // - model_ratio: 相对于基础价格的倍率(基础价格 = $0.002/1K tokens)
106
+ // - model_price: 直接指定的价格(优先使用)
107
+ // - completion_ratio: 输出价格相对于输入价格的倍率
108
+ //
109
+ // LobeChat 需要的格式:美元/百万 token
110
+
111
+ let inputPrice: number | undefined;
112
+ let outputPrice: number | undefined;
113
+
114
+ if (pricing.quota_type === 0) {
115
+ // 按量计费
116
+ if (pricing.model_price && pricing.model_price > 0) {
117
+ // model_price is a direct price value; need to confirm its unit.
118
+ // Assumption: model_price is the price per 1,000 tokens (i.e., $/1K tokens).
119
+ // To convert to price per 1,000,000 tokens ($/1M tokens), multiply by 1,000,000 / 1,000 = 1,000.
120
+ // Since the base price is $0.002/1K tokens, multiplying by 2 gives $2/1M tokens.
121
+ // Therefore, inputPrice = model_price * 2 converts the price to $/1M tokens for LobeChat.
122
+ inputPrice = pricing.model_price * 2;
123
+ } else if (pricing.model_ratio) {
124
+ // model_ratio × $0.002/1K = model_ratio × $2/1M
125
+ inputPrice = pricing.model_ratio * 2; // 转换为 $/1M tokens
142
126
  }
143
- // quota_type === 1 按次计费暂不支持
144
- }
145
127
 
146
- // 2. 根据优先级处理 provider 信息并缓存路由
147
- let detectedProvider = 'openai'; // 默认
148
-
149
- // 优先级1:使用 supported_endpoint_types
150
- if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
151
- if (model.supported_endpoint_types.includes('anthropic')) {
152
- detectedProvider = 'anthropic';
153
- } else if (model.supported_endpoint_types.includes('gemini')) {
154
- detectedProvider = 'google';
155
- } else if (model.supported_endpoint_types.includes('xai')) {
156
- detectedProvider = 'xai';
128
+ if (inputPrice !== undefined) {
129
+ // 计算输出价格
130
+ outputPrice = inputPrice * (pricing.completion_ratio || 1);
131
+
132
+ enhancedModel.pricing = {
133
+ input: inputPrice,
134
+ output: outputPrice,
135
+ };
157
136
  }
158
137
  }
159
- // 优先级2:使用 owned_by 字段
160
- else if (model.owned_by) {
161
- detectedProvider = getProviderFromOwnedBy(model.owned_by);
162
- }
163
- // 优先级3:基于模型名称检测
164
- else {
165
- detectedProvider = detectModelProvider(model.id);
138
+ // quota_type === 1 按次计费暂不支持
139
+ }
140
+
141
+ // 2. 根据优先级处理 provider 信息并缓存路由
142
+ let detectedProvider = 'openai'; // 默认
143
+
144
+ // 优先级1:使用 supported_endpoint_types
145
+ if (model.supported_endpoint_types && model.supported_endpoint_types.length > 0) {
146
+ if (model.supported_endpoint_types.includes('anthropic')) {
147
+ detectedProvider = 'anthropic';
148
+ } else if (model.supported_endpoint_types.includes('gemini')) {
149
+ detectedProvider = 'google';
150
+ } else if (model.supported_endpoint_types.includes('xai')) {
151
+ detectedProvider = 'xai';
166
152
  }
153
+ }
154
+ // 优先级2:使用 owned_by 字段
155
+ else if (model.owned_by) {
156
+ detectedProvider = getProviderFromOwnedBy(model.owned_by);
157
+ }
158
+ // 优先级3:基于模型名称检测
159
+ else {
160
+ detectedProvider = detectModelProvider(model.id);
161
+ }
167
162
 
168
- // 将检测到的 provider 信息附加到模型上,供路由使用
169
- enhancedModel._detectedProvider = detectedProvider;
170
- // 同时更新全局路由映射表
171
- globalModelRouteMap.set(model.id, detectedProvider);
163
+ // 将检测到的 provider 信息附加到模型上
164
+ enhancedModel._detectedProvider = detectedProvider;
172
165
 
173
- return enhancedModel;
174
- });
166
+ return enhancedModel;
167
+ });
175
168
 
176
- // 使用 processMultiProviderModelList 处理模型能力
177
- const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
169
+ // 使用 processMultiProviderModelList 处理模型能力
170
+ const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
178
171
 
179
- // 如果我们检测到了 provider,确保它被正确应用
180
- return processedModels.map((model: any) => {
181
- if (model._detectedProvider) {
182
- // Here you can adjust certain model properties as needed.
183
- // FIXME: The current data structure does not support storing provider information, and the official NewAPI does not provide a corresponding field. Consider extending the model schema if provider tracking is required in the future.
184
- delete model._detectedProvider; // Remove temporary field
185
- }
186
- return model;
187
- });
188
- },
189
- // 使用动态 routers 配置,在构造时获取用户的 baseURL
190
- routers: (options) => {
191
- // 使用全局的模型路由映射
192
- const userBaseURL = options.baseURL?.replace(/\/v1\/?$/, '') || '';
193
-
194
- return [
172
+ // 清理临时字段
173
+ return processedModels.map((model: any) => {
174
+ if (model._detectedProvider) {
175
+ delete model._detectedProvider;
176
+ }
177
+ return model;
178
+ });
179
+ },
180
+ routers: (options) => {
181
+ const userBaseURL = options.baseURL?.replace(/\/v\d+[a-z]*\/?$/, '') || '';
182
+
183
+ return [
195
184
  {
196
185
  apiType: 'anthropic',
197
- models: () =>
198
- Promise.resolve(
199
- Array.from(globalModelRouteMap.entries())
200
- .filter(([, provider]) => provider === 'anthropic')
201
- .map(([modelId]) => modelId),
202
- ),
186
+ models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
187
+ (id) => detectModelProvider(id) === 'anthropic',
188
+ ),
203
189
  options: {
204
- // Anthropic 在 NewAPI 中使用 /v1 路径,会自动转换为 /v1/messages
205
- baseURL: urlJoin(userBaseURL, '/v1'),
190
+ ...options,
191
+ baseURL: userBaseURL,
206
192
  },
207
193
  },
208
194
  {
209
195
  apiType: 'google',
210
- models: () =>
211
- Promise.resolve(
212
- Array.from(globalModelRouteMap.entries())
213
- .filter(([, provider]) => provider === 'google')
214
- .map(([modelId]) => modelId),
215
- ),
196
+ models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
197
+ (id) => detectModelProvider(id) === 'google',
198
+ ),
216
199
  options: {
217
- // Gemini 在 NewAPI 中使用 /v1beta 路径
218
- baseURL: urlJoin(userBaseURL, '/v1beta'),
200
+ ...options,
201
+ baseURL: userBaseURL,
219
202
  },
220
203
  },
221
204
  {
222
205
  apiType: 'xai',
223
- models: () =>
224
- Promise.resolve(
225
- Array.from(globalModelRouteMap.entries())
226
- .filter(([, provider]) => provider === 'xai')
227
- .map(([modelId]) => modelId),
228
- ),
206
+ models: LOBE_DEFAULT_MODEL_LIST.map((m) => m.id).filter(
207
+ (id) => detectModelProvider(id) === 'xai',
208
+ ),
229
209
  options: {
230
- // xAI 使用标准 OpenAI 格式,走 /v1 路径
210
+ ...options,
231
211
  baseURL: urlJoin(userBaseURL, '/v1'),
232
212
  },
233
213
  },
234
214
  {
235
215
  apiType: 'openai',
236
216
  options: {
217
+ ...options,
237
218
  baseURL: urlJoin(userBaseURL, '/v1'),
238
219
  chatCompletion: {
239
220
  handlePayload,
240
221
  },
241
222
  },
242
223
  },
243
- ];
244
- },
224
+ ];
225
+ },
245
226
  });
@@ -16,7 +16,7 @@ const Page = () => {
16
16
  ...NewAPIProviderCard.settings,
17
17
  proxyUrl: {
18
18
  desc: t('newapi.apiUrl.desc'),
19
- placeholder: 'https://any-newapi-provider.com/v1',
19
+ placeholder: 'https://any-newapi-provider.com/',
20
20
  title: t('newapi.apiUrl.title'),
21
21
  },
22
22
  }}