@lobehub/lobehub 2.0.0-next.320 → 2.0.0-next.321

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 (106) hide show
  1. package/.codex/skills/vercel-react-best-practices/AGENTS.md +2410 -0
  2. package/.codex/skills/vercel-react-best-practices/SKILL.md +125 -0
  3. package/.codex/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  4. package/.codex/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
  5. package/.codex/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  6. package/.codex/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  7. package/.codex/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
  8. package/.codex/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  9. package/.codex/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  10. package/.codex/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  11. package/.codex/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  12. package/.codex/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  13. package/.codex/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  14. package/.codex/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  15. package/.codex/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  16. package/.codex/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  17. package/.codex/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  18. package/.codex/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  19. package/.codex/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
  20. package/.codex/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  21. package/.codex/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  22. package/.codex/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  23. package/.codex/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  24. package/.codex/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  25. package/.codex/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  26. package/.codex/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  27. package/.codex/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  28. package/.codex/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  29. package/.codex/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  30. package/.codex/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  31. package/.codex/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  32. package/.codex/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  33. package/.codex/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  34. package/.codex/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  35. package/.codex/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  36. package/.codex/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  37. package/.codex/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  38. package/.codex/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  39. package/.codex/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  40. package/.codex/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  41. package/.codex/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  42. package/.codex/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  43. package/.codex/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  44. package/.codex/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  45. package/.codex/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  46. package/.codex/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  47. package/.codex/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  48. package/.codex/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  49. package/.codex/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  50. package/.cursor/skills/vercel-react-best-practices/AGENTS.md +2410 -0
  51. package/.cursor/skills/vercel-react-best-practices/SKILL.md +125 -0
  52. package/.cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  53. package/.cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
  54. package/.cursor/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  55. package/.cursor/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
  56. package/.cursor/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
  57. package/.cursor/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  58. package/.cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  59. package/.cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
  60. package/.cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  61. package/.cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  62. package/.cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  63. package/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  64. package/.cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  65. package/.cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  66. package/.cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  67. package/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  68. package/.cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
  69. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  70. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  71. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  72. package/.cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  73. package/.cursor/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  74. package/.cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  75. package/.cursor/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  76. package/.cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  77. package/.cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  78. package/.cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  79. package/.cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  80. package/.cursor/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  81. package/.cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  82. package/.cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  83. package/.cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  84. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  85. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  86. package/.cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  87. package/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  88. package/.cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  89. package/.cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  90. package/.cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  91. package/.cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  92. package/.cursor/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  93. package/.cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  94. package/.cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  95. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  96. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  97. package/.cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  98. package/.cursor/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  99. package/CHANGELOG.md +25 -0
  100. package/changelog/v1.json +5 -0
  101. package/package.json +1 -1
  102. package/src/layout/GlobalProvider/FaviconProvider.tsx +45 -21
  103. package/src/server/globalConfig/parseMemoryExtractionConfig.ts +43 -4
  104. package/src/server/services/memory/userMemory/__tests__/extract.payload.test.ts +101 -0
  105. package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +111 -0
  106. package/src/server/services/memory/userMemory/extract.ts +155 -17
@@ -32,12 +32,18 @@ export type MemoryLayerExtractorConfig = MemoryLayerExtractorPublicConfig &
32
32
 
33
33
  export interface MemoryExtractionPrivateConfig {
34
34
  agentGateKeeper: MemoryAgentConfig;
35
+ agentGateKeeperPreferredModels?: string[];
36
+ agentGateKeeperPreferredProviders?: string[];
35
37
  agentLayerExtractor: MemoryLayerExtractorConfig;
38
+ agentLayerExtractorPreferredModels?: string[];
39
+ agentLayerExtractorPreferredProviders?: string[];
36
40
  concurrency?: number;
37
41
  embedding: MemoryAgentConfig;
42
+ embeddingPreferredModels?: string[];
43
+ embeddingPreferredProviders?: string[];
38
44
  featureFlags: {
39
45
  enableBenchmarkLoCoMo: boolean;
40
- },
46
+ };
41
47
  observabilityS3?: {
42
48
  accessKeyId?: string;
43
49
  bucketName?: string;
@@ -157,6 +163,12 @@ const sanitizeAgent = (agent?: MemoryAgentConfig): MemoryAgentPublicConfig | und
157
163
  return sanitized as MemoryAgentPublicConfig;
158
164
  };
