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.
Files changed (61) hide show
  1. package/README.md +5 -1
  2. package/bin/cli.js +14 -6
  3. package/bin/lynkr-desktop-token.js +108 -0
  4. package/bin/lynkr-init.js +13 -1
  5. package/bin/lynkr-restart.js +125 -0
  6. package/bin/opencode-setup.js +175 -0
  7. package/bin/run.js +92 -0
  8. package/config/model-capabilities.json +20 -0
  9. package/config/model-capability-seeds.json +75 -0
  10. package/config/model-tiers.json +13 -1
  11. package/package.json +10 -4
  12. package/scripts/claude-desktop.js +212 -0
  13. package/scripts/ope-report.js +78 -0
  14. package/scripts/seed-capabilities.js +230 -0
  15. package/src/api/claude-desktop-gateway.js +123 -0
  16. package/src/api/mcp-broker.js +149 -0
  17. package/src/api/middleware/budget-enforcer.js +77 -7
  18. package/src/api/middleware/budget.js +36 -2
  19. package/src/api/openai-router.js +125 -4
  20. package/src/api/providers-handler.js +16 -0
  21. package/src/api/router.js +67 -103
  22. package/src/auth-mode.js +40 -4
  23. package/src/budget/index.js +86 -0
  24. package/src/cache/embeddings.js +125 -35
  25. package/src/cache/onnx-embedder.js +92 -0
  26. package/src/clients/databricks.js +285 -21
  27. package/src/clients/gpt-utils.js +83 -7
  28. package/src/clients/health-probe.js +157 -0
  29. package/src/clients/provider-capabilities.js +27 -0
  30. package/src/config/index.js +13 -1
  31. package/src/context/compression.js +127 -19
  32. package/src/dashboard/api.js +1 -0
  33. package/src/observability/metrics.js +52 -0
  34. package/src/observability/otel.js +180 -0
  35. package/src/orchestrator/azure-responses-sse.js +223 -0
  36. package/src/orchestrator/index.js +219 -136
  37. package/src/orchestrator/passthrough-stream.js +7 -1
  38. package/src/orchestrator/sse-transformer.js +62 -3
  39. package/src/routing/bandit.js +39 -0
  40. package/src/routing/capabilities.js +88 -0
  41. package/src/routing/capability-seeds/benchmarks-dir.js +58 -0
  42. package/src/routing/capability-seeds/family-heuristics.js +77 -0
  43. package/src/routing/capability-seeds/family.js +105 -0
  44. package/src/routing/capability-seeds/models-dev.js +64 -0
  45. package/src/routing/capability-seeds/normalize.js +160 -0
  46. package/src/routing/capability-seeds/registry.js +125 -0
  47. package/src/routing/capability-seeds/sources.js +69 -0
  48. package/src/routing/capability-seeds/swebench.js +66 -0
  49. package/src/routing/decide.js +149 -0
  50. package/src/routing/deescalator.js +37 -0
  51. package/src/routing/index.js +202 -86
  52. package/src/routing/model-registry.js +25 -0
  53. package/src/routing/model-slots.js +65 -0
  54. package/src/routing/model-tiers.js +2 -0
  55. package/src/routing/ope.js +255 -0
  56. package/src/routing/openai-model-slots.js +82 -0
  57. package/src/routing/shortfall.js +290 -0
  58. package/src/routing/stuck-detector.js +127 -0
  59. package/src/routing/telemetry.js +11 -2
  60. package/src/server.js +41 -3
  61. package/src/tools/web-search-exec.js +182 -0
@@ -115,55 +115,136 @@ function simpleHash(str) {
115
115
  return hash;
116
116
  }
117
117
 
118
- // Track if embedding provider is available
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 async (text) => {
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 async (text) => {
150
- try {
151
- const result = await generateLlamaCppEmbedding(text);
152
- embeddingProviderAvailable = true;
153
- return result;
154
- } catch (err) {
155
- logger.debug({ error: err.message }, 'LlamaCpp embedding failed, using hash fallback');
156
- embeddingProviderAvailable = false;
157
- return generateHashEmbedding(text);
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
- logger.debug({ error: err.message }, 'Embedding generation failed, using hash fallback');
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 };