lynkr 9.7.3 → 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 (74) 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/api/middleware/loop-guard.js +87 -0
  15. package/src/api/middleware/request-logging.js +5 -64
  16. package/src/api/middleware/session.js +0 -0
  17. package/src/api/openai-router.js +120 -101
  18. package/src/api/providers-handler.js +27 -2
  19. package/src/api/router.js +450 -125
  20. package/src/budget/index.js +2 -19
  21. package/src/cache/semantic.js +9 -0
  22. package/src/clients/databricks.js +455 -142
  23. package/src/clients/gpt-utils.js +11 -105
  24. package/src/clients/openai-format.js +10 -3
  25. package/src/clients/openrouter-utils.js +49 -24
  26. package/src/clients/prompt-cache-injection.js +1 -0
  27. package/src/clients/provider-capabilities.js +1 -1
  28. package/src/clients/responses-format.js +34 -3
  29. package/src/clients/routing.js +15 -0
  30. package/src/config/index.js +36 -2
  31. package/src/context/gcf.js +275 -0
  32. package/src/context/tool-result-compressor.js +51 -9
  33. package/src/dashboard/api.js +1 -0
  34. package/src/logger/index.js +14 -1
  35. package/src/memory/search.js +12 -40
  36. package/src/memory/tools.js +3 -24
  37. package/src/orchestrator/bypass.js +4 -2
  38. package/src/orchestrator/index.js +120 -85
  39. package/src/routing/affinity-store.js +194 -0
  40. package/src/routing/agentic-detector.js +36 -6
  41. package/src/routing/bandit.js +25 -6
  42. package/src/routing/calibration.js +212 -0
  43. package/src/routing/client-profiles.js +292 -0
  44. package/src/routing/complexity-analyzer.js +48 -11
  45. package/src/routing/deescalator.js +148 -0
  46. package/src/routing/degradation.js +109 -0
  47. package/src/routing/feedback.js +157 -0
  48. package/src/routing/index.js +897 -87
  49. package/src/routing/intent-score.js +339 -0
  50. package/src/routing/interaction.js +3 -0
  51. package/src/routing/knn-router.js +70 -21
  52. package/src/routing/model-registry.js +28 -7
  53. package/src/routing/model-tiers.js +25 -2
  54. package/src/routing/reward-pipeline.js +68 -2
  55. package/src/routing/risk-analyzer.js +30 -1
  56. package/src/routing/risk-classifier.js +6 -2
  57. package/src/routing/session-affinity.js +162 -34
  58. package/src/routing/telemetry.js +286 -13
  59. package/src/routing/verifier.js +267 -0
  60. package/src/server.js +66 -21
  61. package/src/sessions/cleanup.js +17 -0
  62. package/src/tools/index.js +1 -15
  63. package/src/tools/smart-selection.js +10 -0
  64. package/src/tools/web-client.js +3 -3
  65. package/.eslintrc.cjs +0 -12
  66. package/benchmark-configs/litellm_config.yaml +0 -86
  67. package/benchmark-configs/lynkr.env +0 -48
  68. package/benchmark-configs/portkey-config.json +0 -60
  69. package/benchmark-configs/portkey-docker.sh +0 -23
  70. package/benchmark-tier-routing.js +0 -449
  71. package/funding.json +0 -110
  72. package/src/api/middleware/validation.js +0 -261
  73. package/src/routing/drift-monitor.js +0 -113
  74. package/src/workers/helpers.js +0 -185
package/README.md CHANGED
@@ -2,25 +2,27 @@
2
2
 
3
3
  ### An LLM Gateway which optimises your token usage.
4
4
 
