@lobehub/lobehub 2.0.0-next.200 → 2.0.0-next.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +2 -0
  4. package/locales/ar/models.json +64 -7
  5. package/locales/ar/plugin.json +2 -1
  6. package/locales/ar/providers.json +1 -0
  7. package/locales/bg-BG/chat.json +2 -0
  8. package/locales/bg-BG/models.json +49 -5
  9. package/locales/bg-BG/plugin.json +2 -1
  10. package/locales/bg-BG/providers.json +1 -0
  11. package/locales/de-DE/chat.json +2 -0
  12. package/locales/de-DE/models.json +36 -7
  13. package/locales/de-DE/plugin.json +2 -1
  14. package/locales/de-DE/providers.json +1 -0
  15. package/locales/en-US/chat.json +2 -0
  16. package/locales/en-US/models.json +10 -10
  17. package/locales/en-US/plugin.json +2 -1
  18. package/locales/en-US/providers.json +1 -0
  19. package/locales/es-ES/chat.json +2 -0
  20. package/locales/es-ES/models.json +106 -7
  21. package/locales/es-ES/plugin.json +2 -1
  22. package/locales/es-ES/providers.json +1 -0
  23. package/locales/fa-IR/chat.json +2 -0
  24. package/locales/fa-IR/models.json +83 -5
  25. package/locales/fa-IR/plugin.json +2 -1
  26. package/locales/fa-IR/providers.json +1 -0
  27. package/locales/fr-FR/chat.json +2 -0
  28. package/locales/fr-FR/models.json +38 -7
  29. package/locales/fr-FR/plugin.json +2 -1
  30. package/locales/fr-FR/providers.json +1 -0
  31. package/locales/it-IT/chat.json +2 -0
  32. package/locales/it-IT/models.json +40 -5
  33. package/locales/it-IT/plugin.json +2 -1
  34. package/locales/it-IT/providers.json +1 -0
  35. package/locales/ja-JP/chat.json +2 -0
  36. package/locales/ja-JP/models.json +84 -7
  37. package/locales/ja-JP/plugin.json +2 -1
  38. package/locales/ja-JP/providers.json +1 -0
  39. package/locales/ko-KR/chat.json +2 -0
  40. package/locales/ko-KR/models.json +65 -7
  41. package/locales/ko-KR/plugin.json +2 -1
  42. package/locales/ko-KR/providers.json +1 -0
  43. package/locales/nl-NL/chat.json +2 -0
  44. package/locales/nl-NL/models.json +62 -5
  45. package/locales/nl-NL/plugin.json +2 -1
  46. package/locales/nl-NL/providers.json +1 -0
  47. package/locales/pl-PL/chat.json +2 -0
  48. package/locales/pl-PL/models.json +85 -0
  49. package/locales/pl-PL/plugin.json +2 -1
  50. package/locales/pl-PL/providers.json +1 -0
  51. package/locales/pt-BR/chat.json +2 -0
  52. package/locales/pt-BR/models.json +37 -6
  53. package/locales/pt-BR/plugin.json +2 -1
  54. package/locales/pt-BR/providers.json +1 -0
  55. package/locales/ru-RU/chat.json +2 -0
  56. package/locales/ru-RU/models.json +36 -7
  57. package/locales/ru-RU/plugin.json +2 -1
  58. package/locales/ru-RU/providers.json +1 -0
  59. package/locales/tr-TR/chat.json +2 -0
  60. package/locales/tr-TR/models.json +28 -7
  61. package/locales/tr-TR/plugin.json +2 -1
  62. package/locales/tr-TR/providers.json +1 -0
  63. package/locales/vi-VN/chat.json +2 -0
  64. package/locales/vi-VN/models.json +62 -5
  65. package/locales/vi-VN/plugin.json +2 -1
  66. package/locales/vi-VN/providers.json +1 -0
  67. package/locales/zh-CN/chat.json +2 -0
  68. package/locales/zh-CN/models.json +87 -6
  69. package/locales/zh-CN/plugin.json +2 -1
  70. package/locales/zh-CN/providers.json +1 -0
  71. package/locales/zh-TW/chat.json +2 -0
  72. package/locales/zh-TW/models.json +71 -7
  73. package/locales/zh-TW/plugin.json +2 -1
  74. package/locales/zh-TW/providers.json +1 -0
  75. package/package.json +2 -2
  76. package/packages/builtin-tool-gtd/src/client/Inspector/ExecTask/index.tsx +30 -15
  77. package/packages/builtin-tool-gtd/src/manifest.ts +1 -1
  78. package/packages/model-runtime/src/core/ModelRuntime.test.ts +44 -86
  79. package/packages/types/src/aiChat.ts +0 -1
  80. package/packages/types/src/message/ui/chat.ts +1 -1
  81. package/src/app/(backend)/middleware/auth/index.ts +16 -2
  82. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +30 -15
  83. package/src/app/(backend)/webapi/chat/[provider]/route.ts +44 -40
  84. package/src/app/(backend)/webapi/models/[provider]/pull/route.ts +4 -3
  85. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +36 -13
  86. package/src/app/(backend)/webapi/models/[provider]/route.ts +4 -11
  87. package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +8 -2
  88. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +21 -23
  89. package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +16 -3
  90. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +17 -20
  91. package/src/features/Conversation/Messages/Tasks/shared/ErrorState.tsx +16 -11
  92. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +6 -20
  93. package/src/features/Conversation/Messages/Tasks/shared/ProcessingState.tsx +10 -20
  94. package/src/features/User/DataStatistics.tsx +4 -4
  95. package/src/hooks/useQueryParam.ts +0 -2
  96. package/src/libs/trpc/async/asyncAuth.ts +0 -2
  97. package/src/libs/trpc/async/context.ts +3 -11
  98. package/src/locales/default/chat.ts +2 -0
  99. package/src/locales/default/plugin.ts +2 -1
  100. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +6 -6
  101. package/src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts +3 -3
  102. package/src/server/modules/AgentRuntime/factory.ts +39 -20
  103. package/src/server/modules/ModelRuntime/index.ts +138 -1
  104. package/src/server/routers/async/__tests__/caller.test.ts +22 -27
  105. package/src/server/routers/async/caller.ts +4 -6
  106. package/src/server/routers/async/file.ts +10 -5
  107. package/src/server/routers/async/image.ts +5 -4
  108. package/src/server/routers/async/ragEval.ts +7 -5
  109. package/src/server/routers/lambda/__tests__/aiChat.test.ts +8 -37
  110. package/src/server/routers/lambda/aiChat.ts +5 -21
  111. package/src/server/routers/lambda/chunk.ts +9 -28
  112. package/src/server/routers/lambda/image.ts +1 -7
  113. package/src/server/routers/lambda/ragEval.ts +1 -1
  114. package/src/server/routers/lambda/userMemories/reembed.ts +4 -1
  115. package/src/server/routers/lambda/userMemories/search.ts +7 -7
  116. package/src/server/routers/lambda/userMemories/shared.ts +8 -10
  117. package/src/server/routers/lambda/userMemories/tools.ts +140 -118
  118. package/src/server/routers/lambda/userMemories.test.ts +3 -7
  119. package/src/server/routers/lambda/userMemories.ts +44 -29
  120. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +87 -0
  121. package/src/server/services/agentRuntime/AgentRuntimeService.ts +53 -2
  122. package/src/server/services/agentRuntime/__tests__/executeSync.test.ts +2 -6
  123. package/src/server/services/agentRuntime/__tests__/stepLifecycleCallbacks.test.ts +1 -1
  124. package/src/server/services/chunk/index.ts +6 -5
  125. package/src/server/services/toolExecution/types.ts +1 -2
  126. package/src/services/__tests__/_url.test.ts +0 -1
  127. package/src/services/_url.ts +0 -3
  128. package/src/services/aiChat.ts +5 -12
  129. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +0 -2
  130. 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 { type ClientSecretPayload } from '@lobechat/types';
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', { apiKey: 'test-key' });
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 user payload in x-lobe-chat-auth header', async () => {
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', testPayload);
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', {})).rejects.toThrow(
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', { apiKey: 'test-key' });
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, payload: ClientSecretPayload) => {
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({ payload, userId })),
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, jwtPayload } = options;
61
+ const { userId } = options;
64
62
 
65
- const httpClient = await createAsyncServerClient(userId, jwtPayload);
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 { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
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
- const agentRuntime = initModelRuntimeWithUserPayload(provider, ctx.jwtPayload);
99
-
100
- const embeddings = await agentRuntime.embeddings({
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, ctx.jwtPayload);
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 { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
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
- const agentRuntime = initModelRuntimeWithUserPayload(provider, ctx.jwtPayload);
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 agentRuntime.createImage!({
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 = agentRuntime.getAuthHeaders();
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 { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
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
- const agentRuntime = await initModelRuntimeWithUserPayload(
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 agentRuntime.embeddings({
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 agentRuntime.chat({
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
- initModelRuntimeWithUserPayload: vi.fn(),
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 { getXorPayload } = await import('@/utils/server');
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(getXorPayload).mockReturnValue(mockPayload);
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(getXorPayload).toHaveBeenCalledWith('encrypted-payload');
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 { getXorPayload } = await import('@/utils/server');
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(getXorPayload).mockReturnValue(mockPayload);
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 { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
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
- let payload: object | undefined;
46
-
47
- try {
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({