159
165
 
166
+ const parsePreferredList = (value?: string) =>
167
+ value
168
+ ?.split(',')
169
+ .map((item) => item.trim().toLowerCase())
170
+ .filter(Boolean);
171
+
160
172
  export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig => {
161
173
  const agentGateKeeper = parseGateKeeperAgent();
162
174
  const agentLayerExtractor = parseLayerExtractorAgent(agentGateKeeper.model);
@@ -167,8 +179,8 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
167
179
  );
168
180
  const extractorObservabilityS3 = parseExtractorAgentObservabilityS3();
169
181
  const featureFlags = {
170
- enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true'
171
- }
182
+ enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true',
183
+ };
172
184
  const concurrencyRaw = process.env.MEMORY_USER_MEMORY_CONCURRENCY;
173
185
  const concurrency =
174
186
  concurrencyRaw !== undefined
@@ -191,7 +203,9 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
191
203
  return acc;
192
204
  }, {});
193
205
 
194
- const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(',')
206
+ const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(
207
+ ',',
208
+ )
195
209
  .filter(Boolean)
196
210
  .reduce<Record<string, string>>((acc, pair) => {
197
211
  const [key, value] = pair.split('=').map((s) => s.trim());
@@ -201,11 +215,36 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
201
215
  return acc;
202
216
  }, {});
203
217
 
