@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.
- package/.env.example +92 -0
- package/.github/workflows/ci.yml +61 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/release.yml +52 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.releaserc.json +59 -0
- package/.yamlfmt +4 -0
- package/CHANGELOG.md +73 -0
- package/CONTRIBUTING.md +176 -0
- package/LICENSE +21 -0
- package/README.md +714 -0
- package/build/embeddings/base.d.ts +23 -0
- package/build/embeddings/base.d.ts.map +1 -0
- package/build/embeddings/base.js +2 -0
- package/build/embeddings/base.js.map +1 -0
- package/build/embeddings/cohere.d.ts +17 -0
- package/build/embeddings/cohere.d.ts.map +1 -0
- package/build/embeddings/cohere.js +102 -0
- package/build/embeddings/cohere.js.map +1 -0
- package/build/embeddings/cohere.test.d.ts +2 -0
- package/build/embeddings/cohere.test.d.ts.map +1 -0
- package/build/embeddings/cohere.test.js +279 -0
- package/build/embeddings/cohere.test.js.map +1 -0
- package/build/embeddings/factory.d.ts +10 -0
- package/build/embeddings/factory.d.ts.map +1 -0
- package/build/embeddings/factory.js +98 -0
- package/build/embeddings/factory.js.map +1 -0
- package/build/embeddings/factory.test.d.ts +2 -0
- package/build/embeddings/factory.test.d.ts.map +1 -0
- package/build/embeddings/factory.test.js +329 -0
- package/build/embeddings/factory.test.js.map +1 -0
- package/build/embeddings/ollama.d.ts +18 -0
- package/build/embeddings/ollama.d.ts.map +1 -0
- package/build/embeddings/ollama.js +135 -0
- package/build/embeddings/ollama.js.map +1 -0
- package/build/embeddings/ollama.test.d.ts +2 -0
- package/build/embeddings/ollama.test.d.ts.map +1 -0
- package/build/embeddings/ollama.test.js +399 -0
- package/build/embeddings/ollama.test.js.map +1 -0
- package/build/embeddings/openai.d.ts +16 -0
- package/build/embeddings/openai.d.ts.map +1 -0
- package/build/embeddings/openai.js +108 -0
- package/build/embeddings/openai.js.map +1 -0
- package/build/embeddings/openai.test.d.ts +2 -0
- package/build/embeddings/openai.test.d.ts.map +1 -0
- package/build/embeddings/openai.test.js +283 -0
- package/build/embeddings/openai.test.js.map +1 -0
- package/build/embeddings/voyage.d.ts +19 -0
- package/build/embeddings/voyage.d.ts.map +1 -0
- package/build/embeddings/voyage.js +113 -0
- package/build/embeddings/voyage.js.map +1 -0
- package/build/embeddings/voyage.test.d.ts +2 -0
- package/build/embeddings/voyage.test.d.ts.map +1 -0
- package/build/embeddings/voyage.test.js +371 -0
- package/build/embeddings/voyage.test.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +534 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +2 -0
- package/build/index.test.d.ts.map +1 -0
- package/build/index.test.js +241 -0
- package/build/index.test.js.map +1 -0
- package/build/qdrant/client.d.ts +37 -0
- package/build/qdrant/client.d.ts.map +1 -0
- package/build/qdrant/client.js +142 -0
- package/build/qdrant/client.js.map +1 -0
- package/build/qdrant/client.test.d.ts +2 -0
- package/build/qdrant/client.test.d.ts.map +1 -0
- package/build/qdrant/client.test.js +340 -0
- package/build/qdrant/client.test.js.map +1 -0
- package/commitlint.config.js +25 -0
- package/docker-compose.yml +22 -0
- package/docs/test_report.md +259 -0
- package/examples/README.md +315 -0
- package/examples/basic/README.md +111 -0
- package/examples/filters/README.md +262 -0
- package/examples/knowledge-base/README.md +207 -0
- package/examples/rate-limiting/README.md +376 -0
- package/package.json +59 -0
- package/scripts/verify-providers.js +238 -0
- package/src/embeddings/base.ts +25 -0
- package/src/embeddings/cohere.test.ts +408 -0
- package/src/embeddings/cohere.ts +152 -0
- package/src/embeddings/factory.test.ts +453 -0
- package/src/embeddings/factory.ts +163 -0
- package/src/embeddings/ollama.test.ts +543 -0
- package/src/embeddings/ollama.ts +196 -0
- package/src/embeddings/openai.test.ts +402 -0
- package/src/embeddings/openai.ts +158 -0
- package/src/embeddings/voyage.test.ts +520 -0
- package/src/embeddings/voyage.ts +168 -0
- package/src/index.test.ts +304 -0
- package/src/index.ts +614 -0
- package/src/qdrant/client.test.ts +456 -0
- package/src/qdrant/client.ts +195 -0
- package/tsconfig.json +19 -0
- 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
|
+
});
|