@lobehub/chat 1.45.16 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -1
- package/CHANGELOG.md +51 -0
- package/README.ja-JP.md +2 -2
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/advanced/knowledge-base.mdx +9 -0
- package/docs/self-hosting/advanced/knowledge-base.zh-CN.mdx +9 -0
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/providers.json +3 -0
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +4 -4
- package/src/app/(main)/settings/provider/(detail)/[id]/index.tsx +0 -1
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/aiModels/lmstudio.ts +27 -0
- package/src/config/aiModels/minimax.ts +50 -0
- package/src/config/knowledge.ts +2 -0
- package/src/config/modelProviders/index.ts +6 -3
- package/src/config/modelProviders/lmstudio.ts +25 -0
- package/src/const/settings/knowledge.ts +25 -0
- package/src/const/settings/llm.ts +9 -0
- package/src/database/schemas/ragEvals.ts +2 -2
- package/src/libs/agent-runtime/AgentRuntime.ts +7 -0
- package/src/libs/agent-runtime/bedrock/index.ts +64 -3
- package/src/libs/agent-runtime/lmstudio/index.test.ts +255 -0
- package/src/libs/agent-runtime/lmstudio/index.ts +11 -0
- package/src/libs/agent-runtime/minimax/index.ts +39 -183
- package/src/libs/agent-runtime/ollama/index.ts +37 -1
- package/src/libs/agent-runtime/types/type.ts +1 -0
- package/src/libs/agent-runtime/utils/streams/index.ts +0 -1
- package/src/server/globalConfig/index.ts +6 -0
- package/src/server/globalConfig/parseFilesConfig.test.ts +17 -0
- package/src/server/globalConfig/parseFilesConfig.ts +57 -0
- package/src/server/routers/async/file.ts +8 -8
- package/src/server/routers/lambda/chunk.ts +12 -16
- package/src/types/knowledgeBase/index.ts +8 -0
- package/src/types/user/settings/filesConfig.ts +9 -0
- package/src/types/user/settings/keyVaults.ts +1 -0
- package/src/app/(backend)/webapi/chat/minimax/route.test.ts +0 -26
- package/src/app/(backend)/webapi/chat/minimax/route.ts +0 -6
- package/src/libs/agent-runtime/minimax/index.test.ts +0 -275
- package/src/libs/agent-runtime/utils/streams/minimax.test.ts +0 -27
- package/src/libs/agent-runtime/utils/streams/minimax.ts +0 -57
@@ -1,275 +0,0 @@
|
|
1
|
-
// @vitest-environment edge-runtime
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
-
|
4
|
-
import { ChatStreamPayload, ModelProvider } from '@/libs/agent-runtime';
|
5
|
-
import * as debugStreamModule from '@/libs/agent-runtime/utils/debugStream';
|
6
|
-
|
7
|
-
import { LobeMinimaxAI } from './index';
|
8
|
-
|
9
|
-
const provider = ModelProvider.Minimax;
|
10
|
-
const bizErrorType = 'ProviderBizError';
|
11
|
-
const invalidErrorType = 'InvalidProviderAPIKey';
|
12
|
-
|
13
|
-
const encoder = new TextEncoder();
|
14
|
-
|
15
|
-
// Mock the console.error to avoid polluting test output
|
16
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
17
|
-
|
18
|
-
let instance: LobeMinimaxAI;
|
19
|
-
|
20
|
-
beforeEach(() => {
|
21
|
-
instance = new LobeMinimaxAI({ apiKey: 'test' });
|
22
|
-
});
|
23
|
-
|
24
|
-
afterEach(() => {
|
25
|
-
vi.clearAllMocks();
|
26
|
-
});
|
27
|
-
|
28
|
-
describe('LobeMinimaxAI', () => {
|
29
|
-
describe('init', () => {
|
30
|
-
it('should correctly initialize with an API key', async () => {
|
31
|
-
const instance = new LobeMinimaxAI({ apiKey: 'test_api_key' });
|
32
|
-
expect(instance).toBeInstanceOf(LobeMinimaxAI);
|
33
|
-
});
|
34
|
-
|
35
|
-
it('should throw AgentRuntimeError with InvalidMinimaxAPIKey if no apiKey is provided', async () => {
|
36
|
-
try {
|
37
|
-
new LobeMinimaxAI({});
|
38
|
-
} catch (e) {
|
39
|
-
expect(e).toEqual({ errorType: invalidErrorType });
|
40
|
-
}
|
41
|
-
});
|
42
|
-
});
|
43
|
-
|
44
|
-
describe('chat', () => {
|
45
|
-
it('should return a StreamingTextResponse on successful API call', async () => {
|
46
|
-
const mockResponseData = {
|
47
|
-
choices: [{ delta: { content: 'Hello, world!' } }],
|
48
|
-
};
|
49
|
-
const mockResponse = new Response(
|
50
|
-
new ReadableStream({
|
51
|
-
start(controller) {
|
52
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(mockResponseData)}`));
|
53
|
-
controller.close();
|
54
|
-
},
|
55
|
-
}),
|
56
|
-
);
|
57
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(mockResponse);
|
58
|
-
|
59
|
-
const result = await instance.chat({
|
60
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
61
|
-
model: 'text-davinci-003',
|
62
|
-
temperature: 0,
|
63
|
-
});
|
64
|
-
|
65
|
-
expect(result).toBeInstanceOf(Response);
|
66
|
-
});
|
67
|
-
|
68
|
-
it('should handle text messages correctly', async () => {
|
69
|
-
const mockResponseData = {
|
70
|
-
choices: [{ delta: { content: 'Hello, world!' } }],
|
71
|
-
};
|
72
|
-
const mockResponse = new Response(
|
73
|
-
new ReadableStream({
|
74
|
-
start(controller) {
|
75
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(mockResponseData)}`));
|
76
|
-
controller.close();
|
77
|
-
},
|
78
|
-
}),
|
79
|
-
);
|
80
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(mockResponse);
|
81
|
-
|
82
|
-
const result = await instance.chat({
|
83
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
84
|
-
model: 'text-davinci-003',
|
85
|
-
temperature: 0,
|
86
|
-
});
|
87
|
-
|
88
|
-
expect(result).toBeInstanceOf(Response);
|
89
|
-
});
|
90
|
-
|
91
|
-
it('should call debugStream in DEBUG mode', async () => {
|
92
|
-
process.env.DEBUG_MINIMAX_CHAT_COMPLETION = '1';
|
93
|
-
|
94
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
95
|
-
new Response(
|
96
|
-
new ReadableStream({
|
97
|
-
start(controller) {
|
98
|
-
controller.enqueue(encoder.encode(JSON.stringify('Hello, world!')));
|
99
|
-
controller.close();
|
100
|
-
},
|
101
|
-
}),
|
102
|
-
),
|
103
|
-
);
|
104
|
-
|
105
|
-
vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
|
106
|
-
|
107
|
-
await instance.chat({
|
108
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
109
|
-
model: 'text-davinci-003',
|
110
|
-
temperature: 0,
|
111
|
-
});
|
112
|
-
|
113
|
-
// Assert
|
114
|
-
expect(debugStreamModule.debugStream).toHaveBeenCalled();
|
115
|
-
|
116
|
-
delete process.env.DEBUG_MINIMAX_CHAT_COMPLETION;
|
117
|
-
});
|
118
|
-
|
119
|
-
describe('Error', () => {
|
120
|
-
it('should throw InvalidMinimaxAPIKey error on API_KEY_INVALID error', async () => {
|
121
|
-
const mockErrorResponse = {
|
122
|
-
base_resp: {
|
123
|
-
status_code: 1004,
|
124
|
-
status_msg: 'API key not valid',
|
125
|
-
},
|
126
|
-
};
|
127
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
128
|
-
new Response(
|
129
|
-
new ReadableStream({
|
130
|
-
start(controller) {
|
131
|
-
controller.enqueue(encoder.encode(JSON.stringify(mockErrorResponse)));
|
132
|
-
controller.close();
|
133
|
-
},
|
134
|
-
}),
|
135
|
-
),
|
136
|
-
);
|
137
|
-
|
138
|
-
try {
|
139
|
-
await instance.chat({
|
140
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
141
|
-
model: 'text-davinci-003',
|
142
|
-
temperature: 0,
|
143
|
-
});
|
144
|
-
} catch (e) {
|
145
|
-
expect(e).toEqual({
|
146
|
-
errorType: invalidErrorType,
|
147
|
-
error: {
|
148
|
-
code: 1004,
|
149
|
-
message: 'API key not valid',
|
150
|
-
},
|
151
|
-
provider,
|
152
|
-
});
|
153
|
-
}
|
154
|
-
});
|
155
|
-
|
156
|
-
it('should throw MinimaxBizError error on other error status codes', async () => {
|
157
|
-
const mockErrorResponse = {
|
158
|
-
base_resp: {
|
159
|
-
status_code: 1001,
|
160
|
-
status_msg: 'Some error occurred',
|
161
|
-
},
|
162
|
-
};
|
163
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
164
|
-
new Response(
|
165
|
-
new ReadableStream({
|
166
|
-
start(controller) {
|
167
|
-
controller.enqueue(encoder.encode(JSON.stringify(mockErrorResponse)));
|
168
|
-
controller.close();
|
169
|
-
},
|
170
|
-
}),
|
171
|
-
),
|
172
|
-
);
|
173
|
-
|
174
|
-
try {
|
175
|
-
await instance.chat({
|
176
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
177
|
-
model: 'text-davinci-003',
|
178
|
-
temperature: 0,
|
179
|
-
});
|
180
|
-
} catch (e) {
|
181
|
-
expect(e).toEqual({
|
182
|
-
errorType: bizErrorType,
|
183
|
-
error: {
|
184
|
-
code: 1001,
|
185
|
-
message: 'Some error occurred',
|
186
|
-
},
|
187
|
-
provider,
|
188
|
-
});
|
189
|
-
}
|
190
|
-
});
|
191
|
-
|
192
|
-
it('should throw MinimaxBizError error on generic errors', async () => {
|
193
|
-
const mockError = new Error('Something went wrong');
|
194
|
-
vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(mockError);
|
195
|
-
|
196
|
-
try {
|
197
|
-
await instance.chat({
|
198
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
199
|
-
model: 'text-davinci-003',
|
200
|
-
temperature: 0,
|
201
|
-
});
|
202
|
-
} catch (e) {
|
203
|
-
expect(e).toEqual({
|
204
|
-
errorType: bizErrorType,
|
205
|
-
error: {
|
206
|
-
cause: undefined,
|
207
|
-
message: 'Something went wrong',
|
208
|
-
name: 'Error',
|
209
|
-
stack: mockError.stack,
|
210
|
-
},
|
211
|
-
provider,
|
212
|
-
});
|
213
|
-
}
|
214
|
-
});
|
215
|
-
});
|
216
|
-
});
|
217
|
-
|
218
|
-
describe('private methods', () => {
|
219
|
-
describe('buildCompletionsParams', () => {
|
220
|
-
it('should build the correct parameters', () => {
|
221
|
-
const payload: ChatStreamPayload = {
|
222
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
223
|
-
model: 'text-davinci-003',
|
224
|
-
temperature: 0.5,
|
225
|
-
top_p: 0.8,
|
226
|
-
};
|
227
|
-
|
228
|
-
const result = instance['buildCompletionsParams'](payload);
|
229
|
-
|
230
|
-
expect(result).toEqual({
|
231
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
232
|
-
model: 'text-davinci-003',
|
233
|
-
stream: true,
|
234
|
-
temperature: 0.25,
|
235
|
-
top_p: 0.8,
|
236
|
-
});
|
237
|
-
});
|
238
|
-
|
239
|
-
it('should exclude temperature and top_p when they are 0', () => {
|
240
|
-
const payload: ChatStreamPayload = {
|
241
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
242
|
-
model: 'text-davinci-003',
|
243
|
-
temperature: 0,
|
244
|
-
top_p: 0,
|
245
|
-
};
|
246
|
-
|
247
|
-
const result = instance['buildCompletionsParams'](payload);
|
248
|
-
|
249
|
-
expect(result).toEqual({
|
250
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
251
|
-
model: 'text-davinci-003',
|
252
|
-
stream: true,
|
253
|
-
});
|
254
|
-
});
|
255
|
-
|
256
|
-
it('should include max tokens when model is abab6.5t-chat', () => {
|
257
|
-
const payload: ChatStreamPayload = {
|
258
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
259
|
-
model: 'abab6.5t-chat',
|
260
|
-
temperature: 0,
|
261
|
-
top_p: 0,
|
262
|
-
};
|
263
|
-
|
264
|
-
const result = instance['buildCompletionsParams'](payload);
|
265
|
-
|
266
|
-
expect(result).toEqual({
|
267
|
-
messages: [{ content: 'Hello', role: 'user' }],
|
268
|
-
model: 'abab6.5t-chat',
|
269
|
-
stream: true,
|
270
|
-
max_tokens: 4096,
|
271
|
-
});
|
272
|
-
});
|
273
|
-
});
|
274
|
-
});
|
275
|
-
});
|
@@ -1,27 +0,0 @@
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
2
|
-
|
3
|
-
import { processDoubleData } from './minimax';
|
4
|
-
|
5
|
-
// 假设文件名为 minimax.ts
|
6
|
-
|
7
|
-
describe('processDoubleData', () => {
|
8
|
-
it('should remove the second "data: {"id": and everything after it when matchCount is 2', () => {
|
9
|
-
const chunkValue = `data: {"id":"first"} some other text
|
10
|
-
|
11
|
-
data: {"id":"second"} more text`;
|
12
|
-
const result = processDoubleData(chunkValue);
|
13
|
-
expect(result).toBe('data: {"id":"first"} some other text');
|
14
|
-
});
|
15
|
-
|
16
|
-
it('should not modify chunkValue when matchCount is not 2', () => {
|
17
|
-
const chunkValue = `data: {"id":"first"} some other text`;
|
18
|
-
const result = processDoubleData(chunkValue);
|
19
|
-
expect(result).toBe(chunkValue);
|
20
|
-
});
|
21
|
-
|
22
|
-
it('should not modify chunkValue when matchCount is more than 2', () => {
|
23
|
-
const chunkValue = `data: {"id":"first"} some other text data: {"id":"second"} more text data: {"id":"third"} even more text`;
|
24
|
-
const result = processDoubleData(chunkValue);
|
25
|
-
expect(result).toBe(chunkValue);
|
26
|
-
});
|
27
|
-
});
|
@@ -1,57 +0,0 @@
|
|
1
|
-
import OpenAI from 'openai';
|
2
|
-
|
3
|
-
import { ChatStreamCallbacks } from '../../types';
|
4
|
-
import { transformOpenAIStream } from './openai';
|
5
|
-
import { createCallbacksTransformer, createSSEProtocolTransformer } from './protocol';
|
6
|
-
|
7
|
-
export const processDoubleData = (chunkValue: string): string => {
|
8
|
-
const dataPattern = /data: {"id":"/g;
|
9
|
-
const matchCount = (chunkValue.match(dataPattern) || []).length;
|
10
|
-
let modifiedChunkValue = chunkValue;
|
11
|
-
if (matchCount === 2) {
|
12
|
-
const secondDataIdIndex = chunkValue.indexOf(
|
13
|
-
'data: {"id":',
|
14
|
-
chunkValue.indexOf('data: {"id":') + 1,
|
15
|
-
);
|
16
|
-
if (secondDataIdIndex !== -1) {
|
17
|
-
modifiedChunkValue = chunkValue.slice(0, secondDataIdIndex).trim();
|
18
|
-
}
|
19
|
-
}
|
20
|
-
return modifiedChunkValue;
|
21
|
-
};
|
22
|
-
|
23
|
-
const unit8ArrayToJSONChunk = (unit8Array: Uint8Array): OpenAI.ChatCompletionChunk => {
|
24
|
-
const decoder = new TextDecoder();
|
25
|
-
|
26
|
-
let chunkValue = decoder.decode(unit8Array, { stream: true });
|
27
|
-
|
28
|
-
// chunkValue example:
|
29
|
-
// data: {"id":"028a65377137d57aaceeffddf48ae99f","choices":[{"finish_reason":"tool_calls","index":0,"delta":{"role":"assistant","tool_calls":[{"id":"call_function_7371372822","type":"function","function":{"name":"realtime-weather____fetchCurrentWeather","arguments":"{\"city\": [\"杭州\", \"北京\"]}"}}]}}],"created":155511,"model":"abab6.5s-chat","object":"chat.completion.chunk"}
|
30
|
-
|
31
|
-
chunkValue = processDoubleData(chunkValue);
|
32
|
-
|
33
|
-
// so we need to remove `data:` prefix and then parse it as JSON
|
34
|
-
if (chunkValue.startsWith('data:')) {
|
35
|
-
chunkValue = chunkValue.slice(5).trim();
|
36
|
-
}
|
37
|
-
|
38
|
-
try {
|
39
|
-
return JSON.parse(chunkValue);
|
40
|
-
} catch (e) {
|
41
|
-
console.error('minimax chunk parse error:', e);
|
42
|
-
|
43
|
-
return { raw: chunkValue } as any;
|
44
|
-
}
|
45
|
-
};
|
46
|
-
|
47
|
-
export const MinimaxStream = (stream: ReadableStream, callbacks?: ChatStreamCallbacks) => {
|
48
|
-
return stream
|
49
|
-
.pipeThrough(
|
50
|
-
createSSEProtocolTransformer((buffer) => {
|
51
|
-
const chunk = unit8ArrayToJSONChunk(buffer);
|
52
|
-
|
53
|
-
return transformOpenAIStream(chunk);
|
54
|
-
}),
|
55
|
-
)
|
56
|
-
.pipeThrough(createCallbacksTransformer(callbacks));
|
57
|
-
};
|