lynkr 3.3.1 → 4.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.
@@ -0,0 +1,484 @@
1
+ const assert = require("assert");
2
+ const { describe, it, beforeEach, afterEach } = require("node:test");
3
+ const {
4
+ convertOpenAIToAnthropic,
5
+ convertAnthropicToOpenAI,
6
+ mapStopReason
7
+ } = require("../src/clients/openai-format");
8
+
9
+ describe("Cursor IDE Integration (OpenAI API Compatibility)", () => {
10
+ describe("Format Conversion: OpenAI → Anthropic", () => {
11
+ it("should convert simple OpenAI chat request to Anthropic format", () => {
12
+ const openaiRequest = {
13
+ model: "gpt-4",
14
+ messages: [
15
+ { role: "system", content: "You are a helpful assistant." },
16
+ { role: "user", content: "Hello, world!" }
17
+ ],
18
+ max_tokens: 1000,
19
+ temperature: 0.7
20
+ };
21
+
22
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
23
+
24
+ assert.strictEqual(anthropicRequest.system, "You are a helpful assistant.");
25
+ assert.strictEqual(anthropicRequest.messages.length, 1);
26
+ assert.strictEqual(anthropicRequest.messages[0].role, "user");
27
+ assert.strictEqual(anthropicRequest.messages[0].content, "Hello, world!");
28
+ assert.strictEqual(anthropicRequest.max_tokens, 1000);
29
+ assert.strictEqual(anthropicRequest.temperature, 0.7);
30
+ });
31
+
32
+ it("should convert OpenAI tools to Anthropic format", () => {
33
+ const openaiRequest = {
34
+ model: "gpt-4",
35
+ messages: [{ role: "user", content: "Read the file" }],
36
+ tools: [
37
+ {
38
+ type: "function",
39
+ function: {
40
+ name: "Read",
41
+ description: "Read a file",
42
+ parameters: {
43
+ type: "object",
44
+ properties: {
45
+ file_path: { type: "string" }
46
+ },
47
+ required: ["file_path"]
48
+ }
49
+ }
50
+ }
51
+ ]
52
+ };
53
+
54
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
55
+
56
+ assert.strictEqual(anthropicRequest.tools.length, 1);
57
+ assert.strictEqual(anthropicRequest.tools[0].name, "Read");
58
+ assert.strictEqual(anthropicRequest.tools[0].description, "Read a file");
59
+ assert.deepStrictEqual(anthropicRequest.tools[0].input_schema, {
60
+ type: "object",
61
+ properties: {
62
+ file_path: { type: "string" }
63
+ },
64
+ required: ["file_path"]
65
+ });
66
+ });
67
+
68
+ it("should convert OpenAI tool_calls in assistant message", () => {
69
+ const openaiRequest = {
70
+ model: "gpt-4",
71
+ messages: [
72
+ {
73
+ role: "assistant",
74
+ content: "I'll read the file.",
75
+ tool_calls: [
76
+ {
77
+ id: "call_123",
78
+ type: "function",
79
+ function: {
80
+ name: "Read",
81
+ arguments: '{"file_path": "/tmp/test.txt"}'
82
+ }
83
+ }
84
+ ]
85
+ }
86
+ ]
87
+ };
88
+
89
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
90
+
91
+ assert.strictEqual(anthropicRequest.messages.length, 1);
92
+ assert.strictEqual(anthropicRequest.messages[0].role, "assistant");
93
+ assert.strictEqual(anthropicRequest.messages[0].content.length, 2);
94
+ assert.strictEqual(anthropicRequest.messages[0].content[0].type, "text");
95
+ assert.strictEqual(anthropicRequest.messages[0].content[1].type, "tool_use");
96
+ assert.strictEqual(anthropicRequest.messages[0].content[1].name, "Read");
97
+ assert.deepStrictEqual(anthropicRequest.messages[0].content[1].input, {
98
+ file_path: "/tmp/test.txt"
99
+ });
100
+ });
101
+
102
+ it("should convert OpenAI tool results", () => {
103
+ const openaiRequest = {
104
+ model: "gpt-4",
105
+ messages: [
106
+ {
107
+ role: "tool",
108
+ tool_call_id: "call_123",
109
+ content: "File contents here"
110
+ }
111
+ ]
112
+ };
113
+
114
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
115
+
116
+ assert.strictEqual(anthropicRequest.messages.length, 1);
117
+ assert.strictEqual(anthropicRequest.messages[0].role, "user");
118
+ assert.strictEqual(anthropicRequest.messages[0].content[0].type, "tool_result");
119
+ assert.strictEqual(anthropicRequest.messages[0].content[0].tool_use_id, "call_123");
120
+ assert.strictEqual(anthropicRequest.messages[0].content[0].content, "File contents here");
121
+ });
122
+
123
+ it("should handle tool_choice conversion", () => {
124
+ const autoRequest = {
125
+ model: "gpt-4",
126
+ messages: [{ role: "user", content: "test" }],
127
+ tool_choice: "auto"
128
+ };
129
+
130
+ const noneRequest = {
131
+ model: "gpt-4",
132
+ messages: [{ role: "user", content: "test" }],
133
+ tool_choice: "none"
134
+ };
135
+
136
+ const specificRequest = {
137
+ model: "gpt-4",
138
+ messages: [{ role: "user", content: "test" }],
139
+ tool_choice: { type: "function", function: { name: "Read" } }
140
+ };
141
+
142
+ const anthropicAuto = convertOpenAIToAnthropic(autoRequest);
143
+ const anthropicNone = convertOpenAIToAnthropic(noneRequest);
144
+ const anthropicSpecific = convertOpenAIToAnthropic(specificRequest);
145
+
146
+ assert.deepStrictEqual(anthropicAuto.tool_choice, { type: "auto" });
147
+ assert.deepStrictEqual(anthropicNone.tool_choice, { type: "none" });
148
+ assert.deepStrictEqual(anthropicSpecific.tool_choice, { type: "tool", name: "Read" });
149
+ });
150
+ });
151
+
152
+ describe("Format Conversion: Anthropic → OpenAI", () => {
153
+ it("should convert simple Anthropic response to OpenAI format", () => {
154
+ const anthropicResponse = {
155
+ id: "msg_123",
156
+ type: "message",
157
+ role: "assistant",
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: "Hello! How can I help you?"
162
+ }
163
+ ],
164
+ stop_reason: "end_turn",
165
+ usage: {
166
+ input_tokens: 10,
167
+ output_tokens: 20
168
+ }
169
+ };
170
+
171
+ const openaiResponse = convertAnthropicToOpenAI(anthropicResponse, "gpt-4");
172
+
173
+ assert.strictEqual(openaiResponse.id, "msg_123");
174
+ assert.strictEqual(openaiResponse.object, "chat.completion");
175
+ assert.strictEqual(openaiResponse.model, "gpt-4");
176
+ assert.strictEqual(openaiResponse.choices.length, 1);
177
+ assert.strictEqual(openaiResponse.choices[0].message.role, "assistant");
178
+ assert.strictEqual(openaiResponse.choices[0].message.content, "Hello! How can I help you?");
179
+ assert.strictEqual(openaiResponse.choices[0].finish_reason, "stop");
180
+ assert.strictEqual(openaiResponse.usage.prompt_tokens, 10);
181
+ assert.strictEqual(openaiResponse.usage.completion_tokens, 20);
182
+ assert.strictEqual(openaiResponse.usage.total_tokens, 30);
183
+ });
184
+
185
+ it("should convert Anthropic tool_use to OpenAI tool_calls", () => {
186
+ const anthropicResponse = {
187
+ id: "msg_456",
188
+ content: [
189
+ {
190
+ type: "text",
191
+ text: "I'll read the file."
192
+ },
193
+ {
194
+ type: "tool_use",
195
+ id: "toolu_789",
196
+ name: "Read",
197
+ input: {
198
+ file_path: "/tmp/test.txt"
199
+ }
200
+ }
201
+ ],
202
+ stop_reason: "tool_use",
203
+ usage: {
204
+ input_tokens: 50,
205
+ output_tokens: 30
206
+ }
207
+ };
208
+
209
+ const openaiResponse = convertAnthropicToOpenAI(anthropicResponse, "gpt-4");
210
+
211
+ assert.strictEqual(openaiResponse.choices[0].message.content, "I'll read the file.");
212
+ assert.strictEqual(openaiResponse.choices[0].message.tool_calls.length, 1);
213
+ assert.strictEqual(openaiResponse.choices[0].message.tool_calls[0].id, "toolu_789");
214
+ assert.strictEqual(openaiResponse.choices[0].message.tool_calls[0].type, "function");
215
+ assert.strictEqual(openaiResponse.choices[0].message.tool_calls[0].function.name, "Read");
216
+ assert.strictEqual(
217
+ openaiResponse.choices[0].message.tool_calls[0].function.arguments,
218
+ '{"file_path":"/tmp/test.txt"}'
219
+ );
220
+ assert.strictEqual(openaiResponse.choices[0].finish_reason, "tool_calls");
221
+ });
222
+ });
223
+
224
+ describe("Stop Reason Mapping", () => {
225
+ it("should map Anthropic stop reasons to OpenAI finish reasons", () => {
226
+ assert.strictEqual(mapStopReason("end_turn"), "stop");
227
+ assert.strictEqual(mapStopReason("max_tokens"), "length");
228
+ assert.strictEqual(mapStopReason("stop_sequence"), "stop");
229
+ assert.strictEqual(mapStopReason("tool_use"), "tool_calls");
230
+ assert.strictEqual(mapStopReason("unknown_reason"), "stop");
231
+ });
232
+ });
233
+
234
+ describe("OpenAI Router Endpoints", () => {
235
+ it("GET /v1/models should return model list based on provider", () => {
236
+ // This is an integration test - would need actual server running
237
+ // Just verify the route exists in the router
238
+ const openaiRouter = require("../src/api/openai-router");
239
+ assert.ok(openaiRouter, "OpenAI router should be defined");
240
+ });
241
+
242
+ it("POST /v1/chat/completions should handle request", () => {
243
+ // Integration test - would need actual server
244
+ const openaiRouter = require("../src/api/openai-router");
245
+ assert.ok(openaiRouter, "OpenAI router should be defined");
246
+ });
247
+
248
+ it("POST /v1/embeddings should return 501 when not configured", () => {
249
+ // Integration test - would need actual server
250
+ const openaiRouter = require("../src/api/openai-router");
251
+ assert.ok(openaiRouter, "OpenAI router should be defined");
252
+ });
253
+
254
+ it("GET /v1/health should return health status", () => {
255
+ // Integration test - would need actual server
256
+ const openaiRouter = require("../src/api/openai-router");
257
+ assert.ok(openaiRouter, "OpenAI router should be defined");
258
+ });
259
+ });
260
+
261
+ describe("Edge Cases", () => {
262
+ it("should handle empty messages array", () => {
263
+ const openaiRequest = {
264
+ model: "gpt-4",
265
+ messages: []
266
+ };
267
+
268
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
269
+ assert.strictEqual(anthropicRequest.messages.length, 0);
270
+ });
271
+
272
+ it("should handle missing optional fields", () => {
273
+ const openaiRequest = {
274
+ model: "gpt-4",
275
+ messages: [{ role: "user", content: "test" }]
276
+ };
277
+
278
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest);
279
+ assert.ok(anthropicRequest.max_tokens); // Should have default
280
+ assert.ok(!anthropicRequest.temperature); // Should not exist if not provided
281
+ });
282
+
283
+ it("should handle multiple text blocks in Anthropic response", () => {
284
+ const anthropicResponse = {
285
+ id: "msg_multi",
286
+ content: [
287
+ { type: "text", text: "First part. " },
288
+ { type: "text", text: "Second part." }
289
+ ],
290
+ stop_reason: "end_turn",
291
+ usage: { input_tokens: 5, output_tokens: 10 }
292
+ };
293
+
294
+ const openaiResponse = convertAnthropicToOpenAI(anthropicResponse);
295
+ assert.strictEqual(openaiResponse.choices[0].message.content, "First part. Second part.");
296
+ });
297
+ });
298
+
299
+ describe("OpenRouter Embeddings Configuration", () => {
300
+ it("should use configured embeddings model", () => {
301
+ process.env.OPENROUTER_EMBEDDINGS_MODEL = "openai/text-embedding-3-small";
302
+ delete require.cache[require.resolve("../src/config")];
303
+ const config = require("../src/config");
304
+
305
+ assert.strictEqual(config.openrouter.embeddingsModel, "openai/text-embedding-3-small");
306
+ });
307
+
308
+ it("should default to ada-002 when not configured", () => {
309
+ delete process.env.OPENROUTER_EMBEDDINGS_MODEL;
310
+ delete require.cache[require.resolve("../src/config")];
311
+ const config = require("../src/config");
312
+
313
+ assert.strictEqual(config.openrouter.embeddingsModel, "openai/text-embedding-ada-002");
314
+ });
315
+
316
+ it("should allow different models for chat and embeddings", () => {
317
+ process.env.OPENROUTER_MODEL = "anthropic/claude-3.5-sonnet";
318
+ process.env.OPENROUTER_EMBEDDINGS_MODEL = "openai/text-embedding-3-small";
319
+ delete require.cache[require.resolve("../src/config")];
320
+ const config = require("../src/config");
321
+
322
+ assert.strictEqual(config.openrouter.model, "anthropic/claude-3.5-sonnet");
323
+ assert.strictEqual(config.openrouter.embeddingsModel, "openai/text-embedding-3-small");
324
+ assert.notStrictEqual(config.openrouter.model, config.openrouter.embeddingsModel);
325
+ });
326
+ });
327
+
328
+ describe("Local Embeddings Configuration", () => {
329
+ let originalEnv;
330
+
331
+ beforeEach(() => {
332
+ originalEnv = { ...process.env };
333
+ delete require.cache[require.resolve("../src/config")];
334
+ });
335
+
336
+ afterEach(() => {
337
+ process.env = originalEnv;
338
+ delete require.cache[require.resolve("../src/config")];
339
+ });
340
+
341
+ describe("Ollama Embeddings", () => {
342
+ it("should use configured Ollama embeddings model", () => {
343
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "mxbai-embed-large";
344
+ const config = require("../src/config");
345
+
346
+ assert.strictEqual(config.ollama.embeddingsModel, "mxbai-embed-large");
347
+ });
348
+
349
+ it("should default to nomic-embed-text when not configured", () => {
350
+ delete process.env.OLLAMA_EMBEDDINGS_MODEL;
351
+ const config = require("../src/config");
352
+
353
+ assert.strictEqual(config.ollama.embeddingsModel, "nomic-embed-text");
354
+ });
355
+
356
+ it("should use custom Ollama embeddings endpoint", () => {
357
+ process.env.OLLAMA_EMBEDDINGS_ENDPOINT = "http://localhost:9999/api/embeddings";
358
+ const config = require("../src/config");
359
+
360
+ assert.strictEqual(config.ollama.embeddingsEndpoint, "http://localhost:9999/api/embeddings");
361
+ });
362
+
363
+ it("should default Ollama embeddings endpoint to /api/embeddings", () => {
364
+ delete process.env.OLLAMA_EMBEDDINGS_ENDPOINT;
365
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
366
+ const config = require("../src/config");
367
+
368
+ assert.strictEqual(config.ollama.embeddingsEndpoint, "http://localhost:11434/api/embeddings");
369
+ });
370
+
371
+ it("should allow different models for Ollama chat and embeddings", () => {
372
+ process.env.OLLAMA_MODEL = "llama3.2";
373
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
374
+ const config = require("../src/config");
375
+
376
+ assert.strictEqual(config.ollama.model, "llama3.2");
377
+ assert.strictEqual(config.ollama.embeddingsModel, "nomic-embed-text");
378
+ assert.notStrictEqual(config.ollama.model, config.ollama.embeddingsModel);
379
+ });
380
+ });
381
+
382
+ describe("llama.cpp Embeddings", () => {
383
+ it("should use configured llama.cpp embeddings endpoint", () => {
384
+ process.env.LLAMACPP_EMBEDDINGS_ENDPOINT = "http://localhost:9000/embeddings";
385
+ const config = require("../src/config");
386
+
387
+ assert.strictEqual(config.llamacpp.embeddingsEndpoint, "http://localhost:9000/embeddings");
388
+ });
389
+
390
+ it("should default llama.cpp embeddings endpoint to /embeddings", () => {
391
+ delete process.env.LLAMACPP_EMBEDDINGS_ENDPOINT;
392
+ process.env.LLAMACPP_ENDPOINT = "http://localhost:8080";
393
+ const config = require("../src/config");
394
+
395
+ assert.strictEqual(config.llamacpp.embeddingsEndpoint, "http://localhost:8080/embeddings");
396
+ });
397
+ });
398
+
399
+ describe("Embeddings Provider Priority", () => {
400
+ it("should use explicit EMBEDDINGS_PROVIDER when set to ollama", () => {
401
+ process.env.EMBEDDINGS_PROVIDER = "ollama";
402
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
403
+ process.env.OPENROUTER_API_KEY = "sk-test";
404
+
405
+ // This test verifies the config allows the explicit provider to be set
406
+ // Actual provider detection logic is in openai-router.js
407
+ const config = require("../src/config");
408
+ assert.strictEqual(process.env.EMBEDDINGS_PROVIDER, "ollama");
409
+ });
410
+
411
+ it("should use explicit EMBEDDINGS_PROVIDER when set to llamacpp", () => {
412
+ process.env.EMBEDDINGS_PROVIDER = "llamacpp";
413
+ process.env.LLAMACPP_EMBEDDINGS_ENDPOINT = "http://localhost:8080/embeddings";
414
+ process.env.OPENROUTER_API_KEY = "sk-test";
415
+
416
+ const config = require("../src/config");
417
+ assert.strictEqual(process.env.EMBEDDINGS_PROVIDER, "llamacpp");
418
+ });
419
+
420
+ it("should use explicit EMBEDDINGS_PROVIDER when set to openrouter", () => {
421
+ process.env.EMBEDDINGS_PROVIDER = "openrouter";
422
+ process.env.OPENROUTER_API_KEY = "sk-test";
423
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
424
+
425
+ const config = require("../src/config");
426
+ assert.strictEqual(process.env.EMBEDDINGS_PROVIDER, "openrouter");
427
+ });
428
+
429
+ it("should use explicit EMBEDDINGS_PROVIDER when set to openai", () => {
430
+ process.env.EMBEDDINGS_PROVIDER = "openai";
431
+ process.env.OPENAI_API_KEY = "sk-test";
432
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
433
+
434
+ const config = require("../src/config");
435
+ assert.strictEqual(process.env.EMBEDDINGS_PROVIDER, "openai");
436
+ });
437
+ });
438
+
439
+ describe("Privacy and Cost Comparison", () => {
440
+ it("should support 100% local setup with Ollama chat + Ollama embeddings", () => {
441
+ process.env.MODEL_PROVIDER = "ollama";
442
+ process.env.OLLAMA_MODEL = "llama3.2";
443
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
444
+ delete process.env.OPENROUTER_API_KEY;
445
+ delete process.env.OPENAI_API_KEY;
446
+
447
+ const config = require("../src/config");
448
+ assert.strictEqual(config.modelProvider.type, "ollama");
449
+ assert.strictEqual(config.ollama.model, "llama3.2");
450
+ assert.strictEqual(config.ollama.embeddingsModel, "nomic-embed-text");
451
+ // Verify no cloud API keys configured (100% local)
452
+ assert.ok(!config.openrouter?.apiKey);
453
+ assert.ok(!config.openai?.apiKey);
454
+ });
455
+
456
+ it("should support 100% local setup with Ollama chat + llama.cpp embeddings", () => {
457
+ process.env.MODEL_PROVIDER = "ollama";
458
+ process.env.OLLAMA_MODEL = "llama3.2";
459
+ process.env.LLAMACPP_EMBEDDINGS_ENDPOINT = "http://localhost:8080/embeddings";
460
+ delete process.env.OPENROUTER_API_KEY;
461
+ delete process.env.OPENAI_API_KEY;
462
+
463
+ const config = require("../src/config");
464
+ assert.strictEqual(config.modelProvider.type, "ollama");
465
+ assert.strictEqual(config.ollama.model, "llama3.2");
466
+ assert.strictEqual(config.llamacpp.embeddingsEndpoint, "http://localhost:8080/embeddings");
467
+ // Verify no cloud API keys configured (100% local)
468
+ assert.ok(!config.openrouter?.apiKey);
469
+ assert.ok(!config.openai?.apiKey);
470
+ });
471
+
472
+ it("should support hybrid setup with Databricks chat + Ollama embeddings", () => {
473
+ process.env.MODEL_PROVIDER = "databricks";
474
+ process.env.DATABRICKS_API_KEY = "test-key";
475
+ process.env.DATABRICKS_API_BASE = "http://test.com";
476
+ process.env.OLLAMA_EMBEDDINGS_MODEL = "nomic-embed-text";
477
+
478
+ const config = require("../src/config");
479
+ assert.strictEqual(config.modelProvider.type, "databricks");
480
+ assert.strictEqual(config.ollama.embeddingsModel, "nomic-embed-text");
481
+ });
482
+ });
483
+ });
484
+ });