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