@mhalder/qdrant-mcp-server 1.1.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 (100) hide show
  1. package/.env.example +92 -0
  2. package/.github/workflows/ci.yml +61 -0
  3. package/.github/workflows/claude-code-review.yml +57 -0
  4. package/.github/workflows/claude.yml +50 -0
  5. package/.github/workflows/release.yml +52 -0
  6. package/.husky/commit-msg +1 -0
  7. package/.husky/pre-commit +1 -0
  8. package/.releaserc.json +59 -0
  9. package/.yamlfmt +4 -0
  10. package/CHANGELOG.md +73 -0
  11. package/CONTRIBUTING.md +176 -0
  12. package/LICENSE +21 -0
  13. package/README.md +714 -0
  14. package/build/embeddings/base.d.ts +23 -0
  15. package/build/embeddings/base.d.ts.map +1 -0
  16. package/build/embeddings/base.js +2 -0
  17. package/build/embeddings/base.js.map +1 -0
  18. package/build/embeddings/cohere.d.ts +17 -0
  19. package/build/embeddings/cohere.d.ts.map +1 -0
  20. package/build/embeddings/cohere.js +102 -0
  21. package/build/embeddings/cohere.js.map +1 -0
  22. package/build/embeddings/cohere.test.d.ts +2 -0
  23. package/build/embeddings/cohere.test.d.ts.map +1 -0
  24. package/build/embeddings/cohere.test.js +279 -0
  25. package/build/embeddings/cohere.test.js.map +1 -0
  26. package/build/embeddings/factory.d.ts +10 -0
  27. package/build/embeddings/factory.d.ts.map +1 -0
  28. package/build/embeddings/factory.js +98 -0
  29. package/build/embeddings/factory.js.map +1 -0
  30. package/build/embeddings/factory.test.d.ts +2 -0
  31. package/build/embeddings/factory.test.d.ts.map +1 -0
  32. package/build/embeddings/factory.test.js +329 -0
  33. package/build/embeddings/factory.test.js.map +1 -0
  34. package/build/embeddings/ollama.d.ts +18 -0
  35. package/build/embeddings/ollama.d.ts.map +1 -0
  36. package/build/embeddings/ollama.js +135 -0
  37. package/build/embeddings/ollama.js.map +1 -0
  38. package/build/embeddings/ollama.test.d.ts +2 -0
  39. package/build/embeddings/ollama.test.d.ts.map +1 -0
  40. package/build/embeddings/ollama.test.js +399 -0
  41. package/build/embeddings/ollama.test.js.map +1 -0
  42. package/build/embeddings/openai.d.ts +16 -0
  43. package/build/embeddings/openai.d.ts.map +1 -0
  44. package/build/embeddings/openai.js +108 -0
  45. package/build/embeddings/openai.js.map +1 -0
  46. package/build/embeddings/openai.test.d.ts +2 -0
  47. package/build/embeddings/openai.test.d.ts.map +1 -0
  48. package/build/embeddings/openai.test.js +283 -0
  49. package/build/embeddings/openai.test.js.map +1 -0
  50. package/build/embeddings/voyage.d.ts +19 -0
  51. package/build/embeddings/voyage.d.ts.map +1 -0
  52. package/build/embeddings/voyage.js +113 -0
  53. package/build/embeddings/voyage.js.map +1 -0
  54. package/build/embeddings/voyage.test.d.ts +2 -0
  55. package/build/embeddings/voyage.test.d.ts.map +1 -0
  56. package/build/embeddings/voyage.test.js +371 -0
  57. package/build/embeddings/voyage.test.js.map +1 -0
  58. package/build/index.d.ts +3 -0
  59. package/build/index.d.ts.map +1 -0
  60. package/build/index.js +534 -0
  61. package/build/index.js.map +1 -0
  62. package/build/index.test.d.ts +2 -0
  63. package/build/index.test.d.ts.map +1 -0
  64. package/build/index.test.js +241 -0
  65. package/build/index.test.js.map +1 -0
  66. package/build/qdrant/client.d.ts +37 -0
  67. package/build/qdrant/client.d.ts.map +1 -0
  68. package/build/qdrant/client.js +142 -0
  69. package/build/qdrant/client.js.map +1 -0
  70. package/build/qdrant/client.test.d.ts +2 -0
  71. package/build/qdrant/client.test.d.ts.map +1 -0
  72. package/build/qdrant/client.test.js +340 -0
  73. package/build/qdrant/client.test.js.map +1 -0
  74. package/commitlint.config.js +25 -0
  75. package/docker-compose.yml +22 -0
  76. package/docs/test_report.md +259 -0
  77. package/examples/README.md +315 -0
  78. package/examples/basic/README.md +111 -0
  79. package/examples/filters/README.md +262 -0
  80. package/examples/knowledge-base/README.md +207 -0
  81. package/examples/rate-limiting/README.md +376 -0
  82. package/package.json +59 -0
  83. package/scripts/verify-providers.js +238 -0
  84. package/src/embeddings/base.ts +25 -0
  85. package/src/embeddings/cohere.test.ts +408 -0
  86. package/src/embeddings/cohere.ts +152 -0
  87. package/src/embeddings/factory.test.ts +453 -0
  88. package/src/embeddings/factory.ts +163 -0
  89. package/src/embeddings/ollama.test.ts +543 -0
  90. package/src/embeddings/ollama.ts +196 -0
  91. package/src/embeddings/openai.test.ts +402 -0
  92. package/src/embeddings/openai.ts +158 -0
  93. package/src/embeddings/voyage.test.ts +520 -0
  94. package/src/embeddings/voyage.ts +168 -0
  95. package/src/index.test.ts +304 -0
  96. package/src/index.ts +614 -0
  97. package/src/qdrant/client.test.ts +456 -0
  98. package/src/qdrant/client.ts +195 -0
  99. package/tsconfig.json +19 -0
  100. package/vitest.config.ts +37 -0
