@lobehub/chat 1.136.13 → 1.137.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 (190) hide show
  1. package/.cursor/rules/add-setting-env.mdc +175 -0
  2. package/.cursor/rules/db-migrations.mdc +25 -0
  3. package/.env.example +7 -0
  4. package/CHANGELOG.md +50 -0
  5. package/Dockerfile +3 -2
  6. package/Dockerfile.database +15 -3
  7. package/Dockerfile.pglite +3 -2
  8. package/changelog/v1.json +18 -0
  9. package/docs/development/database-schema.dbml +1 -0
  10. package/docs/self-hosting/advanced/feature-flags.mdx +25 -15
  11. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +25 -15
  12. package/docs/self-hosting/environment-variables/basic.mdx +12 -0
  13. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +12 -0
  14. package/locales/ar/setting.json +8 -0
  15. package/locales/bg-BG/setting.json +8 -0
  16. package/locales/de-DE/setting.json +8 -0
  17. package/locales/en-US/setting.json +8 -0
  18. package/locales/es-ES/setting.json +8 -0
  19. package/locales/fa-IR/setting.json +8 -0
  20. package/locales/fr-FR/setting.json +8 -0
  21. package/locales/it-IT/setting.json +8 -0
  22. package/locales/ja-JP/setting.json +8 -0
  23. package/locales/ko-KR/setting.json +8 -0
  24. package/locales/nl-NL/setting.json +8 -0
  25. package/locales/pl-PL/setting.json +8 -0
  26. package/locales/pt-BR/setting.json +8 -0
  27. package/locales/ru-RU/setting.json +8 -0
  28. package/locales/tr-TR/setting.json +8 -0
  29. package/locales/vi-VN/setting.json +8 -0
  30. package/locales/zh-CN/setting.json +8 -0
  31. package/locales/zh-TW/setting.json +8 -0
  32. package/package.json +1 -1
  33. package/packages/agent-runtime/examples/tools-calling.ts +4 -3
  34. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +559 -29
  35. package/packages/agent-runtime/src/core/runtime.ts +171 -43
  36. package/packages/agent-runtime/src/types/instruction.ts +32 -6
  37. package/packages/agent-runtime/src/types/runtime.ts +2 -2
  38. package/packages/agent-runtime/src/types/state.ts +1 -8
  39. package/packages/agent-runtime/vitest.config.mts +14 -0
  40. package/packages/const/src/settings/image.ts +8 -0
  41. package/packages/const/src/settings/index.ts +3 -0
  42. package/packages/context-engine/src/__tests__/pipeline.test.ts +485 -0
  43. package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +381 -0
  44. package/packages/context-engine/src/base/__tests__/BaseProvider.test.ts +392 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +346 -0
  46. package/packages/context-engine/src/processors/__tests__/ToolCall.test.ts +552 -0
  47. package/packages/database/migrations/0038_add_image_user_settings.sql +1 -0
  48. package/packages/database/migrations/meta/0038_snapshot.json +7580 -0
  49. package/packages/database/migrations/meta/_journal.json +7 -0
  50. package/packages/database/src/core/migrations.json +6 -0
  51. package/packages/database/src/models/user.ts +3 -1
  52. package/packages/database/src/schemas/user.ts +1 -0
  53. package/packages/file-loaders/src/loaders/docx/index.test.ts +0 -1
  54. package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +30 -0
  55. package/packages/file-loaders/src/loaders/excel/index.test.ts +8 -0
  56. package/packages/file-loaders/src/loaders/pptx/index.test.ts +25 -0
  57. package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
  58. package/packages/file-loaders/vitest.config.mts +8 -0
  59. package/packages/model-runtime/CLAUDE.md +5 -0
  60. package/packages/model-runtime/docs/test-coverage.md +706 -0
  61. package/packages/model-runtime/src/core/ModelRuntime.test.ts +231 -0
  62. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +1 -1
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.test.ts +799 -0
  64. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +188 -4
  65. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +41 -10
  66. package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +439 -0
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +789 -0
  68. package/packages/model-runtime/src/core/streams/openai/responsesStream.test.ts +551 -0
  69. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +230 -0
  70. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +334 -37
  71. package/packages/model-runtime/src/providerTestUtils.ts +148 -145
  72. package/packages/model-runtime/src/providers/ai302/index.test.ts +60 -0
  73. package/packages/model-runtime/src/providers/ai302/index.ts +9 -4
  74. package/packages/model-runtime/src/providers/ai360/index.test.ts +1213 -1
  75. package/packages/model-runtime/src/providers/ai360/index.ts +9 -4
  76. package/packages/model-runtime/src/providers/aihubmix/index.test.ts +73 -0
  77. package/packages/model-runtime/src/providers/aihubmix/index.ts +6 -9
  78. package/packages/model-runtime/src/providers/akashchat/index.test.ts +433 -3
  79. package/packages/model-runtime/src/providers/akashchat/index.ts +12 -7
  80. package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +183 -29
  81. package/packages/model-runtime/src/providers/anthropic/generateObject.ts +40 -24
  82. package/packages/model-runtime/src/providers/azureai/index.test.ts +102 -0
  83. package/packages/model-runtime/src/providers/baichuan/index.test.ts +416 -26
  84. package/packages/model-runtime/src/providers/baichuan/index.ts +23 -20
  85. package/packages/model-runtime/src/providers/bedrock/index.test.ts +420 -2
  86. package/packages/model-runtime/src/providers/cerebras/index.test.ts +465 -0
  87. package/packages/model-runtime/src/providers/cerebras/index.ts +8 -3
  88. package/packages/model-runtime/src/providers/cohere/index.test.ts +1074 -1
  89. package/packages/model-runtime/src/providers/cohere/index.ts +8 -3
  90. package/packages/model-runtime/src/providers/cometapi/index.test.ts +439 -3
  91. package/packages/model-runtime/src/providers/cometapi/index.ts +8 -3
  92. package/packages/model-runtime/src/providers/deepseek/index.test.ts +116 -1
  93. package/packages/model-runtime/src/providers/deepseek/index.ts +8 -3
  94. package/packages/model-runtime/src/providers/fireworksai/index.test.ts +264 -3
  95. package/packages/model-runtime/src/providers/fireworksai/index.ts +8 -3
  96. package/packages/model-runtime/src/providers/giteeai/index.test.ts +325 -3
  97. package/packages/model-runtime/src/providers/giteeai/index.ts +23 -6
  98. package/packages/model-runtime/src/providers/github/index.test.ts +532 -3
  99. package/packages/model-runtime/src/providers/github/index.ts +8 -3
  100. package/packages/model-runtime/src/providers/groq/index.test.ts +344 -31
  101. package/packages/model-runtime/src/providers/groq/index.ts +8 -3
  102. package/packages/model-runtime/src/providers/higress/index.test.ts +142 -0
  103. package/packages/model-runtime/src/providers/higress/index.ts +8 -3
  104. package/packages/model-runtime/src/providers/huggingface/index.test.ts +612 -1
  105. package/packages/model-runtime/src/providers/huggingface/index.ts +9 -4
  106. package/packages/model-runtime/src/providers/hunyuan/index.test.ts +365 -1
  107. package/packages/model-runtime/src/providers/hunyuan/index.ts +9 -3
  108. package/packages/model-runtime/src/providers/infiniai/index.test.ts +71 -0
  109. package/packages/model-runtime/src/providers/internlm/index.test.ts +369 -2
  110. package/packages/model-runtime/src/providers/internlm/index.ts +10 -5
  111. package/packages/model-runtime/src/providers/jina/index.test.ts +164 -3
  112. package/packages/model-runtime/src/providers/jina/index.ts +8 -3
  113. package/packages/model-runtime/src/providers/lmstudio/index.test.ts +182 -3
  114. package/packages/model-runtime/src/providers/lmstudio/index.ts +8 -3
  115. package/packages/model-runtime/src/providers/mistral/index.test.ts +779 -27
  116. package/packages/model-runtime/src/providers/mistral/index.ts +8 -3
  117. package/packages/model-runtime/src/providers/modelscope/index.test.ts +232 -1
  118. package/packages/model-runtime/src/providers/modelscope/index.ts +8 -3
  119. package/packages/model-runtime/src/providers/moonshot/index.test.ts +489 -2
  120. package/packages/model-runtime/src/providers/moonshot/index.ts +8 -3
  121. package/packages/model-runtime/src/providers/nebius/index.test.ts +381 -3
  122. package/packages/model-runtime/src/providers/nebius/index.ts +8 -3
  123. package/packages/model-runtime/src/providers/newapi/index.test.ts +667 -3
  124. package/packages/model-runtime/src/providers/newapi/index.ts +6 -3
  125. package/packages/model-runtime/src/providers/nvidia/index.test.ts +168 -1
  126. package/packages/model-runtime/src/providers/nvidia/index.ts +12 -7
  127. package/packages/model-runtime/src/providers/ollama/index.test.ts +797 -1
  128. package/packages/model-runtime/src/providers/ollama/index.ts +8 -0
  129. package/packages/model-runtime/src/providers/ollamacloud/index.test.ts +411 -0
  130. package/packages/model-runtime/src/providers/ollamacloud/index.ts +8 -3
  131. package/packages/model-runtime/src/providers/openai/index.test.ts +171 -2
  132. package/packages/model-runtime/src/providers/openai/index.ts +8 -3
  133. package/packages/model-runtime/src/providers/openrouter/index.test.ts +1647 -95
  134. package/packages/model-runtime/src/providers/openrouter/index.ts +12 -7
  135. package/packages/model-runtime/src/providers/qiniu/index.test.ts +294 -1
  136. package/packages/model-runtime/src/providers/qiniu/index.ts +8 -3
  137. package/packages/model-runtime/src/providers/search1api/index.test.ts +1131 -11
  138. package/packages/model-runtime/src/providers/search1api/index.ts +10 -4
  139. package/packages/model-runtime/src/providers/sensenova/index.test.ts +1069 -1
  140. package/packages/model-runtime/src/providers/sensenova/index.ts +8 -3
  141. package/packages/model-runtime/src/providers/siliconcloud/index.test.ts +196 -0
  142. package/packages/model-runtime/src/providers/siliconcloud/index.ts +8 -3
  143. package/packages/model-runtime/src/providers/spark/index.test.ts +293 -1
  144. package/packages/model-runtime/src/providers/spark/index.ts +8 -3
  145. package/packages/model-runtime/src/providers/stepfun/index.test.ts +322 -3
  146. package/packages/model-runtime/src/providers/stepfun/index.ts +8 -3
  147. package/packages/model-runtime/src/providers/tencentcloud/index.test.ts +182 -3
  148. package/packages/model-runtime/src/providers/tencentcloud/index.ts +8 -3
  149. package/packages/model-runtime/src/providers/togetherai/index.test.ts +359 -4
  150. package/packages/model-runtime/src/providers/togetherai/index.ts +12 -5
  151. package/packages/model-runtime/src/providers/v0/index.test.ts +341 -0
  152. package/packages/model-runtime/src/providers/v0/index.ts +20 -6
  153. package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +710 -0
  154. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +19 -13
  155. package/packages/model-runtime/src/providers/vllm/index.test.ts +45 -1
  156. package/packages/model-runtime/src/providers/volcengine/index.test.ts +75 -0
  157. package/packages/model-runtime/src/providers/wenxin/index.test.ts +144 -1
  158. package/packages/model-runtime/src/providers/wenxin/index.ts +8 -3
  159. package/packages/model-runtime/src/providers/xai/index.test.ts +105 -1
  160. package/packages/model-runtime/src/providers/xinference/index.test.ts +70 -1
  161. package/packages/model-runtime/src/providers/zeroone/index.test.ts +327 -3
  162. package/packages/model-runtime/src/providers/zeroone/index.ts +23 -6
  163. package/packages/model-runtime/src/providers/zhipu/index.test.ts +908 -236
  164. package/packages/model-runtime/src/providers/zhipu/index.ts +8 -3
  165. package/packages/model-runtime/src/types/structureOutput.ts +5 -1
  166. package/packages/model-runtime/vitest.config.mts +7 -1
  167. package/packages/types/src/aiChat.ts +20 -2
  168. package/packages/types/src/serverConfig.ts +7 -1
  169. package/packages/types/src/tool/index.ts +1 -0
  170. package/packages/types/src/tool/tool.ts +33 -0
  171. package/packages/types/src/user/settings/image.ts +3 -0
  172. package/packages/types/src/user/settings/index.ts +3 -0
  173. package/src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx +3 -0
  174. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +8 -3
  175. package/src/app/[variants]/(main)/settings/image/index.tsx +74 -0
  176. package/src/components/FormInput/FormSliderWithInput.tsx +40 -0
  177. package/src/components/FormInput/index.ts +1 -0
  178. package/src/envs/image.ts +27 -0
  179. package/src/features/Conversation/Messages/Assistant/index.tsx +1 -1
  180. package/src/features/Conversation/Messages/User/index.tsx +2 -2
  181. package/src/hooks/useFetchAiImageConfig.ts +12 -17
  182. package/src/locales/default/setting.ts +8 -0
  183. package/src/server/globalConfig/index.ts +5 -0
  184. package/src/server/routers/lambda/aiChat.ts +2 -0
  185. package/src/store/global/initialState.ts +1 -0
  186. package/src/store/image/slices/generationConfig/action.test.ts +17 -0
  187. package/src/store/image/slices/generationConfig/action.ts +18 -21
  188. package/src/store/image/slices/generationConfig/initialState.ts +3 -2
  189. package/src/store/user/slices/common/action.ts +1 -0
  190. package/src/store/user/slices/settings/selectors/settings.ts +3 -0
