lynkr 8.0.0 → 9.0.1

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 (128) hide show
  1. package/.lynkr/telemetry.db +0 -0
  2. package/.lynkr/telemetry.db-shm +0 -0
  3. package/.lynkr/telemetry.db-wal +0 -0
  4. package/README.md +196 -322
  5. package/lynkr-skill.tar.gz +0 -0
  6. package/package.json +4 -3
  7. package/src/api/openai-router.js +64 -13
  8. package/src/api/providers-handler.js +171 -3
  9. package/src/api/router.js +9 -2
  10. package/src/clients/circuit-breaker.js +10 -247
  11. package/src/clients/codex-process.js +342 -0
  12. package/src/clients/codex-utils.js +143 -0
  13. package/src/clients/databricks.js +210 -63
  14. package/src/clients/resilience.js +540 -0
  15. package/src/clients/retry.js +22 -167
  16. package/src/clients/standard-tools.js +23 -0
  17. package/src/config/index.js +77 -0
  18. package/src/context/compression.js +42 -9
  19. package/src/context/distill.js +492 -0
  20. package/src/orchestrator/index.js +48 -8
  21. package/src/routing/complexity-analyzer.js +258 -5
  22. package/src/routing/index.js +12 -2
  23. package/src/routing/latency-tracker.js +148 -0
  24. package/src/routing/model-tiers.js +2 -0
  25. package/src/routing/quality-scorer.js +113 -0
  26. package/src/routing/telemetry.js +464 -0
  27. package/src/server.js +13 -12
  28. package/src/tools/code-graph.js +538 -0
  29. package/src/tools/code-mode.js +304 -0
  30. package/src/tools/index.js +4 -0
  31. package/src/tools/lazy-loader.js +18 -0
  32. package/src/tools/mcp-remote.js +7 -0
  33. package/src/tools/smart-selection.js +11 -0
  34. package/src/tools/tinyfish.js +358 -0
  35. package/src/tools/truncate.js +1 -0
  36. package/src/utils/payload.js +206 -0
  37. package/src/utils/perf-timer.js +80 -0
  38. package/.github/FUNDING.yml +0 -15
  39. package/.github/workflows/README.md +0 -215
  40. package/.github/workflows/ci.yml +0 -69
  41. package/.github/workflows/index.yml +0 -62
  42. package/.github/workflows/web-tools-tests.yml +0 -56
  43. package/CITATIONS.bib +0 -6
  44. package/DEPLOYMENT.md +0 -1001
  45. package/LYNKR-TUI-PLAN.md +0 -984
  46. package/PERFORMANCE-REPORT.md +0 -866
  47. package/PLAN-per-client-model-routing.md +0 -252
  48. package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
  49. package/docs/BingSiteAuth.xml +0 -4
  50. package/docs/docs-style.css +0 -478
  51. package/docs/docs.html +0 -198
  52. package/docs/google5be250e608e6da39.html +0 -1
  53. package/docs/index.html +0 -577
  54. package/docs/index.md +0 -584
  55. package/docs/robots.txt +0 -4
  56. package/docs/sitemap.xml +0 -44
  57. package/docs/style.css +0 -1223
  58. package/docs/toon-integration-spec.md +0 -130
  59. package/documentation/README.md +0 -101
  60. package/documentation/api.md +0 -806
  61. package/documentation/claude-code-cli.md +0 -679
  62. package/documentation/codex-cli.md +0 -397
  63. package/documentation/contributing.md +0 -571
  64. package/documentation/cursor-integration.md +0 -734
  65. package/documentation/docker.md +0 -874
  66. package/documentation/embeddings.md +0 -762
  67. package/documentation/faq.md +0 -713
  68. package/documentation/features.md +0 -403
  69. package/documentation/headroom.md +0 -519
  70. package/documentation/installation.md +0 -758
  71. package/documentation/memory-system.md +0 -476
  72. package/documentation/production.md +0 -636
  73. package/documentation/providers.md +0 -1009
  74. package/documentation/routing.md +0 -476
  75. package/documentation/testing.md +0 -629
  76. package/documentation/token-optimization.md +0 -325
  77. package/documentation/tools.md +0 -697
  78. package/documentation/troubleshooting.md +0 -969
  79. package/final-test.js +0 -33
  80. package/headroom-sidecar/config.py +0 -93
  81. package/headroom-sidecar/requirements.txt +0 -14
  82. package/headroom-sidecar/server.py +0 -451
  83. package/monitor-agents.sh +0 -31
  84. package/scripts/audit-log-reader.js +0 -399
  85. package/scripts/compact-dictionary.js +0 -204
  86. package/scripts/test-deduplication.js +0 -448
  87. package/src/db/database.sqlite +0 -0
  88. package/te +0 -11622
  89. package/test/README.md +0 -212
  90. package/test/azure-openai-config.test.js +0 -213
  91. package/test/azure-openai-error-resilience.test.js +0 -238
  92. package/test/azure-openai-format-conversion.test.js +0 -354
  93. package/test/azure-openai-integration.test.js +0 -287
  94. package/test/azure-openai-routing.test.js +0 -175
  95. package/test/azure-openai-streaming.test.js +0 -171
  96. package/test/bedrock-integration.test.js +0 -457
  97. package/test/comprehensive-test-suite.js +0 -928
  98. package/test/config-validation.test.js +0 -207
  99. package/test/cursor-integration.test.js +0 -484
  100. package/test/format-conversion.test.js +0 -578
  101. package/test/hybrid-routing-integration.test.js +0 -269
  102. package/test/hybrid-routing-performance.test.js +0 -428
  103. package/test/llamacpp-integration.test.js +0 -882
  104. package/test/lmstudio-integration.test.js +0 -347
  105. package/test/memory/extractor.test.js +0 -398
  106. package/test/memory/retriever.test.js +0 -613
  107. package/test/memory/retriever.test.js.bak +0 -585
  108. package/test/memory/search.test.js +0 -537
  109. package/test/memory/search.test.js.bak +0 -389
  110. package/test/memory/store.test.js +0 -344
  111. package/test/memory/store.test.js.bak +0 -312
  112. package/test/memory/surprise.test.js +0 -300
  113. package/test/memory-performance.test.js +0 -472
  114. package/test/openai-integration.test.js +0 -683
  115. package/test/openrouter-error-resilience.test.js +0 -418
  116. package/test/passthrough-mode.test.js +0 -385
  117. package/test/performance-benchmark.js +0 -351
  118. package/test/performance-tests.js +0 -528
  119. package/test/routing.test.js +0 -225
  120. package/test/toon-compression.test.js +0 -131
  121. package/test/web-tools.test.js +0 -329
  122. package/test-agents-simple.js +0 -43
  123. package/test-cli-connection.sh +0 -33
  124. package/test-learning-unit.js +0 -126
  125. package/test-learning.js +0 -112
  126. package/test-parallel-agents.sh +0 -124
  127. package/test-parallel-direct.js +0 -155
  128. package/test-subagents.sh +0 -117
