lynkr 8.0.0 → 9.0.1
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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +196 -322
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +64 -13
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +23 -0
- package/src/config/index.js +77 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +48 -8
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +13 -12
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +4 -0
- package/src/tools/lazy-loader.js +18 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -198
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -584
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/docs/toon-integration-spec.md +0 -130
- package/documentation/README.md +0 -101
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -679
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -734
- package/documentation/docker.md +0 -874
- package/documentation/embeddings.md +0 -762
- package/documentation/faq.md +0 -713
- package/documentation/features.md +0 -403
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -758
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -636
- package/documentation/providers.md +0 -1009
- package/documentation/routing.md +0 -476
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -325
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -969
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/te +0 -11622
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -213
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -287
- package/test/azure-openai-routing.test.js +0 -175
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -457
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -269
- package/test/hybrid-routing-performance.test.js +0 -428
- package/test/llamacpp-integration.test.js +0 -882
- package/test/lmstudio-integration.test.js +0 -347
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -683
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -225
- package/test/toon-compression.test.js +0 -131
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- package/test-subagents.sh +0 -117
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
* Complexity Analyzer Module
|
|
3
3
|
*
|
|
4
4
|
* Analyzes request complexity to determine optimal model routing.
|
|
5
|
-
* Implements all
|
|
5
|
+
* Implements all 5 phases of auto model selection:
|
|
6
6
|
* - Phase 1: Basic Scoring (token count, tool count, task classification)
|
|
7
7
|
* - Phase 2: Advanced Classification (code complexity, reasoning detection)
|
|
8
8
|
* - Phase 3: Learning & Tracking (metrics, feedback storage)
|
|
9
9
|
* - Phase 4: ML-Based (embeddings similarity)
|
|
10
|
+
* - Phase 5: Structural Analysis (code-review-graph blast radius & dependency signals)
|
|
10
11
|
*
|
|
11
12
|
* @module routing/complexity-analyzer
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
const logger = require('../logger');
|
|
15
16
|
const config = require('../config');
|
|
17
|
+
const codeGraph = require('../tools/code-graph');
|
|
16
18
|
|
|
17
19
|
// ============================================================================
|
|
18
20
|
// PHASE 1: Basic Scoring Patterns
|
|
@@ -178,6 +180,189 @@ const routingMetrics = {
|
|
|
178
180
|
},
|
|
179
181
|
};
|
|
180
182
|
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// PHASE 5: Structural Analysis Helpers (code-review-graph)
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/** Pattern to match file paths in message content */
|
|
188
|
+
const FILE_PATH_PATTERN = /(?:^|\s|["'`(])([.\w/-]+\.(?:js|ts|py|rb|go|rs|java|cpp|c|h|jsx|tsx|vue|svelte|json|yaml|yml|toml|sql|sh|bash|css|scss|html))\b/gi;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract file paths from text using the FILE_PATH_PATTERN regex.
|
|
192
|
+
* @param {string} text
|
|
193
|
+
* @param {Set<string>} paths — accumulator set
|
|
194
|
+
*/
|
|
195
|
+
function extractPathsFromText(text, paths) {
|
|
196
|
+
if (typeof text !== 'string') return;
|
|
197
|
+
for (const match of text.matchAll(FILE_PATH_PATTERN)) {
|
|
198
|
+
paths.add(match[1]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract file paths from the full conversation payload.
|
|
204
|
+
*
|
|
205
|
+
* Supports both Anthropic and OpenAI message formats:
|
|
206
|
+
* - Anthropic: system (string/array), messages with tool_use/tool_result blocks
|
|
207
|
+
* - OpenAI: messages with role=system, tool_calls with function.arguments
|
|
208
|
+
* - Cursor/Windsurf: file context embedded in system prompts
|
|
209
|
+
* - Codex CLI / Aider: function call arguments with file paths
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} payload — request payload
|
|
212
|
+
* @returns {string[]} deduplicated file paths
|
|
213
|
+
*/
|
|
214
|
+
function extractFilePaths(payload) {
|
|
215
|
+
const paths = new Set();
|
|
216
|
+
|
|
217
|
+
if (!payload) return [];
|
|
218
|
+
|
|
219
|
+
// --- Anthropic system prompt (string or array of content blocks) ---
|
|
220
|
+
if (typeof payload.system === 'string') {
|
|
221
|
+
extractPathsFromText(payload.system, paths);
|
|
222
|
+
} else if (Array.isArray(payload.system)) {
|
|
223
|
+
for (const block of payload.system) {
|
|
224
|
+
if (block?.type === 'text' && block.text) {
|
|
225
|
+
extractPathsFromText(block.text, paths);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!Array.isArray(payload.messages)) return Array.from(paths);
|
|
231
|
+
|
|
232
|
+
for (const msg of payload.messages) {
|
|
233
|
+
// --- String content (both Anthropic and OpenAI) ---
|
|
234
|
+
if (typeof msg.content === 'string') {
|
|
235
|
+
extractPathsFromText(msg.content, paths);
|
|
236
|
+
} else if (Array.isArray(msg.content)) {
|
|
237
|
+
for (const block of msg.content) {
|
|
238
|
+
// Text blocks (Anthropic format)
|
|
239
|
+
if (block?.type === 'text' && block.text) {
|
|
240
|
+
extractPathsFromText(block.text, paths);
|
|
241
|
+
}
|
|
242
|
+
// Tool use blocks (Anthropic format — Claude Code, Cline, Zed)
|
|
243
|
+
if (block?.type === 'tool_use' && block.input) {
|
|
244
|
+
const input = block.input;
|
|
245
|
+
if (typeof input.file_path === 'string') paths.add(input.file_path);
|
|
246
|
+
if (typeof input.path === 'string') paths.add(input.path);
|
|
247
|
+
if (typeof input.command === 'string') {
|
|
248
|
+
extractPathsFromText(input.command, paths);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Tool result blocks (Anthropic format)
|
|
252
|
+
if (block?.type === 'tool_result') {
|
|
253
|
+
const resultContent = Array.isArray(block.content) ? block.content : [];
|
|
254
|
+
for (const rc of resultContent) {
|
|
255
|
+
if (rc?.type === 'text' && rc.text) {
|
|
256
|
+
extractPathsFromText(rc.text, paths);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- OpenAI tool_calls format (Codex CLI, Aider, Continue.dev) ---
|
|
264
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
265
|
+
for (const tc of msg.tool_calls) {
|
|
266
|
+
if (tc?.function?.arguments) {
|
|
267
|
+
try {
|
|
268
|
+
// function.arguments is a JSON string in OpenAI format
|
|
269
|
+
const args = typeof tc.function.arguments === 'string'
|
|
270
|
+
? JSON.parse(tc.function.arguments)
|
|
271
|
+
: tc.function.arguments;
|
|
272
|
+
if (typeof args.file_path === 'string') paths.add(args.file_path);
|
|
273
|
+
if (typeof args.path === 'string') paths.add(args.path);
|
|
274
|
+
if (typeof args.command === 'string') {
|
|
275
|
+
extractPathsFromText(args.command, paths);
|
|
276
|
+
}
|
|
277
|
+
// Also scan the full arguments text for paths
|
|
278
|
+
if (typeof tc.function.arguments === 'string') {
|
|
279
|
+
extractPathsFromText(tc.function.arguments, paths);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// If arguments isn't valid JSON, scan as text
|
|
283
|
+
if (typeof tc.function.arguments === 'string') {
|
|
284
|
+
extractPathsFromText(tc.function.arguments, paths);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- OpenAI function_call format (legacy, some tools still use it) ---
|
|
292
|
+
if (msg.function_call?.arguments) {
|
|
293
|
+
try {
|
|
294
|
+
const args = typeof msg.function_call.arguments === 'string'
|
|
295
|
+
? JSON.parse(msg.function_call.arguments)
|
|
296
|
+
: msg.function_call.arguments;
|
|
297
|
+
if (typeof args.file_path === 'string') paths.add(args.file_path);
|
|
298
|
+
if (typeof args.path === 'string') paths.add(args.path);
|
|
299
|
+
} catch {
|
|
300
|
+
if (typeof msg.function_call.arguments === 'string') {
|
|
301
|
+
extractPathsFromText(msg.function_call.arguments, paths);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return Array.from(paths);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Calculate score adjustment from Graphify complexity signals.
|
|
312
|
+
* Capped at +35 (increased from +25 due to richer signals).
|
|
313
|
+
*
|
|
314
|
+
* @param {{ blast_radius: number, dependency_depth: number, test_coverage_pct: number, is_infrastructure: boolean, god_node_touched: boolean, community_count: number, cohesion: number }} signals
|
|
315
|
+
* @returns {{ adjustment: number, reasons: string[] }}
|
|
316
|
+
*/
|
|
317
|
+
function scoreGraphSignals(signals) {
|
|
318
|
+
let adjustment = 0;
|
|
319
|
+
const reasons = [];
|
|
320
|
+
|
|
321
|
+
// Blast radius — how many files are affected
|
|
322
|
+
if (signals.blast_radius > 30) {
|
|
323
|
+
adjustment += 15;
|
|
324
|
+
reasons.push('blast_radius_high');
|
|
325
|
+
} else if (signals.blast_radius > 10) {
|
|
326
|
+
adjustment += 10;
|
|
327
|
+
reasons.push('blast_radius_medium');
|
|
328
|
+
} else if (signals.blast_radius > 5) {
|
|
329
|
+
adjustment += 5;
|
|
330
|
+
reasons.push('blast_radius_low');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Dependency depth — deep call chains are harder to reason about
|
|
334
|
+
if (signals.dependency_depth > 4) {
|
|
335
|
+
adjustment += 5;
|
|
336
|
+
reasons.push('deep_dependencies');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Infrastructure files — config/CI/deploy changes are high-risk
|
|
340
|
+
if (signals.is_infrastructure) {
|
|
341
|
+
adjustment += 10;
|
|
342
|
+
reasons.push('infrastructure_file');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Low test coverage — changes in untested areas are riskier
|
|
346
|
+
if (signals.test_coverage_pct < 30) {
|
|
347
|
+
adjustment += 5;
|
|
348
|
+
reasons.push('low_test_coverage');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// God node touched — editing a hub class that many things depend on
|
|
352
|
+
if (signals.god_node_touched) {
|
|
353
|
+
adjustment += 10;
|
|
354
|
+
reasons.push('god_node_touched');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Low community cohesion — loosely coupled code is harder to change safely
|
|
358
|
+
if (typeof signals.cohesion === 'number' && signals.cohesion < 0.15 && signals.community_count > 1) {
|
|
359
|
+
adjustment += 5;
|
|
360
|
+
reasons.push('low_community_cohesion');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { adjustment: Math.min(adjustment, 35), reasons };
|
|
364
|
+
}
|
|
365
|
+
|
|
181
366
|
// ============================================================================
|
|
182
367
|
// CORE ANALYSIS FUNCTIONS
|
|
183
368
|
// ============================================================================
|
|
@@ -546,7 +731,7 @@ function getThreshold() {
|
|
|
546
731
|
* @param {Object} options - Analysis options
|
|
547
732
|
* @returns {Object} Complexity analysis result
|
|
548
733
|
*/
|
|
549
|
-
function analyzeComplexity(payload, options = {}) {
|
|
734
|
+
async function analyzeComplexity(payload, options = {}) {
|
|
550
735
|
const content = extractContent(payload);
|
|
551
736
|
const messageCount = payload?.messages?.length ?? 0;
|
|
552
737
|
const useWeighted = options.weighted ?? config.routing?.weightedScoring ?? false;
|
|
@@ -568,7 +753,7 @@ function analyzeComplexity(payload, options = {}) {
|
|
|
568
753
|
recommendation = weighted.score >= threshold ? 'cloud' : 'local';
|
|
569
754
|
}
|
|
570
755
|
|
|
571
|
-
|
|
756
|
+
const result = {
|
|
572
757
|
score: weighted.score,
|
|
573
758
|
threshold,
|
|
574
759
|
mode: 'weighted',
|
|
@@ -578,7 +763,39 @@ function analyzeComplexity(payload, options = {}) {
|
|
|
578
763
|
meta: weighted.meta,
|
|
579
764
|
forceReason: taskTypeResult.reason?.startsWith('force_') ? taskTypeResult.reason : null,
|
|
580
765
|
content: content.slice(0, 100) + (content.length > 100 ? '...' : ''),
|
|
766
|
+
graphSignals: null,
|
|
581
767
|
};
|
|
768
|
+
|
|
769
|
+
// Phase 5: Structural Analysis (code-review-graph, optional)
|
|
770
|
+
try {
|
|
771
|
+
const filePaths = extractFilePaths(payload);
|
|
772
|
+
const graphOpts = { filePaths, workspace: options?.workspace };
|
|
773
|
+
const graphAvailable = await codeGraph.isAvailable(graphOpts);
|
|
774
|
+
if (graphAvailable && filePaths.length > 0) {
|
|
775
|
+
const signals = await codeGraph.getComplexitySignals(filePaths, graphOpts);
|
|
776
|
+
if (signals) {
|
|
777
|
+
const { adjustment, reasons } = scoreGraphSignals(signals);
|
|
778
|
+
result.score = Math.min(result.score + adjustment, 100);
|
|
779
|
+
result.graphSignals = { ...signals, adjustment, reasons };
|
|
780
|
+
|
|
781
|
+
// Re-evaluate recommendation with adjusted score
|
|
782
|
+
if (!result.forceReason) {
|
|
783
|
+
result.recommendation = result.score >= threshold ? 'cloud' : 'local';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
logger.debug({
|
|
787
|
+
filePaths: filePaths.slice(0, 5),
|
|
788
|
+
signals,
|
|
789
|
+
adjustment,
|
|
790
|
+
reasons,
|
|
791
|
+
}, '[complexity] Phase 5: graph signals applied');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch (err) {
|
|
795
|
+
logger.debug({ err: err.message }, '[complexity] Phase 5: code-graph query failed');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return result;
|
|
582
799
|
}
|
|
583
800
|
|
|
584
801
|
// Standard scoring (original logic)
|
|
@@ -600,7 +817,7 @@ function analyzeComplexity(payload, options = {}) {
|
|
|
600
817
|
|
|
601
818
|
// Conversation length bonus (long conversations tend to be complex)
|
|
602
819
|
const conversationBonus = messageCount > 10 ? 5 : (messageCount > 5 ? 2 : 0);
|
|
603
|
-
|
|
820
|
+
let adjustedScore = Math.min(totalScore + conversationBonus, 100);
|
|
604
821
|
|
|
605
822
|
// Determine recommendation
|
|
606
823
|
const threshold = getThreshold();
|
|
@@ -615,7 +832,7 @@ function analyzeComplexity(payload, options = {}) {
|
|
|
615
832
|
recommendation = adjustedScore >= threshold ? 'cloud' : 'local';
|
|
616
833
|
}
|
|
617
834
|
|
|
618
|
-
|
|
835
|
+
const result = {
|
|
619
836
|
score: adjustedScore,
|
|
620
837
|
threshold,
|
|
621
838
|
mode,
|
|
@@ -629,7 +846,39 @@ function analyzeComplexity(payload, options = {}) {
|
|
|
629
846
|
conversationBonus,
|
|
630
847
|
},
|
|
631
848
|
content: content.slice(0, 100) + (content.length > 100 ? '...' : ''),
|
|
849
|
+
graphSignals: null,
|
|
632
850
|
};
|
|
851
|
+
|
|
852
|
+
// Phase 5: Structural Analysis (code-review-graph, optional)
|
|
853
|
+
try {
|
|
854
|
+
const filePaths = extractFilePaths(payload);
|
|
855
|
+
const graphOpts = { filePaths, workspace: options?.workspace };
|
|
856
|
+
const graphAvailable = await codeGraph.isAvailable(graphOpts);
|
|
857
|
+
if (graphAvailable && filePaths.length > 0) {
|
|
858
|
+
const signals = await codeGraph.getComplexitySignals(filePaths, graphOpts);
|
|
859
|
+
if (signals) {
|
|
860
|
+
const { adjustment, reasons } = scoreGraphSignals(signals);
|
|
861
|
+
result.score = Math.min(result.score + adjustment, 100);
|
|
862
|
+
result.graphSignals = { ...signals, adjustment, reasons };
|
|
863
|
+
|
|
864
|
+
// Re-evaluate recommendation with adjusted score
|
|
865
|
+
if (taskTypeResult.reason !== 'force_local' && taskTypeResult.reason !== 'force_cloud') {
|
|
866
|
+
result.recommendation = result.score >= threshold ? 'cloud' : 'local';
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
logger.debug({
|
|
870
|
+
filePaths: filePaths.slice(0, 5),
|
|
871
|
+
signals,
|
|
872
|
+
adjustment,
|
|
873
|
+
reasons,
|
|
874
|
+
}, '[complexity] Phase 5: graph signals applied');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
} catch (err) {
|
|
878
|
+
logger.debug({ err: err.message }, '[complexity] Phase 5: code-graph query failed');
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return result;
|
|
633
882
|
}
|
|
634
883
|
|
|
635
884
|
/**
|
|
@@ -784,6 +1033,10 @@ module.exports = {
|
|
|
784
1033
|
analyzeWithEmbeddings,
|
|
785
1034
|
getContentEmbedding,
|
|
786
1035
|
|
|
1036
|
+
// Phase 5: Structural Analysis (code-review-graph)
|
|
1037
|
+
extractFilePaths,
|
|
1038
|
+
scoreGraphSignals,
|
|
1039
|
+
|
|
787
1040
|
// Constants (for testing)
|
|
788
1041
|
PATTERNS,
|
|
789
1042
|
ADVANCED_PATTERNS,
|
package/src/routing/index.js
CHANGED
|
@@ -23,6 +23,11 @@ const { getAgenticDetector, AGENT_TYPES } = require('./agentic-detector');
|
|
|
23
23
|
const { getModelTierSelector, TIER_DEFINITIONS } = require('./model-tiers');
|
|
24
24
|
const { getCostOptimizer } = require('./cost-optimizer');
|
|
25
25
|
|
|
26
|
+
// Telemetry modules
|
|
27
|
+
const telemetry = require('./telemetry');
|
|
28
|
+
const { scoreResponseQuality } = require('./quality-scorer');
|
|
29
|
+
const { getLatencyTracker } = require('./latency-tracker');
|
|
30
|
+
|
|
26
31
|
// Local providers
|
|
27
32
|
const LOCAL_PROVIDERS = ['ollama', 'llamacpp', 'lmstudio'];
|
|
28
33
|
|
|
@@ -148,9 +153,9 @@ async function determineProviderSmart(payload, options = {}) {
|
|
|
148
153
|
return decision;
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
// Full complexity analysis
|
|
156
|
+
// Full complexity analysis (pass workspace for code-graph integration)
|
|
152
157
|
const useWeightedScoring = config.routing?.weightedScoring ?? false;
|
|
153
|
-
const analysis = analyzeComplexity(payload, { weighted: useWeightedScoring });
|
|
158
|
+
const analysis = await analyzeComplexity(payload, { weighted: useWeightedScoring, workspace: options.workspace });
|
|
154
159
|
|
|
155
160
|
// Phase 4: Optional embeddings adjustment
|
|
156
161
|
let embeddingsResult = null;
|
|
@@ -381,4 +386,9 @@ module.exports = {
|
|
|
381
386
|
getCostOptimizer,
|
|
382
387
|
AGENT_TYPES,
|
|
383
388
|
TIER_DEFINITIONS,
|
|
389
|
+
|
|
390
|
+
// Telemetry
|
|
391
|
+
telemetry,
|
|
392
|
+
scoreResponseQuality,
|
|
393
|
+
getLatencyTracker,
|
|
384
394
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling Latency Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks per-provider latency using circular buffers to provide
|
|
5
|
+
* P50/P95/P99 percentile statistics for routing decisions.
|
|
6
|
+
*
|
|
7
|
+
* @module routing/latency-tracker
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require("../logger");
|
|
11
|
+
|
|
12
|
+
/** Size of the circular buffer per provider */
|
|
13
|
+
const BUFFER_SIZE = 200;
|
|
14
|
+
|
|
15
|
+
/** Minimum sample count before penalizeScore returns a meaningful value */
|
|
16
|
+
const MIN_SAMPLES = 10;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} LatencyStats
|
|
20
|
+
* @property {number} p50 - 50th percentile latency (ms)
|
|
21
|
+
* @property {number} p95 - 95th percentile latency (ms)
|
|
22
|
+
* @property {number} p99 - 99th percentile latency (ms)
|
|
23
|
+
* @property {number} avg - Average latency (ms)
|
|
24
|
+
* @property {number} count - Total measurements recorded
|
|
25
|
+
* @property {number} lastUpdated - Timestamp of the last recorded measurement
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
class LatencyTracker {
|
|
29
|
+
constructor() {
|
|
30
|
+
/** @type {Map<string, { buffer: number[], index: number, count: number, lastUpdated: number }>} */
|
|
31
|
+
this._providers = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Record a latency measurement for a provider.
|
|
36
|
+
* @param {string} provider - Provider name (e.g. "databricks", "ollama")
|
|
37
|
+
* @param {number} latencyMs - Measured latency in milliseconds
|
|
38
|
+
*/
|
|
39
|
+
record(provider, latencyMs) {
|
|
40
|
+
if (!provider || typeof latencyMs !== "number" || latencyMs < 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let entry = this._providers.get(provider);
|
|
45
|
+
if (!entry) {
|
|
46
|
+
entry = {
|
|
47
|
+
buffer: new Array(BUFFER_SIZE).fill(0),
|
|
48
|
+
index: 0,
|
|
49
|
+
count: 0,
|
|
50
|
+
lastUpdated: 0,
|
|
51
|
+
};
|
|
52
|
+
this._providers.set(provider, entry);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
entry.buffer[entry.index] = latencyMs;
|
|
56
|
+
entry.index = (entry.index + 1) % BUFFER_SIZE;
|
|
57
|
+
entry.count += 1;
|
|
58
|
+
entry.lastUpdated = Date.now();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get latency statistics for a specific provider.
|
|
63
|
+
* @param {string} provider - Provider name
|
|
64
|
+
* @returns {LatencyStats|null} Statistics or null if no data
|
|
65
|
+
*/
|
|
66
|
+
getStats(provider) {
|
|
67
|
+
const entry = this._providers.get(provider);
|
|
68
|
+
if (!entry || entry.count === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sampleCount = Math.min(entry.count, BUFFER_SIZE);
|
|
73
|
+
const samples = entry.buffer.slice(0, sampleCount);
|
|
74
|
+
const sorted = samples.slice().sort((a, b) => a - b);
|
|
75
|
+
|
|
76
|
+
const sum = sorted.reduce((acc, v) => acc + v, 0);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
p50: sorted[Math.floor(sampleCount * 0.5)],
|
|
80
|
+
p95: sorted[Math.floor(sampleCount * 0.95)],
|
|
81
|
+
p99: sorted[Math.floor(sampleCount * 0.99)],
|
|
82
|
+
avg: Math.round(sum / sampleCount),
|
|
83
|
+
count: entry.count,
|
|
84
|
+
lastUpdated: entry.lastUpdated,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Calculate a routing score penalty/bonus based on provider latency.
|
|
90
|
+
*
|
|
91
|
+
* Returns a value from -5 to +10 that can be added to a routing score:
|
|
92
|
+
* +10 if P95 > 10000ms (very slow, penalise by boosting complexity toward cloud)
|
|
93
|
+
* +5 if P95 > 5000ms
|
|
94
|
+
* -5 if P50 < 1000ms (fast, reward)
|
|
95
|
+
* 0 otherwise or if insufficient data
|
|
96
|
+
*
|
|
97
|
+
* @param {string} provider - Provider name
|
|
98
|
+
* @returns {number} Score adjustment (-5 to +10)
|
|
99
|
+
*/
|
|
100
|
+
penalizeScore(provider) {
|
|
101
|
+
const stats = this.getStats(provider);
|
|
102
|
+
if (!stats || stats.count < MIN_SAMPLES) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stats.p95 > 10000) return 10;
|
|
107
|
+
if (stats.p95 > 5000) return 5;
|
|
108
|
+
if (stats.p50 < 1000) return -5;
|
|
109
|
+
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get statistics for all tracked providers.
|
|
115
|
+
* @returns {Map<string, LatencyStats>}
|
|
116
|
+
*/
|
|
117
|
+
getAllStats() {
|
|
118
|
+
const result = new Map();
|
|
119
|
+
for (const provider of this._providers.keys()) {
|
|
120
|
+
const stats = this.getStats(provider);
|
|
121
|
+
if (stats) {
|
|
122
|
+
result.set(provider, stats);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Singleton
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/** @type {LatencyTracker|null} */
|
|
134
|
+
let instance = null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get the singleton LatencyTracker instance.
|
|
138
|
+
* @returns {LatencyTracker}
|
|
139
|
+
*/
|
|
140
|
+
function getLatencyTracker() {
|
|
141
|
+
if (!instance) {
|
|
142
|
+
instance = new LatencyTracker();
|
|
143
|
+
logger.debug("LatencyTracker initialised");
|
|
144
|
+
}
|
|
145
|
+
return instance;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { LatencyTracker, getLatencyTracker };
|
|
@@ -213,6 +213,8 @@ class ModelTierSelector {
|
|
|
213
213
|
return config.zai?.model || null;
|
|
214
214
|
case 'moonshot':
|
|
215
215
|
return config.moonshot?.model || null;
|
|
216
|
+
case 'codex':
|
|
217
|
+
return config.codex?.model || null;
|
|
216
218
|
case 'vertex':
|
|
217
219
|
return config.vertex?.model || null;
|
|
218
220
|
case 'databricks':
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Scorer Module
|
|
3
|
+
*
|
|
4
|
+
* Lightweight heuristic scorer that evaluates response quality on a 0-100
|
|
5
|
+
* scale. Used by the telemetry system to detect over/under-provisioned
|
|
6
|
+
* routing decisions so they can be corrected over time.
|
|
7
|
+
*
|
|
8
|
+
* @module routing/quality-scorer
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} RequestContext
|
|
13
|
+
* @property {string} [tier] - Routing tier (SIMPLE, MODERATE, COMPLEX, REASONING)
|
|
14
|
+
* @property {boolean} [hasTools] - Whether the original request included tools
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ResponseOutcome
|
|
19
|
+
* @property {number} [status_code] - HTTP status code
|
|
20
|
+
* @property {number} [output_tokens] - Tokens produced in the response
|
|
21
|
+
* @property {number} [tool_calls_made] - Number of tool calls executed
|
|
22
|
+
* @property {boolean} [was_fallback] - Whether a fallback provider was used
|
|
23
|
+
* @property {number} [retry_count] - Number of retries before success
|
|
24
|
+
* @property {string} [error_type] - Error classification if the request failed
|
|
25
|
+
* @property {number} [latency_ms] - End-to-end latency in milliseconds
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Score the quality of a routed response.
|
|
30
|
+
*
|
|
31
|
+
* Starts at 50 and applies additive/subtractive heuristics.
|
|
32
|
+
* Final value is clamped to [0, 100].
|
|
33
|
+
*
|
|
34
|
+
* @param {RequestContext} request - Contextual information about the request
|
|
35
|
+
* @param {Object} _response - Raw response object (reserved for future use)
|
|
36
|
+
* @param {ResponseOutcome} outcome - Measured outcome metrics
|
|
37
|
+
* @returns {number} Quality score in range 0-100
|
|
38
|
+
*/
|
|
39
|
+
function scoreResponseQuality(request, _response, outcome) {
|
|
40
|
+
let score = 50;
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
status_code,
|
|
44
|
+
output_tokens,
|
|
45
|
+
tool_calls_made,
|
|
46
|
+
was_fallback,
|
|
47
|
+
retry_count,
|
|
48
|
+
error_type,
|
|
49
|
+
latency_ms,
|
|
50
|
+
} = outcome || {};
|
|
51
|
+
|
|
52
|
+
const tier = request?.tier;
|
|
53
|
+
const hasTools = request?.hasTools ?? false;
|
|
54
|
+
|
|
55
|
+
// --- Positive signals ---
|
|
56
|
+
|
|
57
|
+
if (status_code === 200) {
|
|
58
|
+
score += 10;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof output_tokens === "number" && output_tokens > 100) {
|
|
62
|
+
score += 5;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof tool_calls_made === "number" && tool_calls_made > 0 && hasTools) {
|
|
66
|
+
score += 10;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!was_fallback) {
|
|
70
|
+
score += 5;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (retry_count === 0) {
|
|
74
|
+
score += 5;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Negative signals ---
|
|
78
|
+
|
|
79
|
+
if (error_type) {
|
|
80
|
+
score -= 30;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (was_fallback) {
|
|
84
|
+
score -= 10;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof retry_count === "number" && retry_count > 1) {
|
|
88
|
+
score -= 10;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof latency_ms === "number" && latency_ms > 30000) {
|
|
92
|
+
score -= 10;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof output_tokens === "number" && output_tokens < 20 && hasTools) {
|
|
96
|
+
score -= 15;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Tier mismatch signals ---
|
|
100
|
+
|
|
101
|
+
if (tier === "REASONING" && typeof output_tokens === "number" && output_tokens < 50) {
|
|
102
|
+
score -= 10;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tier === "COMPLEX" && typeof latency_ms === "number" && latency_ms < 500) {
|
|
106
|
+
score -= 5;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Clamp to [0, 100]
|
|
110
|
+
return Math.max(0, Math.min(100, score));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { scoreResponseQuality };
|