@cmdoss/memwal-sdk 0.9.0 → 1.0.0

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 (174) hide show
  1. package/README.md +157 -52
  2. package/dist/client/ClientMemoryManager.d.ts.map +1 -1
  3. package/dist/client/ClientMemoryManager.js +25 -8
  4. package/dist/client/ClientMemoryManager.js.map +1 -1
  5. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  6. package/dist/client/SimplePDWClient.d.ts +2 -1
  7. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  8. package/dist/client/SimplePDWClient.js +23 -6
  9. package/dist/client/SimplePDWClient.js.map +1 -1
  10. package/dist/client/namespaces/MemoryNamespace.d.ts +6 -0
  11. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  12. package/dist/client/namespaces/MemoryNamespace.js +131 -18
  13. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  14. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +3 -1
  15. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  16. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  17. package/dist/config/ConfigurationHelper.js +61 -61
  18. package/dist/config/index.d.ts +1 -0
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/config/index.js +2 -0
  21. package/dist/config/index.js.map +1 -1
  22. package/dist/config/modelDefaults.d.ts +67 -0
  23. package/dist/config/modelDefaults.d.ts.map +1 -0
  24. package/dist/config/modelDefaults.js +91 -0
  25. package/dist/config/modelDefaults.js.map +1 -0
  26. package/dist/graph/GraphService.d.ts.map +1 -1
  27. package/dist/graph/GraphService.js +22 -21
  28. package/dist/graph/GraphService.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/langchain/createPDWRAG.js +30 -30
  34. package/dist/pipeline/MemoryPipeline.d.ts.map +1 -1
  35. package/dist/pipeline/MemoryPipeline.js +2 -1
  36. package/dist/pipeline/MemoryPipeline.js.map +1 -1
  37. package/dist/services/GeminiAIService.d.ts.map +1 -1
  38. package/dist/services/GeminiAIService.js +311 -310
  39. package/dist/services/GeminiAIService.js.map +1 -1
  40. package/dist/services/StorageService.d.ts +4 -1
  41. package/dist/services/StorageService.d.ts.map +1 -1
  42. package/dist/services/StorageService.js.map +1 -1
  43. package/dist/services/storage/QuiltBatchManager.d.ts +7 -0
  44. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  45. package/dist/services/storage/QuiltBatchManager.js +24 -5
  46. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  47. package/dist/services/storage/WalrusStorageManager.d.ts +10 -1
  48. package/dist/services/storage/WalrusStorageManager.d.ts.map +1 -1
  49. package/dist/services/storage/WalrusStorageManager.js +53 -12
  50. package/dist/services/storage/WalrusStorageManager.js.map +1 -1
  51. package/dist/vector/BrowserHnswIndexService.js +2 -2
  52. package/dist/vector/BrowserHnswIndexService.js.map +1 -1
  53. package/dist/vector/NodeHnswService.js +4 -4
  54. package/dist/vector/NodeHnswService.js.map +1 -1
  55. package/dist/vector/createHnswService.d.ts +4 -0
  56. package/dist/vector/createHnswService.d.ts.map +1 -1
  57. package/dist/vector/createHnswService.js +15 -3
  58. package/dist/vector/createHnswService.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/access/PermissionService.ts +635 -635
  61. package/src/aggregation/AggregationService.ts +389 -389
  62. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  63. package/src/ai-sdk/index.ts +65 -65
  64. package/src/ai-sdk/tools.ts +460 -460
  65. package/src/ai-sdk/types.ts +404 -404
  66. package/src/batch/BatchManager.ts +597 -597
  67. package/src/batch/BatchingService.ts +429 -429
  68. package/src/batch/MemoryProcessingCache.ts +492 -492
  69. package/src/batch/index.ts +30 -30
  70. package/src/browser.ts +200 -200
  71. package/src/client/ClientMemoryManager.ts +1004 -987
  72. package/src/client/PersonalDataWallet.ts +345 -345
  73. package/src/client/SimplePDWClient.ts +1387 -1369
  74. package/src/client/factory.ts +154 -154
  75. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  76. package/src/client/namespaces/BatchNamespace.ts +356 -356
  77. package/src/client/namespaces/CacheNamespace.ts +123 -123
  78. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  79. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  80. package/src/client/namespaces/ContextNamespace.ts +297 -297
  81. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  82. package/src/client/namespaces/GraphNamespace.ts +468 -468
  83. package/src/client/namespaces/IndexNamespace.ts +364 -364
  84. package/src/client/namespaces/MemoryNamespace.ts +1704 -1569
  85. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  86. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  87. package/src/client/namespaces/StorageNamespace.ts +458 -458
  88. package/src/client/namespaces/TxNamespace.ts +260 -260
  89. package/src/client/namespaces/WalletNamespace.ts +243 -243
  90. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -607
  91. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  92. package/src/client/namespaces/consolidated/StorageNamespace.ts +1143 -1141
  93. package/src/client/namespaces/consolidated/index.ts +41 -41
  94. package/src/client/signers/KeypairSigner.ts +108 -108
  95. package/src/client/signers/UnifiedSigner.ts +110 -110
  96. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  97. package/src/client/signers/index.ts +26 -26
  98. package/src/config/ConfigurationHelper.ts +412 -412
  99. package/src/config/defaults.ts +56 -56
  100. package/src/config/index.ts +16 -9
  101. package/src/config/modelDefaults.ts +103 -0
  102. package/src/config/validation.ts +70 -70
  103. package/src/core/index.ts +14 -14
  104. package/src/core/interfaces/IService.ts +307 -307
  105. package/src/core/interfaces/index.ts +8 -8
  106. package/src/core/types/capability.ts +297 -297
  107. package/src/core/types/index.ts +874 -874
  108. package/src/core/types/wallet.ts +270 -270
  109. package/src/core/types.ts +9 -9
  110. package/src/core/wallet.ts +222 -222
  111. package/src/embedding/index.ts +19 -19
  112. package/src/embedding/types.ts +357 -357
  113. package/src/errors/index.ts +602 -602
  114. package/src/errors/recovery.ts +461 -461
  115. package/src/errors/validation.ts +567 -567
  116. package/src/generated/pdw/capability.ts +319 -319
  117. package/src/graph/GraphService.ts +888 -887
  118. package/src/graph/KnowledgeGraphManager.ts +728 -728
  119. package/src/graph/index.ts +25 -25
  120. package/src/index.ts +498 -498
  121. package/src/infrastructure/index.ts +22 -22
  122. package/src/infrastructure/seal/EncryptionService.ts +628 -628
  123. package/src/infrastructure/seal/SealService.ts +613 -613
  124. package/src/infrastructure/seal/index.ts +9 -9
  125. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  126. package/src/infrastructure/sui/SuiService.ts +888 -888
  127. package/src/infrastructure/sui/index.ts +9 -9
  128. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  129. package/src/infrastructure/walrus/WalrusStorageService.ts +637 -637
  130. package/src/infrastructure/walrus/index.ts +9 -9
  131. package/src/langchain/createPDWRAG.ts +303 -303
  132. package/src/langchain/index.ts +47 -47
  133. package/src/permissions/ConsentRepository.browser.ts +249 -249
  134. package/src/permissions/ConsentRepository.ts +364 -364
  135. package/src/pipeline/MemoryPipeline.ts +863 -862
  136. package/src/pipeline/PipelineManager.ts +683 -683
  137. package/src/pipeline/index.ts +26 -26
  138. package/src/retrieval/AdvancedSearchService.ts +629 -629
  139. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  140. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -825
  141. package/src/retrieval/index.ts +42 -42
  142. package/src/services/BatchService.ts +352 -352
  143. package/src/services/CapabilityService.ts +464 -464
  144. package/src/services/ClassifierService.ts +465 -465
  145. package/src/services/CrossContextPermissionService.ts +486 -486
  146. package/src/services/EmbeddingService.ts +796 -796
  147. package/src/services/EncryptionService.ts +712 -712
  148. package/src/services/GeminiAIService.ts +754 -753
  149. package/src/services/MemoryIndexService.ts +1009 -1009
  150. package/src/services/MemoryService.ts +369 -369
  151. package/src/services/QueryService.ts +890 -890
  152. package/src/services/StorageService.ts +1185 -1182
  153. package/src/services/TransactionService.ts +838 -838
  154. package/src/services/VectorService.ts +462 -462
  155. package/src/services/ViewService.ts +484 -484
  156. package/src/services/index.ts +25 -25
  157. package/src/services/storage/BlobAttributesManager.ts +333 -333
  158. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  159. package/src/services/storage/MemorySearchManager.ts +387 -387
  160. package/src/services/storage/QuiltBatchManager.ts +1157 -1130
  161. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  162. package/src/services/storage/WalrusStorageManager.ts +333 -287
  163. package/src/services/storage/index.ts +57 -57
  164. package/src/types/index.ts +13 -13
  165. package/src/utils/index.ts +76 -76
  166. package/src/utils/memoryIndexOnChain.ts +507 -507
  167. package/src/vector/BrowserHnswIndexService.ts +758 -758
  168. package/src/vector/HnswWasmService.ts +731 -731
  169. package/src/vector/IHnswService.ts +233 -233
  170. package/src/vector/NodeHnswService.ts +833 -833
  171. package/src/vector/createHnswService.ts +147 -135
  172. package/src/vector/index.ts +56 -56
  173. package/src/wallet/ContextWalletService.ts +656 -656
  174. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,796 +1,796 @@
