lynkr 9.14.0 → 9.14.2
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/package.json +10 -22
- package/src/api/health.js +0 -1
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +94 -13
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +6 -3
- package/src/cache/prompt.js +0 -7
- package/src/clients/bedrock-utils.js +2 -1
- package/src/clients/codex-process.js +1 -1
- package/src/clients/codex-utils.js +0 -2
- package/src/clients/databricks.js +59 -25
- package/src/clients/gpt-utils.js +1 -1
- package/src/clients/openai-format.js +73 -23
- package/src/clients/provider-capabilities.js +0 -2
- package/src/clients/resilience.js +0 -1
- package/src/clients/responses-format.js +0 -56
- package/src/clients/routing.js +1 -1
- package/src/config/index.js +0 -1
- package/src/context/budget.js +1 -1
- package/src/context/tool-result-compressor.js +3 -5
- package/src/dashboard/api.js +0 -1
- package/src/headroom/client.js +44 -20
- package/src/logger/oversized-error-stream.js +1 -1
- package/src/memory/extractor.js +3 -3
- package/src/memory/search.js +2 -2
- package/src/memory/store.js +9 -24
- package/src/memory/tools.js +14 -0
- package/src/observability/metrics.js +0 -3
- package/src/orchestrator/index.js +1 -80
- package/src/orchestrator/sse-transformer.js +68 -10
- package/src/policy/safe-commands.js +0 -1
- package/src/routing/calibration.js +21 -1
- package/src/routing/client-profiles.js +26 -0
- package/src/routing/complexity-analyzer.js +5 -8
- package/src/routing/confidence-scorer.js +2 -2
- package/src/routing/cost-optimizer.js +1 -2
- package/src/routing/deescalator.js +1 -1
- package/src/routing/embedding-cache.js +0 -1
- package/src/routing/intent-score.js +0 -1
- package/src/routing/knn-router.js +27 -5
- package/src/routing/latency-tracker.js +1 -1
- package/src/routing/regret-estimator.js +1 -1
- package/src/stores/file-store.js +5 -1
- package/src/training/trajectory-compressor.js +1 -1
- package/src/workers/pool.js +1 -1
- package/src/workers/worker.js +1 -1
- package/scripts/convert-routellm.py +0 -105
- package/src/diff/comments.js +0 -153
- package/src/policy/web-fallback.js +0 -33
|
@@ -158,7 +158,62 @@ async function* _openaiToAnthropicEvents(upstream, opts = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
+
// GLM-family reasoning leak filter (streaming twin of the fix in
|
|
162
|
+
// convertOpenAIToAnthropic): suppresses matched <think>…</think> spans and
|
|
163
|
+
// strips a bare orphan closing tag from visible text deltas, holding back
|
|
164
|
+
// trailing partial-tag fragments so tags split across chunk boundaries
|
|
165
|
+
// still match. Orphan pre-text has already been emitted by the time the
|
|
166
|
+
// closer arrives — a stream can't retract — so only the tag is removed.
|
|
167
|
+
let _inThink = false;
|
|
168
|
+
let _tagTail = "";
|
|
169
|
+
const _partialTagLen = (s) => {
|
|
170
|
+
for (let k = Math.min(8, s.length); k > 0; k--) {
|
|
171
|
+
const suf = s.slice(-k);
|
|
172
|
+
if ("</think>".startsWith(suf) || "<think>".startsWith(suf)) return k;
|
|
173
|
+
}
|
|
174
|
+
return 0;
|
|
175
|
+
};
|
|
176
|
+
const filterThink = (text) => {
|
|
177
|
+
let s = _tagTail + text;
|
|
178
|
+
_tagTail = "";
|
|
179
|
+
let out = "";
|
|
180
|
+
for (;;) {
|
|
181
|
+
if (_inThink) {
|
|
182
|
+
const close = s.indexOf("</think>");
|
|
183
|
+
if (close === -1) break; // still inside thinking — discard s (tail kept below)
|
|
184
|
+
s = s.slice(close + 8);
|
|
185
|
+
_inThink = false;
|
|
186
|
+
} else {
|
|
187
|
+
const open = s.indexOf("<think>");
|
|
188
|
+
const close = s.indexOf("</think>");
|
|
189
|
+
if (close !== -1 && (open === -1 || close < open)) {
|
|
190
|
+
out += s.slice(0, close); // orphan closer: drop the tag only
|
|
191
|
+
s = s.slice(close + 8);
|
|
192
|
+
} else if (open !== -1) {
|
|
193
|
+
out += s.slice(0, open);
|
|
194
|
+
s = s.slice(open + 7);
|
|
195
|
+
_inThink = true;
|
|
196
|
+
} else {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const keep = _partialTagLen(s);
|
|
202
|
+
if (keep > 0) _tagTail = s.slice(-keep);
|
|
203
|
+
if (!_inThink && s.length > keep) out += s.slice(0, s.length - keep);
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
|
|
161
207
|
const closeTextBlock = function* () {
|
|
208
|
+
// Flush a held-back fragment that turned out not to be a tag.
|
|
209
|
+
if (_tagTail && !_inThink && textIndex !== null) {
|
|
210
|
+
yield _sse("content_block_delta", {
|
|
211
|
+
type: "content_block_delta",
|
|
212
|
+
index: textIndex,
|
|
213
|
+
delta: { type: "text_delta", text: _tagTail },
|
|
214
|
+
});
|
|
215
|
+
_tagTail = "";
|
|
216
|
+
}
|
|
162
217
|
if (textIndex === null) return;
|
|
163
218
|
yield _sse("content_block_stop", { type: "content_block_stop", index: textIndex });
|
|
164
219
|
textIndex = null;
|
|
@@ -217,19 +272,22 @@ async function* _openaiToAnthropicEvents(upstream, opts = {}) {
|
|
|
217
272
|
// Text deltas are straightforward: open a block on first text, then
|
|
218
273
|
// emit a text_delta per chunk.
|
|
219
274
|
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
275
|
+
const visible = filterThink(delta.content);
|
|
276
|
+
if (visible.length > 0) {
|
|
277
|
+
if (textIndex === null) {
|
|
278
|
+
textIndex = nextIndex++;
|
|
279
|
+
yield _sse("content_block_start", {
|
|
280
|
+
type: "content_block_start",
|
|
281
|
+
index: textIndex,
|
|
282
|
+
content_block: { type: "text", text: "" },
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
yield _sse("content_block_delta", {
|
|
286
|
+
type: "content_block_delta",
|
|
224
287
|
index: textIndex,
|
|
225
|
-
|
|
288
|
+
delta: { type: "text_delta", text: visible },
|
|
226
289
|
});
|
|
227
290
|
}
|
|
228
|
-
yield _sse("content_block_delta", {
|
|
229
|
-
type: "content_block_delta",
|
|
230
|
-
index: textIndex,
|
|
231
|
-
delta: { type: "text_delta", text: delta.content },
|
|
232
|
-
});
|
|
233
291
|
}
|
|
234
292
|
|
|
235
293
|
// Tool-call fragments: accumulate per OpenAI tool index. Fragments of
|
|
@@ -19,6 +19,8 @@ const path = require('path');
|
|
|
19
19
|
|
|
20
20
|
const DEFAULT_DAYS = 7;
|
|
21
21
|
const MIN_SAMPLES = 100;
|
|
22
|
+
/** Narrower than this and a calibrated band is treated as collapse, not signal. */
|
|
23
|
+
const MIN_TIER_WIDTH = 10;
|
|
22
24
|
|
|
23
25
|
/** Quality score below which a complexity bucket is "underperforming" for its tier. */
|
|
24
26
|
const QUALITY_FLOOR = {
|
|
@@ -188,7 +190,24 @@ function runCalibration({ days = DEFAULT_DAYS, dryRun = false, dbPath, outputPat
|
|
|
188
190
|
const prev = ranges[tierOrder[i - 1]];
|
|
189
191
|
const cur = ranges[tierOrder[i]];
|
|
190
192
|
if (cur[0] !== prev[1] + 1) cur[0] = prev[1] + 1;
|
|
191
|
-
if (cur[0] > cur[1]) cur[1] = cur[0]; // collapsed;
|
|
193
|
+
if (cur[0] > cur[1]) cur[1] = cur[0]; // collapsed; see guardrail below
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Guardrail: a tier squeezed under MIN_TIER_WIDTH means calibration had
|
|
197
|
+
// degenerate signal, not a real finding — e.g. 2026-07-26 (MEDIUM →
|
|
198
|
+
// [20,20]) and 2026-08-22 (COMPLEX → [51,54], which re-stitched
|
|
199
|
+
// REASONING's floor down to 55 and sent ~all agentic traffic to the most
|
|
200
|
+
// expensive tier; skewed telemetry then re-taught the same bands every
|
|
201
|
+
// 24h). Collapsed bands revert the whole set to DEFAULT_RANGES — bands
|
|
202
|
+
// are interdependent after stitching, so partial reverts would leave
|
|
203
|
+
// gaps.
|
|
204
|
+
const collapsed = tierOrder.filter(
|
|
205
|
+
(t) => ranges[t][1] - ranges[t][0] < MIN_TIER_WIDTH
|
|
206
|
+
);
|
|
207
|
+
let guardrail = null;
|
|
208
|
+
if (collapsed.length > 0) {
|
|
209
|
+
guardrail = { reverted: true, reason: 'band_collapse', collapsed };
|
|
210
|
+
for (const t of tierOrder) ranges[t] = [...DEFAULT_RANGES[t]];
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
const out = {
|
|
@@ -197,6 +216,7 @@ function runCalibration({ days = DEFAULT_DAYS, dryRun = false, dbPath, outputPat
|
|
|
197
216
|
sampleCount: rows.length,
|
|
198
217
|
ranges,
|
|
199
218
|
stats,
|
|
219
|
+
...(guardrail ? { guardrail } : {}),
|
|
200
220
|
};
|
|
201
221
|
|
|
202
222
|
if (dryRun) return { ...out, dryRun: true };
|
|
@@ -114,6 +114,32 @@ const PROFILES = {
|
|
|
114
114
|
minToolFingerprintMatch: 0.5,
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
|
+
'opencode': {
|
|
118
|
+
name: 'opencode',
|
|
119
|
+
baselineTools: new Set([
|
|
120
|
+
// OpenCode 1.18.x default loadout — captured live 2026-08-22 from
|
|
121
|
+
// Lynkr logs (user-agent `opencode/1.18.21 ai-sdk/provider-utils/…`).
|
|
122
|
+
// All of these attach to every request; counting them as intent
|
|
123
|
+
// scored every OpenCode turn ~80 → REASONING band → session pin
|
|
124
|
+
// locked the whole session there (2026-08-22: 1163/1203 REASONING
|
|
125
|
+
// routings were pin-inherited from a floored first turn).
|
|
126
|
+
'bash', 'edit', 'glob', 'grep', 'question', 'read', 'skill', 'task',
|
|
127
|
+
'todowrite', 'webfetch', 'write',
|
|
128
|
+
// Per opencode.ai/docs/tools — enabled-by-default or common-config
|
|
129
|
+
// names not seen in our capture; harmless if absent (subtraction
|
|
130
|
+
// only removes names actually present).
|
|
131
|
+
'list', 'todoread', 'websearch',
|
|
132
|
+
]),
|
|
133
|
+
detect: {
|
|
134
|
+
headerPatterns: [/opencode/i],
|
|
135
|
+
// 0.5 (like goose): `question`/`skill`/`todo*` vary with user config,
|
|
136
|
+
// so a minimal loadout matches ~8 of 14 baseline names. The lowercase
|
|
137
|
+
// set is disjoint from claude-code's capitalized and cursor's
|
|
138
|
+
// snake_case names, and opencode always sends its user-agent, so the
|
|
139
|
+
// header pattern is the primary detector anyway.
|
|
140
|
+
minToolFingerprintMatch: 0.5,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
117
143
|
'openai-codex': {
|
|
118
144
|
name: 'openai-codex',
|
|
119
145
|
baselineTools: new Set([
|
|
@@ -44,7 +44,7 @@ function _effectiveTools(payload) {
|
|
|
44
44
|
// Pre-compiled regex patterns for performance
|
|
45
45
|
const PATTERNS = {
|
|
46
46
|
// Greetings - always local
|
|
47
|
-
greeting: /^(hi|hello|hey|good\s*(morning|afternoon|evening)|howdy|greetings|sup|yo|thanks?|thank\s*you)[\s
|
|
47
|
+
greeting: /^(hi|hello|hey|good\s*(morning|afternoon|evening)|howdy|greetings|sup|yo|thanks?|thank\s*you)[\s.!?,]*$/i,
|
|
48
48
|
|
|
49
49
|
// Simple questions - likely local
|
|
50
50
|
simpleQuestion: /^(what\s+is|what's|define|who\s+is|when\s+was|where\s+is)\s+\w/i,
|
|
@@ -56,7 +56,7 @@ const PATTERNS = {
|
|
|
56
56
|
technical: /\b(code|function|class|method|variable|api|database|server|client|module|import|export|async|await|promise|component|interface|type|struct|enum)\b/i,
|
|
57
57
|
|
|
58
58
|
// File/path references
|
|
59
|
-
fileReference: /\b(\w+\.(js|ts|py|rb|go|rs|java|cpp|c|h|jsx|tsx|vue|svelte|md|json|yaml|yml|toml|sql|sh|bash))\b|[
|
|
59
|
+
fileReference: /\b(\w+\.(js|ts|py|rb|go|rs|java|cpp|c|h|jsx|tsx|vue|svelte|md|json|yaml|yml|toml|sql|sh|bash))\b|[./]\w+\//i,
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// ============================================================================
|
|
@@ -119,10 +119,10 @@ const FORCE_CLOUD_PATTERNS = [
|
|
|
119
119
|
|
|
120
120
|
// Force local patterns - always route to local regardless of score
|
|
121
121
|
const FORCE_LOCAL_PATTERNS = [
|
|
122
|
-
/^(hi|hello|hey|thanks?|thank\s*you|bye|goodbye)[\s
|
|
122
|
+
/^(hi|hello|hey|thanks?|thank\s*you|bye|goodbye)[\s.!?]*$/i,
|
|
123
123
|
/^what\s+(time|day|date)\s+is\s+it/i,
|
|
124
|
-
/^(yes|no|ok|okay|sure|got\s+it|understood)[\s
|
|
125
|
-
/^(help|menu|commands?|options?)[\s
|
|
124
|
+
/^(yes|no|ok|okay|sure|got\s+it|understood)[\s.!?]*$/i,
|
|
125
|
+
/^(help|menu|commands?|options?)[\s.!?]*$/i,
|
|
126
126
|
];
|
|
127
127
|
|
|
128
128
|
// Weighted Scoring (15 Dimensions)
|
|
@@ -478,8 +478,6 @@ function scoreTools(payload) {
|
|
|
478
478
|
* Score based on task type (0-25 points)
|
|
479
479
|
*/
|
|
480
480
|
function scoreTaskType(content) {
|
|
481
|
-
const contentLower = content.toLowerCase();
|
|
482
|
-
|
|
483
481
|
// Check force patterns first (priority: REASONING > cloud > local)
|
|
484
482
|
for (const pattern of FORCE_REASONING_PATTERNS) {
|
|
485
483
|
if (pattern.test(content)) {
|
|
@@ -796,7 +794,6 @@ async function analyzeComplexity(payload, options = {}) {
|
|
|
796
794
|
if (useWeighted) {
|
|
797
795
|
const weighted = calculateWeightedScore(workingPayload, content);
|
|
798
796
|
const threshold = getThreshold();
|
|
799
|
-
const mode = config.smartToolSelection?.mode ?? 'heuristic';
|
|
800
797
|
|
|
801
798
|
// Check force patterns
|
|
802
799
|
const taskTypeResult = scoreTaskType(content);
|
|
@@ -82,8 +82,8 @@ function scoreCode(response) {
|
|
|
82
82
|
const fenced = (text.match(/```[\s\S]*?```/g) || []).join('\n');
|
|
83
83
|
if (!fenced) return 0.6; // Code-gen request without code is suspicious
|
|
84
84
|
// Very basic balance check
|
|
85
|
-
const opens = (fenced.match(/[
|
|
86
|
-
const closes = (fenced.match(/[
|
|
85
|
+
const opens = (fenced.match(/[{[(]/g) || []).length;
|
|
86
|
+
const closes = (fenced.match(/[}\])]/g) || []).length;
|
|
87
87
|
if (Math.abs(opens - closes) > 2) return 0.5;
|
|
88
88
|
return 0.9;
|
|
89
89
|
}
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const logger = require('../logger');
|
|
8
|
-
const config = require('../config');
|
|
9
8
|
const { getModelRegistry, getModelRegistrySync } = require('./model-registry');
|
|
10
|
-
const { getModelTierSelector
|
|
9
|
+
const { getModelTierSelector } = require('./model-tiers');
|
|
11
10
|
const { ratioFor } = require('./output-ratios');
|
|
12
11
|
|
|
13
12
|
// Session cost tracking (in-memory)
|
|
@@ -57,7 +57,7 @@ function _lowerTier(tier) {
|
|
|
57
57
|
* @param {Function} [args.deps.now] - Override Date.now() for tests.
|
|
58
58
|
* @returns {string|null} The lower tier, or null if demotion is not warranted.
|
|
59
59
|
*/
|
|
60
|
-
function suggestDemotion({ tier, requestType, analysis, deps = {} } = {}) {
|
|
60
|
+
function suggestDemotion({ tier, requestType, analysis: _analysis, deps = {} } = {}) {
|
|
61
61
|
if (!tier || !requestType) return null;
|
|
62
62
|
const lower = _lowerTier(tier);
|
|
63
63
|
if (!lower) return null;
|
|
@@ -206,7 +206,6 @@ function classify(embedding, centroids) {
|
|
|
206
206
|
* Lexical fallback path stays clamped ≤75 (see _lexicalCleanScore).
|
|
207
207
|
*/
|
|
208
208
|
function blendScore(sims) {
|
|
209
|
-
const classes = Object.keys(CLASS_VALUES);
|
|
210
209
|
// Frontier similarity floor: a weak topical match can't jump tiers.
|
|
211
210
|
const frontierSim = sims.frontier ?? -1;
|
|
212
211
|
const activeSims = { ...sims };
|
|
@@ -22,7 +22,11 @@ const logger = require('../logger');
|
|
|
22
22
|
const { generateEmbedding } = require('../cache/embeddings');
|
|
23
23
|
const { getEmbeddingCache } = require('./embedding-cache');
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// LYNKR_KNN_DIR override exists so test runs point at a scratch dir instead
|
|
26
|
+
// of the production index — parallel test processes saving to the real path
|
|
27
|
+
// produced a torn read → corrupt link-list lengths → a 135GB zero-filled
|
|
28
|
+
// index file (2026-08-21).
|
|
29
|
+
const INDEX_DIR = process.env.LYNKR_KNN_DIR || path.join(__dirname, '../../data/knn');
|
|
26
30
|
const INDEX_FILE = path.join(INDEX_DIR, 'index.hnsw');
|
|
27
31
|
const META_FILE = path.join(INDEX_DIR, 'meta.json');
|
|
28
32
|
|
|
@@ -85,7 +89,7 @@ class KnnRouter {
|
|
|
85
89
|
// (Earlier Lynkr code passed MAX_ELEMENTS here — wrong type, threw on load.)
|
|
86
90
|
this.index.readIndexSync(INDEX_FILE, false);
|
|
87
91
|
// resize if needed so we can keep adding up to MAX_ELEMENTS
|
|
88
|
-
try { this.index.resizeIndex(MAX_ELEMENTS); } catch (
|
|
92
|
+
try { this.index.resizeIndex(MAX_ELEMENTS); } catch (err) { logger.debug({ err: err.message }, '[KnnRouter] resizeIndex failed — index may not grow past loaded size'); }
|
|
89
93
|
this.ready = true;
|
|
90
94
|
logger.info({ size: this.size, dim: this.dim }, '[KnnRouter] Index loaded');
|
|
91
95
|
return true;
|
|
@@ -99,8 +103,26 @@ class KnnRouter {
|
|
|
99
103
|
if (!this.ready || !this.index) return;
|
|
100
104
|
try {
|
|
101
105
|
fs.mkdirSync(INDEX_DIR, { recursive: true });
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
// Atomic save: hnswlib's writeIndexSync is truncate-then-write, so a
|
|
107
|
+
// concurrent reader can observe a torn image and later persist a
|
|
108
|
+
// corrupt index. Write to a per-pid temp file and rename into place —
|
|
109
|
+
// readers then always see a complete image.
|
|
110
|
+
const tmpIndex = `${INDEX_FILE}.tmp-${process.pid}`;
|
|
111
|
+
const tmpMeta = `${META_FILE}.tmp-${process.pid}`;
|
|
112
|
+
this.index.writeIndexSync(tmpIndex);
|
|
113
|
+
// Sanity bound: a healthy index is ~(elements × ~3.3KB) + slack. A
|
|
114
|
+
// corrupted link-list length manifests as a wildly oversized file —
|
|
115
|
+
// refuse to install it over the good one.
|
|
116
|
+
const expectedMax = (this.size + 1000) * 8192 + 1_000_000;
|
|
117
|
+
const written = fs.statSync(tmpIndex).size;
|
|
118
|
+
if (written > expectedMax) {
|
|
119
|
+
fs.rmSync(tmpIndex, { force: true });
|
|
120
|
+
logger.error({ written, expectedMax, size: this.size }, '[KnnRouter] Refusing to save implausibly large index — in-memory index likely corrupt');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(tmpMeta, JSON.stringify({ dim: this.dim, entries: this.meta }, null, 0));
|
|
124
|
+
fs.renameSync(tmpIndex, INDEX_FILE);
|
|
125
|
+
fs.renameSync(tmpMeta, META_FILE);
|
|
104
126
|
} catch (err) {
|
|
105
127
|
logger.warn({ err: err.message }, '[KnnRouter] Index save failed');
|
|
106
128
|
}
|
|
@@ -197,7 +219,7 @@ class KnnRouter {
|
|
|
197
219
|
|
|
198
220
|
let best = null;
|
|
199
221
|
let bestScore = -Infinity;
|
|
200
|
-
for (const [
|
|
222
|
+
for (const [, agg] of byModel) {
|
|
201
223
|
const avgQ = agg.quality / agg.weight;
|
|
202
224
|
const avgC = agg.cost / agg.weight;
|
|
203
225
|
// Score = quality / log(cost+1) — reward quality, penalise cost gently
|
|
@@ -95,7 +95,7 @@ class LatencyTracker {
|
|
|
95
95
|
}
|
|
96
96
|
// Aggregate across all models for this provider
|
|
97
97
|
const provEntries = [];
|
|
98
|
-
for (const [
|
|
98
|
+
for (const [_k, entry] of this._entries) {
|
|
99
99
|
if (entry.provider === provider) provEntries.push(entry);
|
|
100
100
|
}
|
|
101
101
|
if (provEntries.length === 0) return null;
|
|
@@ -68,7 +68,7 @@ async function estimate(args) {
|
|
|
68
68
|
fs.mkdirSync(path.dirname(ALERTS_PATH), { recursive: true });
|
|
69
69
|
let existing = [];
|
|
70
70
|
if (fs.existsSync(ALERTS_PATH)) {
|
|
71
|
-
try { existing = JSON.parse(fs.readFileSync(ALERTS_PATH, 'utf8')); } catch {}
|
|
71
|
+
try { existing = JSON.parse(fs.readFileSync(ALERTS_PATH, 'utf8')); } catch (err) { logger.debug({ err: err.message }, '[RegretEstimator] Existing alerts file unreadable — starting fresh'); }
|
|
72
72
|
}
|
|
73
73
|
const out = Array.isArray(existing) ? existing : [];
|
|
74
74
|
out.push(...alerts);
|
package/src/stores/file-store.js
CHANGED
|
@@ -89,7 +89,11 @@ async function getFileContent(id) {
|
|
|
89
89
|
async function deleteFile(id) {
|
|
90
90
|
const entry = metadata.get(id);
|
|
91
91
|
if (!entry) return false;
|
|
92
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
await fsp.unlink(entry.storage_path);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.debug({ err: err.message, fileId: id }, "Best-effort unlink failed during file delete");
|
|
96
|
+
}
|
|
93
97
|
metadata.delete(id);
|
|
94
98
|
persistMetadata();
|
|
95
99
|
return true;
|
|
@@ -38,7 +38,7 @@ const telemetry = require("../routing/telemetry");
|
|
|
38
38
|
const ANONYMIZE_PATTERNS = [
|
|
39
39
|
// API keys and bearer tokens
|
|
40
40
|
[/sk-[A-Za-z0-9_-]{20,}/g, "<API_KEY>"],
|
|
41
|
-
[/Bearer\s+[A-Za-z0-9._
|
|
41
|
+
[/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer <REDACTED>"],
|
|
42
42
|
[/dapi_[A-Za-z0-9_-]+/g, "<DATABRICKS_KEY>"],
|
|
43
43
|
[/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "<JWT>"],
|
|
44
44
|
// AWS keys
|
package/src/workers/pool.js
CHANGED
|
@@ -310,7 +310,7 @@ class WorkerPool {
|
|
|
310
310
|
logger.info('[WorkerPool] Shutting down...');
|
|
311
311
|
|
|
312
312
|
// Reject all pending tasks
|
|
313
|
-
for (const [
|
|
313
|
+
for (const [_taskId, pending] of this.pendingTasks) {
|
|
314
314
|
clearTimeout(pending.timeout);
|
|
315
315
|
pending.reject(new Error('Worker pool shutting down'));
|
|
316
316
|
}
|
package/src/workers/worker.js
CHANGED
|
@@ -65,7 +65,7 @@ const handlers = {
|
|
|
65
65
|
let tokensReduced = 0;
|
|
66
66
|
const seenToolResults = new Map(); // For deduplication
|
|
67
67
|
|
|
68
|
-
const transformed = messages.map((msg
|
|
68
|
+
const transformed = messages.map((msg) => {
|
|
69
69
|
if (!msg) return msg;
|
|
70
70
|
|
|
71
71
|
// Clone to avoid mutating original
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Convert routellm/gpt4_dataset (train.jsonl) → Lynkr kNN bootstrap JSONL.
|
|
4
|
-
|
|
5
|
-
Stream-downloads from HuggingFace and maps mixtral_score (1-5) to your
|
|
6
|
-
TIER_* model config so the kNN index learns to recommend YOUR real models,
|
|
7
|
-
not RouteLLM's pool (mixtral / gpt-4).
|
|
8
|
-
|
|
9
|
-
Output format matches scripts/build-knn-index.js bootstrap input:
|
|
10
|
-
{"query": "...", "provider": "ollama", "model": "minimax-m2.5:cloud",
|
|
11
|
-
"quality": 95, "cost": 0.001, "latency": 200, "tier": "SIMPLE"}
|
|
12
|
-
|
|
13
|
-
Usage:
|
|
14
|
-
.venv/bin/python scripts/convert-routellm.py [--limit 30000] -o routellm.jsonl
|
|
15
|
-
"""
|
|
16
|
-
import argparse
|
|
17
|
-
import json
|
|
18
|
-
import sys
|
|
19
|
-
import urllib.request
|
|
20
|
-
import ssl
|
|
21
|
-
|
|
22
|
-
# Map mixtral_score → user's tier config.
|
|
23
|
-
# Spread is intentionally aggressive: only score=5 (Mixtral truly nailed it)
|
|
24
|
-
# stays SIMPLE; score=4 escalates to MEDIUM so the kNN index has enough MEDIUM
|
|
25
|
-
# examples to actually recommend non-SIMPLE tiers (vs the previous mapping
|
|
26
|
-
# where 86% of records were SIMPLE and kNN never disagreed with the heuristic).
|
|
27
|
-
SCORE_TO_ROUTE = {
|
|
28
|
-
5: {"tier": "SIMPLE", "provider": "ollama", "model": "minimax-m2.5:cloud", "quality": 95, "cost": 0.001, "latency": 200},
|
|
29
|
-
4: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 80, "cost": 0.003, "latency": 1500},
|
|
30
|
-
3: {"tier": "MEDIUM", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 70, "cost": 0.003, "latency": 1500},
|
|
31
|
-
2: {"tier": "COMPLEX", "provider": "azure-anthropic", "model": "claude-sonnet-4-6", "quality": 55, "cost": 0.003, "latency": 1500},
|
|
32
|
-
1: {"tier": "REASONING", "provider": "azure-anthropic", "model": "claude-opus-4-8", "quality": 40, "cost": 0.015, "latency": 3500},
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
URL = "https://huggingface.co/datasets/routellm/gpt4_dataset/resolve/main/train.jsonl"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def main():
|
|
39
|
-
p = argparse.ArgumentParser()
|
|
40
|
-
p.add_argument("--output", "-o", default="data/routellm-bootstrap.jsonl")
|
|
41
|
-
p.add_argument("--limit", type=int, default=None,
|
|
42
|
-
help="Only convert first N rows")
|
|
43
|
-
args = p.parse_args()
|
|
44
|
-
|
|
45
|
-
print(f"Streaming {URL} …", file=sys.stderr)
|
|
46
|
-
# Some macOS Python builds have SSL issues with huggingface.co — fall back
|
|
47
|
-
# to unverified context if needed. The data is public, no auth.
|
|
48
|
-
ctx = ssl.create_default_context()
|
|
49
|
-
try:
|
|
50
|
-
with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
|
|
51
|
-
_stream(resp, args.output, args.limit)
|
|
52
|
-
except (ssl.SSLError, OSError) as e:
|
|
53
|
-
print(f"SSL strict failed ({e}); retrying with unverified context", file=sys.stderr)
|
|
54
|
-
ctx = ssl._create_unverified_context()
|
|
55
|
-
with urllib.request.urlopen(URL, context=ctx, timeout=60) as resp:
|
|
56
|
-
_stream(resp, args.output, args.limit)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _stream(resp, out_path, limit):
|
|
60
|
-
written = 0
|
|
61
|
-
by_score = {s: 0 for s in SCORE_TO_ROUTE}
|
|
62
|
-
skipped = 0
|
|
63
|
-
with open(out_path, "w") as out:
|
|
64
|
-
for line in resp:
|
|
65
|
-
if limit and written >= limit:
|
|
66
|
-
break
|
|
67
|
-
try:
|
|
68
|
-
row = json.loads(line)
|
|
69
|
-
except Exception:
|
|
70
|
-
skipped += 1
|
|
71
|
-
continue
|
|
72
|
-
score = row.get("mixtral_score")
|
|
73
|
-
if score not in SCORE_TO_ROUTE:
|
|
74
|
-
skipped += 1
|
|
75
|
-
continue
|
|
76
|
-
route = SCORE_TO_ROUTE[score]
|
|
77
|
-
rec = {
|
|
78
|
-
"query": row.get("prompt", ""),
|
|
79
|
-
"provider": route["provider"],
|
|
80
|
-
"model": route["model"],
|
|
81
|
-
"quality": route["quality"],
|
|
82
|
-
"cost": route["cost"],
|
|
83
|
-
"latency": route["latency"],
|
|
84
|
-
"tier": route["tier"],
|
|
85
|
-
}
|
|
86
|
-
if not rec["query"]:
|
|
87
|
-
skipped += 1
|
|
88
|
-
continue
|
|
89
|
-
out.write(json.dumps(rec) + "\n")
|
|
90
|
-
written += 1
|
|
91
|
-
by_score[score] += 1
|
|
92
|
-
if written % 5000 == 0:
|
|
93
|
-
print(f" {written} written…", file=sys.stderr)
|
|
94
|
-
|
|
95
|
-
print(f"\nWrote {written} records to {out_path} ({skipped} skipped)", file=sys.stderr)
|
|
96
|
-
print(f"By mixtral_score: {by_score}", file=sys.stderr)
|
|
97
|
-
print(f"\nTier distribution:", file=sys.stderr)
|
|
98
|
-
for score, route in SCORE_TO_ROUTE.items():
|
|
99
|
-
n = by_score[score]
|
|
100
|
-
if n:
|
|
101
|
-
print(f" {route['tier']:>10} {route['provider']}:{route['model']:30s} n={n}", file=sys.stderr)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if __name__ == "__main__":
|
|
105
|
-
main()
|
package/src/diff/comments.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
const crypto = require("crypto");
|
|
2
|
-
const db = require("../db");
|
|
3
|
-
const logger = require("../logger");
|
|
4
|
-
|
|
5
|
-
const insertCommentStmt = db.prepare(
|
|
6
|
-
`INSERT INTO diff_comments (
|
|
7
|
-
thread_id,
|
|
8
|
-
session_id,
|
|
9
|
-
file_path,
|
|
10
|
-
hunk,
|
|
11
|
-
line,
|
|
12
|
-
comment,
|
|
13
|
-
author,
|
|
14
|
-
created_at,
|
|
15
|
-
metadata
|
|
16
|
-
) VALUES (
|
|
17
|
-
@thread_id,
|
|
18
|
-
@session_id,
|
|
19
|
-
@file_path,
|
|
20
|
-
@hunk,
|
|
21
|
-
@line,
|
|
22
|
-
@comment,
|
|
23
|
-
@author,
|
|
24
|
-
@created_at,
|
|
25
|
-
@metadata
|
|
26
|
-
)`,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const deleteCommentStmt = db.prepare("DELETE FROM diff_comments WHERE id = ?");
|
|
30
|
-
|
|
31
|
-
function buildSelectCommentsQuery({ filePath, threadId }) {
|
|
32
|
-
let sql = `SELECT id,
|
|
33
|
-
thread_id,
|
|
34
|
-
session_id,
|
|
35
|
-
file_path,
|
|
36
|
-
hunk,
|
|
37
|
-
line,
|
|
38
|
-
comment,
|
|
39
|
-
author,
|
|
40
|
-
created_at,
|
|
41
|
-
metadata
|
|
42
|
-
FROM diff_comments`;
|
|
43
|
-
const conditions = [];
|
|
44
|
-
const params = [];
|
|
45
|
-
if (filePath) {
|
|
46
|
-
conditions.push("file_path = ?");
|
|
47
|
-
params.push(filePath);
|
|
48
|
-
}
|
|
49
|
-
if (threadId) {
|
|
50
|
-
conditions.push("thread_id = ?");
|
|
51
|
-
params.push(threadId);
|
|
52
|
-
}
|
|
53
|
-
if (conditions.length) {
|
|
54
|
-
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
55
|
-
}
|
|
56
|
-
sql += " ORDER BY created_at ASC, id ASC";
|
|
57
|
-
return { sql, params };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normaliseCommentRow(row) {
|
|
61
|
-
return {
|
|
62
|
-
id: row.id,
|
|
63
|
-
threadId: row.thread_id ?? null,
|
|
64
|
-
sessionId: row.session_id ?? null,
|
|
65
|
-
filePath: row.file_path,
|
|
66
|
-
hunk: row.hunk ?? null,
|
|
67
|
-
line: typeof row.line === "number" ? row.line : null,
|
|
68
|
-
comment: row.comment,
|
|
69
|
-
author: row.author ?? null,
|
|
70
|
-
createdAt: row.created_at,
|
|
71
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function addDiffComment({
|
|
76
|
-
threadId,
|
|
77
|
-
sessionId,
|
|
78
|
-
filePath,
|
|
79
|
-
line,
|
|
80
|
-
hunk,
|
|
81
|
-
comment,
|
|
82
|
-
author,
|
|
83
|
-
metadata,
|
|
84
|
-
}) {
|
|
85
|
-
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
86
|
-
throw new Error("Diff comment requires a file path.");
|
|
87
|
-
}
|
|
88
|
-
if (typeof comment !== "string" || comment.trim().length === 0) {
|
|
89
|
-
throw new Error("Diff comment requires non-empty comment text.");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const createdAt = Date.now();
|
|
93
|
-
const thread = threadId ?? crypto.randomUUID();
|
|
94
|
-
|
|
95
|
-
const params = {
|
|
96
|
-
thread_id: thread,
|
|
97
|
-
session_id: sessionId ?? null,
|
|
98
|
-
file_path: filePath,
|
|
99
|
-
hunk: hunk ?? null,
|
|
100
|
-
line: typeof line === "number" ? line : null,
|
|
101
|
-
comment,
|
|
102
|
-
author: author ?? null,
|
|
103
|
-
created_at: createdAt,
|
|
104
|
-
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const result = insertCommentStmt.run(params);
|
|
108
|
-
|
|
109
|
-
logger.debug(
|
|
110
|
-
{
|
|
111
|
-
id: result.lastInsertRowid,
|
|
112
|
-
thread,
|
|
113
|
-
filePath,
|
|
114
|
-
line,
|
|
115
|
-
},
|
|
116
|
-
"Recorded diff comment",
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
return normaliseCommentRow({
|
|
120
|
-
id: result.lastInsertRowid,
|
|
121
|
-
thread_id: thread,
|
|
122
|
-
session_id: sessionId ?? null,
|
|
123
|
-
file_path: filePath,
|
|
124
|
-
hunk: hunk ?? null,
|
|
125
|
-
line: typeof line === "number" ? line : null,
|
|
126
|
-
comment,
|
|
127
|
-
author: author ?? null,
|
|
128
|
-
created_at: createdAt,
|
|
129
|
-
metadata: metadata ? JSON.stringify(metadata) : null,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function listDiffComments({ filePath, threadId } = {}) {
|
|
134
|
-
const { sql, params } = buildSelectCommentsQuery({ filePath, threadId });
|
|
135
|
-
return db
|
|
136
|
-
.prepare(sql)
|
|
137
|
-
.all(...params)
|
|
138
|
-
.map(normaliseCommentRow);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function deleteDiffComment({ id }) {
|
|
142
|
-
if (!id) {
|
|
143
|
-
throw new Error("diff_comment_delete requires an id.");
|
|
144
|
-
}
|
|
145
|
-
const result = deleteCommentStmt.run(id);
|
|
146
|
-
return result.changes > 0;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
module.exports = {
|
|
150
|
-
addDiffComment,
|
|
151
|
-
listDiffComments,
|
|
152
|
-
deleteDiffComment,
|
|
153
|
-
};
|