@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,8 +1,10 @@
1
1
  // @vitest-environment node
2
+ import { LobeOpenAICompatibleRuntime } from '@lobechat/model-runtime';
2
3
  import { ModelProvider } from 'model-bank';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
5
 
4
6
  import { testProvider } from '../../providerTestUtils';
5
- import { LobeCohereAI } from './index';
7
+ import { LobeCohereAI, params } from './index';
6
8
 
7
9
  const provider = ModelProvider.Cohere;
8
10
  const defaultBaseURL = 'https://api.cohere.ai/compatibility/v1';
@@ -17,3 +19,1074 @@ testProvider({
17
19
  skipAPICall: true,
18
20
  },
19
21
  });
22
+
23
+ // Mock the console.error to avoid polluting test output
24
+ vi.spyOn(console, 'error').mockImplementation(() => {});
25
+
26
+ let instance: LobeOpenAICompatibleRuntime;
27
+
28
+ beforeEach(() => {
29
+ instance = new LobeCohereAI({ apiKey: 'test' });
30
+
31
+ // Mock chat.completions.create method
32
+ vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
33
+ new ReadableStream() as any,
34
+ );
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.clearAllMocks();
39
+ });
40
+
41
+ describe('LobeCohereAI - custom features', () => {
42
+ describe('Debug Configuration', () => {
43
+ it('should disable debug by default', () => {
44
+ delete process.env.DEBUG_COHERE_CHAT_COMPLETION;
45
+ const result = params.debug.chatCompletion();
46
+ expect(result).toBe(false);
47
+ });
48
+
49
+ it('should enable debug when env is set', () => {
50
+ process.env.DEBUG_COHERE_CHAT_COMPLETION = '1';
51
+ const result = params.debug.chatCompletion();
52
+ expect(result).toBe(true);
53
+ delete process.env.DEBUG_COHERE_CHAT_COMPLETION;
54
+ });
55
+ });
56
+
57
+ describe('handlePayload - parameter constraints', () => {
58
+ it('should clamp frequency_penalty to [0, 1] range', async () => {
59
+ // Test upper bound
60
+ await instance.chat({
61
+ messages: [{ content: 'Hello', role: 'user' }],
62
+ model: 'command-r7b',
63
+ frequency_penalty: 1.5,
64
+ });
65
+
66
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
67
+ expect.objectContaining({ frequency_penalty: 1 }),
68
+ expect.anything(),
69
+ );
70
+ });
71
+
72
+ it('should clamp frequency_penalty negative values to 0', async () => {
73
+ await instance.chat({
74
+ messages: [{ content: 'Hello', role: 'user' }],
75
+ model: 'command-r7b',
76
+ frequency_penalty: -0.5,
77
+ });
78
+
79
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
80
+ expect.objectContaining({ frequency_penalty: 0 }),
81
+ expect.anything(),
82
+ );
83
+ });
84
+
85
+ it('should clamp presence_penalty to [0, 1] range', async () => {
86
+ // Test upper bound
87
+ await instance.chat({
88
+ messages: [{ content: 'Hello', role: 'user' }],
89
+ model: 'command-r7b',
90
+ presence_penalty: 1.5,
91
+ });
92
+
93
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
94
+ expect.objectContaining({ presence_penalty: 1 }),
95
+ expect.anything(),
96
+ );
97
+ });
98
+
99
+ it('should clamp presence_penalty negative values to 0', async () => {
100
+ await instance.chat({
101
+ messages: [{ content: 'Hello', role: 'user' }],
102
+ model: 'command-r7b',
103
+ presence_penalty: -0.3,
104
+ });
105
+
106
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
107
+ expect.objectContaining({ presence_penalty: 0 }),
108
+ expect.anything(),
109
+ );
110
+ });
111
+
112
+ it('should clamp top_p to [0, 1] range', async () => {
113
+ // Test upper bound
114
+ await instance.chat({
115
+ messages: [{ content: 'Hello', role: 'user' }],
116
+ model: 'command-r7b',
117
+ top_p: 1.2,
118
+ });
119
+
120
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
121
+ expect.objectContaining({ top_p: 1 }),
122
+ expect.anything(),
123
+ );
124
+ });
125
+
126
+ it('should clamp top_p negative values to 0', async () => {
127
+ await instance.chat({
128
+ messages: [{ content: 'Hello', role: 'user' }],
129
+ model: 'command-r7b',
130
+ top_p: -0.1,
131
+ });
132
+
133
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
134
+ expect.objectContaining({ top_p: 0 }),
135
+ expect.anything(),
136
+ );
137
+ });
138
+
139
+ it('should accept valid frequency_penalty values', async () => {
140
+ await instance.chat({
141
+ messages: [{ content: 'Hello', role: 'user' }],
142
+ model: 'command-r7b',
143
+ frequency_penalty: 0.5,
144
+ });
145
+
146
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
147
+ expect.objectContaining({ frequency_penalty: 0.5 }),
148
+ expect.anything(),
149
+ );
150
+ });
151
+
152
+ it('should accept valid presence_penalty values', async () => {
153
+ await instance.chat({
154
+ messages: [{ content: 'Hello', role: 'user' }],
155
+ model: 'command-r7b',
156
+ presence_penalty: 0.7,
157
+ });
158
+
159
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
160
+ expect.objectContaining({ presence_penalty: 0.7 }),
161
+ expect.anything(),
162
+ );
163
+ });
164
+
165
+ it('should accept valid top_p values', async () => {
166
+ await instance.chat({
167
+ messages: [{ content: 'Hello', role: 'user' }],
168
+ model: 'command-r7b',
169
+ top_p: 0.9,
170
+ });
171
+
172
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
173
+ expect.objectContaining({ top_p: 0.9 }),
174
+ expect.anything(),
175
+ );
176
+ });
177
+
178
+ it('should handle all penalty parameters together', async () => {
179
+ await instance.chat({
180
+ messages: [{ content: 'Hello', role: 'user' }],
181
+ model: 'command-r7b',
182
+ frequency_penalty: 0.3,
183
+ presence_penalty: 0.4,
184
+ top_p: 0.8,
185
+ });
186
+
187
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
188
+ expect.objectContaining({
189
+ frequency_penalty: 0.3,
190
+ presence_penalty: 0.4,
191
+ top_p: 0.8,
192
+ }),
193
+ expect.anything(),
194
+ );
195
+ });
196
+
197
+ it('should handle boundary values correctly', async () => {
198
+ await instance.chat({
199
+ messages: [{ content: 'Hello', role: 'user' }],
200
+ model: 'command-r7b',
201
+ frequency_penalty: 0,
202
+ presence_penalty: 1,
203
+ top_p: 0.5,
204
+ });
205
+
206
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
207
+ expect.objectContaining({
208
+ frequency_penalty: 0,
209
+ presence_penalty: 1,
210
+ top_p: 0.5,
211
+ }),
212
+ expect.anything(),
213
+ );
214
+ });
215
+
216
+ it('should not normalize temperature (keep original value)', async () => {
217
+ await instance.chat({
218
+ messages: [{ content: 'Hello', role: 'user' }],
219
+ model: 'command-r7b',
220
+ temperature: 1.0,
221
+ });
222
+
223
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
224
+ expect.objectContaining({ temperature: 1.0 }),
225
+ expect.anything(),
226
+ );
227
+ });
228
+
229
+ it('should preserve other payload properties', async () => {
230
+ await instance.chat({
231
+ messages: [{ content: 'Hello', role: 'user' }],
232
+ model: 'command-r7b',
233
+ temperature: 0.5,
234
+ max_tokens: 100,
235
+ stream: true,
236
+ });
237
+
238
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
239
+ expect.objectContaining({
240
+ messages: [{ content: 'Hello', role: 'user' }],
241
+ model: 'command-r7b',
242
+ temperature: 0.5,
243
+ max_tokens: 100,
244
+ stream: true,
245
+ }),
246
+ expect.anything(),
247
+ );
248
+ });
249
+
250
+ it('should omit undefined penalty parameters', async () => {
251
+ await instance.chat({
252
+ messages: [{ content: 'Hello', role: 'user' }],
253
+ model: 'command-r7b',
254
+ temperature: 0.5,
255
+ });
256
+
257
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
258
+ expect(callArgs).not.toHaveProperty('frequency_penalty');
259
+ expect(callArgs).not.toHaveProperty('presence_penalty');
260
+ expect(callArgs).not.toHaveProperty('top_p');
261
+ });
262
+
263
+ it('should handle edge case: all penalties at maximum', async () => {
264
+ await instance.chat({
265
+ messages: [{ content: 'Hello', role: 'user' }],
266
+ model: 'command-r7b',
267
+ frequency_penalty: 2.0,
268
+ presence_penalty: 2.0,
269
+ top_p: 2.0,
270
+ });
271
+
272
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
273
+ expect.objectContaining({
274
+ frequency_penalty: 1,
275
+ presence_penalty: 1,
276
+ top_p: 1,
277
+ }),
278
+ expect.anything(),
279
+ );
280
+ });
281
+
282
+ it('should handle edge case: all penalties at minimum', async () => {
283
+ await instance.chat({
284
+ messages: [{ content: 'Hello', role: 'user' }],
285
+ model: 'command-r7b',
286
+ frequency_penalty: -1.0,
287
+ presence_penalty: -1.0,
288
+ top_p: -1.0,
289
+ });
290
+
291
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
292
+ expect.objectContaining({
293
+ frequency_penalty: 0,
294
+ presence_penalty: 0,
295
+ top_p: 0,
296
+ }),
297
+ expect.anything(),
298
+ );
299
+ });
300
+ });
301
+
302
+ describe('handlePayload - excludeUsage and noUserId', () => {
303
+ it('should verify excludeUsage is set to true', () => {
304
+ expect(params.chatCompletion.excludeUsage).toBe(true);
305
+ });
306
+
307
+ it('should verify noUserId is set to true', () => {
308
+ expect(params.chatCompletion.noUserId).toBe(true);
309
+ });
310
+ });
311
+
312
+ describe('models', () => {
313
+ const mockClient = {
314
+ baseURL: 'https://api.cohere.ai/compatibility/v1',
315
+ models: {
316
+ list: vi.fn(),
317
+ },
318
+ };
319
+
320
+ beforeEach(() => {
321
+ vi.clearAllMocks();
322
+ });
323
+
324
+ it('should fetch and process models with tools support', async () => {
325
+ mockClient.models.list.mockResolvedValue({
326
+ body: {
327
+ models: [
328
+ {
329
+ name: 'command-r-plus',
330
+ context_length: 128000,
331
+ features: ['tools', 'chat'],
332
+ supports_vision: false,
333
+ },
334
+ {
335
+ name: 'command-r',
336
+ context_length: 128000,
337
+ features: ['tools', 'chat'],
338
+ supports_vision: false,
339
+ },
340
+ ],
341
+ },
342
+ });
343
+
344
+ const models = await params.models({ client: mockClient as any });
345
+
346
+ expect(models).toHaveLength(2);
347
+ expect(models[0]).toMatchObject({
348
+ id: 'command-r-plus',
349
+ contextWindowTokens: 128000,
350
+ functionCall: true, // Has tools in features
351
+ vision: false,
352
+ });
353
+ expect(models[1]).toMatchObject({
354
+ id: 'command-r',
355
+ contextWindowTokens: 128000,
356
+ functionCall: true, // Has tools in features
357
+ vision: false,
358
+ });
359
+ });
360
+
361
+ it('should detect vision support from supports_vision flag', async () => {
362
+ mockClient.models.list.mockResolvedValue({
363
+ body: {
364
+ models: [
365
+ {
366
+ name: 'command-r-plus-08-2024',
367
+ context_length: 128000,
368
+ features: ['tools', 'chat'],
369
+ supports_vision: true,
370
+ },
371
+ ],
372
+ },
373
+ });
374
+
375
+ const models = await params.models({ client: mockClient as any });
376
+
377
+ expect(models).toHaveLength(1);
378
+ expect(models[0]).toMatchObject({
379
+ id: 'command-r-plus-08-2024',
380
+ vision: true,
381
+ });
382
+ });
383
+
384
+ it('should handle models without features (null)', async () => {
385
+ mockClient.models.list.mockResolvedValue({
386
+ body: {
387
+ models: [
388
+ {
389
+ name: 'command',
390
+ context_length: 4096,
391
+ features: null,
392
+ supports_vision: false,
393
+ },
394
+ ],
395
+ },
396
+ });
397
+
398
+ const models = await params.models({ client: mockClient as any });
399
+
400
+ expect(models).toHaveLength(1);
401
+ expect(models[0]).toMatchObject({
402
+ id: 'command',
403
+ contextWindowTokens: 4096,
404
+ functionCall: false, // No tools in features, fallback to known model
405
+ vision: false,
406
+ });
407
+ });
408
+
409
+ it('should handle models with features but no tools', async () => {
410
+ mockClient.models.list.mockResolvedValue({
411
+ body: {
412
+ models: [
413
+ {
414
+ name: 'command-light',
415
+ context_length: 4096,
416
+ features: ['chat'],
417
+ supports_vision: false,
418
+ },
419
+ ],
420
+ },
421
+ });
422
+
423
+ const models = await params.models({ client: mockClient as any });
424
+
425
+ expect(models).toHaveLength(1);
426
+ expect(models[0]).toMatchObject({
427
+ id: 'command-light',
428
+ functionCall: false, // No tools in features
429
+ vision: false,
430
+ });
431
+ });
432
+
433
+ it('should merge with known model list for display name and enabled status', async () => {
434
+ mockClient.models.list.mockResolvedValue({
435
+ body: {
436
+ models: [
437
+ {
438
+ name: 'command-r-plus',
439
+ context_length: 128000,
440
+ features: ['tools', 'chat'],
441
+ supports_vision: false,
442
+ },
443
+ ],
444
+ },
445
+ });
446
+
447
+ const models = await params.models({ client: mockClient as any });
448
+
449
+ expect(models).toHaveLength(1);
450
+ // Should have displayName and enabled from LOBE_DEFAULT_MODEL_LIST
451
+ expect(models[0].displayName).toBeDefined();
452
+ expect(models[0].enabled).toBeDefined();
453
+ });
454
+
455
+ it('should handle models not in known model list', async () => {
456
+ mockClient.models.list.mockResolvedValue({
457
+ body: {
458
+ models: [
459
+ {
460
+ name: 'unknown-cohere-model',
461
+ context_length: 8192,
462
+ features: ['chat'],
463
+ supports_vision: false,
464
+ },
465
+ ],
466
+ },
467
+ });
468
+
469
+ const models = await params.models({ client: mockClient as any });
470
+
471
+ expect(models).toHaveLength(1);
472
+ expect(models[0]).toMatchObject({
473
+ id: 'unknown-cohere-model',
474
+ contextWindowTokens: 8192,
475
+ displayName: undefined,
476
+ enabled: false,
477
+ functionCall: false,
478
+ vision: false,
479
+ });
480
+ });
481
+
482
+ it('should handle case-insensitive model matching', async () => {
483
+ mockClient.models.list.mockResolvedValue({
484
+ body: {
485
+ models: [
486
+ {
487
+ name: 'COMMAND-R-PLUS',
488
+ context_length: 128000,
489
+ features: ['tools'],
490
+ supports_vision: false,
491
+ },
492
+ ],
493
+ },
494
+ });
495
+
496
+ const models = await params.models({ client: mockClient as any });
497
+
498
+ expect(models).toHaveLength(1);
499
+ expect(models[0].id).toBe('COMMAND-R-PLUS');
500
+ // Should match with lowercase in LOBE_DEFAULT_MODEL_LIST
501
+ expect(models[0].displayName).toBeDefined();
502
+ });
503
+
504
+ it('should combine capabilities from both API and known model list', async () => {
505
+ mockClient.models.list.mockResolvedValue({
506
+ body: {
507
+ models: [
508
+ {
509
+ name: 'command-r-plus',
510
+ context_length: 128000,
511
+ features: ['tools', 'chat'],
512
+ supports_vision: false,
513
+ },
514
+ ],
515
+ },
516
+ });
517
+
518
+ const models = await params.models({ client: mockClient as any });
519
+
520
+ expect(models).toHaveLength(1);
521
+ // Should combine API features with known model abilities
522
+ expect(models[0].functionCall).toBe(true); // From features
523
+ expect(models[0].vision).toBe(false); // From API
524
+ });
525
+
526
+ it('should handle empty model list', async () => {
527
+ mockClient.models.list.mockResolvedValue({
528
+ body: {
529
+ models: [],
530
+ },
531
+ });
532
+
533
+ const models = await params.models({ client: mockClient as any });
534
+
535
+ expect(models).toEqual([]);
536
+ });
537
+
538
+ it('should change client baseURL to v1 endpoint', async () => {
539
+ const clientWithBaseURL = {
540
+ baseURL: 'https://api.cohere.ai/compatibility/v1',
541
+ models: {
542
+ list: vi.fn().mockResolvedValue({
543
+ body: { models: [] },
544
+ }),
545
+ },
546
+ };
547
+
548
+ await params.models({ client: clientWithBaseURL as any });
549
+
550
+ expect(clientWithBaseURL.baseURL).toBe('https://api.cohere.com/v1');
551
+ });
552
+
553
+ it('should handle models with all capabilities', async () => {
554
+ mockClient.models.list.mockResolvedValue({
555
+ body: {
556
+ models: [
557
+ {
558
+ name: 'command-r-plus-vision',
559
+ context_length: 128000,
560
+ features: ['tools', 'chat', 'vision'],
561
+ supports_vision: true,
562
+ },
563
+ ],
564
+ },
565
+ });
566
+
567
+ const models = await params.models({ client: mockClient as any });
568
+
569
+ expect(models).toHaveLength(1);
570
+ expect(models[0]).toMatchObject({
571
+ id: 'command-r-plus-vision',
572
+ contextWindowTokens: 128000,
573
+ functionCall: true,
574
+ vision: true,
575
+ });
576
+ });
577
+
578
+ it('should preserve abilities from known model list when API has no features', async () => {
579
+ mockClient.models.list.mockResolvedValue({
580
+ body: {
581
+ models: [
582
+ {
583
+ name: 'command-r-plus',
584
+ context_length: 128000,
585
+ features: null,
586
+ supports_vision: false,
587
+ },
588
+ ],
589
+ },
590
+ });
591
+
592
+ const models = await params.models({ client: mockClient as any });
593
+
594
+ expect(models).toHaveLength(1);
595
+ // Should use known model abilities as fallback
596
+ expect(models[0].functionCall).toBeDefined();
597
+ });
598
+
599
+ it('should handle models with various context lengths', async () => {
600
+ mockClient.models.list.mockResolvedValue({
601
+ body: {
602
+ models: [
603
+ {
604
+ name: 'command-light',
605
+ context_length: 4096,
606
+ features: ['chat'],
607
+ supports_vision: false,
608
+ },
609
+ {
610
+ name: 'command-r',
611
+ context_length: 128000,
612
+ features: ['tools', 'chat'],
613
+ supports_vision: false,
614
+ },
615
+ {
616
+ name: 'command-r-plus',
617
+ context_length: 256000,
618
+ features: ['tools', 'chat'],
619
+ supports_vision: false,
620
+ },
621
+ ],
622
+ },
623
+ });
624
+
625
+ const models = await params.models({ client: mockClient as any });
626
+
627
+ expect(models).toHaveLength(3);
628
+ expect(models[0].contextWindowTokens).toBe(4096);
629
+ expect(models[1].contextWindowTokens).toBe(128000);
630
+ expect(models[2].contextWindowTokens).toBe(256000);
631
+ });
632
+
633
+ it('should handle complex features array', async () => {
634
+ mockClient.models.list.mockResolvedValue({
635
+ body: {
636
+ models: [
637
+ {
638
+ name: 'command-r-plus',
639
+ context_length: 128000,
640
+ features: ['tools', 'chat', 'embeddings', 'rerank'],
641
+ supports_vision: false,
642
+ },
643
+ ],
644
+ },
645
+ });
646
+
647
+ const models = await params.models({ client: mockClient as any });
648
+
649
+ expect(models).toHaveLength(1);
650
+ expect(models[0].functionCall).toBe(true); // Should detect tools
651
+ });
652
+
653
+ it('should handle API errors gracefully', async () => {
654
+ mockClient.models.list.mockRejectedValue(new Error('API Error'));
655
+
656
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow('API Error');
657
+ });
658
+
659
+ it('should handle network timeout errors', async () => {
660
+ mockClient.models.list.mockRejectedValue(new Error('Network timeout'));
661
+
662
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow('Network timeout');
663
+ });
664
+
665
+ it('should handle invalid API response structure', async () => {
666
+ mockClient.models.list.mockResolvedValue({
667
+ body: {
668
+ // Missing models array
669
+ },
670
+ });
671
+
672
+ // Should throw error when trying to map over undefined
673
+ await expect(params.models({ client: mockClient as any })).rejects.toThrow();
674
+ });
675
+
676
+ it('should handle malformed model data', async () => {
677
+ mockClient.models.list.mockResolvedValue({
678
+ body: {
679
+ models: [
680
+ {
681
+ // Missing required fields
682
+ name: 'incomplete-model',
683
+ } as any,
684
+ ],
685
+ },
686
+ });
687
+
688
+ const models = await params.models({ client: mockClient as any });
689
+
690
+ expect(models).toHaveLength(1);
691
+ expect(models[0].id).toBe('incomplete-model');
692
+ // Should handle undefined values
693
+ expect(models[0].contextWindowTokens).toBeUndefined();
694
+ });
695
+
696
+ it('should verify baseURL changes to v1 endpoint for models API', async () => {
697
+ const customClient = {
698
+ baseURL: 'https://api.cohere.ai/compatibility/v1',
699
+ models: {
700
+ list: vi.fn().mockResolvedValue({
701
+ body: {
702
+ models: [
703
+ {
704
+ name: 'test-model',
705
+ context_length: 8000,
706
+ features: ['chat'],
707
+ supports_vision: false,
708
+ },
709
+ ],
710
+ },
711
+ }),
712
+ },
713
+ };
714
+
715
+ await params.models({ client: customClient as any });
716
+
717
+ // Should mutate baseURL to v1
718
+ expect(customClient.baseURL).toBe('https://api.cohere.com/v1');
719
+ });
720
+
721
+ it('should handle very large context lengths', async () => {
722
+ mockClient.models.list.mockResolvedValue({
723
+ body: {
724
+ models: [
725
+ {
726
+ name: 'command-r-plus-extended',
727
+ context_length: 1000000,
728
+ features: ['tools'],
729
+ supports_vision: false,
730
+ },
731
+ ],
732
+ },
733
+ });
734
+
735
+ const models = await params.models({ client: mockClient as any });
736
+
737
+ expect(models).toHaveLength(1);
738
+ expect(models[0].contextWindowTokens).toBe(1000000);
739
+ });
740
+
741
+ it('should handle models with zero context length', async () => {
742
+ mockClient.models.list.mockResolvedValue({
743
+ body: {
744
+ models: [
745
+ {
746
+ name: 'test-model',
747
+ context_length: 0,
748
+ features: null,
749
+ supports_vision: false,
750
+ },
751
+ ],
752
+ },
753
+ });
754
+
755
+ const models = await params.models({ client: mockClient as any });
756
+
757
+ expect(models).toHaveLength(1);
758
+ expect(models[0].contextWindowTokens).toBe(0);
759
+ });
760
+
761
+ it('should merge vision capability from both API and known model list', async () => {
762
+ mockClient.models.list.mockResolvedValue({
763
+ body: {
764
+ models: [
765
+ {
766
+ name: 'command-r-plus-08-2024',
767
+ context_length: 128000,
768
+ features: ['tools', 'chat'],
769
+ supports_vision: true,
770
+ },
771
+ ],
772
+ },
773
+ });
774
+
775
+ const models = await params.models({ client: mockClient as any });
776
+
777
+ expect(models).toHaveLength(1);
778
+ // Vision should be true from either API or known model list
779
+ expect(models[0].vision).toBe(true);
780
+ });
781
+
782
+ it('should correctly filter and map all model fields', async () => {
783
+ mockClient.models.list.mockResolvedValue({
784
+ body: {
785
+ models: [
786
+ {
787
+ name: 'command-r-plus',
788
+ context_length: 128000,
789
+ features: ['tools', 'chat'],
790
+ supports_vision: true,
791
+ },
792
+ {
793
+ name: 'command-r',
794
+ context_length: 128000,
795
+ features: ['tools'],
796
+ supports_vision: false,
797
+ },
798
+ ],
799
+ },
800
+ });
801
+
802
+ const models = await params.models({ client: mockClient as any });
803
+
804
+ expect(models).toHaveLength(2);
805
+
806
+ // Verify all required fields are present
807
+ models.forEach((model) => {
808
+ expect(model).toHaveProperty('id');
809
+ expect(model).toHaveProperty('contextWindowTokens');
810
+ expect(model).toHaveProperty('functionCall');
811
+ expect(model).toHaveProperty('vision');
812
+ expect(model).toHaveProperty('enabled');
813
+ });
814
+ });
815
+ });
816
+
817
+ describe('baseURL configuration', () => {
818
+ it('should use correct default baseURL', () => {
819
+ expect(params.baseURL).toBe('https://api.cohere.ai/compatibility/v1');
820
+ });
821
+
822
+ it('should initialize instance with custom baseURL', () => {
823
+ const customInstance = new LobeCohereAI({
824
+ apiKey: 'test',
825
+ baseURL: 'https://custom.cohere.ai/v1',
826
+ });
827
+
828
+ expect(customInstance).toBeDefined();
829
+ });
830
+ });
831
+
832
+ describe('provider configuration', () => {
833
+ it('should have correct provider ID', () => {
834
+ expect(params.provider).toBe(ModelProvider.Cohere);
835
+ });
836
+
837
+ it('should export params object', () => {
838
+ expect(params).toBeDefined();
839
+ expect(params).toHaveProperty('baseURL');
840
+ expect(params).toHaveProperty('chatCompletion');
841
+ expect(params).toHaveProperty('debug');
842
+ expect(params).toHaveProperty('models');
843
+ expect(params).toHaveProperty('provider');
844
+ });
845
+ });
846
+
847
+ describe('chatCompletion configuration', () => {
848
+ it('should have excludeUsage set to true', () => {
849
+ expect(params.chatCompletion.excludeUsage).toBe(true);
850
+ });
851
+
852
+ it('should have noUserId set to true', () => {
853
+ expect(params.chatCompletion.noUserId).toBe(true);
854
+ });
855
+
856
+ it('should have handlePayload function', () => {
857
+ expect(params.chatCompletion.handlePayload).toBeDefined();
858
+ expect(typeof params.chatCompletion.handlePayload).toBe('function');
859
+ });
860
+ });
861
+
862
+ describe('edge cases for payload handling', () => {
863
+ it('should handle missing optional parameters gracefully', async () => {
864
+ await instance.chat({
865
+ messages: [{ content: 'Hello', role: 'user' }],
866
+ model: 'command-r7b',
867
+ // No temperature, frequency_penalty, presence_penalty, or top_p
868
+ });
869
+
870
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
871
+ expect(callArgs).toHaveProperty('messages');
872
+ expect(callArgs).toHaveProperty('model');
873
+ expect(callArgs).not.toHaveProperty('temperature');
874
+ expect(callArgs).not.toHaveProperty('frequency_penalty');
875
+ expect(callArgs).not.toHaveProperty('presence_penalty');
876
+ expect(callArgs).not.toHaveProperty('top_p');
877
+ });
878
+
879
+ it('should handle very small positive penalty values', async () => {
880
+ await instance.chat({
881
+ messages: [{ content: 'Hello', role: 'user' }],
882
+ model: 'command-r7b',
883
+ frequency_penalty: 0.001,
884
+ presence_penalty: 0.0001,
885
+ top_p: 0.01,
886
+ });
887
+
888
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
889
+ expect.objectContaining({
890
+ frequency_penalty: 0.001,
891
+ presence_penalty: 0.0001,
892
+ top_p: 0.01,
893
+ }),
894
+ expect.anything(),
895
+ );
896
+ });
897
+
898
+ it('should handle exact boundary values (0 and 1)', async () => {
899
+ await instance.chat({
900
+ messages: [{ content: 'Hello', role: 'user' }],
901
+ model: 'command-r7b',
902
+ frequency_penalty: 1.0,
903
+ presence_penalty: 0.0,
904
+ top_p: 1.0,
905
+ });
906
+
907
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
908
+ expect.objectContaining({
909
+ frequency_penalty: 1.0,
910
+ presence_penalty: 0.0,
911
+ top_p: 1.0,
912
+ }),
913
+ expect.anything(),
914
+ );
915
+ });
916
+
917
+ it('should handle multiple messages with different roles', async () => {
918
+ await instance.chat({
919
+ messages: [
920
+ { content: 'System prompt', role: 'system' },
921
+ { content: 'User message 1', role: 'user' },
922
+ { content: 'Assistant response', role: 'assistant' },
923
+ { content: 'User message 2', role: 'user' },
924
+ ],
925
+ model: 'command-r7b',
926
+ });
927
+
928
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
929
+ expect.objectContaining({
930
+ messages: [
931
+ { content: 'System prompt', role: 'system' },
932
+ { content: 'User message 1', role: 'user' },
933
+ { content: 'Assistant response', role: 'assistant' },
934
+ { content: 'User message 2', role: 'user' },
935
+ ],
936
+ }),
937
+ expect.anything(),
938
+ );
939
+ });
940
+
941
+ it('should handle max_tokens parameter', async () => {
942
+ await instance.chat({
943
+ messages: [{ content: 'Hello', role: 'user' }],
944
+ model: 'command-r7b',
945
+ max_tokens: 4096,
946
+ });
947
+
948
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
949
+ expect.objectContaining({
950
+ max_tokens: 4096,
951
+ }),
952
+ expect.anything(),
953
+ );
954
+ });
955
+
956
+ it('should handle stream parameter', async () => {
957
+ await instance.chat({
958
+ messages: [{ content: 'Hello', role: 'user' }],
959
+ model: 'command-r7b',
960
+ stream: true,
961
+ });
962
+
963
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
964
+ expect.objectContaining({
965
+ stream: true,
966
+ }),
967
+ expect.anything(),
968
+ );
969
+ });
970
+
971
+ it('should handle tools parameter', async () => {
972
+ const tools = [
973
+ {
974
+ type: 'function' as const,
975
+ function: {
976
+ name: 'get_weather',
977
+ description: 'Get weather',
978
+ parameters: { type: 'object', properties: {} },
979
+ },
980
+ },
981
+ ];
982
+
983
+ await instance.chat({
984
+ messages: [{ content: 'Hello', role: 'user' }],
985
+ model: 'command-r7b',
986
+ tools,
987
+ });
988
+
989
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
990
+ expect.objectContaining({
991
+ tools,
992
+ }),
993
+ expect.anything(),
994
+ );
995
+ });
996
+
997
+ it('should handle combined complex payload', async () => {
998
+ const tools = [
999
+ {
1000
+ type: 'function' as const,
1001
+ function: {
1002
+ name: 'calculate',
1003
+ description: 'Calculate',
1004
+ parameters: { type: 'object', properties: {} },
1005
+ },
1006
+ },
1007
+ ];
1008
+
1009
+ await instance.chat({
1010
+ messages: [
1011
+ { content: 'System', role: 'system' },
1012
+ { content: 'Hello', role: 'user' },
1013
+ ],
1014
+ model: 'command-r-plus',
1015
+ temperature: 0.7,
1016
+ max_tokens: 2048,
1017
+ top_p: 0.95,
1018
+ frequency_penalty: 0.1,
1019
+ presence_penalty: 0.2,
1020
+ stream: true,
1021
+ tools,
1022
+ });
1023
+
1024
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
1025
+ expect.objectContaining({
1026
+ messages: [
1027
+ { content: 'System', role: 'system' },
1028
+ { content: 'Hello', role: 'user' },
1029
+ ],
1030
+ model: 'command-r-plus',
1031
+ temperature: 0.7,
1032
+ max_tokens: 2048,
1033
+ top_p: 0.95,
1034
+ frequency_penalty: 0.1,
1035
+ presence_penalty: 0.2,
1036
+ stream: true,
1037
+ tools,
1038
+ }),
1039
+ expect.anything(),
1040
+ );
1041
+ });
1042
+
1043
+ it('should handle extreme out-of-range values correctly', async () => {
1044
+ await instance.chat({
1045
+ messages: [{ content: 'Hello', role: 'user' }],
1046
+ model: 'command-r7b',
1047
+ frequency_penalty: 10.0,
1048
+ presence_penalty: -5.0,
1049
+ top_p: 100.0,
1050
+ });
1051
+
1052
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
1053
+ expect.objectContaining({
1054
+ frequency_penalty: 1, // Clamped to max
1055
+ presence_penalty: 0, // Clamped to min
1056
+ top_p: 1, // Clamped to max
1057
+ }),
1058
+ expect.anything(),
1059
+ );
1060
+ });
1061
+
1062
+ it('should handle temperature parameter when present', async () => {
1063
+ await instance.chat({
1064
+ messages: [{ content: 'Hello', role: 'user' }],
1065
+ model: 'command-r7b',
1066
+ temperature: 0.7,
1067
+ });
1068
+
1069
+ const callArgs = vi.mocked(instance['client'].chat.completions.create).mock.calls[0][0];
1070
+ expect(callArgs.temperature).toBe(0.7);
1071
+ });
1072
+
1073
+ it('should not modify messages array', async () => {
1074
+ const messages = [
1075
+ { content: 'User message', role: 'user' as const },
1076
+ { content: 'Assistant reply', role: 'assistant' as const },
1077
+ ];
1078
+
1079
+ await instance.chat({
1080
+ messages,
1081
+ model: 'command-r7b',
1082
+ });
1083
+
1084
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
1085
+ expect.objectContaining({
1086
+ messages,
1087
+ }),
1088
+ expect.anything(),
1089
+ );
1090
+ });
1091
+ });
1092
+ });