lynkr 9.0.2 → 9.1.3
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 +21 -10
- package/bin/cli.js +18 -1
- package/bin/lynkr-trajectory.js +136 -0
- package/bin/lynkr-usage.js +219 -0
- package/funding.json +110 -0
- package/package.json +4 -2
- package/public/dashboard.html +665 -0
- package/scripts/build-knn-index.js +130 -0
- package/scripts/calibrate-thresholds.js +197 -0
- package/scripts/compare-policies.js +67 -0
- package/scripts/learn-output-ratios.js +162 -0
- package/scripts/refresh-pricing.js +122 -0
- package/scripts/run-routerarena.js +26 -0
- package/scripts/sample-regret.js +84 -0
- package/scripts/train-risk-classifier.js +191 -0
- package/src/api/files-router.js +6 -6
- package/src/api/middleware/budget-enforcer.js +60 -0
- package/src/api/middleware/budget.js +19 -1
- package/src/api/middleware/load-shedding.js +17 -0
- package/src/api/middleware/tenant.js +21 -0
- package/src/api/openai-router.js +1 -1
- package/src/api/router.js +204 -87
- package/src/budget/hierarchical-budget.js +159 -0
- package/src/cache/semantic.js +28 -2
- package/src/clients/databricks.js +68 -10
- package/src/clients/openai-format.js +31 -5
- package/src/config/index.js +246 -43
- package/src/context/toon.js +5 -4
- package/src/dashboard/api.js +170 -0
- package/src/dashboard/router.js +13 -0
- package/src/headroom/client.js +3 -109
- package/src/headroom/index.js +0 -14
- package/src/memory/search.js +0 -50
- package/src/orchestrator/index.js +106 -11
- package/src/orchestrator/preflight.js +188 -0
- package/src/prompts/system.js +34 -6
- package/src/routing/bandit.js +246 -0
- package/src/routing/cascade.js +106 -0
- package/src/routing/complexity-analyzer.js +7 -15
- package/src/routing/confidence-scorer.js +121 -0
- package/src/routing/context-validator.js +71 -0
- package/src/routing/cost-optimizer.js +5 -2
- package/src/routing/deadline.js +52 -0
- package/src/routing/drift-monitor.js +113 -0
- package/src/routing/embedding-cache.js +77 -0
- package/src/routing/index.js +374 -4
- package/src/routing/interaction.js +183 -0
- package/src/routing/knn-router.js +206 -0
- package/src/routing/latency-tracker.js +113 -71
- package/src/routing/model-tiers.js +156 -6
- package/src/routing/output-ratios.js +57 -0
- package/src/routing/regret-estimator.js +91 -0
- package/src/routing/reward-pipeline.js +62 -0
- package/src/routing/risk-analyzer.js +194 -0
- package/src/routing/risk-classifier.js +130 -0
- package/src/routing/shadow-mode.js +77 -0
- package/src/routing/telemetry.js +7 -0
- package/src/routing/tenant-policy.js +96 -0
- package/src/routing/tokenizer.js +162 -0
- package/src/server.js +12 -0
- package/src/stores/file-store.js +42 -7
- package/src/tools/smart-selection.js +11 -2
- package/src/training/trajectory-compressor.js +266 -0
- package/src/usage/aggregator.js +206 -0
- package/src/utils/markdown-ansi.js +146 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Reads routing telemetry from .lynkr/telemetry.db and produces
|
|
5
|
+
* actionable spend / savings reports.
|
|
6
|
+
*
|
|
7
|
+
* The "savings" calculation answers the question:
|
|
8
|
+
* "How much would this same workload have cost if every request
|
|
9
|
+
* had hit the most expensive flagship model?"
|
|
10
|
+
*
|
|
11
|
+
* That's the number Lynkr's tier router exists to make small.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const telemetry = require("../routing/telemetry");
|
|
15
|
+
const { getCostOptimizer } = require("../routing/cost-optimizer");
|
|
16
|
+
|
|
17
|
+
// What we treat as the "flagship comparison" — the model a developer
|
|
18
|
+
// would otherwise run every request against if they didn't have Lynkr.
|
|
19
|
+
// Picked to match Claude Code / Cursor defaults.
|
|
20
|
+
const DEFAULT_FLAGSHIP_MODEL = "claude-sonnet-4-5-20250929";
|
|
21
|
+
|
|
22
|
+
const WINDOW_PRESETS = {
|
|
23
|
+
"1d": 1 * 24 * 60 * 60 * 1000,
|
|
24
|
+
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
25
|
+
"30d": 30 * 24 * 60 * 60 * 1000,
|
|
26
|
+
all: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a window string ("7d", "30d", "all") or a Date / ISO string
|
|
31
|
+
* into a `since` timestamp in ms. Returns null for "all".
|
|
32
|
+
*/
|
|
33
|
+
function resolveSince(window) {
|
|
34
|
+
if (!window || window === "all") return null;
|
|
35
|
+
if (window instanceof Date) return window.getTime();
|
|
36
|
+
if (typeof window === "string") {
|
|
37
|
+
if (WINDOW_PRESETS[window] !== undefined) {
|
|
38
|
+
return WINDOW_PRESETS[window] === null ? null : Date.now() - WINDOW_PRESETS[window];
|
|
39
|
+
}
|
|
40
|
+
if (/^\d+d$/.test(window)) {
|
|
41
|
+
const days = parseInt(window, 10);
|
|
42
|
+
return Date.now() - days * 24 * 60 * 60 * 1000;
|
|
43
|
+
}
|
|
44
|
+
const parsed = Date.parse(window);
|
|
45
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
46
|
+
}
|
|
47
|
+
if (typeof window === "number") return window;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute usage stats for a time window.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {string|Date|number} [options.window="30d"] "1d", "7d", "30d", "all", ISO string, or epoch ms
|
|
56
|
+
* @param {string} [options.flagship] Model id used for the "what if I'd run flagship-only" comparison
|
|
57
|
+
* @param {string} [options.model] Filter to a single model
|
|
58
|
+
* @param {string} [options.provider] Filter to a single provider
|
|
59
|
+
* @returns {Object} Aggregated usage report
|
|
60
|
+
*/
|
|
61
|
+
function getUsage(options = {}) {
|
|
62
|
+
const window = options.window || "30d";
|
|
63
|
+
const since = resolveSince(window);
|
|
64
|
+
const flagship = options.flagship || DEFAULT_FLAGSHIP_MODEL;
|
|
65
|
+
|
|
66
|
+
const filters = { limit: 100000 };
|
|
67
|
+
if (since !== null) filters.since = since;
|
|
68
|
+
if (options.provider) filters.provider = options.provider;
|
|
69
|
+
|
|
70
|
+
let rows;
|
|
71
|
+
try {
|
|
72
|
+
rows = telemetry.query(filters);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
window,
|
|
76
|
+
since: since ? new Date(since).toISOString() : null,
|
|
77
|
+
flagship,
|
|
78
|
+
totals: emptyTotals(),
|
|
79
|
+
byTier: {},
|
|
80
|
+
byProvider: {},
|
|
81
|
+
byModel: {},
|
|
82
|
+
error: err.message,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Optional model filter (telemetry.query doesn't support it natively)
|
|
87
|
+
if (options.model) {
|
|
88
|
+
rows = rows.filter((r) => r.model === options.model);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const optimizer = (() => {
|
|
92
|
+
try {
|
|
93
|
+
return getCostOptimizer();
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
const totals = emptyTotals();
|
|
100
|
+
const byTier = {};
|
|
101
|
+
const byProvider = {};
|
|
102
|
+
const byModel = {};
|
|
103
|
+
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const inputTokens = row.input_tokens || 0;
|
|
106
|
+
const outputTokens = row.output_tokens || 0;
|
|
107
|
+
const totalTokens = inputTokens + outputTokens;
|
|
108
|
+
const actualCost = Number(row.cost_usd) || 0;
|
|
109
|
+
|
|
110
|
+
// Hypothetical cost if this same request had hit the flagship model.
|
|
111
|
+
let flagshipCost = 0;
|
|
112
|
+
if (optimizer && totalTokens > 0) {
|
|
113
|
+
try {
|
|
114
|
+
const est = optimizer.estimateCost(flagship, inputTokens, outputTokens);
|
|
115
|
+
flagshipCost = (est.inputCost || 0) + (est.outputCost || 0);
|
|
116
|
+
} catch {
|
|
117
|
+
flagshipCost = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const saved = Math.max(0, flagshipCost - actualCost);
|
|
121
|
+
|
|
122
|
+
totals.requests += 1;
|
|
123
|
+
totals.inputTokens += inputTokens;
|
|
124
|
+
totals.outputTokens += outputTokens;
|
|
125
|
+
totals.totalTokens += totalTokens;
|
|
126
|
+
totals.actualCost += actualCost;
|
|
127
|
+
totals.flagshipCost += flagshipCost;
|
|
128
|
+
totals.saved += saved;
|
|
129
|
+
if (row.was_fallback) totals.fallbacks += 1;
|
|
130
|
+
if (row.error_type) totals.errors += 1;
|
|
131
|
+
|
|
132
|
+
bumpBucket(byTier, row.tier || "UNKNOWN", inputTokens, outputTokens, actualCost, flagshipCost);
|
|
133
|
+
bumpBucket(byProvider, row.provider || "unknown", inputTokens, outputTokens, actualCost, flagshipCost);
|
|
134
|
+
bumpBucket(byModel, row.model || "unknown", inputTokens, outputTokens, actualCost, flagshipCost);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
window,
|
|
139
|
+
since: since ? new Date(since).toISOString() : null,
|
|
140
|
+
flagship,
|
|
141
|
+
totals,
|
|
142
|
+
byTier,
|
|
143
|
+
byProvider,
|
|
144
|
+
byModel,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function emptyTotals() {
|
|
149
|
+
return {
|
|
150
|
+
requests: 0,
|
|
151
|
+
inputTokens: 0,
|
|
152
|
+
outputTokens: 0,
|
|
153
|
+
totalTokens: 0,
|
|
154
|
+
actualCost: 0,
|
|
155
|
+
flagshipCost: 0,
|
|
156
|
+
saved: 0,
|
|
157
|
+
savedPercent: 0,
|
|
158
|
+
fallbacks: 0,
|
|
159
|
+
errors: 0,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function bumpBucket(bucket, key, inputTokens, outputTokens, actualCost, flagshipCost) {
|
|
164
|
+
if (!bucket[key]) {
|
|
165
|
+
bucket[key] = {
|
|
166
|
+
requests: 0,
|
|
167
|
+
inputTokens: 0,
|
|
168
|
+
outputTokens: 0,
|
|
169
|
+
totalTokens: 0,
|
|
170
|
+
actualCost: 0,
|
|
171
|
+
flagshipCost: 0,
|
|
172
|
+
saved: 0,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const b = bucket[key];
|
|
176
|
+
b.requests += 1;
|
|
177
|
+
b.inputTokens += inputTokens;
|
|
178
|
+
b.outputTokens += outputTokens;
|
|
179
|
+
b.totalTokens += inputTokens + outputTokens;
|
|
180
|
+
b.actualCost += actualCost;
|
|
181
|
+
b.flagshipCost += flagshipCost;
|
|
182
|
+
b.saved += Math.max(0, flagshipCost - actualCost);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compute and finalise totals (savedPercent etc.) on a usage object.
|
|
187
|
+
* Mutates and returns the object — convenient for chaining.
|
|
188
|
+
*/
|
|
189
|
+
function finalise(usage) {
|
|
190
|
+
const t = usage.totals;
|
|
191
|
+
t.savedPercent = t.flagshipCost > 0 ? Math.round((t.saved / t.flagshipCost) * 1000) / 10 : 0;
|
|
192
|
+
for (const bucket of [usage.byTier, usage.byProvider, usage.byModel]) {
|
|
193
|
+
for (const key of Object.keys(bucket)) {
|
|
194
|
+
const b = bucket[key];
|
|
195
|
+
b.savedPercent = b.flagshipCost > 0 ? Math.round((b.saved / b.flagshipCost) * 1000) / 10 : 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return usage;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
getUsage: (options) => finalise(getUsage(options)),
|
|
203
|
+
resolveSince,
|
|
204
|
+
DEFAULT_FLAGSHIP_MODEL,
|
|
205
|
+
WINDOW_PRESETS,
|
|
206
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → ANSI escape code renderer.
|
|
3
|
+
*
|
|
4
|
+
* Activated by MARKDOWN_RENDER_ANSI=true in the environment.
|
|
5
|
+
* Applied to text blocks in the SSE emission path so clients like claw
|
|
6
|
+
* receive pre-formatted output without needing their own markdown renderer.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately avoids external dependencies — pure regex + string ops.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// ANSI primitives
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const R = '\x1b[0m'; // reset all
|
|
15
|
+
const B = '\x1b[1m'; // bold on
|
|
16
|
+
const B_ = '\x1b[22m'; // bold off
|
|
17
|
+
const I = '\x1b[3m'; // italic on
|
|
18
|
+
const I_ = '\x1b[23m'; // italic off
|
|
19
|
+
const S = '\x1b[9m'; // strikethrough on
|
|
20
|
+
const S_ = '\x1b[29m'; // strikethrough off
|
|
21
|
+
const DIM = '\x1b[2m'; // dim
|
|
22
|
+
|
|
23
|
+
const CYAN = '\x1b[1;96m'; // bold bright-cyan — H1
|
|
24
|
+
const BLUE = '\x1b[1;94m'; // bold bright-blue — H2
|
|
25
|
+
const MAGENTA = '\x1b[1;95m'; // bold bright-magenta — H3
|
|
26
|
+
const WHITE_B = '\x1b[1;97m'; // bold white — H4-H6
|
|
27
|
+
const YELLOW = '\x1b[33m'; // yellow — inline code
|
|
28
|
+
const GREEN = '\x1b[92m'; // bright green — code block body
|
|
29
|
+
const GRAY = '\x1b[90m'; // dark gray — HR / code fence border
|
|
30
|
+
const ORANGE = '\x1b[38;5;214m'; // orange — code fence lang tag
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Inline formatting (applied to single lines outside code fences)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
function inlineFmt(line) {
|
|
36
|
+
// Bold + italic: ***text***
|
|
37
|
+
line = line.replace(/\*\*\*(.+?)\*\*\*/g, `${B}${I}$1${I_}${B_}`);
|
|
38
|
+
// Bold: **text** or __text__
|
|
39
|
+
line = line.replace(/\*\*(.+?)\*\*/g, `${B}$1${B_}`);
|
|
40
|
+
line = line.replace(/__(.+?)__/g, `${B}$1${B_}`);
|
|
41
|
+
// Italic: *text* or _text_ (single, not preceded/followed by same char)
|
|
42
|
+
line = line.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `${I}$1${I_}`);
|
|
43
|
+
line = line.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, `${I}$1${I_}`);
|
|
44
|
+
// Strikethrough: ~~text~~
|
|
45
|
+
line = line.replace(/~~(.+?)~~/g, `${S}$1${S_}`);
|
|
46
|
+
// Inline code: `code` (done last so ANSI inside code isn't re-processed)
|
|
47
|
+
line = line.replace(/`([^`]+)`/g, `${YELLOW}$1${R}`);
|
|
48
|
+
return line;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Block-level rendering (processes the whole text at once)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
function markdownToAnsi(text) {
|
|
55
|
+
if (!text) return text;
|
|
56
|
+
|
|
57
|
+
const lines = text.split('\n');
|
|
58
|
+
const out = [];
|
|
59
|
+
let inCode = false;
|
|
60
|
+
let codeLang = '';
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const raw = lines[i];
|
|
64
|
+
|
|
65
|
+
// ── Code fence open/close ──────────────────────────────────────────────
|
|
66
|
+
const fenceMatch = raw.match(/^(`{3,})(.*)/);
|
|
67
|
+
if (fenceMatch) {
|
|
68
|
+
if (!inCode) {
|
|
69
|
+
inCode = true;
|
|
70
|
+
codeLang = fenceMatch[2].trim();
|
|
71
|
+
const tag = codeLang ? ` ${codeLang} ` : '';
|
|
72
|
+
out.push(`${GRAY}┌─${ORANGE}${tag}${GRAY}${'─'.repeat(Math.max(0, 46 - tag.length))}${R}`);
|
|
73
|
+
} else {
|
|
74
|
+
inCode = false;
|
|
75
|
+
out.push(`${GRAY}└${'─'.repeat(48)}${R}`);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Inside a code block ───────────────────────────────────────────────
|
|
81
|
+
if (inCode) {
|
|
82
|
+
out.push(`${GRAY}│ ${GREEN}${raw}${R}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Horizontal rule ───────────────────────────────────────────────────
|
|
87
|
+
if (/^[-*_]{3,}\s*$/.test(raw.trim())) {
|
|
88
|
+
out.push(`${GRAY}${'─'.repeat(50)}${R}`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Headings ──────────────────────────────────────────────────────────
|
|
93
|
+
const h6 = raw.match(/^(#{1,6})\s+(.*)/);
|
|
94
|
+
if (h6) {
|
|
95
|
+
const level = h6[1].length;
|
|
96
|
+
const title = inlineFmt(h6[2]);
|
|
97
|
+
const colors = [CYAN, BLUE, MAGENTA, WHITE_B, WHITE_B, WHITE_B];
|
|
98
|
+
const prefix = ['━━ ', '── ', ' ', ' ', ' ', ' '][level - 1];
|
|
99
|
+
out.push(`${colors[level - 1]}${prefix}${title}${R}`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Blockquote ────────────────────────────────────────────────────────
|
|
104
|
+
if (raw.startsWith('> ')) {
|
|
105
|
+
out.push(`${DIM}│ ${inlineFmt(raw.slice(2))}${R}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Unordered list ────────────────────────────────────────────────────
|
|
110
|
+
const ulMatch = raw.match(/^(\s*)[*\-+] (.*)/);
|
|
111
|
+
if (ulMatch) {
|
|
112
|
+
const indent = ulMatch[1];
|
|
113
|
+
const depth = Math.floor(indent.length / 2);
|
|
114
|
+
const bullet = ['•', '◦', '▸'][Math.min(depth, 2)];
|
|
115
|
+
out.push(`${indent}${YELLOW}${bullet}${R} ${inlineFmt(ulMatch[2])}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Ordered list ──────────────────────────────────────────────────────
|
|
120
|
+
const olMatch = raw.match(/^(\s*)(\d+)\. (.*)/);
|
|
121
|
+
if (olMatch) {
|
|
122
|
+
out.push(`${olMatch[1]}${YELLOW}${olMatch[2]}.${R} ${inlineFmt(olMatch[3])}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Normal line (apply inline formatting) ─────────────────────────────
|
|
127
|
+
out.push(inlineFmt(raw));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Close an unclosed code fence gracefully
|
|
131
|
+
if (inCode) out.push(`${GRAY}└${'─'.repeat(48)}${R}`);
|
|
132
|
+
|
|
133
|
+
return out.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Public API
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
const enabled = process.env.MARKDOWN_RENDER_ANSI === 'true';
|
|
140
|
+
|
|
141
|
+
function renderText(text) {
|
|
142
|
+
if (!enabled || !text) return text;
|
|
143
|
+
return markdownToAnsi(text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { renderText, markdownToAnsi, enabled };
|