@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,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Provider Verification Script
5
+ *
6
+ * This script verifies that all embedding providers can be instantiated
7
+ * correctly and that the factory pattern works as expected.
8
+ */
9
+
10
+ import { EmbeddingProviderFactory } from "../build/embeddings/factory.js";
11
+
12
+ console.log("=".repeat(60));
13
+ console.log("QDRANT MCP SERVER - PROVIDER VERIFICATION");
14
+ console.log("=".repeat(60));
15
+ console.log();
16
+
17
+ const results = {
18
+ passed: 0,
19
+ failed: 0,
20
+ tests: [],
21
+ };
22
+
23
+ function test(name, fn) {
24
+ try {
25
+ fn();
26
+ console.log(`✅ PASS: ${name}`);
27
+ results.passed++;
28
+ results.tests.push({ name, status: "PASS" });
29
+ } catch (error) {
30
+ console.log(`❌ FAIL: ${name}`);
31
+ console.log(` Error: ${error.message}`);
32
+ results.failed++;
33
+ results.tests.push({ name, status: "FAIL", error: error.message });
34
+ }
35
+ }
36
+
37
+ console.log("Testing Provider Factory...\n");
38
+
39
+ // Test 1: Factory should reject unknown providers
40
+ test("Factory rejects unknown provider", () => {
41
+ try {
42
+ EmbeddingProviderFactory.create({
43
+ provider: "unknown-provider",
44
+ apiKey: "test-key",
45
+ });
46
+ throw new Error("Should have thrown error for unknown provider");
47
+ } catch (error) {
48
+ if (!error.message.includes("Unknown embedding provider")) {
49
+ throw error;
50
+ }
51
+ }
52
+ });
53
+
54
+ // Test 2: OpenAI provider requires API key
55
+ test("OpenAI provider requires API key", () => {
56
+ try {
57
+ EmbeddingProviderFactory.create({
58
+ provider: "openai",
59
+ });
60
+ throw new Error("Should have thrown error for missing API key");
61
+ } catch (error) {
62
+ if (!error.message.includes("API key is required")) {
63
+ throw error;
64
+ }
65
+ }
66
+ });
67
+
68
+ // Test 3: Cohere provider requires API key
69
+ test("Cohere provider requires API key", () => {
70
+ try {
71
+ EmbeddingProviderFactory.create({
72
+ provider: "cohere",
73
+ });
74
+ throw new Error("Should have thrown error for missing API key");
75
+ } catch (error) {
76
+ if (!error.message.includes("API key is required")) {
77
+ throw error;
78
+ }
79
+ }
80
+ });
81
+
82
+ // Test 4: Voyage AI provider requires API key
83
+ test("Voyage AI provider requires API key", () => {
84
+ try {
85
+ EmbeddingProviderFactory.create({
86
+ provider: "voyage",
87
+ });
88
+ throw new Error("Should have thrown error for missing API key");
89
+ } catch (error) {
90
+ if (!error.message.includes("API key is required")) {
91
+ throw error;
92
+ }
93
+ }
94
+ });
95
+
96
+ // Test 5: Ollama provider does NOT require API key
97
+ test("Ollama provider does not require API key", () => {
98
+ const provider = EmbeddingProviderFactory.create({
99
+ provider: "ollama",
100
+ });
101
+ if (!provider) {
102
+ throw new Error("Failed to create Ollama provider");
103
+ }
104
+ if (provider.getModel() !== "nomic-embed-text") {
105
+ throw new Error(
106
+ `Expected default model 'nomic-embed-text', got '${provider.getModel()}'`,
107
+ );
108
+ }
109
+ if (provider.getDimensions() !== 768) {
110
+ throw new Error(
111
+ `Expected default dimensions 768, got ${provider.getDimensions()}`,
112
+ );
113
+ }
114
+ });
115
+
116
+ // Test 6: OpenAI provider with valid config
117
+ test("OpenAI provider instantiates with API key", () => {
118
+ const provider = EmbeddingProviderFactory.create({
119
+ provider: "openai",
120
+ apiKey: "test-key-123",
121
+ });
122
+ if (!provider) {
123
+ throw new Error("Failed to create OpenAI provider");
124
+ }
125
+ if (provider.getModel() !== "text-embedding-3-small") {
126
+ throw new Error(
127
+ `Expected default model 'text-embedding-3-small', got '${provider.getModel()}'`,
128
+ );
129
+ }
130
+ if (provider.getDimensions() !== 1536) {
131
+ throw new Error(
132
+ `Expected default dimensions 1536, got ${provider.getDimensions()}`,
133
+ );
134
+ }
135
+ });
136
+
137
+ // Test 7: Cohere provider with valid config
138
+ test("Cohere provider instantiates with API key", () => {
139
+ const provider = EmbeddingProviderFactory.create({
140
+ provider: "cohere",
141
+ apiKey: "test-key-123",
142
+ });
143
+ if (!provider) {
144
+ throw new Error("Failed to create Cohere provider");
145
+ }
146
+ if (provider.getModel() !== "embed-english-v3.0") {
147
+ throw new Error(
148
+ `Expected default model 'embed-english-v3.0', got '${provider.getModel()}'`,
149
+ );
150
+ }
151
+ if (provider.getDimensions() !== 1024) {
152
+ throw new Error(
153
+ `Expected default dimensions 1024, got ${provider.getDimensions()}`,
154
+ );
155
+ }
156
+ });
157
+
158
+ // Test 8: Voyage AI provider with valid config
159
+ test("Voyage AI provider instantiates with API key", () => {
160
+ const provider = EmbeddingProviderFactory.create({
161
+ provider: "voyage",
162
+ apiKey: "test-key-123",
163
+ });
164
+ if (!provider) {
165
+ throw new Error("Failed to create Voyage AI provider");
166
+ }
167
+ if (provider.getModel() !== "voyage-2") {
168
+ throw new Error(
169
+ `Expected default model 'voyage-2', got '${provider.getModel()}'`,
170
+ );
171
+ }
172
+ if (provider.getDimensions() !== 1024) {
173
+ throw new Error(
174
+ `Expected default dimensions 1024, got ${provider.getDimensions()}`,
175
+ );
176
+ }
177
+ });
178
+
179
+ // Test 9: Custom model configuration
180
+ test("Custom model configuration works", () => {
181
+ const provider = EmbeddingProviderFactory.create({
182
+ provider: "openai",
183
+ apiKey: "test-key-123",
184
+ model: "text-embedding-3-large",
185
+ });
186
+ if (provider.getModel() !== "text-embedding-3-large") {
187
+ throw new Error(
188
+ `Expected model 'text-embedding-3-large', got '${provider.getModel()}'`,
189
+ );
190
+ }
191
+ if (provider.getDimensions() !== 3072) {
192
+ throw new Error(
193
+ `Expected dimensions 3072 for large model, got ${provider.getDimensions()}`,
194
+ );
195
+ }
196
+ });
197
+
198
+ // Test 10: Custom dimensions override
199
+ test("Custom dimensions override works", () => {
200
+ const provider = EmbeddingProviderFactory.create({
201
+ provider: "openai",
202
+ apiKey: "test-key-123",
203
+ dimensions: 512,
204
+ });
205
+ if (provider.getDimensions() !== 512) {
206
+ throw new Error(
207
+ `Expected custom dimensions 512, got ${provider.getDimensions()}`,
208
+ );
209
+ }
210
+ });
211
+
212
+ console.log();
213
+ console.log("=".repeat(60));
214
+ console.log("RESULTS");
215
+ console.log("=".repeat(60));
216
+ console.log(`Total Tests: ${results.passed + results.failed}`);
217
+ console.log(`Passed: ${results.passed} ✅`);
218
+ console.log(`Failed: ${results.failed} ${results.failed > 0 ? "❌" : ""}`);
219
+ console.log(
220
+ `Success Rate: ${Math.round((results.passed / (results.passed + results.failed)) * 100)}%`,
221
+ );
222
+ console.log("=".repeat(60));
223
+
224
+ if (results.failed > 0) {
225
+ console.log();
226
+ console.log("Failed Tests:");
227
+ results.tests
228
+ .filter((t) => t.status === "FAIL")
229
+ .forEach((t) => {
230
+ console.log(` - ${t.name}: ${t.error}`);
231
+ });
232
+ process.exit(1);
233
+ } else {
234
+ console.log();
235
+ console.log("✅ All provider instantiation tests passed!");
236
+ console.log("Multi-provider architecture is working correctly.");
237
+ process.exit(0);
238
+ }
@@ -0,0 +1,25 @@
1
+ export interface EmbeddingResult {
2
+ embedding: number[];
3
+ dimensions: number;
4
+ }
5
+
6
+ export interface RateLimitConfig {
7
+ maxRequestsPerMinute?: number;
8
+ retryAttempts?: number;
9
+ retryDelayMs?: number;
10
+ }
11
+
12
+ export interface EmbeddingProvider {
13
+ embed(text: string): Promise<EmbeddingResult>;
14
+ embedBatch(texts: string[]): Promise<EmbeddingResult[]>;
15
+ getDimensions(): number;
16
+ getModel(): string;
17
+ }
18
+
19
+ export interface ProviderConfig {
20
+ model?: string;
21
+ dimensions?: number;
22
+ rateLimitConfig?: RateLimitConfig;
23
+ apiKey?: string;
24
+ baseUrl?: string;
25
+ }
@@ -0,0 +1,408 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { CohereEmbeddings } from "./cohere.js";
3
+ import { CohereClient } from "cohere-ai";
4
+
5
+ vi.mock("cohere-ai", () => ({
6
+ CohereClient: vi.fn(),
7
+ }));
8
+
9
+ describe("CohereEmbeddings", () => {
10
+ let embeddings: CohereEmbeddings;
11
+ let mockClient: any;
12
+
13
+ beforeEach(() => {
14
+ mockClient = {
15
+ embed: vi.fn(),
16
+ };
17
+
18
+ vi.mocked(CohereClient).mockImplementation(() => mockClient as any);
19
+
20
+ embeddings = new CohereEmbeddings("test-api-key");
21
+ });
22
+
23
+ describe("constructor", () => {
24
+ it("should use default model and dimensions", () => {
25
+ expect(embeddings.getModel()).toBe("embed-english-v3.0");
26
+ expect(embeddings.getDimensions()).toBe(1024);
27
+ });
28
+
29
+ it("should use custom model", () => {
30
+ const customEmbeddings = new CohereEmbeddings(
31
+ "test-api-key",
32
+ "embed-multilingual-v3.0",
33
+ );
34
+ expect(customEmbeddings.getModel()).toBe("embed-multilingual-v3.0");
35
+ expect(customEmbeddings.getDimensions()).toBe(1024);
36
+ });
37
+
38
+ it("should use custom dimensions", () => {
39
+ const customEmbeddings = new CohereEmbeddings(
40
+ "test-api-key",
41
+ "embed-english-v3.0",
42
+ 512,
43
+ );
44
+ expect(customEmbeddings.getDimensions()).toBe(512);
45
+ });
46
+
47
+ it("should use default dimensions for light models", () => {
48
+ const lightEmbeddings = new CohereEmbeddings(
49
+ "test-api-key",
50
+ "embed-english-light-v3.0",
51
+ );
52
+ expect(lightEmbeddings.getDimensions()).toBe(384);
53
+ });
54
+
55
+ it("should default to 1024 for unknown models", () => {
56
+ const unknownEmbeddings = new CohereEmbeddings(
57
+ "test-api-key",
58
+ "custom-model",
59
+ );
60
+ expect(unknownEmbeddings.getDimensions()).toBe(1024);
61
+ });
62
+
63
+ it("should accept custom input type", () => {
64
+ const searchQueryEmbeddings = new CohereEmbeddings(
65
+ "test-api-key",
66
+ "embed-english-v3.0",
67
+ undefined,
68
+ undefined,
69
+ "search_query",
70
+ );
71
+ expect(searchQueryEmbeddings).toBeInstanceOf(CohereEmbeddings);
72
+ });
73
+ });
74
+
75
+ describe("embed", () => {
76
+ it("should generate embedding for single text", async () => {
77
+ const mockEmbedding = Array(1024)
78
+ .fill(0)
79
+ .map((_, i) => i * 0.001);
80
+ mockClient.embed.mockResolvedValue({
81
+ embeddings: [mockEmbedding],
82
+ });
83
+
84
+ const result = await embeddings.embed("test text");
85
+
86
+ expect(result).toEqual({
87
+ embedding: mockEmbedding,
88
+ dimensions: 1024,
89
+ });
90
+ expect(mockClient.embed).toHaveBeenCalledWith({
91
+ texts: ["test text"],
92
+ model: "embed-english-v3.0",
93
+ inputType: "search_document",
94
+ embeddingTypes: ["float"],
95
+ });
96
+ });
97
+
98
+ it("should handle long text", async () => {
99
+ const longText = "word ".repeat(1000);
100
+ const mockEmbedding = Array(1024).fill(0.5);
101
+ mockClient.embed.mockResolvedValue({
102
+ embeddings: [mockEmbedding],
103
+ });
104
+
105
+ const result = await embeddings.embed(longText);
106
+
107
+ expect(result.embedding).toEqual(mockEmbedding);
108
+ expect(mockClient.embed).toHaveBeenCalledWith({
109
+ texts: [longText],
110
+ model: "embed-english-v3.0",
111
+ inputType: "search_document",
112
+ embeddingTypes: ["float"],
113
+ });
114
+ });
115
+
116
+ it("should use custom model configuration", async () => {
117
+ const customEmbeddings = new CohereEmbeddings(
118
+ "test-api-key",
119
+ "embed-multilingual-v3.0",
120
+ 1024,
121
+ );
122
+ const mockEmbedding = Array(1024).fill(0.1);
123
+ mockClient.embed.mockResolvedValue({
124
+ embeddings: [mockEmbedding],
125
+ });
126
+
127
+ await customEmbeddings.embed("test");
128
+
129
+ expect(mockClient.embed).toHaveBeenCalledWith({
130
+ texts: ["test"],
131
+ model: "embed-multilingual-v3.0",
132
+ inputType: "search_document",
133
+ embeddingTypes: ["float"],
134
+ });
135
+ });
136
+
137
+ it("should throw error if no embedding returned", async () => {
138
+ mockClient.embed.mockResolvedValue({
139
+ embeddings: [],
140
+ });
141
+
142
+ await expect(embeddings.embed("test")).rejects.toThrow(
143
+ "No embedding returned from Cohere API",
144
+ );
145
+ });
146
+
147
+ it("should propagate errors", async () => {
148
+ mockClient.embed.mockRejectedValue(new Error("API Error"));
149
+
150
+ await expect(embeddings.embed("test")).rejects.toThrow("API Error");
151
+ });
152
+ });
153
+
154
+ describe("embedBatch", () => {
155
+ it("should generate embeddings for multiple texts", async () => {
156
+ const mockEmbeddings = [
157
+ Array(1024).fill(0.1),
158
+ Array(1024).fill(0.2),
159
+ Array(1024).fill(0.3),
160
+ ];
161
+ mockClient.embed.mockResolvedValue({
162
+ embeddings: mockEmbeddings,
163
+ });
164
+
165
+ const texts = ["text1", "text2", "text3"];
166
+ const results = await embeddings.embedBatch(texts);
167
+
168
+ expect(results).toEqual([
169
+ { embedding: mockEmbeddings[0], dimensions: 1024 },
170
+ { embedding: mockEmbeddings[1], dimensions: 1024 },
171
+ { embedding: mockEmbeddings[2], dimensions: 1024 },
172
+ ]);
173
+ expect(mockClient.embed).toHaveBeenCalledWith({
174
+ texts,
175
+ model: "embed-english-v3.0",
176
+ inputType: "search_document",
177
+ embeddingTypes: ["float"],
178
+ });
179
+ });
180
+
181
+ it("should handle empty batch", async () => {
182
+ mockClient.embed.mockResolvedValue({
183
+ embeddings: [],
184
+ });
185
+
186
+ const results = await embeddings.embedBatch([]);
187
+
188
+ expect(results).toEqual([]);
189
+ });
190
+
191
+ it("should handle single item in batch", async () => {
192
+ const mockEmbedding = Array(1024).fill(0.5);
193
+ mockClient.embed.mockResolvedValue({
194
+ embeddings: [mockEmbedding],
195
+ });
196
+
197
+ const results = await embeddings.embedBatch(["single text"]);
198
+
199
+ expect(results).toHaveLength(1);
200
+ expect(results[0].embedding).toEqual(mockEmbedding);
201
+ });
202
+
203
+ it("should handle large batches", async () => {
204
+ const batchSize = 100;
205
+ const mockEmbeddings = Array(batchSize)
206
+ .fill(null)
207
+ .map(() => Array(1024).fill(Math.random()));
208
+
209
+ mockClient.embed.mockResolvedValue({
210
+ embeddings: mockEmbeddings,
211
+ });
212
+
213
+ const texts = Array(batchSize)
214
+ .fill(null)
215
+ .map((_, i) => `text ${i}`);
216
+ const results = await embeddings.embedBatch(texts);
217
+
218
+ expect(results).toHaveLength(batchSize);
219
+ });
220
+
221
+ it("should throw error if no embeddings returned", async () => {
222
+ mockClient.embed.mockResolvedValue({});
223
+
224
+ await expect(embeddings.embedBatch(["text1"])).rejects.toThrow(
225
+ "No embeddings returned from Cohere API",
226
+ );
227
+ });
228
+
229
+ it("should propagate errors in batch", async () => {
230
+ mockClient.embed.mockRejectedValue(new Error("Batch API Error"));
231
+
232
+ await expect(embeddings.embedBatch(["text1", "text2"])).rejects.toThrow(
233
+ "Batch API Error",
234
+ );
235
+ });
236
+ });
237
+
238
+ describe("getDimensions", () => {
239
+ it("should return configured dimensions", () => {
240
+ expect(embeddings.getDimensions()).toBe(1024);
241
+ });
242
+
243
+ it("should return custom dimensions", () => {
244
+ const customEmbeddings = new CohereEmbeddings(
245
+ "test-api-key",
246
+ "embed-english-v3.0",
247
+ 512,
248
+ );
249
+ expect(customEmbeddings.getDimensions()).toBe(512);
250
+ });
251
+ });
252
+
253
+ describe("getModel", () => {
254
+ it("should return configured model", () => {
255
+ expect(embeddings.getModel()).toBe("embed-english-v3.0");
256
+ });
257
+
258
+ it("should return custom model", () => {
259
+ const customEmbeddings = new CohereEmbeddings(
260
+ "test-api-key",
261
+ "embed-multilingual-v3.0",
262
+ );
263
+ expect(customEmbeddings.getModel()).toBe("embed-multilingual-v3.0");
264
+ });
265
+ });
266
+
267
+ describe("rate limiting", () => {
268
+ it("should retry on rate limit error (status 429)", async () => {
269
+ const mockEmbedding = Array(1024).fill(0.5);
270
+
271
+ mockClient.embed
272
+ .mockRejectedValueOnce({ status: 429, message: "Rate limit exceeded" })
273
+ .mockRejectedValueOnce({ status: 429, message: "Rate limit exceeded" })
274
+ .mockResolvedValue({ embeddings: [mockEmbedding] });
275
+
276
+ const result = await embeddings.embed("test text");
277
+
278
+ expect(result.embedding).toEqual(mockEmbedding);
279
+ expect(mockClient.embed).toHaveBeenCalledTimes(3);
280
+ });
281
+
282
+ it("should retry on rate limit error (statusCode 429)", async () => {
283
+ const mockEmbedding = Array(1024).fill(0.5);
284
+
285
+ mockClient.embed
286
+ .mockRejectedValueOnce({
287
+ statusCode: 429,
288
+ message: "Rate limit exceeded",
289
+ })
290
+ .mockResolvedValue({ embeddings: [mockEmbedding] });
291
+
292
+ const result = await embeddings.embed("test text");
293
+
294
+ expect(result.embedding).toEqual(mockEmbedding);
295
+ expect(mockClient.embed).toHaveBeenCalledTimes(2);
296
+ });
297
+
298
+ it("should retry on rate limit message", async () => {
299
+ const mockEmbedding = Array(1024).fill(0.5);
300
+
301
+ mockClient.embed
302
+ .mockRejectedValueOnce({
303
+ message: "You have exceeded the rate limit",
304
+ })
305
+ .mockResolvedValue({ embeddings: [mockEmbedding] });
306
+
307
+ const result = await embeddings.embed("test text");
308
+
309
+ expect(result.embedding).toEqual(mockEmbedding);
310
+ expect(mockClient.embed).toHaveBeenCalledTimes(2);
311
+ });
312
+
313
+ it("should use exponential backoff", async () => {
314
+ const rateLimitEmbeddings = new CohereEmbeddings(
315
+ "test-api-key",
316
+ "embed-english-v3.0",
317
+ undefined,
318
+ {
319
+ retryAttempts: 3,
320
+ retryDelayMs: 100,
321
+ },
322
+ );
323
+
324
+ const mockEmbedding = Array(1024).fill(0.5);
325
+ const rateLimitError = {
326
+ status: 429,
327
+ message: "Rate limit exceeded",
328
+ };
329
+
330
+ mockClient.embed
331
+ .mockRejectedValueOnce(rateLimitError)
332
+ .mockRejectedValueOnce(rateLimitError)
333
+ .mockResolvedValue({ embeddings: [mockEmbedding] });
334
+
335
+ const startTime = Date.now();
336
+ await rateLimitEmbeddings.embed("test text");
337
+ const duration = Date.now() - startTime;
338
+
339
+ // Should wait: 100ms (first retry) + 200ms (second retry) = 300ms
340
+ expect(duration).toBeGreaterThanOrEqual(250);
341
+ });
342
+
343
+ it("should throw error after max retries exceeded", async () => {
344
+ const rateLimitEmbeddings = new CohereEmbeddings(
345
+ "test-api-key",
346
+ "embed-english-v3.0",
347
+ undefined,
348
+ {
349
+ retryAttempts: 2,
350
+ retryDelayMs: 100,
351
+ },
352
+ );
353
+
354
+ const rateLimitError = {
355
+ status: 429,
356
+ message: "Rate limit exceeded",
357
+ };
358
+
359
+ mockClient.embed.mockRejectedValue(rateLimitError);
360
+
361
+ await expect(rateLimitEmbeddings.embed("test text")).rejects.toThrow(
362
+ "Cohere API rate limit exceeded after 2 retry attempts",
363
+ );
364
+
365
+ expect(mockClient.embed).toHaveBeenCalledTimes(3);
366
+ });
367
+
368
+ it("should handle rate limit errors in batch operations", async () => {
369
+ const mockEmbeddings = [Array(1024).fill(0.1), Array(1024).fill(0.2)];
370
+
371
+ mockClient.embed
372
+ .mockRejectedValueOnce({ status: 429, message: "Rate limit exceeded" })
373
+ .mockResolvedValue({
374
+ embeddings: mockEmbeddings,
375
+ });
376
+
377
+ const results = await embeddings.embedBatch(["text1", "text2"]);
378
+
379
+ expect(results).toHaveLength(2);
380
+ expect(mockClient.embed).toHaveBeenCalledTimes(2);
381
+ });
382
+
383
+ it("should not retry on non-rate-limit errors", async () => {
384
+ const apiError = new Error("Invalid API key");
385
+ mockClient.embed.mockRejectedValue(apiError);
386
+
387
+ await expect(embeddings.embed("test text")).rejects.toThrow(
388
+ "Invalid API key",
389
+ );
390
+ expect(mockClient.embed).toHaveBeenCalledTimes(1);
391
+ });
392
+
393
+ it("should accept custom rate limit configuration", () => {
394
+ const customEmbeddings = new CohereEmbeddings(
395
+ "test-api-key",
396
+ "embed-english-v3.0",
397
+ undefined,
398
+ {
399
+ maxRequestsPerMinute: 200,
400
+ retryAttempts: 5,
401
+ retryDelayMs: 2000,
402
+ },
403
+ );
404
+
405
+ expect(customEmbeddings).toBeDefined();
406
+ });
407
+ });
408
+ });