lynkr 9.14.2 → 9.14.7
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 +5 -1
- package/bin/cli.js +14 -6
- package/bin/lynkr-desktop-token.js +108 -0
- package/bin/lynkr-init.js +13 -1
- package/bin/lynkr-restart.js +125 -0
- package/bin/opencode-setup.js +175 -0
- package/bin/run.js +92 -0
- package/config/model-capabilities.json +20 -0
- package/config/model-capability-seeds.json +75 -0
- package/config/model-tiers.json +13 -1
- package/package.json +10 -4
- package/scripts/claude-desktop.js +212 -0
- package/scripts/ope-report.js +78 -0
- package/scripts/seed-capabilities.js +230 -0
- package/src/api/claude-desktop-gateway.js +123 -0
- package/src/api/mcp-broker.js +149 -0
- package/src/api/middleware/budget-enforcer.js +77 -7
- package/src/api/middleware/budget.js +36 -2
- package/src/api/openai-router.js +125 -4
- package/src/api/providers-handler.js +16 -0
- package/src/api/router.js +67 -103
- package/src/auth-mode.js +40 -4
- package/src/budget/index.js +86 -0
- package/src/cache/embeddings.js +125 -35
- package/src/cache/onnx-embedder.js +92 -0
- package/src/clients/databricks.js +285 -21
- package/src/clients/gpt-utils.js +83 -7
- package/src/clients/health-probe.js +157 -0
- package/src/clients/provider-capabilities.js +27 -0
- package/src/config/index.js +13 -1
- package/src/context/compression.js +127 -19
- package/src/dashboard/api.js +1 -0
- package/src/observability/metrics.js +52 -0
- package/src/observability/otel.js +180 -0
- package/src/orchestrator/azure-responses-sse.js +223 -0
- package/src/orchestrator/index.js +219 -136
- package/src/orchestrator/passthrough-stream.js +7 -1
- package/src/orchestrator/sse-transformer.js +62 -3
- package/src/routing/bandit.js +39 -0
- package/src/routing/capabilities.js +88 -0
- package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
- package/src/routing/capability-seeds/family-heuristics.js +77 -0
- package/src/routing/capability-seeds/family.js +105 -0
- package/src/routing/capability-seeds/models-dev.js +64 -0
- package/src/routing/capability-seeds/normalize.js +160 -0
- package/src/routing/capability-seeds/registry.js +125 -0
- package/src/routing/capability-seeds/sources.js +69 -0
- package/src/routing/capability-seeds/swebench.js +66 -0
- package/src/routing/decide.js +149 -0
- package/src/routing/deescalator.js +37 -0
- package/src/routing/index.js +202 -86
- package/src/routing/model-registry.js +25 -0
- package/src/routing/model-slots.js +65 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/ope.js +255 -0
- package/src/routing/openai-model-slots.js +82 -0
- package/src/routing/shortfall.js +290 -0
- package/src/routing/stuck-detector.js +127 -0
- package/src/routing/telemetry.js +11 -2
- package/src/server.js +41 -3
- package/src/tools/web-search-exec.js +182 -0
package/src/cache/embeddings.js
CHANGED
|
@@ -115,55 +115,136 @@ function simpleHash(str) {
|
|
|
115
115
|
return hash;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// Embedding provider availability state.
|
|
119
|
+
//
|
|
120
|
+
// The hash fallback is NOT a semantic embedding — when it's active, the
|
|
121
|
+
// semantic cache and kNN router are effectively disabled (hash vectors are
|
|
122
|
+
// 384-dim vs the providers' 768-dim, so they never match real entries; the
|
|
123
|
+
// kNN index rejects them on dimension). Historically this degradation was a
|
|
124
|
+
// permanent latch (one transient provider failure disabled semantic matching
|
|
125
|
+
// until process restart) and logged only at debug level — i.e. invisible.
|
|
126
|
+
// The same failure shape elsewhere in the industry silently misrouted ~20%
|
|
127
|
+
// of a production cluster's spend before anyone noticed, so this state is
|
|
128
|
+
// now: retried on a cooldown, logged loudly on every transition, counted,
|
|
129
|
+
// and exposed via getEmbeddingStatus() for /metrics.
|
|
119
130
|
let embeddingProviderAvailable = null;
|
|
131
|
+
let degradedSince = null;
|
|
132
|
+
let lastProviderAttempt = 0;
|
|
133
|
+
let fallbackCount = 0;
|
|
134
|
+
let lastProviderError = null;
|
|
135
|
+
|
|
136
|
+
// After a provider failure, wait this long before trying it again (instead
|
|
137
|
+
// of latching to the fallback forever). Env-tunable for tests/impatience.
|
|
138
|
+
const RETRY_COOLDOWN_MS = Number.parseInt(process.env.LYNKR_EMBEDDINGS_RETRY_COOLDOWN_MS, 10) || 60_000;
|
|
139
|
+
|
|
140
|
+
// Strict mode: throw instead of silently degrading to hash embeddings.
|
|
141
|
+
// For deployments that prefer fail-loud (no semantic cache is better than a
|
|
142
|
+
// silently fake one). Default stays fail-soft to match Lynkr's philosophy.
|
|
143
|
+
const STRICT = process.env.LYNKR_EMBEDDINGS_STRICT === 'true';
|
|
144
|
+
|
|
145
|
+
function _noteFallback(providerName, err) {
|
|
146
|
+
fallbackCount += 1;
|
|
147
|
+
lastProviderError = err?.message || String(err);
|
|
148
|
+
if (embeddingProviderAvailable !== false) {
|
|
149
|
+
embeddingProviderAvailable = false;
|
|
150
|
+
degradedSince = Date.now();
|
|
151
|
+
logger.warn({
|
|
152
|
+
provider: providerName,
|
|
153
|
+
error: lastProviderError,
|
|
154
|
+
retryCooldownMs: RETRY_COOLDOWN_MS,
|
|
155
|
+
}, '[Embeddings] Provider unreachable — DEGRADED to non-semantic hash embeddings. Semantic cache and kNN routing are effectively disabled until the provider recovers.');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _noteRecovery(providerName) {
|
|
160
|
+
if (embeddingProviderAvailable === false) {
|
|
161
|
+
logger.info({
|
|
162
|
+
provider: providerName,
|
|
163
|
+
degradedForMs: degradedSince ? Date.now() - degradedSince : null,
|
|
164
|
+
fallbacksServed: fallbackCount,
|
|
165
|
+
}, '[Embeddings] Provider recovered — semantic embeddings restored');
|
|
166
|
+
}
|
|
167
|
+
embeddingProviderAvailable = true;
|
|
168
|
+
degradedSince = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _wrapProvider(providerName, providerFn) {
|
|
172
|
+
return async (text) => {
|
|
173
|
+
// While degraded, only re-attempt the provider after the cooldown; serve
|
|
174
|
+
// the fallback in between so a dead provider doesn't add per-request
|
|
175
|
+
// connect timeouts to the hot path.
|
|
176
|
+
if (embeddingProviderAvailable === false
|
|
177
|
+
&& Date.now() - lastProviderAttempt < RETRY_COOLDOWN_MS) {
|
|
178
|
+
if (STRICT) throw new Error(`Embedding provider ${providerName} degraded: ${lastProviderError}`);
|
|
179
|
+
fallbackCount += 1;
|
|
180
|
+
return generateHashEmbedding(text);
|
|
181
|
+
}
|
|
182
|
+
lastProviderAttempt = Date.now();
|
|
183
|
+
try {
|
|
184
|
+
const result = await providerFn(text);
|
|
185
|
+
_noteRecovery(providerName);
|
|
186
|
+
return result;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
_noteFallback(providerName, err);
|
|
189
|
+
if (STRICT) throw err;
|
|
190
|
+
return generateHashEmbedding(text);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
120
194
|
|
|
121
195
|
/**
|
|
122
196
|
* Get the appropriate embedding function based on config
|
|
123
197
|
* @returns {Function} - Embedding generation function
|
|
124
198
|
*/
|
|
125
199
|
function getEmbeddingFunction() {
|
|
126
|
-
// If we already know embedding provider isn't available, use fallback
|
|
127
|
-
if (embeddingProviderAvailable === false) {
|
|
128
|
-
return (text) => Promise.resolve(generateHashEmbedding(text));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
200
|
const provider = config.modelProvider?.type || 'databricks';
|
|
132
201
|
|
|
202
|
+
// In-process ONNX embedder (opt-in): no external embedding server on the
|
|
203
|
+
// hot path at all. Same underlying model as the Ollama default
|
|
204
|
+
// (nomic-embed-text, 768-dim), so existing kNN/cache vectors stay valid.
|
|
205
|
+
// Wrapped in the same degradation machinery — a failed model load logs
|
|
206
|
+
// loudly, serves the hash fallback, and retries after the cooldown.
|
|
207
|
+
if (process.env.LYNKR_EMBEDDINGS_PROVIDER === 'onnx') {
|
|
208
|
+
const { generateOnnxEmbedding, isOnnxAvailable } = require('./onnx-embedder');
|
|
209
|
+
if (isOnnxAvailable()) {
|
|
210
|
+
return _wrapProvider('onnx', generateOnnxEmbedding);
|
|
211
|
+
}
|
|
212
|
+
logger.warn('[Embeddings] LYNKR_EMBEDDINGS_PROVIDER=onnx but @huggingface/transformers is not installed (optionalDependency) — falling through to the configured network provider');
|
|
213
|
+
}
|
|
214
|
+
|
|
133
215
|
// Check if we have a local embedding provider configured
|
|
134
216
|
if (config.ollama?.embeddingsEndpoint || provider === 'ollama') {
|
|
135
|
-
return
|
|
136
|
-
try {
|
|
137
|
-
const result = await generateOllamaEmbedding(text);
|
|
138
|
-
embeddingProviderAvailable = true;
|
|
139
|
-
return result;
|
|
140
|
-
} catch (err) {
|
|
141
|
-
logger.debug({ error: err.message }, 'Ollama embedding failed, using hash fallback');
|
|
142
|
-
embeddingProviderAvailable = false;
|
|
143
|
-
return generateHashEmbedding(text);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
217
|
+
return _wrapProvider('ollama', generateOllamaEmbedding);
|
|
146
218
|
}
|
|
147
219
|
|
|
148
220
|
if (config.llamacpp?.embeddingsEndpoint || provider === 'llamacpp') {
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Fallback to hash-based embeddings
|
|
163
|
-
logger.debug('No embedding provider configured, using hash-based fallback');
|
|
221
|
+
return _wrapProvider('llamacpp', generateLlamaCppEmbedding);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// No provider configured at all — hash fallback is the deliberate mode,
|
|
225
|
+
// not a degradation. Warn once so the operator knows semantic matching is
|
|
226
|
+
// approximate, then stay quiet.
|
|
227
|
+
if (embeddingProviderAvailable === null) {
|
|
228
|
+
embeddingProviderAvailable = false;
|
|
229
|
+
logger.warn('[Embeddings] No embedding provider configured — using non-semantic hash embeddings. Semantic cache matches will be approximate; configure an Ollama/llama.cpp embeddings endpoint for real semantic matching.');
|
|
230
|
+
}
|
|
164
231
|
return (text) => Promise.resolve(generateHashEmbedding(text));
|
|
165
232
|
}
|
|
166
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Current embedding subsystem status, for /metrics and health surfaces.
|
|
236
|
+
* @returns {{ providerAvailable: boolean|null, degradedSince: number|null,
|
|
237
|
+
* fallbackCount: number, lastProviderError: string|null }}
|
|
238
|
+
*/
|
|
239
|
+
function getEmbeddingStatus() {
|
|
240
|
+
return {
|
|
241
|
+
providerAvailable: embeddingProviderAvailable,
|
|
242
|
+
degradedSince,
|
|
243
|
+
fallbackCount,
|
|
244
|
+
lastProviderError,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
167
248
|
/**
|
|
168
249
|
* Generate embedding for text
|
|
169
250
|
* @param {string} text - Text to embed
|
|
@@ -178,12 +259,16 @@ async function generateEmbedding(text) {
|
|
|
178
259
|
const maxLength = 8000;
|
|
179
260
|
const truncated = text.length > maxLength ? text.substring(0, maxLength) : text;
|
|
180
261
|
|
|
262
|
+
const embedFn = getEmbeddingFunction();
|
|
263
|
+
if (STRICT) return embedFn(truncated);
|
|
181
264
|
try {
|
|
182
|
-
const embedFn = getEmbeddingFunction();
|
|
183
265
|
return await embedFn(truncated);
|
|
184
266
|
} catch (err) {
|
|
185
|
-
// Final fallback to hash embeddings if everything else fails
|
|
186
|
-
|
|
267
|
+
// Final fallback to hash embeddings if everything else fails. The
|
|
268
|
+
// provider wrapper already logged the degradation transition loudly;
|
|
269
|
+
// this catch only covers unexpected non-provider errors.
|
|
270
|
+
logger.warn({ error: err.message }, '[Embeddings] Embedding generation failed unexpectedly, serving hash fallback');
|
|
271
|
+
fallbackCount += 1;
|
|
187
272
|
return generateHashEmbedding(truncated);
|
|
188
273
|
}
|
|
189
274
|
}
|
|
@@ -193,6 +278,10 @@ async function generateEmbedding(text) {
|
|
|
193
278
|
*/
|
|
194
279
|
function resetEmbeddingProvider() {
|
|
195
280
|
embeddingProviderAvailable = null;
|
|
281
|
+
degradedSince = null;
|
|
282
|
+
lastProviderAttempt = 0;
|
|
283
|
+
fallbackCount = 0;
|
|
284
|
+
lastProviderError = null;
|
|
196
285
|
}
|
|
197
286
|
|
|
198
287
|
/**
|
|
@@ -231,5 +320,6 @@ module.exports = {
|
|
|
231
320
|
generateHashEmbedding,
|
|
232
321
|
cosineSimilarity,
|
|
233
322
|
getEmbeddingFunction,
|
|
323
|
+
getEmbeddingStatus,
|
|
234
324
|
resetEmbeddingProvider,
|
|
235
325
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ONNX embedder (ROUTING-NOTES §4.10.2).
|
|
3
|
+
*
|
|
4
|
+
* Runs the embedding model inside the Lynkr process via transformers.js
|
|
5
|
+
* (@huggingface/transformers, an optionalDependency), removing the external
|
|
6
|
+
* Ollama/llama.cpp server from the semantic-cache and kNN hot path — no
|
|
7
|
+
* network hop, no "is the server up" failure mode, no contention with
|
|
8
|
+
* completion traffic on the same local runtime.
|
|
9
|
+
*
|
|
10
|
+
* Model choice is deliberate: the ONNX build of the SAME model the Ollama
|
|
11
|
+
* path defaults to (nomic-embed-text, 768-dim). Same model = same embedding
|
|
12
|
+
* space = existing kNN index entries and semantic-cache vectors stay valid.
|
|
13
|
+
* (INT8 quantization introduces small numeric differences vs Ollama's
|
|
14
|
+
* serving precision, but cosine similarity is preserved far above the 0.92
|
|
15
|
+
* cache threshold.)
|
|
16
|
+
*
|
|
17
|
+
* Weights are downloaded once from the HuggingFace hub into
|
|
18
|
+
* ~/.lynkr/models (override: LYNKR_ONNX_CACHE_DIR) and verified by
|
|
19
|
+
* transformers.js's own integrity checks. Nothing is bundled in the npm
|
|
20
|
+
* package.
|
|
21
|
+
*
|
|
22
|
+
* Opt-in via LYNKR_EMBEDDINGS_PROVIDER=onnx. If the optional dependency is
|
|
23
|
+
* missing or the model fails to load, the caller's degradation machinery
|
|
24
|
+
* (cache/embeddings.js) takes over — loud, observable, recoverable.
|
|
25
|
+
*
|
|
26
|
+
* @module cache/onnx-embedder
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const os = require('os');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const logger = require('../logger');
|
|
32
|
+
|
|
33
|
+
const MODEL_ID = process.env.LYNKR_ONNX_EMBEDDING_MODEL || 'Xenova/nomic-embed-text-v1';
|
|
34
|
+
const CACHE_DIR = process.env.LYNKR_ONNX_CACHE_DIR
|
|
35
|
+
|| path.join(os.homedir(), '.lynkr', 'models');
|
|
36
|
+
// q8 keeps the download ~25–35MB and inference CPU-friendly.
|
|
37
|
+
const DTYPE = process.env.LYNKR_ONNX_DTYPE || 'q8';
|
|
38
|
+
|
|
39
|
+
/** @type {Promise<Function>|null} memoized pipeline load (single flight) */
|
|
40
|
+
let pipelinePromise = null;
|
|
41
|
+
|
|
42
|
+
async function _loadPipeline() {
|
|
43
|
+
// Lazy, inside the function: @huggingface/transformers is an
|
|
44
|
+
// optionalDependency and an ESM-only package — import() from CJS.
|
|
45
|
+
const { pipeline, env } = await import('@huggingface/transformers');
|
|
46
|
+
env.cacheDir = CACHE_DIR;
|
|
47
|
+
const started = Date.now();
|
|
48
|
+
logger.info({ model: MODEL_ID, cacheDir: CACHE_DIR, dtype: DTYPE },
|
|
49
|
+
'[OnnxEmbedder] Loading embedding model (first run downloads weights)');
|
|
50
|
+
const extractor = await pipeline('feature-extraction', MODEL_ID, { dtype: DTYPE });
|
|
51
|
+
logger.info({ model: MODEL_ID, loadMs: Date.now() - started },
|
|
52
|
+
'[OnnxEmbedder] Embedding model ready');
|
|
53
|
+
return extractor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate an embedding fully in-process.
|
|
58
|
+
* @param {string} text
|
|
59
|
+
* @returns {Promise<number[]>} mean-pooled, L2-normalized vector (768-dim
|
|
60
|
+
* for the default model)
|
|
61
|
+
*/
|
|
62
|
+
async function generateOnnxEmbedding(text) {
|
|
63
|
+
if (!pipelinePromise) {
|
|
64
|
+
pipelinePromise = _loadPipeline().catch((err) => {
|
|
65
|
+
// Reset so a later call can retry (e.g. transient download failure) —
|
|
66
|
+
// a failed load must not latch permanently, same principle as the
|
|
67
|
+
// provider degradation fix in cache/embeddings.js.
|
|
68
|
+
pipelinePromise = null;
|
|
69
|
+
throw err;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const extractor = await pipelinePromise;
|
|
73
|
+
const output = await extractor(text, { pooling: 'mean', normalize: true });
|
|
74
|
+
return Array.from(output.data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True when the optional dependency is installed (cheap resolve check). */
|
|
78
|
+
function isOnnxAvailable() {
|
|
79
|
+
try {
|
|
80
|
+
require.resolve('@huggingface/transformers');
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test helper — drop the memoized pipeline. */
|
|
88
|
+
function _resetPipeline() {
|
|
89
|
+
pipelinePromise = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { generateOnnxEmbedding, isOnnxAvailable, _resetPipeline, MODEL_ID };
|