@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,9 +1,9 @@
1
1
  // @vitest-environment node
2
2
  import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
3
- import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { testProvider } from '../../providerTestUtils';
6
- import { LobeMistralAI } from './index';
6
+ import { LobeMistralAI, params } from './index';
7
7
 
8
8
  testProvider({
9
9
  provider: 'mistral',
@@ -11,18 +11,19 @@ testProvider({
11
11
  chatModel: 'open-mistral-7b',
12
12
  Runtime: LobeMistralAI,
13
13
  chatDebugEnv: 'DEBUG_MISTRAL_CHAT_COMPLETION',
14
-
15
14
  test: {
16
- skipAPICall: true,
15
+ skipAPICall: true, // Mistral has custom payload handling (excludeUsage, temperature normalization)
17
16
  },
18
17
  });
19
18
 
19
+ // Mock the console.error to avoid polluting test output
20
+ vi.spyOn(console, 'error').mockImplementation(() => {});
21
+
20
22
  let instance: LobeOpenAICompatibleRuntime;
21
23
 
22
24
  beforeEach(() => {
23
25
  instance = new LobeMistralAI({ apiKey: 'test' });
24
26
 
25
- // 使用 vi.spyOn 来模拟 chat.completions.create 方法
26
27
  vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
27
28
  new ReadableStream() as any,
28
29
  );
@@ -32,35 +33,786 @@ afterEach(() => {
32
33
  vi.clearAllMocks();
33
34
  });
34
35
 
35
- describe('specific LobeMistralAI tests', () => {
36
- it(`should call API with corresponding options`, async () => {
37
- // Arrange
38
- const mockStream = new ReadableStream();
39
- const mockResponse = Promise.resolve(mockStream);
36
+ describe('LobeMistralAI - custom features', () => {
37
+ describe('Debug Configuration', () => {
38
+ it('should disable debug by default', () => {
39
+ delete process.env.DEBUG_MISTRAL_CHAT_COMPLETION;
40
+ const result = params.debug.chatCompletion();
41
+ expect(result).toBe(false);
42
+ });
43
+
44
+ it('should enable debug when env is set', () => {
45
+ process.env.DEBUG_MISTRAL_CHAT_COMPLETION = '1';
46
+ const result = params.debug.chatCompletion();
47
+ expect(result).toBe(true);
48
+ delete process.env.DEBUG_MISTRAL_CHAT_COMPLETION;
49
+ });
50
+ });
51
+
52
+ describe('handlePayload', () => {
53
+ it('should exclude stream_options (excludeUsage)', async () => {
54
+ await instance.chat({
55
+ messages: [{ content: 'Hello', role: 'user' }],
56
+ model: 'open-mistral-7b',
57
+ temperature: 0.7,
58
+ });
59
+
60
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
61
+ expect.not.objectContaining({ stream_options: expect.anything() }),
62
+ expect.anything(),
63
+ );
64
+ });
65
+
66
+ it('should normalize temperature (divide by 2)', async () => {
67
+ await instance.chat({
68
+ messages: [{ content: 'Hello', role: 'user' }],
69
+ model: 'open-mistral-7b',
70
+ temperature: 0.8,
71
+ });
72
+
73
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
74
+ expect.objectContaining({ temperature: 0.4 }),
75
+ expect.anything(),
76
+ );
77
+ });
78
+
79
+ it('should normalize temperature to 0.5 when temperature is 1', async () => {
80
+ await instance.chat({
81
+ messages: [{ content: 'Hello', role: 'user' }],
82
+ model: 'open-mistral-7b',
83
+ temperature: 1,
84
+ });
85
+
86
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
87
+ expect.objectContaining({ temperature: 0.5 }),
88
+ expect.anything(),
89
+ );
90
+ });
91
+
92
+ it('should normalize temperature to 0 when temperature is 0', async () => {
93
+ await instance.chat({
94
+ messages: [{ content: 'Hello', role: 'user' }],
95
+ model: 'open-mistral-7b',
96
+ temperature: 0,
97
+ });
98
+
99
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
100
+ expect.objectContaining({ temperature: 0 }),
101
+ expect.anything(),
102
+ );
103
+ });
104
+
105
+ it('should handle high temperature values (2.0 normalized to 1.0)', async () => {
106
+ await instance.chat({
107
+ messages: [{ content: 'Hello', role: 'user' }],
108
+ model: 'open-mistral-7b',
109
+ temperature: 2.0,
110
+ });
111
+
112
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
113
+ expect.objectContaining({ temperature: 1.0 }),
114
+ expect.anything(),
115
+ );
116
+ });
117
+
118
+ it('should set stream to true by default', async () => {
119
+ await instance.chat({
120
+ messages: [{ content: 'Hello', role: 'user' }],
121
+ model: 'open-mistral-7b',
122
+ });
123
+
124
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
125
+ expect.objectContaining({ stream: true }),
126
+ expect.anything(),
127
+ );
128
+ });
40
129
 
41
- (instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
130
+ it('should preserve top_p without modification', async () => {
131
+ await instance.chat({
132
+ messages: [{ content: 'Hello', role: 'user' }],
133
+ model: 'open-mistral-7b',
134
+ top_p: 0.9,
135
+ });
42
136
 
43
- // Act
44
- const result = await instance.chat({
45
- max_tokens: 1024,
46
- messages: [{ content: 'Hello', role: 'user' }],
47
- model: 'open-mistral-7b',
48
- temperature: 0.7,
49
- top_p: 1,
137
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
138
+ expect.objectContaining({ top_p: 0.9 }),
139
+ expect.anything(),
140
+ );
50
141
  });
51
142
 
52
- // Assert
53
- expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
54
- {
143
+ it('should preserve max_tokens without modification', async () => {
144
+ await instance.chat({
145
+ messages: [{ content: 'Hello', role: 'user' }],
146
+ model: 'open-mistral-7b',
55
147
  max_tokens: 1024,
148
+ });
149
+
150
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
151
+ expect.objectContaining({ max_tokens: 1024 }),
152
+ expect.anything(),
153
+ );
154
+ });
155
+
156
+ it('should include tools when provided', async () => {
157
+ const tools = [
158
+ {
159
+ type: 'function' as const,
160
+ function: { name: 'test_tool', description: 'A test tool', parameters: {} },
161
+ },
162
+ ];
163
+
164
+ await instance.chat({
56
165
  messages: [{ content: 'Hello', role: 'user' }],
57
166
  model: 'open-mistral-7b',
58
- stream: true,
59
- temperature: 0.35,
60
- top_p: 1,
167
+ tools,
168
+ });
169
+
170
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
171
+ expect.objectContaining({ tools }),
172
+ expect.anything(),
173
+ );
174
+ });
175
+
176
+ it('should not include tools when not provided', async () => {
177
+ await instance.chat({
178
+ messages: [{ content: 'Hello', role: 'user' }],
179
+ model: 'open-mistral-7b',
180
+ });
181
+
182
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
183
+ expect(callArgs).not.toHaveProperty('tools');
184
+ });
185
+
186
+ it('should preserve messages as-is', async () => {
187
+ const messages = [
188
+ { content: 'Hello', role: 'user' as const },
189
+ { content: 'Hi there!', role: 'assistant' as const },
190
+ { content: 'How are you?', role: 'user' as const },
191
+ ];
192
+
193
+ await instance.chat({
194
+ messages,
195
+ model: 'open-mistral-7b',
196
+ });
197
+
198
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
199
+ expect.objectContaining({ messages }),
200
+ expect.anything(),
201
+ );
202
+ });
203
+
204
+ it('should preserve model name', async () => {
205
+ await instance.chat({
206
+ messages: [{ content: 'Hello', role: 'user' }],
207
+ model: 'mistral-large-latest',
208
+ });
209
+
210
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
211
+ expect.objectContaining({ model: 'mistral-large-latest' }),
212
+ expect.anything(),
213
+ );
214
+ });
215
+
216
+ it('should handle combined parameters correctly', async () => {
217
+ await instance.chat({
218
+ messages: [{ content: 'Hello', role: 'user' }],
219
+ model: 'mistral-large-latest',
220
+ temperature: 1.4,
221
+ top_p: 0.85,
222
+ max_tokens: 2048,
223
+ tools: [
224
+ {
225
+ type: 'function' as const,
226
+ function: { name: 'calculator', description: 'Calculate', parameters: {} },
227
+ },
228
+ ],
229
+ });
230
+
231
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
232
+ expect.objectContaining({
233
+ model: 'mistral-large-latest',
234
+ temperature: 0.7, // 1.4 / 2
235
+ top_p: 0.85,
236
+ max_tokens: 2048,
237
+ stream: true,
238
+ tools: expect.any(Array),
239
+ }),
240
+ expect.anything(),
241
+ );
242
+ });
243
+
244
+ it('should not include undefined parameters in payload', async () => {
245
+ await instance.chat({
246
+ messages: [{ content: 'Hello', role: 'user' }],
247
+ model: 'open-mistral-7b',
248
+ });
249
+
250
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
251
+ expect(callArgs).not.toHaveProperty('temperature');
252
+ expect(callArgs).not.toHaveProperty('top_p');
253
+ expect(callArgs).not.toHaveProperty('max_tokens');
254
+ });
255
+ });
256
+
257
+ describe('exports', () => {
258
+ it('should export params object', () => {
259
+ expect(params).toBeDefined();
260
+ expect(params.provider).toBe('mistral');
261
+ expect(params.baseURL).toBe('https://api.mistral.ai/v1');
262
+ expect(params.chatCompletion).toBeDefined();
263
+ expect(params.debug).toBeDefined();
264
+ expect(params.models).toBeDefined();
265
+ });
266
+
267
+ it('should export chatCompletion configuration', () => {
268
+ expect(params.chatCompletion.excludeUsage).toBe(true);
269
+ expect(params.chatCompletion.noUserId).toBe(true);
270
+ expect(params.chatCompletion.handlePayload).toBeDefined();
271
+ });
272
+ });
273
+
274
+ describe('models', () => {
275
+ const mockClient = {
276
+ models: {
277
+ list: vi.fn(),
61
278
  },
62
- { headers: { Accept: '*/*' } },
63
- );
64
- expect(result).toBeInstanceOf(Response);
279
+ };
280
+
281
+ beforeEach(() => {
282
+ vi.clearAllMocks();
283
+ });
284
+
285
+ it('should fetch and process models with function calling capability', async () => {
286
+ mockClient.models.list.mockResolvedValue({
287
+ data: [
288
+ {
289
+ id: 'mistral-large-latest',
290
+ max_context_length: 128000,
291
+ capabilities: {
292
+ function_calling: true,
293
+ vision: false,
294
+ },
295
+ description: 'Mistral Large model',
296
+ },
297
+ {
298
+ id: 'mistral-small-latest',
299
+ max_context_length: 32000,
300
+ capabilities: {
301
+ function_calling: true,
302
+ vision: false,
303
+ },
304
+ description: 'Mistral Small model',
305
+ },
306
+ ],
307
+ });
308
+
309
+ const models = await params.models({ client: mockClient as any });
310
+
311
+ expect(models).toHaveLength(2);
312
+ expect(models[0]).toMatchObject({
313
+ id: 'mistral-large-latest',
314
+ contextWindowTokens: 128000,
315
+ functionCall: true,
316
+ vision: false,
317
+ description: 'Mistral Large model',
318
+ });
319
+ expect(models[1]).toMatchObject({
320
+ id: 'mistral-small-latest',
321
+ contextWindowTokens: 32000,
322
+ functionCall: true,
323
+ vision: false,
324
+ description: 'Mistral Small model',
325
+ });
326
+ });
327
+
328
+ it('should detect vision capability from model capabilities', async () => {
329
+ mockClient.models.list.mockResolvedValue({
330
+ data: [
331
+ {
332
+ id: 'pixtral-12b-latest',
333
+ max_context_length: 128000,
334
+ capabilities: {
335
+ function_calling: true,
336
+ vision: true,
337
+ },
338
+ description: 'Pixtral vision model',
339
+ },
340
+ ],
341
+ });
342
+
343
+ const models = await params.models({ client: mockClient as any });
344
+
345
+ expect(models).toHaveLength(1);
346
+ expect(models[0]).toMatchObject({
347
+ id: 'pixtral-12b-latest',
348
+ vision: true,
349
+ functionCall: true,
350
+ });
351
+ });
352
+
353
+ it('should merge with known model list for display name and enabled status', async () => {
354
+ mockClient.models.list.mockResolvedValue({
355
+ data: [
356
+ {
357
+ id: 'mistral-large-latest',
358
+ max_context_length: 128000,
359
+ capabilities: {
360
+ function_calling: true,
361
+ vision: false,
362
+ },
363
+ description: 'Latest Mistral Large',
364
+ },
365
+ ],
366
+ });
367
+
368
+ const models = await params.models({ client: mockClient as any });
369
+
370
+ expect(models).toHaveLength(1);
371
+ // Should have displayName and enabled from LOBE_DEFAULT_MODEL_LIST
372
+ expect(models[0].displayName).toBeDefined();
373
+ });
374
+
375
+ it('should handle models not in known model list', async () => {
376
+ mockClient.models.list.mockResolvedValue({
377
+ data: [
378
+ {
379
+ id: 'unknown-mistral-model',
380
+ max_context_length: 8192,
381
+ capabilities: {
382
+ function_calling: false,
383
+ vision: false,
384
+ },
385
+ description: 'Unknown model',
386
+ },
387
+ ],
388
+ });
389
+
390
+ const models = await params.models({ client: mockClient as any });
391
+
392
+ expect(models).toHaveLength(1);
393
+ expect(models[0]).toMatchObject({
394
+ id: 'unknown-mistral-model',
395
+ contextWindowTokens: 8192,
396
+ functionCall: false,
397
+ vision: false,
398
+ displayName: undefined,
399
+ enabled: false,
400
+ description: 'Unknown model',
401
+ });
402
+ });
403
+
404
+ it('should handle case-insensitive model matching', async () => {
405
+ mockClient.models.list.mockResolvedValue({
406
+ data: [
407
+ {
408
+ id: 'MISTRAL-LARGE-LATEST',
409
+ max_context_length: 128000,
410
+ capabilities: {
411
+ function_calling: true,
412
+ vision: false,
413
+ },
414
+ description: 'Large model',
415
+ },
416
+ ],
417
+ });
418
+
419
+ const models = await params.models({ client: mockClient as any });
420
+
421
+ expect(models).toHaveLength(1);
422
+ expect(models[0].id).toBe('MISTRAL-LARGE-LATEST');
423
+ // Should match with lowercase in LOBE_DEFAULT_MODEL_LIST
424
+ expect(models[0].displayName).toBeDefined();
425
+ });
426
+
427
+ it('should detect reasoning capability from known model list', async () => {
428
+ mockClient.models.list.mockResolvedValue({
429
+ data: [
430
+ {
431
+ id: 'mistral-large-latest',
432
+ max_context_length: 128000,
433
+ capabilities: {
434
+ function_calling: true,
435
+ vision: false,
436
+ },
437
+ description: 'Latest large model',
438
+ },
439
+ ],
440
+ });
441
+
442
+ const models = await params.models({ client: mockClient as any });
443
+
444
+ expect(models).toHaveLength(1);
445
+ expect(models[0]).toHaveProperty('reasoning');
446
+ // reasoning should be false unless specified in LOBE_DEFAULT_MODEL_LIST
447
+ expect(models[0].reasoning).toBe(false);
448
+ });
449
+
450
+ it('should handle empty model list', async () => {
451
+ mockClient.models.list.mockResolvedValue({
452
+ data: [],
453
+ });
454
+
455
+ const models = await params.models({ client: mockClient as any });
456
+
457
+ expect(models).toEqual([]);
458
+ });
459
+
460
+ it('should combine multiple capabilities correctly', async () => {
461
+ mockClient.models.list.mockResolvedValue({
462
+ data: [
463
+ {
464
+ id: 'pixtral-large-latest',
465
+ max_context_length: 128000,
466
+ capabilities: {
467
+ function_calling: true,
468
+ vision: true,
469
+ },
470
+ description: 'Multimodal large model',
471
+ },
472
+ ],
473
+ });
474
+
475
+ const models = await params.models({ client: mockClient as any });
476
+
477
+ expect(models).toHaveLength(1);
478
+ expect(models[0]).toMatchObject({
479
+ id: 'pixtral-large-latest',
480
+ contextWindowTokens: 128000,
481
+ functionCall: true,
482
+ vision: true,
483
+ description: 'Multimodal large model',
484
+ });
485
+ });
486
+
487
+ it('should preserve context window tokens accurately', async () => {
488
+ mockClient.models.list.mockResolvedValue({
489
+ data: [
490
+ {
491
+ id: 'model-1',
492
+ max_context_length: 4096,
493
+ capabilities: { function_calling: false, vision: false },
494
+ description: 'Small context',
495
+ },
496
+ {
497
+ id: 'model-2',
498
+ max_context_length: 32768,
499
+ capabilities: { function_calling: true, vision: false },
500
+ description: 'Medium context',
501
+ },
502
+ {
503
+ id: 'model-3',
504
+ max_context_length: 200000,
505
+ capabilities: { function_calling: true, vision: true },
506
+ description: 'Large context',
507
+ },
508
+ ],
509
+ });
510
+
511
+ const models = await params.models({ client: mockClient as any });
512
+
513
+ expect(models).toHaveLength(3);
514
+ expect(models[0].contextWindowTokens).toBe(4096);
515
+ expect(models[1].contextWindowTokens).toBe(32768);
516
+ expect(models[2].contextWindowTokens).toBe(200000);
517
+ });
518
+
519
+ it('should handle mixed capability models', async () => {
520
+ mockClient.models.list.mockResolvedValue({
521
+ data: [
522
+ {
523
+ id: 'text-only-model',
524
+ max_context_length: 8192,
525
+ capabilities: {
526
+ function_calling: true,
527
+ vision: false,
528
+ },
529
+ description: 'Text only',
530
+ },
531
+ {
532
+ id: 'vision-model',
533
+ max_context_length: 16384,
534
+ capabilities: {
535
+ function_calling: false,
536
+ vision: true,
537
+ },
538
+ description: 'Vision only',
539
+ },
540
+ {
541
+ id: 'basic-model',
542
+ max_context_length: 4096,
543
+ capabilities: {
544
+ function_calling: false,
545
+ vision: false,
546
+ },
547
+ description: 'Basic model',
548
+ },
549
+ ],
550
+ });
551
+
552
+ const models = await params.models({ client: mockClient as any });
553
+
554
+ expect(models).toHaveLength(3);
555
+ expect(models[0].functionCall).toBe(true);
556
+ expect(models[0].vision).toBe(false);
557
+ expect(models[1].functionCall).toBe(false);
558
+ expect(models[1].vision).toBe(true);
559
+ expect(models[2].functionCall).toBe(false);
560
+ expect(models[2].vision).toBe(false);
561
+ });
562
+
563
+ it('should handle all properties from API response', async () => {
564
+ mockClient.models.list.mockResolvedValue({
565
+ data: [
566
+ {
567
+ id: 'complete-model',
568
+ max_context_length: 100000,
569
+ capabilities: {
570
+ function_calling: true,
571
+ vision: true,
572
+ },
573
+ description: 'A complete test model',
574
+ },
575
+ ],
576
+ });
577
+
578
+ const models = await params.models({ client: mockClient as any });
579
+
580
+ expect(models).toHaveLength(1);
581
+ const model = models[0];
582
+ expect(model).toHaveProperty('id');
583
+ expect(model).toHaveProperty('contextWindowTokens');
584
+ expect(model).toHaveProperty('functionCall');
585
+ expect(model).toHaveProperty('vision');
586
+ expect(model).toHaveProperty('description');
587
+ expect(model).toHaveProperty('displayName');
588
+ expect(model).toHaveProperty('enabled');
589
+ expect(model).toHaveProperty('reasoning');
590
+ });
591
+
592
+ it('should handle API errors gracefully', async () => {
593
+ mockClient.models.list.mockRejectedValue(new Error('Network error'));
594
+
595
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow('Network error');
596
+ });
597
+
598
+ it('should handle null or undefined data gracefully', async () => {
599
+ mockClient.models.list.mockResolvedValue({
600
+ data: null as any,
601
+ });
602
+
603
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow();
604
+ });
605
+
606
+ it('should handle model with missing capabilities', async () => {
607
+ mockClient.models.list.mockResolvedValue({
608
+ data: [
609
+ {
610
+ id: 'incomplete-model',
611
+ max_context_length: 8192,
612
+ capabilities: {} as any,
613
+ description: 'Model with missing capabilities',
614
+ },
615
+ ],
616
+ });
617
+
618
+ const models = await params.models({ client: mockClient as any });
619
+
620
+ expect(models).toHaveLength(1);
621
+ expect(models[0].functionCall).toBeUndefined();
622
+ expect(models[0].vision).toBeUndefined();
623
+ });
624
+
625
+ it('should handle model with null capabilities', async () => {
626
+ mockClient.models.list.mockResolvedValue({
627
+ data: [
628
+ {
629
+ id: 'null-caps-model',
630
+ max_context_length: 8192,
631
+ capabilities: null as any,
632
+ description: 'Model with null capabilities',
633
+ },
634
+ ],
635
+ });
636
+
637
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow();
638
+ });
639
+
640
+ it('should filter out falsy values', async () => {
641
+ mockClient.models.list.mockResolvedValue({
642
+ data: [
643
+ {
644
+ id: 'valid-model',
645
+ max_context_length: 8192,
646
+ capabilities: {
647
+ function_calling: false,
648
+ vision: false,
649
+ },
650
+ description: 'Valid model',
651
+ },
652
+ null,
653
+ undefined,
654
+ ].filter(Boolean),
655
+ });
656
+
657
+ const models = await params.models({ client: mockClient as any });
658
+
659
+ expect(models).toHaveLength(1);
660
+ expect(models[0].id).toBe('valid-model');
661
+ });
662
+ });
663
+
664
+ describe('handlePayload - edge cases', () => {
665
+ it('should handle payload with frequency_penalty', async () => {
666
+ await instance.chat({
667
+ messages: [{ content: 'Hello', role: 'user' }],
668
+ model: 'open-mistral-7b',
669
+ frequency_penalty: 0.5,
670
+ });
671
+
672
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
673
+ // frequency_penalty is not part of the parameters handled by resolveParameters in mistral
674
+ expect(callArgs).not.toHaveProperty('frequency_penalty');
675
+ });
676
+
677
+ it('should handle payload with presence_penalty', async () => {
678
+ await instance.chat({
679
+ messages: [{ content: 'Hello', role: 'user' }],
680
+ model: 'open-mistral-7b',
681
+ presence_penalty: 0.5,
682
+ });
683
+
684
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
685
+ // presence_penalty is not part of the parameters handled by resolveParameters in mistral
686
+ expect(callArgs).not.toHaveProperty('presence_penalty');
687
+ });
688
+
689
+ it('should handle empty messages array', async () => {
690
+ await instance.chat({
691
+ messages: [],
692
+ model: 'open-mistral-7b',
693
+ });
694
+
695
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
696
+ expect.objectContaining({
697
+ messages: [],
698
+ }),
699
+ expect.anything(),
700
+ );
701
+ });
702
+
703
+ it('should handle tool_choice parameter', async () => {
704
+ const tools = [
705
+ {
706
+ type: 'function' as const,
707
+ function: { name: 'test_tool', description: 'A test tool', parameters: {} },
708
+ },
709
+ ];
710
+
711
+ await instance.chat({
712
+ messages: [{ content: 'Hello', role: 'user' }],
713
+ model: 'open-mistral-7b',
714
+ tools,
715
+ tool_choice: 'auto',
716
+ });
717
+
718
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
719
+ expect(callArgs.tools).toEqual(tools);
720
+ // tool_choice is not explicitly handled in mistral handlePayload
721
+ });
722
+
723
+ it('should handle response_format parameter', async () => {
724
+ await instance.chat({
725
+ messages: [{ content: 'Hello', role: 'user' }],
726
+ model: 'open-mistral-7b',
727
+ response_format: { type: 'json_object' },
728
+ });
729
+
730
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
731
+ // response_format is not explicitly handled in mistral handlePayload
732
+ expect(callArgs).not.toHaveProperty('response_format');
733
+ });
734
+
735
+ it('should handle very small temperature values', async () => {
736
+ await instance.chat({
737
+ messages: [{ content: 'Hello', role: 'user' }],
738
+ model: 'open-mistral-7b',
739
+ temperature: 0.01,
740
+ });
741
+
742
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
743
+ expect.objectContaining({
744
+ temperature: 0.005, // 0.01 / 2
745
+ }),
746
+ expect.anything(),
747
+ );
748
+ });
749
+
750
+ it('should handle decimal temperature values', async () => {
751
+ await instance.chat({
752
+ messages: [{ content: 'Hello', role: 'user' }],
753
+ model: 'open-mistral-7b',
754
+ temperature: 1.5,
755
+ });
756
+
757
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
758
+ expect.objectContaining({
759
+ temperature: 0.75, // 1.5 / 2
760
+ }),
761
+ expect.anything(),
762
+ );
763
+ });
764
+
765
+ it('should handle very small top_p values', async () => {
766
+ await instance.chat({
767
+ messages: [{ content: 'Hello', role: 'user' }],
768
+ model: 'open-mistral-7b',
769
+ top_p: 0.01,
770
+ });
771
+
772
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
773
+ expect.objectContaining({
774
+ top_p: 0.01,
775
+ }),
776
+ expect.anything(),
777
+ );
778
+ });
779
+
780
+ it('should handle large max_tokens values', async () => {
781
+ await instance.chat({
782
+ messages: [{ content: 'Hello', role: 'user' }],
783
+ model: 'open-mistral-7b',
784
+ max_tokens: 100000,
785
+ });
786
+
787
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
788
+ expect.objectContaining({
789
+ max_tokens: 100000,
790
+ }),
791
+ expect.anything(),
792
+ );
793
+ });
794
+
795
+ it('should handle complex message with multiple roles', async () => {
796
+ const messages = [
797
+ { content: 'You are a helpful assistant', role: 'system' as const },
798
+ { content: 'Hello', role: 'user' as const },
799
+ { content: 'Hi! How can I help?', role: 'assistant' as const },
800
+ { content: 'What is 2+2?', role: 'user' as const },
801
+ { content: '2+2 equals 4', role: 'assistant' as const },
802
+ { content: 'Thanks', role: 'user' as const },
803
+ ];
804
+
805
+ await instance.chat({
806
+ messages,
807
+ model: 'open-mistral-7b',
808
+ });
809
+
810
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
811
+ expect.objectContaining({
812
+ messages,
813
+ }),
814
+ expect.anything(),
815
+ );
816
+ });
65
817
  });
66
818
  });