5
- **87.6% fewer tokens on JSON tool results. 53% fewer tokens on tool-heavy requests. 171ms semantic cache hits. Zero code changes.**
5
+ **84% fewer tokens on JSON tool results. 53% fewer tokens on tool-heavy requests. Sub-300ms semantic cache hits. Zero code changes.**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/lynkr.svg)](https://www.npmjs.com/package/lynkr)
8
- [![Tests](https://img.shields.io/badge/tests-699%20passing-brightgreen)](https://github.com/Fast-Editor/Lynkr)
8
+ [![Tests](https://img.shields.io/badge/tests-1041%20passing-brightgreen)](https://github.com/Fast-Editor/Lynkr)
9
9
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Node.js](https://img.shields.io/badge/node-20%2B-green)](https://nodejs.org)
11
11
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Fast-Editor/Lynkr)
12
12
 
13
13
  <table>
14
14
  <tr>
15
- <td align="center"><strong>87.6%</strong><br/>JSON Compression</td>
15
+ <td align="center"><strong>84%</strong><br/>JSON Compression</td>
16
16
  <td align="center"><strong>53%</strong><br/>Tool Token Reduction</td>
17
- <td align="center"><strong>171ms</strong><br/>Semantic Cache Hits</td>
17
+ <td align="center"><strong>&lt;300ms</strong><br/>Semantic Cache Hits</td>
18
18
  <td align="center"><strong>13+</strong><br/>LLM Providers</td>
19
19
  <td align="center"><strong>0</strong><br/>Code Changes Required</td>
20
20
  </tr>
21
21
  </table>
22
22
 
23
- > Numbers from a live benchmark against LiteLLM on identical workloads. [See full report →](BENCHMARK_REPORT.md)
23
+ > Numbers from the bundled benchmark against LiteLLM on identical free local backends — run it yourself: `node benchmark-tier-routing.js`. It doubles as a 19-scenario routing regression harness (currently 12/12 correctness checks), and `MODE=routing` runs a routing-only head-to-head that judges **both** proxies on the same acceptable-tier sets — including LiteLLM's Auto Router v2. [How it works →](docs/benchmarking.md)
24
+
25
+ > **Third-party benchmark:** on [RouterArena](https://github.com/RouteWorks/RouterArena) (ICLR 2026, 8,400 queries) Lynkr's routing scores **67.65 arena / 68.41% accuracy at $0.29 per 1K queries with 92.38 robustness** — above GPT-5's built-in router and NotDiamond at a fraction of their cost. [Methodology & caveats →](docs/routerarena-benchmark.md)
24
26
 
25
27
  ---
26
28
 
@@ -40,8 +42,9 @@ lynkr wrap claude
40
42
 
41
43
  **Wrapping gives you:**
42
44
  - ✅ Tier routing (send simple tasks to free Ollama, complex to your subscription/API)
43
- - ✅ TOON/RTK compression (87% token reduction on tool outputs)
44
- - ✅ Semantic caching (171ms cache hits)
45
+ - ✅ Sticky sessions: one routing decision per conversation via content fingerprinting, with automatic escalation when the task outgrows the model
46
+ - ✅ TOON/RTK compression (84% token reduction on large JSON tool outputs)
47
+ - ✅ Semantic caching (sub-300ms cache hits, 0 tokens billed)
45
48
  - ✅ **3-5x more usage from the same subscription limits**
46
49
  - ✅ Works with OAuth (Claude, Copilot, Cursor) or API keys (Aider, Codex)
47
50
 
@@ -180,9 +183,10 @@ Claude Code / Cursor / Codex / Cline / Continue
180
183
  Lynkr
181
184
  ┌─────────────────────┐
182
185
  │ Strip unused tools │ ← 53% fewer tokens on tool calls
183
- │ Compress JSON blobs │ ← 87.6% on large tool results
184
- │ Semantic cache │ ← 171ms hits, 0 tokens billed
186
+ │ Compress JSON blobs │ ← 84% on large tool results
187
+ │ Semantic cache │ ← <300ms hits, 0 tokens billed
185
188
  │ Route by complexity │ ← cheap model for simple, cloud for hard
189
+ │ Learn from outcomes │ ← kNN + bandit + auto-calibration
186
190
  └─────────────────────┘
187
191
 
188
192
  Ollama | Bedrock | Azure | Moonshot | OpenRouter | OpenAI
@@ -190,9 +194,10 @@ Claude Code / Cursor / Codex / Cline / Continue
190
194
 
191
195
  **What you get:**
192
196
  - ✅ **53% fewer tokens** on tool-heavy requests (Claude Code, Cursor sessions)
193
- - ✅ **87.6% compression** on large JSON tool results (grep, file reads, test output)
194
- - ✅ **Semantic cache** serves repeated queries in 171ms with 0 tokens billed
195
- - ✅ **Automatic tier routing** — simple questions go to cheap models, complex ones escalate
197
+ - ✅ **84% compression** on large JSON tool results (grep, file reads, test output)
198
+ - ✅ **Semantic cache** serves repeated queries in under 300ms with 0 tokens billed
199
+ - ✅ **Automatic tier routing** — simple questions go to cheap models, complex ones escalate; sessions stick to one model until the task genuinely outgrows it
200
+ - ✅ **A closed learning loop** — every outcome trains a kNN router and bandit, and tier thresholds re-calibrate nightly from your own traffic
196
201
  - ✅ Route through **your company's infrastructure** (Databricks, Azure, Bedrock)
197
202
  - ✅ **Zero code changes** — just change one environment variable
198
203
 
@@ -245,7 +250,7 @@ Lynkr analyzes each request and routes it to the appropriate tier. Simple questi
245
250
 
246
251
  **Result:** 70-90% of requests use cheaper/faster models. Only hard problems hit expensive models.
247
252
 
248
- Tier configuration is strictly authoritative — bandit exploration is constrained to the models you've listed in `TIER_*`, and multi-turn conversations score with a recency-weighted sliding window so context isn't lost on short follow-ups. See [`docs/intent-window-routing.md`](docs/intent-window-routing.md).
253
+ Tier configuration is strictly authoritative — bandit exploration is constrained to the models you've listed in `TIER_*`, and multi-turn conversations score with a recency-weighted sliding window so context isn't lost on short follow-ups. Conversations get a content-fingerprint session id (clients like Claude Code send none), the decision pins for the session, and a guarded escape ladder (risk keywords, force phrases, score drift, context overflow) re-escalates the moment a task outgrows its model. Full pipeline: [`docs/routing-intelligence.md`](docs/routing-intelligence.md) · intent scorer: [`docs/intent-window-routing.md`](docs/intent-window-routing.md) · verify any change: [`docs/benchmarking.md`](docs/benchmarking.md).
249
254
 
250
255
  ---
251
256
 
@@ -428,14 +433,19 @@ Lynkr strips irrelevant tool schemas (smart tool selection) and binary-compresse
428
433
 
429
434
  Near-identical prompts return cached responses in 171ms. Zero model tokens billed on a cache hit.
430
435
 
431
- ### Tier routing
436
+ ### Tier routing — vs LiteLLM Auto Router v2 (July 15, 2026)
437
+
438
+ LiteLLM v1.94 shipped a native complexity router (`auto_router/complexity_router`) with the same four tier names Lynkr uses. Head-to-head on the **same backends** with identical prompts, both proxies judged against the same acceptable-tier sets (11 routing scenarios, `MODE=routing node benchmark-tier-routing.js`, LiteLLM config in `litellm-autorouter-v2.yaml`):
432
439
 
433
- | Request | Lynkr routes to | LiteLLM routes to |
440
+ | Router | Routing-correct | Routing overhead |
434
441
  |---|---|---|
435
- | "What does git stash do?" | `minimax-m2.5` (local, free) | Ollama (local) |
436
- | JWT vs cookies security analysis | `moonshot` (cloudcorrect) | **Ollama (local wrong call)** |
442
+ | **Lynkr** | **11/11** | local embedding, ~0 marginal cost/latency after cache |
443
+ | LiteLLM v2 heuristic (default) | 4/11 | <1ms, free but every miss under-routed hard work (banking security analysis, autonomous agentic loop) to the free local model |
444
+ | LiteLLM v2 — LLM classifier | 6–8/11 (non-deterministic across runs) | a paid GPT-5.2 call + ~2–3s on **every** request; fails outright with local classifier models (structured-output errors → silent heuristic fallback) |
445
+
446
+ Lynkr scores cleaned user-authored text against embedding anchors plus 13 weighted heuristic dimensions, detects agentic workflows, and strips harness-injected noise before scoring. LiteLLM's router reads the raw last message and has no verify-then-escalate cascade — its fallbacks trigger only on HTTP errors, never on a bad answer.
437
447
 
438
- Lynkr scores each request on 15 dimensions (token count, code complexity, reasoning markers, risk signals, agentic patterns) and escalates automatically. LiteLLM's `cost-based-routing` sends everything to the cheapest model regardless of complexity.
448
+ _Fairness notes: the 11 scenarios derive from Lynkr's own regression suite, so Lynkr has home-field advantage — the transferable finding is the direction of LiteLLM's failures (systematic under-routing on defaults; per-request cost and instability with the LLM classifier), not the exact scores. Lynkr's top tiers used Azure gpt-5.2-chat / Z.ai GLM-5.2; LiteLLM was given the identical tier targets._
439
449
 
440
450
  ### Cost projection (100,000 requests/month, same backend)
441
451
 
@@ -469,7 +479,7 @@ With tier routing + token optimization: **additional 50-87% savings** on cloud p
469
479
  | **Claude Code native** | ✅ Drop-in | ⚠️ Requires config | ❌ | ⚠️ Partial |
470
480
  | **Cursor native** | ✅ Drop-in | ⚠️ Partial | ❌ | ⚠️ Partial |
471
481
  | **Local models** | Ollama, llama.cpp, LM Studio | Ollama only | ❌ | ❌ |
472
- | **Automatic tier routing** | ✅ 15-dimension scorer | ⚠️ Cost-only | ❌ | ❌ Manual metadata |
482
+ | **Automatic tier routing** | ✅ embedding intent + 13-dimension scorer, verified cascade | ⚠️ Auto Router v2 (v1.94): heuristic under-routes, LLM classifier billed per request | ❌ | ❌ Manual metadata |
473
483
  | **TOON JSON compression** | ✅ up to 87.6% | ❌ | ❌ | ❌ |
474
484
  | **Smart tool selection** | ✅ up to 60% token reduction | ❌ | ❌ | ❌ |
475
485
  | **Semantic cache** | ✅ 171ms hits, 0 tokens | ❌ | ❌ | ✅ Prompt cache only |
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const pkg = require('../package.json');
7
7
  // don't start the proxy. Add new subcommands here, not in scattered binaries.
8
8
  const SUBCOMMANDS = {
9
9
  usage: path.join(__dirname, "lynkr-usage.js"),
10
+ stats: path.join(__dirname, "lynkr-usage.js"),
10
11
  trajectory: path.join(__dirname, "lynkr-trajectory.js"),
11
12
  wrap: path.join(__dirname, "wrap.js"),
12
13
  init: path.join(__dirname, "lynkr-init.js"),
@@ -22,6 +23,15 @@ if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
22
23
  return;
23
24
  }
24
25
 
26
+ // Unknown positional commands must error loudly, not silently boot the
27
+ // server (`lynkr codex` used to start a bare gateway and never say why).
28
+ if (sub && !sub.startsWith('-')) {
29
+ console.error(`Error: unknown command '${sub}'.`);
30
+ console.error(`Did you mean: lynkr wrap ${sub}`);
31
+ console.error(`Known commands: ${Object.keys(SUBCOMMANDS).join(', ')} (or no command to start the server)`);
32
+ process.exit(1);
33
+ }
34
+
25
35
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
26
36
  console.log(pkg.version);
27
37
  process.exit(0);
@@ -37,6 +47,7 @@ Usage:
37
47
  lynkr [options] Start the proxy server (default)
38
48
  lynkr wrap <target> [options] Wrap CLI tools through Lynkr proxy
39
49
  lynkr usage [options] Show AI spend report and tier-routing savings
50
+ lynkr stats [options] Shareable savings-receipt card (also: lynkr usage --card)
40
51
  lynkr trajectory [options] Export agent trajectories as JSONL training data
41
52
 
42
53
  Options:
package/bin/lynkr-init.js CHANGED
@@ -98,6 +98,15 @@ const PROVIDERS = {
98
98
  extras: [],
99
99
  defaultModel: 'anthropic/claude-sonnet-4',
100
100
  },
101
+ edenai: {
102
+ label: 'Eden AI (600+ models, one key, EU/GDPR)',
103
+ local: false,
104
+ creds: [
105
+ { key: 'EDENAI_API_KEY', label: 'Eden AI API key', secret: true },
106
+ ],
107
+ extras: [],
108
+ defaultModel: 'anthropic/claude-sonnet-4-5',
109
+ },
101
110
  databricks: {
102
111
  label: 'Databricks Foundation Models',
103
112
  local: false,
@@ -154,7 +163,7 @@ const PROVIDERS = {
154
163
 
155
164
  const PROVIDER_ORDER = [
156
165
  'ollama', 'llamacpp', 'lmstudio',
157
- 'azure-anthropic', 'azure-openai', 'openai', 'openrouter',
166
+ 'azure-anthropic', 'azure-openai', 'openai', 'openrouter', 'edenai',
158
167
  'databricks', 'bedrock', 'vertex', 'zai', 'moonshot',
159
168
  ];
160
169
  const TIERS = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
@@ -304,6 +313,10 @@ const BASELINE_ENV = {
304
313
  OPENROUTER_EMBEDDINGS_MODEL: 'openai/text-embedding-ada-002',
305
314
  OPENROUTER_ENDPOINT: 'https://openrouter.ai/api/v1/chat/completions',
306
315
  OPENROUTER_MAX_TOOLS_FOR_ROUTING: '15',
316
+ EDENAI_API_KEY: '',
317
+ EDENAI_MODEL: 'openai/gpt-4o-mini',
318
+ EDENAI_EMBEDDINGS_MODEL: 'openai/text-embedding-ada-002',
319
+ EDENAI_ENDPOINT: 'https://api.edenai.run/v3/chat/completions',
307
320
  MOONSHOT_API_KEY: '',
308
321
  MOONSHOT_ENDPOINT: 'https://api.moonshot.ai/v1/chat/completions',
309
322
  MOONSHOT_MODEL: 'kimi-k2.6',
@@ -18,6 +18,10 @@ const path = require("path");
18
18
  // Make sure config/logger pick up the workspace root
19
19
  process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "..");
20
20
 
21
+ // Reports and receipts go to stdout for humans (and screenshots) — telemetry
22
+ // init logs would pollute them. Respect an explicit LOG_LEVEL override.
23
+ if (!process.env.LOG_LEVEL) process.env.LOG_LEVEL = "silent";
24
+
21
25
  const aggregator = require("../src/usage/aggregator");
22
26
 
23
27
  function parseArgs(argv) {
@@ -26,6 +30,7 @@ function parseArgs(argv) {
26
30
  const a = argv[i];
27
31
  const next = argv[i + 1];
28
32
  if (a === "--json") opts.json = true;
33
+ else if (a === "--card") opts.card = true;
29
34
  else if (a === "--days" && next) {
30
35
  opts.window = `${parseInt(next, 10)}d`;
31
36
  i++;
@@ -204,6 +209,74 @@ function printReport(usage) {
204
209
  console.log("");
205
210
  }
206
211
 
212
+ // ---------------------------------------------------------------------------
213
+ // Shareable receipt card (`lynkr stats` / `lynkr usage --card`)
214
+ // ---------------------------------------------------------------------------
215
+
216
+ const LOCAL_PROVIDERS = new Set(["ollama", "llamacpp", "llama-cpp", "llama_cpp", "lmstudio", "lm-studio"]);
217
+
218
+ function printCard(usage, opts) {
219
+ const { totals, byProvider, flagship, window } = usage;
220
+
221
+ let localReqs = 0;
222
+ let cloudReqs = 0;
223
+ for (const [provider, bucket] of Object.entries(byProvider)) {
224
+ if (LOCAL_PROVIDERS.has(String(provider).toLowerCase())) localReqs += bucket.requests;
225
+ else cloudReqs += bucket.requests;
226
+ }
227
+ const totalReqs = localReqs + cloudReqs;
228
+ const pct = (n) => (totalReqs > 0 ? `${Math.round((n / totalReqs) * 100)}%` : "0%");
229
+
230
+ let savings = { total: 0, byCategory: {} };
231
+ try {
232
+ const telemetry = require("../src/routing/telemetry");
233
+ const since = aggregator.resolveSince(opts.window) ?? 0;
234
+ savings = telemetry.getSavingsSummary(since);
235
+ } catch { /* savings table optional */ }
236
+
237
+ const WIDTH = 46;
238
+ const line = (label, value) => {
239
+ const l = ` ${label}`;
240
+ const v = `${value} `;
241
+ const gap = Math.max(1, WIDTH - l.length - v.length);
242
+ return `│${l}${" ".repeat(gap)}${v}│`;
243
+ };
244
+ const blank = `│${" ".repeat(WIDTH)}│`;
245
+
246
+ const rows = [
247
+ `╭${"─".repeat(WIDTH)}╮`,
248
+ line("Lynkr — savings receipt", `last ${window}`),
249
+ blank,
250
+ line("Requests routed", fmtInt(totalReqs)),
251
+ line(" local (free)", `${fmtInt(localReqs)} · ${pct(localReqs)}`),
252
+ line(" cloud", `${fmtInt(cloudReqs)} · ${pct(cloudReqs)}`),
253
+ blank,
254
+ ];
255
+
256
+ if (savings.total > 0) {
257
+ rows.push(line("Tokens saved", fmtTokens(savings.total)));
258
+ const labels = {
259
+ tool_stripping: " tool-schema stripping",
260
+ compression: " JSON compression",
261
+ cache_hit: " semantic cache hits",
262
+ };
263
+ for (const [cat, label] of Object.entries(labels)) {
264
+ if (savings.byCategory[cat]) rows.push(line(label, fmtTokens(savings.byCategory[cat])));
265
+ }
266
+ rows.push(blank);
267
+ }
268
+
269
+ rows.push(line("Est. cost avoided*", fmtUSD(totals.saved)));
270
+ rows.push(blank);
271
+ rows.push(line("github.com/Fast-Editor/Lynkr", `v${require("../package.json").version}`));
272
+ rows.push(`╰${"─".repeat(WIDTH)}╯`);
273
+ rows.push(colour(` *estimate vs routing everything to ${flagship}`, C.dim));
274
+
275
+ console.log("");
276
+ for (const row of rows) console.log(row);
277
+ console.log("");
278
+ }
279
+
207
280
  function main() {
208
281
  const opts = parseArgs(process.argv);
209
282
  const usage = aggregator.getUsage(opts);
@@ -213,6 +286,11 @@ function main() {
213
286
  return;
214
287
  }
215
288
 
289
+ if (opts.card || process.env._LYNKR_SUBCMD === "stats") {
290
+ printCard(usage, opts);
291
+ return;
292
+ }
293
+
216
294
  printReport(usage);
217
295
  }
218
296
 
package/bin/wrap.js CHANGED
@@ -224,7 +224,7 @@ async function wrapClaude() {
224
224
  // Show stats if enabled and clean exit
225
225
  if (process.env.LYNKR_WRAP_SHOW_STATS !== 'false' && code === 0) {
226
226
  try {
227
- await showSessionStats();
227
+ await showSessionStats(port);
228
228
  } catch (err) {
229
229
  // Stats are nice-to-have, don't fail on error
230
230
  }
@@ -323,9 +323,21 @@ async function wrapCodex() {
323
323
  binaryName: 'codex',
324
324
  findBinary: findCodexBinary,
325
325
  envVar: 'OPENAI_API_BASE',
326
+ // Modern Codex (Rust CLI) ignores OPENAI_API_BASE — with ChatGPT-plan
327
+ // auth it talks straight to its own backend. A custom model_provider
328
+ // passed via -c overrides is the only supported redirect, and it beats
329
+ // every config file. env_key must name an env var that is set.
330
+ extraArgs: (port) => [
331
+ '-c', 'model_providers.lynkr.name="Lynkr"',
332
+ '-c', `model_providers.lynkr.base_url="http://localhost:${port}/v1"`,
333
+ '-c', 'model_providers.lynkr.wire_api="responses"',
334
+ '-c', 'model_providers.lynkr.env_key="LYNKR_API_KEY"',
335
+ '-c', 'model_provider="lynkr"',
336
+ ],
337
+ extraEnv: { LYNKR_API_KEY: 'lynkr-local' },
326
338
  installInstructions: [
327
- ' • Install OpenAI CLI: pip install openai',
328
- ' • Or: npm install -g openai',
339
+ ' • brew install --cask codex',
340
+ ' • Or: npm install -g @openai/codex',
329
341
  ],
330
342
  });
331
343
  }
@@ -409,10 +421,12 @@ async function wrapGeneric(opts) {
409
421
  console.log('');
410
422
 
411
423
  // 4. Launch binary with Lynkr as base URL
412
- const child = spawn(binaryPath, targetArgs, {
424
+ const extraArgs = typeof opts.extraArgs === 'function' ? opts.extraArgs(port) : [];
425
+ const child = spawn(binaryPath, [...extraArgs, ...targetArgs], {
413
426
  env: {
414
427
  ...process.env,
415
428
  [opts.envVar]: `http://localhost:${port}`,
429
+ ...(opts.extraEnv || {}),
416
430
  },
417
431
  stdio: 'inherit',
418
432
  });
@@ -451,7 +465,7 @@ async function wrapGeneric(opts) {
451
465
  // Show stats if enabled and clean exit
452
466
  if (process.env.LYNKR_WRAP_SHOW_STATS !== 'false' && code === 0) {
453
467
  try {
454
- await showSessionStats();
468
+ await showSessionStats(port);
455
469
  } catch (err) {
456
470
  // Stats are nice-to-have, don't fail on error
457
471
  }
@@ -628,19 +642,39 @@ function formatDuration(ms) {
628
642
  return `${seconds}s`;
629
643
  }
630
644
 
631
- async function showSessionStats() {
632
- try {
633
- const { getMetricsCollector } = require('../src/observability/metrics');
634
- const metricsCollector = getMetricsCollector();
635
- const metrics = metricsCollector.getMetrics();
636
-
637
- // Check if we have any data
638
- const hasRequests = metrics && (
639
- (typeof metrics.totalRequests === 'number' && metrics.totalRequests > 0) ||
640
- (typeof metrics.requestCount === 'number' && metrics.requestCount > 0)
645
+ async function showSessionStats(port) {
646
+ // Read metrics from the RUNNING gateway over HTTP first — correct under
647
+ // cluster mode (workers hold the counters, not this process) — and fall
648
+ // back to the in-process collector. Field names follow getMetrics()'s
649
+ // snake_case output (requests_total, tokens_total, cost_usd_total):
650
+ // this function historically read camelCase fields that never existed,
651
+ // so every session ended with "No requests tracked" regardless of
652
+ // traffic.
653
+ const fetchMetrics = () => new Promise((resolve) => {
654
+ if (!port) return resolve(null);
655
+ const req = require('http').get(
656
+ `http://localhost:${port}/metrics/observability`,
657
+ (res) => {
658
+ let buf = '';
659
+ res.on('data', (c) => { buf += c; });
660
+ res.on('end', () => {
661
+ try { resolve(JSON.parse(buf)); } catch { resolve(null); }
662
+ });
663
+ }
641
664
  );
665
+ req.on('error', () => resolve(null));
666
+ req.setTimeout(1500, () => { req.destroy(); resolve(null); });
667
+ });
642
668
 
643
- if (!hasRequests) {
669
+ try {
670
+ let metrics = await fetchMetrics();
671
+ if (!metrics) {
672
+ const { getMetricsCollector } = require('../src/observability/metrics');
673
+ metrics = getMetricsCollector().getMetrics();
674
+ }
675
+
676
+ const requestCount = Number(metrics?.requests_total) || 0;
677
+ if (!requestCount) {
644
678
  console.log('');
645
679
  console.log('╭─ Lynkr Session Stats ────────────────────────────────');
646
680
  console.log('│ No requests tracked (check dashboard for details)');
@@ -650,29 +684,20 @@ async function showSessionStats() {
650
684
 
651
685
  console.log('');
652
686
  console.log('╭─ Lynkr Session Stats ────────────────────────────────');
687
+ console.log(`│ Requests ${requestCount} (${Number(metrics.requests_errors_total) || 0} errors)`);
653
688
 
654
- const requestCount = metrics.totalRequests || metrics.requestCount || 0;
655
- console.log(`│ Requests ${requestCount}`);
656
-
657
- if (metrics.tokensUsed || metrics.tokensSaved) {
658
- const tokensUsed = metrics.tokensUsed || 0;
659
- const tokensSaved = metrics.tokensSaved || 0;
660
- const originalTokens = tokensUsed + tokensSaved;
661
- if (originalTokens > 0) {
662
- const savingsPercent = Math.round((tokensSaved / originalTokens) * 100);
663
- console.log(`│ Tokens Original: ${originalTokens.toLocaleString()} → Routed: ${tokensUsed.toLocaleString()} (${savingsPercent}% saved)`);
664
- }
689
+ const tokensIn = Number(metrics.tokens_input_total) || 0;
690
+ const tokensOut = Number(metrics.tokens_output_total) || 0;
691
+ if (tokensIn || tokensOut) {
692
+ console.log(`│ Tokens in ${tokensIn.toLocaleString()} · out ${tokensOut.toLocaleString()}`);
665
693
  }
666
694
 
667
- if (metrics.tierBreakdown && Object.keys(metrics.tierBreakdown).length > 0) {
668
- const tiers = Object.entries(metrics.tierBreakdown)
669
- .map(([tier, count]) => `${tier}: ${count}`)
670
- .join(' ');
671
- console.log(`│ Tier Mix ${tiers}`);
672
- }
695
+ const cost = Number(metrics.cost_usd_total) || 0;
696
+ console.log(`│ Est. cost $${cost.toFixed(4)}`);
673
697
 
674
- if (metrics.cacheHits && metrics.cacheHits > 0) {
675
- console.log(`│ Cache Hits ${metrics.cacheHits}`);
698
+ const p95 = metrics.latency_ms && Number(metrics.latency_ms.p95);
699
+ if (p95) {
700
+ console.log(`│ Latency p95 ${p95.toLocaleString()} ms`);
676
701
  }
677
702
 
678
703
  console.log('╰──────────────────────────────────────────────────────');
@@ -0,0 +1,22 @@
1
+ {
2
+ "_comment": "WS7.1b — difficulty anchor exemplars. All texts are REAL user asks from live telemetry / the engineering ledger (do not invent synthetic anchors; mine new ones with the §7 telemetry query). Embedded at boot via the kNN router's embed(); centroid vectors cached in difficulty-anchors.vectors.json (safe to delete — regenerates).",
3
+ "trivial": [
4
+ "hi",
5
+ "How are you",
6
+ "ok thanks",
7
+ "345+32",
8
+ "how do I install golang on mac?",
9
+ "what is meaning of purging?",
10
+ "Explain the difference between TCP and UDP in two sentences."
11
+ ],
12
+ "substantive": [
13
+ "Review this retry helper for bugs and race conditions",
14
+ "give me a plan to refactor this code",
15
+ "Read the current repo and explain it to me"
16
+ ],
17
+ "heavyweight": [
18
+ "Do an architecture review of the orchestrator",
19
+ "analyze every module in src/ for circular dependencies and give me a dependency-order refactor plan",
20
+ "design a horizontally scalable architecture for this service with failure-mode analysis"
21
+ ]
22
+ }
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "9.7.3",
3
+ "version": "9.9.0",
4
+ "files": [
5
+ "index.js",
6
+ "install.sh",
7
+ "bin",
8
+ "config",
9
+ "public",
10
+ "skills",
11
+ "lynkr-skill.tar.gz",
12
+ "src",
13
+ "!src/db/database.sqlite",
14
+ "!src/**/*.db",
15
+ "!src/**/*.backup",
16
+ "native/index.js",
17
+ "native/lynkr-native.node",
18
+ "native/src/lib.rs",
19
+ "native/Cargo.toml",
20
+ "models/router/README.md",
21
+ "scripts",
22
+ "!scripts/export-routellm-bert.py"
23
+ ],
4
24
  "description": "Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.",
5
25
  "main": "index.js",
6
26
  "bin": {
@@ -16,7 +36,7 @@
16
36
  "dev": "nodemon index.js",
17
37
  "lint": "eslint src index.js",
18
38
  "test": "npm run test:unit && npm run test:performance",
19
- "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/agent-learning.test.js test/tool-call-response-metadata.test.js",
39
+ "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/agent-learning.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/usage-stats.test.js test/loop-guard.test.js",
20
40
  "test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
21
41
  "test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
22
42
  "test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
@@ -71,6 +91,7 @@
71
91
  "@azure/openai": "^2.0.0",
72
92
  "@babel/parser": "^7.29.0",
73
93
  "@babel/traverse": "^7.29.0",
94
+ "@blackwell-systems/gcf": "2.4.0",
74
95
  "@toon-format/toon": "^2.1.0",
75
96
  "cockatiel": "^3.2.1",
76
97
  "compression": "^1.7.4",
@@ -103,4 +124,4 @@
103
124
  "nodemon": "^3.1.0",
104
125
  "pino-pretty": "^10.2.0"
105
126
  }
106
- }
127
+ }