1
- /**
2
- * EmbeddingService - AI SDK Integration
3
- *
4
- * Refactored to use Vercel AI SDK as the underlying embedding provider.
5
- * Supports any AI SDK compatible provider (OpenAI, Google, Cohere, etc.)
6
- * while maintaining backward compatibility with existing PDW code.
7
- *
8
- * OpenRouter now uses the official @openrouter/sdk instead of raw fetch calls.
9
- *
10
- * Key features:
11
- * - Provider-agnostic: Accept any ai-sdk EmbeddingModel
12
- * - Backward compatible: Existing code continues to work
13
- * - Flexible configuration: Direct model OR provider config
14
- */
15
-
16
- import type { EmbeddingModelV2 } from '@ai-sdk/provider';
17
- import { embed, embedMany } from 'ai';
18
- import { createGoogleGenerativeAI } from '@ai-sdk/google';
19
- import { createOpenAI } from '@ai-sdk/openai';
20
- import { OpenRouter } from '@openrouter/sdk';
21
-
22
- // Type alias for embedding models - V2 is the default in AI SDK v5
23
- type EmbeddingModel<VALUE> = EmbeddingModelV2<VALUE>;
24
-
25
- // Provider instances (lazily initialized)
26
- let googleProvider: ReturnType<typeof createGoogleGenerativeAI> | null = null;
27
- let openaiProvider: ReturnType<typeof createOpenAI> | null = null;
28
- let openrouterProvider: ReturnType<typeof createOpenAI> | null = null;
29
- let cohereProvider: any = null;
30
-
31
- export interface EmbeddingConfig {
32
- /**
33
- * Option 1: Direct ai-sdk model (most flexible)
34
- * User provides their own EmbeddingModel from any provider
35
- *
36
- * For backward compatibility, also accepts string (treated as modelName)
37
- *
38
- * @example
39
- * ```typescript
40
- * import { openai } from '@ai-sdk/openai';
41
- * const service = new EmbeddingService({
42
- * model: openai.embedding('text-embedding-3-large')
43
- * });
44
- *
45
- * // Backward compatible:
46
- * const service = new EmbeddingService({
47
- * model: 'text-embedding-004', // Treated as modelName
48
- * apiKey: 'your-key'
49
- * });
50
- * ```
51
- */
52
- model?: EmbeddingModel<string> | string;
53
-
54
- /**
55
- * Option 2: Provider-based configuration
56
- * PDW creates the model from provider settings
57
- *
58
- * - google: Direct Google AI API
59
- * - openai: Direct OpenAI API
60
- * - openrouter: OpenRouter API gateway (supports multiple models)
61
- * - cohere: Direct Cohere API
62
- */
63
- provider?: 'google' | 'openai' | 'openrouter' | 'cohere';
64
-
65
- /**
66
- * API key for the provider
67
- * Falls back to environment variables:
68
- * - GEMINI_API_KEY or GOOGLE_AI_API_KEY (for google)
69
- * - OPENAI_API_KEY (for openai)
70
- * - OPENROUTER_API_KEY (for openrouter)
71
- * - COHERE_API_KEY (for cohere)
72
- */
73
- apiKey?: string;
74
-
75
- /**
76
- * Model name to use
77
- * - Google: 'text-embedding-004', 'gemini-embedding-001'
78
- * - OpenAI: 'text-embedding-3-small', 'text-embedding-3-large'
79
- * - OpenRouter: 'google/gemini-embedding-001', 'openai/text-embedding-3-small', etc.
80
- * - Cohere: 'embed-english-v3.0', 'embed-multilingual-v3.0'
81
- */
82
- modelName?: string;
83
-
84
- /**
85
- * Embedding dimensions (optional, provider-dependent)
86
- * - Google: Up to 3072
87
- * - OpenAI: 256, 512, 1024, 1536, 3072 (depending on model)
88
- * - OpenRouter: Depends on the underlying model
89
- * - Cohere: Model-specific
90
- */
91
- dimensions?: number;
92
-
93
- /**
94
- * Rate limiting
95
- */
96
- requestsPerMinute?: number;
97
- }
98
-
99
- export interface EmbeddingOptions {
100
- text: string;
101
- type?: 'content' | 'metadata' | 'query';
102
- taskType?: 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY';
103
- }
104
-
105
- export interface EmbeddingResult {
106
- vector: number[];
107
- dimension: number;
108
- model: string;
109
- processingTime: number;
110
- tokenCount?: number;
111
- }
112
-
113
- export interface BatchEmbeddingResult {
114
- vectors: number[][];
115
- dimension: number;
116
- model: string;
117
- totalProcessingTime: number;
118
- averageProcessingTime: number;
119
- successCount: number;
120
- failedCount: number;
121
- }
122
-
123
- /**
124
- * Embedding service using Vercel AI SDK
125
- * Supports all AI SDK compatible providers
126
- * OpenRouter uses the official @openrouter/sdk for better type safety
127
- */
128
- export class EmbeddingService {
129
- private embeddingModel: EmbeddingModel<string> | null = null;
130
- private modelName: string;
131
- private dimensions: number;
132
- private requestCount = 0;
133
- private lastReset = Date.now();
134
- private readonly maxRequestsPerMinute: number;
135
- private provider: 'google' | 'openai' | 'openrouter' | 'cohere' | 'custom';
136
- private apiKey: string = '';
137
- private openRouterClient: OpenRouter | null = null;
138
-
139
- constructor(config: EmbeddingConfig = {}) {
140
- this.maxRequestsPerMinute = config.requestsPerMinute || 1500;
141
-
142
- // Case 1: Direct model provided (most flexible)
143
- if (config.model) {
144
- // Backward compatibility: If model is a string, treat as modelName
145
- if (typeof config.model === 'string') {
146
- const modelNameFromString = config.model;
147
- console.log(`🔄 Backward compatibility: treating model string "${modelNameFromString}" as modelName`);
148
-
149
- // Treat string as modelName and use provider config path
150
- const provider = config.provider || 'google';
151
- this.apiKey = this.resolveApiKey(provider, config.apiKey);
152
-
153
- if (!this.apiKey) {
154
- throw new Error(
155
- `API key is required for ${provider} provider. ` +
156
- `Provide it via config.apiKey or environment variable.`
157
- );
158
- }
159
-
160
- this.provider = provider;
161
- this.modelName = modelNameFromString;
162
- this.dimensions = config.dimensions || this.getDefaultDimensions(provider);
163
-
164
- // OpenRouter uses SDK, others use AI SDK
165
- if (provider === 'openrouter') {
166
- this.openRouterClient = new OpenRouter({ apiKey: this.apiKey });
167
- } else {
168
- this.embeddingModel = this.createModel(provider, this.apiKey, this.modelName);
169
- }
170
-
171
- console.log(`✅ EmbeddingService initialized with ${provider} provider (${this.modelName}) [backward compat mode]`);
172
- return;
173
- }
174
-
175
- // New behavior: Direct EmbeddingModel from ai-sdk
176
- this.embeddingModel = config.model;
177
- this.modelName = 'custom';
178
- this.dimensions = config.dimensions || 768; // Default 768 for speed (was 3072)
179
- this.provider = 'custom';
180
- console.log('✅ EmbeddingService initialized with custom ai-sdk model');
181
- return;
182
- }
183
-
184
- // Case 2: Provider-based configuration
185
- const provider = config.provider || 'google'; // Default to google for backward compat
186
- this.apiKey = this.resolveApiKey(provider, config.apiKey);
187
-
188
- if (!this.apiKey) {
189
- throw new Error(
190
- `API key is required for ${provider} provider. ` +
191
- `Provide it via config.apiKey or environment variable.`
192
- );
193
- }
194
-
195
- this.provider = provider;
196
- this.modelName = config.modelName || this.getDefaultModelName(provider);
197
- this.dimensions = config.dimensions || this.getDefaultDimensions(provider);
198
-
199
- // OpenRouter uses SDK, others use AI SDK
200
- if (provider === 'openrouter') {
201
- this.openRouterClient = new OpenRouter({ apiKey: this.apiKey });
202
- } else {
203
- this.embeddingModel = this.createModel(provider, this.apiKey, this.modelName);
204
- }
205
-
206
- console.log(`✅ EmbeddingService initialized with ${provider} provider (${this.modelName})`);
207
- }
208
-
209
- /**
210
- * Resolve API key from config or environment
211
- */
212
- private resolveApiKey(provider: string, configKey?: string): string {
213
- if (configKey) return configKey;
214
-
215
- switch (provider) {
216
- case 'google':
217
- return process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY || '';
218
- case 'openai':
219
- return process.env.OPENAI_API_KEY || '';
220
- case 'openrouter':
221
- return process.env.OPENROUTER_API_KEY || '';
222
- case 'cohere':
223
- return process.env.COHERE_API_KEY || '';
224
- default:
225
- return '';
226
- }
227
- }
228
-
229
- /**
230
- * Get default model name for provider
231
- */
232
- private getDefaultModelName(provider: string): string {
233
- switch (provider) {
234
- case 'google':
235
- return 'text-embedding-004';
236
- case 'openai':
237
- return 'text-embedding-3-small';
238
- case 'openrouter':
239
- return 'google/gemini-embedding-001'; // Default OpenRouter embedding model
240
- case 'cohere':
241
- return 'embed-english-v3.0';
242
- default:
243
- return 'text-embedding-004';
244
- }
245
- }
246
-
247
- /**
248
- * Get default dimensions for provider
249
- *
250
- * Default is now 768 for faster performance:
251
- * - 4x smaller vectors = faster indexing & search
252
- * - ~4x less storage space
253
- * - Minimal quality loss for most use cases
254
- *
255
- * Users can override via config.dimensions
256
- */
257
- private getDefaultDimensions(provider: string): number {
258
- switch (provider) {
259
- case 'google':
260
- return 768; // text-embedding-004 supports output_dimensionality
261
- case 'openai':
262
- return 768; // text-embedding-3-small supports dimensions param
263
- case 'openrouter':
264
- return 768; // Most models support dimension truncation
265
- case 'cohere':
266
- return 768; // embed-english-v3.0 supports dimensions
267
- default:
268
- return 768; // Default to 768 for speed
269
- }
270
- }
271
-
272
- /**
273
- * Create embedding model from provider
274
- */
275
- private createModel(
276
- provider: string,
277
- apiKey: string,
278
- modelName: string
279
- ): EmbeddingModel<string> {
280
- switch (provider) {
281
- case 'google': {
282
- if (!googleProvider) {
283
- googleProvider = createGoogleGenerativeAI({ apiKey });
284
- }
285
- return googleProvider.textEmbeddingModel(modelName);
286
- }
287
-
288
- case 'openai': {
289
- if (!openaiProvider) {
290
- openaiProvider = createOpenAI({ apiKey });
291
- }
292
- // OpenAI returns EmbeddingModelV2 but is compatible with ai SDK
293
- return openaiProvider.textEmbeddingModel(modelName) as unknown as EmbeddingModel<string>;
294
- }
295
-
296
- case 'openrouter': {
297
- // OpenRouter uses OpenAI-compatible API with custom baseURL
298
- if (!openrouterProvider) {
299
- openrouterProvider = createOpenAI({
300
- baseURL: 'https://openrouter.ai/api/v1',
301
- apiKey,
302
- });
303
- }
304
- // OpenRouter embedding models use the same interface as OpenAI
305
- return openrouterProvider.textEmbeddingModel(modelName) as unknown as EmbeddingModel<string>;
306
- }
307
-
308
- case 'cohere': {
309
- if (!cohereProvider) {
310
- throw new Error(
311
- 'Cohere provider requires manual initialization. ' +
312
- 'Import createCohere from @ai-sdk/cohere and set cohereProvider before use.'
313
- );
314
- }
315
- return cohereProvider.textEmbedding(modelName);
316
- }
317
-
318
- default:
319
- throw new Error(`Unsupported provider: ${provider}`);
320
- }
321
- }
322
-
323
- /**
324
- * Generate embedding for a single text
325
- */
326
- async embedText(options: EmbeddingOptions): Promise<EmbeddingResult> {
327
- const startTime = Date.now();
328
-
329
- // Validate input
330
- if (!options.text || typeof options.text !== 'string' || options.text.trim().length === 0) {
331
- throw new Error('Invalid or empty text provided for embedding');
332
- }
333
-
334
- await this.checkRateLimit();
335
-
336
- try {
337
- // OpenRouter uses native fetch API for better compatibility
338
- if (this.provider === 'openrouter') {
339
- return await this.embedTextOpenRouter(options.text, startTime);
340
- }
341
-
342
- // Other providers use AI SDK
343
- if (!this.embeddingModel) {
344
- throw new Error('Embedding model not initialized');
345
- }
346
-
347
- const result = await embed({
348
- model: this.embeddingModel,
349
- value: options.text,
350
- ...this.getProviderOptions(options),
351
- });
352
-
353
- this.requestCount++;
354
-
355
- return {
356
- vector: result.embedding,
357
- dimension: result.embedding.length,
358
- model: this.modelName,
359
- processingTime: Date.now() - startTime,
360
- tokenCount: result.usage?.tokens,
361
- };
362
- } catch (error) {
363
- throw new Error(
364
- `Embedding generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
365
- );
366
- }
367
- }
368
-
369
- /**
370
- * Generate embedding using OpenRouter SDK
371
- * Uses official @openrouter/sdk for embeddings
372
- * Passes dimensions parameter to truncate output vectors
373
- */
374
- private async embedTextOpenRouter(text: string, startTime: number): Promise<EmbeddingResult> {
375
- if (!this.openRouterClient) {
376
- throw new Error('OpenRouter client not initialized');
377
- }
378
-
379
- // Build request with optional dimensions parameter
380
- const request: any = {
381
- model: this.modelName,
382
- input: text
383
- };
384
-
385
- // Add dimensions if configured (enables output truncation)
386
- if (this.dimensions && this.dimensions < 3072) {
387
- request.dimensions = this.dimensions;
388
- }
389
-
390
- const result = await this.openRouterClient.embeddings.generate(request);
391
-
392
- // Handle union type - result can be string or object
393
- if (typeof result === 'string') {
394
- throw new Error('Unexpected string response from OpenRouter embeddings API');
395
- }
396
-
397
- const data = (result as any).data;
398
- if (!data || !data[0] || !data[0].embedding) {
399
- throw new Error('Invalid response from OpenRouter embeddings API');
400
- }
401
-
402
- this.requestCount++;
403
-
404
- // Handle embedding which can be string or number[]
405
- const embedding = data[0].embedding;
406
- const vector = typeof embedding === 'string'
407
- ? JSON.parse(embedding) as number[]
408
- : embedding as number[];
409
-
410
- const usage = (result as any).usage;
411
-
412
- return {
413
- vector,
414
- dimension: vector.length,
415
- model: this.modelName,
416
- processingTime: Date.now() - startTime,
417
- tokenCount: usage?.totalTokens
418
- };
419
- }
420
-
421
- /**
422
- * Generate embeddings for multiple texts (batched)
423
- */
424
- async embedBatch(
425
- texts: string[],
426
- options: Omit<EmbeddingOptions, 'text'> = {}
427
- ): Promise<BatchEmbeddingResult> {
428
- const startTime = Date.now();
429
- let successCount = 0;
430
- let failedCount = 0;
431
-
432
- try {
433
- await this.checkRateLimit();
434
-
435
- // OpenRouter uses native fetch API for better compatibility
436
- if (this.provider === 'openrouter') {
437
- return await this.embedBatchOpenRouter(texts, startTime);
438
- }
439
-
440
- // Other providers use AI SDK
441
- if (!this.embeddingModel) {
442
- throw new Error('Embedding model not initialized');
443
- }
444
-
445
- const result = await embedMany({
446
- model: this.embeddingModel,
447
- values: texts,
448
- ...this.getProviderOptions(options as EmbeddingOptions),
449
- });
450
-
451
- successCount = result.embeddings.length;
452
- const totalTime = Date.now() - startTime;
453
-
454
- return {
455
- vectors: result.embeddings,
456
- dimension: result.embeddings[0]?.length || this.dimensions,
457
- model: this.modelName,
458
- totalProcessingTime: totalTime,
459
- averageProcessingTime: totalTime / texts.length,
460
- successCount,
461
- failedCount,
462
- };
463
- } catch (error) {
464
- throw new Error(
465
- `Batch embedding failed: ${error instanceof Error ? error.message : 'Unknown error'}`
466
- );
467
- }
468
- }
469
-
470
- /**
471
- * Generate batch embeddings using OpenRouter SDK
472
- * Passes dimensions parameter to truncate output vectors
473
- */
474
- private async embedBatchOpenRouter(texts: string[], startTime: number): Promise<BatchEmbeddingResult> {
475
- if (!this.openRouterClient) {
476
- throw new Error('OpenRouter client not initialized');
477
- }
478
-
479
- // Build request with optional dimensions parameter
480
- const request: any = {
481
- model: this.modelName,
482
- input: texts
483
- };
484
-
485
- // Add dimensions if configured (enables output truncation)
486
- if (this.dimensions && this.dimensions < 3072) {
487
- request.dimensions = this.dimensions;
488
- }
489
-
490
- const result = await this.openRouterClient.embeddings.generate(request);
491
-
492
- // Handle union type - result can be string or object
493
- if (typeof result === 'string') {
494
- throw new Error('Unexpected string response from OpenRouter embeddings API');
495
- }
496
-
497
- const data = (result as any).data;
498
- if (!data || !Array.isArray(data)) {
499
- throw new Error('Invalid response from OpenRouter embeddings API');
500
- }
501
-
502
- // Sort by index to ensure correct order
503
- const sortedData = [...data].sort((a: any, b: any) => (a.index || 0) - (b.index || 0));
504
- const vectors = sortedData.map((item: any) => {
505
- const embedding = item.embedding;
506
- return typeof embedding === 'string'
507
- ? JSON.parse(embedding) as number[]
508
- : embedding as number[];
509
- });
510
-
511
- this.requestCount++;
512
- const totalTime = Date.now() - startTime;
513
-
514
- return {
515
- vectors,
516
- dimension: vectors[0]?.length || this.dimensions,
517
- model: this.modelName,
518
- totalProcessingTime: totalTime,
519
- averageProcessingTime: totalTime / texts.length,
520
- successCount: vectors.length,
521
- failedCount: texts.length - vectors.length
522
- };
523
- }
524
-
525
- /**
526
- * Get provider-specific options
527
- */
528
- private getProviderOptions(options: EmbeddingOptions): any {
529
- const providerOpts: any = {};
530
-
531
- if (this.provider === 'google') {
532
- providerOpts.providerOptions = {
533
- google: {
534
- outputDimensionality: this.dimensions,
535
- taskType: this.getGoogleTaskType(options.type),
536
- },
537
- };
538
- } else if (this.provider === 'openai') {
539
- providerOpts.providerOptions = {
540
- openai: {
541
- dimensions: this.dimensions,
542
- },
543
- };
544
- } else if (this.provider === 'openrouter') {
545
- // OpenRouter uses OpenAI-compatible options
546
- // Note: dimensions may not be supported for all models via OpenRouter
547
- providerOpts.providerOptions = {
548
- openai: {
549
- dimensions: this.dimensions,
550
- },
551
- };
552
- } else if (this.provider === 'cohere') {
553
- providerOpts.providerOptions = {
554
- cohere: {
555
- inputType: this.getCohereInputType(options.type),
556
- },
557
- };
558
- }
559
-
560
- return providerOpts;
561
- }
562
-
563
- /**
564
- * Map PDW type to Google task type
565
- */
566
- private getGoogleTaskType(type?: string): string {
567
- switch (type) {
568
- case 'query':
569
- return 'RETRIEVAL_QUERY';
570
- case 'content':
571
- return 'RETRIEVAL_DOCUMENT';
572
- case 'metadata':
573
- return 'SEMANTIC_SIMILARITY';
574
- default:
575
- return 'RETRIEVAL_DOCUMENT';
576
- }
577
- }
578
-
579
- /**
580
- * Map PDW type to Cohere input type
581
- */
582
- private getCohereInputType(type?: string): string {
583
- switch (type) {
584
- case 'query':
585
- return 'search_query';
586
- case 'content':
587
- return 'search_document';
588
- default:
589
- return 'search_document';
590
- }
591
- }
592
-
593
- /**
594
- * Calculate cosine similarity between two vectors
595
- */
596
- calculateCosineSimilarity(vectorA: number[], vectorB: number[]): number {
597
- if (vectorA.length !== vectorB.length) {
598
- throw new Error(`Vector dimension mismatch: ${vectorA.length} vs ${vectorB.length}`);
599
- }
600
-
601
- let dotProduct = 0;
602
- let normA = 0;
603
- let normB = 0;
604
-
605
- for (let i = 0; i < vectorA.length; i++) {
606
- dotProduct += vectorA[i] * vectorB[i];
607
- normA += vectorA[i] * vectorA[i];
608
- normB += vectorB[i] * vectorB[i];
609
- }
610
-
611
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
612
-
613
- if (magnitude === 0) {
614
- return 0;
615
- }
616
-
617
- return dotProduct / magnitude;
618
- }
619
-
620
- /**
621
- * Calculate Euclidean distance between two vectors
622
- */
623
- calculateEuclideanDistance(vectorA: number[], vectorB: number[]): number {
624
- if (vectorA.length !== vectorB.length) {
625
- throw new Error(`Vector dimension mismatch: ${vectorA.length} vs ${vectorB.length}`);
626
- }
627
-
628
- let sum = 0;
629
- for (let i = 0; i < vectorA.length; i++) {
630
- const diff = vectorA[i] - vectorB[i];
631
- sum += diff * diff;
632
- }
633
-
634
- return Math.sqrt(sum);
635
- }
636
-
637
- /**
638
- * Normalize a vector to unit length
639
- */
640
- normalizeVector(vector: number[]): number[] {
641
- const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
642
-
643
- if (magnitude === 0) {
644
- return vector;
645
- }
646
-
647
- return vector.map(val => val / magnitude);
648
- }
649
-
650
- /**
651
- * Find the most similar vectors to a query vector
652
- */
653
- findMostSimilar(
654
- queryVector: number[],
655
- candidateVectors: number[][],
656
- k: number = 5
657
- ): Array<{ index: number; similarity: number; distance: number }> {
658
- const similarities = candidateVectors.map((vector, index) => {
659
- const similarity = this.calculateCosineSimilarity(queryVector, vector);
660
- const distance = this.calculateEuclideanDistance(queryVector, vector);
661
-
662
- return { index, similarity, distance };
663
- });
664
-
665
- similarities.sort((a, b) => b.similarity - a.similarity);
666
-
667
- return similarities.slice(0, k);
668
- }
669
-
670
- /**
671
- * Get embedding statistics
672
- */
673
- getStats(): {
674
- totalRequests: number;
675
- requestsThisMinute: number;
676
- model: string;
677
- dimensions: number;
678
- rateLimit: number;
679
- provider: string;
680
- } {
681
- const now = Date.now();
682
- const requestsThisMinute = (now - this.lastReset) < 60000 ? this.requestCount : 0;
683
-
684
- return {
685
- totalRequests: this.requestCount,
686
- requestsThisMinute,
687
- model: this.modelName,
688
- dimensions: this.dimensions,
689
- rateLimit: this.maxRequestsPerMinute,
690
- provider: this.provider,
691
- };
692
- }
693
-
694
- /**
695
- * Reset rate limiting counters
696
- */
697
- private resetRateLimit(): void {
698
- const now = Date.now();
699
- if (now - this.lastReset >= 60000) {
700
- this.requestCount = 0;
701
- this.lastReset = now;
702
- }
703
- }
704
-
705
- /**
706
- * Check rate limiting and wait if necessary
707
- */
708
- private async checkRateLimit(): Promise<void> {
709
- this.resetRateLimit();
710
-
711
- if (this.requestCount >= this.maxRequestsPerMinute) {
712
- const waitTime = 60000 - (Date.now() - this.lastReset);
713
- if (waitTime > 0) {
714
- if (process.env.NODE_ENV === 'development') {
715
- console.warn(`Rate limit reached, waiting ${waitTime}ms`);
716
- }
717
- await this.delay(waitTime);
718
- this.resetRateLimit();
719
- }
720
- }
721
- }
722
-
723
- /**
724
- * Utility delay function
725
- */
726
- private delay(ms: number): Promise<void> {
727
- return new Promise(resolve => setTimeout(resolve, ms));
728
- }
729
- }
730
-
731
- export default EmbeddingService;
732
-
733
- // ==================== Singleton Pattern ====================
734
-
735
- /**
736
- * Generate config key for singleton cache
737
- */
738
- function getConfigKey(config: EmbeddingConfig): string {
739
- const provider = config.provider || 'google';
740
- const modelName = typeof config.model === 'string'
741
- ? config.model
742
- : (config.modelName || 'default');
743
- const dimensions = config.dimensions || 'default';
744
- return `${provider}:${modelName}:${dimensions}`;
745
- }
746
-
747
- /** Singleton cache */
748
- const sharedInstances = new Map<string, EmbeddingService>();
749
-
750
- /**
751
- * Get or create a shared EmbeddingService instance (Singleton)
752
- *
753
- * All clients with same provider/model/dimensions share one instance.
754
- * Reduces memory usage and connection overhead.
755
- *
756
- * @example
757
- * ```typescript
758
- * // Instead of: new EmbeddingService({ apiKey, modelName })
759
- * const embedding = getSharedEmbeddingService({ apiKey, modelName });
760
- * ```
761
- */
762
- export function getSharedEmbeddingService(config: EmbeddingConfig): EmbeddingService {
763
- const key = getConfigKey(config);
764
-
765
- let instance = sharedInstances.get(key);
766
- if (!instance) {
767
- console.log(`🔧 [Singleton] Creating shared EmbeddingService: ${key}`);
768
- instance = new EmbeddingService(config);
769
- sharedInstances.set(key, instance);
770
- }
771
-
772
- return instance;
773
- }
774
-
775
- /**
776
- * Clear all shared instances (for testing)
777
- */
778
- export function clearSharedEmbeddingServices(): void {
779
- sharedInstances.clear();
780
- }
781
-
782
- /**
783
- * Get singleton stats
784
- */
785
- export function getSharedEmbeddingStats(): {
786
- instanceCount: number;
787
- instances: Array<{ key: string; stats: ReturnType<EmbeddingService['getStats']> }>;
788
- } {
789
- return {
790
- instanceCount: sharedInstances.size,
791
- instances: Array.from(sharedInstances.entries()).map(([key, svc]) => ({
792
- key,
793
- stats: svc.getStats(),
794
- })),
795
- };
796
- }
1
+ /**
2
+ * EmbeddingService - AI SDK Integration
3
+ *
4
+ * Refactored to use Vercel AI SDK as the underlying embedding provider.
5
+ * Supports any AI SDK compatible provider (OpenAI, Google, Cohere, etc.)
6
+ * while maintaining backward compatibility with existing PDW code.
7
+ *
8
+ * OpenRouter now uses the official @openrouter/sdk instead of raw fetch calls.
9
+ *
10
+ * Key features:
11
+ * - Provider-agnostic: Accept any ai-sdk EmbeddingModel
12
+ * - Backward compatible: Existing code continues to work
13
+ * - Flexible configuration: Direct model OR provider config
14
+ */
15
+
16
+ import type { EmbeddingModelV2 } from '@ai-sdk/provider';
17
+ import { embed, embedMany } from 'ai';
18
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
19
+ import { createOpenAI } from '@ai-sdk/openai';
20
+ import { OpenRouter } from '@openrouter/sdk';
21
+
22
+ // Type alias for embedding models - V2 is the default in AI SDK v5
23
+ type EmbeddingModel<VALUE> = EmbeddingModelV2<VALUE>;
24
+
25
+ // Provider instances (lazily initialized)
26
+ let googleProvider: ReturnType<typeof createGoogleGenerativeAI> | null = null;
27
+ let openaiProvider: ReturnType<typeof createOpenAI> | null = null;
28
+ let openrouterProvider: ReturnType<typeof createOpenAI> | null = null;
29
+ let cohereProvider: any = null;
30
+
31
+ export interface EmbeddingConfig {
32
+ /**
33
+ * Option 1: Direct ai-sdk model (most flexible)
34
+ * User provides their own EmbeddingModel from any provider
35
+ *
36
+ * For backward compatibility, also accepts string (treated as modelName)
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * import { openai } from '@ai-sdk/openai';
41
+ * const service = new EmbeddingService({
42
+ * model: openai.embedding('text-embedding-3-large')
43
+ * });
44
+ *
45
+ * // Backward compatible:
46
+ * const service = new EmbeddingService({
47
+ * model: 'text-embedding-004', // Treated as modelName
48
+ * apiKey: 'your-key'
49
+ * });
50
+ * ```
51
+ */
52
+ model?: EmbeddingModel<string> | string;
53
+
54
+ /**
55
+ * Option 2: Provider-based configuration
56
+ * PDW creates the model from provider settings
57
+ *
58
+ * - google: Direct Google AI API
59
+ * - openai: Direct OpenAI API
60
+ * - openrouter: OpenRouter API gateway (supports multiple models)
61
+ * - cohere: Direct Cohere API
62
+ */
63
+ provider?: 'google' | 'openai' | 'openrouter' | 'cohere';
64
+
65
+ /**
66
+ * API key for the provider
67
+ * Falls back to environment variables:
68
+ * - GEMINI_API_KEY or GOOGLE_AI_API_KEY (for google)
69
+ * - OPENAI_API_KEY (for openai)
70
+ * - OPENROUTER_API_KEY (for openrouter)
71
+ * - COHERE_API_KEY (for cohere)
72
+ */
73
+ apiKey?: string;
74
+
75
+ /**
76
+ * Model name to use
77
+ * - Google: 'text-embedding-004', 'gemini-embedding-001'
78
+ * - OpenAI: 'text-embedding-3-small', 'text-embedding-3-large'
79
+ * - OpenRouter: 'google/gemini-embedding-001', 'openai/text-embedding-3-small', etc.
80
+ * - Cohere: 'embed-english-v3.0', 'embed-multilingual-v3.0'
81
+ */
82
+ modelName?: string;
83
+
84
+ /**
85
+ * Embedding dimensions (optional, provider-dependent)
86
+ * - Google: Up to 3072
87
+ * - OpenAI: 256, 512, 1024, 1536, 3072 (depending on model)
88
+ * - OpenRouter: Depends on the underlying model
89
+ * - Cohere: Model-specific
90
+ */
91
+ dimensions?: number;
92
+
93
+ /**
94
+ * Rate limiting
95
+ */
96
+ requestsPerMinute?: number;
97
+ }
98
+
99
+ export interface EmbeddingOptions {
100
+ text: string;
101
+ type?: 'content' | 'metadata' | 'query';
102
+ taskType?: 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY';
103
+ }
104
+
105
+ export interface EmbeddingResult {
106
+ vector: number[];
107
+ dimension: number;
108
+ model: string;
109
+ processingTime: number;
110
+ tokenCount?: number;
111
+ }
112
+
113
+ export interface BatchEmbeddingResult {
114
+ vectors: number[][];
115
+ dimension: number;
116
+ model: string;
117
+ totalProcessingTime: number;
118
+ averageProcessingTime: number;
119
+ successCount: number;
120
+ failedCount: number;
121
+ }
122
+
123
+ /**
124
+ * Embedding service using Vercel AI SDK
125
+ * Supports all AI SDK compatible providers
126
+ * OpenRouter uses the official @openrouter/sdk for better type safety
127
+ */
128
+ export class EmbeddingService {
129
+ private embeddingModel: EmbeddingModel<string> | null = null;
130
+ private modelName: string;
131
+ private dimensions: number;
132
+ private requestCount = 0;
133
+ private lastReset = Date.now();
134
+ private readonly maxRequestsPerMinute: number;
135
+ private provider: 'google' | 'openai' | 'openrouter' | 'cohere' | 'custom';
136
+ private apiKey: string = '';
137
+ private openRouterClient: OpenRouter | null = null;
138
+
139
+ constructor(config: EmbeddingConfig = {}) {
140
+ this.maxRequestsPerMinute = config.requestsPerMinute || 1500;
141
+
142
+ // Case 1: Direct model provided (most flexible)
143
+ if (config.model) {
144
+ // Backward compatibility: If model is a string, treat as modelName
145
+ if (typeof config.model === 'string') {
146
+ const modelNameFromString = config.model;
147
+ console.log(`🔄 Backward compatibility: treating model string "${modelNameFromString}" as modelName`);
148
+
149
+ // Treat string as modelName and use provider config path
150
+ const provider = config.provider || 'google';
151
+ this.apiKey = this.resolveApiKey(provider, config.apiKey);
152
+
153
+ if (!this.apiKey) {
154
+ throw new Error(
155
+ `API key is required for ${provider} provider. ` +
156
+ `Provide it via config.apiKey or environment variable.`
157
+ );
158
+ }
159
+
160
+ this.provider = provider;
161
+ this.modelName = modelNameFromString;
162
+ this.dimensions = config.dimensions || this.getDefaultDimensions(provider);
163
+
164
+ // OpenRouter uses SDK, others use AI SDK
165
+ if (provider === 'openrouter') {
166
+ this.openRouterClient = new OpenRouter({ apiKey: this.apiKey });
167
+ } else {
168
+ this.embeddingModel = this.createModel(provider, this.apiKey, this.modelName);
169
+ }
170
+
171
+ console.log(`✅ EmbeddingService initialized with ${provider} provider (${this.modelName}) [backward compat mode]`);
172
+ return;
173
+ }
174
+
175
+ // New behavior: Direct EmbeddingModel from ai-sdk
176
+ this.embeddingModel = config.model;
177
+ this.modelName = 'custom';
178
+ this.dimensions = config.dimensions || 768; // Default 768 for speed (was 3072)
179
+ this.provider = 'custom';
180
+ console.log('✅ EmbeddingService initialized with custom ai-sdk model');
181
+ return;
182
+ }
183
+
184
+ // Case 2: Provider-based configuration
185
+ const provider = config.provider || 'google'; // Default to google for backward compat
186
+ this.apiKey = this.resolveApiKey(provider, config.apiKey);
187
+
188
+ if (!this.apiKey) {
189
+ throw new Error(
190
+ `API key is required for ${provider} provider. ` +
191
+ `Provide it via config.apiKey or environment variable.`
192
+ );
193
+ }
194
+
195
+ this.provider = provider;
196
+ this.modelName = config.modelName || this.getDefaultModelName(provider);
197
+ this.dimensions = config.dimensions || this.getDefaultDimensions(provider);
198
+
199
+ // OpenRouter uses SDK, others use AI SDK
200
+ if (provider === 'openrouter') {
201
+ this.openRouterClient = new OpenRouter({ apiKey: this.apiKey });
202
+ } else {
203
+ this.embeddingModel = this.createModel(provider, this.apiKey, this.modelName);
204
+ }
205
+
206
+ console.log(`✅ EmbeddingService initialized with ${provider} provider (${this.modelName})`);
207
+ }
208
+
209
+ /**
210
+ * Resolve API key from config or environment
211
+ */
212
+ private resolveApiKey(provider: string, configKey?: string): string {
213
+ if (configKey) return configKey;
214
+
215
+ switch (provider) {
216
+ case 'google':
217
+ return process.env.GEMINI_API_KEY || process.env.GOOGLE_AI_API_KEY || '';
218
+ case 'openai':
219
+ return process.env.OPENAI_API_KEY || '';
220
+ case 'openrouter':
221
+ return process.env.OPENROUTER_API_KEY || '';
222
+ case 'cohere':
223
+ return process.env.COHERE_API_KEY || '';
224
+ default:
225
+ return '';
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get default model name for provider
231
+ */
232
+ private getDefaultModelName(provider: string): string {
233
+ switch (provider) {
234
+ case 'google':
235
+ return 'text-embedding-004';
236
+ case 'openai':
237
+ return 'text-embedding-3-small';
238
+ case 'openrouter':
239
+ return 'google/gemini-embedding-001'; // Default OpenRouter embedding model
240
+ case 'cohere':
241
+ return 'embed-english-v3.0';
242
+ default:
243
+ return 'text-embedding-004';
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Get default dimensions for provider
249
+ *
250
+ * Default is now 768 for faster performance:
251
+ * - 4x smaller vectors = faster indexing & search
252
+ * - ~4x less storage space
253
+ * - Minimal quality loss for most use cases
254
+ *
255
+ * Users can override via config.dimensions
256
+ */
257
+ private getDefaultDimensions(provider: string): number {
258
+ switch (provider) {
259
+ case 'google':
260
+ return 768; // text-embedding-004 supports output_dimensionality
261
+ case 'openai':
262
+ return 768; // text-embedding-3-small supports dimensions param
263
+ case 'openrouter':
264
+ return 768; // Most models support dimension truncation
265
+ case 'cohere':
266
+ return 768; // embed-english-v3.0 supports dimensions
267
+ default:
268
+ return 768; // Default to 768 for speed
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Create embedding model from provider
274
+ */
275
+ private createModel(
276
+ provider: string,
277
+ apiKey: string,
278
+ modelName: string
279
+ ): EmbeddingModel<string> {
280
+ switch (provider) {
281
+ case 'google': {
282
+ if (!googleProvider) {
283
+ googleProvider = createGoogleGenerativeAI({ apiKey });
284
+ }
285
+ return googleProvider.textEmbeddingModel(modelName);
286
+ }
287
+
288
+ case 'openai': {
289
+ if (!openaiProvider) {
290
+ openaiProvider = createOpenAI({ apiKey });
291
+ }
292
+ // OpenAI returns EmbeddingModelV2 but is compatible with ai SDK
293
+ return openaiProvider.textEmbeddingModel(modelName) as unknown as EmbeddingModel<string>;
294
+ }
295
+
296
+ case 'openrouter': {
297
+ // OpenRouter uses OpenAI-compatible API with custom baseURL
298
+ if (!openrouterProvider) {
299
+ openrouterProvider = createOpenAI({
300
+ baseURL: 'https://openrouter.ai/api/v1',
301
+ apiKey,
302
+ });
303
+ }
304
+ // OpenRouter embedding models use the same interface as OpenAI
305
+ return openrouterProvider.textEmbeddingModel(modelName) as unknown as EmbeddingModel<string>;
306
+ }
307
+
308
+ case 'cohere': {
309
+ if (!cohereProvider) {
310
+ throw new Error(
311
+ 'Cohere provider requires manual initialization. ' +
312
+ 'Import createCohere from @ai-sdk/cohere and set cohereProvider before use.'
313
+ );
314
+ }
315
+ return cohereProvider.textEmbedding(modelName);
316
+ }
317
+
318
+ default:
319
+ throw new Error(`Unsupported provider: ${provider}`);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Generate embedding for a single text
325
+ */
326
+ async embedText(options: EmbeddingOptions): Promise<EmbeddingResult> {
327
+ const startTime = Date.now();
328
+
329
+ // Validate input
330
+ if (!options.text || typeof options.text !== 'string' || options.text.trim().length === 0) {
331
+ throw new Error('Invalid or empty text provided for embedding');
332
+ }
333
+
334
+ await this.checkRateLimit();
335
+
336
+ try {
337
+ // OpenRouter uses native fetch API for better compatibility
338
+ if (this.provider === 'openrouter') {
339
+ return await this.embedTextOpenRouter(options.text, startTime);
340
+ }
341
+
342
+ // Other providers use AI SDK
343
+ if (!this.embeddingModel) {
344
+ throw new Error('Embedding model not initialized');
345
+ }
346
+
347
+ const result = await embed({
348
+ model: this.embeddingModel,
349
+ value: options.text,
350
+ ...this.getProviderOptions(options),
351
+ });
352
+
353
+ this.requestCount++;
354
+
355
+ return {
356
+ vector: result.embedding,
357
+ dimension: result.embedding.length,
358
+ model: this.modelName,
359
+ processingTime: Date.now() - startTime,
360
+ tokenCount: result.usage?.tokens,
361
+ };
362
+ } catch (error) {
363
+ throw new Error(
364
+ `Embedding generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`
365
+ );
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Generate embedding using OpenRouter SDK
371
+ * Uses official @openrouter/sdk for embeddings
372
+ * Passes dimensions parameter to truncate output vectors
373
+ */
374
+ private async embedTextOpenRouter(text: string, startTime: number): Promise<EmbeddingResult> {
375
+ if (!this.openRouterClient) {
376
+ throw new Error('OpenRouter client not initialized');
377
+ }
378
+
379
+ // Build request with optional dimensions parameter
380
+ const request: any = {
381
+ model: this.modelName,
382
+ input: text
383
+ };
384
+
385
+ // Add dimensions if configured (enables output truncation)
386
+ if (this.dimensions && this.dimensions < 3072) {
387
+ request.dimensions = this.dimensions;
388
+ }
389
+
390
+ const result = await this.openRouterClient.embeddings.generate(request);
391
+
392
+ // Handle union type - result can be string or object
393
+ if (typeof result === 'string') {
394
+ throw new Error('Unexpected string response from OpenRouter embeddings API');
395
+ }
396
+
397
+ const data = (result as any).data;
398
+ if (!data || !data[0] || !data[0].embedding) {
399
+ throw new Error('Invalid response from OpenRouter embeddings API');
400
+ }
401
+
402
+ this.requestCount++;
403
+
404
+ // Handle embedding which can be string or number[]
405
+ const embedding = data[0].embedding;
406
+ const vector = typeof embedding === 'string'
407
+ ? JSON.parse(embedding) as number[]
408
+ : embedding as number[];
409
+
410
+ const usage = (result as any).usage;
411
+
412
+ return {
413
+ vector,
414
+ dimension: vector.length,
415
+ model: this.modelName,
416
+ processingTime: Date.now() - startTime,
417
+ tokenCount: usage?.totalTokens
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Generate embeddings for multiple texts (batched)
423
+ */
424
+ async embedBatch(
425
+ texts: string[],
426
+ options: Omit<EmbeddingOptions, 'text'> = {}
427
+ ): Promise<BatchEmbeddingResult> {
428
+ const startTime = Date.now();
429
+ let successCount = 0;
430
+ let failedCount = 0;
431
+
432
+ try {
433
+ await this.checkRateLimit();
434
+
435
+ // OpenRouter uses native fetch API for better compatibility
436
+ if (this.provider === 'openrouter') {
437
+ return await this.embedBatchOpenRouter(texts, startTime);
438
+ }
439
+
440
+ // Other providers use AI SDK
441
+ if (!this.embeddingModel) {
442
+ throw new Error('Embedding model not initialized');
443
+ }
444
+
445
+ const result = await embedMany({
446
+ model: this.embeddingModel,
447
+ values: texts,
448
+ ...this.getProviderOptions(options as EmbeddingOptions),
449
+ });
450
+
451
+ successCount = result.embeddings.length;
452
+ const totalTime = Date.now() - startTime;
453
+
454
+ return {
455
+ vectors: result.embeddings,
456
+ dimension: result.embeddings[0]?.length || this.dimensions,
457
+ model: this.modelName,
458
+ totalProcessingTime: totalTime,
459
+ averageProcessingTime: totalTime / texts.length,
460
+ successCount,
461
+ failedCount,
462
+ };
463
+ } catch (error) {
464
+ throw new Error(
465
+ `Batch embedding failed: ${error instanceof Error ? error.message : 'Unknown error'}`
466
+ );
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Generate batch embeddings using OpenRouter SDK
472
+ * Passes dimensions parameter to truncate output vectors
473
+ */
474
+ private async embedBatchOpenRouter(texts: string[], startTime: number): Promise<BatchEmbeddingResult> {
475
+ if (!this.openRouterClient) {
476
+ throw new Error('OpenRouter client not initialized');
477
+ }
478
+
479
+ // Build request with optional dimensions parameter
480
+ const request: any = {
481
+ model: this.modelName,
482
+ input: texts
483
+ };
484
+
485
+ // Add dimensions if configured (enables output truncation)
486
+ if (this.dimensions && this.dimensions < 3072) {
487
+ request.dimensions = this.dimensions;
488
+ }
489
+
490
+ const result = await this.openRouterClient.embeddings.generate(request);
491
+
492
+ // Handle union type - result can be string or object
493
+ if (typeof result === 'string') {
494
+ throw new Error('Unexpected string response from OpenRouter embeddings API');
495
+ }
496
+
497
+ const data = (result as any).data;
498
+ if (!data || !Array.isArray(data)) {
499
+ throw new Error('Invalid response from OpenRouter embeddings API');
500
+ }
501
+
502
+ // Sort by index to ensure correct order
503
+ const sortedData = [...data].sort((a: any, b: any) => (a.index || 0) - (b.index || 0));
504
+ const vectors = sortedData.map((item: any) => {
505
+ const embedding = item.embedding;
506
+ return typeof embedding === 'string'
507
+ ? JSON.parse(embedding) as number[]
508
+ : embedding as number[];
509
+ });
510
+
511
+ this.requestCount++;
512
+ const totalTime = Date.now() - startTime;
513
+
514
+ return {
515
+ vectors,
516
+ dimension: vectors[0]?.length || this.dimensions,
517
+ model: this.modelName,
518
+ totalProcessingTime: totalTime,
519
+ averageProcessingTime: totalTime / texts.length,
520
+ successCount: vectors.length,
521
+ failedCount: texts.length - vectors.length
522
+ };
523
+ }
524
+
525
+ /**
526
+ * Get provider-specific options
527
+ */
528
+ private getProviderOptions(options: EmbeddingOptions): any {
529
+ const providerOpts: any = {};
530
+
531
+ if (this.provider === 'google') {
532
+ providerOpts.providerOptions = {
533
+ google: {
534
+ outputDimensionality: this.dimensions,
535
+ taskType: this.getGoogleTaskType(options.type),
536
+ },
537
+ };
538
+ } else if (this.provider === 'openai') {
539
+ providerOpts.providerOptions = {
540
+ openai: {
541
+ dimensions: this.dimensions,
542
+ },
543
+ };
544
+ } else if (this.provider === 'openrouter') {
545
+ // OpenRouter uses OpenAI-compatible options
546
+ // Note: dimensions may not be supported for all models via OpenRouter
547
+ providerOpts.providerOptions = {
548
+ openai: {
549
+ dimensions: this.dimensions,
550
+ },
551
+ };
552
+ } else if (this.provider === 'cohere') {
553
+ providerOpts.providerOptions = {
554
+ cohere: {
555
+ inputType: this.getCohereInputType(options.type),
556
+ },
557
+ };
558
+ }
559
+
560
+ return providerOpts;
561
+ }
562
+
563
+ /**
564
+ * Map PDW type to Google task type
565
+ */
566
+ private getGoogleTaskType(type?: string): string {
567
+ switch (type) {
568
+ case 'query':
569
+ return 'RETRIEVAL_QUERY';
570
+ case 'content':
571
+ return 'RETRIEVAL_DOCUMENT';
572
+ case 'metadata':
573
+ return 'SEMANTIC_SIMILARITY';
574
+ default:
575
+ return 'RETRIEVAL_DOCUMENT';
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Map PDW type to Cohere input type
581
+ */
582
+ private getCohereInputType(type?: string): string {
583
+ switch (type) {
584
+ case 'query':
585
+ return 'search_query';
586
+ case 'content':
587
+ return 'search_document';
588
+ default:
589
+ return 'search_document';
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Calculate cosine similarity between two vectors
595
+ */
596
+ calculateCosineSimilarity(vectorA: number[], vectorB: number[]): number {
597
+ if (vectorA.length !== vectorB.length) {
598
+ throw new Error(`Vector dimension mismatch: ${vectorA.length} vs ${vectorB.length}`);
599
+ }
600
+
601
+ let dotProduct = 0;
602
+ let normA = 0;
603
+ let normB = 0;
604
+
605
+ for (let i = 0; i < vectorA.length; i++) {
606
+ dotProduct += vectorA[i] * vectorB[i];
607
+ normA += vectorA[i] * vectorA[i];
608
+ normB += vectorB[i] * vectorB[i];
609
+ }
610
+
611
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
612
+
613
+ if (magnitude === 0) {
614
+ return 0;
615
+ }
616
+
617
+ return dotProduct / magnitude;
618
+ }
619
+
620
+ /**
621
+ * Calculate Euclidean distance between two vectors
622
+ */
623
+ calculateEuclideanDistance(vectorA: number[], vectorB: number[]): number {
624
+ if (vectorA.length !== vectorB.length) {
625
+ throw new Error(`Vector dimension mismatch: ${vectorA.length} vs ${vectorB.length}`);
626
+ }
627
+
628
+ let sum = 0;
629
+ for (let i = 0; i < vectorA.length; i++) {
630
+ const diff = vectorA[i] - vectorB[i];
631
+ sum += diff * diff;
632
+ }
633
+
634
+ return Math.sqrt(sum);
635
+ }
636
+
637
+ /**
638
+ * Normalize a vector to unit length
639
+ */
640
+ normalizeVector(vector: number[]): number[] {
641
+ const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
642
+
643
+ if (magnitude === 0) {
644
+ return vector;
645
+ }
646
+
647
+ return vector.map(val => val / magnitude);
648
+ }
649
+
650
+ /**
651
+ * Find the most similar vectors to a query vector
652
+ */
653
+ findMostSimilar(
654
+ queryVector: number[],
655
+ candidateVectors: number[][],
656
+ k: number = 5
657
+ ): Array<{ index: number; similarity: number; distance: number }> {
658
+ const similarities = candidateVectors.map((vector, index) => {
659
+ const similarity = this.calculateCosineSimilarity(queryVector, vector);
660
+ const distance = this.calculateEuclideanDistance(queryVector, vector);
661
+
662
+ return { index, similarity, distance };
663
+ });
664
+
665
+ similarities.sort((a, b) => b.similarity - a.similarity);
666
+
667
+ return similarities.slice(0, k);
668
+ }
669
+
670
+ /**
671
+ * Get embedding statistics
672
+ */
673
+ getStats(): {
674
+ totalRequests: number;
675
+ requestsThisMinute: number;
676
+ model: string;
677
+ dimensions: number;
678
+ rateLimit: number;
679
+ provider: string;
680
+ } {
681
+ const now = Date.now();
682
+ const requestsThisMinute = (now - this.lastReset) < 60000 ? this.requestCount : 0;
683
+
684
+ return {
685
+ totalRequests: this.requestCount,
686
+ requestsThisMinute,
687
+ model: this.modelName,
688
+ dimensions: this.dimensions,
689
+ rateLimit: this.maxRequestsPerMinute,
690
+ provider: this.provider,
691
+ };
692
+ }
693
+
694
+ /**
695
+ * Reset rate limiting counters
696
+ */
697
+ private resetRateLimit(): void {
698
+ const now = Date.now();
699
+ if (now - this.lastReset >= 60000) {
700
+ this.requestCount = 0;
701
+ this.lastReset = now;
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Check rate limiting and wait if necessary
707
+ */
708
+ private async checkRateLimit(): Promise<void> {
709
+ this.resetRateLimit();
710
+
711
+ if (this.requestCount >= this.maxRequestsPerMinute) {
712
+ const waitTime = 60000 - (Date.now() - this.lastReset);
713
+ if (waitTime > 0) {
714
+ if (process.env.NODE_ENV === 'development') {
715
+ console.warn(`Rate limit reached, waiting ${waitTime}ms`);
716
+ }
717
+ await this.delay(waitTime);
718
+ this.resetRateLimit();
719
+ }
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Utility delay function
725
+ */
726
+ private delay(ms: number): Promise<void> {
727
+ return new Promise(resolve => setTimeout(resolve, ms));
728
+ }
729
+ }
730
+
731
+ export default EmbeddingService;
732
+
733
+ // ==================== Singleton Pattern ====================
734
+
735
+ /**
736
+ * Generate config key for singleton cache
737
+ */
738
+ function getConfigKey(config: EmbeddingConfig): string {
739
+ const provider = config.provider || 'google';
740
+ const modelName = typeof config.model === 'string'
741
+ ? config.model
742
+ : (config.modelName || 'default');
743
+ const dimensions = config.dimensions || 'default';
744
+ return `${provider}:${modelName}:${dimensions}`;
745
+ }
746
+
747
+ /** Singleton cache */
748
+ const sharedInstances = new Map<string, EmbeddingService>();
749
+
750
+ /**
751
+ * Get or create a shared EmbeddingService instance (Singleton)
752
+ *
753
+ * All clients with same provider/model/dimensions share one instance.
754
+ * Reduces memory usage and connection overhead.
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * // Instead of: new EmbeddingService({ apiKey, modelName })
759
+ * const embedding = getSharedEmbeddingService({ apiKey, modelName });
760
+ * ```
761
+ */
762
+ export function getSharedEmbeddingService(config: EmbeddingConfig): EmbeddingService {
763
+ const key = getConfigKey(config);
764
+
765
+ let instance = sharedInstances.get(key);
766
+ if (!instance) {
767
+ console.log(`🔧 [Singleton] Creating shared EmbeddingService: ${key}`);
768
+ instance = new EmbeddingService(config);
769
+ sharedInstances.set(key, instance);
770
+ }
771
+
772
+ return instance;
773
+ }
774
+
775
+ /**
776
+ * Clear all shared instances (for testing)
777
+ */
778
+ export function clearSharedEmbeddingServices(): void {
779
+ sharedInstances.clear();
780
+ }
781
+
782
+ /**
783
+ * Get singleton stats
784
+ */
785
+ export function getSharedEmbeddingStats(): {
786
+ instanceCount: number;
787
+ instances: Array<{ key: string; stats: ReturnType<EmbeddingService['getStats']> }>;
788
+ } {
789
+ return {
790
+ instanceCount: sharedInstances.size,
791
+ instances: Array.from(sharedInstances.entries()).map(([key, svc]) => ({
792
+ key,
793
+ stats: svc.getStats(),
794
+ })),
795
+ };
796
+ }