@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.
- package/.cursor/rules/add-setting-env.mdc +175 -0
- package/.cursor/rules/db-migrations.mdc +25 -0
- package/.env.example +7 -0
- package/CHANGELOG.md +50 -0
- package/Dockerfile +3 -2
- package/Dockerfile.database +15 -3
- package/Dockerfile.pglite +3 -2
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +1 -0
- package/docs/self-hosting/advanced/feature-flags.mdx +25 -15
- package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +25 -15
- package/docs/self-hosting/environment-variables/basic.mdx +12 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +12 -0
- package/locales/ar/setting.json +8 -0
- package/locales/bg-BG/setting.json +8 -0
- package/locales/de-DE/setting.json +8 -0
- package/locales/en-US/setting.json +8 -0
- package/locales/es-ES/setting.json +8 -0
- package/locales/fa-IR/setting.json +8 -0
- package/locales/fr-FR/setting.json +8 -0
- package/locales/it-IT/setting.json +8 -0
- package/locales/ja-JP/setting.json +8 -0
- package/locales/ko-KR/setting.json +8 -0
- package/locales/nl-NL/setting.json +8 -0
- package/locales/pl-PL/setting.json +8 -0
- package/locales/pt-BR/setting.json +8 -0
- package/locales/ru-RU/setting.json +8 -0
- package/locales/tr-TR/setting.json +8 -0
- package/locales/vi-VN/setting.json +8 -0
- package/locales/zh-CN/setting.json +8 -0
- package/locales/zh-TW/setting.json +8 -0
- package/package.json +1 -1
- package/packages/agent-runtime/examples/tools-calling.ts +4 -3
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +559 -29
- package/packages/agent-runtime/src/core/runtime.ts +171 -43
- package/packages/agent-runtime/src/types/instruction.ts +32 -6
- package/packages/agent-runtime/src/types/runtime.ts +2 -2
- package/packages/agent-runtime/src/types/state.ts +1 -8
- package/packages/agent-runtime/vitest.config.mts +14 -0
- package/packages/const/src/settings/image.ts +8 -0
- package/packages/const/src/settings/index.ts +3 -0
- package/packages/context-engine/src/__tests__/pipeline.test.ts +485 -0
- package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +381 -0
- package/packages/context-engine/src/base/__tests__/BaseProvider.test.ts +392 -0
- package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +346 -0
- package/packages/context-engine/src/processors/__tests__/ToolCall.test.ts +552 -0
- package/packages/database/migrations/0038_add_image_user_settings.sql +1 -0
- package/packages/database/migrations/meta/0038_snapshot.json +7580 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +6 -0
- package/packages/database/src/models/user.ts +3 -1
- package/packages/database/src/schemas/user.ts +1 -0
- package/packages/file-loaders/src/loaders/docx/index.test.ts +0 -1
- package/packages/file-loaders/src/loaders/excel/__snapshots__/index.test.ts.snap +30 -0
- package/packages/file-loaders/src/loaders/excel/index.test.ts +8 -0
- package/packages/file-loaders/src/loaders/pptx/index.test.ts +25 -0
- package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
- package/packages/file-loaders/vitest.config.mts +8 -0
- package/packages/model-runtime/CLAUDE.md +5 -0
- package/packages/model-runtime/docs/test-coverage.md +706 -0
- package/packages/model-runtime/src/core/ModelRuntime.test.ts +231 -0
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +1 -1
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.test.ts +799 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +188 -4
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +41 -10
- package/packages/model-runtime/src/core/streams/openai/__snapshots__/responsesStream.test.ts.snap +439 -0
- package/packages/model-runtime/src/core/streams/openai/openai.test.ts +789 -0
- package/packages/model-runtime/src/core/streams/openai/responsesStream.test.ts +551 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +230 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +334 -37
- package/packages/model-runtime/src/providerTestUtils.ts +148 -145
- package/packages/model-runtime/src/providers/ai302/index.test.ts +60 -0
- package/packages/model-runtime/src/providers/ai302/index.ts +9 -4
- package/packages/model-runtime/src/providers/ai360/index.test.ts +1213 -1
- package/packages/model-runtime/src/providers/ai360/index.ts +9 -4
- package/packages/model-runtime/src/providers/aihubmix/index.test.ts +73 -0
- package/packages/model-runtime/src/providers/aihubmix/index.ts +6 -9
- package/packages/model-runtime/src/providers/akashchat/index.test.ts +433 -3
- package/packages/model-runtime/src/providers/akashchat/index.ts +12 -7
- package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +183 -29
- package/packages/model-runtime/src/providers/anthropic/generateObject.ts +40 -24
- package/packages/model-runtime/src/providers/azureai/index.test.ts +102 -0
- package/packages/model-runtime/src/providers/baichuan/index.test.ts +416 -26
- package/packages/model-runtime/src/providers/baichuan/index.ts +23 -20
- package/packages/model-runtime/src/providers/bedrock/index.test.ts +420 -2
- package/packages/model-runtime/src/providers/cerebras/index.test.ts +465 -0
- package/packages/model-runtime/src/providers/cerebras/index.ts +8 -3
- package/packages/model-runtime/src/providers/cohere/index.test.ts +1074 -1
- package/packages/model-runtime/src/providers/cohere/index.ts +8 -3
- package/packages/model-runtime/src/providers/cometapi/index.test.ts +439 -3
- package/packages/model-runtime/src/providers/cometapi/index.ts +8 -3
- package/packages/model-runtime/src/providers/deepseek/index.test.ts +116 -1
- package/packages/model-runtime/src/providers/deepseek/index.ts +8 -3
- package/packages/model-runtime/src/providers/fireworksai/index.test.ts +264 -3
- package/packages/model-runtime/src/providers/fireworksai/index.ts +8 -3
- package/packages/model-runtime/src/providers/giteeai/index.test.ts +325 -3
- package/packages/model-runtime/src/providers/giteeai/index.ts +23 -6
- package/packages/model-runtime/src/providers/github/index.test.ts +532 -3
- package/packages/model-runtime/src/providers/github/index.ts +8 -3
- package/packages/model-runtime/src/providers/groq/index.test.ts +344 -31
- package/packages/model-runtime/src/providers/groq/index.ts +8 -3
- package/packages/model-runtime/src/providers/higress/index.test.ts +142 -0
- package/packages/model-runtime/src/providers/higress/index.ts +8 -3
- package/packages/model-runtime/src/providers/huggingface/index.test.ts +612 -1
- package/packages/model-runtime/src/providers/huggingface/index.ts +9 -4
- package/packages/model-runtime/src/providers/hunyuan/index.test.ts +365 -1
- package/packages/model-runtime/src/providers/hunyuan/index.ts +9 -3
- package/packages/model-runtime/src/providers/infiniai/index.test.ts +71 -0
- package/packages/model-runtime/src/providers/internlm/index.test.ts +369 -2
- package/packages/model-runtime/src/providers/internlm/index.ts +10 -5
- package/packages/model-runtime/src/providers/jina/index.test.ts +164 -3
- package/packages/model-runtime/src/providers/jina/index.ts +8 -3
- package/packages/model-runtime/src/providers/lmstudio/index.test.ts +182 -3
- package/packages/model-runtime/src/providers/lmstudio/index.ts +8 -3
- package/packages/model-runtime/src/providers/mistral/index.test.ts +779 -27
- package/packages/model-runtime/src/providers/mistral/index.ts +8 -3
- package/packages/model-runtime/src/providers/modelscope/index.test.ts +232 -1
- package/packages/model-runtime/src/providers/modelscope/index.ts +8 -3
- package/packages/model-runtime/src/providers/moonshot/index.test.ts +489 -2
- package/packages/model-runtime/src/providers/moonshot/index.ts +8 -3
- package/packages/model-runtime/src/providers/nebius/index.test.ts +381 -3
- package/packages/model-runtime/src/providers/nebius/index.ts +8 -3
- package/packages/model-runtime/src/providers/newapi/index.test.ts +667 -3
- package/packages/model-runtime/src/providers/newapi/index.ts +6 -3
- package/packages/model-runtime/src/providers/nvidia/index.test.ts +168 -1
- package/packages/model-runtime/src/providers/nvidia/index.ts +12 -7
- package/packages/model-runtime/src/providers/ollama/index.test.ts +797 -1
- package/packages/model-runtime/src/providers/ollama/index.ts +8 -0
- package/packages/model-runtime/src/providers/ollamacloud/index.test.ts +411 -0
- package/packages/model-runtime/src/providers/ollamacloud/index.ts +8 -3
- package/packages/model-runtime/src/providers/openai/index.test.ts +171 -2
- package/packages/model-runtime/src/providers/openai/index.ts +8 -3
- package/packages/model-runtime/src/providers/openrouter/index.test.ts +1647 -95
- package/packages/model-runtime/src/providers/openrouter/index.ts +12 -7
- package/packages/model-runtime/src/providers/qiniu/index.test.ts +294 -1
- package/packages/model-runtime/src/providers/qiniu/index.ts +8 -3
- package/packages/model-runtime/src/providers/search1api/index.test.ts +1131 -11
- package/packages/model-runtime/src/providers/search1api/index.ts +10 -4
- package/packages/model-runtime/src/providers/sensenova/index.test.ts +1069 -1
- package/packages/model-runtime/src/providers/sensenova/index.ts +8 -3
- package/packages/model-runtime/src/providers/siliconcloud/index.test.ts +196 -0
- package/packages/model-runtime/src/providers/siliconcloud/index.ts +8 -3
- package/packages/model-runtime/src/providers/spark/index.test.ts +293 -1
- package/packages/model-runtime/src/providers/spark/index.ts +8 -3
- package/packages/model-runtime/src/providers/stepfun/index.test.ts +322 -3
- package/packages/model-runtime/src/providers/stepfun/index.ts +8 -3
- package/packages/model-runtime/src/providers/tencentcloud/index.test.ts +182 -3
- package/packages/model-runtime/src/providers/tencentcloud/index.ts +8 -3
- package/packages/model-runtime/src/providers/togetherai/index.test.ts +359 -4
- package/packages/model-runtime/src/providers/togetherai/index.ts +12 -5
- package/packages/model-runtime/src/providers/v0/index.test.ts +341 -0
- package/packages/model-runtime/src/providers/v0/index.ts +20 -6
- package/packages/model-runtime/src/providers/vercelaigateway/index.test.ts +710 -0
- package/packages/model-runtime/src/providers/vercelaigateway/index.ts +19 -13
- package/packages/model-runtime/src/providers/vllm/index.test.ts +45 -1
- package/packages/model-runtime/src/providers/volcengine/index.test.ts +75 -0
- package/packages/model-runtime/src/providers/wenxin/index.test.ts +144 -1
- package/packages/model-runtime/src/providers/wenxin/index.ts +8 -3
- package/packages/model-runtime/src/providers/xai/index.test.ts +105 -1
- package/packages/model-runtime/src/providers/xinference/index.test.ts +70 -1
- package/packages/model-runtime/src/providers/zeroone/index.test.ts +327 -3
- package/packages/model-runtime/src/providers/zeroone/index.ts +23 -6
- package/packages/model-runtime/src/providers/zhipu/index.test.ts +908 -236
- package/packages/model-runtime/src/providers/zhipu/index.ts +8 -3
- package/packages/model-runtime/src/types/structureOutput.ts +5 -1
- package/packages/model-runtime/vitest.config.mts +7 -1
- package/packages/types/src/aiChat.ts +20 -2
- package/packages/types/src/serverConfig.ts +7 -1
- package/packages/types/src/tool/index.ts +1 -0
- package/packages/types/src/tool/tool.ts +33 -0
- package/packages/types/src/user/settings/image.ts +3 -0
- package/packages/types/src/user/settings/index.ts +3 -0
- package/src/app/[variants]/(main)/settings/_layout/SettingsContent.tsx +3 -0
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +8 -3
- package/src/app/[variants]/(main)/settings/image/index.tsx +74 -0
- package/src/components/FormInput/FormSliderWithInput.tsx +40 -0
- package/src/components/FormInput/index.ts +1 -0
- package/src/envs/image.ts +27 -0
- package/src/features/Conversation/Messages/Assistant/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +2 -2
- package/src/hooks/useFetchAiImageConfig.ts +12 -17
- package/src/locales/default/setting.ts +8 -0
- package/src/server/globalConfig/index.ts +5 -0
- package/src/server/routers/lambda/aiChat.ts +2 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/image/slices/generationConfig/action.test.ts +17 -0
- package/src/store/image/slices/generationConfig/action.ts +18 -21
- package/src/store/image/slices/generationConfig/initialState.ts +3 -2
- package/src/store/user/slices/common/action.ts +1 -0
- 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
|
+
});
|