lynkr 9.7.3 → 9.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +63 -25
  2. package/bin/cli.js +16 -1
  3. package/bin/lynkr-init.js +44 -1
  4. package/bin/lynkr-usage.js +78 -0
  5. package/bin/wrap.js +60 -35
  6. package/config/difficulty-anchors.json +22 -0
  7. package/package.json +23 -2
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/build-eval-set.js +256 -0
  10. package/scripts/calibrate-thresholds.js +38 -157
  11. package/scripts/compact-dictionary.js +204 -0
  12. package/scripts/mine-difficulty-anchors.js +288 -0
  13. package/scripts/test-deduplication.js +448 -0
  14. package/scripts/validate-difficulty-classifier.js +123 -0
  15. package/scripts/validate-intent-anchors.js +186 -0
  16. package/scripts/ws7-anchor-replay.js +108 -0
  17. package/skills/lynkr/SKILL.md +195 -0
  18. package/src/api/middleware/loop-guard.js +87 -0
  19. package/src/api/middleware/request-logging.js +5 -64
  20. package/src/api/middleware/session.js +0 -0
  21. package/src/api/openai-router.js +120 -101
  22. package/src/api/providers-handler.js +27 -2
  23. package/src/api/router.js +467 -125
  24. package/src/budget/index.js +2 -19
  25. package/src/cache/semantic.js +9 -0
  26. package/src/clients/databricks.js +455 -142
  27. package/src/clients/gpt-utils.js +11 -105
  28. package/src/clients/openai-format.js +10 -3
  29. package/src/clients/openrouter-utils.js +49 -24
  30. package/src/clients/prompt-cache-injection.js +1 -0
  31. package/src/clients/provider-capabilities.js +1 -1
  32. package/src/clients/responses-format.js +34 -3
  33. package/src/clients/routing.js +15 -0
  34. package/src/config/index.js +36 -2
  35. package/src/context/gcf.js +275 -0
  36. package/src/context/tool-result-compressor.js +932 -47
  37. package/src/dashboard/api.js +1 -0
  38. package/src/logger/index.js +14 -1
  39. package/src/memory/search.js +12 -40
  40. package/src/memory/tools.js +3 -24
  41. package/src/orchestrator/bypass.js +4 -2
  42. package/src/orchestrator/index.js +120 -85
  43. package/src/routing/affinity-store.js +194 -0
  44. package/src/routing/agentic-detector.js +36 -6
  45. package/src/routing/bandit.js +25 -6
  46. package/src/routing/calibration.js +212 -0
  47. package/src/routing/classifier-setup.js +207 -0
  48. package/src/routing/client-profiles.js +292 -0
  49. package/src/routing/complexity-analyzer.js +88 -15
  50. package/src/routing/deescalator.js +148 -0
  51. package/src/routing/degradation.js +109 -0
  52. package/src/routing/difficulty-classifier.js +219 -0
  53. package/src/routing/feedback.js +157 -0
  54. package/src/routing/index.js +931 -90
  55. package/src/routing/intent-score.js +441 -0
  56. package/src/routing/interaction.js +3 -0
  57. package/src/routing/knn-router.js +70 -21
  58. package/src/routing/model-registry.js +28 -7
  59. package/src/routing/model-tiers.js +25 -2
  60. package/src/routing/reward-pipeline.js +68 -2
  61. package/src/routing/risk-analyzer.js +30 -1
  62. package/src/routing/risk-classifier.js +6 -2
  63. package/src/routing/session-affinity.js +162 -34
  64. package/src/routing/telemetry.js +286 -13
  65. package/src/routing/verifier.js +267 -0
  66. package/src/server.js +86 -21
  67. package/src/sessions/cleanup.js +17 -0
  68. package/src/tools/index.js +1 -15
  69. package/src/tools/smart-selection.js +10 -0
  70. package/src/tools/web-client.js +3 -3
  71. package/.eslintrc.cjs +0 -12
  72. package/benchmark-configs/litellm_config.yaml +0 -86
  73. package/benchmark-configs/lynkr.env +0 -48
  74. package/benchmark-configs/portkey-config.json +0 -60
  75. package/benchmark-configs/portkey-docker.sh +0 -23
  76. package/benchmark-tier-routing.js +0 -449
  77. package/funding.json +0 -110
  78. package/src/api/middleware/validation.js +0 -261
  79. package/src/routing/drift-monitor.js +0 -113
  80. package/src/workers/helpers.js +0 -185