218
+ const agentGateKeeperPreferredProviders = parsePreferredList(
219
+ process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_PROVIDERS,
220
+ );
221
+ const agentGateKeeperPreferredModels = parsePreferredList(
222
+ process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_MODELS,
223
+ );
224
+ const embeddingPreferredProviders = parsePreferredList(
225
+ process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_PROVIDERS,
226
+ );
227
+ const embeddingPreferredModels = parsePreferredList(
228
+ process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_MODELS,
229
+ );
230
+ const agentLayerExtractorPreferredProviders = parsePreferredList(
231
+ process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_PROVIDERS,
232
+ );
233
+ const agentLayerExtractorPreferredModels = parsePreferredList(
234
+ process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_MODELS,
235
+ );
236
+
204
237
  return {
205
238
  agentGateKeeper,
239
+ agentGateKeeperPreferredModels,
240
+ agentGateKeeperPreferredProviders,
206
241
  agentLayerExtractor,
242
+ agentLayerExtractorPreferredModels,
243
+ agentLayerExtractorPreferredProviders,
207
244
  concurrency,
208
245
  embedding,
246
+ embeddingPreferredModels,
247
+ embeddingPreferredProviders,
209
248
  featureFlags,
210
249
  observabilityS3: extractorObservabilityS3,
211
250
  upstashWorkflowExtraHeaders,
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { LayersEnum, MemorySourceType } from '@/types/userMemory';
4
+
5
+ import {
6
+ type MemoryExtractionNormalizedPayload,
7
+ type MemoryExtractionPayloadInput,
8
+ buildWorkflowPayloadInput,
9
+ normalizeMemoryExtractionPayload,
10
+ } from '../extract';
11
+
12
+ describe('normalizeMemoryExtractionPayload', () => {
13
+ it('normalizes sources, layers, ids, and dates with fallback baseUrl', () => {
14
+ const fromDate = new Date('2024-01-01T00:00:00Z');
15
+ const toDate = new Date('2024-02-01T00:00:00Z');
16
+
17
+ const payload: MemoryExtractionPayloadInput = {
18
+ forceAll: true,
19
+ forceTopics: true,
20
+ fromDate,
21
+ identityCursor: 3,
22
+ layers: [LayersEnum.Context, LayersEnum.Identity, LayersEnum.Context],
23
+ mode: 'direct',
24
+ sourceIds: ['source-1', 'source-1', ''],
25
+ sources: ['chatTopics', 'benchmark_locomo', 'unknown'],
26
+ toDate,
27
+ topicIds: ['topic-1', 'topic-1', ''],
28
+ userId: 'user-a',
29
+ userIds: ['user-a', 'user-b', ''],
30
+ };
31
+
32
+ const normalized = normalizeMemoryExtractionPayload(payload, 'https://api.example.com');
33
+
34
+ expect(normalized.baseUrl).toBe('https://api.example.com');
35
+ expect(normalized.forceAll).toBe(true);
36
+ expect(normalized.forceTopics).toBe(true);
37
+ expect(normalized.from).toEqual(fromDate);
38
+ expect(normalized.to).toEqual(toDate);
39
+ expect(normalized.identityCursor).toBe(3);
40
+ expect(normalized.layers).toEqual([LayersEnum.Context, LayersEnum.Identity]);
41
+ expect(normalized.sources).toEqual([
42
+ MemorySourceType.ChatTopic,
43
+ MemorySourceType.BenchmarkLocomo,
44
+ ]);
45
+ expect(normalized.sourceIds).toEqual(['source-1']);
46
+ expect(normalized.topicIds).toEqual(['topic-1']);
47
+ expect(normalized.userId).toBe('user-a');
48
+ expect(normalized.userIds).toEqual(['user-a', 'user-b']);
49
+ });
50
+
51
+ it('throws when baseUrl is missing in both payload and fallback', () => {
52
+ const payload: MemoryExtractionPayloadInput = {
53
+ forceAll: false,
54
+ forceTopics: false,
55
+ userIds: [],
56
+ };
57
+
58
+ expect(() => normalizeMemoryExtractionPayload(payload)).toThrow('Missing baseUrl');
59
+ });
60
+ });
61
+
62
+ describe('buildWorkflowPayloadInput', () => {
63
+ const baseNormalized: MemoryExtractionNormalizedPayload = {
64
+ baseUrl: 'https://api.example.com',
65
+ forceAll: false,
66
+ forceTopics: false,
67
+ from: undefined,
68
+ identityCursor: 0,
69
+ layers: [],
70
+ mode: 'workflow',
71
+ sourceIds: [],
72
+ sources: [MemorySourceType.ChatTopic],
73
+ to: undefined,
74
+ topicCursor: undefined,
75
+ topicIds: [],
76
+ userCursor: undefined,
77
+ userId: undefined,
78
+ userIds: ['user-x', 'user-y'],
79
+ };
80
+
81
+ it('falls back to the first user id when userId is missing', () => {
82
+ const payload = buildWorkflowPayloadInput(baseNormalized);
83
+
84
+ expect(payload.userId).toBe('user-x');
85
+ expect(payload.userIds).toEqual(['user-x', 'user-y']);
86
+ expect(payload.baseUrl).toBe('https://api.example.com');
87
+ expect(payload.mode).toBe('workflow');
88
+ });
89
+
90
+ it('preserves explicit userId when provided', () => {
91
+ const normalized: MemoryExtractionNormalizedPayload = {
92
+ ...baseNormalized,
93
+ userId: 'user-z',
94
+ };
95
+
96
+ const payload = buildWorkflowPayloadInput(normalized);
97
+
98
+ expect(payload.userId).toBe('user-z');
99
+ expect(payload.userIds).toEqual(['user-x', 'user-y']);
100
+ });
101
+ });
@@ -0,0 +1,111 @@
1
+ import type { AiProviderRuntimeState } from '@lobechat/types';
2
+ import type { EnabledAiModel } from 'model-bank';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import type { MemoryExtractionPrivateConfig } from '@/server/globalConfig/parseMemoryExtractionConfig';
6
+
7
+ import { MemoryExtractionExecutor } from '../extract';
8
+
9
+ const createRuntimeState = (models: EnabledAiModel[], keyVaults: Record<string, any>) =>
10
+ ({
11
+ enabledAiModels: models,
12
+ enabledAiProviders: [],
13
+ enabledChatAiProviders: [],
14
+ enabledImageAiProviders: [],
15
+ runtimeConfig: Object.fromEntries(
16
+ Object.entries(keyVaults).map(([providerId, vault]) => [
17
+ providerId,
18
+ { config: {}, keyVaults: vault, settings: {} },
19
+ ]),
20
+ ),
21
+ }) as AiProviderRuntimeState;
22
+
23
+ const createExecutor = (privateOverrides?: Partial<MemoryExtractionPrivateConfig>) => {
24
+ const basePrivateConfig: MemoryExtractionPrivateConfig = {
25
+ agentGateKeeper: { model: 'gate-2', provider: 'provider-b' },
26
+ agentLayerExtractor: {
27
+ contextLimit: 2048,
28
+ layers: {
29
+ context: 'layer-ctx',
30
+ experience: 'layer-exp',
31
+ identity: 'layer-id',
32
+ preference: 'layer-pref',
33
+ },
34
+ model: 'layer-1',
35
+ provider: 'provider-l',
36
+ },
37
+ concurrency: 1,
38
+ embedding: { model: 'embed-1', provider: 'provider-e' },
39
+ featureFlags: { enableBenchmarkLoCoMo: false },
40
+ observabilityS3: { enabled: false },
41
+ };
42
+
43
+ const serverConfig = {
44
+ aiProvider: {},
45
+ memory: {},
46
+ };
47
+
48
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
49
+ // @ts-ignore accessing private constructor for testing
50
+ return new MemoryExtractionExecutor(serverConfig as any, {
51
+ ...basePrivateConfig,
52
+ ...privateOverrides,
53
+ });
54
+ };
55
+
56
+ describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
57
+ it('prefers configured providers/models for gatekeeper, embedding, and layer extractors', () => {
58
+ const executor = createExecutor({
59
+ embeddingPreferredProviders: ['provider-e'],
60
+ agentGateKeeperPreferredModels: ['gate-1'],
61
+ agentGateKeeperPreferredProviders: ['provider-a', 'provider-b'],
62
+ agentLayerExtractorPreferredProviders: ['provider-l'],
63
+ });
64
+
65
+ const runtimeState = createRuntimeState(
66
+ [
67
+ { abilities: {}, id: 'gate-1', providerId: 'provider-a', type: 'chat' },
68
+ { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
69
+ { abilities: {}, id: 'embed-1', providerId: 'provider-e', type: 'embedding' },
70
+ { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
71
+ { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
72
+ { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
73
+ { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
74
+ ],
75
+ {
76
+ 'provider-a': { apiKey: 'a-key' },
77
+ 'provider-b': { apiKey: 'b-key' },
78
+ 'provider-e': { apiKey: 'e-key' },
79
+ 'provider-l': { apiKey: 'l-key' },
80
+ },
81
+ );
82
+
83
+ const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
84
+
85
+ expect(keyVaults).toMatchObject({
86
+ 'provider-a': { apiKey: 'a-key' }, // gatekeeper picked preferred provider/model
87
+ 'provider-e': { apiKey: 'e-key' }, // embedding honored preferred provider
88
+ 'provider-l': { apiKey: 'l-key' }, // layer extractor models resolved
89
+ });
90
+ });
91
+
92
+ it('throws when no provider can satisfy an embedding model', () => {
93
+ const executor = createExecutor();
94
+
95
+ const runtimeState = createRuntimeState(
96
+ [
97
+ { abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
98
+ { abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
99
+ { abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
100
+ { abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
101
+ { abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
102
+ ],
103
+ {
104
+ 'provider-b': { apiKey: 'b-key' },
105
+ 'provider-l': { apiKey: 'l-key' },
106
+ },
107
+ );
108
+
109
+ expect(() => (executor as any).resolveRuntimeKeyVaults(runtimeState)).toThrow(/embedding/i);
110
+ });
111
+ });
@@ -38,6 +38,7 @@ import {
38
38
  } from '@lobechat/observability-otel/modules/memory-user-memory';
39
39
  import { attributesCommon } from '@lobechat/observability-otel/node';
40
40
  import type {
41
+ AiProviderRuntimeState,
41
42
  IdentityMemoryDetail,
42
43
  MemoryExtractionAgentCallTrace,
43
44
  MemoryExtractionTraceError,
@@ -55,6 +56,7 @@ import type { ListUsersForMemoryExtractorCursor } from '@/database/models/user';
55
56
  import { UserModel } from '@/database/models/user';
56
57
  import { UserMemoryModel } from '@/database/models/userMemory';
57
58
  import { UserMemorySourceBenchmarkLoCoMoModel } from '@/database/models/userMemory/sources/benchmarkLoCoMo';
59
+ import { AiInfraRepos } from '@/database/repositories/aiInfra';
58
60
  import { getServerDB } from '@/database/server';
59
61
  import { getServerGlobalConfig } from '@/server/globalConfig';
60
62
  import {
@@ -64,7 +66,7 @@ import {
64
66
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
65
67
  import { S3 } from '@/server/modules/S3';
66
68
  import type { GlobalMemoryLayer } from '@/types/serverConfig';
67
- import type { UserKeyVaults } from '@/types/user/settings';
69
+ import type { ProviderConfig } from '@/types/user/settings';
68
70
  import {
69
71
  LayersEnum,
70
72
  MemorySourceType,
@@ -212,6 +214,11 @@ export interface TopicBatchWorkflowPayload extends MemoryExtractionPayloadInput
212
214
  userId: string;
213
215
  }
214
216
 
217
+ type ProviderKeyVaultMap = Record<
218
+ string,
219
+ AiProviderRuntimeState['runtimeConfig'][string]['keyVaults'] | undefined
220
+ >;
221
+
215
222
  export const buildWorkflowPayloadInput = (
216
223
  payload: MemoryExtractionNormalizedPayload,
217
224
  ): MemoryExtractionPayloadInput => ({
@@ -232,11 +239,9 @@ export const buildWorkflowPayloadInput = (
232
239
  userIds: payload.userIds,
233
240
  });
234
241
 
235
- const normalizeProvider = (provider: string) => provider.toLowerCase() as keyof UserKeyVaults;
236
-
237
- const extractCredentialsFromVault = (provider: string, keyVaults?: UserKeyVaults) => {
238
- const vault = keyVaults?.[normalizeProvider(provider)];
242
+ const normalizeProvider = (provider: string) => provider.toLowerCase();
239
243
 
244
+ const extractCredentialsFromVault = (vault?: Record<string, unknown>) => {
240
245
  if (!vault || typeof vault !== 'object') return {};
241
246
 
242
247
  const apiKey = 'apiKey' in vault && typeof vault.apiKey === 'string' ? vault.apiKey : undefined;
@@ -275,11 +280,10 @@ const maskSecret = (value?: string) => {
275
280
  return `${value.slice(0, 6)}***${value.slice(-4)}`;
276
281
  };
277
282
 
278
- const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?: UserKeyVaults) => {
283
+ const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
279
284
  const provider = agent.provider || 'openai';
280
285
  const { apiKey: userApiKey, baseURL: userBaseURL } = extractCredentialsFromVault(
281
- provider,
282
- keyVaults,
286
+ keyVaults?.[normalizeProvider(provider)],
283
287
  );
284
288
 
285
289
  // Only use the user baseURL if we are also using their API key; otherwise fall back entirely
@@ -309,7 +313,7 @@ const debugRuntimeInit = (
309
313
  });
310
314
  };
311
315
 
312
- const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: UserKeyVaults) => {
316
+ const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
313
317
  const resolved = resolveRuntimeAgentConfig(agent, keyVaults);
314
318
  debugRuntimeInit(agent, resolved);
315
319
 
@@ -366,6 +370,13 @@ type MemoryExtractionConfig = ReturnType<typeof parseMemoryExtractionConfig>;
366
370
  type ServerConfig = Awaited<ReturnType<typeof getServerGlobalConfig>>;
367
371
 
368
372
  export class MemoryExtractionExecutor {
373
+ private readonly aiProviderConfig: Record<string, ProviderConfig>;
374
+ private readonly embeddingPreferredModels?: string[];
375
+ private readonly embeddingPreferredProviders?: string[];
376
+ private readonly gatekeeperPreferredModels?: string[];
377
+ private readonly gatekeeperPreferredProviders?: string[];
378
+ private readonly layerPreferredModels?: string[];
379
+ private readonly layerPreferredProviders?: string[];
369
380
  private readonly privateConfig: MemoryExtractionConfig;
370
381
  private readonly modelConfig: {
371
382
  embeddingsModel: string;
@@ -380,6 +391,13 @@ export class MemoryExtractionExecutor {
380
391
 
381
392
  private constructor(serverConfig: ServerConfig, privateConfig: MemoryExtractionConfig) {
382
393
  this.privateConfig = privateConfig;
394
+ this.aiProviderConfig = (serverConfig.aiProvider || {}) as Record<string, ProviderConfig>;
395
+ this.embeddingPreferredProviders = privateConfig.embeddingPreferredProviders;
396
+ this.embeddingPreferredModels = privateConfig.embeddingPreferredModels;
397
+ this.gatekeeperPreferredProviders = privateConfig.agentGateKeeperPreferredProviders;
398
+ this.gatekeeperPreferredModels = privateConfig.agentGateKeeperPreferredModels;
399
+ this.layerPreferredProviders = privateConfig.agentLayerExtractorPreferredProviders;
400
+ this.layerPreferredModels = privateConfig.agentLayerExtractorPreferredModels;
383
401
 
384
402
  const publicMemoryConfig = serverConfig.memory?.userMemory;
385
403
 
@@ -1014,8 +1032,11 @@ export class MemoryExtractionExecutor {
1014
1032
  };
1015
1033
 
1016
1034
  const userModel = new UserModel(db, job.userId);
1017
- const userState = await userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
1018
- const keyVaults = userState.settings?.keyVaults as UserKeyVaults | undefined;
1035
+ const [userState, aiProviderRuntimeState] = await Promise.all([
1036
+ userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
1037
+ this.getAiProviderRuntimeState(job.userId),
1038
+ ]);
1039
+ const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
1019
1040
  const language = userState.settings?.general?.responseLanguage;
1020
1041
 
1021
1042
  const runtimes = await this.getRuntime(job.userId, keyVaults);
@@ -1625,7 +1646,112 @@ export class MemoryExtractionExecutor {
1625
1646
  };
1626
1647
  }
1627
1648
 
1628
- private async getRuntime(userId: string, keyVaults?: UserKeyVaults): Promise<RuntimeBundle> {
1649
+ private async getAiProviderRuntimeState(userId: string): Promise<AiProviderRuntimeState> {
1650
+ const db = await this.db;
1651
+ const aiInfraRepos = new AiInfraRepos(db, userId, this.aiProviderConfig);
1652
+
1653
+ return aiInfraRepos.getAiProviderRuntimeState(KeyVaultsGateKeeper.getUserKeyVaults);
1654
+ }
1655
+
1656
+ private resolveRuntimeKeyVaults(runtimeState: AiProviderRuntimeState): ProviderKeyVaultMap {
1657
+ const normalizedRuntimeConfig = Object.fromEntries(
1658
+ Object.entries(runtimeState.runtimeConfig || {}).map(([providerId, config]) => [
1659
+ normalizeProvider(providerId),
1660
+ config,
1661
+ ]),
1662
+ );
1663
+ const providerModels = runtimeState.enabledAiModels.reduce<Record<string, Set<string>>>(
1664
+ (acc, model) => {
1665
+ const providerId = normalizeProvider(model.providerId);
1666
+ acc[providerId] = acc[providerId] || new Set<string>();
1667
+ acc[providerId].add(model.id);
1668
+ return acc;
1669
+ },
1670
+ {},
1671
+ );
1672
+
1673
+ const resolveProviderForModel = (
1674
+ modelId: string,
1675
+ fallbackProvider?: string,
1676
+ preferredProviders?: string[],
1677
+ preferredModels?: string[],
1678
+ label?: string,
1679
+ ) => {
1680
+ const providerOrder = Array.from(
1681
+ new Set(
1682
+ [
1683
+ ...(preferredProviders?.map(normalizeProvider) || []),
1684
+ fallbackProvider ? normalizeProvider(fallbackProvider) : undefined,
1685
+ ...Object.keys(providerModels),
1686
+ ].filter(Boolean) as string[],
1687
+ ),
1688
+ );
1689
+
1690
+ const candidateModels = preferredModels && preferredModels.length > 0 ? preferredModels : [];
1691
+
1692
+ for (const providerId of providerOrder) {
1693
+ const models = providerModels[providerId];
1694
+ if (!models) continue;
1695
+
1696
+ if (models.has(modelId)) return providerId;
1697
+
1698
+ const preferredMatch = candidateModels.find((preferredModel) => models.has(preferredModel));
1699
+ if (preferredMatch) return providerId;
1700
+ }
1701
+
1702
+ throw new Error(
1703
+ `Unable to resolve provider for ${label || 'model'} "${modelId}". Check preferred providers/models configuration.`,
1704
+ );
1705
+ };
1706
+
1707
+ const keyVaults: ProviderKeyVaultMap = {};
1708
+
1709
+ const gatekeeperProvider = resolveProviderForModel(
1710
+ this.modelConfig.gateModel,
1711
+ this.privateConfig.agentGateKeeper.provider,
1712
+ this.gatekeeperPreferredProviders,
1713
+ this.gatekeeperPreferredModels,
1714
+ 'gatekeeper',
1715
+ );
1716
+ const gatekeeperRuntime = normalizedRuntimeConfig[gatekeeperProvider];
1717
+ if (gatekeeperRuntime?.keyVaults) {
1718
+ keyVaults[gatekeeperProvider] = gatekeeperRuntime.keyVaults;
1719
+ }
1720
+
1721
+ const embeddingProvider = resolveProviderForModel(
1722
+ this.modelConfig.embeddingsModel,
1723
+ this.privateConfig.embedding.provider,
1724
+ this.embeddingPreferredProviders,
1725
+ this.embeddingPreferredModels,
1726
+ 'embedding',
1727
+ );
1728
+ const embeddingRuntime = normalizedRuntimeConfig[embeddingProvider];
1729
+ if (embeddingRuntime?.keyVaults) {
1730
+ keyVaults[embeddingProvider] = embeddingRuntime.keyVaults;
1731
+ }
1732
+
1733
+ Object.values(this.modelConfig.layerModels).forEach((model) => {
1734
+ if (!model) return;
1735
+ const providerId = resolveProviderForModel(
1736
+ model,
1737
+ this.privateConfig.agentLayerExtractor.provider,
1738
+ this.layerPreferredProviders,
1739
+ this.layerPreferredModels,
1740
+ 'layer extractor',
1741
+ );
1742
+ const runtime = normalizedRuntimeConfig[providerId];
1743
+ if (runtime?.keyVaults) {
1744
+ keyVaults[providerId] = runtime.keyVaults;
1745
+ }
1746
+ });
1747
+
1748
+ return keyVaults;
1749
+ }
1750
+
1751
+ private async getRuntime(
1752
+ userId: string,
1753
+ keyVaults?: ProviderKeyVaultMap,
1754
+ ): Promise<RuntimeBundle> {
1629
1755
  // TODO: implement a better cache eviction strategy
1630
1756
  // TODO: make cache size configurable
1631
1757
  if (this.runtimeCache.keys.length > 200) {
@@ -1673,8 +1799,11 @@ export class MemoryExtractionExecutor {
1673
1799
  try {
1674
1800
  const db = await this.db;
1675
1801
  const userModel = new UserModel(db, params.userId);
1676
- const userState = await userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
1677
- const keyVaults = userState.settings?.keyVaults as UserKeyVaults | undefined;
1802
+ const [userState, aiProviderRuntimeState] = await Promise.all([
1803
+ userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
1804
+ this.getAiProviderRuntimeState(params.userId),
1805
+ ]);
1806
+ const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
1678
1807
  const language = params.language || userState.settings?.general?.responseLanguage;
1679
1808
 
1680
1809
  const runtimes = await this.getRuntime(params.userId, keyVaults);
@@ -1849,7 +1978,10 @@ export class MemoryExtractionWorkflowService {
1849
1978
  return this.client;
1850
1979
  }
1851
1980
 
1852
- static triggerProcessUsers(payload: MemoryExtractionPayloadInput, options?: { extraHeaders?: Record<string, string> }) {
1981
+ static triggerProcessUsers(
1982
+ payload: MemoryExtractionPayloadInput,
1983
+ options?: { extraHeaders?: Record<string, string> },
1984
+ ) {
1853
1985
  if (!payload.baseUrl) {
1854
1986
  throw new Error('Missing baseUrl for workflow trigger');
1855
1987
  }
@@ -1858,7 +1990,10 @@ export class MemoryExtractionWorkflowService {
1858
1990
  return this.getClient().trigger({ body: payload, headers: options?.extraHeaders, url });
1859
1991
  }
1860
1992
 
1861
- static triggerProcessUserTopics(payload: UserTopicWorkflowPayload, options?: { extraHeaders?: Record<string, string> }) {
1993
+ static triggerProcessUserTopics(
1994
+ payload: UserTopicWorkflowPayload,
1995
+ options?: { extraHeaders?: Record<string, string> },
1996
+ ) {
1862
1997
  if (!payload.baseUrl) {
1863
1998
  throw new Error('Missing baseUrl for workflow trigger');
1864
1999
  }
@@ -1867,7 +2002,10 @@ export class MemoryExtractionWorkflowService {
1867
2002
  return this.getClient().trigger({ body: payload, headers: options?.extraHeaders, url });
1868
2003
  }
1869
2004
 
1870
- static triggerProcessTopics(payload: MemoryExtractionPayloadInput, options?: { extraHeaders?: Record<string, string> }) {
2005
+ static triggerProcessTopics(
2006
+ payload: MemoryExtractionPayloadInput,
2007
+ options?: { extraHeaders?: Record<string, string> },
2008
+ ) {
1871
2009
  if (!payload.baseUrl) {
1872
2010
  throw new Error('Missing baseUrl for workflow trigger');
1873
2011
  }