@lobehub/lobehub 2.0.5 → 2.0.7

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 (38) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v2.json +14 -0
  4. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +2 -0
  5. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +2 -0
  6. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +2 -0
  7. package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +2 -0
  8. package/docs/self-hosting/platform/docker.mdx +6 -0
  9. package/docs/self-hosting/platform/docker.zh-CN.mdx +6 -0
  10. package/docs/self-hosting/platform/vercel.mdx +0 -49
  11. package/docs/self-hosting/platform/vercel.zh-CN.mdx +0 -47
  12. package/docs/self-hosting/start.mdx +0 -20
  13. package/docs/self-hosting/start.zh-CN.mdx +0 -18
  14. package/package.json +1 -1
  15. package/packages/database/src/repositories/aiInfra/index.test.ts +52 -0
  16. package/packages/database/src/repositories/aiInfra/index.ts +103 -0
  17. package/packages/model-runtime/src/core/streams/protocol.ts +3 -1
  18. package/src/app/(backend)/api/workflows/memory-user-memory/pipelines/persona/update-writing/route.ts +19 -19
  19. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert.tsx +3 -2
  20. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +3 -3
  21. package/src/app/[variants]/onboarding/components/KlavisServerList/hooks/useKlavisOAuth.ts +12 -5
  22. package/src/components/DebugNode.tsx +21 -0
  23. package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +3 -3
  24. package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +16 -13
  25. package/src/features/ChatInput/ActionBar/Tools/ToolsList.tsx +51 -40
  26. package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +7 -1
  27. package/src/features/ChatInput/ActionBar/components/ActionPopover.tsx +14 -11
  28. package/src/layout/GlobalProvider/useUserStateRedirect.ts +6 -2
  29. package/src/libs/observability/traceparent.test.ts +46 -7
  30. package/src/libs/observability/traceparent.ts +12 -10
  31. package/src/server/routers/lambda/klavis.ts +38 -10
  32. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +181 -26
  33. package/src/server/services/memory/userMemory/extract.ts +120 -96
  34. package/src/server/services/memory/userMemory/persona/__tests__/service.test.ts +46 -0
  35. package/src/server/services/memory/userMemory/persona/service.ts +47 -6
  36. package/src/store/tool/slices/klavisStore/action.ts +20 -0
  37. package/docs/self-hosting/server-database.mdx +0 -157
  38. package/docs/self-hosting/server-database.zh-CN.mdx +0 -146
@@ -57,54 +57,123 @@ const createExecutor = (privateOverrides?: Partial<MemoryExtractionPrivateConfig
57
57
  };
58
58
 
59
59
  describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
