lynkr 9.14.11 → 9.14.13
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 +1 -0
- package/bin/cli.js +1 -0
- package/bin/lynkr-connect-orcarouter.js +246 -0
- package/bin/lynkr-init.js +15 -1
- package/config/difficulty-anchors.json +12 -2
- package/config/model-capability-seeds.json +5 -0
- package/package.json +3 -3
- package/scripts/validate-intent-anchors.js +2 -2
- package/src/api/openai-router.js +22 -4
- package/src/api/providers-handler.js +33 -10
- package/src/api/router.js +370 -16
- package/src/clients/databricks.js +196 -22
- package/src/clients/openai-format.js +37 -2
- package/src/clients/openrouter-utils.js +20 -0
- package/src/clients/orcarouter-catalog.js +330 -0
- package/src/clients/orcarouter-credentials.js +428 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/config/index.js +63 -2
- package/src/dashboard/api.js +1 -0
- package/src/orchestrator/index.js +49 -10
- package/src/orchestrator/sse-transformer.js +3 -0
- package/src/prompts/system.js +41 -49
- package/src/routing/affinity-store.js +47 -21
- package/src/routing/cache-economics.js +1 -0
- package/src/routing/cache-switch-cost.js +46 -2
- package/src/routing/complexity-analyzer.js +21 -4
- package/src/routing/index.js +123 -1
- package/src/routing/intent-score.js +48 -13
- package/src/routing/jev-router.js +272 -0
- package/src/routing/model-tiers.js +15 -0
- package/src/routing/passthrough-route.js +286 -0
- package/src/routing/quota-ledger.js +110 -0
- package/src/routing/risk-analyzer.js +17 -1
- package/src/routing/telemetry.js +47 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription quota ledger (Phase 4 — quota-aware descents).
|
|
3
|
+
*
|
|
4
|
+
* Flat-fee subscriptions have no dollar price per token, but they do have a
|
|
5
|
+
* quota bucket: sustained Opus serving drains weekly runway several times
|
|
6
|
+
* faster than Haiku. This module tracks per-session burn in QUOTA UNITS so
|
|
7
|
+
* the downgrade gate can shorten its horizon under pressure (see
|
|
8
|
+
* cache-switch-cost.js `sessionBurnPressure`): high burn → fewer effective
|
|
9
|
+
* remaining turns → holds collapse → descents clear sooner.
|
|
10
|
+
*
|
|
11
|
+
* Units: 1 quota unit = 1000 Haiku-class tokens (input+output blended).
|
|
12
|
+
* Model weight = (inputPerM + outputPerM) / (haiku input+output), resolved
|
|
13
|
+
* via cache-economics with a fail-to-middle default (2.0) for unpriced ids —
|
|
14
|
+
* unknown models should exert middling pressure, never zero, never Opus.
|
|
15
|
+
* Today's registry: haiku 1.0, sonnet-5 2.0, opus-4-8 5.0.
|
|
16
|
+
*
|
|
17
|
+
* Pressure = min(1, windowBurn / BURN_WINDOW_QUOTA) over the trailing window
|
|
18
|
+
* (default 10 turns). BURN_WINDOW_QUOTA is a heuristic constant: ~ten sustained
|
|
19
|
+
* full-context Opus turns. It is deliberately coarse — pressure only shortens
|
|
20
|
+
* the hold horizon, it never forces a descent past the gate's own math, and
|
|
21
|
+
* upgrades/tool-loops/compaction-floors bypass the gate entirely.
|
|
22
|
+
*
|
|
23
|
+
* Pure except for the in-memory session map (no I/O, no DB). Never throws.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const BURN_WINDOW_TURNS = 10;
|
|
27
|
+
// ~10 sustained 10k-token Opus turns (10 × 10 × 5.0). Sustained peaks pin
|
|
28
|
+
// pressure at 1; occasional upgrades barely register. Heuristic — see above.
|
|
29
|
+
const BURN_WINDOW_QUOTA = 500;
|
|
30
|
+
const MAX_SESSIONS = 500;
|
|
31
|
+
const UNKNOWN_MODEL_WEIGHT = 2.0;
|
|
32
|
+
const HAIKU_BASELINE_PER_M = 6; // claude-haiku-4-5-20251001: 1 input + 5 output
|
|
33
|
+
|
|
34
|
+
const _windows = new Map(); // sessionId -> number[] (recent turn burns)
|
|
35
|
+
|
|
36
|
+
function _evictIfNeeded() {
|
|
37
|
+
while (_windows.size > MAX_SESSIONS) {
|
|
38
|
+
const oldest = _windows.keys().next();
|
|
39
|
+
if (oldest.done) break;
|
|
40
|
+
_windows.delete(oldest.value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Quota weight for a model id: output+input price relative to Haiku baseline.
|
|
46
|
+
* @param {string|null} model
|
|
47
|
+
* @param {function|null} [resolveFn] - (provider, model) => {inputPerM, outputPerM} (tests)
|
|
48
|
+
*/
|
|
49
|
+
function modelWeight(model, resolveFn = null) {
|
|
50
|
+
try {
|
|
51
|
+
const resolve = resolveFn || require('./cache-economics').resolveCacheEconomics;
|
|
52
|
+
const econ = resolve('azure-anthropic', model);
|
|
53
|
+
if (!econ || typeof econ.inputPerM !== 'number' || typeof econ.outputPerM !== 'number') {
|
|
54
|
+
return UNKNOWN_MODEL_WEIGHT;
|
|
55
|
+
}
|
|
56
|
+
if (econ.unknownPricing) return UNKNOWN_MODEL_WEIGHT;
|
|
57
|
+
return (econ.inputPerM + econ.outputPerM) / HAIKU_BASELINE_PER_M;
|
|
58
|
+
} catch {
|
|
59
|
+
return UNKNOWN_MODEL_WEIGHT;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record one served turn.
|
|
65
|
+
* @param {string|null} sessionId
|
|
66
|
+
* @param {object} usage - {model, inputTokens, outputTokens}
|
|
67
|
+
* @param {function|null} [resolveFn] - test injection for modelWeight
|
|
68
|
+
*/
|
|
69
|
+
function record(sessionId, usage = {}, resolveFn = null) {
|
|
70
|
+
if (!sessionId) return;
|
|
71
|
+
const input = Number(usage.inputTokens) || 0;
|
|
72
|
+
const output = Number(usage.outputTokens) || 0;
|
|
73
|
+
if (input <= 0 && output <= 0) return;
|
|
74
|
+
const burn = ((input + output) / 1000) * modelWeight(usage.model, resolveFn);
|
|
75
|
+
let window = _windows.get(sessionId);
|
|
76
|
+
if (!window) {
|
|
77
|
+
window = [];
|
|
78
|
+
_windows.set(sessionId, window);
|
|
79
|
+
}
|
|
80
|
+
window.push(burn);
|
|
81
|
+
while (window.length > BURN_WINDOW_TURNS) window.shift();
|
|
82
|
+
_evictIfNeeded();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Current burn pressure for a session: 0 (idle/cheap) → 1 (sustained peaks).
|
|
87
|
+
*/
|
|
88
|
+
function pressure(sessionId) {
|
|
89
|
+
const window = _windows.get(sessionId);
|
|
90
|
+
if (!window || window.length === 0) return 0;
|
|
91
|
+
const burn = window.reduce((a, b) => a + b, 0);
|
|
92
|
+
return Math.max(0, Math.min(1, burn / BURN_WINDOW_QUOTA));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Test/ops helpers. */
|
|
96
|
+
function _reset() { _windows.clear(); }
|
|
97
|
+
function _windowBurn(sessionId) {
|
|
98
|
+
const window = _windows.get(sessionId);
|
|
99
|
+
return window ? window.reduce((a, b) => a + b, 0) : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
record,
|
|
104
|
+
pressure,
|
|
105
|
+
modelWeight,
|
|
106
|
+
BURN_WINDOW_TURNS,
|
|
107
|
+
BURN_WINDOW_QUOTA,
|
|
108
|
+
_reset,
|
|
109
|
+
_windowBurn,
|
|
110
|
+
};
|
|
@@ -125,9 +125,22 @@ function findHits(keywords, haystack) {
|
|
|
125
125
|
* not what the harness injected; genuinely risky asks live in the typed
|
|
126
126
|
* text and still fire. (The intent scorers have stripped these blocks
|
|
127
127
|
* since WS3 — this brings the risk scan in line.)
|
|
128
|
+
*
|
|
129
|
+
* Bare-line MCP boilerplate (HARNESS_BOILERPLATE_LINE) gets the same
|
|
130
|
+
* treatment: auth notices also arrive as PLAIN text outside any tags.
|
|
128
131
|
* @param {string} text
|
|
129
132
|
* @returns {string}
|
|
130
133
|
*/
|
|
134
|
+
// Bare-line harness boilerplate that escapes the tag stripping above: MCP
|
|
135
|
+
// auth notices also arrive as PLAIN text (tool results, echoed banners),
|
|
136
|
+
// where the same credential-flavored words detonate the keyword scan —
|
|
137
|
+
// live 2026-09-18: "Can you pull sprint stories…" + an "Authentication
|
|
138
|
+
// successful. Connected to claude.ai BT1_MCP" notice routed REASONING on
|
|
139
|
+
// instructionHits ["authentication"] the user never typed. Full-LINE match
|
|
140
|
+
// only: these are fixed harness templates, never user prose — typed risky
|
|
141
|
+
// asks ("disable the authentication check") span the line differently and
|
|
142
|
+
// keep firing. Tool_use inputs are untouched (real activity still counts).
|
|
143
|
+
const HARNESS_BOILERPLATE_LINE = /^\s*(authentication successful[.!]?(\s+connected to .+)?|connected to \S+ MCP\.?|(\d+\s+)?MCP servers? need authentication\b.*|run \/mcp\b.*)\s*$/i;
|
|
131
144
|
function stripSystemReminders(text) {
|
|
132
145
|
if (typeof text !== 'string' || !text) return '';
|
|
133
146
|
return text
|
|
@@ -139,7 +152,10 @@ function stripSystemReminders(text) {
|
|
|
139
152
|
// "Hi", force-escalated to COMPLEX on it) and <user_instructions>
|
|
140
153
|
// carries AGENTS.md contents the user never typed this turn.
|
|
141
154
|
.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, ' ')
|
|
142
|
-
.replace(/<user_instructions>[\s\S]*?<\/user_instructions>/g, ' ')
|
|
155
|
+
.replace(/<user_instructions>[\s\S]*?<\/user_instructions>/g, ' ')
|
|
156
|
+
.split('\n')
|
|
157
|
+
.filter((line) => !HARNESS_BOILERPLATE_LINE.test(line))
|
|
158
|
+
.join('\n');
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
/**
|
package/src/routing/telemetry.js
CHANGED
|
@@ -181,6 +181,14 @@ function init() {
|
|
|
181
181
|
// NULL on rows where the bandit didn't run (deterministic decisions)
|
|
182
182
|
// or recorded before this column existed.
|
|
183
183
|
["context", "TEXT"],
|
|
184
|
+
// Jev routing judge — shadow verdict recorded alongside every scored
|
|
185
|
+
// decision so heuristic/Jev eras stay separable. NULL when Jev didn't
|
|
186
|
+
// run (pinned serves, side frames, no key) or predates this column.
|
|
187
|
+
["jev_verdict", "TEXT"],
|
|
188
|
+
["jev_confidence", "REAL"],
|
|
189
|
+
["jev_probabilities", "TEXT"],
|
|
190
|
+
["jev_model", "TEXT"],
|
|
191
|
+
["criteria_hash", "TEXT"],
|
|
184
192
|
];
|
|
185
193
|
for (const [col, type] of additiveCols) {
|
|
186
194
|
if (!existingCols.has(col)) {
|
|
@@ -243,7 +251,8 @@ function record(data) {
|
|
|
243
251
|
retry_count, circuit_breaker_state, quality_score, tokens_per_second,
|
|
244
252
|
cost_efficiency, request_text, response_text,
|
|
245
253
|
base_tier, escalation_source, propensity, candidates, pinned, switch_reason,
|
|
246
|
-
cache_decision, cache_read_tokens, cache_creation_tokens, context
|
|
254
|
+
cache_decision, cache_read_tokens, cache_creation_tokens, context,
|
|
255
|
+
jev_verdict, jev_confidence, jev_probabilities, jev_model, criteria_hash
|
|
247
256
|
) VALUES (
|
|
248
257
|
@request_id, @session_id, @timestamp, @complexity_score, @tier,
|
|
249
258
|
@agentic_type, @tool_count, @input_tokens, @message_count, @request_type,
|
|
@@ -252,7 +261,8 @@ function record(data) {
|
|
|
252
261
|
@retry_count, @circuit_breaker_state, @quality_score, @tokens_per_second,
|
|
253
262
|
@cost_efficiency, @request_text, @response_text,
|
|
254
263
|
@base_tier, @escalation_source, @propensity, @candidates, @pinned, @switch_reason,
|
|
255
|
-
@cache_decision, @cache_read_tokens, @cache_creation_tokens, @context
|
|
264
|
+
@cache_decision, @cache_read_tokens, @cache_creation_tokens, @context,
|
|
265
|
+
@jev_verdict, @jev_confidence, @jev_probabilities, @jev_model, @criteria_hash
|
|
256
266
|
)`
|
|
257
267
|
);
|
|
258
268
|
if (!insert) return;
|
|
@@ -308,6 +318,13 @@ function record(data) {
|
|
|
308
318
|
context: data.context == null
|
|
309
319
|
? null
|
|
310
320
|
: (typeof data.context === "string" ? data.context : JSON.stringify(data.context)),
|
|
321
|
+
jev_verdict: data.jev_verdict ?? null,
|
|
322
|
+
jev_confidence: data.jev_confidence ?? null,
|
|
323
|
+
jev_probabilities: data.jev_probabilities === null || data.jev_probabilities === undefined
|
|
324
|
+
? null
|
|
325
|
+
: (typeof data.jev_probabilities === "string" ? data.jev_probabilities : JSON.stringify(data.jev_probabilities)),
|
|
326
|
+
jev_model: data.jev_model ?? null,
|
|
327
|
+
criteria_hash: data.criteria_hash ?? null,
|
|
311
328
|
});
|
|
312
329
|
} catch (err) {
|
|
313
330
|
logger.debug({ err: err.message }, "Telemetry record failed");
|
|
@@ -1136,8 +1153,36 @@ function getAnalytics(opts = {}) {
|
|
|
1136
1153
|
}
|
|
1137
1154
|
}
|
|
1138
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* Map a Jev verdict (decision.jev or analysis.jev shape) onto the
|
|
1158
|
+
* jev_* telemetry columns. Null-safe: anything missing yields all-null
|
|
1159
|
+
* fields so call sites spread this unconditionally.
|
|
1160
|
+
*/
|
|
1161
|
+
function jevFields(src) {
|
|
1162
|
+
const j = (src && typeof src === 'object')
|
|
1163
|
+
? (src.jev && typeof src.jev === 'object' ? src.jev
|
|
1164
|
+
: (src.analysis && typeof src.analysis === 'object' && src.analysis.jev ? src.analysis.jev : null))
|
|
1165
|
+
: null;
|
|
1166
|
+
if (!j) {
|
|
1167
|
+
return {
|
|
1168
|
+
jev_verdict: null, jev_confidence: null, jev_probabilities: null,
|
|
1169
|
+
jev_model: null, criteria_hash: null,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
jev_verdict: j.tier ?? null,
|
|
1174
|
+
jev_confidence: j.confidence ?? null,
|
|
1175
|
+
jev_probabilities: j.probabilities === null || j.probabilities === undefined
|
|
1176
|
+
? null
|
|
1177
|
+
: (typeof j.probabilities === 'string' ? j.probabilities : JSON.stringify(j.probabilities)),
|
|
1178
|
+
jev_model: j.model ?? null,
|
|
1179
|
+
criteria_hash: j.criteriaHash ?? j.criteria_hash ?? null,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1139
1183
|
module.exports = {
|
|
1140
1184
|
record,
|
|
1185
|
+
jevFields,
|
|
1141
1186
|
query,
|
|
1142
1187
|
getStats: getStatsCached,
|
|
1143
1188
|
getProviderStats: getProviderStatsCached,
|