@@ -1,882 +0,0 @@
1
- const assert = require("assert");
2
- const { describe, it, beforeEach, afterEach } = require("node:test");
3
-
4
- describe("llama.cpp Integration", () => {
5
- let originalEnv;
6
-
7
- beforeEach(() => {
8
- originalEnv = { ...process.env };
9
-
10
- // Clear module cache
11
- delete require.cache[require.resolve("../src/config")];
12
- delete require.cache[require.resolve("../src/clients/routing")];
13
- delete require.cache[require.resolve("../src/clients/openrouter-utils")];
14
-
15
- // Prevent .env TIER_* values from being picked up by dotenv
16
- process.env.TIER_SIMPLE = "";
17
- process.env.TIER_MEDIUM = "";
18
- process.env.TIER_COMPLEX = "";
19
- process.env.TIER_REASONING = "";
20
- });
21
-
22
- afterEach(() => {
23
- process.env = originalEnv;
24
- });
25
-
26
- describe("Configuration", () => {
27
- it("should accept llamacpp as a valid MODEL_PROVIDER", () => {
28
- process.env.MODEL_PROVIDER = "llamacpp";
29
- process.env.LLAMACPP_ENDPOINT = "http://localhost:8080";
30
-
31
- const config = require("../src/config");
32
- assert.strictEqual(config.modelProvider.type, "llamacpp");
33
- });
34
-
35
- it("should use default endpoint when LLAMACPP_ENDPOINT is not set", () => {
36
- process.env.MODEL_PROVIDER = "llamacpp";
37
- delete process.env.LLAMACPP_ENDPOINT; // Remove from test env
38
- process.env.LLAMACPP_ENDPOINT = undefined; // Ensure it's truly unset
39
-
40
- const config = require("../src/config");
41
- assert.strictEqual(config.llamacpp.endpoint, "http://localhost:8080");
42
- });
43
-
44
- it("should use custom endpoint when LLAMACPP_ENDPOINT is set", () => {
45
- process.env.MODEL_PROVIDER = "llamacpp";
46
- process.env.LLAMACPP_ENDPOINT = "http://192.168.1.100:9000";
47
-
48
- const config = require("../src/config");
49
- assert.strictEqual(config.llamacpp.endpoint, "http://192.168.1.100:9000");
50
- });
51
-
52
- it("should throw error when LLAMACPP_ENDPOINT is invalid URL", () => {
53
- process.env.MODEL_PROVIDER = "llamacpp";
54
- process.env.LLAMACPP_ENDPOINT = "not-a-valid-url";
55
-
56
- assert.throws(
57
- () => require("../src/config"),
58
- /LLAMACPP_ENDPOINT must be a valid URL/
59
- );
60
- });
61
-
62
- it("should use default model when LLAMACPP_MODEL is not set", () => {
63
- process.env.MODEL_PROVIDER = "llamacpp";
64
- delete process.env.LLAMACPP_MODEL; // Remove from test env
65
- process.env.LLAMACPP_MODEL = undefined; // Ensure it's truly unset
66
-
67
- const config = require("../src/config");
68
- assert.strictEqual(config.llamacpp.model, "default");
69
- });
70
-
71
- it("should use custom model when LLAMACPP_MODEL is set", () => {
72
- process.env.MODEL_PROVIDER = "llamacpp";
73
- process.env.LLAMACPP_MODEL = "qwen2.5-coder-7b";
74
-
75
- const config = require("../src/config");
76
- assert.strictEqual(config.llamacpp.model, "qwen2.5-coder-7b");
77
- });
78
-
79
- it("should use default timeout when LLAMACPP_TIMEOUT_MS is not set", () => {
80
- process.env.MODEL_PROVIDER = "llamacpp";
81
- delete process.env.LLAMACPP_TIMEOUT_MS;
82
-
83
- const config = require("../src/config");
84
- assert.strictEqual(config.llamacpp.timeout, 120000);
85
- });
86
-
87
- it("should use custom timeout when LLAMACPP_TIMEOUT_MS is set", () => {
88
- process.env.MODEL_PROVIDER = "llamacpp";
89
- process.env.LLAMACPP_TIMEOUT_MS = "300000";
90
-
91
- const config = require("../src/config");
92
- assert.strictEqual(config.llamacpp.timeout, 300000);
93
- });
94
-
95
- it("should have null apiKey when LLAMACPP_API_KEY is not set", () => {
96
- process.env.MODEL_PROVIDER = "llamacpp";
97
- delete process.env.LLAMACPP_API_KEY;
98
-
99
- const config = require("../src/config");
100
- assert.strictEqual(config.llamacpp.apiKey, null);
101
- });
102
-
103
- it("should store apiKey when LLAMACPP_API_KEY is set", () => {
104
- process.env.MODEL_PROVIDER = "llamacpp";
105
- process.env.LLAMACPP_API_KEY = "my-secret-key";
106
-
107
- const config = require("../src/config");
108
- assert.strictEqual(config.llamacpp.apiKey, "my-secret-key");
109
- });
110
- });
111
-
112
- describe("Routing", () => {
113
- it("should route to llamacpp when MODEL_PROVIDER is llamacpp", async () => {
114
- process.env.MODEL_PROVIDER = "llamacpp";
115
- process.env.LLAMACPP_ENDPOINT = "http://localhost:8080";
116
-
117
- const config = require("../src/config");
118
- const routing = require("../src/clients/routing");
119
-
120
- const payload = { messages: [{ role: "user", content: "test" }] };
121
- const result = await routing.determineProviderSmart(payload);
122
-
123
- assert.strictEqual(result.provider, "llamacpp");
124
- assert.strictEqual(result.method, "static");
125
- });
126
-
127
- it("should return static routing from determineProviderSmart when tiers disabled", async () => {
128
- process.env.MODEL_PROVIDER = "llamacpp";
129
- process.env.LLAMACPP_ENDPOINT = "http://localhost:8080";
130
-
131
- const config = require("../src/config");
132
- const routing = require("../src/clients/routing");
133
-
134
- const payload = { messages: [{ role: "user", content: "test" }] };
135
- const result = await routing.determineProviderSmart(payload);
136
-
137
- assert.strictEqual(result.provider, "llamacpp");
138
- assert.strictEqual(result.method, "static");
139
- assert.strictEqual(result.reason, "tier_routing_disabled");
140
- });
141
-
142
- it("should throw error when llamacpp is set as FALLBACK_PROVIDER", () => {
143
- process.env.MODEL_PROVIDER = "ollama";
144
- process.env.OLLAMA_MODEL = "qwen2.5-coder:latest";
145
- process.env.FALLBACK_PROVIDER = "llamacpp";
146
- process.env.LLAMACPP_ENDPOINT = "http://localhost:8080";
147
- process.env.FALLBACK_ENABLED = "true";
148
- // Enable tier routing so fallback validation runs
149
- process.env.TIER_SIMPLE = "ollama:llama3.2";
150
- process.env.TIER_MEDIUM = "ollama:llama3.2";
151
- process.env.TIER_COMPLEX = "ollama:llama3.2";
152
- process.env.TIER_REASONING = "ollama:llama3.2";
153
-
154
- assert.throws(
155
- () => require("../src/config"),
156
- /FALLBACK_PROVIDER cannot be 'llamacpp'/
157
- );
158
- });
159
- });
160
-
161
- describe("Response Conversion", () => {
162
- // llama.cpp uses OpenAI-compatible format, so we reuse the same converter
163
-
164
- it("should convert llama.cpp text response to Anthropic format", () => {
165
- process.env.MODEL_PROVIDER = "databricks";
166
- process.env.DATABRICKS_API_KEY = "test-key";
167
- process.env.DATABRICKS_API_BASE = "http://test.com";
168
-
169
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
170
-
171
- const llamacppResponse = {
172
- id: "chatcmpl-123",
173
- object: "chat.completion",
174
- created: 1677652288,
175
- model: "qwen2.5-coder-7b",
176
- choices: [
177
- {
178
- index: 0,
179
- message: {
180
- role: "assistant",
181
- content: "Hello! I'm running on llama.cpp."
182
- },
183
- finish_reason: "stop"
184
- }
185
- ],
186
- usage: {
187
- prompt_tokens: 9,
188
- completion_tokens: 12,
189
- total_tokens: 21
190
- }
191
- };
192
-
193
- const result = convertOpenRouterResponseToAnthropic(llamacppResponse, "claude-sonnet-4-5");
194
-
195
- assert.strictEqual(result.role, "assistant");
196
- assert.strictEqual(result.model, "claude-sonnet-4-5");
197
- assert.strictEqual(Array.isArray(result.content), true);
198
- assert.strictEqual(result.content.length, 1);
199
- assert.strictEqual(result.content[0].type, "text");
200
- assert.strictEqual(result.content[0].text, "Hello! I'm running on llama.cpp.");
201
- assert.strictEqual(result.stop_reason, "end_turn");
202
- assert.strictEqual(result.usage.input_tokens, 9);
203
- assert.strictEqual(result.usage.output_tokens, 12);
204
- });
205
-
206
- it("should convert llama.cpp tool call response to Anthropic format", () => {
207
- process.env.MODEL_PROVIDER = "databricks";
208
- process.env.DATABRICKS_API_KEY = "test-key";
209
- process.env.DATABRICKS_API_BASE = "http://test.com";
210
-
211
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
212
-
213
- const llamacppResponse = {
214
- id: "chatcmpl-123",
215
- object: "chat.completion",
216
- model: "qwen2.5-coder-7b",
217
- choices: [
218
- {
219
- index: 0,
220
- message: {
221
- role: "assistant",
222
- content: "I'll read that file for you.",
223
- tool_calls: [
224
- {
225
- id: "call_abc123",
226
- type: "function",
227
- function: {
228
- name: "Read",
229
- arguments: JSON.stringify({
230
- file_path: "/tmp/example.txt"
231
- })
232
- }
233
- }
234
- ]
235
- },
236
- finish_reason: "tool_calls"
237
- }
238
- ],
239
- usage: {
240
- prompt_tokens: 50,
241
- completion_tokens: 30,
242
- total_tokens: 80
243
- }
244
- };
245
-
246
- const result = convertOpenRouterResponseToAnthropic(llamacppResponse, "claude-sonnet-4-5");
247
-
248
- assert.strictEqual(result.role, "assistant");
249
- assert.strictEqual(result.content.length, 2); // text + tool_use
250
- assert.strictEqual(result.content[0].type, "text");
251
- assert.strictEqual(result.content[0].text, "I'll read that file for you.");
252
- assert.strictEqual(result.content[1].type, "tool_use");
253
- assert.strictEqual(result.content[1].name, "Read");
254
- assert.strictEqual(result.content[1].id, "call_abc123");
255
- assert.deepStrictEqual(result.content[1].input, {
256
- file_path: "/tmp/example.txt"
257
- });
258
- assert.strictEqual(result.stop_reason, "tool_use");
259
- });
260
-
261
- it("should convert llama.cpp parallel tool calls to Anthropic format", () => {
262
- process.env.MODEL_PROVIDER = "databricks";
263
- process.env.DATABRICKS_API_KEY = "test-key";
264
- process.env.DATABRICKS_API_BASE = "http://test.com";
265
-
266
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
267
-
268
- const llamacppResponse = {
269
- id: "chatcmpl-123",
270
- model: "qwen2.5-coder-7b",
271
- choices: [
272
- {
273
- message: {
274
- role: "assistant",
275
- content: "I'll search for both patterns.",
276
- tool_calls: [
277
- {
278
- id: "call_1",
279
- type: "function",
280
- function: {
281
- name: "Grep",
282
- arguments: JSON.stringify({ pattern: "TODO" })
283
- }
284
- },
285
- {
286
- id: "call_2",
287
- type: "function",
288
- function: {
289
- name: "Grep",
290
- arguments: JSON.stringify({ pattern: "FIXME" })
291
- }
292
- }
293
- ]
294
- },
295
- finish_reason: "tool_calls"
296
- }
297
- ],
298
- usage: { prompt_tokens: 30, completion_tokens: 40, total_tokens: 70 }
299
- };
300
-
301
- const result = convertOpenRouterResponseToAnthropic(llamacppResponse, "claude-sonnet-4-5");
302
-
303
- assert.strictEqual(result.content.length, 3); // text + 2 tool_uses
304
- assert.strictEqual(result.content[0].type, "text");
305
- assert.strictEqual(result.content[1].type, "tool_use");
306
- assert.strictEqual(result.content[1].name, "Grep");
307
- assert.strictEqual(result.content[1].id, "call_1");
308
- assert.strictEqual(result.content[2].type, "tool_use");
309
- assert.strictEqual(result.content[2].name, "Grep");
310
- assert.strictEqual(result.content[2].id, "call_2");
311
- });
312
-
313
- it("should handle llama.cpp response with only tool calls (no text content)", () => {
314
- process.env.MODEL_PROVIDER = "databricks";
315
- process.env.DATABRICKS_API_KEY = "test-key";
316
- process.env.DATABRICKS_API_BASE = "http://test.com";
317
-
318
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
319
-
320
- const llamacppResponse = {
321
- id: "chatcmpl-123",
322
- model: "qwen2.5-coder-7b",
323
- choices: [
324
- {
325
- message: {
326
- role: "assistant",
327
- content: null, // llama.cpp may return null content with tool calls
328
- tool_calls: [
329
- {
330
- id: "call_xyz",
331
- type: "function",
332
- function: {
333
- name: "Bash",
334
- arguments: JSON.stringify({ command: "pwd" })
335
- }
336
- }
337
- ]
338
- },
339
- finish_reason: "tool_calls"
340
- }
341
- ],
342
- usage: { prompt_tokens: 20, completion_tokens: 15, total_tokens: 35 }
343
- };
344
-
345
- const result = convertOpenRouterResponseToAnthropic(llamacppResponse, "claude-sonnet-4-5");
346
-
347
- // Should have tool_use block (at least one)
348
- assert.strictEqual(result.role, "assistant");
349
- assert.strictEqual(Array.isArray(result.content), true);
350
- assert.strictEqual(result.content.length >= 1, true);
351
- // Find the tool_use block
352
- const toolUseBlock = result.content.find(c => c.type === "tool_use");
353
- assert.strictEqual(toolUseBlock !== undefined, true);
354
- assert.strictEqual(toolUseBlock.name, "Bash");
355
- });
356
- });
357
-
358
- describe("Message Conversion", () => {
359
- it("should convert Anthropic messages to llama.cpp (OpenAI) format", () => {
360
- process.env.MODEL_PROVIDER = "databricks";
361
- process.env.DATABRICKS_API_KEY = "test-key";
362
- process.env.DATABRICKS_API_BASE = "http://test.com";
363
-
364
- const { convertAnthropicMessagesToOpenRouter } = require("../src/clients/openrouter-utils");
365
-
366
- const anthropicMessages = [
367
- {
368
- role: "user",
369
- content: [
370
- { type: "text", text: "What is 2 + 2?" }
371
- ]
372
- },
373
- {
374
- role: "assistant",
375
- content: [
376
- { type: "text", text: "2 + 2 equals 4." }
377
- ]
378
- }
379
- ];
380
-
381
- const result = convertAnthropicMessagesToOpenRouter(anthropicMessages);
382
-
383
- assert.strictEqual(result.length, 2);
384
- assert.strictEqual(result[0].role, "user");
385
- assert.strictEqual(result[0].content, "What is 2 + 2?");
386
- assert.strictEqual(result[1].role, "assistant");
387
- assert.strictEqual(result[1].content, "2 + 2 equals 4.");
388
- });
389
-
390
- it("should convert Anthropic tool_result messages to llama.cpp (OpenAI) format", () => {
391
- process.env.MODEL_PROVIDER = "databricks";
392
- process.env.DATABRICKS_API_KEY = "test-key";
393
- process.env.DATABRICKS_API_BASE = "http://test.com";
394
-
395
- const { convertAnthropicMessagesToOpenRouter } = require("../src/clients/openrouter-utils");
396
-
397
- // Must have a preceding assistant message with tool_use for tool_result to be valid
398
- const anthropicMessages = [
399
- {
400
- role: "user",
401
- content: [{ type: "text", text: "Run a command" }]
402
- },
403
- {
404
- role: "assistant",
405
- content: [
406
- { type: "text", text: "I'll run that command." },
407
- {
408
- type: "tool_use",
409
- id: "call_456",
410
- name: "Bash",
411
- input: { command: "echo hello" }
412
- }
413
- ]
414
- },
415
- {
416
- role: "user",
417
- content: [
418
- {
419
- type: "tool_result",
420
- tool_use_id: "call_456",
421
- content: "hello"
422
- }
423
- ]
424
- }
425
- ];
426
-
427
- const result = convertAnthropicMessagesToOpenRouter(anthropicMessages);
428
-
429
- // Should have user message, assistant message with tool call, and tool result
430
- assert.strictEqual(result.length >= 3, true);
431
- // Find the tool result message
432
- const toolResultMsg = result.find(m => m.role === "tool");
433
- assert.strictEqual(toolResultMsg !== undefined, true);
434
- assert.strictEqual(toolResultMsg.tool_call_id, "call_456");
435
- assert.strictEqual(toolResultMsg.content, "hello");
436
- });
437
- });
438
-
439
- describe("Tool Conversion", () => {
440
- it("should convert Anthropic tools to llama.cpp (OpenAI) format", () => {
441
- process.env.MODEL_PROVIDER = "databricks";
442
- process.env.DATABRICKS_API_KEY = "test-key";
443
- process.env.DATABRICKS_API_BASE = "http://test.com";
444
-
445
- const { convertAnthropicToolsToOpenRouter } = require("../src/clients/openrouter-utils");
446
-
447
- const anthropicTools = [
448
- {
449
- name: "Read",
450
- description: "Read a file from disk",
451
- input_schema: {
452
- type: "object",
453
- properties: {
454
- file_path: { type: "string", description: "Path to the file" }
455
- },
456
- required: ["file_path"]
457
- }
458
- }
459
- ];
460
-
461
- const result = convertAnthropicToolsToOpenRouter(anthropicTools);
462
-
463
- assert.strictEqual(result.length, 1);
464
- assert.strictEqual(result[0].type, "function");
465
- assert.strictEqual(result[0].function.name, "Read");
466
- assert.strictEqual(result[0].function.description, "Read a file from disk");
467
- assert.deepStrictEqual(result[0].function.parameters, {
468
- type: "object",
469
- properties: {
470
- file_path: { type: "string", description: "Path to the file" }
471
- },
472
- required: ["file_path"]
473
- });
474
- });
475
- });
476
-
477
- describe("Error Handling", () => {
478
- it("should throw error when llama.cpp response has no choices", () => {
479
- process.env.MODEL_PROVIDER = "databricks";
480
- process.env.DATABRICKS_API_KEY = "test-key";
481
- process.env.DATABRICKS_API_BASE = "http://test.com";
482
-
483
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
484
-
485
- const errorResponse = {
486
- error: {
487
- message: "Model not loaded",
488
- type: "invalid_request_error"
489
- }
490
- };
491
-
492
- assert.throws(
493
- () => convertOpenRouterResponseToAnthropic(errorResponse, "test-model"),
494
- /No choices in OpenRouter response/
495
- );
496
- });
497
-
498
- it("should throw error when llama.cpp response has empty choices array", () => {
499
- process.env.MODEL_PROVIDER = "databricks";
500
- process.env.DATABRICKS_API_KEY = "test-key";
501
- process.env.DATABRICKS_API_BASE = "http://test.com";
502
-
503
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
504
-
505
- const emptyChoicesResponse = {
506
- id: "chatcmpl-123",
507
- model: "qwen2.5-coder-7b",
508
- choices: [],
509
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
510
- };
511
-
512
- assert.throws(
513
- () => convertOpenRouterResponseToAnthropic(emptyChoicesResponse, "test-model"),
514
- /No choices in OpenRouter response/
515
- );
516
- });
517
-
518
- it("should handle malformed tool call arguments gracefully", () => {
519
- process.env.MODEL_PROVIDER = "databricks";
520
- process.env.DATABRICKS_API_KEY = "test-key";
521
- process.env.DATABRICKS_API_BASE = "http://test.com";
522
-
523
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
524
-
525
- const responseWithBadArgs = {
526
- id: "chatcmpl-123",
527
- model: "qwen2.5-coder-7b",
528
- choices: [
529
- {
530
- message: {
531
- role: "assistant",
532
- content: "Using tool",
533
- tool_calls: [
534
- {
535
- id: "call_bad",
536
- type: "function",
537
- function: {
538
- name: "Read",
539
- arguments: "invalid json {"
540
- }
541
- }
542
- ]
543
- },
544
- finish_reason: "tool_calls"
545
- }
546
- ],
547
- usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30 }
548
- };
549
-
550
- const result = convertOpenRouterResponseToAnthropic(responseWithBadArgs, "test-model");
551
-
552
- // Should still convert, but with empty input object
553
- assert.strictEqual(result.content[1].type, "tool_use");
554
- assert.deepStrictEqual(result.content[1].input, {});
555
- });
556
- });
557
-
558
- describe("Finish Reason Mapping", () => {
559
- it("should map stop finish_reason to end_turn", () => {
560
- process.env.MODEL_PROVIDER = "databricks";
561
- process.env.DATABRICKS_API_KEY = "test-key";
562
- process.env.DATABRICKS_API_BASE = "http://test.com";
563
-
564
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
565
-
566
- const response = {
567
- choices: [
568
- {
569
- message: { role: "assistant", content: "Complete" },
570
- finish_reason: "stop"
571
- }
572
- ],
573
- usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 }
574
- };
575
-
576
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
577
- assert.strictEqual(result.stop_reason, "end_turn");
578
- });
579
-
580
- it("should map tool_calls finish_reason to tool_use", () => {
581
- process.env.MODEL_PROVIDER = "databricks";
582
- process.env.DATABRICKS_API_KEY = "test-key";
583
- process.env.DATABRICKS_API_BASE = "http://test.com";
584
-
585
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
586
-
587
- const response = {
588
- choices: [
589
- {
590
- message: {
591
- role: "assistant",
592
- content: "Executing tool",
593
- tool_calls: [
594
- {
595
- id: "call_1",
596
- type: "function",
597
- function: { name: "test", arguments: "{}" }
598
- }
599
- ]
600
- },
601
- finish_reason: "tool_calls"
602
- }
603
- ],
604
- usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 }
605
- };
606
-
607
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
608
- assert.strictEqual(result.stop_reason, "tool_use");
609
- });
610
-
611
- it("should map length finish_reason to max_tokens", () => {
612
- process.env.MODEL_PROVIDER = "databricks";
613
- process.env.DATABRICKS_API_KEY = "test-key";
614
- process.env.DATABRICKS_API_BASE = "http://test.com";
615
-
616
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
617
-
618
- const response = {
619
- choices: [
620
- {
621
- message: { role: "assistant", content: "Truncated response..." },
622
- finish_reason: "length"
623
- }
624
- ],
625
- usage: { prompt_tokens: 5, completion_tokens: 100, total_tokens: 105 }
626
- };
627
-
628
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
629
- assert.strictEqual(result.stop_reason, "max_tokens");
630
- });
631
- });
632
-
633
- describe("Usage Metrics", () => {
634
- it("should correctly map llama.cpp usage to Anthropic format", () => {
635
- process.env.MODEL_PROVIDER = "databricks";
636
- process.env.DATABRICKS_API_KEY = "test-key";
637
- process.env.DATABRICKS_API_BASE = "http://test.com";
638
-
639
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
640
-
641
- const response = {
642
- choices: [
643
- {
644
- message: { role: "assistant", content: "Response" },
645
- finish_reason: "stop"
646
- }
647
- ],
648
- usage: {
649
- prompt_tokens: 200,
650
- completion_tokens: 100,
651
- total_tokens: 300
652
- }
653
- };
654
-
655
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
656
-
657
- assert.strictEqual(result.usage.input_tokens, 200);
658
- assert.strictEqual(result.usage.output_tokens, 100);
659
- });
660
-
661
- it("should handle missing usage gracefully", () => {
662
- process.env.MODEL_PROVIDER = "databricks";
663
- process.env.DATABRICKS_API_KEY = "test-key";
664
- process.env.DATABRICKS_API_BASE = "http://test.com";
665
-
666
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
667
-
668
- const response = {
669
- choices: [
670
- {
671
- message: { role: "assistant", content: "Response" },
672
- finish_reason: "stop"
673
- }
674
- ]
675
- // No usage field
676
- };
677
-
678
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
679
-
680
- assert.strictEqual(result.usage.input_tokens, 0);
681
- assert.strictEqual(result.usage.output_tokens, 0);
682
- });
683
-
684
- it("should filter duplicate tool call JSON from content when tool_calls are present", () => {
685
- process.env.MODEL_PROVIDER = "databricks";
686
- process.env.DATABRICKS_API_KEY = "test-key";
687
- process.env.DATABRICKS_API_BASE = "http://test.com";
688
-
689
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
690
-
691
- // Simulate llama.cpp response with BOTH content (as JSON) and tool_calls
692
- const response = {
693
- id: "chatcmpl-123",
694
- choices: [
695
- {
696
- message: {
697
- role: "assistant",
698
- content: '{"type": "function", "function": {"name": "Write", "parameters": {"file_path": "test.cpp", "content": "int main() {}"}}}',
699
- tool_calls: [
700
- {
701
- id: "call_abc123",
702
- type: "function",
703
- function: {
704
- name: "Write",
705
- arguments: '{"file_path": "test.cpp", "content": "int main() {}"}'
706
- }
707
- }
708
- ]
709
- },
710
- finish_reason: "tool_calls"
711
- }
712
- ],
713
- usage: {
714
- prompt_tokens: 100,
715
- completion_tokens: 50
716
- }
717
- };
718
-
719
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
720
-
721
- // Should have only 1 content block (tool_use), not 2 (text + tool_use)
722
- assert.strictEqual(result.content.length, 1);
723
- assert.strictEqual(result.content[0].type, "tool_use");
724
- assert.strictEqual(result.content[0].name, "Write");
725
- assert.strictEqual(result.stop_reason, "tool_use");
726
-
727
- // Verify the JSON text was NOT included as a text block
728
- const textBlocks = result.content.filter(block => block.type === "text");
729
- assert.strictEqual(textBlocks.length, 0, "Should not include text block with duplicate JSON");
730
- });
731
-
732
- it("should preserve normal text content when tool_calls are NOT present", () => {
733
- process.env.MODEL_PROVIDER = "databricks";
734
- process.env.DATABRICKS_API_KEY = "test-key";
735
- process.env.DATABRICKS_API_BASE = "http://test.com";
736
-
737
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
738
-
739
- const response = {
740
- id: "chatcmpl-456",
741
- choices: [
742
- {
743
- message: {
744
- role: "assistant",
745
- content: "Here is the code you requested.",
746
- // No tool_calls
747
- },
748
- finish_reason: "stop"
749
- }
750
- ],
751
- usage: {
752
- prompt_tokens: 50,
753
- completion_tokens: 25
754
- }
755
- };
756
-
757
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
758
-
759
- // Should have 1 text block
760
- assert.strictEqual(result.content.length, 1);
761
- assert.strictEqual(result.content[0].type, "text");
762
- assert.strictEqual(result.content[0].text, "Here is the code you requested.");
763
- assert.strictEqual(result.stop_reason, "end_turn");
764
- });
765
-
766
- it("should preserve text content with tool_calls when text is NOT JSON", () => {
767
- process.env.MODEL_PROVIDER = "databricks";
768
- process.env.DATABRICKS_API_KEY = "test-key";
769
- process.env.DATABRICKS_API_BASE = "http://test.com";
770
-
771
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
772
-
773
- // Some models include explanatory text before/with tool calls
774
- const response = {
775
- id: "chatcmpl-789",
776
- choices: [
777
- {
778
- message: {
779
- role: "assistant",
780
- content: "I'll write the file for you now.",
781
- tool_calls: [
782
- {
783
- id: "call_xyz789",
784
- type: "function",
785
- function: {
786
- name: "Write",
787
- arguments: '{"file_path": "test.cpp", "content": "int main() {}"}'
788
- }
789
- }
790
- ]
791
- },
792
- finish_reason: "tool_calls"
793
- }
794
- ],
795
- usage: {
796
- prompt_tokens: 100,
797
- completion_tokens: 60
798
- }
799
- };
800
-
801
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
802
-
803
- // Should have 2 content blocks (text + tool_use)
804
- assert.strictEqual(result.content.length, 2);
805
- assert.strictEqual(result.content[0].type, "text");
806
- assert.strictEqual(result.content[0].text, "I'll write the file for you now.");
807
- assert.strictEqual(result.content[1].type, "tool_use");
808
- assert.strictEqual(result.content[1].name, "Write");
809
- assert.strictEqual(result.stop_reason, "tool_use");
810
- });
811
-
812
- it("should filter malformed JSON when model outputs ONLY JSON without tool_calls", () => {
813
- process.env.MODEL_PROVIDER = "databricks";
814
- process.env.DATABRICKS_API_KEY = "test-key";
815
- process.env.DATABRICKS_API_BASE = "http://test.com";
816
-
817
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
818
-
819
- // Simulate llama.cpp model that outputs JSON in content but doesn't provide tool_calls
820
- // This is a model training/configuration issue - model learned to output JSON
821
- // but llama.cpp server isn't converting it to structured tool_calls
822
- const response = {
823
- id: "chatcmpl-malformed",
824
- choices: [
825
- {
826
- message: {
827
- role: "assistant",
828
- content: '{"function": "Write", "parameters": {"file_path": "test.go", "content": "package main"}}',
829
- // No tool_calls array - model error!
830
- },
831
- finish_reason: "stop"
832
- }
833
- ],
834
- usage: {
835
- prompt_tokens: 50,
836
- completion_tokens: 30
837
- }
838
- };
839
-
840
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
841
-
842
- // Should have 1 empty text block (JSON was filtered out)
843
- assert.strictEqual(result.content.length, 1);
844
- assert.strictEqual(result.content[0].type, "text");
845
- assert.strictEqual(result.content[0].text, "");
846
- assert.strictEqual(result.stop_reason, "end_turn");
847
- });
848
-
849
- it("should filter alternative JSON formats without tool_calls", () => {
850
- process.env.MODEL_PROVIDER = "databricks";
851
- process.env.DATABRICKS_API_KEY = "test-key";
852
- process.env.DATABRICKS_API_BASE = "http://test.com";
853
-
854
- const { convertOpenRouterResponseToAnthropic } = require("../src/clients/openrouter-utils");
855
-
856
- // Test the other JSON format seen in the wild
857
- const response = {
858
- id: "chatcmpl-alt-format",
859
- choices: [
860
- {
861
- message: {
862
- role: "assistant",
863
- content: '{"type": "function", "function": {"name": "Read", "arguments": {"file_path": "config.json"}}}',
864
- },
865
- finish_reason: "stop"
866
- }
867
- ],
868
- usage: {
869
- prompt_tokens: 40,
870
- completion_tokens: 25
871
- }
872
- };
873
-
874
- const result = convertOpenRouterResponseToAnthropic(response, "test-model");
875
-
876
- // Should filter out the JSON
877
- assert.strictEqual(result.content.length, 1);
878
- assert.strictEqual(result.content[0].type, "text");
879
- assert.strictEqual(result.content[0].text, "");
880
- });
881
- });
882
- });