60
- it('prefers configured providers/models for gatekeeper, embedding, and layer extractors', () => {
60
+ it('prefers configured providers/models for gatekeeper, embedding, and layer extractors', async () => {
61
61
  const executor = createExecutor({
62
- embeddingPreferredProviders: ['provider-e'],
63
- agentGateKeeperPreferredModels: ['gate-1'],
64
- agentGateKeeperPreferredProviders: ['provider-a', 'provider-b'],
65
- agentLayerExtractorPreferredProviders: ['provider-l'],
62
+ embeddingPreferredProviders: ['provider-c', 'provider-a'],
63
+ agentGateKeeperPreferredModels: ['model-chat-1', 'vendor-prefix/model-chat-1'],
64
+ agentGateKeeperPreferredProviders: ['provider-c', 'provider-a'],
65
+ agentLayerExtractorPreferredProviders: ['provider-c', 'provider-a'],
66
66
  });
67
67
 
68
68
  const runtimeState = createRuntimeState(
69
69
  [
70
- { abilities: {}, id: 'gate-1', providerId: 'provider-a', type: 'chat' },
71
- { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
72
- { abilities: {}, id: 'embed-1', providerId: 'provider-e', type: 'embedding' },
73
- { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
74
- { abilities: {}, id: 'layer-act', providerId: 'provider-l', type: 'chat' },
75
- { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
76
- { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
77
- { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
70
+ {
71
+ abilities: {},
72
+ enabled: true,
73
+ id: 'model-chat-1',
74
+ type: 'chat',
75
+ providerId: 'provider-a',
76
+ },
77
+ {
78
+ abilities: {},
79
+ enabled: true,
80
+ id: 'model-embedding-1',
81
+ type: 'embedding',
82
+ providerId: 'provider-e',
83
+ },
84
+ {
85
+ abilities: {},
86
+ enabled: true,
87
+ id: 'vendor-prefix/model-chat-1',
88
+ type: 'chat',
89
+ providerId: 'provider-b',
90
+ },
91
+ {
92
+ abilities: {},
93
+ enabled: true,
94
+ id: 'vendor-prefix/model-embedding-1',
95
+ type: 'embedding',
96
+ providerId: 'provider-b',
97
+ },
98
+ {
99
+ abilities: {},
100
+ enabled: false,
101
+ id: 'model-chat-1',
102
+ type: 'chat',
103
+ providerId: 'provider-c',
104
+ },
105
+ {
106
+ abilities: {},
107
+ enabled: false,
108
+ id: 'model-embedding-1',
109
+ type: 'embedding',
110
+ providerId: 'provider-c',
111
+ },
78
112
  ],
79
113
  {
80
114
  'provider-a': { apiKey: 'a-key' },
81
115
  'provider-b': { apiKey: 'b-key' },
116
+ 'provider-c': { apiKey: 'c-key' },
82
117
  'provider-e': { apiKey: 'e-key' },
83
- 'provider-l': { apiKey: 'l-key' },
84
118
  },
85
119
  );
86
120
 
87
- const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
121
+ const keyVaults = await (executor as any).resolveRuntimeKeyVaults(runtimeState);
88
122
 
89
123
  expect(keyVaults).toMatchObject({
90
- 'provider-a': { apiKey: 'a-key' }, // gatekeeper picked preferred provider/model
91
- 'provider-e': { apiKey: 'e-key' }, // embedding honored preferred provider
92
- 'provider-l': { apiKey: 'l-key' }, // layer extractor models resolved
124
+ 'provider-a': { apiKey: 'a-key' },
125
+ 'provider-e': { apiKey: 'e-key' },
93
126
  });
94
127
  });
95
128
 
96
- it('warns and falls back to server provider when no enabled provider satisfies embedding model', () => {
129
+ it('warns and falls back to server provider when no enabled provider satisfies embedding model', async () => {
97
130
  const executor = createExecutor();
98
131
  const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
99
132
 
100
133
  const runtimeState = createRuntimeState(
101
134
  [
102
- { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
103
- { abilities: {}, id: 'layer-act', providerId: 'provider-l', type: 'chat' },
104
- { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
105
- { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
106
- { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
107
- { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
135
+ {
136
+ abilities: {},
137
+ enabled: true,
138
+ id: 'model-chat-1',
139
+ type: 'chat',
140
+ providerId: 'provider-a',
141
+ },
142
+ {
143
+ abilities: {},
144
+ enabled: true,
145
+ id: 'model-embedding-1',
146
+ type: 'embedding',
147
+ providerId: 'provider-e',
148
+ },
149
+ {
150
+ abilities: {},
151
+ enabled: true,
152
+ id: 'vendor-prefix/model-chat-1',
153
+ type: 'chat',
154
+ providerId: 'provider-b',
155
+ },
156
+ {
157
+ abilities: {},
158
+ enabled: true,
159
+ id: 'vendor-prefix/model-embedding-1',
160
+ type: 'embedding',
161
+ providerId: 'provider-b',
162
+ },
163
+ {
164
+ abilities: {},
165
+ enabled: false,
166
+ id: 'model-chat-1',
167
+ type: 'chat',
168
+ providerId: 'provider-c',
169
+ },
170
+ {
171
+ abilities: {},
172
+ enabled: false,
173
+ id: 'model-embedding-1',
174
+ type: 'embedding',
175
+ providerId: 'provider-c',
176
+ },
108
177
  ],
109
178
  {
110
179
  'provider-b': { apiKey: 'b-key' },
@@ -112,7 +181,7 @@ describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
112
181
  },
113
182
  );
114
183
 
115
- const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
184
+ const keyVaults = await (executor as any).resolveRuntimeKeyVaults(runtimeState);
116
185
 
117
186
  expect(keyVaults).toMatchObject({
118
187
  'provider-b': { apiKey: 'b-key' },
@@ -123,4 +192,90 @@ describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
123
192
 
124
193
  warnSpy.mockRestore();
125
194
  });
195
+
196
+ it('ignores disabled providers when resolving key vaults', async () => {
197
+ const executor = createExecutor({
198
+ embeddingPreferredProviders: ['provider-disabled', 'provider-a'],
199
+ });
200
+
201
+ const runtimeState = createRuntimeState(
202
+ [
203
+ {
204
+ abilities: {},
205
+ enabled: false,
206
+ id: 'embed-1',
207
+ type: 'embedding',
208
+ providerId: 'provider-disabled',
209
+ },
210
+ {
211
+ abilities: {},
212
+ enabled: true,
213
+ id: 'embed-1',
214
+ type: 'embedding',
215
+ providerId: 'provider-a',
216
+ },
217
+ ],
218
+ {
219
+ 'provider-disabled': { apiKey: 'disabled-key' },
220
+ 'provider-a': { apiKey: 'a-key' },
221
+ },
222
+ );
223
+
224
+ const keyVaults = await (executor as any).resolveRuntimeKeyVaults(runtimeState);
225
+
226
+ expect(keyVaults).toMatchObject({
227
+ 'provider-a': { apiKey: 'a-key' },
228
+ });
229
+ expect(keyVaults).not.toHaveProperty('provider-disabled');
230
+ });
231
+
232
+ it('respects preferred provider order when multiple providers have the model', async () => {
233
+ const executor = createExecutor({
234
+ agentGateKeeper: {
235
+ model: 'gate-2',
236
+ provider: 'provider-a', // fallback provider differs from preferred order
237
+ apiKey: 'sys-a-key',
238
+ baseURL: 'https://api-a.example.com',
239
+ language: 'English',
240
+ },
241
+ agentGateKeeperPreferredProviders: ['provider-b', 'provider-a'],
242
+ });
243
+
244
+ const runtimeState = createRuntimeState(
245
+ [
246
+ { abilities: {}, enabled: true, id: 'gate-2', type: 'chat', providerId: 'provider-a' },
247
+ { abilities: {}, enabled: true, id: 'gate-2', type: 'chat', providerId: 'provider-b' },
248
+ ],
249
+ {
250
+ 'provider-a': { apiKey: 'a-key' },
251
+ 'provider-b': { apiKey: 'b-key' },
252
+ },
253
+ );
254
+
255
+ const keyVaults = await (executor as any).resolveRuntimeKeyVaults(runtimeState);
256
+
257
+ expect(keyVaults).toMatchObject({
258
+ 'provider-b': { apiKey: 'b-key' }, // picks first preferred provider
259
+ });
260
+ expect(keyVaults).not.toHaveProperty('provider-a');
261
+ });
262
+
263
+ it('falls back to configured provider when no enabled models match', async () => {
264
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
265
+ const executor = createExecutor({
266
+ agentGateKeeper: { model: 'gate-2', provider: 'provider-fallback', apiKey: 'sys-fb-key' },
267
+ });
268
+
269
+ const runtimeState = createRuntimeState([], {
270
+ 'provider-fallback': { apiKey: 'fb-key' },
271
+ });
272
+
273
+ const keyVaults = await (executor as any).resolveRuntimeKeyVaults(runtimeState);
274
+
275
+ expect(keyVaults).toMatchObject({
276
+ 'provider-fallback': { apiKey: 'fb-key' },
277
+ });
278
+
279
+ warnSpy.mockRestore();
280
+ });
126
281
  });
@@ -296,20 +296,54 @@ const maskSecret = (value?: string) => {
296
296
  return `${value.slice(0, 6)}***${value.slice(-4)}`;
297
297
  };
298
298
 
299
- const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
300
- const provider = agent.provider || 'openai';
301
- const { apiKey: userApiKey, baseURL: userBaseURL } = extractCredentialsFromVault(
302
- keyVaults?.[normalizeProvider(provider)],
299
+ type ProviderCredential = { apiKey?: string; baseURL?: string };
300
+
301
+ type RuntimeResolveOptions = {
302
+ fallback?: ProviderCredential;
303
+ preferred?: {
304
+ providerIds?: string[];
305
+ };
306
+ };
307
+
308
+ const resolveRuntimeAgentConfig = (
309
+ agent: MemoryAgentConfig,
310
+ keyVaults?: ProviderKeyVaultMap,
311
+ options?: RuntimeResolveOptions,
312
+ ) => {
313
+ const normalizedPreferredProviders = (options?.preferred?.providerIds || [])
314
+ .map(normalizeProvider)
315
+ .filter(Boolean);
316
+
317
+ const providerOrder = Array.from(
318
+ new Set([
319
+ ...normalizedPreferredProviders,
320
+ normalizeProvider(agent.provider || 'openai'),
321
+ ...Object.keys(keyVaults || {}),
322
+ ]),
303
323
  );
304
324
 
305
- // Only use the user baseURL if we are also using their API key; otherwise fall back entirely
306
- // to system config to avoid mixing credentials.
307
- const useUserCredential = !!userApiKey;
308
- const apiKey = useUserCredential ? userApiKey : agent.apiKey;
309
- const baseURL = useUserCredential ? userBaseURL || agent.baseURL : agent.baseURL;
310
- const source = useUserCredential ? 'user-keyvault' : 'system-config';
325
+ for (const provider of providerOrder) {
326
+ const { apiKey: userApiKey, baseURL: userBaseURL } = extractCredentialsFromVault(
327
+ keyVaults?.[provider],
328
+ );
329
+ if (!userApiKey) continue;
330
+
331
+ // Only use the user baseURL if we are also using their API key; otherwise fall back entirely
332
+ // to system config to avoid mixing credentials.
333
+ return {
334
+ apiKey: userApiKey,
335
+ baseURL: userBaseURL || agent.baseURL || options?.fallback?.baseURL,
336
+ provider,
337
+ source: 'user-keyvault' as const,
338
+ };
339
+ }
311
340
 
312
- return { apiKey, baseURL, provider, source };
341
+ return {
342
+ apiKey: agent.apiKey || options?.fallback?.apiKey,
343
+ baseURL: agent.baseURL || options?.fallback?.baseURL,
344
+ provider: agent.provider || 'openai',
345
+ source: 'system-config' as const,
346
+ };
313
347
  };
314
348
 
315
349
  const logRuntime = debug('lobe-server:memory:user-memory:runtime');
@@ -329,8 +363,12 @@ const debugRuntimeInit = (
329
363
  });
330
364
  };
331
365
 
332
- const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
333
- const resolved = resolveRuntimeAgentConfig(agent, keyVaults);
366
+ const initRuntimeForAgent = async (
367
+ agent: MemoryAgentConfig,
368
+ keyVaults?: ProviderKeyVaultMap,
369
+ options?: RuntimeResolveOptions,
370
+ ) => {
371
+ const resolved = resolveRuntimeAgentConfig(agent, keyVaults, options);
334
372
  debugRuntimeInit(agent, resolved);
335
373
 
336
374
  if (!resolved.apiKey) {
@@ -1142,7 +1180,7 @@ export class MemoryExtractionExecutor {
1142
1180
  userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
1143
1181
  this.getAiProviderRuntimeState(job.userId),
1144
1182
  ]);
1145
- const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
1183
+ const keyVaults = await this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
1146
1184
  const language = userState.settings?.general?.responseLanguage;
1147
1185
 
1148
1186
  const runtimes = await this.getRuntime(job.userId, keyVaults);
@@ -1827,7 +1865,9 @@ export class MemoryExtractionExecutor {
1827
1865
  return aiInfraRepos.getAiProviderRuntimeState(KeyVaultsGateKeeper.getUserKeyVaults);
1828
1866
  }
1829
1867
 
1830
- private resolveRuntimeKeyVaults(runtimeState: AiProviderRuntimeState): ProviderKeyVaultMap {
1868
+ private async resolveRuntimeKeyVaults(
1869
+ runtimeState: AiProviderRuntimeState,
1870
+ ): Promise<ProviderKeyVaultMap> {
1831
1871
  const normalizedRuntimeConfig = Object.fromEntries(
1832
1872
  Object.entries(runtimeState.runtimeConfig || {}).map(([providerId, config]) => [
1833
1873
  normalizeProvider(providerId),
@@ -1835,98 +1875,46 @@ export class MemoryExtractionExecutor {
1835
1875
  ]),
1836
1876
  );
1837
1877
 
1838
- const providerModels = runtimeState.enabledAiModels.reduce<Record<string, Set<string>>>(
1839
- (acc, model) => {
1840
- const providerId = normalizeProvider(model.providerId);
1841
- acc[providerId] = acc[providerId] || new Set<string>();
1842
- acc[providerId].add(model.id);
1843
- return acc;
1844
- },
1845
- {},
1846
- );
1847
-
1848
- const resolveProviderForModel = (
1849
- modelId: string,
1850
- fallbackProvider?: string,
1851
- preferredProviders?: string[],
1852
- preferredModels?: string[],
1853
- label?: string,
1854
- ) => {
1855
- const providerOrder = Array.from(
1856
- new Set(
1857
- [
1858
- ...(preferredProviders?.map(normalizeProvider) || []),
1859
- fallbackProvider ? normalizeProvider(fallbackProvider) : undefined,
1860
- ...Object.keys(providerModels),
1861
- ].filter(Boolean) as string[],
1862
- ),
1863
- );
1864
-
1865
- const candidateModels = preferredModels && preferredModels.length > 0 ? preferredModels : [];
1866
-
1867
- for (const providerId of providerOrder) {
1868
- const models = providerModels[providerId];
1869
- if (!models) continue;
1870
- if (models.has(modelId)) return providerId;
1871
-
1872
- const preferredMatch = candidateModels.find((preferredModel) => models.has(preferredModel));
1873
- if (preferredMatch) return providerId;
1874
- }
1875
- if (fallbackProvider) {
1876
- console.warn(
1877
- `[memory-extraction] no enabled provider found for ${label || 'model'} "${modelId}"`,
1878
- `(preferred ${preferredProviders}), falling back to server-configured provider "${fallbackProvider}".`,
1879
- );
1880
-
1881
- return normalizeProvider(fallbackProvider);
1882
- }
1883
-
1884
- throw new Error(
1885
- `Unable to resolve provider for ${label || 'model'} "${modelId}". ` +
1886
- `Check preferred providers/models configuration.`,
1887
- );
1888
- };
1889
-
1890
1878
  const keyVaults: ProviderKeyVaultMap = {};
1891
1879
 
1892
- const gatekeeperProvider = resolveProviderForModel(
1893
- this.modelConfig.gateModel,
1894
- this.privateConfig.agentGateKeeper.provider,
1895
- this.gatekeeperPreferredProviders,
1896
- this.gatekeeperPreferredModels,
1897
- 'gatekeeper',
1898
- );
1880
+ const gatekeeperProvider = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1881
+ fallbackProvider: this.privateConfig.agentGateKeeper.provider,
1882
+ label: 'gatekeeper',
1883
+ modelId: this.modelConfig.gateModel,
1884
+ preferredModels: this.gatekeeperPreferredModels,
1885
+ preferredProviders: this.gatekeeperPreferredProviders,
1886
+ });
1899
1887
  const gatekeeperRuntime = normalizedRuntimeConfig[gatekeeperProvider];
1900
1888
  if (gatekeeperRuntime?.keyVaults) {
1901
1889
  keyVaults[gatekeeperProvider] = gatekeeperRuntime.keyVaults;
1902
1890
  }
1903
1891
 
1904
- const embeddingProvider = resolveProviderForModel(
1905
- this.modelConfig.embeddingsModel,
1906
- this.privateConfig.embedding.provider,
1907
- this.embeddingPreferredProviders,
1908
- this.embeddingPreferredModels,
1909
- 'embedding',
1910
- );
1892
+ const embeddingProvider = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1893
+ fallbackProvider: this.privateConfig.embedding.provider,
1894
+ label: 'embedding',
1895
+ modelId: this.modelConfig.embeddingsModel,
1896
+ preferredModels: this.embeddingPreferredModels,
1897
+ preferredProviders: this.embeddingPreferredProviders,
1898
+ });
1911
1899
  const embeddingRuntime = normalizedRuntimeConfig[embeddingProvider];
1912
1900
  if (embeddingRuntime?.keyVaults) {
1913
1901
  keyVaults[embeddingProvider] = embeddingRuntime.keyVaults;
1914
1902
  }
1915
1903
 
1916
- Object.values(this.modelConfig.layerModels).forEach((model) => {
1917
- if (!model) return;
1918
- const providerId = resolveProviderForModel(
1919
- model,
1920
- this.privateConfig.agentLayerExtractor.provider,
1921
- this.layerPreferredProviders,
1922
- this.layerPreferredModels,
1923
- 'layer extractor',
1924
- );
1904
+ for (const model of Object.values(this.modelConfig.layerModels)) {
1905
+ if (!model) continue;
1906
+ const providerId = await AiInfraRepos.tryMatchingProviderFrom(runtimeState, {
1907
+ fallbackProvider: this.privateConfig.agentLayerExtractor.provider,
1908
+ label: 'layer extractor',
1909
+ modelId: model,
1910
+ preferredModels: this.layerPreferredModels,
1911
+ preferredProviders: this.layerPreferredProviders,
1912
+ });
1925
1913
  const runtime = normalizedRuntimeConfig[providerId];
1926
1914
  if (runtime?.keyVaults) {
1927
1915
  keyVaults[providerId] = runtime.keyVaults;
1928
1916
  }
1929
- });
1917
+ }
1930
1918
 
1931
1919
  return keyVaults;
1932
1920
  }
@@ -1944,10 +1932,46 @@ export class MemoryExtractionExecutor {
1944
1932
  const cached = this.runtimeCache.get(userId);
1945
1933
  if (cached) return cached;
1946
1934
 
1935
+ const embeddingOptions: RuntimeResolveOptions = {
1936
+ fallback: {
1937
+ apiKey: this.privateConfig.embedding.apiKey,
1938
+ baseURL: this.privateConfig.embedding.baseURL,
1939
+ },
1940
+ preferred: { providerIds: this.embeddingPreferredProviders },
1941
+ };
1942
+
1943
+ const gatekeeperOptions: RuntimeResolveOptions = {
1944
+ fallback: {
1945
+ apiKey: this.privateConfig.agentGateKeeper.apiKey,
1946
+ baseURL: this.privateConfig.agentGateKeeper.baseURL,
1947
+ },
1948
+ preferred: { providerIds: this.gatekeeperPreferredProviders },
1949
+ };
1950
+
1951
+ const layerExtractorOptions: RuntimeResolveOptions = {
1952
+ fallback: {
1953
+ apiKey: this.privateConfig.agentLayerExtractor.apiKey,
1954
+ baseURL: this.privateConfig.agentLayerExtractor.baseURL,
1955
+ },
1956
+ preferred: { providerIds: this.layerPreferredProviders },
1957
+ };
1958
+
1947
1959
  const runtimes: RuntimeBundle = {
1948
- embeddings: await initRuntimeForAgent(this.privateConfig.embedding, keyVaults),
1949
- gatekeeper: await initRuntimeForAgent(this.privateConfig.agentGateKeeper, keyVaults),
1950
- layerExtractor: await initRuntimeForAgent(this.privateConfig.agentLayerExtractor, keyVaults),
1960
+ embeddings: await initRuntimeForAgent(
1961
+ { ...this.privateConfig.embedding },
1962
+ keyVaults,
1963
+ embeddingOptions,
1964
+ ),
1965
+ gatekeeper: await initRuntimeForAgent(
1966
+ { ...this.privateConfig.agentGateKeeper },
1967
+ keyVaults,
1968
+ gatekeeperOptions,
1969
+ ),
1970
+ layerExtractor: await initRuntimeForAgent(
1971
+ { ...this.privateConfig.agentLayerExtractor },
1972
+ keyVaults,
1973
+ layerExtractorOptions,
1974
+ ),
1951
1975
  };
1952
1976
 
1953
1977
  this.runtimeCache.set(userId, runtimes);
@@ -1986,7 +2010,7 @@ export class MemoryExtractionExecutor {
1986
2010
  userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
1987
2011
  this.getAiProviderRuntimeState(params.userId),
1988
2012
  ]);
1989
- const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
2013
+ const keyVaults = await this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
1990
2014
  const language = params.language || userState.settings?.general?.responseLanguage;
1991
2015
 
1992
2016
  const runtimes = await this.getRuntime(params.userId, keyVaults);
@@ -2266,7 +2290,7 @@ export class MemoryExtractionWorkflowService {
2266
2290
 
2267
2291
  const url = getWorkflowUrl(WORKFLOW_PATHS.personaUpdate, baseUrl);
2268
2292
  return this.getClient().trigger({
2269
- body: { userId },
2293
+ body: { userIds: [userId] },
2270
2294
  flowControl: {
2271
2295
  key: `memory-user-memory.pipelines.persona.update-write.${userId}`,
2272
2296
  parallelism: 1,
@@ -8,6 +8,32 @@ import { UserPersonaModel } from '@/database/models/userMemory/persona';
8
8
 
9
9
  import { UserPersonaService } from '../service';
10
10
 
11
+ // Use var to avoid TDZ with vi.mock hoisting
12
+ var aiInfraMocks:
13
+ | undefined
14
+ | {
15
+ getAiProviderRuntimeState: ReturnType<typeof vi.fn>;
16
+ tryMatchingModelFrom: ReturnType<typeof vi.fn>;
17
+ tryMatchingProviderFrom: ReturnType<typeof vi.fn>;
18
+ };
19
+
20
+ vi.mock('@/database/repositories/aiInfra', () => {
21
+ aiInfraMocks = {
22
+ getAiProviderRuntimeState: vi.fn(),
23
+ tryMatchingModelFrom: vi.fn(),
24
+ tryMatchingProviderFrom: vi.fn(),
25
+ };
26
+
27
+ const AiInfraRepos = vi.fn().mockImplementation(() => ({
28
+ getAiProviderRuntimeState: aiInfraMocks!.getAiProviderRuntimeState,
29
+ })) as unknown as typeof import('@/database/repositories/aiInfra').AiInfraRepos;
30
+
31
+ (AiInfraRepos as any).tryMatchingModelFrom = aiInfraMocks!.tryMatchingModelFrom;
32
+ (AiInfraRepos as any).tryMatchingProviderFrom = aiInfraMocks!.tryMatchingProviderFrom;
33
+
34
+ return { AiInfraRepos };
35
+ });
36
+
11
37
  vi.mock('@/server/globalConfig/parseMemoryExtractionConfig', () => ({
12
38
  parseMemoryExtractionConfig: () => ({
13
39
  agentLayerExtractor: {
@@ -28,6 +54,10 @@ vi.mock('@/server/globalConfig/parseMemoryExtractionConfig', () => ({
28
54
  }),
29
55
  }));
30
56
 
57
+ vi.mock('@/server/modules/KeyVaultsEncrypt', () => ({
58
+ KeyVaultsGateKeeper: { getUserKeyVaults: vi.fn() },
59
+ }));
60
+
31
61
  const structuredResult = {
32
62
  diff: '- updated',
33
63
  memoryIds: ['mem-1'],
@@ -56,6 +86,22 @@ const userId = 'user-persona-service';
56
86
 
57
87
  beforeEach(async () => {
58
88
  toolCall.mockClear();
89
+ aiInfraMocks!.getAiProviderRuntimeState.mockReset();
90
+ aiInfraMocks!.tryMatchingModelFrom.mockReset();
91
+ aiInfraMocks!.tryMatchingProviderFrom.mockReset();
92
+ aiInfraMocks!.tryMatchingModelFrom.mockResolvedValue('openai');
93
+ aiInfraMocks!.tryMatchingProviderFrom.mockResolvedValue('openai');
94
+ aiInfraMocks!.getAiProviderRuntimeState.mockResolvedValue({
95
+ enabledAiModels: [
96
+ { abilities: {}, enabled: true, id: 'gpt-mock', providerId: 'openai', type: 'chat' },
97
+ ],
98
+ enabledAiProviders: [],
99
+ enabledChatAiProviders: [],
100
+ enabledImageAiProviders: [],
101
+ runtimeConfig: {
102
+ openai: { keyVaults: { apiKey: 'vault-key', baseURL: 'https://vault.example.com' } },
103
+ },
104
+ });
59
105
  db = await getTestDB();
60
106
 
61
107
  await db.delete(users);