@lobehub/chat 1.123.4 → 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.
- package/.env.example +5 -0
- package/CHANGELOG.md +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/changelog/v1.json +21 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
- package/locales/ar/chat.json +8 -2
- package/locales/ar/editor.json +47 -0
- package/locales/bg-BG/chat.json +8 -2
- package/locales/bg-BG/editor.json +47 -0
- package/locales/de-DE/chat.json +8 -2
- package/locales/de-DE/editor.json +47 -0
- package/locales/en-US/chat.json +8 -2
- package/locales/en-US/editor.json +47 -0
- package/locales/es-ES/chat.json +8 -2
- package/locales/es-ES/editor.json +47 -0
- package/locales/es-ES/models.json +3 -1
- package/locales/fa-IR/chat.json +8 -2
- package/locales/fa-IR/editor.json +47 -0
- package/locales/fr-FR/chat.json +8 -2
- package/locales/fr-FR/editor.json +47 -0
- package/locales/it-IT/chat.json +8 -2
- package/locales/it-IT/editor.json +47 -0
- package/locales/ja-JP/chat.json +8 -2
- package/locales/ja-JP/editor.json +47 -0
- package/locales/ko-KR/chat.json +8 -2
- package/locales/ko-KR/editor.json +47 -0
- package/locales/ko-KR/models.json +3 -1
- package/locales/nl-NL/chat.json +8 -2
- package/locales/nl-NL/editor.json +47 -0
- package/locales/nl-NL/models.json +3 -1
- package/locales/pl-PL/chat.json +8 -2
- package/locales/pl-PL/editor.json +47 -0
- package/locales/pt-BR/chat.json +8 -2
- package/locales/pt-BR/editor.json +47 -0
- package/locales/ru-RU/chat.json +8 -2
- package/locales/ru-RU/editor.json +47 -0
- package/locales/tr-TR/chat.json +8 -2
- package/locales/tr-TR/editor.json +47 -0
- package/locales/vi-VN/chat.json +8 -2
- package/locales/vi-VN/editor.json +47 -0
- package/locales/zh-CN/chat.json +8 -2
- package/locales/zh-CN/editor.json +47 -0
- package/locales/zh-CN/modelProvider.json +1 -1
- package/locales/zh-TW/chat.json +8 -2
- package/locales/zh-TW/editor.json +47 -0
- package/locales/zh-TW/models.json +3 -1
- package/next.config.ts +4 -0
- package/package.json +4 -2
- package/packages/const/src/layoutTokens.ts +1 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
- package/packages/model-bank/src/aiModels/groq.ts +26 -8
- package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
- package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
- package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
- package/packages/model-bank/src/aiModels/novita.ts +40 -9
- package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
- package/packages/model-bank/src/aiModels/qwen.ts +62 -1
- package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
- package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
- package/packages/model-runtime/src/newapi/index.test.ts +49 -42
- package/packages/model-runtime/src/newapi/index.ts +124 -143
- package/packages/types/src/index.ts +1 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
- package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
- package/src/config/llm.ts +8 -0
- package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
- package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
- package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
- package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
- package/src/features/ChatInput/ActionBar/config.ts +7 -1
- package/src/features/ChatInput/ActionBar/index.tsx +40 -51
- package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
- package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
- package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
- package/src/features/ChatInput/Desktop/index.tsx +94 -69
- package/src/features/ChatInput/InputEditor/index.tsx +134 -0
- package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
- package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
- package/src/features/ChatInput/Mobile/index.tsx +72 -0
- package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
- package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
- package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
- package/src/features/ChatInput/SendArea/index.tsx +36 -0
- package/src/features/ChatInput/StoreUpdater.tsx +41 -0
- package/src/features/ChatInput/TypoBar/index.tsx +139 -0
- package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
- package/src/features/ChatInput/index.ts +7 -0
- package/src/features/ChatInput/store/action.ts +75 -0
- package/src/features/ChatInput/store/index.ts +23 -0
- package/src/features/ChatInput/store/initialState.ts +54 -0
- package/src/features/ChatInput/store/selectors.ts +5 -0
- package/src/features/Conversation/components/BackBottom/style.ts +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +10 -3
- package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
- package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
- package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
- package/src/features/Portal/Thread/Chat/index.tsx +2 -2
- package/src/features/Portal/Thread/Header/index.tsx +1 -1
- package/src/hooks/useHotkeys/chatScope.ts +5 -3
- package/src/layout/GlobalProvider/Editor.tsx +27 -0
- package/src/layout/GlobalProvider/Locale.tsx +3 -23
- package/src/locales/default/chat.ts +8 -2
- package/src/locales/default/editor.ts +47 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/modelProvider.ts +1 -1
- package/src/services/aiChat.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
- package/src/store/chat/slices/aiChat/initialState.ts +19 -0
- package/src/store/chat/slices/aiChat/selectors.ts +18 -0
- package/src/store/global/action.test.ts +6 -5
- package/src/store/global/actions/__tests__/general.test.ts +6 -6
- package/src/store/global/actions/workspacePane.ts +6 -0
- package/src/store/global/initialState.ts +2 -4
- package/src/store/global/selectors/systemStatus.test.ts +1 -2
- package/src/store/global/selectors/systemStatus.ts +2 -5
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
- package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
- package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
- package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
- package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
- package/src/features/ChatInput/useSend.ts +0 -102
- package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
- package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
- package/src/libs/trpc/client/types.ts +0 -18
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -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> = {
|
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
|
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(/\/
|
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(/\/
|
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
|
-
//
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
const
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
//
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
//
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
//
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
169
|
-
|
170
|
-
// 同时更新全局路由映射表
|
171
|
-
globalModelRouteMap.set(model.id, detectedProvider);
|
163
|
+
// 将检测到的 provider 信息附加到模型上
|
164
|
+
enhancedModel._detectedProvider = detectedProvider;
|
172
165
|
|
173
|
-
|
174
|
-
|
166
|
+
return enhancedModel;
|
167
|
+
});
|
175
168
|
|
176
|
-
|
177
|
-
|
169
|
+
// 使用 processMultiProviderModelList 处理模型能力
|
170
|
+
const processedModels = await processMultiProviderModelList(enrichedModelList, 'newapi');
|
178
171
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
199
|
-
|
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
|
-
|
205
|
-
baseURL:
|
190
|
+
...options,
|
191
|
+
baseURL: userBaseURL,
|
206
192
|
},
|
207
193
|
},
|
208
194
|
{
|
209
195
|
apiType: 'google',
|
210
|
-
models: () =>
|
211
|
-
|
212
|
-
|
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
|
-
|
218
|
-
baseURL:
|
200
|
+
...options,
|
201
|
+
baseURL: userBaseURL,
|
219
202
|
},
|
220
203
|
},
|
221
204
|
{
|
222
205
|
apiType: 'xai',
|
223
|
-
models: () =>
|
224
|
-
|
225
|
-
|
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
|
-
|
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
|
});
|