@remnic/core 9.3.562 → 9.3.564

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 (156) hide show
  1. package/dist/access-cli.js +40 -39
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.js +16 -16
  4. package/dist/access-mcp.js +13 -13
  5. package/dist/access-schema.js +3 -3
  6. package/dist/access-service.js +11 -11
  7. package/dist/active-recall.js +1 -1
  8. package/dist/adapters/index.js +4 -4
  9. package/dist/adapters/registry.js +2 -2
  10. package/dist/briefing.js +4 -4
  11. package/dist/causal-consolidation.js +5 -5
  12. package/dist/{chunk-I2K6KCVC.js → chunk-2FHLI4U6.js} +49 -49
  13. package/dist/chunk-3ONXXHQO.js +57 -0
  14. package/dist/chunk-3ONXXHQO.js.map +1 -0
  15. package/dist/{chunk-5GX5MUQ2.js → chunk-574MU2Y3.js} +3 -3
  16. package/dist/{chunk-65OLPXBU.js → chunk-5WB4C7KM.js} +6 -6
  17. package/dist/chunk-6PTSXBPE.js +483 -0
  18. package/dist/chunk-6PTSXBPE.js.map +1 -0
  19. package/dist/{chunk-Z56KAZQL.js → chunk-74VA26CT.js} +2 -2
  20. package/dist/{chunk-CC2ESOOG.js → chunk-7X7TBJRX.js} +2 -2
  21. package/dist/{chunk-O4M4WH6V.js → chunk-ARY5OOLG.js} +2 -2
  22. package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
  23. package/dist/{chunk-PM3QHTFT.js → chunk-CF3ZF2YU.js} +3 -3
  24. package/dist/{chunk-SI3QCHWF.js → chunk-DARLGSFX.js} +5 -5
  25. package/dist/chunk-EWLQPEO6.js +308 -0
  26. package/dist/chunk-EWLQPEO6.js.map +1 -0
  27. package/dist/{chunk-FVCZINOF.js → chunk-FHBEL473.js} +2 -2
  28. package/dist/{chunk-7Q3RCKAQ.js → chunk-FXKPZ3H6.js} +2 -2
  29. package/dist/{chunk-5WLYNZPC.js → chunk-GBXGCFRH.js} +2 -2
  30. package/dist/{chunk-ILJXM3FV.js → chunk-HQO5EBUC.js} +10 -10
  31. package/dist/{chunk-FK556DDH.js → chunk-I4UNL747.js} +4 -4
  32. package/dist/{chunk-RLPIT4YI.js → chunk-IOTTZLFF.js} +38 -38
  33. package/dist/{chunk-TVZ6LKKS.js → chunk-IRFF6LSF.js} +8 -8
  34. package/dist/{chunk-M5T4Q2ZU.js → chunk-KGK2QKWL.js} +1 -1
  35. package/dist/chunk-KGK2QKWL.js.map +1 -0
  36. package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
  37. package/dist/chunk-M46RYSMW.js +597 -0
  38. package/dist/chunk-M46RYSMW.js.map +1 -0
  39. package/dist/{chunk-KXULCVOC.js → chunk-M6I5Z4SR.js} +4 -2
  40. package/dist/chunk-M6I5Z4SR.js.map +1 -0
  41. package/dist/{chunk-JFN6K74Q.js → chunk-MQEIWDYW.js} +2 -2
  42. package/dist/{chunk-7H6CFEBJ.js → chunk-NZPF2SYV.js} +8 -1
  43. package/dist/{chunk-7H6CFEBJ.js.map → chunk-NZPF2SYV.js.map} +1 -1
  44. package/dist/{chunk-SML26KED.js → chunk-OB6353F7.js} +16 -12
  45. package/dist/chunk-OB6353F7.js.map +1 -0
  46. package/dist/{chunk-SOTR74FK.js → chunk-OPYFD6PD.js} +2 -2
  47. package/dist/{chunk-3C5RPJAX.js → chunk-OXJBNGBK.js} +2 -2
  48. package/dist/{chunk-BD5LHQWD.js → chunk-PPPZY2EU.js} +2 -2
  49. package/dist/{chunk-25BY3HHZ.js → chunk-SUTSSOYU.js} +2 -2
  50. package/dist/{chunk-KS7WO6EQ.js → chunk-VFB2G5YL.js} +20 -20
  51. package/dist/{chunk-BUUYY2H2.js → chunk-WP5OWVLZ.js} +4 -4
  52. package/dist/{chunk-6URPAY2D.js → chunk-XCAZF7KQ.js} +207 -53
  53. package/dist/chunk-XCAZF7KQ.js.map +1 -0
  54. package/dist/{chunk-S53PAX2V.js → chunk-XM7BYXT7.js} +2 -2
  55. package/dist/{chunk-FADZBOR4.js → chunk-XRWTAEZM.js} +2 -2
  56. package/dist/{chunk-E5OECWZ5.js → chunk-XT7XVA53.js} +2 -2
  57. package/dist/{chunk-R3PS27B4.js → chunk-Z4R6RI2N.js} +2 -2
  58. package/dist/cli.js +44 -43
  59. package/dist/compounding/engine.js +4 -4
  60. package/dist/config.js +1 -1
  61. package/dist/connectors/codex-materialize-runner.js +4 -4
  62. package/dist/connectors/index.js +4 -4
  63. package/dist/embedding-fallback.d.ts +12 -1
  64. package/dist/embedding-fallback.js +4 -1
  65. package/dist/entity-retrieval.js +4 -4
  66. package/dist/host-embedding-provider.d.ts +21 -0
  67. package/dist/host-embedding-provider.js +14 -0
  68. package/dist/host-embedding-provider.js.map +1 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +71 -63
  71. package/dist/index.js.map +1 -1
  72. package/dist/lcm/index.js +3 -3
  73. package/dist/maintenance/memory-governance.js +4 -4
  74. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  75. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  76. package/dist/namespaces/migrate.js +14 -13
  77. package/dist/namespaces/search.js +9 -8
  78. package/dist/namespaces/storage.js +4 -4
  79. package/dist/operator-toolkit.js +17 -16
  80. package/dist/orchestrator.js +32 -31
  81. package/dist/recall-explain-renderer.js +3 -3
  82. package/dist/recall-xray-cli.js +4 -4
  83. package/dist/recall-xray-renderer.js +3 -3
  84. package/dist/recall-xray.js +2 -2
  85. package/dist/resume-bundles.js +2 -2
  86. package/dist/search/embed-helper.d.ts +48 -4
  87. package/dist/search/embed-helper.js +2 -1
  88. package/dist/search/factory.js +8 -7
  89. package/dist/search/index.d.ts +1 -0
  90. package/dist/search/index.js +12 -11
  91. package/dist/search/lancedb-backend.d.ts +11 -0
  92. package/dist/search/lancedb-backend.js +2 -2
  93. package/dist/search/meilisearch-backend.js +2 -2
  94. package/dist/search/orama-backend.d.ts +16 -0
  95. package/dist/search/orama-backend.js +2 -2
  96. package/dist/semantic-consolidation.js +5 -5
  97. package/dist/semantic-rule-promotion.js +4 -4
  98. package/dist/semantic-rule-verifier.js +4 -4
  99. package/dist/storage.js +3 -3
  100. package/dist/transfer/autodetect.js +1 -1
  101. package/dist/transfer/backup.js +1 -1
  102. package/dist/transfer/capsule-export.js +2 -2
  103. package/dist/transfer/types.d.ts +6 -6
  104. package/dist/types.d.ts +17 -0
  105. package/dist/types.js +1 -1
  106. package/dist/verified-recall.js +4 -4
  107. package/package.json +11 -1
  108. package/src/config.ts +18 -0
  109. package/src/embedding-fallback.ts +293 -61
  110. package/src/host-embedding-provider.ts +84 -0
  111. package/src/index.ts +7 -0
  112. package/src/namespaces/search.ts +9 -1
  113. package/src/qmd.test.ts +28 -0
  114. package/src/search/embed-helper.ts +319 -51
  115. package/src/search/factory.ts +6 -2
  116. package/src/search/lancedb-backend.ts +297 -41
  117. package/src/search/orama-backend.ts +418 -47
  118. package/src/types.ts +17 -0
  119. package/dist/chunk-6URPAY2D.js.map +0 -1
  120. package/dist/chunk-FUC4LZMD.js +0 -301
  121. package/dist/chunk-FUC4LZMD.js.map +0 -1
  122. package/dist/chunk-KXULCVOC.js.map +0 -1
  123. package/dist/chunk-M5T4Q2ZU.js.map +0 -1
  124. package/dist/chunk-ONPLNAPX.js +0 -133
  125. package/dist/chunk-ONPLNAPX.js.map +0 -1
  126. package/dist/chunk-QVJ4NWL2.js +0 -335
  127. package/dist/chunk-QVJ4NWL2.js.map +0 -1
  128. package/dist/chunk-SML26KED.js.map +0 -1
  129. /package/dist/{chunk-I2K6KCVC.js.map → chunk-2FHLI4U6.js.map} +0 -0
  130. /package/dist/{chunk-5GX5MUQ2.js.map → chunk-574MU2Y3.js.map} +0 -0
  131. /package/dist/{chunk-65OLPXBU.js.map → chunk-5WB4C7KM.js.map} +0 -0
  132. /package/dist/{chunk-Z56KAZQL.js.map → chunk-74VA26CT.js.map} +0 -0
  133. /package/dist/{chunk-CC2ESOOG.js.map → chunk-7X7TBJRX.js.map} +0 -0
  134. /package/dist/{chunk-O4M4WH6V.js.map → chunk-ARY5OOLG.js.map} +0 -0
  135. /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
  136. /package/dist/{chunk-PM3QHTFT.js.map → chunk-CF3ZF2YU.js.map} +0 -0
  137. /package/dist/{chunk-SI3QCHWF.js.map → chunk-DARLGSFX.js.map} +0 -0
  138. /package/dist/{chunk-FVCZINOF.js.map → chunk-FHBEL473.js.map} +0 -0
  139. /package/dist/{chunk-7Q3RCKAQ.js.map → chunk-FXKPZ3H6.js.map} +0 -0
  140. /package/dist/{chunk-5WLYNZPC.js.map → chunk-GBXGCFRH.js.map} +0 -0
  141. /package/dist/{chunk-ILJXM3FV.js.map → chunk-HQO5EBUC.js.map} +0 -0
  142. /package/dist/{chunk-FK556DDH.js.map → chunk-I4UNL747.js.map} +0 -0
  143. /package/dist/{chunk-RLPIT4YI.js.map → chunk-IOTTZLFF.js.map} +0 -0
  144. /package/dist/{chunk-TVZ6LKKS.js.map → chunk-IRFF6LSF.js.map} +0 -0
  145. /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
  146. /package/dist/{chunk-JFN6K74Q.js.map → chunk-MQEIWDYW.js.map} +0 -0
  147. /package/dist/{chunk-SOTR74FK.js.map → chunk-OPYFD6PD.js.map} +0 -0
  148. /package/dist/{chunk-3C5RPJAX.js.map → chunk-OXJBNGBK.js.map} +0 -0
  149. /package/dist/{chunk-BD5LHQWD.js.map → chunk-PPPZY2EU.js.map} +0 -0
  150. /package/dist/{chunk-25BY3HHZ.js.map → chunk-SUTSSOYU.js.map} +0 -0
  151. /package/dist/{chunk-KS7WO6EQ.js.map → chunk-VFB2G5YL.js.map} +0 -0
  152. /package/dist/{chunk-BUUYY2H2.js.map → chunk-WP5OWVLZ.js.map} +0 -0
  153. /package/dist/{chunk-S53PAX2V.js.map → chunk-XM7BYXT7.js.map} +0 -0
  154. /package/dist/{chunk-FADZBOR4.js.map → chunk-XRWTAEZM.js.map} +0 -0
  155. /package/dist/{chunk-E5OECWZ5.js.map → chunk-XT7XVA53.js.map} +0 -0
  156. /package/dist/{chunk-R3PS27B4.js.map → chunk-Z4R6RI2N.js.map} +0 -0