@@ -2,11 +2,9 @@
2
2
  /**
3
3
  * Calibrate tier thresholds from telemetry.
4
4
  *
5
- * Phase 1.4 of the routing overhaul. Reads quality_score history from the
6
- * routing_telemetry table, finds where each tier's median quality drops below
7
- * acceptable, and writes adjusted [lo, hi] ranges to
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 fs = require('fs');
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
- const DEFAULT_RANGES = {
56
- SIMPLE: [0, 25],
57
- MEDIUM: [26, 50],
58
- COMPLEX: [51, 75],
59
- REASONING: [76, 100],
60
- };
61
-
62
- function _openDb(dbPath) {
63
- let Database;
64
- try {
65
- Database = require('better-sqlite3');
66
- } catch (err) {
67
- console.error('better-sqlite3 not installed. Install with: npm install --save-optional better-sqlite3');
68
- process.exit(2);
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({ days = DEFAULT_DAYS, dryRun = false } = {}) {
74
- const dbPath = _findDb();
75
- if (!dbPath) {
76
- console.log('No telemetry DB found — skipping calibration.');
77
- return { skipped: true, reason: 'no_db' };
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
- if (!rows || rows.length < MIN_SAMPLES) {
109
- console.log(`Only ${rows ? rows.length : 0} rows with quality_score in last ${days}d (need ≥${MIN_SAMPLES}). Skipping.`);
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
- const stats = {};
134
-
135
- for (const tier of tierOrder) {
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) {
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Compact LLM Audit Dictionary
5
+ *
6
+ * Removes redundant UPDATE entries from the dictionary file, keeping only:
7
+ * - One entry per hash with full content
8
+ * - Latest metadata (useCount, lastSeen)
9
+ *
10
+ * Usage:
11
+ * node scripts/compact-dictionary.js [options]
12
+ *
13
+ * Options:
14
+ * --dict-path <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
15
+ * --backup Create backup before compacting (default: true)
16
+ * --dry-run Show what would be done without making changes
17
+ * --help Show this help message
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const readline = require('readline');
23
+
24
+ // Parse command line arguments
25
+ function parseArgs() {
26
+ const args = process.argv.slice(2);
27
+ const options = {
28
+ dictPath: 'logs/llm-audit-dictionary.jsonl',
29
+ backup: true,
30
+ dryRun: false,
31
+ };
32
+
33
+ for (let i = 0; i < args.length; i++) {
34
+ const arg = args[i];
35
+ switch (arg) {
36
+ case '--dict-path':
37
+ options.dictPath = args[++i];
38
+ break;
39
+ case '--backup':
40
+ options.backup = true;
41
+ break;
42
+ case '--no-backup':
43
+ options.backup = false;
44
+ break;
45
+ case '--dry-run':
46
+ options.dryRun = true;
47
+ break;
48
+ case '--help':
49
+ console.log(`
50
+ Compact LLM Audit Dictionary
51
+
52
+ Removes redundant UPDATE entries from the dictionary file.
53
+
54
+ Usage:
55
+ node scripts/compact-dictionary.js [options]
56
+
57
+ Options:
58
+ --dict-path <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
59
+ --backup Create backup before compacting (default: true)
60
+ --no-backup Skip creating backup
61
+ --dry-run Show what would be done without making changes
62
+ --help Show this help message
63
+
64
+ Example:
65
+ node scripts/compact-dictionary.js --dict-path logs/llm-audit-dictionary.jsonl --dry-run
66
+ `);
67
+ process.exit(0);
68
+ default:
69
+ if (arg.startsWith('--')) {
70
+ console.error(`Unknown option: ${arg}`);
71
+ process.exit(1);
72
+ }
73
+ }
74
+ }
75
+
76
+ return options;
77
+ }
78
+
79
+ // Read and compact dictionary
80
+ async function compactDictionary(dictPath) {
81
+ if (!fs.existsSync(dictPath)) {
82
+ throw new Error(`Dictionary file not found: ${dictPath}`);
83
+ }
84
+
85
+ console.log(`Reading dictionary: ${dictPath}`);
86
+
87
+ // Map: hash -> entry object
88
+ // For each hash, we'll keep the latest metadata merged with content
89
+ const entries = new Map();
90
+ let totalLines = 0;
91
+ let malformedLines = 0;
92
+
93
+ // Read all entries
94
+ const fileStream = fs.createReadStream(dictPath);
95
+ const rl = readline.createInterface({
96
+ input: fileStream,
97
+ crlfDelay: Infinity,
98
+ });
99
+
100
+ for await (const line of rl) {
101
+ totalLines++;
102
+ if (!line.trim()) continue;
103
+
104
+ try {
105
+ const entry = JSON.parse(line);
106
+ if (!entry.hash) {
107
+ malformedLines++;
108
+ continue;
109
+ }
110
+
111
+ const hash = entry.hash;
112
+
113
+ // Check if we already have an entry for this hash
114
+ if (entries.has(hash)) {
115
+ const existing = entries.get(hash);
116
+
117
+ // Merge: keep content from entry that has it, use latest metadata
118
+ const merged = {
119
+ hash,
120
+ firstSeen: existing.firstSeen || entry.firstSeen,
121
+ useCount: entry.useCount || existing.useCount,
122
+ lastSeen: entry.lastSeen || existing.lastSeen,
123
+ content: existing.content || entry.content,
124
+ };
125
+
126
+ entries.set(hash, merged);
127
+ } else {
128
+ // First time seeing this hash
129
+ entries.set(hash, entry);
130
+ }
131
+ } catch (err) {
132
+ malformedLines++;
133
+ console.warn(`Skipping malformed line ${totalLines}: ${err.message}`);
134
+ }
135
+ }
136
+
137
+ const compactedCount = entries.size;
138
+ const removedCount = totalLines - malformedLines - compactedCount;
139
+
140
+ return {
141
+ entries,
142
+ stats: {
143
+ totalLines,
144
+ malformedLines,
145
+ uniqueHashes: compactedCount,
146
+ removedEntries: removedCount,
147
+ },
148
+ };
149
+ }
150
+
151
+ // Write compacted dictionary
152
+ async function writeCompactedDictionary(dictPath, entries, backup = true) {
153
+ // Create backup if requested
154
+ if (backup) {
155
+ const backupPath = `${dictPath}.backup.${Date.now()}`;
156
+ console.log(`Creating backup: ${backupPath}`);
157
+ fs.copyFileSync(dictPath, backupPath);
158
+ }
159
+
160
+ // Write compacted entries
161
+ console.log(`Writing compacted dictionary: ${dictPath}`);
162
+ const lines = Array.from(entries.values()).map((entry) => JSON.stringify(entry));
163
+ fs.writeFileSync(dictPath, lines.join('\n') + '\n');
164
+ }
165
+
166
+ // Main
167
+ async function main() {
168
+ try {
169
+ const options = parseArgs();
170
+ const dictPath = path.resolve(options.dictPath);
171
+
172
+ console.log('=== LLM Audit Dictionary Compaction ===\n');
173
+
174
+ // Read and compact
175
+ const { entries, stats } = await compactDictionary(dictPath);
176
+
177
+ // Report statistics
178
+ console.log('\nCompaction Statistics:');
179
+ console.log(` Total lines in dictionary: ${stats.totalLines}`);
180
+ console.log(` Malformed lines skipped: ${stats.malformedLines}`);
181
+ console.log(` Unique content hashes: ${stats.uniqueHashes}`);
182
+ console.log(` Redundant entries removed: ${stats.removedEntries}`);
183
+
184
+ const reductionPercent =
185
+ stats.totalLines > 0
186
+ ? ((stats.removedEntries / stats.totalLines) * 100).toFixed(1)
187
+ : 0;
188
+ console.log(` Size reduction: ${reductionPercent}%\n`);
189
+
190
+ if (options.dryRun) {
191
+ console.log('DRY RUN: No changes made to dictionary file.');
192
+ console.log(`Would have written ${stats.uniqueHashes} entries.\n`);
193
+ } else {
194
+ // Write compacted dictionary
195
+ await writeCompactedDictionary(dictPath, entries, options.backup);
196
+ console.log('✓ Dictionary compaction complete!\n');
197
+ }
198
+ } catch (err) {
199
+ console.error('Error:', err.message);
200
+ process.exit(1);
201
+ }
202
+ }
203
+
204
+ main();