lynkr 9.11.0 → 9.13.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 +58 -9
- package/bin/lynkr-init.js +25 -2
- package/bin/statusline.js +80 -0
- package/bin/wrap.js +10 -6
- package/package.json +8 -3
- package/public/dashboard.html +325 -1
- package/src/api/openai-router.js +163 -2
- package/src/api/providers-handler.js +28 -0
- package/src/clients/databricks.js +297 -30
- package/src/clients/provider-capabilities.js +1 -1
- package/src/config/index.js +37 -3
- package/src/context/tool-result-compressor.js +6 -3
- package/src/dashboard/api.js +78 -2
- package/src/dashboard/recommendations/cache-weakspots.js +88 -0
- package/src/dashboard/recommendations/deadweight.js +82 -0
- package/src/dashboard/recommendations/downsize.js +109 -0
- package/src/dashboard/recommendations/index.js +106 -0
- package/src/dashboard/recommendations/side-requests.js +68 -0
- package/src/dashboard/recommendations/wilson.js +24 -0
- package/src/dashboard/router.js +5 -0
- package/src/orchestrator/index.js +28 -5
- package/src/orchestrator/sse-transformer.js +10 -3
- package/src/routing/complexity-analyzer.js +30 -9
- package/src/routing/index.js +2 -0
- package/src/routing/intent-score.js +27 -2
- package/src/routing/model-tiers.js +4 -0
- package/src/routing/telemetry.js +206 -2
- package/scripts/convert-routellm.py +0 -105
package/src/dashboard/api.js
CHANGED
|
@@ -208,8 +208,12 @@ function routing(req, res) {
|
|
|
208
208
|
// Cache-aware routing (Phase 6): per-decision switch/hold economics,
|
|
209
209
|
// aggregated into "cache dollars saved by routing".
|
|
210
210
|
const cacheEconomics = telemetry.getCacheEconomics({ since });
|
|
211
|
+
// Lens money-framing: the MEASURED companion — cache reads actually
|
|
212
|
+
// served this window, auditable against the bill (vs the projected
|
|
213
|
+
// figures above). null until cache counters have accrued.
|
|
214
|
+
const cacheMeasured = telemetry.getMeasuredCacheSavings({ since });
|
|
211
215
|
|
|
212
|
-
res.json({ tierDefinitions: TIER_DEFINITIONS, accuracy, stats, providerStats, circuitBreakers: cbStates, cacheEconomics, window: win.label });
|
|
216
|
+
res.json({ tierDefinitions: TIER_DEFINITIONS, accuracy, stats, providerStats, circuitBreakers: cbStates, cacheEconomics, cacheMeasured, window: win.label });
|
|
213
217
|
} catch (e) {
|
|
214
218
|
res.status(500).json({ error: 'routing_api_error', detail: e.message });
|
|
215
219
|
}
|
|
@@ -233,4 +237,76 @@ function logs(req, res) {
|
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
|
|
240
|
+
/* ── Lens endpoints (feature/lens-dashboard) ─────────────────────────── */
|
|
241
|
+
|
|
242
|
+
// Recommendations — analyzer findings ranked by past overspend.
|
|
243
|
+
function recommendations(req, res) {
|
|
244
|
+
try {
|
|
245
|
+
const engine = require('./recommendations');
|
|
246
|
+
const windowMs = req.query.window === '30d' ? 30 * 86400000
|
|
247
|
+
: req.query.window === '24h' ? 86400000
|
|
248
|
+
: 7 * 86400000;
|
|
249
|
+
res.json(engine.run({ windowMs, force: req.query.force === 'true' }));
|
|
250
|
+
} catch (e) {
|
|
251
|
+
res.status(500).json({ error: 'recommendations_api_error', detail: e.message });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Session drill-down: per-model mix + context-growth series + raw rows.
|
|
256
|
+
function sessionDetail(req, res) {
|
|
257
|
+
try {
|
|
258
|
+
const detail = telemetry.getSessionDetail(req.params.id);
|
|
259
|
+
if (!detail) return res.status(404).json({ error: 'session_not_found' });
|
|
260
|
+
res.json(detail);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
res.status(500).json({ error: 'session_api_error', detail: e.message });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Pivot explorer: metric × dimension (× stack), whitelisted server-side.
|
|
267
|
+
function analytics(req, res) {
|
|
268
|
+
try {
|
|
269
|
+
const days = Math.min(90, Math.max(1, parseInt(req.query.days || '7', 10) || 7));
|
|
270
|
+
const data = telemetry.getAnalytics({
|
|
271
|
+
metric: req.query.metric,
|
|
272
|
+
by: req.query.by,
|
|
273
|
+
stack: req.query.stack || null,
|
|
274
|
+
since: Date.now() - days * 86400000,
|
|
275
|
+
});
|
|
276
|
+
if (!data) return res.status(503).json({ error: 'telemetry_unavailable' });
|
|
277
|
+
res.json(data);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
res.status(500).json({ error: 'analytics_api_error', detail: e.message });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Statusline: one cheap snapshot for the zero-token status line — last
|
|
284
|
+
// routed request, today's spend, measured cache re-read share.
|
|
285
|
+
function statusline(req, res) {
|
|
286
|
+
try {
|
|
287
|
+
const last = telemetry.query({ limit: 1 })[0] ?? null;
|
|
288
|
+
const since = Date.now() - 86400000;
|
|
289
|
+
const db = telemetry.getDb();
|
|
290
|
+
let today = null;
|
|
291
|
+
if (db) {
|
|
292
|
+
today = db.prepare(
|
|
293
|
+
`SELECT SUM(COALESCE(cost_usd,0)) spend,
|
|
294
|
+
SUM(COALESCE(cache_read_tokens,0)) rd,
|
|
295
|
+
SUM(COALESCE(cache_read_tokens,0)+COALESCE(cache_creation_tokens,0)+COALESCE(input_tokens,0)) total
|
|
296
|
+
FROM routing_telemetry WHERE timestamp > ?`
|
|
297
|
+
).get(since);
|
|
298
|
+
}
|
|
299
|
+
res.json({
|
|
300
|
+
last: last && {
|
|
301
|
+
tier: last.tier, provider: last.provider, model: last.model,
|
|
302
|
+
pinned: !!last.pinned, at: last.timestamp,
|
|
303
|
+
},
|
|
304
|
+
todaySpendUsd: today?.spend ?? null,
|
|
305
|
+
cacheReadPct: today && today.total > 0 ? Math.round((today.rd / today.total) * 100) : null,
|
|
306
|
+
});
|
|
307
|
+
} catch (e) {
|
|
308
|
+
res.status(500).json({ error: 'statusline_api_error', detail: e.message });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = { overview, usage, routing, logs, recommendations, sessionDetail, analytics, statusline };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache-weakspots — prompt-cache hit ratio per (provider, model), worst first.
|
|
3
|
+
*
|
|
4
|
+
* Reads the per-request cache counters (cache_read_tokens /
|
|
5
|
+
* cache_creation_tokens). Rows recorded before capture began are NULL and
|
|
6
|
+
* excluded — when nothing has been measured yet the finding says so
|
|
7
|
+
* explicitly instead of reporting a fake all-clear.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MIN_REQUESTS = 5;
|
|
11
|
+
const WEAK_RATIO = 0.5;
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
id: 'cache-weakspots',
|
|
15
|
+
title: 'Weak prompt-cache hit ratios',
|
|
16
|
+
|
|
17
|
+
analyze({ db, since }) {
|
|
18
|
+
const rows = db
|
|
19
|
+
.prepare(
|
|
20
|
+
`SELECT provider, model, COUNT(*) n,
|
|
21
|
+
SUM(COALESCE(cache_read_tokens,0)) rd,
|
|
22
|
+
SUM(COALESCE(cache_creation_tokens,0)) cr,
|
|
23
|
+
SUM(COALESCE(input_tokens,0)) inp
|
|
24
|
+
FROM routing_telemetry
|
|
25
|
+
WHERE timestamp > ? AND cache_read_tokens IS NOT NULL
|
|
26
|
+
GROUP BY provider, model
|
|
27
|
+
HAVING COUNT(*) >= ${MIN_REQUESTS}`
|
|
28
|
+
)
|
|
29
|
+
.all(since);
|
|
30
|
+
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
return {
|
|
33
|
+
id: this.id, title: this.title, mechanism: 'info', state: 'not_measured',
|
|
34
|
+
framing: 'usd', pastOverspendUsd: null, pastOverspendTokens: null,
|
|
35
|
+
stats: [], evidence: { columns: [], rows: [] }, fix: null,
|
|
36
|
+
caveat: null,
|
|
37
|
+
estimateBasis: 'Cache counters are captured from this version onward — data accrues as requests flow. An un-run measurement is not an all-clear.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { resolveCacheEconomics } = require('../../routing/cache-economics');
|
|
42
|
+
const weak = [];
|
|
43
|
+
let usd = 0;
|
|
44
|
+
for (const r of rows) {
|
|
45
|
+
const total = r.rd + r.cr + r.inp;
|
|
46
|
+
const ratio = total > 0 ? r.rd / total : 0;
|
|
47
|
+
if (ratio >= WEAK_RATIO) continue;
|
|
48
|
+
const econ = resolveCacheEconomics(r.provider, r.model);
|
|
49
|
+
// Ceiling: what the uncached input cost beyond cache-read price.
|
|
50
|
+
const missedUsd = econ.unknownPricing || econ.mechanism === 'local'
|
|
51
|
+
? null
|
|
52
|
+
: (r.inp * Math.max(0, econ.inputPerM - econ.cacheReadPerM)) / 1_000_000;
|
|
53
|
+
if (missedUsd != null) usd += missedUsd;
|
|
54
|
+
weak.push([r.provider, r.model, r.n, `${Math.round(ratio * 100)}%`, r.inp, missedUsd ?? '(local/unknown)']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!weak.length) {
|
|
58
|
+
return {
|
|
59
|
+
id: this.id, title: this.title, mechanism: 'info', state: 'clean',
|
|
60
|
+
framing: 'usd', pastOverspendUsd: 0, pastOverspendTokens: null,
|
|
61
|
+
stats: [{ label: 'models measured', value: rows.length }],
|
|
62
|
+
evidence: { columns: [], rows: [] }, fix: null, caveat: null,
|
|
63
|
+
estimateBasis: `Hit ratio = cache reads ÷ (reads + writes + uncached input); weak = below ${WEAK_RATIO * 100}%.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
id: this.id,
|
|
69
|
+
title: this.title,
|
|
70
|
+
mechanism: 'info',
|
|
71
|
+
state: 'actionable',
|
|
72
|
+
framing: 'usd',
|
|
73
|
+
pastOverspendUsd: usd,
|
|
74
|
+
pastOverspendTokens: null,
|
|
75
|
+
stats: [
|
|
76
|
+
{ label: 'weak (provider, model) pairs', value: weak.length },
|
|
77
|
+
{ label: 'models measured', value: rows.length },
|
|
78
|
+
],
|
|
79
|
+
evidence: {
|
|
80
|
+
columns: ['provider', 'model', 'requests', 'hit ratio', 'uncached input tokens', 'ceiling $'],
|
|
81
|
+
rows: weak.sort((a, b) => parseFloat(a[3]) - parseFloat(b[3])),
|
|
82
|
+
},
|
|
83
|
+
fix: 'Low ratios usually mean an unstable prefix: per-request bytes in the system prompt, client-side history rewrites, or conversations too short to cache. Check the silent-invalidator list and whether the distiller freeze covers these sessions.',
|
|
84
|
+
caveat: 'The dollar figure is a CEILING — it prices every uncached input token at the read discount, which assumes perfect cacheability. Real recovery is lower.',
|
|
85
|
+
estimateBasis: 'Uncached input tokens × (input price − cache-read price) for pairs below a 50% hit ratio, this window. NULL-counter rows (pre-capture) excluded.',
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deadweight — tool schemas carried on every request but never called.
|
|
3
|
+
*
|
|
4
|
+
* Telemetry stores per-request tool COUNTS, not names, so the detectable
|
|
5
|
+
* unit is the session: sessions whose every request shipped a tool loadout
|
|
6
|
+
* and whose tool_calls_made never left zero paid schema rent for nothing.
|
|
7
|
+
* (Per-tool attribution needs tool names in telemetry — future work.)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const EST_TOKENS_PER_TOOL = 150; // typical JSON schema; stated in estimateBasis
|
|
11
|
+
const MIN_REQUESTS = 3;
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
id: 'deadweight',
|
|
15
|
+
title: 'Tool schemas carried but never used',
|
|
16
|
+
|
|
17
|
+
analyze({ db, since }) {
|
|
18
|
+
const rows = db
|
|
19
|
+
.prepare(
|
|
20
|
+
`SELECT session_id,
|
|
21
|
+
COUNT(*) reqs,
|
|
22
|
+
AVG(COALESCE(tool_count,0)) avg_tools,
|
|
23
|
+
MAX(model) model,
|
|
24
|
+
MAX(provider) provider,
|
|
25
|
+
SUM(COALESCE(cost_usd,0)) session_cost
|
|
26
|
+
FROM routing_telemetry
|
|
27
|
+
WHERE timestamp > ? AND session_id IS NOT NULL AND COALESCE(tool_count,0) > 0
|
|
28
|
+
GROUP BY session_id
|
|
29
|
+
HAVING SUM(COALESCE(tool_calls_made,0)) = 0 AND COUNT(*) >= ${MIN_REQUESTS}
|
|
30
|
+
ORDER BY reqs * avg_tools DESC
|
|
31
|
+
LIMIT 50`
|
|
32
|
+
)
|
|
33
|
+
.all(since);
|
|
34
|
+
|
|
35
|
+
if (!rows.length) {
|
|
36
|
+
return {
|
|
37
|
+
id: this.id, title: this.title, mechanism: 'snippet', state: 'clean',
|
|
38
|
+
framing: 'usd', pastOverspendUsd: 0, pastOverspendTokens: 0,
|
|
39
|
+
stats: [], evidence: { columns: [], rows: [] },
|
|
40
|
+
fix: null, caveat: null,
|
|
41
|
+
estimateBasis: 'Sessions with >=3 requests, a tool loadout on every request, and zero tool calls.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { resolveCacheEconomics } = require('../../routing/cache-economics');
|
|
46
|
+
let tokens = 0;
|
|
47
|
+
let usd = 0;
|
|
48
|
+
const evidence = [];
|
|
49
|
+
for (const r of rows) {
|
|
50
|
+
const wastedTokens = Math.round(r.avg_tools * EST_TOKENS_PER_TOOL * r.reqs);
|
|
51
|
+
tokens += wastedTokens;
|
|
52
|
+
const econ = resolveCacheEconomics(r.provider, r.model);
|
|
53
|
+
const rowUsd = econ.unknownPricing ? null : (wastedTokens * econ.inputPerM) / 1_000_000;
|
|
54
|
+
if (rowUsd != null) usd += rowUsd;
|
|
55
|
+
evidence.push([
|
|
56
|
+
r.session_id, r.reqs, Math.round(r.avg_tools), wastedTokens,
|
|
57
|
+
rowUsd != null ? rowUsd : '(price unknown)',
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id: this.id,
|
|
63
|
+
title: this.title,
|
|
64
|
+
mechanism: 'snippet',
|
|
65
|
+
state: 'actionable',
|
|
66
|
+
framing: 'usd',
|
|
67
|
+
pastOverspendUsd: usd,
|
|
68
|
+
pastOverspendTokens: tokens,
|
|
69
|
+
stats: [
|
|
70
|
+
{ label: 'sessions affected', value: rows.length },
|
|
71
|
+
{ label: 'est. schema tokens re-sent', value: tokens },
|
|
72
|
+
],
|
|
73
|
+
evidence: {
|
|
74
|
+
columns: ['session', 'requests', 'avg tools', 'est. wasted tokens', 'est. $'],
|
|
75
|
+
rows: evidence,
|
|
76
|
+
},
|
|
77
|
+
fix: 'These sessions never called a tool — their MCP servers/tool registrations paid schema rent on every request. Trim unused MCP servers from the client config, or scope them per-project.',
|
|
78
|
+
caveat: 'Schema size is estimated at ~150 tokens per tool; telemetry stores tool counts, not names, so attribution is per-session rather than per-tool.',
|
|
79
|
+
estimateBasis: 'avg tools × ~150 est. tokens/schema × requests per session, priced at each session\'s model input rate. Sessions with any tool call are excluded entirely.',
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* downsize — request types where the tier below has statistically proven
|
|
3
|
+
* itself, Wilson-bounded.
|
|
4
|
+
*
|
|
5
|
+
* The de-escalator's live rule uses raw averages (>=30 rows, avg quality
|
|
6
|
+
* >=70). This analyzer applies the stricter test TokenJam-bench uses for
|
|
7
|
+
* verdicts: the WILSON LOWER BOUND of the lower tier's success rate must
|
|
8
|
+
* clear the bar, so 21-of-30 (lower bound ~0.52) stays "unproven" while
|
|
9
|
+
* 140-of-170 (~0.76) is a verdict. Success = quality >= 70 and no error.
|
|
10
|
+
*
|
|
11
|
+
* No quality-equivalence claim is made — the verdict is "the lower tier's
|
|
12
|
+
* measured floor clears the bar on this request type", nothing more.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { wilsonLowerBound } = require('./wilson');
|
|
16
|
+
|
|
17
|
+
const TIER_ORDER = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
18
|
+
const MIN_SAMPLES = 20;
|
|
19
|
+
const SUCCESS_QUALITY = 70;
|
|
20
|
+
const WILSON_BAR = 0.7;
|
|
21
|
+
const EVIDENCE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; // evidence looks back further than the spend window
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
id: 'downsize',
|
|
25
|
+
title: 'Tiers that could step down (Wilson-proven)',
|
|
26
|
+
|
|
27
|
+
analyze({ db, since, until }) {
|
|
28
|
+
// Lower-tier track record per request_type over 30 days.
|
|
29
|
+
const evidence = db
|
|
30
|
+
.prepare(
|
|
31
|
+
`SELECT tier, request_type,
|
|
32
|
+
COUNT(*) n,
|
|
33
|
+
SUM(CASE WHEN COALESCE(quality_score,0) >= ${SUCCESS_QUALITY} AND error_type IS NULL THEN 1 ELSE 0 END) k,
|
|
34
|
+
AVG(COALESCE(cost_usd,0)) avg_cost
|
|
35
|
+
FROM routing_telemetry
|
|
36
|
+
WHERE timestamp > ? AND tier IS NOT NULL AND request_type IS NOT NULL
|
|
37
|
+
GROUP BY tier, request_type`
|
|
38
|
+
)
|
|
39
|
+
.all(until - EVIDENCE_WINDOW_MS);
|
|
40
|
+
|
|
41
|
+
const byKey = new Map(evidence.map((r) => [`${r.tier}::${r.request_type}`, r]));
|
|
42
|
+
|
|
43
|
+
// Spend at each upper tier per request_type, this window.
|
|
44
|
+
const upperSpend = db
|
|
45
|
+
.prepare(
|
|
46
|
+
`SELECT tier, request_type, COUNT(*) n, SUM(COALESCE(cost_usd,0)) cost, AVG(COALESCE(cost_usd,0)) avg_cost
|
|
47
|
+
FROM routing_telemetry
|
|
48
|
+
WHERE timestamp > ? AND tier IS NOT NULL AND request_type IS NOT NULL
|
|
49
|
+
GROUP BY tier, request_type`
|
|
50
|
+
)
|
|
51
|
+
.all(since);
|
|
52
|
+
|
|
53
|
+
const rows = [];
|
|
54
|
+
let usd = 0;
|
|
55
|
+
let anyEvidence = false;
|
|
56
|
+
for (const u of upperSpend) {
|
|
57
|
+
const idx = TIER_ORDER.indexOf(u.tier);
|
|
58
|
+
if (idx <= 0) continue;
|
|
59
|
+
const lower = byKey.get(`${TIER_ORDER[idx - 1]}::${u.request_type}`);
|
|
60
|
+
if (!lower) continue;
|
|
61
|
+
anyEvidence = true;
|
|
62
|
+
if (lower.n < MIN_SAMPLES) continue;
|
|
63
|
+
const bound = wilsonLowerBound(lower.k, lower.n);
|
|
64
|
+
if (bound < WILSON_BAR) continue;
|
|
65
|
+
// Empirical per-request price delta between the two tiers on the SAME
|
|
66
|
+
// request type — measured, not modeled.
|
|
67
|
+
const delta = Math.max(0, (u.avg_cost ?? 0) - (lower.avg_cost ?? 0)) * u.n;
|
|
68
|
+
usd += delta;
|
|
69
|
+
rows.push([
|
|
70
|
+
u.request_type, u.tier, `${TIER_ORDER[idx - 1]}`,
|
|
71
|
+
`${lower.k}/${lower.n}`, bound.toFixed(2), u.n, delta,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!rows.length) {
|
|
76
|
+
return {
|
|
77
|
+
id: this.id, title: this.title, mechanism: 'info',
|
|
78
|
+
state: anyEvidence ? 'clean' : 'not_measured',
|
|
79
|
+
framing: 'usd', pastOverspendUsd: anyEvidence ? 0 : null, pastOverspendTokens: null,
|
|
80
|
+
stats: [],
|
|
81
|
+
evidence: { columns: [], rows: [] }, fix: null, caveat: null,
|
|
82
|
+
estimateBasis: anyEvidence
|
|
83
|
+
? `No (tier, request_type) pair has a lower tier whose Wilson lower bound clears ${WILSON_BAR} on >=${MIN_SAMPLES} samples yet.`
|
|
84
|
+
: 'Needs quality-scored telemetry with request_type populated — accrues as traffic flows (request_type capture is recent).',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
rows.sort((a, b) => b[6] - a[6]);
|
|
89
|
+
return {
|
|
90
|
+
id: this.id,
|
|
91
|
+
title: this.title,
|
|
92
|
+
mechanism: 'info',
|
|
93
|
+
state: 'actionable',
|
|
94
|
+
framing: 'usd',
|
|
95
|
+
pastOverspendUsd: usd,
|
|
96
|
+
pastOverspendTokens: null,
|
|
97
|
+
stats: [
|
|
98
|
+
{ label: 'proven (request_type, tier) pairs', value: rows.length },
|
|
99
|
+
],
|
|
100
|
+
evidence: {
|
|
101
|
+
columns: ['request_type', 'served tier', 'proven lower tier', 'lower success', 'Wilson lower bound', 'requests this window', 'measured delta $'],
|
|
102
|
+
rows,
|
|
103
|
+
},
|
|
104
|
+
fix: 'The live de-escalator will demote these automatically as its own thresholds clear; to act sooner, adjust the TIER_* mapping for these request types or lower the de-escalator sample bar deliberately.',
|
|
105
|
+
caveat: 'Verdict is "the lower tier\'s measured success floor clears the bar" — not quality equivalence. Success = quality >= 70 with no error.',
|
|
106
|
+
estimateBasis: 'Per-request cost delta between tiers measured on the SAME request_type (30-day evidence window), × this window\'s upper-tier request count. Wilson 95% lower bound.',
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lens recommendations engine.
|
|
3
|
+
*
|
|
4
|
+
* Registry-driven analyzers over routing telemetry that answer "what should
|
|
5
|
+
* the operator change", TokenJam-style, with three disciplines borrowed
|
|
6
|
+
* deliberately:
|
|
7
|
+
*
|
|
8
|
+
* - PAST OVERSPEND, never projected savings: every dollar figure is
|
|
9
|
+
* backward-looking so the operator can check it against a bill already
|
|
10
|
+
* paid. Ceilings are labeled as ceilings in estimateBasis.
|
|
11
|
+
* - THREE-STATE HONESTY: 'actionable' (evidence found), 'clean' (ran,
|
|
12
|
+
* found nothing), 'not_measured' (data doesn't exist yet on this
|
|
13
|
+
* install). An un-run scan is not an all-clear.
|
|
14
|
+
* - MECHANISM badges: 'auto' (Lynkr could act), 'snippet' (client-side
|
|
15
|
+
* fix the operator applies), 'info' (diagnosis only).
|
|
16
|
+
*
|
|
17
|
+
* Analyzer contract: analyze({db, since, until}) -> finding
|
|
18
|
+
* {id, title, mechanism, state, framing: 'usd'|'tokens',
|
|
19
|
+
* pastOverspendUsd|null, pastOverspendTokens|null, stats: [{label,value}],
|
|
20
|
+
* evidence: {columns:[], rows:[][]}, fix, caveat, estimateBasis}
|
|
21
|
+
*
|
|
22
|
+
* The analyzers' overspend bases are disjoint by construction (dead tool
|
|
23
|
+
* schemas / harness side traffic / uncached input / tier price deltas), so
|
|
24
|
+
* the total is a plain sum — revisit if an analyzer is added whose basis
|
|
25
|
+
* overlaps an existing one.
|
|
26
|
+
*
|
|
27
|
+
* @module dashboard/recommendations
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const logger = require('../../logger');
|
|
31
|
+
const telemetry = require('../../routing/telemetry');
|
|
32
|
+
|
|
33
|
+
const ANALYZERS = [
|
|
34
|
+
require('./deadweight'),
|
|
35
|
+
require('./side-requests'),
|
|
36
|
+
require('./cache-weakspots'),
|
|
37
|
+
require('./downsize'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
41
|
+
const RESULT_TTL_MS = 45 * 1000;
|
|
42
|
+
|
|
43
|
+
let _cached = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run all analyzers. Cached for 45s — findings shift slowly and the
|
|
47
|
+
* dashboard polls every 30s.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} [opts] {windowMs, force}
|
|
50
|
+
*/
|
|
51
|
+
function run(opts = {}) {
|
|
52
|
+
const windowMs = opts.windowMs ?? DEFAULT_WINDOW_MS;
|
|
53
|
+
if (!opts.force && _cached && Date.now() - _cached.computedAt < RESULT_TTL_MS
|
|
54
|
+
&& _cached.windowMs === windowMs) {
|
|
55
|
+
return _cached;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const db = telemetry.getDb();
|
|
59
|
+
const until = Date.now();
|
|
60
|
+
const since = until - windowMs;
|
|
61
|
+
|
|
62
|
+
const findings = [];
|
|
63
|
+
for (const analyzer of ANALYZERS) {
|
|
64
|
+
try {
|
|
65
|
+
const finding = db
|
|
66
|
+
? analyzer.analyze({ db, since, until })
|
|
67
|
+
: { id: analyzer.id, title: analyzer.title, state: 'not_measured', reason: 'telemetry unavailable' };
|
|
68
|
+
if (finding) findings.push(finding);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
logger.debug({ analyzer: analyzer.id, err: err.message }, '[Recommendations] analyzer failed');
|
|
71
|
+
findings.push({ id: analyzer.id, title: analyzer.title, state: 'not_measured', reason: err.message });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rank by dollars, biggest first; token-framed findings after usd ones.
|
|
76
|
+
findings.sort((a, b) => (b.pastOverspendUsd ?? -1) - (a.pastOverspendUsd ?? -1));
|
|
77
|
+
|
|
78
|
+
let spendUsd = 0;
|
|
79
|
+
try {
|
|
80
|
+
if (db) {
|
|
81
|
+
spendUsd = db
|
|
82
|
+
.prepare('SELECT SUM(COALESCE(cost_usd,0)) s FROM routing_telemetry WHERE timestamp > ?')
|
|
83
|
+
.get(since)?.s ?? 0;
|
|
84
|
+
}
|
|
85
|
+
} catch { /* spend share stays 0 */ }
|
|
86
|
+
|
|
87
|
+
const totalRecoverableUsd = findings
|
|
88
|
+
.filter((f) => f.state === 'actionable' && typeof f.pastOverspendUsd === 'number')
|
|
89
|
+
.reduce((acc, f) => acc + f.pastOverspendUsd, 0);
|
|
90
|
+
|
|
91
|
+
_cached = {
|
|
92
|
+
computedAt: Date.now(),
|
|
93
|
+
windowMs,
|
|
94
|
+
spendUsd,
|
|
95
|
+
totalRecoverableUsd,
|
|
96
|
+
findings,
|
|
97
|
+
};
|
|
98
|
+
return _cached;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Test helper — drop the memoized result. */
|
|
102
|
+
function _clearCacheForTests() {
|
|
103
|
+
_cached = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { run, ANALYZERS, _clearCacheForTests };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* side-requests — harness side traffic served above the SIMPLE tier.
|
|
3
|
+
*
|
|
4
|
+
* Coding harnesses replay the conversation for their own bookkeeping
|
|
5
|
+
* (title generation, topic detection). Those calls embed the user's text,
|
|
6
|
+
* so they score like the user's request and ride the same tier — observed
|
|
7
|
+
* live: a title request served by a frontier COMPLEX model. The whole
|
|
8
|
+
* category needs a tiny model.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SIDE_PATTERNS = [
|
|
12
|
+
"Generate a title%",
|
|
13
|
+
"%Analyze if this message indicates a new conversation topic%",
|
|
14
|
+
"Please write a %commit message%",
|
|
15
|
+
"Summarize this conversation%",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
id: 'side-requests',
|
|
20
|
+
title: 'Harness side traffic on expensive tiers',
|
|
21
|
+
|
|
22
|
+
analyze({ db, since }) {
|
|
23
|
+
const where = SIDE_PATTERNS.map(() => 'request_text LIKE ?').join(' OR ');
|
|
24
|
+
const rows = db
|
|
25
|
+
.prepare(
|
|
26
|
+
`SELECT tier, provider, model, COUNT(*) n, SUM(COALESCE(cost_usd,0)) cost
|
|
27
|
+
FROM routing_telemetry
|
|
28
|
+
WHERE timestamp > ? AND COALESCE(tool_count,0) = 0 AND (${where})
|
|
29
|
+
GROUP BY tier, provider, model
|
|
30
|
+
ORDER BY cost DESC`
|
|
31
|
+
)
|
|
32
|
+
.all(since, ...SIDE_PATTERNS);
|
|
33
|
+
|
|
34
|
+
const above = rows.filter((r) => r.tier && r.tier !== 'SIMPLE');
|
|
35
|
+
const total = rows.reduce((a, r) => a + r.n, 0);
|
|
36
|
+
if (!above.length) {
|
|
37
|
+
return {
|
|
38
|
+
id: this.id, title: this.title, mechanism: 'auto', state: total > 0 ? 'clean' : 'clean',
|
|
39
|
+
framing: 'usd', pastOverspendUsd: 0, pastOverspendTokens: null,
|
|
40
|
+
stats: [{ label: 'side requests seen', value: total }],
|
|
41
|
+
evidence: { columns: [], rows: [] }, fix: null, caveat: null,
|
|
42
|
+
estimateBasis: 'Tool-less requests matching known harness bookkeeping prompts (title/topic/commit/summary).',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const usd = above.reduce((a, r) => a + r.cost, 0);
|
|
47
|
+
return {
|
|
48
|
+
id: this.id,
|
|
49
|
+
title: this.title,
|
|
50
|
+
mechanism: 'auto',
|
|
51
|
+
state: 'actionable',
|
|
52
|
+
framing: 'usd',
|
|
53
|
+
pastOverspendUsd: usd,
|
|
54
|
+
pastOverspendTokens: null,
|
|
55
|
+
stats: [
|
|
56
|
+
{ label: 'side requests above SIMPLE', value: above.reduce((a, r) => a + r.n, 0) },
|
|
57
|
+
{ label: 'total side requests', value: total },
|
|
58
|
+
],
|
|
59
|
+
evidence: {
|
|
60
|
+
columns: ['tier', 'provider', 'model', 'requests', 'spend $'],
|
|
61
|
+
rows: above.map((r) => [r.tier, r.provider, r.model, r.n, r.cost]),
|
|
62
|
+
},
|
|
63
|
+
fix: 'Route harness bookkeeping (title generation, topic detection, commit messages) to the SIMPLE tier unconditionally — a pattern guard ahead of intent scoring. Lynkr can enforce this; the patterns above are the trigger list.',
|
|
64
|
+
caveat: 'Ceiling figure: it counts the full spend of these calls, not the delta vs a SIMPLE-tier serve (which would be near zero for local SIMPLE tiers).',
|
|
65
|
+
estimateBasis: 'Sum of cost_usd for tool-less requests matching harness prompt patterns, served on any tier above SIMPLE, this window.',
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wilson score interval — lower bound on a binomial proportion.
|
|
3
|
+
*
|
|
4
|
+
* Used by the downsize analyzer: "the lower tier succeeded k of n times"
|
|
5
|
+
* is only evidence when the LOWER BOUND of the confidence interval clears
|
|
6
|
+
* the bar. A raw average (k/n >= 0.7) treats 21/30 the same as 700/1000;
|
|
7
|
+
* Wilson does not — small samples get wide intervals and stay unproven.
|
|
8
|
+
*
|
|
9
|
+
* @param {number} successes
|
|
10
|
+
* @param {number} n
|
|
11
|
+
* @param {number} [z=1.96] - 95% confidence
|
|
12
|
+
* @returns {number} lower bound in [0,1]; 0 when n === 0
|
|
13
|
+
*/
|
|
14
|
+
function wilsonLowerBound(successes, n, z = 1.96) {
|
|
15
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
16
|
+
const p = Math.min(1, Math.max(0, successes / n));
|
|
17
|
+
const z2 = z * z;
|
|
18
|
+
const denom = 1 + z2 / n;
|
|
19
|
+
const centre = p + z2 / (2 * n);
|
|
20
|
+
const margin = z * Math.sqrt((p * (1 - p) + z2 / (4 * n)) / n);
|
|
21
|
+
return Math.max(0, (centre - margin) / denom);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { wilsonLowerBound };
|
package/src/dashboard/router.js
CHANGED
|
@@ -18,5 +18,10 @@ router.get('/api/overview', api.overview);
|
|
|
18
18
|
router.get('/api/usage', api.usage);
|
|
19
19
|
router.get('/api/routing', api.routing);
|
|
20
20
|
router.get('/api/logs', api.logs);
|
|
21
|
+
// Lens (feature/lens-dashboard)
|
|
22
|
+
router.get('/api/recommendations', api.recommendations);
|
|
23
|
+
router.get('/api/sessions/:id', api.sessionDetail);
|
|
24
|
+
router.get('/api/analytics', api.analytics);
|
|
25
|
+
router.get('/api/statusline', api.statusline);
|
|
21
26
|
|
|
22
27
|
module.exports = router;
|
|
@@ -43,7 +43,9 @@ function getDestinationUrl(providerType) {
|
|
|
43
43
|
case 'edenai':
|
|
44
44
|
return config.edenai?.endpoint ?? 'unknown';
|
|
45
45
|
case 'openai':
|
|
46
|
-
return 'https://api.openai.com/v1/chat/completions';
|
|
46
|
+
return config.openai?.endpoint ?? 'https://api.openai.com/v1/chat/completions';
|
|
47
|
+
case 'atlas':
|
|
48
|
+
return config.atlas?.endpoint ?? 'https://api.atlascloud.ai/v1/chat/completions';
|
|
47
49
|
case 'llamacpp':
|
|
48
50
|
return config.llamacpp?.endpoint ?? 'unknown';
|
|
49
51
|
case 'lmstudio':
|
|
@@ -56,6 +58,8 @@ function getDestinationUrl(providerType) {
|
|
|
56
58
|
return config.vertex?.endpoint ?? 'unknown';
|
|
57
59
|
case 'moonshot':
|
|
58
60
|
return config.moonshot?.endpoint ?? 'unknown';
|
|
61
|
+
case 'baidu':
|
|
62
|
+
return config.baidu?.endpoint ?? 'unknown';
|
|
59
63
|
case 'codex':
|
|
60
64
|
return 'codex://app-server (local process)';
|
|
61
65
|
default:
|
|
@@ -1145,8 +1149,16 @@ function sanitizePayload(payload) {
|
|
|
1145
1149
|
} else {
|
|
1146
1150
|
clean.tools = ensureAnthropicToolFormat(clean.tools);
|
|
1147
1151
|
}
|
|
1148
|
-
} else if (providerType === "
|
|
1149
|
-
//
|
|
1152
|
+
} else if (providerType === "baidu") {
|
|
1153
|
+
// Baidu Qianfan supports tools - keep them in Anthropic format
|
|
1154
|
+
// They will be converted to OpenAI format in invokeBaidu
|
|
1155
|
+
if (!Array.isArray(clean.tools) || clean.tools.length === 0) {
|
|
1156
|
+
delete clean.tools;
|
|
1157
|
+
} else {
|
|
1158
|
+
clean.tools = ensureAnthropicToolFormat(clean.tools);
|
|
1159
|
+
}
|
|
1160
|
+
} else if (providerType === "azure-openai" || providerType === "openai" || providerType === "atlas") {
|
|
1161
|
+
// Azure OpenAI / OpenAI-compatible providers support tools — keep Anthropic format; the
|
|
1150
1162
|
// client converts to Chat Completions / Responses format. Without this
|
|
1151
1163
|
// branch the unknown-provider catch-all below deletes the tools.
|
|
1152
1164
|
if (!Array.isArray(clean.tools) || clean.tools.length === 0) {
|
|
@@ -1839,8 +1851,13 @@ IMPORTANT TOOL USAGE RULES:
|
|
|
1839
1851
|
// before they reach the model (saves 60-90% on test/git/lint output)
|
|
1840
1852
|
if (config.toolResultCompression?.enabled !== false) {
|
|
1841
1853
|
const { compressToolResults } = require("../context/tool-result-compressor");
|
|
1842
|
-
|
|
1843
|
-
|
|
1854
|
+
// Fixed threshold: compression must be deterministic per message — the
|
|
1855
|
+
// routed tier flaps between turns, and re-compressing history differently
|
|
1856
|
+
// breaks provider prompt-cache prefixes. COMPLEX (>2000 chars) compresses
|
|
1857
|
+
// only bulky outputs: with prompt caching live, resending history is
|
|
1858
|
+
// cheap, so lighter lossiness beats aggressive compression; still bounds
|
|
1859
|
+
// context growth enough to stay clear of the token-budget compressor.
|
|
1860
|
+
compressToolResults(cleanPayload.messages, { tier: "COMPLEX" });
|
|
1844
1861
|
}
|
|
1845
1862
|
|
|
1846
1863
|
// MCP-aware tool dedup: drop built-in tools superseded by present MCP tools
|
|
@@ -2606,6 +2623,12 @@ IMPORTANT TOOL USAGE RULES:
|
|
|
2606
2623
|
if (Array.isArray(anthropicPayload?.content)) {
|
|
2607
2624
|
anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
|
|
2608
2625
|
}
|
|
2626
|
+
} else if (actualProvider === "baidu") {
|
|
2627
|
+
// Baidu Qianfan responses are already converted to Anthropic format in invokeBaidu
|
|
2628
|
+
anthropicPayload = databricksResponse.json;
|
|
2629
|
+
if (Array.isArray(anthropicPayload?.content)) {
|
|
2630
|
+
anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
|
|
2631
|
+
}
|
|
2609
2632
|
} else if (actualProvider === "codex") {
|
|
2610
2633
|
// Codex responses are already in Anthropic format from invokeCodex
|
|
2611
2634
|
anthropicPayload = databricksResponse.json;
|