@@ -2,15 +2,48 @@ import { log } from "../logger.js";
2
2
  import type { PluginConfig } from "../types.js";
3
3
  import { isAbortError } from "../abort-error.js";
4
4
  import { withTimeoutSignal } from "./abort.js";
5
+ import {
6
+ getHostEmbeddingProvider,
7
+ type HostEmbeddingProvider,
8
+ type HostEmbeddingInputType,
9
+ normalizeHostEmbeddingVector,
10
+ } from "../host-embedding-provider.js";
5
11
 
6
12
  type ProviderConfig = {
7
- type: "openai" | "local";
13
+ type: "openai" | "local" | "host";
8
14
  model: string;
9
- endpoint: string;
10
- headers: Record<string, string>;
15
+ endpoint?: string;
16
+ headers?: Record<string, string>;
17
+ hostProvider?: HostEmbeddingProvider;
18
+ };
19
+
20
+ type HostEmbeddingScopeConfig = PluginConfig & {
21
+ /**
22
+ * Internal namespace-router metadata. Host adapters register providers at
23
+ * the root memoryDir while namespace backends operate under scoped dirs.
24
+ */
25
+ hostEmbeddingProviderScope?: string;
26
+ };
27
+
28
+ type EmbedHelperOptions = {
29
+ /** Backend-local vector schema dimension for host-provider validation. */
30
+ hostEmbeddingExpectedDimension?: number;
31
+ };
32
+
33
+ export type EmbedProviderIdentity = `${ProviderConfig["type"]}:${string}`;
34
+
35
+ export type EmbedWithProviderResult = {
36
+ vector: number[];
37
+ providerIdentity: EmbedProviderIdentity;
38
+ };
39
+
40
+ export type EmbedBatchWithProviderResult = {
41
+ vectors: (number[] | null)[];
42
+ providerIdentity: EmbedProviderIdentity;
11
43
  };