@@ -0,0 +1,152 @@
1
+ import { CohereClient } from "cohere-ai";
2
+ import Bottleneck from "bottleneck";
3
+ import { EmbeddingProvider, EmbeddingResult, RateLimitConfig } from "./base.js";
4
+
5
+ interface CohereError {
6
+ status?: number;
7
+ statusCode?: number;
8
+ message?: string;
9
+ }
10
+
11
+ export class CohereEmbeddings implements EmbeddingProvider {
12
+ private client: CohereClient;
13
+ private model: string;
14
+ private dimensions: number;
15
+ private limiter: Bottleneck;
16
+ private retryAttempts: number;
17
+ private retryDelayMs: number;
18
+ private inputType:
19
+ | "search_document"
20
+ | "search_query"
21
+ | "classification"
22
+ | "clustering";
23
+
24
+ constructor(
25
+ apiKey: string,
26
+ model: string = "embed-english-v3.0",
27
+ dimensions?: number,
28
+ rateLimitConfig?: RateLimitConfig,
29
+ inputType:
30
+ | "search_document"
31
+ | "search_query"
32
+ | "classification"
33
+ | "clustering" = "search_document",
34
+ ) {
35
+ this.client = new CohereClient({ token: apiKey });
36
+ this.model = model;
37
+ this.inputType = inputType;
38
+
39
+ // Default dimensions for different models
40
+ const defaultDimensions: Record<string, number> = {
41
+ "embed-english-v3.0": 1024,
42
+ "embed-multilingual-v3.0": 1024,
43
+ "embed-english-light-v3.0": 384,
44
+ "embed-multilingual-light-v3.0": 384,
45
+ };
46
+
47
+ this.dimensions = dimensions || defaultDimensions[model] || 1024;
48
+
49
+ // Rate limiting configuration
50
+ const maxRequestsPerMinute = rateLimitConfig?.maxRequestsPerMinute || 100;
51
+ this.retryAttempts = rateLimitConfig?.retryAttempts || 3;
52
+ this.retryDelayMs = rateLimitConfig?.retryDelayMs || 1000;
53
+
54
+ this.limiter = new Bottleneck({
55
+ reservoir: maxRequestsPerMinute,
56
+ reservoirRefreshAmount: maxRequestsPerMinute,
57
+ reservoirRefreshInterval: 60 * 1000,
58
+ maxConcurrent: 5,
59
+ minTime: Math.floor((60 * 1000) / maxRequestsPerMinute),
60
+ });
61
+ }
62
+
63
+ private async retryWithBackoff<T>(
64
+ fn: () => Promise<T>,
65
+ attempt: number = 0,
66
+ ): Promise<T> {
67
+ try {
68
+ return await fn();
69
+ } catch (error: unknown) {
70
+ const apiError = error as CohereError;
71
+ const isRateLimitError =
72
+ apiError?.status === 429 ||
73
+ apiError?.statusCode === 429 ||
74
+ apiError?.message?.toLowerCase().includes("rate limit");
75
+
76
+ if (isRateLimitError && attempt < this.retryAttempts) {
77
+ const delayMs = this.retryDelayMs * Math.pow(2, attempt);
78
+ const waitTimeSeconds = (delayMs / 1000).toFixed(1);
79
+ console.error(
80
+ `Rate limit reached. Retrying in ${waitTimeSeconds}s (attempt ${attempt + 1}/${this.retryAttempts})...`,
81
+ );
82
+
83
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
84
+ return this.retryWithBackoff(fn, attempt + 1);
85
+ }
86
+
87
+ if (isRateLimitError) {
88
+ throw new Error(
89
+ `Cohere API rate limit exceeded after ${this.retryAttempts} retry attempts. Please try again later or reduce request frequency.`,
90
+ );
91
+ }
92
+
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ async embed(text: string): Promise<EmbeddingResult> {
98
+ return this.limiter.schedule(() =>
99
+ this.retryWithBackoff(async () => {
100
+ const response = await this.client.embed({
101
+ texts: [text],
102
+ model: this.model,
103
+ inputType: this.inputType,
104
+ embeddingTypes: ["float"],
105
+ });
106
+
107
+ // Cohere v7+ returns embeddings as number[][]
108
+ const embeddings = response.embeddings as number[][];
109
+ if (!embeddings || embeddings.length === 0) {
110
+ throw new Error("No embedding returned from Cohere API");
111
+ }
112
+
113
+ return {
114
+ embedding: embeddings[0],
115
+ dimensions: this.dimensions,
116
+ };
117
+ }),
118
+ );
119
+ }
120
+
121
+ async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
122
+ return this.limiter.schedule(() =>
123
+ this.retryWithBackoff(async () => {
124
+ const response = await this.client.embed({
125
+ texts,
126
+ model: this.model,
127
+ inputType: this.inputType,
128
+ embeddingTypes: ["float"],
129
+ });
130
+
131
+ // Cohere v7+ returns embeddings as number[][]
132
+ const embeddings = response.embeddings as number[][];
133
+ if (!embeddings) {
134
+ throw new Error("No embeddings returned from Cohere API");
135
+ }
136
+
137
+ return embeddings.map((embedding: number[]) => ({
138
+ embedding,
139
+ dimensions: this.dimensions,
140
+ }));
141
+ }),
142
+ );
143
+ }
144
+
145
+ getDimensions(): number {
146
+ return this.dimensions;
147
+ }
148
+
149
+ getModel(): string {
150
+ return this.model;
151
+ }
152
+ }
@@ -0,0 +1,453 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { EmbeddingProviderFactory, type FactoryConfig } from "./factory.js";
3
+ import { OpenAIEmbeddings } from "./openai.js";
4
+ import { CohereEmbeddings } from "./cohere.js";
5
+ import { VoyageEmbeddings } from "./voyage.js";
6
+ import { OllamaEmbeddings } from "./ollama.js";
7
+
8
+ describe("EmbeddingProviderFactory", () => {
9
+ let originalEnv: NodeJS.ProcessEnv;
10
+
11
+ beforeEach(() => {
12
+ // Save original environment
13
+ originalEnv = { ...process.env };
14
+ });
15
+
16
+ afterEach(() => {
17
+ // Restore original environment
18
+ process.env = originalEnv;
19
+ });
20
+
21
+ describe("create", () => {
22
+ describe("Unknown provider", () => {
23
+ it("should throw error for unknown provider", () => {
24
+ expect(() =>
25
+ EmbeddingProviderFactory.create({
26
+ provider: "unknown" as any,
27
+ }),
28
+ ).toThrow("Unknown embedding provider: unknown");
29
+ });
30
+
31
+ it("should list supported providers in error message", () => {
32
+ expect(() =>
33
+ EmbeddingProviderFactory.create({
34
+ provider: "invalid" as any,
35
+ }),
36
+ ).toThrow("openai, cohere, voyage, ollama");
37
+ });
38
+ });
39
+
40
+ describe("OpenAI provider", () => {
41
+ it("should throw error if API key is missing", () => {
42
+ expect(() =>
43
+ EmbeddingProviderFactory.create({
44
+ provider: "openai",
45
+ }),
46
+ ).toThrow("API key is required for OpenAI provider");
47
+ });
48
+
49
+ it("should create OpenAI provider with API key", () => {
50
+ const provider = EmbeddingProviderFactory.create({
51
+ provider: "openai",
52
+ apiKey: "test-key",
53
+ });
54
+
55
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
56
+ expect(provider.getModel()).toBe("text-embedding-3-small");
57
+ expect(provider.getDimensions()).toBe(1536);
58
+ });
59
+
60
+ it("should use custom model", () => {
61
+ const provider = EmbeddingProviderFactory.create({
62
+ provider: "openai",
63
+ apiKey: "test-key",
64
+ model: "text-embedding-3-large",
65
+ });
66
+
67
+ expect(provider.getModel()).toBe("text-embedding-3-large");
68
+ expect(provider.getDimensions()).toBe(3072);
69
+ });
70
+
71
+ it("should use custom dimensions", () => {
72
+ const provider = EmbeddingProviderFactory.create({
73
+ provider: "openai",
74
+ apiKey: "test-key",
75
+ dimensions: 512,
76
+ });
77
+
78
+ expect(provider.getDimensions()).toBe(512);
79
+ });
80
+
81
+ it("should pass rate limit config", () => {
82
+ const provider = EmbeddingProviderFactory.create({
83
+ provider: "openai",
84
+ apiKey: "test-key",
85
+ rateLimitConfig: {
86
+ maxRequestsPerMinute: 1000,
87
+ retryAttempts: 5,
88
+ retryDelayMs: 2000,
89
+ },
90
+ });
91
+
92
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
93
+ });
94
+ });
95
+
96
+ describe("Cohere provider", () => {
97
+ it("should throw error if API key is missing", () => {
98
+ expect(() =>
99
+ EmbeddingProviderFactory.create({
100
+ provider: "cohere",
101
+ }),
102
+ ).toThrow("API key is required for Cohere provider");
103
+ });
104
+
105
+ it("should create Cohere provider with API key", () => {
106
+ const provider = EmbeddingProviderFactory.create({
107
+ provider: "cohere",
108
+ apiKey: "test-key",
109
+ });
110
+
111
+ expect(provider).toBeInstanceOf(CohereEmbeddings);
112
+ expect(provider.getModel()).toBe("embed-english-v3.0");
113
+ expect(provider.getDimensions()).toBe(1024);
114
+ });
115
+
116
+ it("should use custom model", () => {
117
+ const provider = EmbeddingProviderFactory.create({
118
+ provider: "cohere",
119
+ apiKey: "test-key",
120
+ model: "embed-multilingual-v3.0",
121
+ });
122
+
123
+ expect(provider.getModel()).toBe("embed-multilingual-v3.0");
124
+ });
125
+
126
+ it("should use custom dimensions", () => {
127
+ const provider = EmbeddingProviderFactory.create({
128
+ provider: "cohere",
129
+ apiKey: "test-key",
130
+ dimensions: 384,
131
+ });
132
+
133
+ expect(provider.getDimensions()).toBe(384);
134
+ });
135
+ });
136
+
137
+ describe("Voyage provider", () => {
138
+ it("should throw error if API key is missing", () => {
139
+ expect(() =>
140
+ EmbeddingProviderFactory.create({
141
+ provider: "voyage",
142
+ }),
143
+ ).toThrow("API key is required for Voyage AI provider");
144
+ });
145
+
146
+ it("should create Voyage provider with API key", () => {
147
+ const provider = EmbeddingProviderFactory.create({
148
+ provider: "voyage",
149
+ apiKey: "test-key",
150
+ });
151
+
152
+ expect(provider).toBeInstanceOf(VoyageEmbeddings);
153
+ expect(provider.getModel()).toBe("voyage-2");
154
+ expect(provider.getDimensions()).toBe(1024);
155
+ });
156
+
157
+ it("should use custom model", () => {
158
+ const provider = EmbeddingProviderFactory.create({
159
+ provider: "voyage",
160
+ apiKey: "test-key",
161
+ model: "voyage-large-2",
162
+ });
163
+
164
+ expect(provider.getModel()).toBe("voyage-large-2");
165
+ expect(provider.getDimensions()).toBe(1536);
166
+ });
167
+
168
+ it("should use default base URL", () => {
169
+ const provider = EmbeddingProviderFactory.create({
170
+ provider: "voyage",
171
+ apiKey: "test-key",
172
+ });
173
+
174
+ expect(provider).toBeInstanceOf(VoyageEmbeddings);
175
+ });
176
+
177
+ it("should use custom base URL", () => {
178
+ const provider = EmbeddingProviderFactory.create({
179
+ provider: "voyage",
180
+ apiKey: "test-key",
181
+ baseUrl: "https://custom.voyageai.com/v1",
182
+ });
183
+
184
+ expect(provider).toBeInstanceOf(VoyageEmbeddings);
185
+ });
186
+ });
187
+
188
+ describe("Ollama provider", () => {
189
+ it("should not require API key", () => {
190
+ const provider = EmbeddingProviderFactory.create({
191
+ provider: "ollama",
192
+ });
193
+
194
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
195
+ expect(provider.getModel()).toBe("nomic-embed-text");
196
+ expect(provider.getDimensions()).toBe(768);
197
+ });
198
+
199
+ it("should use custom model", () => {
200
+ const provider = EmbeddingProviderFactory.create({
201
+ provider: "ollama",
202
+ model: "mxbai-embed-large",
203
+ });
204
+
205
+ expect(provider.getModel()).toBe("mxbai-embed-large");
206
+ expect(provider.getDimensions()).toBe(1024);
207
+ });
208
+
209
+ it("should use default base URL", () => {
210
+ const provider = EmbeddingProviderFactory.create({
211
+ provider: "ollama",
212
+ });
213
+
214
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
215
+ });
216
+
217
+ it("should use custom base URL", () => {
218
+ const provider = EmbeddingProviderFactory.create({
219
+ provider: "ollama",
220
+ baseUrl: "http://custom:11434",
221
+ });
222
+
223
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
224
+ });
225
+ });
226
+ });
227
+
228
+ describe("createFromEnv", () => {
229
+ it("should default to Ollama provider", () => {
230
+ const provider = EmbeddingProviderFactory.createFromEnv();
231
+
232
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
233
+ });
234
+
235
+ it("should create OpenAI provider from environment", () => {
236
+ process.env.EMBEDDING_PROVIDER = "openai";
237
+ process.env.OPENAI_API_KEY = "test-openai-key";
238
+
239
+ const provider = EmbeddingProviderFactory.createFromEnv();
240
+
241
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
242
+ });
243
+
244
+ it("should create Cohere provider from environment", () => {
245
+ process.env.EMBEDDING_PROVIDER = "cohere";
246
+ process.env.COHERE_API_KEY = "test-cohere-key";
247
+
248
+ const provider = EmbeddingProviderFactory.createFromEnv();
249
+
250
+ expect(provider).toBeInstanceOf(CohereEmbeddings);
251
+ });
252
+
253
+ it("should create Voyage provider from environment", () => {
254
+ process.env.EMBEDDING_PROVIDER = "voyage";
255
+ process.env.VOYAGE_API_KEY = "test-voyage-key";
256
+
257
+ const provider = EmbeddingProviderFactory.createFromEnv();
258
+
259
+ expect(provider).toBeInstanceOf(VoyageEmbeddings);
260
+ });
261
+
262
+ it("should create Ollama provider from environment", () => {
263
+ process.env.EMBEDDING_PROVIDER = "ollama";
264
+
265
+ const provider = EmbeddingProviderFactory.createFromEnv();
266
+
267
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
268
+ });
269
+
270
+ it("should be case insensitive for provider name", () => {
271
+ process.env.EMBEDDING_PROVIDER = "OpenAI";
272
+ process.env.OPENAI_API_KEY = "test-key";
273
+
274
+ const provider = EmbeddingProviderFactory.createFromEnv();
275
+
276
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
277
+ });
278
+
279
+ it("should use custom model from environment", () => {
280
+ process.env.EMBEDDING_PROVIDER = "openai";
281
+ process.env.OPENAI_API_KEY = "test-key";
282
+ process.env.EMBEDDING_MODEL = "text-embedding-3-large";
283
+
284
+ const provider = EmbeddingProviderFactory.createFromEnv();
285
+
286
+ expect(provider.getModel()).toBe("text-embedding-3-large");
287
+ });
288
+
289
+ it("should use custom dimensions from environment", () => {
290
+ process.env.EMBEDDING_PROVIDER = "openai";
291
+ process.env.OPENAI_API_KEY = "test-key";
292
+ process.env.EMBEDDING_DIMENSIONS = "512";
293
+
294
+ const provider = EmbeddingProviderFactory.createFromEnv();
295
+
296
+ expect(provider.getDimensions()).toBe(512);
297
+ });
298
+
299
+ it("should use custom base URL from environment", () => {
300
+ process.env.EMBEDDING_PROVIDER = "voyage";
301
+ process.env.VOYAGE_API_KEY = "test-key";
302
+ process.env.EMBEDDING_BASE_URL = "https://custom.voyage.com";
303
+
304
+ const provider = EmbeddingProviderFactory.createFromEnv();
305
+
306
+ expect(provider).toBeInstanceOf(VoyageEmbeddings);
307
+ });
308
+
309
+ it("should use rate limit config from environment", () => {
310
+ process.env.EMBEDDING_PROVIDER = "openai";
311
+ process.env.OPENAI_API_KEY = "test-key";
312
+ process.env.EMBEDDING_MAX_REQUESTS_PER_MINUTE = "1000";
313
+ process.env.EMBEDDING_RETRY_ATTEMPTS = "5";
314
+ process.env.EMBEDDING_RETRY_DELAY = "2000";
315
+
316
+ const provider = EmbeddingProviderFactory.createFromEnv();
317
+
318
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
319
+ });
320
+
321
+ it("should select correct API key based on provider", () => {
322
+ process.env.EMBEDDING_PROVIDER = "cohere";
323
+ process.env.OPENAI_API_KEY = "openai-key";
324
+ process.env.COHERE_API_KEY = "cohere-key";
325
+ process.env.VOYAGE_API_KEY = "voyage-key";
326
+
327
+ const provider = EmbeddingProviderFactory.createFromEnv();
328
+
329
+ expect(provider).toBeInstanceOf(CohereEmbeddings);
330
+ });
331
+
332
+ it("should handle Ollama without API key", () => {
333
+ process.env.EMBEDDING_PROVIDER = "ollama";
334
+ process.env.OPENAI_API_KEY = "openai-key"; // Should not use this
335
+
336
+ const provider = EmbeddingProviderFactory.createFromEnv();
337
+
338
+ expect(provider).toBeInstanceOf(OllamaEmbeddings);
339
+ });
340
+
341
+ describe("Environment variable validation", () => {
342
+ it("should throw error for invalid EMBEDDING_DIMENSIONS (NaN)", () => {
343
+ process.env.EMBEDDING_PROVIDER = "openai";
344
+ process.env.OPENAI_API_KEY = "test-key";
345
+ process.env.EMBEDDING_DIMENSIONS = "not-a-number";
346
+
347
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
348
+ 'Invalid EMBEDDING_DIMENSIONS: must be a positive integer, got "not-a-number"',
349
+ );
350
+ });
351
+
352
+ it("should throw error for invalid EMBEDDING_DIMENSIONS (negative)", () => {
353
+ process.env.EMBEDDING_PROVIDER = "openai";
354
+ process.env.OPENAI_API_KEY = "test-key";
355
+ process.env.EMBEDDING_DIMENSIONS = "-100";
356
+
357
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
358
+ 'Invalid EMBEDDING_DIMENSIONS: must be a positive integer, got "-100"',
359
+ );
360
+ });
361
+
362
+ it("should throw error for invalid EMBEDDING_DIMENSIONS (zero)", () => {
363
+ process.env.EMBEDDING_PROVIDER = "openai";
364
+ process.env.OPENAI_API_KEY = "test-key";
365
+ process.env.EMBEDDING_DIMENSIONS = "0";
366
+
367
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
368
+ 'Invalid EMBEDDING_DIMENSIONS: must be a positive integer, got "0"',
369
+ );
370
+ });
371
+
372
+ it("should throw error for invalid EMBEDDING_MAX_REQUESTS_PER_MINUTE (NaN)", () => {
373
+ process.env.EMBEDDING_PROVIDER = "openai";
374
+ process.env.OPENAI_API_KEY = "test-key";
375
+ process.env.EMBEDDING_MAX_REQUESTS_PER_MINUTE = "invalid";
376
+
377
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
378
+ 'Invalid EMBEDDING_MAX_REQUESTS_PER_MINUTE: must be a positive integer, got "invalid"',
379
+ );
380
+ });
381
+
382
+ it("should throw error for invalid EMBEDDING_MAX_REQUESTS_PER_MINUTE (negative)", () => {
383
+ process.env.EMBEDDING_PROVIDER = "openai";
384
+ process.env.OPENAI_API_KEY = "test-key";
385
+ process.env.EMBEDDING_MAX_REQUESTS_PER_MINUTE = "-50";
386
+
387
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
388
+ 'Invalid EMBEDDING_MAX_REQUESTS_PER_MINUTE: must be a positive integer, got "-50"',
389
+ );
390
+ });
391
+
392
+ it("should throw error for invalid EMBEDDING_RETRY_ATTEMPTS (NaN)", () => {
393
+ process.env.EMBEDDING_PROVIDER = "openai";
394
+ process.env.OPENAI_API_KEY = "test-key";
395
+ process.env.EMBEDDING_RETRY_ATTEMPTS = "abc";
396
+
397
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
398
+ 'Invalid EMBEDDING_RETRY_ATTEMPTS: must be a non-negative integer, got "abc"',
399
+ );
400
+ });
401
+
402
+ it("should throw error for invalid EMBEDDING_RETRY_ATTEMPTS (negative)", () => {
403
+ process.env.EMBEDDING_PROVIDER = "openai";
404
+ process.env.OPENAI_API_KEY = "test-key";
405
+ process.env.EMBEDDING_RETRY_ATTEMPTS = "-5";
406
+
407
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
408
+ 'Invalid EMBEDDING_RETRY_ATTEMPTS: must be a non-negative integer, got "-5"',
409
+ );
410
+ });
411
+
412
+ it("should throw error for invalid EMBEDDING_RETRY_DELAY (NaN)", () => {
413
+ process.env.EMBEDDING_PROVIDER = "openai";
414
+ process.env.OPENAI_API_KEY = "test-key";
415
+ process.env.EMBEDDING_RETRY_DELAY = "xyz";
416
+
417
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
418
+ 'Invalid EMBEDDING_RETRY_DELAY: must be a non-negative integer, got "xyz"',
419
+ );
420
+ });
421
+
422
+ it("should throw error for invalid EMBEDDING_RETRY_DELAY (negative)", () => {
423
+ process.env.EMBEDDING_PROVIDER = "openai";
424
+ process.env.OPENAI_API_KEY = "test-key";
425
+ process.env.EMBEDDING_RETRY_DELAY = "-1000";
426
+
427
+ expect(() => EmbeddingProviderFactory.createFromEnv()).toThrow(
428
+ 'Invalid EMBEDDING_RETRY_DELAY: must be a non-negative integer, got "-1000"',
429
+ );
430
+ });
431
+
432
+ it("should accept valid EMBEDDING_RETRY_ATTEMPTS (zero)", () => {
433
+ process.env.EMBEDDING_PROVIDER = "openai";
434
+ process.env.OPENAI_API_KEY = "test-key";
435
+ process.env.EMBEDDING_RETRY_ATTEMPTS = "0";
436
+
437
+ const provider = EmbeddingProviderFactory.createFromEnv();
438
+
439
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
440
+ });
441
+
442
+ it("should accept valid EMBEDDING_RETRY_DELAY (zero)", () => {
443
+ process.env.EMBEDDING_PROVIDER = "openai";
444
+ process.env.OPENAI_API_KEY = "test-key";
445
+ process.env.EMBEDDING_RETRY_DELAY = "0";
446
+
447
+ const provider = EmbeddingProviderFactory.createFromEnv();
448
+
449
+ expect(provider).toBeInstanceOf(OpenAIEmbeddings);
450
+ });
451
+ });
452
+ });
453
+ });