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.
Files changed (80) hide show
  1. package/README.md +29 -19
  2. package/bin/cli.js +11 -0
  3. package/bin/lynkr-init.js +14 -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 +24 -3
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/calibrate-thresholds.js +38 -157
  10. package/scripts/compact-dictionary.js +204 -0
  11. package/scripts/test-deduplication.js +448 -0
  12. package/scripts/ws7-anchor-replay.js +108 -0
  13. package/skills/lynkr/SKILL.md +195 -0
  14. package/src/agents/context-manager.js +18 -2
  15. package/src/agents/definitions/loader.js +90 -0
  16. package/src/agents/executor.js +24 -2
  17. package/src/agents/index.js +9 -1
  18. package/src/agents/parallel-coordinator.js +2 -2
  19. package/src/agents/reflector.js +11 -1
  20. package/src/api/middleware/loop-guard.js +87 -0
  21. package/src/api/middleware/request-logging.js +5 -64
  22. package/src/api/middleware/session.js +0 -0
  23. package/src/api/openai-router.js +120 -101
  24. package/src/api/providers-handler.js +27 -2
  25. package/src/api/router.js +450 -125
  26. package/src/budget/index.js +2 -19
  27. package/src/cache/semantic.js +9 -0
  28. package/src/clients/databricks.js +459 -146
  29. package/src/clients/gpt-utils.js +11 -105
  30. package/src/clients/openai-format.js +10 -3
  31. package/src/clients/openrouter-utils.js +49 -24
  32. package/src/clients/prompt-cache-injection.js +1 -0
  33. package/src/clients/provider-capabilities.js +1 -1
  34. package/src/clients/responses-format.js +34 -3
  35. package/src/clients/routing.js +15 -0
  36. package/src/config/index.js +36 -2
  37. package/src/context/gcf.js +275 -0
  38. package/src/context/tool-result-compressor.js +51 -9
  39. package/src/dashboard/api.js +1 -0
  40. package/src/logger/index.js +14 -1
  41. package/src/memory/search.js +12 -40
  42. package/src/memory/tools.js +3 -24
  43. package/src/orchestrator/bypass.js +4 -2
  44. package/src/orchestrator/index.js +144 -88
  45. package/src/routing/affinity-store.js +194 -0
  46. package/src/routing/agentic-detector.js +36 -6
  47. package/src/routing/bandit.js +25 -6
  48. package/src/routing/calibration.js +212 -0
  49. package/src/routing/client-profiles.js +292 -0
  50. package/src/routing/complexity-analyzer.js +48 -11
  51. package/src/routing/deescalator.js +148 -0
  52. package/src/routing/degradation.js +109 -0
  53. package/src/routing/feedback.js +157 -0
  54. package/src/routing/index.js +897 -87
  55. package/src/routing/intent-score.js +339 -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 +298 -10
  65. package/src/routing/verifier.js +267 -0
  66. package/src/server.js +66 -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
