@lobehub/lobehub 2.0.0-next.201 → 2.0.0-next.203
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +2 -0
- package/locales/ar/models.json +104 -6
- package/locales/ar/plugin.json +2 -1
- package/locales/bg-BG/chat.json +2 -0
- package/locales/bg-BG/models.json +46 -4
- package/locales/bg-BG/plugin.json +2 -1
- package/locales/de-DE/chat.json +2 -0
- package/locales/de-DE/models.json +74 -4
- package/locales/de-DE/plugin.json +2 -1
- package/locales/en-US/chat.json +2 -0
- package/locales/en-US/plugin.json +2 -1
- package/locales/es-ES/chat.json +2 -0
- package/locales/es-ES/models.json +129 -4
- package/locales/es-ES/plugin.json +2 -1
- package/locales/fa-IR/chat.json +2 -0
- package/locales/fa-IR/models.json +80 -4
- package/locales/fa-IR/plugin.json +2 -1
- package/locales/fr-FR/chat.json +2 -0
- package/locales/fr-FR/models.json +67 -7
- package/locales/fr-FR/plugin.json +2 -1
- package/locales/it-IT/chat.json +2 -0
- package/locales/it-IT/models.json +39 -6
- package/locales/it-IT/plugin.json +2 -1
- package/locales/ja-JP/chat.json +2 -0
- package/locales/ja-JP/models.json +123 -7
- package/locales/ja-JP/plugin.json +2 -1
- package/locales/ko-KR/chat.json +2 -0
- package/locales/ko-KR/models.json +104 -5
- package/locales/ko-KR/plugin.json +2 -1
- package/locales/nl-NL/chat.json +2 -0
- package/locales/nl-NL/models.json +62 -5
- package/locales/nl-NL/plugin.json +2 -1
- package/locales/pl-PL/chat.json +2 -0
- package/locales/pl-PL/models.json +110 -0
- package/locales/pl-PL/plugin.json +2 -1
- package/locales/pt-BR/chat.json +2 -0
- package/locales/pt-BR/models.json +81 -5
- package/locales/pt-BR/plugin.json +2 -1
- package/locales/ru-RU/chat.json +2 -0
- package/locales/ru-RU/models.json +33 -6
- package/locales/ru-RU/plugin.json +2 -1
- package/locales/tr-TR/chat.json +2 -0
- package/locales/tr-TR/models.json +26 -7
- package/locales/tr-TR/plugin.json +2 -1
- package/locales/vi-VN/chat.json +2 -0
- package/locales/vi-VN/models.json +59 -4
- package/locales/vi-VN/plugin.json +2 -1
- package/locales/zh-CN/chat.json +2 -0
- package/locales/zh-CN/models.json +141 -5
- package/locales/zh-CN/plugin.json +2 -1
- package/locales/zh-TW/chat.json +2 -0
- package/locales/zh-TW/models.json +96 -7
- package/locales/zh-TW/plugin.json +2 -1
- package/package.json +1 -1
- package/packages/builtin-tool-gtd/src/client/Inspector/ExecTask/index.tsx +30 -15
- package/packages/builtin-tool-gtd/src/manifest.ts +1 -1
- package/packages/model-runtime/src/core/ModelRuntime.test.ts +44 -86
- package/packages/types/src/aiChat.ts +0 -1
- package/packages/types/src/message/ui/chat.ts +1 -1
- package/src/app/(backend)/middleware/auth/index.ts +16 -2
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +30 -15
- package/src/app/(backend)/webapi/chat/[provider]/route.ts +44 -40
- package/src/app/(backend)/webapi/models/[provider]/pull/route.ts +4 -3
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +36 -13
- package/src/app/(backend)/webapi/models/[provider]/route.ts +4 -11
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +21 -23
- package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +16 -3
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +17 -20
- package/src/features/Conversation/Messages/Tasks/shared/ErrorState.tsx +16 -11
- package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +6 -20
- package/src/features/Conversation/Messages/Tasks/shared/ProcessingState.tsx +10 -20
- package/src/features/User/DataStatistics.tsx +4 -4
- package/src/hooks/useQueryParam.ts +0 -2
- package/src/libs/trpc/async/asyncAuth.ts +0 -2
- package/src/libs/trpc/async/context.ts +3 -11
- package/src/locales/default/chat.ts +2 -0
- package/src/locales/default/plugin.ts +2 -1
- package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +6 -6
- package/src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts +3 -3
- package/src/server/modules/AgentRuntime/factory.ts +39 -20
- package/src/server/modules/ModelRuntime/index.ts +138 -1
- package/src/server/routers/async/__tests__/caller.test.ts +22 -27
- package/src/server/routers/async/caller.ts +4 -6
- package/src/server/routers/async/file.ts +10 -5
- package/src/server/routers/async/image.ts +5 -4
- package/src/server/routers/async/ragEval.ts +7 -5
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +8 -37
- package/src/server/routers/lambda/aiChat.ts +5 -21
- package/src/server/routers/lambda/chunk.ts +9 -28
- package/src/server/routers/lambda/image.ts +1 -7
- package/src/server/routers/lambda/ragEval.ts +1 -1
- package/src/server/routers/lambda/userMemories/reembed.ts +4 -1
- package/src/server/routers/lambda/userMemories/search.ts +7 -7
- package/src/server/routers/lambda/userMemories/shared.ts +8 -10
- package/src/server/routers/lambda/userMemories/tools.ts +140 -118
- package/src/server/routers/lambda/userMemories.test.ts +3 -7
- package/src/server/routers/lambda/userMemories.ts +44 -29
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +87 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +53 -2
- package/src/server/services/agentRuntime/__tests__/executeSync.test.ts +2 -6
- package/src/server/services/agentRuntime/__tests__/stepLifecycleCallbacks.test.ts +1 -1
- package/src/server/services/chunk/index.ts +6 -5
- package/src/server/services/toolExecution/types.ts +1 -2
- package/src/services/__tests__/_url.test.ts +0 -1
- package/src/services/_url.ts +0 -3
- package/src/services/aiChat.ts +5 -12
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +0 -2
- package/src/app/(backend)/webapi/text-to-image/[provider]/route.ts +0 -74
|
@@ -1,16 +1,113 @@
|
|
|
1
1
|
import { type GoogleGenAIOptions } from '@google/genai';
|
|
2
2
|
import { ModelRuntime } from '@lobechat/model-runtime';
|
|
3
3
|
import { LobeVertexAI } from '@lobechat/model-runtime/vertexai';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type AWSBedrockKeyVault,
|
|
6
|
+
type AzureOpenAIKeyVault,
|
|
7
|
+
type ClientSecretPayload,
|
|
8
|
+
type CloudflareKeyVault,
|
|
9
|
+
type ComfyUIKeyVault,
|
|
10
|
+
type OpenAICompatibleKeyVault,
|
|
11
|
+
type VertexAIKeyVault,
|
|
12
|
+
} from '@lobechat/types';
|
|
5
13
|
import { safeParseJSON } from '@lobechat/utils';
|
|
6
14
|
import { ModelProvider } from 'model-bank';
|
|
7
15
|
|
|
16
|
+
import { AiProviderModel } from '@/database/models/aiProvider';
|
|
17
|
+
import { type LobeChatDatabase } from '@/database/type';
|
|
8
18
|
import { getLLMConfig } from '@/envs/llm';
|
|
9
19
|
|
|
20
|
+
import { KeyVaultsGateKeeper } from '../KeyVaultsEncrypt';
|
|
10
21
|
import apiKeyManager from './apiKeyManager';
|
|
11
22
|
|
|
12
23
|
export * from './trace';
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Combined KeyVaults type for all providers
|
|
27
|
+
*/
|
|
28
|
+
type ProviderKeyVaults = OpenAICompatibleKeyVault &
|
|
29
|
+
AzureOpenAIKeyVault &
|
|
30
|
+
AWSBedrockKeyVault &
|
|
31
|
+
CloudflareKeyVault &
|
|
32
|
+
ComfyUIKeyVault &
|
|
33
|
+
VertexAIKeyVault;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build ClientSecretPayload from keyVaults stored in database
|
|
37
|
+
*
|
|
38
|
+
* This is the server-side equivalent of the frontend's getProviderAuthPayload function.
|
|
39
|
+
* It converts the keyVaults object from database to the ClientSecretPayload format
|
|
40
|
+
* expected by initModelRuntimeWithUserPayload.
|
|
41
|
+
*
|
|
42
|
+
* @param provider - The model provider
|
|
43
|
+
* @param keyVaults - The keyVaults object from database (already decrypted)
|
|
44
|
+
* @returns ClientSecretPayload for the provider
|
|
45
|
+
*/
|
|
46
|
+
export const buildPayloadFromKeyVaults = (
|
|
47
|
+
provider: string,
|
|
48
|
+
keyVaults: ProviderKeyVaults,
|
|
49
|
+
): ClientSecretPayload => {
|
|
50
|
+
switch (provider) {
|
|
51
|
+
case ModelProvider.Bedrock: {
|
|
52
|
+
const { accessKeyId, region, secretAccessKey, sessionToken } = keyVaults;
|
|
53
|
+
const apiKey = (secretAccessKey || '') + (accessKeyId || '');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
apiKey,
|
|
57
|
+
awsAccessKeyId: accessKeyId,
|
|
58
|
+
awsRegion: region,
|
|
59
|
+
awsSecretAccessKey: secretAccessKey,
|
|
60
|
+
awsSessionToken: sessionToken,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case ModelProvider.Azure: {
|
|
65
|
+
return {
|
|
66
|
+
apiKey: keyVaults.apiKey,
|
|
67
|
+
azureApiVersion: keyVaults.apiVersion,
|
|
68
|
+
baseURL: keyVaults.baseURL || keyVaults.endpoint,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case ModelProvider.Ollama: {
|
|
73
|
+
return { baseURL: keyVaults.baseURL };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case ModelProvider.Cloudflare: {
|
|
77
|
+
return {
|
|
78
|
+
apiKey: keyVaults.apiKey,
|
|
79
|
+
cloudflareBaseURLOrAccountID: keyVaults.baseURLOrAccountID,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case ModelProvider.ComfyUI: {
|
|
84
|
+
return {
|
|
85
|
+
apiKey: keyVaults.apiKey,
|
|
86
|
+
authType: keyVaults.authType,
|
|
87
|
+
baseURL: keyVaults.baseURL,
|
|
88
|
+
customHeaders: keyVaults.customHeaders,
|
|
89
|
+
password: keyVaults.password,
|
|
90
|
+
username: keyVaults.username,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case ModelProvider.VertexAI: {
|
|
95
|
+
return {
|
|
96
|
+
apiKey: keyVaults.apiKey,
|
|
97
|
+
baseURL: keyVaults.baseURL,
|
|
98
|
+
vertexAIRegion: keyVaults.region,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
default: {
|
|
103
|
+
return {
|
|
104
|
+
apiKey: keyVaults.apiKey,
|
|
105
|
+
baseURL: keyVaults.baseURL,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
14
111
|
/**
|
|
15
112
|
* Retrieves the options object from environment and apikeymanager
|
|
16
113
|
* based on the provider and payload.
|
|
@@ -220,3 +317,43 @@ export const initModelRuntimeWithUserPayload = (
|
|
|
220
317
|
...params,
|
|
221
318
|
});
|
|
222
319
|
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Initialize ModelRuntime by reading user's provider configuration from database
|
|
323
|
+
*
|
|
324
|
+
* This function replaces the pattern of passing userPayload from frontend.
|
|
325
|
+
* It reads the user's AI provider configuration from the database, decrypts
|
|
326
|
+
* the keyVaults, and initializes the ModelRuntime.
|
|
327
|
+
*
|
|
328
|
+
* @param db - The database instance
|
|
329
|
+
* @param userId - The user ID
|
|
330
|
+
* @param provider - The model provider (e.g., 'openai', 'azure')
|
|
331
|
+
* @returns Promise<ModelRuntime> - The initialized ModelRuntime instance
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```typescript
|
|
335
|
+
* const modelRuntime = await initModelRuntimeFromDB(db, userId, 'openai');
|
|
336
|
+
* const response = await modelRuntime.chat({ messages, model });
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
export const initModelRuntimeFromDB = async (
|
|
340
|
+
db: LobeChatDatabase,
|
|
341
|
+
userId: string,
|
|
342
|
+
provider: string,
|
|
343
|
+
): Promise<ModelRuntime> => {
|
|
344
|
+
// 1. Get user's provider configuration from database
|
|
345
|
+
const aiProviderModel = new AiProviderModel(db, userId);
|
|
346
|
+
|
|
347
|
+
// Use getAiProviderById with KeyVaultsGateKeeper.getUserKeyVaults as decryptor
|
|
348
|
+
const providerConfig = await aiProviderModel.getAiProviderById(
|
|
349
|
+
provider,
|
|
350
|
+
KeyVaultsGateKeeper.getUserKeyVaults,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// 2. Build ClientSecretPayload from keyVaults
|
|
354
|
+
const keyVaults = (providerConfig?.keyVaults || {}) as ProviderKeyVaults;
|
|
355
|
+
const payload = buildPayloadFromKeyVaults(provider, keyVaults);
|
|
356
|
+
|
|
357
|
+
// 3. Initialize ModelRuntime with the payload
|
|
358
|
+
return initModelRuntimeWithUserPayload(provider, payload);
|
|
359
|
+
};
|
|
@@ -65,7 +65,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
65
65
|
mockAppEnv.APP_URL = 'https://public.example.com';
|
|
66
66
|
mockAppEnv.INTERNAL_APP_URL = 'http://localhost:3210';
|
|
67
67
|
|
|
68
|
-
await createAsyncServerClient('user-123'
|
|
68
|
+
await createAsyncServerClient('user-123');
|
|
69
69
|
|
|
70
70
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
71
71
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -80,7 +80,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
80
80
|
mockAppEnv.APP_URL = 'https://fallback.example.com';
|
|
81
81
|
mockAppEnv.INTERNAL_APP_URL = 'https://fallback.example.com'; // getInternalAppUrl() returns APP_URL
|
|
82
82
|
|
|
83
|
-
await createAsyncServerClient('user-456'
|
|
83
|
+
await createAsyncServerClient('user-456');
|
|
84
84
|
|
|
85
85
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
86
86
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -92,7 +92,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
92
92
|
mockAppEnv.APP_URL = 'https://cdn-proxied.example.com';
|
|
93
93
|
mockAppEnv.INTERNAL_APP_URL = 'http://127.0.0.1:3210';
|
|
94
94
|
|
|
95
|
-
await createAsyncServerClient('user-789'
|
|
95
|
+
await createAsyncServerClient('user-789');
|
|
96
96
|
|
|
97
97
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
98
98
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -105,7 +105,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
105
105
|
mockAppEnv.APP_URL = 'https://public.example.com';
|
|
106
106
|
mockAppEnv.INTERNAL_APP_URL = 'http://lobe-service:3210';
|
|
107
107
|
|
|
108
|
-
await createAsyncServerClient('user-docker'
|
|
108
|
+
await createAsyncServerClient('user-docker');
|
|
109
109
|
|
|
110
110
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
111
111
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -116,7 +116,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
116
116
|
it('should handle INTERNAL_APP_URL with trailing slash', async () => {
|
|
117
117
|
mockAppEnv.INTERNAL_APP_URL = 'http://localhost:3210/';
|
|
118
118
|
|
|
119
|
-
await createAsyncServerClient('user-trailing'
|
|
119
|
+
await createAsyncServerClient('user-trailing');
|
|
120
120
|
|
|
121
121
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
122
122
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -128,7 +128,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
128
128
|
it('should handle INTERNAL_APP_URL without trailing slash', async () => {
|
|
129
129
|
mockAppEnv.INTERNAL_APP_URL = 'https://example.com';
|
|
130
130
|
|
|
131
|
-
await createAsyncServerClient('user-no-trailing'
|
|
131
|
+
await createAsyncServerClient('user-no-trailing');
|
|
132
132
|
|
|
133
133
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
134
134
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -139,7 +139,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
139
139
|
|
|
140
140
|
describe('authentication and headers', () => {
|
|
141
141
|
it('should include Authorization header with internal JWT token', async () => {
|
|
142
|
-
await createAsyncServerClient('user-auth'
|
|
142
|
+
await createAsyncServerClient('user-auth');
|
|
143
143
|
|
|
144
144
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
145
145
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -148,19 +148,16 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
148
148
|
expect(httpLinkOptions.headers.Authorization).toBe('mock-internal-jwt-token');
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it('should encrypt and include
|
|
152
|
-
const testPayload = { apiKey: 'test-api-key-value', provider: 'openai' };
|
|
151
|
+
it('should encrypt and include userId in x-lobe-chat-auth header', async () => {
|
|
153
152
|
const mockEncrypt = vi.fn().mockResolvedValue('test-encrypted-auth-data');
|
|
154
153
|
vi.mocked(KeyVaultsGateKeeper.initWithEnvKey).mockResolvedValueOnce({
|
|
155
154
|
encrypt: mockEncrypt,
|
|
156
155
|
} as any);
|
|
157
156
|
|
|
158
|
-
await createAsyncServerClient('user-encrypt'
|
|
157
|
+
await createAsyncServerClient('user-encrypt');
|
|
159
158
|
|
|
160
159
|
expect(KeyVaultsGateKeeper.initWithEnvKey).toHaveBeenCalled();
|
|
161
|
-
expect(mockEncrypt).toHaveBeenCalledWith(
|
|
162
|
-
JSON.stringify({ payload: testPayload, userId: 'user-encrypt' }),
|
|
163
|
-
);
|
|
160
|
+
expect(mockEncrypt).toHaveBeenCalledWith(JSON.stringify({ userId: 'user-encrypt' }));
|
|
164
161
|
|
|
165
162
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
166
163
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -176,7 +173,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
176
173
|
const originalEnv = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
177
174
|
process.env.VERCEL_AUTOMATION_BYPASS_SECRET = 'test-bypass-value';
|
|
178
175
|
|
|
179
|
-
await createAsyncServerClient('user-vercel'
|
|
176
|
+
await createAsyncServerClient('user-vercel');
|
|
180
177
|
|
|
181
178
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
182
179
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -195,7 +192,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
195
192
|
it('should not include Vercel bypass secret when not available', async () => {
|
|
196
193
|
delete process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
197
194
|
|
|
198
|
-
await createAsyncServerClient('user-no-vercel'
|
|
195
|
+
await createAsyncServerClient('user-no-vercel');
|
|
199
196
|
|
|
200
197
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
201
198
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -211,9 +208,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
211
208
|
encrypt: mockEncrypt,
|
|
212
209
|
} as any);
|
|
213
210
|
|
|
214
|
-
await expect(createAsyncServerClient('user-enc-fail'
|
|
215
|
-
'Encryption failed',
|
|
216
|
-
);
|
|
211
|
+
await expect(createAsyncServerClient('user-enc-fail')).rejects.toThrow('Encryption failed');
|
|
217
212
|
|
|
218
213
|
expect(KeyVaultsGateKeeper.initWithEnvKey).toHaveBeenCalled();
|
|
219
214
|
});
|
|
@@ -223,7 +218,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
223
218
|
mockAppEnv.APP_URL = 'https://only-app-url.com';
|
|
224
219
|
mockAppEnv.INTERNAL_APP_URL = 'https://only-app-url.com'; // Result of fallback
|
|
225
220
|
|
|
226
|
-
await createAsyncServerClient('user-null'
|
|
221
|
+
await createAsyncServerClient('user-null');
|
|
227
222
|
|
|
228
223
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
229
224
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -236,7 +231,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
236
231
|
mockAppEnv.APP_URL = 'https://fallback-from-empty.com';
|
|
237
232
|
mockAppEnv.INTERNAL_APP_URL = '';
|
|
238
233
|
|
|
239
|
-
await createAsyncServerClient('user-empty'
|
|
234
|
+
await createAsyncServerClient('user-empty');
|
|
240
235
|
|
|
241
236
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
242
237
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -249,7 +244,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
249
244
|
it('should handle malformed URL gracefully', async () => {
|
|
250
245
|
mockAppEnv.INTERNAL_APP_URL = 'not-a-valid-url';
|
|
251
246
|
|
|
252
|
-
await createAsyncServerClient('user-malformed'
|
|
247
|
+
await createAsyncServerClient('user-malformed');
|
|
253
248
|
|
|
254
249
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
255
250
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -262,7 +257,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
262
257
|
|
|
263
258
|
describe('TRPC client configuration', () => {
|
|
264
259
|
it('should configure httpLink with proper options', async () => {
|
|
265
|
-
await createAsyncServerClient('user-config'
|
|
260
|
+
await createAsyncServerClient('user-config');
|
|
266
261
|
|
|
267
262
|
expect(httpLink).toHaveBeenCalled();
|
|
268
263
|
const httpLinkOptions = vi.mocked(httpLink).mock.calls[0][0];
|
|
@@ -273,7 +268,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
273
268
|
});
|
|
274
269
|
|
|
275
270
|
it('should pass httpLink result to createTRPCClient', async () => {
|
|
276
|
-
await createAsyncServerClient('user-link'
|
|
271
|
+
await createAsyncServerClient('user-link');
|
|
277
272
|
|
|
278
273
|
expect(createTRPCClient).toHaveBeenCalledWith({
|
|
279
274
|
links: expect.arrayContaining([
|
|
@@ -286,7 +281,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
286
281
|
});
|
|
287
282
|
|
|
288
283
|
it('should return the created TRPC client', async () => {
|
|
289
|
-
const client = await createAsyncServerClient('user-return'
|
|
284
|
+
const client = await createAsyncServerClient('user-return');
|
|
290
285
|
|
|
291
286
|
expect(client).toBeDefined();
|
|
292
287
|
expect(client).toHaveProperty('_mockClient');
|
|
@@ -299,7 +294,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
299
294
|
mockAppEnv.APP_URL = 'https://lobechat.example.com';
|
|
300
295
|
mockAppEnv.INTERNAL_APP_URL = 'http://localhost:3210';
|
|
301
296
|
|
|
302
|
-
await createAsyncServerClient('prod-user'
|
|
297
|
+
await createAsyncServerClient('prod-user');
|
|
303
298
|
|
|
304
299
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
305
300
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -312,7 +307,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
312
307
|
mockAppEnv.APP_URL = 'https://public.example.com';
|
|
313
308
|
mockAppEnv.INTERNAL_APP_URL = 'http://lobe-chat-database:3210';
|
|
314
309
|
|
|
315
|
-
await createAsyncServerClient('docker-user'
|
|
310
|
+
await createAsyncServerClient('docker-user');
|
|
316
311
|
|
|
317
312
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
318
313
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -325,7 +320,7 @@ describe('createAsyncServerClient - INTERNAL_APP_URL Tests', () => {
|
|
|
325
320
|
mockAppEnv.APP_URL = 'https://direct-access.example.com';
|
|
326
321
|
mockAppEnv.INTERNAL_APP_URL = 'https://direct-access.example.com'; // Result of getInternalAppUrl() fallback
|
|
327
322
|
|
|
328
|
-
await createAsyncServerClient('direct-user'
|
|
323
|
+
await createAsyncServerClient('direct-user');
|
|
329
324
|
|
|
330
325
|
const config = vi.mocked(createTRPCClient).mock.calls[0][0];
|
|
331
326
|
const httpLinkOptions = config.links[0] as any;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ClientSecretPayload } from '@lobechat/types';
|
|
2
1
|
import { createTRPCClient, httpLink } from '@trpc/client';
|
|
3
2
|
import superjson from 'superjson';
|
|
4
3
|
import urlJoin from 'url-join';
|
|
@@ -12,12 +11,12 @@ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
|
12
11
|
import { asyncRouter } from './index';
|
|
13
12
|
import type { AsyncRouter } from './index';
|
|
14
13
|
|
|
15
|
-
export const createAsyncServerClient = async (userId: string
|
|
14
|
+
export const createAsyncServerClient = async (userId: string) => {
|
|
16
15
|
const token = await signInternalJWT();
|
|
17
16
|
const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
|
|
18
17
|
const headers: Record<string, string> = {
|
|
19
18
|
Authorization: token,
|
|
20
|
-
[LOBE_CHAT_AUTH_HEADER]: await gateKeeper.encrypt(JSON.stringify({
|
|
19
|
+
[LOBE_CHAT_AUTH_HEADER]: await gateKeeper.encrypt(JSON.stringify({ userId })),
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
@@ -49,7 +48,6 @@ const helperFunc = () => {
|
|
|
49
48
|
export type UnifiedAsyncCaller = ReturnType<typeof helperFunc>;
|
|
50
49
|
|
|
51
50
|
interface CreateCallerOptions {
|
|
52
|
-
jwtPayload: any;
|
|
53
51
|
userId: string;
|
|
54
52
|
}
|
|
55
53
|
|
|
@@ -60,9 +58,9 @@ interface CreateCallerOptions {
|
|
|
60
58
|
export const createAsyncCaller = async (
|
|
61
59
|
options: CreateCallerOptions,
|
|
62
60
|
): Promise<UnifiedAsyncCaller> => {
|
|
63
|
-
const { userId
|
|
61
|
+
const { userId } = options;
|
|
64
62
|
|
|
65
|
-
const httpClient = await createAsyncServerClient(userId
|
|
63
|
+
const httpClient = await createAsyncServerClient(userId);
|
|
66
64
|
const createRecursiveProxy = (client: any, path: string[]): any => {
|
|
67
65
|
// The target is a dummy function, so that 'apply' can be triggered.
|
|
68
66
|
return new Proxy(() => {}, {
|
|
@@ -15,7 +15,7 @@ import { type NewChunkItem, type NewEmbeddingsItem } from '@/database/schemas';
|
|
|
15
15
|
import { fileEnv } from '@/envs/file';
|
|
16
16
|
import { asyncAuthedProcedure, asyncRouter as router } from '@/libs/trpc/async';
|
|
17
17
|
import { getServerDefaultFilesConfig } from '@/server/globalConfig';
|
|
18
|
-
import {
|
|
18
|
+
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
19
19
|
import { ChunkService } from '@/server/services/chunk';
|
|
20
20
|
import { FileService } from '@/server/services/file';
|
|
21
21
|
import {
|
|
@@ -95,9 +95,14 @@ export const fileRouter = router({
|
|
|
95
95
|
await pMap(
|
|
96
96
|
requestArray,
|
|
97
97
|
async (chunks) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
// Read user's provider config from database
|
|
99
|
+
const modelRuntime = await initModelRuntimeFromDB(
|
|
100
|
+
ctx.serverDB,
|
|
101
|
+
ctx.userId,
|
|
102
|
+
provider,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const embeddings = await modelRuntime.embeddings({
|
|
101
106
|
dimensions: 1024,
|
|
102
107
|
input: chunks.map((c) => c.text),
|
|
103
108
|
model,
|
|
@@ -243,7 +248,7 @@ export const fileRouter = router({
|
|
|
243
248
|
|
|
244
249
|
// if enable auto embedding, trigger the embedding task
|
|
245
250
|
if (fileEnv.CHUNKS_AUTO_EMBEDDING) {
|
|
246
|
-
await chunkService.asyncEmbeddingFileChunks(input.fileId
|
|
251
|
+
await chunkService.asyncEmbeddingFileChunks(input.fileId);
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
return { success: true };
|
|
@@ -14,7 +14,7 @@ import { FileModel } from '@/database/models/file';
|
|
|
14
14
|
import { GenerationModel } from '@/database/models/generation';
|
|
15
15
|
import { GenerationBatchModel } from '@/database/models/generationBatch';
|
|
16
16
|
import { asyncAuthedProcedure, asyncRouter as router } from '@/libs/trpc/async';
|
|
17
|
-
import {
|
|
17
|
+
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
18
18
|
import { GenerationService } from '@/server/services/generation';
|
|
19
19
|
|
|
20
20
|
const log = debug('lobe-image:async');
|
|
@@ -241,12 +241,13 @@ export const imageRouter = router({
|
|
|
241
241
|
const imageGenerationPromise = async (signal: AbortSignal) => {
|
|
242
242
|
log('Initializing agent runtime for provider: %s', provider);
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
// Read user's provider config from database
|
|
245
|
+
const modelRuntime = await initModelRuntimeFromDB(ctx.serverDB, ctx.userId, provider);
|
|
245
246
|
|
|
246
247
|
// Check if operation has been cancelled
|
|
247
248
|
checkAbortSignal(signal);
|
|
248
249
|
log('Agent runtime initialized, calling createImage');
|
|
249
|
-
const response = await
|
|
250
|
+
const response = await modelRuntime.createImage!({
|
|
250
251
|
model,
|
|
251
252
|
params: params as unknown as RuntimeImageGenParams,
|
|
252
253
|
});
|
|
@@ -298,7 +299,7 @@ export const imageRouter = router({
|
|
|
298
299
|
if (provider === 'comfyui') {
|
|
299
300
|
// Use the public interface method to get auth headers
|
|
300
301
|
// This avoids accessing private members and exposing credentials
|
|
301
|
-
authHeaders =
|
|
302
|
+
authHeaders = modelRuntime.getAuthHeaders();
|
|
302
303
|
if (authHeaders) {
|
|
303
304
|
log('Using authentication headers for ComfyUI image download');
|
|
304
305
|
} else {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
EvaluationRecordModel,
|
|
16
16
|
} from '@/database/server/models/ragEval';
|
|
17
17
|
import { asyncAuthedProcedure, asyncRouter as router } from '@/libs/trpc/async';
|
|
18
|
-
import {
|
|
18
|
+
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
19
19
|
import { ChunkService } from '@/server/services/chunk';
|
|
20
20
|
import { AsyncTaskError } from '@/types/asyncTask';
|
|
21
21
|
|
|
@@ -51,9 +51,11 @@ export const ragEvalRouter = router({
|
|
|
51
51
|
|
|
52
52
|
const now = Date.now();
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
// Read user's provider config from database
|
|
55
|
+
const modelRuntime = await initModelRuntimeFromDB(
|
|
56
|
+
ctx.serverDB,
|
|
57
|
+
ctx.userId,
|
|
55
58
|
ModelProvider.OpenAI,
|
|
56
|
-
ctx.jwtPayload,
|
|
57
59
|
);
|
|
58
60
|
|
|
59
61
|
const { question, languageModel, embeddingModel } = evalRecord;
|
|
@@ -63,7 +65,7 @@ export const ragEvalRouter = router({
|
|
|
63
65
|
|
|
64
66
|
// 如果不存在 questionEmbeddingId,那么就需要做一次 embedding
|
|
65
67
|
if (!questionEmbeddingId) {
|
|
66
|
-
const embeddings = await
|
|
68
|
+
const embeddings = await modelRuntime.embeddings({
|
|
67
69
|
dimensions: 1024,
|
|
68
70
|
input: question,
|
|
69
71
|
model: !!embeddingModel ? embeddingModel : DEFAULT_EMBEDDING_MODEL,
|
|
@@ -100,7 +102,7 @@ export const ragEvalRouter = router({
|
|
|
100
102
|
// 做一次生成 LLM 答案生成
|
|
101
103
|
const { messages } = chainAnswerWithContext({ context, knowledge: [], question });
|
|
102
104
|
|
|
103
|
-
const response = await
|
|
105
|
+
const response = await modelRuntime.chat({
|
|
104
106
|
messages: messages!,
|
|
105
107
|
model: !!languageModel ? languageModel : DEFAULT_MODEL,
|
|
106
108
|
responseMode: 'json',
|
|
@@ -18,11 +18,8 @@ vi.mock('@/server/services/aiChat');
|
|
|
18
18
|
vi.mock('@/server/services/file', () => ({
|
|
19
19
|
FileService: vi.fn(),
|
|
20
20
|
}));
|
|
21
|
-
vi.mock('@/utils/server', () => ({
|
|
22
|
-
getXorPayload: vi.fn(),
|
|
23
|
-
}));
|
|
24
21
|
vi.mock('@/server/modules/ModelRuntime', () => ({
|
|
25
|
-
|
|
22
|
+
initModelRuntimeFromDB: vi.fn(),
|
|
26
23
|
}));
|
|
27
24
|
|
|
28
25
|
describe('aiChatRouter', () => {
|
|
@@ -690,22 +687,18 @@ describe('aiChatRouter', () => {
|
|
|
690
687
|
|
|
691
688
|
describe('outputJSON', () => {
|
|
692
689
|
it('should successfully generate structured output', async () => {
|
|
693
|
-
const {
|
|
694
|
-
const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
|
|
690
|
+
const { initModelRuntimeFromDB } = await import('@/server/modules/ModelRuntime');
|
|
695
691
|
|
|
696
|
-
const mockPayload = { apiKey: 'test-key' };
|
|
697
692
|
const mockResult = { object: { name: 'John', age: 30 } };
|
|
698
693
|
const mockGenerateObject = vi.fn().mockResolvedValue(mockResult);
|
|
699
694
|
|
|
700
|
-
vi.mocked(
|
|
701
|
-
vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
|
|
695
|
+
vi.mocked(initModelRuntimeFromDB).mockResolvedValue({
|
|
702
696
|
generateObject: mockGenerateObject,
|
|
703
697
|
} as any);
|
|
704
698
|
|
|
705
|
-
const caller = aiChatRouter.createCaller(mockCtx as any);
|
|
699
|
+
const caller = aiChatRouter.createCaller({ ...mockCtx, serverDB: {} } as any);
|
|
706
700
|
|
|
707
701
|
const input = {
|
|
708
|
-
keyVaultsPayload: 'encrypted-payload',
|
|
709
702
|
messages: [{ content: 'test', role: 'user' }],
|
|
710
703
|
model: 'gpt-4o',
|
|
711
704
|
provider: 'openai',
|
|
@@ -720,8 +713,7 @@ describe('aiChatRouter', () => {
|
|
|
720
713
|
|
|
721
714
|
const result = await caller.outputJSON(input);
|
|
722
715
|
|
|
723
|
-
expect(
|
|
724
|
-
expect(initModelRuntimeWithUserPayload).toHaveBeenCalledWith('openai', mockPayload);
|
|
716
|
+
expect(initModelRuntimeFromDB).toHaveBeenCalledWith({}, 'u1', 'openai');
|
|
725
717
|
expect(mockGenerateObject).toHaveBeenCalledWith({
|
|
726
718
|
messages: input.messages,
|
|
727
719
|
model: 'gpt-4o',
|
|
@@ -731,28 +723,9 @@ describe('aiChatRouter', () => {
|
|
|
731
723
|
expect(result).toEqual(mockResult);
|
|
732
724
|
});
|
|
733
725
|
|
|
734
|
-
it('should throw error when keyVaultsPayload is invalid', async () => {
|
|
735
|
-
const { getXorPayload } = await import('@/utils/server');
|
|
736
|
-
|
|
737
|
-
vi.mocked(getXorPayload).mockReturnValue(undefined as any);
|
|
738
|
-
|
|
739
|
-
const caller = aiChatRouter.createCaller(mockCtx as any);
|
|
740
|
-
|
|
741
|
-
const input = {
|
|
742
|
-
keyVaultsPayload: 'invalid-payload',
|
|
743
|
-
messages: [],
|
|
744
|
-
model: 'gpt-4o',
|
|
745
|
-
provider: 'openai',
|
|
746
|
-
};
|
|
747
|
-
|
|
748
|
-
await expect(caller.outputJSON(input)).rejects.toThrow('keyVaultsPayload is not correct');
|
|
749
|
-
});
|
|
750
|
-
|
|
751
726
|
it('should handle tools parameter when provided', async () => {
|
|
752
|
-
const {
|
|
753
|
-
const { initModelRuntimeWithUserPayload } = await import('@/server/modules/ModelRuntime');
|
|
727
|
+
const { initModelRuntimeFromDB } = await import('@/server/modules/ModelRuntime');
|
|
754
728
|
|
|
755
|
-
const mockPayload = { apiKey: 'test-key' };
|
|
756
729
|
const mockTools = [
|
|
757
730
|
{
|
|
758
731
|
type: 'function' as const,
|
|
@@ -767,15 +740,13 @@ describe('aiChatRouter', () => {
|
|
|
767
740
|
];
|
|
768
741
|
const mockGenerateObject = vi.fn().mockResolvedValue({ object: {} });
|
|
769
742
|
|
|
770
|
-
vi.mocked(
|
|
771
|
-
vi.mocked(initModelRuntimeWithUserPayload).mockReturnValue({
|
|
743
|
+
vi.mocked(initModelRuntimeFromDB).mockResolvedValue({
|
|
772
744
|
generateObject: mockGenerateObject,
|
|
773
745
|
} as any);
|
|
774
746
|
|
|
775
|
-
const caller = aiChatRouter.createCaller(mockCtx as any);
|
|
747
|
+
const caller = aiChatRouter.createCaller({ ...mockCtx, serverDB: {} } as any);
|
|
776
748
|
|
|
777
749
|
const input = {
|
|
778
|
-
keyVaultsPayload: 'encrypted-payload',
|
|
779
750
|
messages: [],
|
|
780
751
|
model: 'gpt-4o',
|
|
781
752
|
provider: 'openai',
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
type SendMessageServerResponse,
|
|
4
4
|
StructureOutputSchema,
|
|
5
5
|
} from '@lobechat/types';
|
|
6
|
-
import { TRPCError } from '@trpc/server';
|
|
7
6
|
import debug from 'debug';
|
|
8
7
|
|
|
9
8
|
import { LOADING_FLAT } from '@/const/message';
|
|
@@ -13,11 +12,10 @@ import { ThreadModel } from '@/database/models/thread';
|
|
|
13
12
|
import { TopicModel } from '@/database/models/topic';
|
|
14
13
|
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
|
15
14
|
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
16
|
-
import {
|
|
15
|
+
import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
|
|
17
16
|
import { resolveContext } from '@/server/routers/lambda/_helpers/resolveContext';
|
|
18
17
|
import { AiChatService } from '@/server/services/aiChat';
|
|
19
18
|
import { FileService } from '@/server/services/file';
|
|
20
|
-
import { getXorPayload } from '@/utils/server';
|
|
21
19
|
|
|
22
20
|
const log = debug('lobe-lambda-router:ai-chat');
|
|
23
21
|
|
|
@@ -37,28 +35,14 @@ const aiChatProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
export const aiChatRouter = router({
|
|
40
|
-
outputJSON: aiChatProcedure.input(StructureOutputSchema).mutation(async ({ input }) => {
|
|
38
|
+
outputJSON: aiChatProcedure.input(StructureOutputSchema).mutation(async ({ input, ctx }) => {
|
|
41
39
|
log('outputJSON called with provider: %s, model: %s', input.provider, input.model);
|
|
42
40
|
log('messages count: %d', input.messages.length);
|
|
43
41
|
log('schema: %O', input.schema);
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
payload = getXorPayload(input.keyVaultsPayload);
|
|
49
|
-
log('payload parsed successfully');
|
|
50
|
-
} catch (e) {
|
|
51
|
-
log('payload parse error: %O', e);
|
|
52
|
-
console.warn('user payload parse error', e);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!payload) {
|
|
56
|
-
log('payload is empty, throwing error');
|
|
57
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'keyVaultsPayload is not correct' });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
log('initializing model runtime with provider: %s', input.provider);
|
|
61
|
-
const modelRuntime = initModelRuntimeWithUserPayload(input.provider, payload);
|
|
43
|
+
log('initializing model runtime from DB with provider: %s', input.provider);
|
|
44
|
+
// Read user's provider config from database
|
|
45
|
+
const modelRuntime = await initModelRuntimeFromDB(ctx.serverDB, ctx.userId, input.provider);
|
|
62
46
|
|
|
63
47
|
log('calling generateObject');
|
|
64
48
|
const result = await modelRuntime.generateObject({
|