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.
- package/README.md +29 -19
- package/bin/cli.js +11 -0
- package/bin/lynkr-init.js +14 -1
- package/bin/lynkr-usage.js +78 -0
- package/bin/wrap.js +60 -35
- package/config/difficulty-anchors.json +22 -0
- package/package.json +24 -3
- package/scripts/audit-log-reader.js +399 -0
- package/scripts/calibrate-thresholds.js +38 -157
- package/scripts/compact-dictionary.js +204 -0
- package/scripts/test-deduplication.js +448 -0
- package/scripts/ws7-anchor-replay.js +108 -0
- package/skills/lynkr/SKILL.md +195 -0
- package/src/api/middleware/loop-guard.js +87 -0
- package/src/api/middleware/request-logging.js +5 -64
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +120 -101
- package/src/api/providers-handler.js +27 -2
- package/src/api/router.js +450 -125
- package/src/budget/index.js +2 -19
- package/src/cache/semantic.js +9 -0
- package/src/clients/databricks.js +455 -142
- package/src/clients/gpt-utils.js +11 -105
- package/src/clients/openai-format.js +10 -3
- package/src/clients/openrouter-utils.js +49 -24
- package/src/clients/prompt-cache-injection.js +1 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/clients/responses-format.js +34 -3
- package/src/clients/routing.js +15 -0
- package/src/config/index.js +36 -2
- package/src/context/gcf.js +275 -0
- package/src/context/tool-result-compressor.js +51 -9
- package/src/dashboard/api.js +1 -0
- package/src/logger/index.js +14 -1
- package/src/memory/search.js +12 -40
- package/src/memory/tools.js +3 -24
- package/src/orchestrator/bypass.js +4 -2
- package/src/orchestrator/index.js +120 -85
- package/src/routing/affinity-store.js +194 -0
- package/src/routing/agentic-detector.js +36 -6
- package/src/routing/bandit.js +25 -6
- package/src/routing/calibration.js +212 -0
- package/src/routing/client-profiles.js +292 -0
- package/src/routing/complexity-analyzer.js +48 -11
- package/src/routing/deescalator.js +148 -0
- package/src/routing/degradation.js +109 -0
- package/src/routing/feedback.js +157 -0
- package/src/routing/index.js +897 -87
- package/src/routing/intent-score.js +339 -0
- package/src/routing/interaction.js +3 -0
- package/src/routing/knn-router.js +70 -21
- package/src/routing/model-registry.js +28 -7
- package/src/routing/model-tiers.js +25 -2
- package/src/routing/reward-pipeline.js +68 -2
- package/src/routing/risk-analyzer.js +30 -1
- package/src/routing/risk-classifier.js +6 -2
- package/src/routing/session-affinity.js +162 -34
- package/src/routing/telemetry.js +286 -13
- package/src/routing/verifier.js +267 -0
- package/src/server.js +66 -21
- package/src/sessions/cleanup.js +17 -0
- package/src/tools/index.js +1 -15
- package/src/tools/smart-selection.js +10 -0
- package/src/tools/web-client.js +3 -3
- package/.eslintrc.cjs +0 -12
- package/benchmark-configs/litellm_config.yaml +0 -86
- package/benchmark-configs/lynkr.env +0 -48
- package/benchmark-configs/portkey-config.json +0 -60
- package/benchmark-configs/portkey-docker.sh +0 -23
- package/benchmark-tier-routing.js +0 -449
- package/funding.json +0 -110
- package/src/api/middleware/validation.js +0 -261
- package/src/routing/drift-monitor.js +0 -113
- 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
|
-
**
|
|
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
|
[](https://www.npmjs.com/package/lynkr)
|
|
8
|
-
[](https://github.com/Fast-Editor/Lynkr)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://deepwiki.com/Fast-Editor/Lynkr)
|
|
12
12
|
|
|
13
13
|
<table>
|
|
14
14
|
<tr>
|
|
15
|
-
<td align="center"><strong>
|
|
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
|
|
17
|
+
<td align="center"><strong><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
|
|
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
|
-
- ✅
|
|
44
|
-
- ✅
|
|
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 │ ←
|
|
184
|
-
│ Semantic cache │ ←
|
|
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
|
-
- ✅ **
|
|
194
|
-
- ✅ **Semantic cache** serves repeated queries in
|
|
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.
|
|
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
|
-
|
|
|
440
|
+
| Router | Routing-correct | Routing overhead |
|
|
434
441
|
|---|---|---|
|
|
435
|
-
|
|
|
436
|
-
|
|
|
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
|
|
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** | ✅
|
|
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',
|
package/bin/lynkr-usage.js
CHANGED
|
@@ -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
|
-
' •
|
|
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
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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.
|
|
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
|
+
}
|