@@ -1,261 +0,0 @@
1
- const { BadRequestError } = require("./error-handling");
2
-
3
- /**
4
- * Input validation middleware
5
- *
6
- * Features:
7
- * - JSON schema-like validation
8
- * - Type checking
9
- * - Range validation
10
- * - Sanitization
11
- * - Performance-optimized (no external dependencies)
12
- */
13
-
14
- /**
15
- * Validate request body against schema
16
- */
17
- function validateBody(schema) {
18
- return (req, res, next) => {
19
- try {
20
- const errors = validateObject(req.body, schema, "body");
21
-
22
- if (errors.length > 0) {
23
- throw new BadRequestError("Validation failed", { errors });
24
- }
25
-
26
- next();
27
- } catch (err) {
28
- next(err);
29
- }
30
- };
31
- }
32
-
33
- /**
34
- * Validate query parameters against schema
35
- */
36
- function validateQuery(schema) {
37
- return (req, res, next) => {
38
- try {
39
- const errors = validateObject(req.query, schema, "query");
40
-
41
- if (errors.length > 0) {
42
- throw new BadRequestError("Validation failed", { errors });
43
- }
44
-
45
- next();
46
- } catch (err) {
47
- next(err);
48
- }
49
- };
50
- }
51
-
52
- /**
53
- * Validate object against schema
54
- */
55
- function validateObject(obj, schema, path = "") {
56
- const errors = [];
57
-
58
- // Check required fields
59
- if (schema.required && Array.isArray(schema.required)) {
60
- for (const field of schema.required) {
61
- if (obj[field] === undefined || obj[field] === null) {
62
- errors.push({
63
- field: `${path}.${field}`,
64
- message: `Field is required`,
65
- code: "required",
66
- });
67
- }
68
- }
69
- }
70
-
71
- // Check properties
72
- if (schema.properties) {
73
- for (const [field, fieldSchema] of Object.entries(schema.properties)) {
74
- const value = obj[field];
75
-
76
- // Skip if optional and not present
77
- if (value === undefined || value === null) {
78
- if (!schema.required || !schema.required.includes(field)) {
79
- continue;
80
- }
81
- }
82
-
83
- // Validate field
84
- const fieldErrors = validateField(value, fieldSchema, `${path}.${field}`);
85
- errors.push(...fieldErrors);
86
- }
87
- }
88
-
89
- return errors;
90
- }
91
-
92
- /**
93
- * Validate individual field
94
- */
95
- function validateField(value, schema, path) {
96
- const errors = [];
97
-
98
- // Type validation
99
- if (schema.type) {
100
- const actualType = Array.isArray(value) ? "array" : typeof value;
101
-
102
- if (actualType !== schema.type) {
103
- errors.push({
104
- field: path,
105
- message: `Expected type ${schema.type}, got ${actualType}`,
106
- code: "invalid_type",
107
- });
108
- return errors; // Stop further validation if type is wrong
109
- }
110
- }
111
-
112
- // String validations
113
- if (schema.type === "string") {
114
- if (schema.minLength && value.length < schema.minLength) {
115
- errors.push({
116
- field: path,
117
- message: `String length must be at least ${schema.minLength}`,
118
- code: "min_length",
119
- });
120
- }
121
-
122
- if (schema.maxLength && value.length > schema.maxLength) {
123
- errors.push({
124
- field: path,
125
- message: `String length must be at most ${schema.maxLength}`,
126
- code: "max_length",
127
- });
128
- }
129
-
130
- if (schema.pattern) {
131
- const regex = new RegExp(schema.pattern);
132
- if (!regex.test(value)) {
133
- errors.push({
134
- field: path,
135
- message: `String does not match pattern ${schema.pattern}`,
136
- code: "pattern_mismatch",
137
- });
138
- }
139
- }
140
-
141
- if (schema.enum && !schema.enum.includes(value)) {
142
- errors.push({
143
- field: path,
144
- message: `Value must be one of: ${schema.enum.join(", ")}`,
145
- code: "invalid_enum",
146
- });
147
- }
148
- }
149
-
150
- // Number validations
151
- if (schema.type === "number") {
152
- if (schema.minimum !== undefined && value < schema.minimum) {
153
- errors.push({
154
- field: path,
155
- message: `Value must be at least ${schema.minimum}`,
156
- code: "minimum",
157
- });
158
- }
159
-
160
- if (schema.maximum !== undefined && value > schema.maximum) {
161
- errors.push({
162
- field: path,
163
- message: `Value must be at most ${schema.maximum}`,
164
- code: "maximum",
165
- });
166
- }
167
- }
168
-
169
- // Array validations
170
- if (schema.type === "array") {
171
- if (schema.minItems && value.length < schema.minItems) {
172
- errors.push({
173
- field: path,
174
- message: `Array must have at least ${schema.minItems} items`,
175
- code: "min_items",
176
- });
177
- }
178
-
179
- if (schema.maxItems && value.length > schema.maxItems) {
180
- errors.push({
181
- field: path,
182
- message: `Array must have at most ${schema.maxItems} items`,
183
- code: "max_items",
184
- });
185
- }
186
-
187
- // Validate array items
188
- if (schema.items) {
189
- value.forEach((item, index) => {
190
- const itemErrors = validateField(item, schema.items, `${path}[${index}]`);
191
- errors.push(...itemErrors);
192
- });
193
- }
194
- }
195
-
196
- // Object validations
197
- if (schema.type === "object" && schema.properties) {
198
- const objectErrors = validateObject(value, schema, path);
199
- errors.push(...objectErrors);
200
- }
201
-
202
- return errors;
203
- }
204
-
205
- /**
206
- * Common validation schemas
207
- */
208
- const commonSchemas = {
209
- // Messages endpoint
210
- messagesRequest: {
211
- type: "object",
212
- required: ["model", "messages"],
213
- properties: {
214
- model: {
215
- type: "string",
216
- minLength: 1,
217
- maxLength: 200,
218
- },
219
- messages: {
220
- type: "array",
221
- minItems: 1,
222
- maxItems: 1000,
223
- items: {
224
- type: "object",
225
- required: ["role", "content"],
226
- properties: {
227
- role: {
228
- type: "string",
229
- enum: ["user", "assistant", "system"],
230
- },
231
- content: {
232
- type: "string",
233
- minLength: 1,
234
- },
235
- },
236
- },
237
- },
238
- max_tokens: {
239
- type: "number",
240
- minimum: 1,
241
- maximum: 100000,
242
- },
243
- temperature: {
244
- type: "number",
245
- minimum: 0,
246
- maximum: 2,
247
- },
248
- stream: {
249
- type: "boolean",
250
- },
251
- },
252
- },
253
- };
254
-
255
- module.exports = {
256
- validateBody,
257
- validateQuery,
258
- validateObject,
259
- validateField,
260
- commonSchemas,
261
- };
@@ -1,113 +0,0 @@
1
- /**
2
- * Drift monitor (Phase 4.3).
3
- *
4
- * Tracks two kinds of drift:
5
- * - Input drift: distribution of query embeddings week-over-week via PSI
6
- * (Population Stability Index) over coarse bucket assignments.
7
- * - Output drift: refusal rate, average response length, latency
8
- * distribution per model.
9
- *
10
- * Computes a PSI per metric; alerts when PSI > 0.2 (warning) or > 0.3
11
- * (full retrain recommended). Writes alerts to data/drift-alerts.json.
12
- *
13
- * Auto-retrain is gated on LYNKR_AUTO_RETRAIN=true and not implemented here —
14
- * the consumer (a cron job or the dashboard) decides what to do.
15
- */
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
- const logger = require('../logger');
20
-
21
- const ALERTS_PATH = path.join(__dirname, '../../data/drift-alerts.json');
22
- const WARN_THRESHOLD = 0.2;
23
- const RETRAIN_THRESHOLD = 0.3;
24
-
25
- function _bucketize(values, bucketCount = 10, min, max) {
26
- if (values.length === 0) return new Array(bucketCount).fill(0);
27
- const lo = min ?? Math.min(...values);
28
- const hi = max ?? Math.max(...values);
29
- const range = Math.max(1e-9, hi - lo);
30
- const counts = new Array(bucketCount).fill(0);
31
- for (const v of values) {
32
- const idx = Math.max(0, Math.min(bucketCount - 1, Math.floor(((v - lo) / range) * bucketCount)));
33
- counts[idx]++;
34
- }
35
- return counts;
36
- }
37
-
38
- /**
39
- * Population Stability Index between two distributions.
40
- * PSI = Σ (p_new - p_old) · ln(p_new / p_old)
41
- */
42
- function psi(oldCounts, newCounts) {
43
- const oldTotal = oldCounts.reduce((s, c) => s + c, 0);
44
- const newTotal = newCounts.reduce((s, c) => s + c, 0);
45
- if (oldTotal === 0 || newTotal === 0) return 0;
46
- let sum = 0;
47
- for (let i = 0; i < oldCounts.length; i++) {
48
- const p = (oldCounts[i] + 0.5) / (oldTotal + oldCounts.length * 0.5);
49
- const q = (newCounts[i] + 0.5) / (newTotal + newCounts.length * 0.5);
50
- sum += (q - p) * Math.log(q / p);
51
- }
52
- return sum;
53
- }
54
-
55
- function _writeAlert(alert) {
56
- try {
57
- fs.mkdirSync(path.dirname(ALERTS_PATH), { recursive: true });
58
- let existing = [];
59
- if (fs.existsSync(ALERTS_PATH)) {
60
- try { existing = JSON.parse(fs.readFileSync(ALERTS_PATH, 'utf8')); } catch {}
61
- }
62
- const out = Array.isArray(existing) ? existing : [];
63
- out.push({ ...alert, timestamp: Date.now() });
64
- fs.writeFileSync(ALERTS_PATH, JSON.stringify(out.slice(-200), null, 2));
65
- } catch (err) {
66
- logger.warn({ err: err.message }, '[DriftMonitor] Alert write failed');
67
- }
68
- }
69
-
70
- /**
71
- * Detect drift between two value series.
72
- * @param {string} metric - name for logging
73
- * @param {number[]} oldValues - reference window (e.g. last week)
74
- * @param {number[]} newValues - current window (e.g. last 24h)
75
- * @returns {{ psi, level, metric }}
76
- */
77
- function detect(metric, oldValues, newValues) {
78
- if (oldValues.length < 50 || newValues.length < 20) {
79
- return { psi: 0, level: 'insufficient_data', metric };
80
- }
81
- const combinedMin = Math.min(...oldValues, ...newValues);
82
- const combinedMax = Math.max(...oldValues, ...newValues);
83
- const oldB = _bucketize(oldValues, 10, combinedMin, combinedMax);
84
- const newB = _bucketize(newValues, 10, combinedMin, combinedMax);
85
- const p = psi(oldB, newB);
86
- let level = 'ok';
87
- if (p >= RETRAIN_THRESHOLD) level = 'retrain';
88
- else if (p >= WARN_THRESHOLD) level = 'warn';
89
-
90
- if (level !== 'ok') {
91
- _writeAlert({ metric, psi: p, level, oldSize: oldValues.length, newSize: newValues.length });
92
- }
93
- return { psi: p, level, metric };
94
- }
95
-
96
- /**
97
- * Detect refusal-rate drift by counting the share of responses containing
98
- * refusal markers in two windows.
99
- */
100
- function refusalRateShift(oldResponses, newResponses) {
101
- const markers = [/i can't help/i, /i won't/i, /against (?:my )?guidelines/i, /i cannot/i];
102
- const _rate = (arr) => arr.filter(t => markers.some(m => m.test(t))).length / Math.max(1, arr.length);
103
- return { old: _rate(oldResponses), new: _rate(newResponses) };
104
- }
105
-
106
- module.exports = {
107
- psi,
108
- detect,
109
- refusalRateShift,
110
- _bucketize,
111
- WARN_THRESHOLD,
112
- RETRAIN_THRESHOLD,
113
- };
@@ -1,185 +0,0 @@
1
- /**
2
- * Worker Pool Helper Functions
3
- *
4
- * Provides easy-to-use async wrappers for offloading CPU-intensive operations
5
- * to worker threads with automatic fallback to sync operations.
6
- *
7
- * @module workers/helpers
8
- */
9
-
10
- const { getWorkerPool, isWorkerPoolReady } = require('./pool');
11
- const config = require('../config');
12
-
13
- // Threshold for offloading (bytes)
14
- const OFFLOAD_THRESHOLD = config.workerPool?.offloadThresholdBytes || 10000;
15
-
16
- /**
17
- * Deep clone an object, using worker thread for large objects
18
- * @param {*} obj - Object to clone
19
- * @returns {Promise<*>} - Cloned object
20
- */
21
- async function asyncClone(obj) {
22
- // Estimate size quickly
23
- let estimatedSize;
24
- try {
25
- estimatedSize = JSON.stringify(obj).length;
26
- } catch {
27
- // If stringify fails, fall back to sync clone
28
- return syncClone(obj);
29
- }
30
-
31
- // Use worker for large objects, sync for small ones
32
- if (config.workerPool?.enabled !== false && isWorkerPoolReady() && estimatedSize >= OFFLOAD_THRESHOLD) {
33
- try {
34
- const pool = getWorkerPool();
35
- return await pool.clone(obj);
36
- } catch (err) {
37
- // Fall back to sync on error
38
- return syncClone(obj);
39
- }
40
- }
41
-
42
- return syncClone(obj);
43
- }
44
-
45
- /**
46
- * Synchronous clone (inline, for small objects)
47
- */
48
- function syncClone(obj) {
49
- if (typeof structuredClone === 'function') {
50
- try {
51
- return structuredClone(obj);
52
- } catch {
53
- // structuredClone can fail on some objects
54
- }
55
- }
56
- return JSON.parse(JSON.stringify(obj));
57
- }
58
-
59
- /**
60
- * Parse JSON, using worker thread for large strings
61
- * @param {string} jsonString - JSON string to parse
62
- * @returns {Promise<*>} - Parsed object
63
- */
64
- async function asyncParse(jsonString) {
65
- if (typeof jsonString !== 'string') {
66
- throw new TypeError('Expected a JSON string');
67
- }
68
-
69
- if (config.workerPool?.enabled !== false && isWorkerPoolReady() && jsonString.length >= OFFLOAD_THRESHOLD) {
70
- try {
71
- const pool = getWorkerPool();
72
- return await pool.parse(jsonString);
73
- } catch (err) {
74
- // Fall back to sync on error
75
- return JSON.parse(jsonString);
76
- }
77
- }
78
-
79
- return JSON.parse(jsonString);
80
- }
81
-
82
- /**
83
- * Stringify object, using worker thread for large objects
84
- * @param {*} obj - Object to stringify
85
- * @returns {Promise<string>} - JSON string
86
- */
87
- async function asyncStringify(obj) {
88
- // Quick check for size
89
- const quick = JSON.stringify(obj);
90
-
91
- if (config.workerPool?.enabled !== false && isWorkerPoolReady() && quick.length >= OFFLOAD_THRESHOLD) {
92
- try {
93
- const pool = getWorkerPool();
94
- return await pool.stringify(obj);
95
- } catch {
96
- // Already have the result from quick check
97
- return quick;
98
- }
99
- }
100
-
101
- return quick;
102
- }
103
-
104
- /**
105
- * Transform messages (compression, truncation), using worker thread
106
- * @param {Array} messages - Messages to transform
107
- * @param {Object} options - Transform options
108
- * @returns {Promise<Object>} - Transformed messages with stats
109
- */
110
- async function asyncTransform(messages, options = {}) {
111
- if (!Array.isArray(messages) || messages.length === 0) {
112
- return { messages, transformed: false, stats: {} };
113
- }
114
-
115
- if (config.workerPool?.enabled !== false && isWorkerPoolReady()) {
116
- try {
117
- const pool = getWorkerPool();
118
- return await pool.transform(messages, options);
119
- } catch (err) {
120
- // Return untransformed on error
121
- return { messages, transformed: false, stats: {}, error: err.message };
122
- }
123
- }
124
-
125
- // Sync fallback - basic truncation
126
- return syncTransform(messages, options);
127
- }
128
-
129
- /**
130
- * Synchronous transform fallback
131
- */
132
- function syncTransform(messages, options = {}) {
133
- const { maxAssistantLength = 5000, maxToolResultLength = 3000 } = options;
134
-
135
- const transformed = messages.map(msg => {
136
- if (!msg) return msg;
137
-
138
- // Truncate long assistant messages
139
- if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.length > maxAssistantLength) {
140
- return {
141
- ...msg,
142
- content: msg.content.substring(0, maxAssistantLength) + '\n[...truncated...]',
143
- };
144
- }
145
-
146
- // Truncate tool results
147
- if (msg.role === 'tool' && typeof msg.content === 'string' && msg.content.length > maxToolResultLength) {
148
- return {
149
- ...msg,
150
- content: msg.content.substring(0, maxToolResultLength) + '\n[...truncated...]',
151
- };
152
- }
153
-
154
- return msg;
155
- });
156
-
157
- return { messages: transformed, transformed: true, stats: {} };
158
- }
159
-
160
- /**
161
- * Get worker pool statistics
162
- * @returns {Object|null} - Pool stats or null if not available
163
- */
164
- function getPoolStats() {
165
- if (!isWorkerPoolReady()) {
166
- return null;
167
- }
168
- try {
169
- const pool = getWorkerPool();
170
- return pool.getStats();
171
- } catch {
172
- return null;
173
- }
174
- }
175
-
176
- module.exports = {
177
- asyncClone,
178
- asyncParse,
179
- asyncStringify,
180
- asyncTransform,
181
- syncClone,
182
- syncTransform,
183
- getPoolStats,
184
- OFFLOAD_THRESHOLD,
185
- };