lynkr 9.7.2 → 9.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/agents/context-manager.js +18 -2
- package/src/agents/definitions/loader.js +90 -0
- package/src/agents/executor.js +24 -2
- package/src/agents/index.js +9 -1
- package/src/agents/parallel-coordinator.js +2 -2
- package/src/agents/reflector.js +11 -1
- 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 +459 -146
- 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 +144 -88
- 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 +298 -10
- 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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LLM Audit Log Reader
|
|
5
|
+
*
|
|
6
|
+
* Utility to read and reconstruct audit log entries from deduplicated logs.
|
|
7
|
+
* Resolves hash references from the dictionary file and outputs full content.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/audit-log-reader.js [options]
|
|
11
|
+
*
|
|
12
|
+
* Options:
|
|
13
|
+
* --log-file <path> Path to audit log file (default: logs/llm-audit.log)
|
|
14
|
+
* --dict-file <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
|
|
15
|
+
* --full Output full restored entries (resolve all references)
|
|
16
|
+
* --filter <type=value> Filter by field (e.g., type=llm_request, provider=anthropic)
|
|
17
|
+
* --correlation-id <id> Filter by correlation ID
|
|
18
|
+
* --last <n> Show only last N entries
|
|
19
|
+
* --stats Show deduplication statistics
|
|
20
|
+
* --verify Verify all references can be resolved
|
|
21
|
+
* --help Show this help message
|
|
22
|
+
*
|
|
23
|
+
* Examples:
|
|
24
|
+
* # Show all entries with full content
|
|
25
|
+
* node scripts/audit-log-reader.js --full
|
|
26
|
+
*
|
|
27
|
+
* # Show only requests
|
|
28
|
+
* node scripts/audit-log-reader.js --filter type=llm_request
|
|
29
|
+
*
|
|
30
|
+
* # Show last 5 entries
|
|
31
|
+
* node scripts/audit-log-reader.js --last 5 --full
|
|
32
|
+
*
|
|
33
|
+
* # Show deduplication statistics
|
|
34
|
+
* node scripts/audit-log-reader.js --stats
|
|
35
|
+
*
|
|
36
|
+
* # Verify all references resolve
|
|
37
|
+
* node scripts/audit-log-reader.js --verify
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const fs = require("fs");
|
|
41
|
+
const path = require("path");
|
|
42
|
+
const readline = require("readline");
|
|
43
|
+
const { ContentDeduplicator } = require("../src/logger/deduplicator");
|
|
44
|
+
|
|
45
|
+
// Default file paths
|
|
46
|
+
const DEFAULT_LOG_FILE = path.join(process.cwd(), "logs", "llm-audit.log");
|
|
47
|
+
const DEFAULT_DICT_FILE = path.join(process.cwd(), "logs", "llm-audit-dictionary.jsonl");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse command line arguments
|
|
51
|
+
*/
|
|
52
|
+
function parseArgs() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
const options = {
|
|
55
|
+
logFile: DEFAULT_LOG_FILE,
|
|
56
|
+
dictFile: DEFAULT_DICT_FILE,
|
|
57
|
+
full: false,
|
|
58
|
+
filter: null,
|
|
59
|
+
correlationId: null,
|
|
60
|
+
last: null,
|
|
61
|
+
stats: false,
|
|
62
|
+
verify: false,
|
|
63
|
+
help: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
68
|
+
switch (arg) {
|
|
69
|
+
case "--log-file":
|
|
70
|
+
options.logFile = args[++i];
|
|
71
|
+
break;
|
|
72
|
+
case "--dict-file":
|
|
73
|
+
options.dictFile = args[++i];
|
|
74
|
+
break;
|
|
75
|
+
case "--full":
|
|
76
|
+
options.full = true;
|
|
77
|
+
break;
|
|
78
|
+
case "--filter":
|
|
79
|
+
const filterArg = args[++i];
|
|
80
|
+
const [key, value] = filterArg.split("=");
|
|
81
|
+
options.filter = { key, value };
|
|
82
|
+
break;
|
|
83
|
+
case "--correlation-id":
|
|
84
|
+
options.correlationId = args[++i];
|
|
85
|
+
break;
|
|
86
|
+
case "--last":
|
|
87
|
+
options.last = Number.parseInt(args[++i], 10);
|
|
88
|
+
break;
|
|
89
|
+
case "--stats":
|
|
90
|
+
options.stats = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--verify":
|
|
93
|
+
options.verify = true;
|
|
94
|
+
break;
|
|
95
|
+
case "--help":
|
|
96
|
+
options.help = true;
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
console.error(`Unknown option: ${arg}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Show help message
|
|
109
|
+
*/
|
|
110
|
+
function showHelp() {
|
|
111
|
+
const helpText = `
|
|
112
|
+
LLM Audit Log Reader
|
|
113
|
+
|
|
114
|
+
Usage: node scripts/audit-log-reader.js [options]
|
|
115
|
+
|
|
116
|
+
Options:
|
|
117
|
+
--log-file <path> Path to audit log file (default: logs/llm-audit.log)
|
|
118
|
+
--dict-file <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
|
|
119
|
+
--full Output full restored entries (resolve all references)
|
|
120
|
+
--filter <type=value> Filter by field (e.g., type=llm_request, provider=anthropic)
|
|
121
|
+
--correlation-id <id> Filter by correlation ID
|
|
122
|
+
--last <n> Show only last N entries
|
|
123
|
+
--stats Show deduplication statistics
|
|
124
|
+
--verify Verify all references can be resolved
|
|
125
|
+
--help Show this help message
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
# Show all entries with full content
|
|
129
|
+
node scripts/audit-log-reader.js --full
|
|
130
|
+
|
|
131
|
+
# Show only requests
|
|
132
|
+
node scripts/audit-log-reader.js --filter type=llm_request
|
|
133
|
+
|
|
134
|
+
# Show last 5 entries
|
|
135
|
+
node scripts/audit-log-reader.js --last 5 --full
|
|
136
|
+
|
|
137
|
+
# Show deduplication statistics
|
|
138
|
+
node scripts/audit-log-reader.js --stats
|
|
139
|
+
|
|
140
|
+
# Verify all references resolve
|
|
141
|
+
node scripts/audit-log-reader.js --verify
|
|
142
|
+
`;
|
|
143
|
+
console.log(helpText);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read and process log entries
|
|
148
|
+
*/
|
|
149
|
+
async function readLogEntries(options) {
|
|
150
|
+
const { logFile, dictFile, full, filter, correlationId, last } = options;
|
|
151
|
+
|
|
152
|
+
// Check if log file exists
|
|
153
|
+
if (!fs.existsSync(logFile)) {
|
|
154
|
+
console.error(`Log file not found: ${logFile}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Initialize deduplicator if needed
|
|
159
|
+
let deduplicator = null;
|
|
160
|
+
if (full && fs.existsSync(dictFile)) {
|
|
161
|
+
deduplicator = new ContentDeduplicator(dictFile);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const entries = [];
|
|
165
|
+
|
|
166
|
+
// Read log file line by line
|
|
167
|
+
const fileStream = fs.createReadStream(logFile);
|
|
168
|
+
const rl = readline.createInterface({
|
|
169
|
+
input: fileStream,
|
|
170
|
+
crlfDelay: Infinity,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
for await (const line of rl) {
|
|
174
|
+
if (!line.trim()) continue;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
let entry = JSON.parse(line);
|
|
178
|
+
|
|
179
|
+
// Apply filters
|
|
180
|
+
if (filter && entry[filter.key] !== filter.value) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (correlationId && entry.correlationId !== correlationId) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Restore full content if requested
|
|
188
|
+
if (full && deduplicator) {
|
|
189
|
+
entry = deduplicator.restoreEntry(entry);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
entries.push(entry);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error("Malformed log entry:", err.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Apply last N filter
|
|
199
|
+
const output = last ? entries.slice(-last) : entries;
|
|
200
|
+
|
|
201
|
+
// Output as JSONL
|
|
202
|
+
for (const entry of output) {
|
|
203
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return entries.length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Show deduplication statistics
|
|
211
|
+
*/
|
|
212
|
+
async function showStats(options) {
|
|
213
|
+
const { logFile, dictFile } = options;
|
|
214
|
+
|
|
215
|
+
if (!fs.existsSync(logFile)) {
|
|
216
|
+
console.error(`Log file not found: ${logFile}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(dictFile)) {
|
|
221
|
+
console.log("No dictionary file found. Deduplication may not be enabled.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get file sizes
|
|
226
|
+
const logStats = fs.statSync(logFile);
|
|
227
|
+
const dictStats = fs.statSync(dictFile);
|
|
228
|
+
|
|
229
|
+
console.log("\n=== LLM Audit Log Deduplication Statistics ===\n");
|
|
230
|
+
console.log(`Log file: ${logFile}`);
|
|
231
|
+
console.log(` Size: ${formatBytes(logStats.size)}`);
|
|
232
|
+
console.log(` Lines: ${await countLines(logFile)}`);
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(`Dictionary file: ${dictFile}`);
|
|
235
|
+
console.log(` Size: ${formatBytes(dictStats.size)}`);
|
|
236
|
+
console.log(` Entries: ${await countLines(dictFile)}`);
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(`Total size: ${formatBytes(logStats.size + dictStats.size)}`);
|
|
239
|
+
console.log();
|
|
240
|
+
|
|
241
|
+
// Count reference occurrences in log
|
|
242
|
+
const refCount = await countReferences(logFile);
|
|
243
|
+
console.log(`Reference objects in log: ${refCount}`);
|
|
244
|
+
console.log(`Estimated space saved: ~${formatBytes(refCount * 2000)} (assuming ~2KB per deduplicated field)`);
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Verify all references can be resolved
|
|
250
|
+
*/
|
|
251
|
+
async function verifyReferences(options) {
|
|
252
|
+
const { logFile, dictFile } = options;
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(logFile)) {
|
|
255
|
+
console.error(`Log file not found: ${logFile}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!fs.existsSync(dictFile)) {
|
|
260
|
+
console.log("No dictionary file found. Nothing to verify.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const deduplicator = new ContentDeduplicator(dictFile);
|
|
265
|
+
const fileStream = fs.createReadStream(logFile);
|
|
266
|
+
const rl = readline.createInterface({
|
|
267
|
+
input: fileStream,
|
|
268
|
+
crlfDelay: Infinity,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
let totalRefs = 0;
|
|
272
|
+
let unresolvedRefs = 0;
|
|
273
|
+
const unresolvedHashes = new Set();
|
|
274
|
+
|
|
275
|
+
console.log("Verifying references...\n");
|
|
276
|
+
|
|
277
|
+
for await (const line of rl) {
|
|
278
|
+
if (!line.trim()) continue;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const entry = JSON.parse(line);
|
|
282
|
+
|
|
283
|
+
// Check all fields for references
|
|
284
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
285
|
+
if (typeof value === "object" && value !== null && value.$ref) {
|
|
286
|
+
totalRefs++;
|
|
287
|
+
const content = deduplicator.getContent(value.$ref);
|
|
288
|
+
if (content === null) {
|
|
289
|
+
unresolvedRefs++;
|
|
290
|
+
unresolvedHashes.add(value.$ref);
|
|
291
|
+
console.error(`✗ Unresolved reference: ${value.$ref} in field "${key}"`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error("Malformed log entry:", err.message);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log("\n=== Verification Results ===\n");
|
|
301
|
+
console.log(`Total references: ${totalRefs}`);
|
|
302
|
+
console.log(`Unresolved references: ${unresolvedRefs}`);
|
|
303
|
+
console.log(`Unique unresolved hashes: ${unresolvedHashes.size}`);
|
|
304
|
+
|
|
305
|
+
if (unresolvedRefs === 0) {
|
|
306
|
+
console.log("\n✓ All references resolved successfully!");
|
|
307
|
+
} else {
|
|
308
|
+
console.log("\n✗ Some references could not be resolved. Dictionary may be incomplete.");
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Helper: Format bytes to human-readable string
|
|
315
|
+
*/
|
|
316
|
+
function formatBytes(bytes) {
|
|
317
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
318
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
319
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Helper: Count lines in a file
|
|
324
|
+
*/
|
|
325
|
+
async function countLines(filePath) {
|
|
326
|
+
const fileStream = fs.createReadStream(filePath);
|
|
327
|
+
const rl = readline.createInterface({
|
|
328
|
+
input: fileStream,
|
|
329
|
+
crlfDelay: Infinity,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
let count = 0;
|
|
333
|
+
for await (const line of rl) {
|
|
334
|
+
if (line.trim()) count++;
|
|
335
|
+
}
|
|
336
|
+
return count;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Helper: Count reference objects in log
|
|
341
|
+
*/
|
|
342
|
+
async function countReferences(filePath) {
|
|
343
|
+
const fileStream = fs.createReadStream(filePath);
|
|
344
|
+
const rl = readline.createInterface({
|
|
345
|
+
input: fileStream,
|
|
346
|
+
crlfDelay: Infinity,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
let count = 0;
|
|
350
|
+
for await (const line of rl) {
|
|
351
|
+
if (!line.trim()) continue;
|
|
352
|
+
try {
|
|
353
|
+
const entry = JSON.parse(line);
|
|
354
|
+
for (const value of Object.values(entry)) {
|
|
355
|
+
if (typeof value === "object" && value !== null && value.$ref) {
|
|
356
|
+
count++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
// Skip malformed lines
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return count;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Main entry point
|
|
368
|
+
*/
|
|
369
|
+
async function main() {
|
|
370
|
+
const options = parseArgs();
|
|
371
|
+
|
|
372
|
+
if (options.help) {
|
|
373
|
+
showHelp();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (options.stats) {
|
|
378
|
+
await showStats(options);
|
|
379
|
+
} else if (options.verify) {
|
|
380
|
+
await verifyReferences(options);
|
|
381
|
+
} else {
|
|
382
|
+
const count = await readLogEntries(options);
|
|
383
|
+
console.error(`\n(Processed ${count} entries)`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Run if called directly
|
|
388
|
+
if (require.main === module) {
|
|
389
|
+
main().catch((err) => {
|
|
390
|
+
console.error("Error:", err.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
readLogEntries,
|
|
397
|
+
showStats,
|
|
398
|
+
verifyReferences,
|
|
399
|
+
};
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Calibrate tier thresholds from telemetry.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* data/calibrated-thresholds.json. ModelTierSelector picks the file up on
|
|
9
|
-
* next start.
|
|
5
|
+
* CLI wrapper around `src/routing/calibration.js`. WS5.6 moved the core
|
|
6
|
+
* logic into a module so the same code path drives both this manual
|
|
7
|
+
* script and the in-process auto-calibration scheduler.
|
|
10
8
|
*
|
|
11
9
|
* Usage: node scripts/calibrate-thresholds.js [--days N] [--dry-run]
|
|
12
10
|
* npx lynkr calibrate
|
|
@@ -16,31 +14,9 @@
|
|
|
16
14
|
* - Exits 0 with a "skipped" message.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
const
|
|
20
|
-
const path = require('path');
|
|
17
|
+
const { runCalibration } = require('../src/routing/calibration');
|
|
21
18
|
|
|
22
19
|
const DEFAULT_DAYS = 7;
|
|
23
|
-
const MIN_SAMPLES = 100;
|
|
24
|
-
/** Quality score below which a complexity bucket is "underperforming" for its tier. */
|
|
25
|
-
const QUALITY_FLOOR = {
|
|
26
|
-
SIMPLE: 55,
|
|
27
|
-
MEDIUM: 60,
|
|
28
|
-
COMPLEX: 65,
|
|
29
|
-
REASONING: 70,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const OUTPUT_PATH = path.join(__dirname, '../data/calibrated-thresholds.json');
|
|
33
|
-
const TELEMETRY_DB_CANDIDATES = [
|
|
34
|
-
path.join(__dirname, '../.lynkr/telemetry.db'),
|
|
35
|
-
path.join(__dirname, '../data/lynkr.db'),
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
function _findDb() {
|
|
39
|
-
for (const p of TELEMETRY_DB_CANDIDATES) {
|
|
40
|
-
if (fs.existsSync(p)) return p;
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
20
|
|
|
45
21
|
function _parseArgs(argv) {
|
|
46
22
|
const out = { days: DEFAULT_DAYS, dryRun: false };
|
|
@@ -52,141 +28,46 @@ function _parseArgs(argv) {
|
|
|
52
28
|
return out;
|
|
53
29
|
}
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
31
|
+
function _reportSkipped(result) {
|
|
32
|
+
switch (result.reason) {
|
|
33
|
+
case 'no_db':
|
|
34
|
+
console.log('No telemetry DB found — skipping calibration.');
|
|
35
|
+
break;
|
|
36
|
+
case 'db_open_failed':
|
|
37
|
+
console.error(`Failed to open telemetry DB: ${result.error}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
break;
|
|
40
|
+
case 'query_failed':
|
|
41
|
+
console.error(`Telemetry query failed (DB may be corrupt or schema missing): ${result.error}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'insufficient_samples':
|
|
44
|
+
console.log(
|
|
45
|
+
`Only ${result.count} rows with quality_score (need ≥${result.minSamples}). Skipping.`
|
|
46
|
+
);
|
|
47
|
+
break;
|
|
48
|
+
case 'write_failed':
|
|
49
|
+
console.error(`Failed to write calibrated thresholds: ${result.error}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
console.log(`Skipped: ${result.reason}`);
|
|
69
54
|
}
|
|
70
|
-
return new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
function calibrate(
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let db;
|
|
81
|
-
try {
|
|
82
|
-
db = _openDb(dbPath);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error(`Failed to open telemetry DB: ${err.message}`);
|
|
85
|
-
return { skipped: true, reason: 'db_open_failed', error: err.message };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const since = Date.now() - days * 24 * 3600 * 1000;
|
|
89
|
-
let rows;
|
|
90
|
-
try {
|
|
91
|
-
rows = db
|
|
92
|
-
.prepare(
|
|
93
|
-
`SELECT tier, complexity_score AS score, quality_score AS q
|
|
94
|
-
FROM routing_telemetry
|
|
95
|
-
WHERE timestamp >= ?
|
|
96
|
-
AND quality_score IS NOT NULL
|
|
97
|
-
AND complexity_score IS NOT NULL
|
|
98
|
-
AND tier IS NOT NULL`
|
|
99
|
-
)
|
|
100
|
-
.all(since);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error(`Telemetry query failed (DB may be corrupt or schema missing): ${err.message}`);
|
|
103
|
-
return { skipped: true, reason: 'query_failed', error: err.message };
|
|
104
|
-
} finally {
|
|
105
|
-
try { db.close(); } catch {}
|
|
57
|
+
function calibrate(opts) {
|
|
58
|
+
const result = runCalibration(opts);
|
|
59
|
+
if (result.skipped) {
|
|
60
|
+
_reportSkipped(result);
|
|
61
|
+
return result;
|
|
106
62
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return { skipped: true, reason: 'insufficient_samples', count: rows ? rows.length : 0 };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Bucket by score (0-100 in width-5 buckets) per tier, compute median quality.
|
|
114
|
-
const buckets = new Map(); // tier -> Map<bucketLowerBound, q-values[]>
|
|
115
|
-
for (const row of rows) {
|
|
116
|
-
const s = Math.max(0, Math.min(100, Math.floor(row.score)));
|
|
117
|
-
const bucket = Math.floor(s / 5) * 5;
|
|
118
|
-
if (!buckets.has(row.tier)) buckets.set(row.tier, new Map());
|
|
119
|
-
const b = buckets.get(row.tier);
|
|
120
|
-
if (!b.has(bucket)) b.set(bucket, []);
|
|
121
|
-
b.get(bucket).push(row.q);
|
|
63
|
+
if (result.dryRun) {
|
|
64
|
+
console.log(JSON.stringify(result, null, 2));
|
|
65
|
+
return result;
|
|
122
66
|
}
|
|
123
|
-
|
|
124
|
-
const _median = (arr) => {
|
|
125
|
-
const s = arr.slice().sort((a, b) => a - b);
|
|
126
|
-
const m = Math.floor(s.length / 2);
|
|
127
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// Default ranges; will adjust per-tier upper bound if late buckets show poor quality.
|
|
131
|
-
const ranges = { ...DEFAULT_RANGES };
|
|
132
67
|
const tierOrder = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const floor = QUALITY_FLOOR[tier];
|
|
137
|
-
const tierBuckets = buckets.get(tier);
|
|
138
|
-
if (!tierBuckets) {
|
|
139
|
-
stats[tier] = { samples: 0, adjusted: false };
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const ordered = Array.from(tierBuckets.entries()).sort((a, b) => a[0] - b[0]);
|
|
143
|
-
let suggestedUpper = DEFAULT_RANGES[tier][1];
|
|
144
|
-
const buckets_summary = [];
|
|
145
|
-
for (const [lo, vals] of ordered) {
|
|
146
|
-
if (vals.length < 5) {
|
|
147
|
-
buckets_summary.push({ bucket: lo, samples: vals.length, median: null });
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const med = _median(vals);
|
|
151
|
-
buckets_summary.push({ bucket: lo, samples: vals.length, median: med });
|
|
152
|
-
if (med < floor && lo + 4 < suggestedUpper) {
|
|
153
|
-
suggestedUpper = lo + 4; // shrink tier upper bound just below the failing bucket
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (suggestedUpper !== DEFAULT_RANGES[tier][1]) {
|
|
157
|
-
ranges[tier] = [DEFAULT_RANGES[tier][0], suggestedUpper];
|
|
158
|
-
stats[tier] = { samples: ordered.reduce((s, [, v]) => s + v.length, 0), adjusted: true, buckets: buckets_summary };
|
|
159
|
-
} else {
|
|
160
|
-
stats[tier] = { samples: ordered.reduce((s, [, v]) => s + v.length, 0), adjusted: false, buckets: buckets_summary };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Re-stitch ranges so they don't overlap or leave gaps.
|
|
165
|
-
for (let i = 1; i < tierOrder.length; i++) {
|
|
166
|
-
const prev = ranges[tierOrder[i - 1]];
|
|
167
|
-
const cur = ranges[tierOrder[i]];
|
|
168
|
-
if (cur[0] !== prev[1] + 1) cur[0] = prev[1] + 1;
|
|
169
|
-
if (cur[0] > cur[1]) cur[1] = cur[0]; // collapsed; tier disabled in practice
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const out = {
|
|
173
|
-
calibratedAt: new Date().toISOString(),
|
|
174
|
-
days,
|
|
175
|
-
sampleCount: rows.length,
|
|
176
|
-
ranges,
|
|
177
|
-
stats,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
if (dryRun) {
|
|
181
|
-
console.log(JSON.stringify(out, null, 2));
|
|
182
|
-
return { ...out, dryRun: true };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
|
|
186
|
-
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(out, null, 2));
|
|
187
|
-
console.log(`Wrote ${OUTPUT_PATH}`);
|
|
188
|
-
console.log(`Ranges: ${tierOrder.map((t) => `${t}=${ranges[t].join('-')}`).join(', ')}`);
|
|
189
|
-
return out;
|
|
68
|
+
console.log(`Wrote ${result.writtenTo}`);
|
|
69
|
+
console.log(`Ranges: ${tierOrder.map((t) => `${t}=${result.ranges[t].join('-')}`).join(', ')}`);
|
|
70
|
+
return result;
|
|
190
71
|
}
|
|
191
72
|
|
|
192
73
|
if (require.main === module) {
|