12
44
 
13
45
  const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
46
+ const DEFAULT_PROVIDER_CACHE_TTL_MS = 250;
14
47
 
15
48
  /**
16
49
  * Standalone embedding helper for search backend adapters.
@@ -22,28 +55,70 @@ const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
22
55
  * Merging them would break the port/adapter separation between search and plugin layers.
23
56
  */
24
57
  export class EmbedHelper {
25
- private provider: ProviderConfig | null | undefined; // undefined = not yet resolved
58
+ private cachedProvider: ProviderConfig | null | undefined;
59
+ private cachedProviderAt = 0;
60
+ private providerCacheTtlMs = DEFAULT_PROVIDER_CACHE_TTL_MS;
26
61
 
27
- constructor(private readonly config: PluginConfig) {}
62
+ constructor(
63
+ private readonly config: PluginConfig,
64
+ private readonly options: EmbedHelperOptions = {},
65
+ ) {}
28
66
 
29
67
  /**
30
68
  * Whether an embedding provider is available.
31
- * Resolves the provider on first call.
69
+ * Re-resolves periodically so late host-provider registration is visible
70
+ * without repeatedly probing provider state on every hot-path call.
32
71
  */
33
72
  isAvailable(): boolean {
34
- if (this.provider === undefined) {
35
- this.provider = this.resolveProvider();
36
- }
37
- return this.provider !== null;
73
+ return this.getProvider() !== null;
38
74
  }
39
75
 
40
76
  /**
41
77
  * Embed a single text string. Returns null if no provider is available.
42
78
  */
43
79
  async embed(text: string, options: { signal?: AbortSignal } = {}): Promise<number[] | null> {
80
+ return (await this.embedWithProvider(text, options))?.vector ?? null;
81
+ }
82
+
83
+ async embedWithProvider(
84
+ text: string,
85
+ options: { signal?: AbortSignal } = {},
86
+ ): Promise<EmbedWithProviderResult | null> {
44
87
  const provider = this.getProvider();
45
88
  if (!provider) return null;
46
- return this.callEmbed(text, provider, options.signal);
89
+ const result = await this.callEmbed(text, provider, options.signal, "query");
90
+ if (result) {
91
+ return {
92
+ vector: result,
93
+ providerIdentity: providerIdentity(provider),
94
+ };
95
+ }
96
+ if (provider.type !== "host") return null;
97
+ const fallbackProvider = this.resolveProvider({ includeHost: false });
98
+ if (!fallbackProvider) return null;
99
+ const fallbackResult = await this.callEmbed(text, fallbackProvider, options.signal, "query");
100
+ return fallbackResult
101
+ ? {
102
+ vector: fallbackResult,
103
+ providerIdentity: providerIdentity(fallbackProvider),
104
+ }
105
+ : null;
106
+ }
107
+
108
+ async embedWithFallbackProviderIdentity(
109
+ text: string,
110
+ identity: EmbedProviderIdentity,
111
+ options: { signal?: AbortSignal } = {},
112
+ ): Promise<EmbedWithProviderResult | null> {
113
+ const provider = this.resolveFallbackProviderForIdentity(identity);
114
+ if (!provider) return null;
115
+ const result = await this.callEmbed(text, provider, options.signal, "query");
116
+ return result
117
+ ? {
118
+ vector: result,
119
+ providerIdentity: providerIdentity(provider),
120
+ }
121
+ : null;
47
122
  }
48
123
 
49
124
  /**
@@ -54,15 +129,65 @@ export class EmbedHelper {
54
129
  batchSize = 32,
55
130
  options: { signal?: AbortSignal } = {},
56
131
  ): Promise<(number[] | null)[]> {
132
+ return (await this.embedBatchWithProvider(texts, batchSize, options))?.vectors ?? texts.map(() => null);
133
+ }
134
+
135
+ async embedBatchWithProvider(
136
+ texts: string[],
137
+ batchSize = 32,
138
+ options: { signal?: AbortSignal } = {},
139
+ ): Promise<EmbedBatchWithProviderResult | null> {
57
140
  const provider = this.getProvider();
58
- if (!provider) return texts.map(() => null);
141
+ if (!provider) return null;
59
142
 
143
+ if (provider.type === "host") {
144
+ const hostResults = await this.embedAllWithProvider(texts, batchSize, provider, options);
145
+ if (!hostResults.some((result) => result === null)) {
146
+ return {
147
+ vectors: hostResults,
148
+ providerIdentity: providerIdentity(provider),
149
+ };
150
+ }
151
+ const fallbackProvider = this.resolveProvider({ includeHost: false });
152
+ if (!fallbackProvider) {
153
+ return {
154
+ vectors: hostResults,
155
+ providerIdentity: providerIdentity(provider),
156
+ };
157
+ }
158
+ const fallbackResults = await this.embedAllWithProvider(texts, batchSize, fallbackProvider, options);
159
+ return {
160
+ vectors: fallbackResults,
161
+ providerIdentity: providerIdentity(fallbackProvider),
162
+ };
163
+ }
164
+
165
+ return {
166
+ vectors: await this.embedAllWithProvider(texts, batchSize, provider, options),
167
+ providerIdentity: providerIdentity(provider),
168
+ };
169
+ }
170
+
171
+ getProviderIdentity(): EmbedProviderIdentity | null {
172
+ const provider = this.getProvider();
173
+ return provider ? providerIdentity(provider) : null;
174
+ }
175
+
176
+ private async embedAllWithProvider(
177
+ texts: string[],
178
+ batchSize: number,
179
+ provider: ProviderConfig,
180
+ options: { signal?: AbortSignal } = {},
181
+ ): Promise<(number[] | null)[]> {
60
182
  const results: (number[] | null)[] = new Array(texts.length).fill(null);
61
183
  for (let i = 0; i < texts.length; i += batchSize) {
62
184
  const batch = texts.slice(i, i + batchSize);
63
- const batchResults = await Promise.all(
64
- batch.map((t) => this.callEmbed(t, provider, options.signal)),
65
- );
185
+ const batchResults =
186
+ provider.type === "host" && provider.hostProvider?.embedBatch
187
+ ? await this.callHostEmbedBatch(batch, provider.hostProvider, options.signal)
188
+ : await Promise.all(
189
+ batch.map((t) => this.callEmbed(t, provider, options.signal, "document")),
190
+ );
66
191
  for (let j = 0; j < batchResults.length; j++) {
67
192
  results[i + j] = batchResults[j];
68
193
  }
@@ -71,62 +196,131 @@ export class EmbedHelper {
71
196
  }
72
197
 
73
198
  private getProvider(): ProviderConfig | null {
74
- if (this.provider === undefined) {
75
- this.provider = this.resolveProvider();
199
+ const now = Date.now();
200
+ if (
201
+ this.cachedProvider !== undefined &&
202
+ now - this.cachedProviderAt < this.providerCacheTtlMs
203
+ ) {
204
+ return this.cachedProvider;
76
205
  }
77
- return this.provider;
206
+ this.cachedProvider = this.resolveProvider();
207
+ this.cachedProviderAt = now;
208
+ return this.cachedProvider;
78
209
  }
79
210
 
80
- private resolveProvider(): ProviderConfig | null {
211
+ private resolveProvider(options: { includeHost?: boolean } = {}): ProviderConfig | null {
81
212
  if (!this.config.embeddingFallbackEnabled) return null;
82
213
 
214
+ if (
215
+ options.includeHost !== false &&
216
+ this.config.hostEmbeddingProviderEnabled !== false
217
+ ) {
218
+ const hostProvider = this.resolveHostEmbeddingProvider();
219
+ if (hostProvider) {
220
+ return {
221
+ type: "host",
222
+ model: hostProvider.model || hostProvider.id,
223
+ hostProvider,
224
+ };
225
+ }
226
+ }
227
+
83
228
  const preferred = this.config.embeddingFallbackProvider;
84
229
  const providers = preferred === "auto" ? ["openai", "local"] : [preferred];
85
230
 
86
231
  for (const p of providers) {
87
- if (p === "openai" && this.config.openaiApiKey) {
88
- const baseUrl = this.config.openaiBaseUrl ?? "https://api.openai.com/v1";
89
- return {
90
- type: "openai",
91
- model: DEFAULT_OPENAI_MODEL,
92
- endpoint: `${baseUrl.replace(/\/$/, "")}/embeddings`,
93
- headers: {
94
- "Content-Type": "application/json",
95
- Authorization: `Bearer ${this.config.openaiApiKey}`,
96
- },
97
- };
232
+ if (p === "openai") {
233
+ const provider = this.createOpenAiProvider();
234
+ if (provider) return provider;
98
235
  }
99
236
 
100
- if (p === "local" && this.config.localLlmEnabled && this.config.localLlmUrl) {
101
- const base = this.config.localLlmUrl.replace(/\/$/, "");
102
- const endpoint = /\/v1$/i.test(base) ? `${base}/embeddings` : `${base}/v1/embeddings`;
103
- const headers: Record<string, string> = {
104
- "Content-Type": "application/json",
105
- ...(this.config.localLlmHeaders ?? {}),
106
- };
107
- if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {
108
- headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;
109
- }
110
- return {
111
- type: "local",
112
- model:
113
- this.config.embeddingFallbackModel ||
114
- this.config.localLlmModel ||
115
- DEFAULT_OPENAI_MODEL,
116
- endpoint,
117
- headers,
118
- };
237
+ if (p === "local") {
238
+ const provider = this.createLocalProvider();
239
+ if (provider) return provider;
119
240
  }
120
241
  }
121
242
 
122
243
  return null;
123
244
  }
124
245
 
246
+ private resolveFallbackProviderForIdentity(identity: EmbedProviderIdentity): ProviderConfig | null {
247
+ if (!this.config.embeddingFallbackEnabled) return null;
248
+ const separator = identity.indexOf(":");
249
+ if (separator <= 0 || separator === identity.length - 1) return null;
250
+ const type = identity.slice(0, separator);
251
+ const model = identity.slice(separator + 1);
252
+
253
+ if (type === "openai") {
254
+ const provider = this.createOpenAiProvider();
255
+ return provider && provider.model === model ? provider : null;
256
+ }
257
+ if (type === "local") {
258
+ const provider = this.createLocalProvider();
259
+ return provider && provider.model === model ? provider : null;
260
+ }
261
+ return null;
262
+ }
263
+
264
+ private createOpenAiProvider(): ProviderConfig | null {
265
+ if (!this.config.openaiApiKey) return null;
266
+ const baseUrl = this.config.openaiBaseUrl ?? "https://api.openai.com/v1";
267
+ return {
268
+ type: "openai",
269
+ model: DEFAULT_OPENAI_MODEL,
270
+ endpoint: `${baseUrl.replace(/\/$/, "")}/embeddings`,
271
+ headers: {
272
+ "Content-Type": "application/json",
273
+ Authorization: `Bearer ${this.config.openaiApiKey}`,
274
+ },
275
+ };
276
+ }
277
+
278
+ private createLocalProvider(): ProviderConfig | null {
279
+ if (!this.config.localLlmEnabled || !this.config.localLlmUrl) return null;
280
+ const base = this.config.localLlmUrl.replace(/\/$/, "");
281
+ const endpoint = /\/v1$/i.test(base) ? `${base}/embeddings` : `${base}/v1/embeddings`;
282
+ const headers: Record<string, string> = {
283
+ "Content-Type": "application/json",
284
+ ...(this.config.localLlmHeaders ?? {}),
285
+ };
286
+ if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {
287
+ headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;
288
+ }
289
+ return {
290
+ type: "local",
291
+ model:
292
+ this.config.embeddingFallbackModel ||
293
+ this.config.localLlmModel ||
294
+ DEFAULT_OPENAI_MODEL,
295
+ endpoint,
296
+ headers,
297
+ };
298
+ }
299
+
300
+ private resolveHostEmbeddingProvider(): HostEmbeddingProvider | undefined {
301
+ const scopedConfig = this.config as HostEmbeddingScopeConfig;
302
+ const scopes = [
303
+ scopedConfig.memoryDir,
304
+ scopedConfig.hostEmbeddingProviderScope,
305
+ ].filter((scope): scope is string => typeof scope === "string" && scope.trim().length > 0);
306
+
307
+ for (const scope of new Set(scopes)) {
308
+ const provider = getHostEmbeddingProvider(scope);
309
+ if (provider) return provider;
310
+ }
311
+ return undefined;
312
+ }
313
+
125
314
  private async callEmbed(
126
315
  input: string,
127
316
  provider: ProviderConfig,
128
317
  signal?: AbortSignal,
318
+ inputType: HostEmbeddingInputType = "document",
129
319
  ): Promise<number[] | null> {
320
+ if (provider.type === "host") {
321
+ return this.callHostEmbed(input, provider.hostProvider, signal, inputType);
322
+ }
323
+ if (!provider.endpoint || !provider.headers) return null;
130
324
  try {
131
325
  const res = await fetch(provider.endpoint, {
132
326
  method: "POST",
@@ -144,12 +338,86 @@ export class EmbedHelper {
144
338
  }
145
339
  const payload = (await res.json()) as any;
146
340
  const vector = payload?.data?.[0]?.embedding;
147
- if (!Array.isArray(vector)) return null;
148
- return vector.map((n: unknown) => { const v = Number(n); return Number.isFinite(v) ? v : 0; });
341
+ return normalizeHttpEmbeddingVector(vector);
149
342
  } catch (err) {
150
343
  if (isAbortError(err)) throw err;
151
344
  log.debug(`EmbedHelper error: ${err}`);
152
345
  return null;
153
346
  }
154
347
  }
348
+
349
+ private async callHostEmbed(
350
+ input: string,
351
+ provider: HostEmbeddingProvider | undefined,
352
+ signal?: AbortSignal,
353
+ inputType: HostEmbeddingInputType = "document",
354
+ ): Promise<number[] | null> {
355
+ if (!provider) return null;
356
+ try {
357
+ const vector = await provider.embed(input.slice(0, 8000), {
358
+ signal: withTimeoutSignal(signal, 30_000),
359
+ inputType,
360
+ });
361
+ return this.normalizeHostEmbeddingVector(vector);
362
+ } catch (err) {
363
+ if (isAbortError(err)) throw err;
364
+ log.debug(`EmbedHelper host provider error: ${err}`);
365
+ return null;
366
+ }
367
+ }
368
+
369
+ private async callHostEmbedBatch(
370
+ inputs: string[],
371
+ provider: HostEmbeddingProvider,
372
+ signal?: AbortSignal,
373
+ ): Promise<(number[] | null)[]> {
374
+ try {
375
+ const vectors = await provider.embedBatch?.(
376
+ inputs.map((input) => input.slice(0, 8000)),
377
+ {
378
+ signal: withTimeoutSignal(signal, 30_000),
379
+ inputType: "document",
380
+ },
381
+ );
382
+ if (!Array.isArray(vectors)) return inputs.map(() => null);
383
+ return inputs.map((_, index) => this.normalizeHostEmbeddingVector(vectors[index]));
384
+ } catch (err) {
385
+ if (isAbortError(err)) throw err;
386
+ log.debug(`EmbedHelper host provider batch error: ${err}`);
387
+ return inputs.map(() => null);
388
+ }
389
+ }
390
+
391
+ private normalizeHostEmbeddingVector(value: unknown): number[] | null {
392
+ const vector = normalizeHostEmbeddingVector(value);
393
+ if (!vector) return null;
394
+ const expectedDimension = this.resolveHostEmbeddingExpectedDimension();
395
+ if (expectedDimension !== null && vector.length !== expectedDimension) {
396
+ return null;
397
+ }
398
+ return vector;
399
+ }
400
+
401
+ private resolveHostEmbeddingExpectedDimension(): number | null {
402
+ const value = this.options.hostEmbeddingExpectedDimension;
403
+ return typeof value === "number" && Number.isInteger(value) && value > 0
404
+ ? value
405
+ : null;
406
+ }
407
+ }
408
+
409
+ function providerIdentity(provider: ProviderConfig): EmbedProviderIdentity {
410
+ return `${provider.type}:${provider.model}`;
411
+ }
412
+
413
+ function normalizeHttpEmbeddingVector(vector: unknown): number[] | null {
414
+ if (!Array.isArray(vector)) return null;
415
+ const normalized: number[] = [];
416
+ for (const component of vector) {
417
+ if (typeof component !== "number" || !Number.isFinite(component)) {
418
+ return null;
419
+ }
420
+ normalized.push(component);
421
+ }
422
+ return normalized;
155
423
  }
@@ -41,7 +41,9 @@ function resolveNonQmdBackend(config: PluginConfig): SearchBackend | undefined {
41
41
  }
42
42
 
43
43
  if (backend === "lancedb") {
44
- const embedHelper = new EmbedHelper(config);
44
+ const embedHelper = new EmbedHelper(config, {
45
+ hostEmbeddingExpectedDimension: config.lanceEmbeddingDimension,
46
+ });
45
47
  return new LanceDbBackend({
46
48
  dbPath: config.lanceDbPath!,
47
49
  collection,
@@ -63,7 +65,9 @@ function resolveNonQmdBackend(config: PluginConfig): SearchBackend | undefined {
63
65
  }
64
66
 
65
67
  if (backend === "orama") {
66
- const embedHelper = new EmbedHelper(config);
68
+ const embedHelper = new EmbedHelper(config, {
69
+ hostEmbeddingExpectedDimension: config.oramaEmbeddingDimension,
70
+ });
67
71
  return new OramaBackend({
68
72
  dbPath: config.oramaDbPath!,
69
73
  collection,