@@ -1,14 +1,14 @@
1
1
  // @vitest-environment node
2
+ import { ModelProvider } from 'model-bank';
2
3
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
4
 
4
5
  import { responsesAPIModels } from '../../const/models';
5
6
  import { ChatStreamPayload } from '../../types/chat';
6
7
  import * as modelParseModule from '../../utils/modelParse';
7
- import { LobeNewAPIAI, NewAPIModelCard, NewAPIPricing } from './index';
8
+ import { LobeNewAPIAI, NewAPIModelCard, NewAPIPricing, handlePayload, params } from './index';
8
9
 
9
10
  // Mock external dependencies
10
- vi.mock('../utils/modelParse');
11
- vi.mock('../const/models');
11
+ vi.mock('../../utils/modelParse');
12
12
 
13
13
  // Mock console methods
14
14
  vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -647,4 +647,668 @@ describe('NewAPI Runtime - 100% Branch Coverage', () => {
647
647
  expect(expectedBaseURL).toBe('https://yourapi.cn');
648
648
  });
649
649
  });
650
+
651
+ // ============================================================================
652
+ // COMPREHENSIVE INTEGRATION TESTS FOR 90%+ COVERAGE
653
+ // ============================================================================
654
+
655
+ describe('Params Object - Runtime Configuration', () => {
656
+ it('should export params with correct provider ID', () => {
657
+ expect(params.id).toBe(ModelProvider.NewAPI);
658
+ });
659
+
660
+ it('should export params with correct defaultHeaders', () => {
661
+ expect(params.defaultHeaders).toEqual({
662
+ 'X-Client': 'LobeHub',
663
+ });
664
+ });
665
+
666
+ it('should export params with debug configuration', () => {
667
+ expect(params.debug).toBeDefined();
668
+ expect(typeof params.debug.chatCompletion).toBe('function');
669
+ });
670
+
671
+ it('should export params with models function', () => {
672
+ expect(params.models).toBeDefined();
673
+ expect(typeof params.models).toBe('function');
674
+ });
675
+
676
+ it('should export params with routers function', () => {
677
+ expect(params.routers).toBeDefined();
678
+ expect(typeof params.routers).toBe('function');
679
+ });
680
+ });
681
+
682
+ describe('Debug Configuration - Direct Testing', () => {
683
+ it('should return false when DEBUG_NEWAPI_CHAT_COMPLETION is not set', () => {
684
+ delete process.env.DEBUG_NEWAPI_CHAT_COMPLETION;
685
+ const result = params.debug.chatCompletion();
686
+ expect(result).toBe(false);
687
+ });
688
+
689
+ it('should return true when DEBUG_NEWAPI_CHAT_COMPLETION is set to 1', () => {
690
+ process.env.DEBUG_NEWAPI_CHAT_COMPLETION = '1';
691
+ const result = params.debug.chatCompletion();
692
+ expect(result).toBe(true);
693
+ delete process.env.DEBUG_NEWAPI_CHAT_COMPLETION;
694
+ });
695
+
696
+ it('should return false when DEBUG_NEWAPI_CHAT_COMPLETION is set to 0', () => {
697
+ process.env.DEBUG_NEWAPI_CHAT_COMPLETION = '0';
698
+ const result = params.debug.chatCompletion();
699
+ expect(result).toBe(false);
700
+ delete process.env.DEBUG_NEWAPI_CHAT_COMPLETION;
701
+ });
702
+ });
703
+
704
+ describe('HandlePayload Function - Direct Testing', () => {
705
+ beforeEach(() => {
706
+ // Mock responsesAPIModels as a Set for testing
707
+ (responsesAPIModels as any).has = vi.fn((model: string) => model === 'o1-pro');
708
+ });
709
+
710
+ it('should add apiMode for models in responsesAPIModels set', () => {
711
+ (responsesAPIModels as any).has = vi.fn((model: string) => model === 'o1-pro');
712
+
713
+ const payload: ChatStreamPayload = {
714
+ model: 'o1-pro',
715
+ messages: [{ role: 'user', content: 'test' }],
716
+ temperature: 0.5,
717
+ };
718
+
719
+ const result = handlePayload(payload);
720
+ expect(result).toEqual({ ...payload, apiMode: 'responses' });
721
+ });
722
+
723
+ it('should add apiMode for gpt- models', () => {
724
+ (responsesAPIModels as any).has = vi.fn(() => false);
725
+
726
+ const payload: ChatStreamPayload = {
727
+ model: 'gpt-4o',
728
+ messages: [{ role: 'user', content: 'test' }],
729
+ temperature: 0.5,
730
+ };
731
+
732
+ const result = handlePayload(payload);
733
+ expect(result).toEqual({ ...payload, apiMode: 'responses' });
734
+ });
735
+
736
+ it('should add apiMode for o1 models', () => {
737
+ (responsesAPIModels as any).has = vi.fn(() => false);
738
+
739
+ const payload: ChatStreamPayload = {
740
+ model: 'o1-mini',
741
+ messages: [{ role: 'user', content: 'test' }],
742
+ temperature: 0.5,
743
+ };
744
+
745
+ const result = handlePayload(payload);
746
+ expect(result).toEqual({ ...payload, apiMode: 'responses' });
747
+ });
748
+
749
+ it('should add apiMode for o3 models', () => {
750
+ (responsesAPIModels as any).has = vi.fn(() => false);
751
+
752
+ const payload: ChatStreamPayload = {
753
+ model: 'o3-turbo',
754
+ messages: [{ role: 'user', content: 'test' }],
755
+ temperature: 0.5,
756
+ };
757
+
758
+ const result = handlePayload(payload);
759
+ expect(result).toEqual({ ...payload, apiMode: 'responses' });
760
+ });
761
+
762
+ it('should not modify payload for regular models', () => {
763
+ (responsesAPIModels as any).has = vi.fn(() => false);
764
+
765
+ const payload: ChatStreamPayload = {
766
+ model: 'claude-3-sonnet',
767
+ messages: [{ role: 'user', content: 'test' }],
768
+ temperature: 0.5,
769
+ };
770
+
771
+ const result = handlePayload(payload);
772
+ expect(result).toEqual(payload);
773
+ });
774
+ });
775
+
776
+ describe('Routers Function - Direct Testing', () => {
777
+ it('should generate routers with correct apiTypes', () => {
778
+ const options = { apiKey: 'test', baseURL: 'https://api.newapi.com/v1' };
779
+ const routers = params.routers(options);
780
+
781
+ expect(routers).toHaveLength(4);
782
+ expect(routers[0].apiType).toBe('anthropic');
783
+ expect(routers[1].apiType).toBe('google');
784
+ expect(routers[2].apiType).toBe('xai');
785
+ expect(routers[3].apiType).toBe('openai');
786
+ });
787
+
788
+ it('should process baseURL by removing version paths', () => {
789
+ const options = { apiKey: 'test', baseURL: 'https://custom.com/v1' };
790
+ const routers = params.routers(options);
791
+
792
+ // Anthropic router should use base URL without /v1
793
+ expect(routers[0].options.baseURL).toBe('https://custom.com');
794
+ // Google router should use base URL without /v1
795
+ expect(routers[1].options.baseURL).toBe('https://custom.com');
796
+ });
797
+
798
+ it('should handle baseURL with v1beta', () => {
799
+ const options = { apiKey: 'test', baseURL: 'https://custom.com/v1beta/' };
800
+ const routers = params.routers(options);
801
+
802
+ expect(routers[0].options.baseURL).toBe('https://custom.com');
803
+ });
804
+
805
+ it('should handle baseURL without version path', () => {
806
+ const options = { apiKey: 'test', baseURL: 'https://custom.com' };
807
+ const routers = params.routers(options);
808
+
809
+ expect(routers[0].options.baseURL).toBe('https://custom.com');
810
+ });
811
+
812
+ it('should configure xai router with /v1 path', () => {
813
+ const options = { apiKey: 'test', baseURL: 'https://custom.com/v1' };
814
+ const routers = params.routers(options);
815
+
816
+ expect(routers[2].options.baseURL).toBe('https://custom.com/v1');
817
+ });
818
+
819
+ it('should configure openai router with /v1 path', () => {
820
+ const options = { apiKey: 'test', baseURL: 'https://custom.com/v1' };
821
+ const routers = params.routers(options);
822
+
823
+ expect(routers[3].options.baseURL).toBe('https://custom.com/v1');
824
+ });
825
+
826
+ it('should configure openai router with handlePayload', () => {
827
+ const options = { apiKey: 'test', baseURL: 'https://custom.com/v1' };
828
+ const routers = params.routers(options);
829
+
830
+ expect((routers[3].options as any).chatCompletion?.handlePayload).toBe(handlePayload);
831
+ });
832
+
833
+ it('should filter anthropic models for anthropic router', () => {
834
+ mockDetectModelProvider.mockImplementation((id: string) => {
835
+ if (id.includes('claude')) return 'anthropic';
836
+ return 'openai';
837
+ });
838
+
839
+ const options = { apiKey: 'test', baseURL: 'https://custom.com' };
840
+ const routers = params.routers(options);
841
+
842
+ expect(routers[0].models).toBeDefined();
843
+ expect(Array.isArray(routers[0].models)).toBe(true);
844
+ });
845
+
846
+ it('should filter google models for google router', () => {
847
+ mockDetectModelProvider.mockImplementation((id: string) => {
848
+ if (id.includes('gemini')) return 'google';
849
+ return 'openai';
850
+ });
851
+
852
+ const options = { apiKey: 'test', baseURL: 'https://custom.com' };
853
+ const routers = params.routers(options);
854
+
855
+ expect(routers[1].models).toBeDefined();
856
+ expect(Array.isArray(routers[1].models)).toBe(true);
857
+ });
858
+
859
+ it('should filter xai models for xai router', () => {
860
+ mockDetectModelProvider.mockImplementation((id: string) => {
861
+ if (id.includes('grok')) return 'xai';
862
+ return 'openai';
863
+ });
864
+
865
+ const options = { apiKey: 'test', baseURL: 'https://custom.com' };
866
+ const routers = params.routers(options);
867
+
868
+ expect(routers[2].models).toBeDefined();
869
+ expect(Array.isArray(routers[2].models)).toBe(true);
870
+ });
871
+
872
+ it('should handle missing baseURL by using empty string', () => {
873
+ const options = { apiKey: 'test' }; // No baseURL
874
+ const routers = params.routers(options);
875
+
876
+ expect(routers).toHaveLength(4);
877
+ expect(routers[0].options.baseURL).toBe('');
878
+ expect(routers[3].options.baseURL).toBe('v1'); // urlJoin('', '/v1') returns 'v1'
879
+ });
880
+ });
881
+
882
+ describe('Models Function - Integration Testing', () => {
883
+ beforeEach(() => {
884
+ mockProcessMultiProviderModelList.mockReturnValue([]);
885
+ });
886
+
887
+ it('should fetch models and process with processMultiProviderModelList', async () => {
888
+ const mockClient = {
889
+ baseURL: 'https://api.newapi.com/v1',
890
+ apiKey: 'test-key',
891
+ models: {
892
+ list: vi.fn().mockResolvedValue({
893
+ data: [
894
+ {
895
+ id: 'test-model',
896
+ object: 'model',
897
+ created: 123,
898
+ owned_by: 'openai',
899
+ },
900
+ ],
901
+ }),
902
+ },
903
+ };
904
+
905
+ mockFetch.mockResolvedValue({
906
+ ok: false,
907
+ });
908
+
909
+ mockProcessMultiProviderModelList.mockReturnValue([
910
+ {
911
+ id: 'test-model',
912
+ displayName: 'Test Model',
913
+ },
914
+ ]);
915
+
916
+ const result = await params.models({ client: mockClient as any });
917
+
918
+ expect(mockClient.models.list).toHaveBeenCalled();
919
+ expect(mockProcessMultiProviderModelList).toHaveBeenCalledWith(
920
+ expect.arrayContaining([
921
+ expect.objectContaining({
922
+ id: 'test-model',
923
+ }),
924
+ ]),
925
+ 'newapi',
926
+ );
927
+ expect(result).toHaveLength(1);
928
+ });
929
+
930
+ it('should handle successful pricing fetch and enrich models', async () => {
931
+ const mockClient = {
932
+ baseURL: 'https://api.newapi.com/v1',
933
+ apiKey: 'test-key',
934
+ models: {
935
+ list: vi.fn().mockResolvedValue({
936
+ data: [
937
+ {
938
+ id: 'test-model',
939
+ object: 'model',
940
+ created: 123,
941
+ owned_by: 'openai',
942
+ },
943
+ ],
944
+ }),
945
+ },
946
+ };
947
+
948
+ mockFetch.mockResolvedValue({
949
+ ok: true,
950
+ json: async () => ({
951
+ success: true,
952
+ data: [
953
+ {
954
+ model_name: 'test-model',
955
+ quota_type: 0,
956
+ model_price: 10,
957
+ completion_ratio: 1.5,
958
+ enable_groups: ['default'],
959
+ },
960
+ ],
961
+ }),
962
+ });
963
+
964
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
965
+
966
+ const result = await params.models({ client: mockClient as any });
967
+
968
+ expect(mockFetch).toHaveBeenCalledWith('https://api.newapi.com/api/pricing', {
969
+ headers: {
970
+ Authorization: 'Bearer test-key',
971
+ },
972
+ });
973
+
974
+ expect(result[0].pricing).toEqual({
975
+ units: [
976
+ {
977
+ name: 'textInput',
978
+ rate: 20, // model_price * 2
979
+ strategy: 'fixed',
980
+ unit: 'millionTokens',
981
+ },
982
+ {
983
+ name: 'textOutput',
984
+ rate: 30, // 20 * 1.5
985
+ strategy: 'fixed',
986
+ unit: 'millionTokens',
987
+ },
988
+ ],
989
+ });
990
+ });
991
+
992
+ it('should handle pricing fetch with model_ratio instead of model_price', async () => {
993
+ const mockClient = {
994
+ baseURL: 'https://api.newapi.com/v1',
995
+ apiKey: 'test-key',
996
+ models: {
997
+ list: vi.fn().mockResolvedValue({
998
+ data: [
999
+ {
1000
+ id: 'test-model',
1001
+ object: 'model',
1002
+ created: 123,
1003
+ owned_by: 'openai',
1004
+ },
1005
+ ],
1006
+ }),
1007
+ },
1008
+ };
1009
+
1010
+ mockFetch.mockResolvedValue({
1011
+ ok: true,
1012
+ json: async () => ({
1013
+ success: true,
1014
+ data: [
1015
+ {
1016
+ model_name: 'test-model',
1017
+ quota_type: 0,
1018
+ model_ratio: 5,
1019
+ enable_groups: ['default'],
1020
+ },
1021
+ ],
1022
+ }),
1023
+ });
1024
+
1025
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1026
+
1027
+ const result = await params.models({ client: mockClient as any });
1028
+
1029
+ expect(result[0].pricing).toEqual({
1030
+ units: [
1031
+ {
1032
+ name: 'textInput',
1033
+ rate: 10, // model_ratio * 2
1034
+ strategy: 'fixed',
1035
+ unit: 'millionTokens',
1036
+ },
1037
+ {
1038
+ name: 'textOutput',
1039
+ rate: 10, // 10 * 1 (default completion_ratio)
1040
+ strategy: 'fixed',
1041
+ unit: 'millionTokens',
1042
+ },
1043
+ ],
1044
+ });
1045
+ });
1046
+
1047
+ it('should skip pricing for quota_type = 1 (pay-per-call)', async () => {
1048
+ const mockClient = {
1049
+ baseURL: 'https://api.newapi.com/v1',
1050
+ apiKey: 'test-key',
1051
+ models: {
1052
+ list: vi.fn().mockResolvedValue({
1053
+ data: [
1054
+ {
1055
+ id: 'test-model',
1056
+ object: 'model',
1057
+ created: 123,
1058
+ owned_by: 'openai',
1059
+ },
1060
+ ],
1061
+ }),
1062
+ },
1063
+ };
1064
+
1065
+ mockFetch.mockResolvedValue({
1066
+ ok: true,
1067
+ json: async () => ({
1068
+ success: true,
1069
+ data: [
1070
+ {
1071
+ model_name: 'test-model',
1072
+ quota_type: 1, // Pay-per-call, not supported
1073
+ model_price: 10,
1074
+ enable_groups: ['default'],
1075
+ },
1076
+ ],
1077
+ }),
1078
+ });
1079
+
1080
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1081
+
1082
+ const result = await params.models({ client: mockClient as any });
1083
+
1084
+ expect(result[0].pricing).toBeUndefined();
1085
+ });
1086
+
1087
+ it('should handle pricing fetch failure gracefully', async () => {
1088
+ const mockClient = {
1089
+ baseURL: 'https://api.newapi.com/v1',
1090
+ apiKey: 'test-key',
1091
+ models: {
1092
+ list: vi.fn().mockResolvedValue({
1093
+ data: [
1094
+ {
1095
+ id: 'test-model',
1096
+ object: 'model',
1097
+ created: 123,
1098
+ owned_by: 'openai',
1099
+ },
1100
+ ],
1101
+ }),
1102
+ },
1103
+ };
1104
+
1105
+ mockFetch.mockResolvedValue({
1106
+ ok: false,
1107
+ });
1108
+
1109
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1110
+
1111
+ const result = await params.models({ client: mockClient as any });
1112
+
1113
+ expect(result[0].pricing).toBeUndefined();
1114
+ });
1115
+
1116
+ it('should handle pricing fetch network error gracefully', async () => {
1117
+ const mockClient = {
1118
+ baseURL: 'https://api.newapi.com/v1',
1119
+ apiKey: 'test-key',
1120
+ models: {
1121
+ list: vi.fn().mockResolvedValue({
1122
+ data: [
1123
+ {
1124
+ id: 'test-model',
1125
+ object: 'model',
1126
+ created: 123,
1127
+ owned_by: 'openai',
1128
+ },
1129
+ ],
1130
+ }),
1131
+ },
1132
+ };
1133
+
1134
+ mockFetch.mockRejectedValue(new Error('Network error'));
1135
+
1136
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1137
+
1138
+ const result = await params.models({ client: mockClient as any });
1139
+
1140
+ expect(console.debug).toHaveBeenCalledWith(
1141
+ 'Failed to fetch NewAPI pricing info:',
1142
+ expect.any(Error),
1143
+ );
1144
+ expect(result[0].pricing).toBeUndefined();
1145
+ });
1146
+
1147
+ it('should handle pricing data with success=false', async () => {
1148
+ const mockClient = {
1149
+ baseURL: 'https://api.newapi.com/v1',
1150
+ apiKey: 'test-key',
1151
+ models: {
1152
+ list: vi.fn().mockResolvedValue({
1153
+ data: [
1154
+ {
1155
+ id: 'test-model',
1156
+ object: 'model',
1157
+ created: 123,
1158
+ owned_by: 'openai',
1159
+ },
1160
+ ],
1161
+ }),
1162
+ },
1163
+ };
1164
+
1165
+ mockFetch.mockResolvedValue({
1166
+ ok: true,
1167
+ json: async () => ({
1168
+ success: false,
1169
+ data: [],
1170
+ }),
1171
+ });
1172
+
1173
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1174
+
1175
+ const result = await params.models({ client: mockClient as any });
1176
+
1177
+ expect(result[0].pricing).toBeUndefined();
1178
+ });
1179
+
1180
+ it('should handle pricing data with missing data field', async () => {
1181
+ const mockClient = {
1182
+ baseURL: 'https://api.newapi.com/v1',
1183
+ apiKey: 'test-key',
1184
+ models: {
1185
+ list: vi.fn().mockResolvedValue({
1186
+ data: [
1187
+ {
1188
+ id: 'test-model',
1189
+ object: 'model',
1190
+ created: 123,
1191
+ owned_by: 'openai',
1192
+ },
1193
+ ],
1194
+ }),
1195
+ },
1196
+ };
1197
+
1198
+ mockFetch.mockResolvedValue({
1199
+ ok: true,
1200
+ json: async () => ({
1201
+ success: true,
1202
+ // Missing data field
1203
+ }),
1204
+ });
1205
+
1206
+ mockProcessMultiProviderModelList.mockImplementation((models) => models);
1207
+
1208
+ const result = await params.models({ client: mockClient as any });
1209
+
1210
+ expect(result[0].pricing).toBeUndefined();
1211
+ });
1212
+
1213
+ it('should handle empty model list', async () => {
1214
+ const mockClient = {
1215
+ baseURL: 'https://api.newapi.com/v1',
1216
+ apiKey: 'test-key',
1217
+ models: {
1218
+ list: vi.fn().mockResolvedValue({
1219
+ data: [],
1220
+ }),
1221
+ },
1222
+ };
1223
+
1224
+ mockFetch.mockResolvedValue({
1225
+ ok: false,
1226
+ });
1227
+
1228
+ mockProcessMultiProviderModelList.mockReturnValue([]);
1229
+
1230
+ const result = await params.models({ client: mockClient as any });
1231
+
1232
+ expect(result).toEqual([]);
1233
+ });
1234
+
1235
+ it('should handle undefined model data', async () => {
1236
+ const mockClient = {
1237
+ baseURL: 'https://api.newapi.com/v1',
1238
+ apiKey: 'test-key',
1239
+ models: {
1240
+ list: vi.fn().mockResolvedValue({
1241
+ data: undefined,
1242
+ }),
1243
+ },
1244
+ };
1245
+
1246
+ mockFetch.mockResolvedValue({
1247
+ ok: false,
1248
+ });
1249
+
1250
+ mockProcessMultiProviderModelList.mockReturnValue([]);
1251
+
1252
+ const result = await params.models({ client: mockClient as any });
1253
+
1254
+ expect(mockProcessMultiProviderModelList).toHaveBeenCalledWith([], 'newapi');
1255
+ expect(result).toEqual([]);
1256
+ });
1257
+
1258
+ it('should strip version paths from baseURL correctly', async () => {
1259
+ const testCases = [
1260
+ { input: 'https://api.com/v1', expected: 'https://api.com' },
1261
+ { input: 'https://api.com/v1/', expected: 'https://api.com' },
1262
+ { input: 'https://api.com/v1beta', expected: 'https://api.com' },
1263
+ { input: 'https://api.com/v2alpha/', expected: 'https://api.com' },
1264
+ { input: 'https://api.com', expected: 'https://api.com' },
1265
+ ];
1266
+
1267
+ for (const testCase of testCases) {
1268
+ const mockClient = {
1269
+ baseURL: testCase.input,
1270
+ apiKey: 'test-key',
1271
+ models: {
1272
+ list: vi.fn().mockResolvedValue({ data: [] }),
1273
+ },
1274
+ };
1275
+
1276
+ mockFetch.mockResolvedValue({ ok: false });
1277
+ mockProcessMultiProviderModelList.mockReturnValue([]);
1278
+
1279
+ await params.models({ client: mockClient as any });
1280
+
1281
+ if (testCase.input !== testCase.expected) {
1282
+ expect(mockFetch).toHaveBeenCalledWith(
1283
+ `${testCase.expected}/api/pricing`,
1284
+ expect.any(Object),
1285
+ );
1286
+ }
1287
+ }
1288
+ });
1289
+ });
1290
+
1291
+ describe('Runtime Instance Creation', () => {
1292
+ it('should create instance with minimal options', () => {
1293
+ const instance = new LobeNewAPIAI({ apiKey: 'test-key' });
1294
+ expect(instance).toBeDefined();
1295
+ expect(instance).toBeInstanceOf(LobeNewAPIAI);
1296
+ });
1297
+
1298
+ it('should create instance with custom baseURL', () => {
1299
+ const instance = new LobeNewAPIAI({
1300
+ apiKey: 'test-key',
1301
+ baseURL: 'https://custom.com/v1',
1302
+ });
1303
+ expect(instance).toBeDefined();
1304
+ });
1305
+
1306
+ it('should create instance with additional options', () => {
1307
+ const instance = new LobeNewAPIAI({
1308
+ apiKey: 'test-key',
1309
+ baseURL: 'https://custom.com',
1310
+ });
1311
+ expect(instance).toBeDefined();
1312
+ });
1313
+ });
650
1314
  });