lynkr 9.7.2 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +29 -19
  2. package/bin/cli.js +11 -0
  3. package/bin/lynkr-init.js +14 -1
  4. package/bin/lynkr-usage.js +78 -0
  5. package/bin/wrap.js +60 -35
  6. package/config/difficulty-anchors.json +22 -0
  7. package/package.json +24 -3
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/calibrate-thresholds.js +38 -157
  10. package/scripts/compact-dictionary.js +204 -0
  11. package/scripts/test-deduplication.js +448 -0
  12. package/scripts/ws7-anchor-replay.js +108 -0
  13. package/skills/lynkr/SKILL.md +195 -0
  14. package/src/agents/context-manager.js +18 -2
  15. package/src/agents/definitions/loader.js +90 -0
  16. package/src/agents/executor.js +24 -2
  17. package/src/agents/index.js +9 -1
  18. package/src/agents/parallel-coordinator.js +2 -2
  19. package/src/agents/reflector.js +11 -1
  20. package/src/api/middleware/loop-guard.js +87 -0
  21. package/src/api/middleware/request-logging.js +5 -64
  22. package/src/api/middleware/session.js +0 -0
  23. package/src/api/openai-router.js +120 -101
  24. package/src/api/providers-handler.js +27 -2
  25. package/src/api/router.js +450 -125
  26. package/src/budget/index.js +2 -19
  27. package/src/cache/semantic.js +9 -0
  28. package/src/clients/databricks.js +459 -146
  29. package/src/clients/gpt-utils.js +11 -105
  30. package/src/clients/openai-format.js +10 -3
  31. package/src/clients/openrouter-utils.js +49 -24
  32. package/src/clients/prompt-cache-injection.js +1 -0
  33. package/src/clients/provider-capabilities.js +1 -1
  34. package/src/clients/responses-format.js +34 -3
  35. package/src/clients/routing.js +15 -0
  36. package/src/config/index.js +36 -2
  37. package/src/context/gcf.js +275 -0
  38. package/src/context/tool-result-compressor.js +51 -9
  39. package/src/dashboard/api.js +1 -0
  40. package/src/logger/index.js +14 -1
  41. package/src/memory/search.js +12 -40
  42. package/src/memory/tools.js +3 -24
  43. package/src/orchestrator/bypass.js +4 -2
  44. package/src/orchestrator/index.js +144 -88
  45. package/src/routing/affinity-store.js +194 -0
  46. package/src/routing/agentic-detector.js +36 -6
  47. package/src/routing/bandit.js +25 -6
  48. package/src/routing/calibration.js +212 -0
  49. package/src/routing/client-profiles.js +292 -0
  50. package/src/routing/complexity-analyzer.js +48 -11
  51. package/src/routing/deescalator.js +148 -0
  52. package/src/routing/degradation.js +109 -0
  53. package/src/routing/feedback.js +157 -0
  54. package/src/routing/index.js +897 -87
  55. package/src/routing/intent-score.js +339 -0
  56. package/src/routing/interaction.js +3 -0
  57. package/src/routing/knn-router.js +70 -21
  58. package/src/routing/model-registry.js +28 -7
  59. package/src/routing/model-tiers.js +25 -2
  60. package/src/routing/reward-pipeline.js +68 -2
  61. package/src/routing/risk-analyzer.js +30 -1
  62. package/src/routing/risk-classifier.js +6 -2
  63. package/src/routing/session-affinity.js +162 -34
  64. package/src/routing/telemetry.js +298 -10
  65. package/src/routing/verifier.js +267 -0
  66. package/src/server.js +66 -21
  67. package/src/sessions/cleanup.js +17 -0
  68. package/src/tools/index.js +1 -15
  69. package/src/tools/smart-selection.js +10 -0
  70. package/src/tools/web-client.js +3 -3
  71. package/.eslintrc.cjs +0 -12
  72. package/benchmark-configs/litellm_config.yaml +0 -86
  73. package/benchmark-configs/lynkr.env +0 -48
  74. package/benchmark-configs/portkey-config.json +0 -60
  75. package/benchmark-configs/portkey-docker.sh +0 -23
  76. package/benchmark-tier-routing.js +0 -449
  77. package/funding.json +0 -110
  78. package/src/api/middleware/validation.js +0 -261
  79. package/src/routing/drift-monitor.js +0 -113
  80. package/src/workers/helpers.js +0 -185
