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