@@ -1,449 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Full-Stack Benchmark: Lynkr vs LiteLLM vs Portkey
4
- *
5
- * Tests 6 scenarios that cover Lynkr's full optimization stack:
6
- * 1. Simple Q&A → tier routing only
7
- * 2. Tool-heavy request → smart tool selection (50-70% token reduction)
8
- * 3. Long history → history compression
9
- * 4. Large payload → TOON compression
10
- * 5. Repeated prompts → semantic cache (2nd call should be ~0 tokens billed)
11
- * 6. Reasoning request → tier routing to top model
12
- *
13
- * LiteLLM and Portkey send tokens as-is. Lynkr compresses before the model sees them.
14
- * The delta in input_tokens IS the compression saving.
15
- *
16
- * Usage:
17
- * ANTHROPIC_API_KEY=sk-ant-... \
18
- * LITELLM_MASTER_KEY=sk-1234 \
19
- * PORTKEY_API_KEY=your-key \
20
- * node benchmark-tier-routing.js
21
- */
22
-
23
- // ─── Proxy config ─────────────────────────────────────────────────────────────
24
-
25
- const PROXIES = [
26
- {
27
- name: 'Lynkr',
28
- url: process.env.LYNKR_URL ?? 'http://localhost:8081',
29
- apiKey: process.env.ANTHROPIC_API_KEY,
30
- defaultModel: 'claude-sonnet-4-5',
31
- headers: {},
32
- getTier: (_b, h) => h['x-lynkr-tier'] ?? 'unknown',
33
- getModel: (_b, h) => h['x-lynkr-model'] ?? h['x-lynkr-provider'] ?? 'unknown',
34
- },
35
- {
36
- name: 'LiteLLM',
37
- url: process.env.LITELLM_URL ?? 'http://localhost:8082',
38
- apiKey: process.env.LITELLM_MASTER_KEY ?? 'sk-1234',
39
- defaultModel: 'smart-router',
40
- headers: {},
41
- getTier: (_b, h) => {
42
- const cost = parseFloat(h['x-litellm-response-cost-original'] ?? '0');
43
- if (cost === 0) return 'SIMPLE/MEDIUM (Ollama)';
44
- if (cost < 0.01) return 'MEDIUM (Moonshot)';
45
- return 'COMPLEX/REASONING (Azure)';
46
- },
47
- getModel: (_b, h) => {
48
- const cost = parseFloat(h['x-litellm-response-cost-original'] ?? '0');
49
- if (cost === 0) return 'ollama (local/free)';
50
- if (cost < 0.01) return 'moonshot/kimi-k2.6';
51
- return 'azure/gpt-5.2-chat';
52
- },
53
- },
54
- {
55
- name: 'Portkey',
56
- url: process.env.PORTKEY_URL ?? 'http://localhost:8083',
57
- apiKey: process.env.ANTHROPIC_API_KEY,
58
- defaultModel: 'claude-sonnet-4-5',
59
- headers: {
60
- 'x-portkey-provider': 'anthropic',
61
- ...(process.env.PORTKEY_API_KEY ? { 'x-portkey-api-key': process.env.PORTKEY_API_KEY } : {}),
62
- },
63
- getTier: () => 'N/A',
64
- getModel: (b) => b?.model ?? 'claude-sonnet-4-5',
65
- },
66
- ];
67
-
68
- // ─── Pricing per 1M tokens [input, output] USD ───────────────────────────────
69
-
70
- const PRICING = {
71
- 'claude-haiku-4-5': [0.80, 4.00],
72
- 'claude-haiku-3': [0.25, 1.25],
73
- 'claude-sonnet-4-5': [3.00, 15.00],
74
- 'claude-sonnet-3-5': [3.00, 15.00],
75
- 'claude-opus-4': [15.00, 75.00],
76
- 'gpt-4o-mini': [0.15, 0.60],
77
- 'gpt-4o': [2.50, 10.00],
78
- 'o3-mini': [1.10, 4.40],
79
- 'default': [3.00, 15.00],
80
- };
81
-
82
- function costUsd(model, inputTok, outputTok) {
83
- const key = Object.keys(PRICING).find(k => model.toLowerCase().includes(k)) ?? 'default';
84
- const [i, o] = PRICING[key];
85
- return (inputTok / 1e6) * i + (outputTok / 1e6) * o;
86
- }
87
-
88
- // Rough token estimator: 1 token ≈ 4 chars (GPT/Claude rule of thumb)
89
- function estimateTokens(payload) {
90
- return Math.ceil(JSON.stringify(payload).length / 4);
91
- }
92
-
93
- // ─── Reusable tool definitions (simulate a real Claude Code session) ──────────
94
- // 14 tools ≈ 2,500 tokens of tool schema sent on every request without smart selection
95
-
96
- const TOOL_DEFINITIONS = [
97
- { name: 'Read', description: 'Read a file from disk', input_schema: { type: 'object', properties: { file_path: { type: 'string' }, limit: { type: 'number' } }, required: ['file_path'] } },
98
- { name: 'Write', description: 'Write content to a file', input_schema: { type: 'object', properties: { file_path: { type: 'string' }, content: { type: 'string' } }, required: ['file_path', 'content'] } },
99
- { name: 'Edit', description: 'Make targeted edits to a file', input_schema: { type: 'object', properties: { file_path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } }, required: ['file_path', 'old_string', 'new_string'] } },
100
- { name: 'Bash', description: 'Execute a shell command', input_schema: { type: 'object', properties: { command: { type: 'string' }, timeout: { type: 'number' } }, required: ['command'] } },
101
- { name: 'Glob', description: 'Find files matching a pattern', input_schema: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' } }, required: ['pattern'] } },
102
- { name: 'Grep', description: 'Search for patterns in files', input_schema: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' }, glob: { type: 'string' } }, required: ['pattern'] } },
103
- { name: 'WebSearch', description: 'Search the web', input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
104
- { name: 'WebFetch', description: 'Fetch a URL', input_schema: { type: 'object', properties: { url: { type: 'string' }, prompt: { type: 'string' } }, required: ['url'] } },
105
- { name: 'TodoWrite', description: 'Write a todo list', input_schema: { type: 'object', properties: { todos: { type: 'array', items: { type: 'object' } } }, required: ['todos'] } },
106
- { name: 'TodoRead', description: 'Read the current todo list', input_schema: { type: 'object', properties: {} } },
107
- { name: 'Task', description: 'Spawn a subagent', input_schema: { type: 'object', properties: { description: { type: 'string' }, prompt: { type: 'string' } }, required: ['description', 'prompt'] } },
108
- { name: 'NotebookRead', description: 'Read a Jupyter notebook', input_schema: { type: 'object', properties: { notebook_path: { type: 'string' } }, required: ['notebook_path'] } },
109
- { name: 'NotebookEdit', description: 'Edit a Jupyter notebook', input_schema: { type: 'object', properties: { notebook_path: { type: 'string' }, cell_index: { type: 'number' }, new_source: { type: 'string' } }, required: ['notebook_path', 'cell_index', 'new_source'] } },
110
- { name: 'mcp__github__create_pull_request', description: 'Create a GitHub pull request via MCP', input_schema: { type: 'object', properties: { title: { type: 'string' }, body: { type: 'string' }, base: { type: 'string' }, head: { type: 'string' } }, required: ['title', 'body'] } },
111
- ];
112
-
113
- // ─── Scenarios ────────────────────────────────────────────────────────────────
114
-
115
- const SCENARIOS = [
116
- // ── 1. Simple Q&A ─────────────────────────────────────────────────────────
117
- {
118
- id: 'S1', label: 'Simple Q&A',
119
- feature: 'Tier routing → cheap model',
120
- buildPayload: (model) => ({
121
- model, max_tokens: 256,
122
- messages: [{ role: 'user', content: 'What does git stash do?' }],
123
- }),
124
- },
125
-
126
- // ── 2. Tool-heavy (smart tool selection) ──────────────────────────────────
127
- // All 14 tools sent — Lynkr strips irrelevant ones before forwarding
128
- {
129
- id: 'T1', label: 'Tool-heavy (14 tools)',
130
- feature: 'Smart tool selection → strips unused tools',
131
- buildPayload: (model) => ({
132
- model, max_tokens: 512,
133
- tools: TOOL_DEFINITIONS,
134
- messages: [{ role: 'user', content: 'What does the README say about installation?' }],
135
- }),
136
- },
137
- {
138
- id: 'T2', label: 'Tool-heavy (14 tools) – write task',
139
- feature: 'Smart tool selection → keeps only write tools',
140
- buildPayload: (model) => ({
141
- model, max_tokens: 512,
142
- tools: TOOL_DEFINITIONS,
143
- messages: [{ role: 'user', content: 'Edit the config file to set DEBUG=true' }],
144
- }),
145
- },
146
-
147
- // ── 3. Long history (history compression) ─────────────────────────────────
148
- // 8-turn conversation — Lynkr compresses older turns before forwarding
149
- {
150
- id: 'H1', label: 'Long history (8 turns)',
151
- feature: 'History compression → dedups older turns',
152
- buildPayload: (model) => ({
153
- model, max_tokens: 512,
154
- messages: [
155
- { role: 'user', content: 'Can you help me refactor my Express app?' },
156
- { role: 'assistant', content: 'Sure! Let\'s start by reviewing your current structure. What does your folder layout look like?' },
157
- { role: 'user', content: 'I have routes/, controllers/, models/, middleware/ folders.' },
158
- { role: 'assistant', content: 'Good structure. Are you using any ORM, and do you have error handling middleware in place?' },
159
- { role: 'user', content: 'I use Sequelize. Error handling is scattered across controllers right now.' },
160
- { role: 'assistant', content: 'Let\'s centralise error handling first. Create middleware/errorHandler.js and export an express error middleware with four params (err, req, res, next).' },
161
- { role: 'user', content: 'Done. Now I need to add input validation — should I use Joi or express-validator?' },
162
- { role: 'assistant', content: 'For Sequelize projects, Joi pairs well. Install it and create a validate() middleware wrapper.' },
163
- { role: 'user', content: 'Great, now how do I add rate limiting to specific routes only?' },
164
- ],
165
- }),
166
- },
167
-
168
- // ── 4a. TOON – large JSON tool result (file read) ─────────────────────────
169
- // Simulates a tool_result block returning a large JSON config file.
170
- // TOON specifically compresses JSON structures — this is its primary trigger.
171
- {
172
- id: 'L1', label: 'TOON – large JSON tool result',
173
- feature: 'TOON compression → compresses JSON tool_result before forwarding',
174
- buildPayload: (model) => ({
175
- model, max_tokens: 512,
176
- tools: [TOOL_DEFINITIONS[0]], // Read tool only
177
- messages: [
178
- { role: 'user', content: 'Read package.json and tell me the dependencies.' },
179
- { role: 'assistant', content: null,
180
- tool_calls: [{ id: 'tr_001', type: 'function', function: { name: 'Read', arguments: JSON.stringify({ file_path: 'package.json' }) } }] },
181
- { role: 'user', content: [
182
- { type: 'tool_result', tool_use_id: 'tr_001', content: JSON.stringify(generateFakeLargeJsonResult()) },
183
- ]},
184
- { role: 'user', content: 'What are the top-level dependencies?' },
185
- ],
186
- }),
187
- },
188
-
189
- // ── 4b. TOON – large grep/glob JSON result ────────────────────────────────
190
- // Simulates a Bash tool returning a large JSON array of search results.
191
- {
192
- id: 'L2', label: 'TOON – large JSON grep result (~2k tokens)',
193
- feature: 'TOON compression → compresses JSON array tool_result',
194
- buildPayload: (model) => ({
195
- model, max_tokens: 512,
196
- tools: [TOOL_DEFINITIONS[3]], // Bash tool only
197
- messages: [
198
- { role: 'user', content: 'Find all TODO comments in the codebase.' },
199
- { role: 'assistant', content: null,
200
- tool_calls: [{ id: 'tr_002', type: 'function', function: { name: 'Bash', arguments: JSON.stringify({ command: 'grep -rn "TODO" src/' }) } }] },
201
- { role: 'user', content: [
202
- { type: 'tool_result', tool_use_id: 'tr_002', content: JSON.stringify(generateFakeGrepResult()) },
203
- ]},
204
- { role: 'user', content: 'Summarise the most important TODOs.' },
205
- ],
206
- }),
207
- },
208
-
209
- // ── 5. Semantic cache (send same prompt twice) ─────────────────────────────
210
- // First call: billed normally. Second call: Lynkr returns cached response (0 LLM tokens).
211
- {
212
- id: 'SC1', label: 'Cache – first call',
213
- feature: 'Semantic cache – populates cache',
214
- buildPayload: (model) => ({
215
- model, max_tokens: 256,
216
- messages: [{ role: 'user', content: 'Explain the difference between TCP and UDP in two sentences.' }],
217
- }),
218
- },
219
- {
220
- id: 'SC2', label: 'Cache – second call (near-identical)',
221
- feature: 'Semantic cache – should hit cache → 0 tokens billed',
222
- buildPayload: (model) => ({
223
- model, max_tokens: 256,
224
- // Slightly paraphrased — semantic cache threshold 0.95 should still match
225
- messages: [{ role: 'user', content: 'What is the difference between TCP and UDP? Keep it brief.' }],
226
- }),
227
- },
228
-
229
- // ── 6. Reasoning ──────────────────────────────────────────────────────────
230
- {
231
- id: 'R1', label: 'Reasoning – security analysis',
232
- feature: 'Tier routing → top model + risk classifier',
233
- buildPayload: (model) => ({
234
- model, max_tokens: 1024,
235
- messages: [{ role: 'user', content: 'Analyse the security trade-offs of storing JWT tokens in localStorage vs httpOnly cookies for a banking application. Step by step.' }],
236
- }),
237
- },
238
- ];
239
-
240
- // ─── JSON payload generators (TOON compresses these, plain text it ignores) ──
241
-
242
- function generateFakeLargeJsonResult() {
243
- // Simulates a package.json with many dependencies — ~1,800 tokens of JSON
244
- const deps = {};
245
- const devDeps = {};
246
- const packages = [
247
- 'express','lodash','axios','react','typescript','webpack','babel','eslint',
248
- 'jest','mocha','chai','sinon','supertest','dotenv','cors','helmet','morgan',
249
- 'winston','pino','joi','yup','zod','mongoose','sequelize','prisma','knex',
250
- 'redis','ioredis','bull','agenda','node-cron','socket.io','ws','graphql',
251
- 'apollo-server','type-graphql','class-transformer','class-validator','reflect-metadata',
252
- ];
253
- packages.forEach((p, i) => {
254
- const ver = `^${Math.floor(i/10)+1}.${i%10}.${Math.floor(Math.random()*20)}`;
255
- if (i % 3 === 0) devDeps[p] = ver; else deps[p] = ver;
256
- });
257
- return {
258
- name: 'my-app', version: '1.0.0',
259
- scripts: { start: 'node index.js', test: 'jest', build: 'webpack', lint: 'eslint src/' },
260
- dependencies: deps,
261
- devDependencies: devDeps,
262
- engines: { node: '>=18.0.0' },
263
- keywords: ['api','backend','nodejs'],
264
- files: Array.from({ length: 30 }, (_, i) => `src/module${i}.js`),
265
- exports: Object.fromEntries(packages.map(p => [`./${p}`, `./dist/${p}/index.js`])),
266
- };
267
- }
268
-
269
- function generateFakeGrepResult() {
270
- // Simulates grep -rn "TODO" returning a large JSON array — ~1,200 tokens
271
- return Array.from({ length: 60 }, (_, i) => ({
272
- file: `src/${['routes','controllers','models','middleware','utils'][i % 5]}/module${i % 15}.js`,
273
- line: Math.floor(Math.random() * 500) + 1,
274
- match: `TODO: ${['fix error handling','add validation','refactor this','add tests','update docs','remove hardcoded value','add rate limiting','handle edge case'][i % 8]} — assigned to ${['alice','bob','carol','dave'][i % 4]}`,
275
- context: ` // TODO: ${['fix error handling','add validation','refactor this','add tests'][i % 4]}\n function handler${i}(req, res) { return res.json({ status: 'ok' }); }`,
276
- }));
277
- }
278
-
279
- // ─── HTTP request ─────────────────────────────────────────────────────────────
280
-
281
- async function sendRequest(proxy, scenario) {
282
- const payload = scenario.buildPayload(proxy.defaultModel);
283
- const estimatedInputTokens = estimateTokens(payload.messages) + estimateTokens(payload.tools ?? []);
284
- const start = Date.now();
285
-
286
- try {
287
- const res = await fetch(`${proxy.url}/v1/messages`, {
288
- method: 'POST',
289
- headers: {
290
- 'content-type': 'application/json',
291
- 'x-api-key': proxy.apiKey,
292
- 'anthropic-version': '2023-06-01',
293
- ...proxy.headers,
294
- },
295
- body: JSON.stringify(payload),
296
- signal: AbortSignal.timeout(90_000),
297
- });
298
-
299
- const latencyMs = Date.now() - start;
300
- const headers = Object.fromEntries(res.headers.entries());
301
-
302
- if (!res.ok) {
303
- const err = await res.text();
304
- return { ok: false, error: `HTTP ${res.status}: ${err.slice(0, 100)}`, latencyMs, estimatedInputTokens };
305
- }
306
-
307
- const body = await res.json();
308
- const billedInput = body?.usage?.input_tokens ?? 0;
309
- const billedOutput = body?.usage?.output_tokens ?? 0;
310
- const model = proxy.getModel(body, headers);
311
- const tier = proxy.getTier(body, headers);
312
- const cost = costUsd(model, billedInput, billedOutput);
313
- const tokensSaved = Math.max(0, estimatedInputTokens - billedInput);
314
- const compressionPct = estimatedInputTokens > 0
315
- ? ((tokensSaved / estimatedInputTokens) * 100).toFixed(1)
316
- : '0.0';
317
-
318
- return { ok: true, tier, model, billedInput, billedOutput, estimatedInputTokens, tokensSaved, compressionPct, cost, latencyMs };
319
- } catch (e) {
320
- return { ok: false, error: e.message, latencyMs: Date.now() - start, estimatedInputTokens };
321
- }
322
- }
323
-
324
- // ─── Formatting helpers ───────────────────────────────────────────────────────
325
-
326
- const col = (s, w) => String(s ?? '').slice(0, w).padEnd(w);
327
- const $ = (n) => `$${n.toFixed(6)}`;
328
-
329
- // ─── Main ─────────────────────────────────────────────────────────────────────
330
-
331
- async function runBenchmark() {
332
- console.log('\n╔═══════════════════════════════════════════════════════════════════╗');
333
- console.log('║ Full-Stack Benchmark: Lynkr vs LiteLLM vs Portkey ║');
334
- console.log('║ Tests: tier routing · tool selection · history · TOON · cache ║');
335
- console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
336
-
337
- // results[proxyName][scenarioId] = result
338
- const results = {};
339
- for (const p of PROXIES) results[p.name] = {};
340
-
341
- for (const scenario of SCENARIOS) {
342
- process.stdout.write(`\n[${scenario.id}] ${scenario.label.padEnd(35)} `);
343
- for (const proxy of PROXIES) {
344
- process.stdout.write(`${proxy.name}… `);
345
- results[proxy.name][scenario.id] = await sendRequest(proxy, scenario);
346
- await new Promise(r => setTimeout(r, 400));
347
- }
348
- process.stdout.write('✓');
349
- }
350
-
351
- // ─── Per-Scenario Detail ────────────────────────────────────────────────────
352
-
353
- console.log('\n\n\n━━━ PER-SCENARIO DETAIL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
354
-
355
- for (const scenario of SCENARIOS) {
356
- console.log(`\n▸ [${scenario.id}] ${scenario.label}`);
357
- console.log(` Feature under test: ${scenario.feature}`);
358
- console.log(` ${'Proxy'.padEnd(10)} ${'Tier'.padEnd(14)} ${'Model'.padEnd(26)} ${'Est.Tok'.padEnd(9)} ${'Billed'.padEnd(9)} ${'Saved'.padEnd(8)} ${'Compress%'.padEnd(11)} ${'Cost'.padEnd(12)} Latency`);
359
- console.log(' ' + '─'.repeat(110));
360
-
361
- for (const proxy of PROXIES) {
362
- const r = results[proxy.name][scenario.id];
363
- if (!r.ok) {
364
- console.log(` ${col(proxy.name,10)} ERROR: ${r.error?.slice(0,80)}`);
365
- continue;
366
- }
367
- console.log(
368
- ' ' +
369
- col(proxy.name, 10) +
370
- col(r.tier, 14) +
371
- col(r.model, 26) +
372
- col(r.estimatedInputTokens, 9) +
373
- col(r.billedInput, 9) +
374
- col(r.tokensSaved, 8) +
375
- col(r.compressionPct + '%', 11) +
376
- col($(r.cost), 12) +
377
- `${r.latencyMs}ms`
378
- );
379
- }
380
- }
381
-
382
- // ─── Feature-Level Summary ──────────────────────────────────────────────────
383
-
384
- console.log('\n\n━━━ FEATURE SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
385
-
386
- const featureGroups = [
387
- { label: 'Tier Routing (S1, R1)', ids: ['S1', 'R1'] },
388
- { label: 'Smart Tool Selection (T1, T2)', ids: ['T1', 'T2'] },
389
- { label: 'History Compression (H1)', ids: ['H1'] },
390
- { label: 'TOON / JSON Tool Results (L1+L2)', ids: ['L1', 'L2'] },
391
- { label: 'Semantic Cache (SC1 + SC2)', ids: ['SC1','SC2'] },
392
- ];
393
-
394
- for (const group of featureGroups) {
395
- console.log(` ${group.label}`);
396
- for (const proxy of PROXIES) {
397
- const rs = group.ids.map(id => results[proxy.name][id]).filter(r => r?.ok);
398
- if (rs.length === 0) { console.log(` ${proxy.name.padEnd(10)} – no data`); continue; }
399
- const totalCost = rs.reduce((s, r) => s + r.cost, 0);
400
- const totalSaved = rs.reduce((s, r) => s + r.tokensSaved, 0);
401
- const totalEst = rs.reduce((s, r) => s + r.estimatedInputTokens, 0);
402
- const avgCompress = totalEst > 0 ? ((totalSaved / totalEst) * 100).toFixed(1) : '0.0';
403
- console.log(` ${proxy.name.padEnd(10)} cost: ${$(totalCost).padEnd(14)} tokens saved: ${String(totalSaved).padEnd(8)} compression: ${avgCompress}%`);
404
- }
405
- console.log();
406
- }
407
-
408
- // ─── Overall Cost Summary ───────────────────────────────────────────────────
409
-
410
- console.log('\n━━━ OVERALL COST (all scenarios) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
411
-
412
- const totals = PROXIES.map(proxy => {
413
- const rs = Object.values(results[proxy.name]).filter(r => r?.ok);
414
- return {
415
- name: proxy.name,
416
- cost: rs.reduce((s, r) => s + r.cost, 0),
417
- tokensSaved: rs.reduce((s, r) => s + r.tokensSaved, 0),
418
- avgLatency: rs.length ? rs.reduce((s, r) => s + r.latencyMs, 0) / rs.length : 0,
419
- };
420
- }).sort((a, b) => a.cost - b.cost);
421
-
422
- const maxCost = Math.max(...totals.map(t => t.cost), 0.000001);
423
- const baseline = totals.find(t => t.name === 'Portkey')?.cost
424
- ?? totals.find(t => t.cost > 0)?.cost
425
- ?? maxCost;
426
-
427
- for (const t of totals) {
428
- const pct = baseline > 0 ? ((baseline - t.cost) / baseline * 100).toFixed(1) : '0.0';
429
- const barLen = maxCost > 0 ? Math.max(1, Math.round((t.cost / maxCost) * 30)) : 1;
430
- const bar = '█'.repeat(barLen);
431
- console.log(` ${t.name.padEnd(10)} ${$(t.cost).padEnd(14)} ${pct.padStart(5)}% cheaper vs baseline avg ${Math.round(t.avgLatency)}ms ${bar}`);
432
- }
433
-
434
- // ─── Extrapolated: 100k requests/month ─────────────────────────────────────
435
-
436
- console.log('\n\n━━━ EXTRAPOLATED: 100,000 requests/month ──────────────────────────\n');
437
- console.log(' (same scenario mix × scale factor)\n');
438
-
439
- const factor = 100_000 / SCENARIOS.length;
440
- for (const t of totals) {
441
- const monthly = t.cost * factor;
442
- const annualSaving = baseline > 0 ? (baseline - t.cost) * factor * 12 : 0;
443
- console.log(` ${t.name.padEnd(10)} ~$${monthly.toFixed(2).padStart(10)}/month ~$${(annualSaving).toFixed(0).padStart(10)}/year saved vs Portkey`);
444
- }
445
-
446
- console.log('\nDone.\n');
447
- }
448
-
449
- runBenchmark().catch(e => { console.error(e); process.exit(1); });
package/funding.json DELETED
@@ -1,110 +0,0 @@
1
- {
2
- "$schema": "https://fundingjson.org/schema/v1.1.0.json",
3
- "version": "v1.1.0",
4
-
5
- "entity": {
6
- "type": "individual",
7
- "role": "maintainer",
8
- "name": "Vishal Veera Reddy",
9
- "email": "veerareddyvishal56@gmail.com",
10
- "description": "Indian software engineer building open-source AI infrastructure. Sole maintainer of Lynkr, a self-hosted AI gateway that lets developers run any AI coding tool on any LLM provider.",
11
- "webpageUrl": {
12
- "url": "https://github.com/vishalveerareddy123"
13
- }
14
- },
15
-
16
- "projects": [
17
- {
18
- "guid": "lynkr",
19
- "name": "Lynkr",
20
- "description": "A self-hosted AI gateway that decouples AI coding tools (Claude Code, Cursor, Codex, Cline, jcode, Pi) from their default LLM providers. Lynkr auto-detects the connecting tool, translates between Anthropic and OpenAI request formats, and routes to any of 12+ backends (Ollama, AWS Bedrock, Azure OpenAI, OpenRouter, Databricks, Moonshot, Google Vertex, llama.cpp, LM Studio, and more). A request-complexity classifier sends simple turns to free local models and complex ones to flagship cloud models, cutting per-developer AI bills 60-80% while removing vendor lock-in. Includes tool-result compression, MCP Code Mode (96% token reduction on tool definitions), persistent memory, and tier-based routing — all configured through a single .env file.",
21
- "webpageUrl": {
22
- "url": "https://fast-editor.github.io/Lynkr/"
23
- },
24
- "repositoryUrl": {
25
- "url": "https://github.com/Fast-Editor/Lynkr"
26
- },
27
- "licenses": ["spdx:Apache-2.0"],
28
- "tags": [
29
- "ai",
30
- "ai-gateway",
31
- "llm",
32
- "llm-router",
33
- "developer-tools",
34
- "proxy",
35
- "claude-code",
36
- "ollama",
37
- "anthropic",
38
- "openai"
39
- ]
40
- }
41
- ],
42
-
43
- "funding": {
44
- "channels": [
45
- {
46
- "guid": "github-sponsors",
47
- "type": "payment-provider",
48
- "address": "https://github.com/sponsors/vishalveerareddy123",
49
- "description": "Support Lynkr development via GitHub Sponsors."
50
- },
51
- {
52
- "guid": "fossunited-grant",
53
- "type": "other",
54
- "address": "grants@fossunited.org",
55
- "description": "FOSS United Foundation grant channel for institutional FOSS funding."
56
- },
57
- {
58
- "guid": "bank-transfer",
59
- "type": "bank",
60
- "address": "Available on request via the project email.",
61
- "description": "Direct bank transfer for organisations or grant disbursements."
62
- }
63
- ],
64
-
65
- "plans": [
66
- {
67
- "guid": "core-maintenance-2026",
68
- "status": "active",
69
- "name": "Core maintenance + roadmap (12 months)",
70
- "description": "Funds full-time work on Lynkr's core gateway: provider-format conversions, tier routing, tool-call translation across 10+ model formats (Minimax, Qwen, GLM, Llama, DeepSeek, Mistral), tool-result compression, persistent memory, MCP Code Mode, observability, tests, and docs. Estimated cost reflects one Indian maintainer working full-time for a year.",
71
- "amount": 500000,
72
- "currency": "INR",
73
- "frequency": "yearly",
74
- "channels": ["fossunited-grant", "bank-transfer"]
75
- },
76
- {
77
- "guid": "infra-2026",
78
- "status": "active",
79
- "name": "Infrastructure + benchmarks",
80
- "description": "Funds CI runners, benchmark harness for cost/quality/latency comparisons across providers, public dashboard at lynkr.dev, and self-hosted SearXNG + telemetry mirrors used by Lynkr's web search and routing layers.",
81
- "amount": 150000,
82
- "currency": "INR",
83
- "frequency": "yearly",
84
- "channels": ["fossunited-grant", "bank-transfer"]
85
- },
86
- {
87
- "guid": "community-sponsor",
88
- "status": "active",
89
- "name": "Community sponsorship",
90
- "description": "Recurring small-amount sponsorship from individual developers and small teams who use Lynkr.",
91
- "amount": 0,
92
- "currency": "USD",
93
- "frequency": "monthly",
94
- "channels": ["github-sponsors"]
95
- },
96
- {
97
- "guid": "one-time",
98
- "status": "active",
99
- "name": "One-time contribution",
100
- "description": "Any-amount one-time contribution from users or supporters.",
101
- "amount": 0,
102
- "currency": "USD",
103
- "frequency": "one-time",
104
- "channels": ["github-sponsors", "bank-transfer"]
105
- }
106
- ],
107
-
108
- "history": []
109
- }
110
- }