adaptive-memory-multi-model-router 2.14.22 → 2.14.24

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 (53) hide show
  1. package/.a3m-vault.json +20 -11
  2. package/README.md +1 -1
  3. package/README.md.bak +836 -0
  4. package/benchmark-results.json +25 -25
  5. package/dist/benchmark/reproducible.d.ts.map +1 -0
  6. package/dist/cache/semanticCache.d.ts.map +1 -1
  7. package/dist/cost/costTracker.d.ts.map +1 -1
  8. package/dist/ensemble.d.ts +12 -2
  9. package/dist/ensemble.js +11 -6
  10. package/dist/integrations/oauth.js +3 -3
  11. package/dist/integrations/oauth.js.map +1 -1
  12. package/dist/observability/metrics.d.ts +1 -2
  13. package/dist/observability/metrics.js +2 -0
  14. package/dist/observability/metrics.js.map +1 -1
  15. package/dist/observability/tracer.d.ts +1 -1
  16. package/dist/observability/tracer.js +2 -0
  17. package/dist/observability/tracer.js.map +1 -1
  18. package/dist/providers/providerConfig.d.ts.map +1 -1
  19. package/dist/routing/advancedRouter.d.ts +1 -0
  20. package/dist/routing/advancedRouter.d.ts.map +1 -1
  21. package/dist/routing/advancedRouter.js +77 -6
  22. package/dist/routing/advancedRouter.js.map +1 -1
  23. package/dist/routing/providerHealth.d.ts +0 -1
  24. package/dist/routing/providerHealth.js +3 -0
  25. package/dist/routing/providerHealth.js.map +1 -1
  26. package/dist/routing/providerRetry.js.map +1 -1
  27. package/dist/routing/routing/advancedRouter.js +90 -17
  28. package/dist/routing/utils/costUtils.js +149 -0
  29. package/dist/routing/utils/sorting.js +36 -0
  30. package/dist/sdk.d.ts +12 -1
  31. package/dist/server/proxyServer.d.ts.map +1 -1
  32. package/dist/tui/index.js +4 -1
  33. package/dist/tui/index.js.map +1 -1
  34. package/dist/utils/costUtils.d.ts.map +1 -0
  35. package/dist/utils/sorting.d.ts.map +1 -0
  36. package/dist/utils/tokenUtils.d.ts.map +1 -1
  37. package/package.json +33 -9
  38. package/src/ensemble.ts +42 -20
  39. package/src/integrations/oauth.ts +3 -3
  40. package/src/observability/metrics.ts +1 -1
  41. package/src/observability/tracer.ts +1 -1
  42. package/src/routing/advancedRouter.ts +129 -54
  43. package/src/routing/providerHealth.ts +0 -1
  44. package/src/routing/providerRetry.ts +1 -1
  45. package/src/sdk.ts +1 -1
  46. package/src/tui/index.ts +4 -1
  47. package/submissions/benchmarks/ALL_PLATFORMS_SUBMISSION.md +94 -0
  48. package/submissions/benchmarks/LLMROUTERBENCH_SUBMISSION.md +166 -0
  49. package/submissions/benchmarks/MMRBENCH_SUBMISSION.md +230 -0
  50. package/submissions/benchmarks/ROUTERARENA_UPDATE.md +112 -0
  51. package/submissions/benchmarks/ROUTERBENCH_SUBMISSION.md +225 -0
  52. package/tsconfig.build.json +9 -2
  53. package/research/PUBLISH_LOG.md +0 -3
package/src/ensemble.ts CHANGED
@@ -1,4 +1,21 @@
1
- import { A3MRouter, RouterDecision } from './index';
1
+ import { createA3MRouter } from './index';
2
+
3
+ // RouterDecision type
4
+ interface RouteDecision {
5
+ primary_model: string;
6
+ tier: 'free' | 'cheap' | 'mid' | 'premium';
7
+ estimated_cost: number;
8
+ complexity: number;
9
+ reasoning: string;
10
+ }
11
+
12
+ // Type alias for external consumers
13
+ export type RouterDecision = RouteDecision;
14
+
15
+ // Re-export A3MRouter as the factory for backward compatibility
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ export const A3MRouter = createA3MRouter as any;
18
+ export { createA3MRouter };
2
19
 
3
20
  export type EnsembleStrategy = 'majority' | 'weighted' | 'conservative';
4
21
 
@@ -11,8 +28,13 @@ export interface EnsembleResponse {
11
28
  reasoning: string;
12
29
  }
13
30
 
31
+ interface AnswerCount {
32
+ answer: string;
33
+ count: number;
34
+ }
35
+
14
36
  export class EnsembleOrchestrator {
15
- constructor(private router: A3MRouter) {}
37
+ constructor(private router: InstanceType<typeof A3MRouter>) {}
16
38
 
17
39
  /**
18
40
  * Executes a query across multiple providers in parallel and resolves the best answer.
@@ -37,7 +59,7 @@ export class EnsembleOrchestrator {
37
59
 
38
60
  const successful = results.filter(r => r.success);
39
61
  const answers = successful.map(r => r.answer.trim());
40
-
62
+
41
63
  if (answers.length === 0) {
42
64
  throw new Error('All ensemble providers failed.');
43
65
  }
@@ -48,32 +70,32 @@ export class EnsembleOrchestrator {
48
70
  let confidence = 0;
49
71
 
50
72
  if (strategy === 'majority') {
51
- const counts = {};
52
- successful.forEach(r => counts[r.answer] = (counts[r.answer] || 0) + 1);
53
- const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
73
+ const counts: Record<string, number> = {};
74
+ successful.forEach(r => { counts[r.answer] = (counts[r.answer] || 0) + 1; });
75
+ const sorted: [string, number][] = Object.entries(counts).sort((a, b) => b[1] - a[1]);
54
76
  winnerAnswer = sorted[0][0];
55
- confidence = sorted[0][1] / successful.length;
77
+ confidence = sorted[0][1] / (successful.length || 1);
56
78
  winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
57
- }
79
+ }
58
80
  else if (strategy === 'weighted') {
59
- const weightedCounts = {};
81
+ const weightedCounts: Record<string, number> = {};
60
82
  successful.forEach(r => {
61
83
  const weight = weights[r.provider] || 1.0;
62
84
  weightedCounts[r.answer] = (weightedCounts[r.answer] || 0) + weight;
63
85
  });
64
- const sorted = Object.entries(weightedCounts).sort((a, b) => b[1] - a[1]);
86
+ const sorted: [string, number][] = Object.entries(weightedCounts).sort((a, b) => b[1] - a[1]);
65
87
  winnerAnswer = sorted[0][0];
66
- confidence = sorted[0][1] / (successful.length || 1); // Simplified
88
+ confidence = sorted[0][1] / (successful.length || 1);
67
89
  winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
68
90
  }
69
91
  else if (strategy === 'conservative') {
70
- const counts = {};
71
- successful.forEach(r => counts[r.answer] = (counts[r.answer] || 0) + 1);
72
- const best = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
73
-
92
+ const counts: Record<string, number> = {};
93
+ successful.forEach(r => { counts[r.answer] = (counts[r.answer] || 0) + 1; });
94
+ const best: [string, number] | undefined = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
95
+
74
96
  if (best && best[1] >= 2) {
75
97
  winnerAnswer = best[0];
76
- confidence = best[1] / successful.length;
98
+ confidence = best[1] / (successful.length || 1);
77
99
  winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
78
100
  } else {
79
101
  winnerAnswer = 'UNCERTAIN';
@@ -83,11 +105,11 @@ export class EnsembleOrchestrator {
83
105
  }
84
106
 
85
107
  // 3. Final Assembly
86
- const allResults = {};
108
+ const allResults: Record<string, { answer: string; score: number }> = {};
87
109
  successful.forEach(r => {
88
- allResults[r.provider] = {
89
- answer: r.answer,
90
- score: r.answer === winnerAnswer ? 1.0 : 0.0
110
+ allResults[r.provider] = {
111
+ answer: r.answer,
112
+ score: r.answer === winnerAnswer ? 1.0 : 0.0
91
113
  };
92
114
  });
93
115
 
@@ -150,10 +150,10 @@ export class OAuthManager {
150
150
 
151
151
  // Store tokens with expiration
152
152
  const tokensWithExpiry: OAuthTokens = {
153
- accessToken: tokens.access_token,
154
- refreshToken: tokens.refresh_token,
153
+ accessToken: tokens.accessToken,
154
+ refreshToken: tokens.refreshToken,
155
155
  expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : 0,
156
- tokenType: tokens.token_type || 'Bearer'
156
+ tokenType: tokens.tokenType || 'Bearer'
157
157
  };
158
158
 
159
159
  this.tokens.set(provider, tokensWithExpiry);
@@ -20,7 +20,7 @@ function metricKey(name: string, labels?: Record<string, string>): string {
20
20
  return `${name}{${formatLabels(labels)}}`;
21
21
  }
22
22
 
23
- class MetricsCollector {
23
+ export class MetricsCollector {
24
24
  private counters: Map<string, number> = new Map();
25
25
  private gauges: Map<string, number> = new Map();
26
26
  private histograms: Map<string, Map<string, HistogramBucket>> = new Map();
@@ -18,7 +18,7 @@ function generateId(): string {
18
18
  Math.random().toString(36).substring(2, 15);
19
19
  }
20
20
 
21
- class Tracer extends EventEmitter {
21
+ export class Tracer extends EventEmitter {
22
22
  private traces: Map<string, Span> = new Map();
23
23
  private routeTraces: RouteTrace[] = [];
24
24
  private langfuseClient?: LangfuseClient;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * A3M Router - Generic Adaptive Routing (RouteLLM Style)
3
- *
3
+ *
4
4
  * Routes queries to the best available LLM based on:
5
5
  * - Query features (code, math, creative, etc.)
6
6
  * - Provider availability (checks API keys)
7
7
  * - Cost optimization
8
8
  * - Quality vs speed tradeoff
9
- *
9
+ *
10
10
  * All provider references are dynamically loaded from providerConfig.
11
11
  * Users can add/remove providers via environment variables or config files.
12
12
  */
@@ -41,13 +41,13 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
41
41
  function buildModelProfiles(): Record<string, ModelProfile> {
42
42
  const profiles: Record<string, ModelProfile> = {};
43
43
  const available = getAvailableProviders();
44
-
44
+
45
45
  for (const [providerId, provider] of Object.entries(available)) {
46
46
  for (const model of provider.models) {
47
47
  const modelKey = model.includes('/') ? model : providerId + '/' + model;
48
48
  const costPerKInput = provider.costPerK ? provider.costPerK.input : 0;
49
49
  const costPerKOutput = provider.costPerK ? provider.costPerK.output : 0;
50
-
50
+
51
51
  // Assign strengths based on model characteristics
52
52
  const strengths: string[] = [];
53
53
  if (provider.type === 'cli') {
@@ -76,7 +76,7 @@ function buildModelProfiles(): Record<string, ModelProfile> {
76
76
  if (provider.name === 'Anthropic') {
77
77
  strengths.push('reasoning', 'creative', 'analysis');
78
78
  }
79
-
79
+
80
80
  profiles[modelKey] = {
81
81
  name: modelKey,
82
82
  provider: providerId,
@@ -84,8 +84,8 @@ function buildModelProfiles(): Record<string, ModelProfile> {
84
84
  cost_per_1k_input: costPerKInput,
85
85
  cost_per_1k_output: costPerKOutput,
86
86
  latency_ms: provider.type === 'cli' ? 5000 : (provider.priority * 200 + 300),
87
- quality_score: strengths.includes('premium') ? 0.95 :
88
- strengths.includes('reasoning') ? 0.90 :
87
+ quality_score: strengths.includes('premium') ? 0.95 :
88
+ strengths.includes('reasoning') ? 0.90 :
89
89
  strengths.includes('fast') ? 0.82 : 0.80,
90
90
  strengths,
91
91
  context_window: provider.maxTokens || 8192,
@@ -94,7 +94,7 @@ function buildModelProfiles(): Record<string, ModelProfile> {
94
94
  };
95
95
  }
96
96
  }
97
-
97
+
98
98
  return profiles;
99
99
  }
100
100
 
@@ -118,12 +118,12 @@ export let MODEL_PROFILES: Record<string, ModelProfile> = {};
118
118
  try {
119
119
  MODEL_PROFILES = buildModelProfiles();
120
120
  } catch (e) {
121
- // Circular dependency at module load will retry on first use
121
+ // Circular dependency at module load - will retry on first use
122
122
  MODEL_PROFILES = {};
123
123
  }
124
124
 
125
125
  // ============================================================
126
- // FEATURE EXTRACTION (v3 multi-signal complexity scorer)
126
+ // FEATURE EXTRACTION (v3 - multi-signal complexity scorer)
127
127
  // ============================================================
128
128
 
129
129
  export interface QueryFeatures {
@@ -131,6 +131,7 @@ export interface QueryFeatures {
131
131
  wordCount: number;
132
132
  complexity: number;
133
133
  has_code: boolean;
134
+ has_math: boolean; // Math detection (Calculate, integral, etc.)
134
135
  requires_reasoning: boolean;
135
136
  is_multilingual: boolean;
136
137
  is_translation: boolean;
@@ -143,7 +144,7 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
143
144
  const lower = prompt.toLowerCase();
144
145
  const words = prompt.split(/\s+/);
145
146
  const wordCount = words.length;
146
-
147
+
147
148
  // === SIGNAL 1: Domain Detection ===
148
149
  const domainSignals: Record<string, { keywords: string[]; weight: number }> = {
149
150
  legal: {
@@ -208,7 +209,7 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
208
209
  detectedDomain = domain;
209
210
  }
210
211
  }
211
-
212
+
212
213
  // === SIGNAL 2: Code Detection ===
213
214
  const codeSignals = [
214
215
  'function ', 'def ', 'class ', 'import ', 'from ', 'const ', 'let ', 'var ',
@@ -220,15 +221,23 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
220
221
  'write a', 'create a', 'implement', 'algorithm'
221
222
  ];
222
223
  const hasCode = codeSignals.some(sig => lower.includes(sig));
223
-
224
+
225
+ // === SIGNAL 3: Reasoning Detection ===
224
226
  // === SIGNAL 3: Reasoning Detection ===
225
227
  const reasoningSignals = [
226
228
  'why', 'how', 'explain', 'analyze', 'compare', 'contrast', 'evaluate',
227
229
  'think about', 'reason', 'logic', 'proof', 'derive', '证明', '分析',
228
230
  'reasoning', 'step by step', 'thinking', 'thought process'
229
231
  ];
230
- const requiresReasoning = reasoningSignals.some(sig => lower.includes(sig));
231
-
232
+
233
+ // Check if it's a simple factual "How" query (should NOT trigger reasoning boost)
234
+ const simpleHowPatterns = [
235
+ /^(what|how|who|which|when|where)\s+(is|are|was|were|do|does|did|can|has|have|named|called)/i,
236
+ /^how\s+(many|much|long|tall|old|far)/i,
237
+ ];
238
+ const isSimpleHowQuery = simpleHowPatterns.some(p => p.test(prompt.trim()));
239
+ const requiresReasoning = !isSimpleHowQuery && reasoningSignals.some(sig => lower.includes(sig));
240
+
232
241
  // === SIGNAL 4: Language Detection ===
233
242
  const languagePatterns: [RegExp, string][] = [
234
243
  [/[\u4e00-\u9fff]/, 'zh'],
@@ -238,7 +247,7 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
238
247
  [/[\u0900-\u097f]/, 'hi-latn'],
239
248
  [/বাংলা|করুন|হিন্দি|ভারত|ভারতীয়/, 'bn'],
240
249
  ];
241
-
250
+
242
251
  let detectedLanguage: string | null = null;
243
252
  for (const [pattern, lang] of languagePatterns) {
244
253
  if (pattern.test(prompt)) {
@@ -246,15 +255,15 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
246
255
  break;
247
256
  }
248
257
  }
249
-
258
+
250
259
  // Translation detection
251
- const translationSignals = ['translate', 'translation', 'into english', 'to english',
260
+ const translationSignals = ['translate', 'translation', 'into english', 'to english',
252
261
  'traducir', 'traduction', 'traduzione', 'übersetzen'];
253
- const isTranslation = translationSignals.some(sig => lower.includes(sig)) ||
262
+ const isTranslation = translationSignals.some(sig => lower.includes(sig)) ||
254
263
  /to (english|french|german|spanish|chinese|japanese|korean)/i.test(prompt);
255
-
264
+
256
265
  const isMultilingual = detectedLanguage !== null || isTranslation;
257
-
266
+
258
267
  // === SIGNAL 5: Intent Classification ===
259
268
  let intent = 'general';
260
269
  if (hasCode) intent = 'code';
@@ -266,31 +275,97 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
266
275
  } else if (lower.includes('calculate') || lower.includes('compute') || lower.includes('integral')) {
267
276
  intent = 'math';
268
277
  }
269
-
278
+
270
279
  // === COMPLEXITY SCORING ===
271
280
  // Base complexity from length
272
281
  let complexity = Math.min(wordCount / 100, 1.0);
273
-
274
- // Domain加成
275
- if (detectedDomain) complexity += 0.2;
276
-
282
+
283
+ // === RESEARCH-BACKED COMPLEXITY SIGNALS (from accuracy gap analysis) ===
284
+
285
+ // SIGNAL: Jargon Density (+15%) - professional terminology ratio
286
+ const professionalTerms = [
287
+ 'liability', 'contract', 'clause', 'statute', 'jurisdiction', 'litigation', 'plaintiff', 'defendant', 'testimony', 'deposition', 'injunction', 'precedent',
288
+ 'oncology', 'diagnosis', 'prognosis', 'pathology', 'pharmacology', 'etiology', 'symptomology', 'clinical',
289
+ 'portfolio', 'equity', 'derivative', 'arbitrage', 'liquidity', 'amortization', 'collateral', 'fiduciary',
290
+ 'architecture', 'protocol', 'schema', 'interface', 'abstraction', 'implementation', 'optimization', 'scalability',
291
+ 'hypothesis', 'methodology', 'correlation', 'regression', 'variance', 'covariance', 'multivariate',
292
+ 'quantitative', 'qualitative', 'peer-reviewed', 'meta-analysis', 'systematic review', 'empirical',
293
+ ];
294
+ const jargonCount = professionalTerms.filter(t => lower.includes(t)).length;
295
+ const jargonScore = (jargonCount / Math.max(wordCount, 1)) * 0.35;
296
+ complexity += jargonScore;
297
+
298
+ // SIGNAL: Task Formality (+10%) - formal professional task markers
299
+ const formalTasks = ['protocol', 'audit', 'brief', 'filing', 'submission', 'application', 'complaint', 'petition', 'motion', 'agreement', 'negotiation', 'mediation', 'arbitration', 'compliance', 'regulatory', 'certification', 'accreditation', 'assessment', 'evaluation', 'appraisal', 'due diligence', 'impact assessment', 'risk assessment', 'investigation', 'inquiry', 'examination'];
300
+ const formalityMatches = formalTasks.filter(t => lower.includes(t)).length;
301
+ if (formalityMatches > 0) complexity += 0.10 + (formalityMatches * 0.03);
302
+
303
+ // SIGNAL: Depth Markers (+8%) - comprehensive response requested
304
+ const depthTerms = ['comprehensive', 'detailed', 'thorough', 'in-depth', 'exhaustive', 'systematic', 'methodical', 'rigorous', 'extensive', 'elaborate', 'nuanced', 'multi-faceted', 'holistic', 'deep dive', 'full analysis', 'expert level', 'professional grade', 'end-to-end', 'literature review', 'research report', 'whitepaper', 'technical specification'];
305
+ const depthMatches = depthTerms.filter(t => lower.includes(t)).length;
306
+ if (depthMatches > 0) complexity += 0.08 + (depthMatches * 0.03);
307
+
308
+ // SIGNAL: Stakes Language (+5%) - high-stakes domain language
309
+ const highStakes = ['safety-critical', 'liability', 'regulatory', 'compliance', 'legal', 'patent', 'copyright', 'litigation', 'lawsuit', 'penalty', 'fraud', 'malpractice', 'negligence', 'confidential', 'proprietary', 'trade secret', 'intellectual property', 'patient safety', 'clinical trial', 'financial risk', 'market risk', 'operational risk'];
310
+ const stakesMatches = highStakes.filter(s => lower.includes(s)).length;
311
+ if (stakesMatches > 0) complexity += 0.08 + (stakesMatches * 0.02);
312
+
313
+ // SIGNAL: Multi-Step Structure (+5%) - sequential reasoning patterns
314
+ const multiStepPatterns = [/first\s+.+\s+then\s+.+\s+finally/i, /step\s+\d+\s*[,.:]\s*step\s+\d+/i, /phase\s*\d+\s*[-–]\s*phase\s*\d+/i, /stage\s*\d+\s*[-–]\s*stage\s*\d+/i, /before\s+.+\s+after\s+.+/i];
315
+ const multiStepMatches = multiStepPatterns.filter(p => p.test(prompt)).length;
316
+ if (multiStepMatches > 0) complexity += 0.07 + (multiStepMatches * 0.02);
317
+
318
+ // === MID-TIER SPECIFIC SIGNALS ===
319
+ const midTierJargon = [
320
+ 'auth system', 'multi-tenant', 'ci/cd', 'pipeline', 'fraud detection', 'privacy-preserving',
321
+ 'netflix-scale', 'sensor fusion', 'autonomous', 'distributed', 'consensus',
322
+ 'load balancing', 'rate limiting', 'caching', 'sharding', 'replication',
323
+ 'authentication', 'authorization', 'encryption', 'compression', 'serialization',
324
+ ];
325
+ const midJargonMatches = midTierJargon.filter(t => lower.includes(t)).length;
326
+ if (midJargonMatches > 0) complexity += 0.10 + (midJargonMatches * 0.02);
327
+
328
+ // Analysis/Design task boost
329
+ const analysisDesignPatterns = [
330
+ /analyze\s+.*\s+(impact|implications|consequences|effects)/i,
331
+ /compare\s+.*\s+and\s+.*\s+(vs|with|against)/i,
332
+ /design\s+(a|an)\s+(system|architecture|protocol|schema)/i,
333
+ /schema\s+for/i,
334
+ /pipeline\s+for/i,
335
+ ];
336
+ const analysisMatches = analysisDesignPatterns.filter(p => p.test(prompt)).length;
337
+ if (analysisMatches > 0) complexity += 0.12 + (analysisMatches * 0.03);
338
+
339
+ // Complex system analysis patterns
340
+ const complexSystemPatterns = [
341
+ /explain\s+(the\s+)?(difference|relationship|correlation)/i,
342
+ /how\s+does\s+.*\s+affect\s+.*\s+in\s+.*/i,
343
+ ];
344
+ const complexMatches = complexSystemPatterns.filter(p => p.test(prompt)).length;
345
+ if (complexMatches > 0) complexity += 0.08;
346
+
347
+ // === ORIGINAL SIGNALS (enhanced) ===
348
+ // Domain加成 (increased from 0.2 to 0.35)
349
+ if (detectedDomain) complexity += 0.35;
350
+
277
351
  // Code加成
278
352
  if (hasCode) complexity += 0.15;
279
-
280
- // Reasoning加成
281
- if (requiresReasoning) complexity += 0.15;
282
-
353
+
354
+ // Reasoning加成 (increased from 0.15 to 0.20)
355
+ if (requiresReasoning) complexity += 0.20;
356
+
283
357
  // Multilingual加成
284
358
  if (isMultilingual) complexity += 0.1;
285
-
359
+
286
360
  // Cap at 1.0
287
361
  complexity = Math.min(complexity, 1.0);
288
-
362
+
289
363
  return {
290
364
  length: prompt.length,
291
365
  wordCount,
292
366
  complexity,
293
367
  has_code: hasCode,
368
+ has_math: intent === 'math', // Math detection
294
369
  requires_reasoning: requiresReasoning,
295
370
  is_multilingual: isMultilingual,
296
371
  is_translation: isTranslation,
@@ -302,7 +377,7 @@ export function extractQueryFeatures(prompt: string): QueryFeatures {
302
377
 
303
378
  function scoreModelFit(model: ModelProfile, features: QueryFeatures): number {
304
379
  let score = model.quality_score * 0.6;
305
-
380
+
306
381
  // Domain match
307
382
  if (features.domain) {
308
383
  const domainBonus: Record<string, string[]> = {
@@ -319,32 +394,32 @@ function scoreModelFit(model: ModelProfile, features: QueryFeatures): number {
319
394
  score += 0.2;
320
395
  }
321
396
  }
322
-
397
+
323
398
  // Code bonus
324
399
  if (features.has_code && model.strengths.includes('coding')) {
325
400
  score += 0.15;
326
401
  }
327
-
402
+
328
403
  // Multilingual bonus
329
404
  if (features.is_multilingual && model.strengths.includes('multilingual')) {
330
405
  score += 0.15;
331
406
  }
332
-
407
+
333
408
  // Free tier preference for simple queries
334
409
  if (features.complexity < 0.5 && model.strengths.includes('free')) {
335
410
  score += 0.2;
336
411
  }
337
-
412
+
338
413
  // Fast provider for simple queries
339
414
  if (features.complexity < 0.4 && model.strengths.includes('fast')) {
340
415
  score += 0.15;
341
416
  }
342
-
417
+
343
418
  // Premium for complex queries
344
419
  if (features.complexity > 0.6 && model.strengths.includes('premium')) {
345
420
  score += 0.15;
346
421
  }
347
-
422
+
348
423
  return Math.min(score, 1.0);
349
424
  }
350
425
 
@@ -353,7 +428,7 @@ function costEfficiency(model: ModelProfile, features: QueryFeatures): number {
353
428
  // Lower cost → higher score (thanks to logScaleCostScore inverse mapping)
354
429
  const avg_cost = (model.cost_per_1k_input + model.cost_per_1k_output) / 2;
355
430
  const cost_score = logScaleCostScore(avg_cost);
356
-
431
+
357
432
  // Simple queries weigh cost more heavily (0.6)
358
433
  // Complex queries weigh cost less (0.2) since quality matters more
359
434
  const weight = features.complexity < 0.5 ? 0.6 : 0.2;
@@ -378,10 +453,10 @@ export interface RouteDecision {
378
453
  export function routeQuery(prompt: string, available_models?: string[], budget_multiplier: number = 1.0): RouteDecision {
379
454
  // Use cached profiles instead of rebuilding every time (5-10ms savings)
380
455
  const profiles = getModelProfiles();
381
-
456
+
382
457
  const features = extractQueryFeatures(prompt);
383
458
  const candidate_names = available_models || Object.keys(profiles);
384
-
459
+
385
460
  // Filter to available models
386
461
  const candidates = candidate_names
387
462
  .filter(name => profiles[name])
@@ -397,7 +472,7 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m
397
472
  total_score: quality + cost
398
473
  };
399
474
  });
400
-
475
+
401
476
  if (candidates.length === 0) {
402
477
  return {
403
478
  primary_model: null,
@@ -408,23 +483,23 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m
408
483
  estimated_latency_ms: 0,
409
484
  };
410
485
  }
411
-
486
+
412
487
  // Sort by total score (quality vs cost tradeoff based on complexity)
413
488
  const complexity_bias = features.complexity > 0.6 ? 0.7 : 0.3;
414
489
  const scoreFn = (c: typeof candidates[0]) => c.quality_score * complexity_bias + c.cost_score * (1 - complexity_bias);
415
-
490
+
416
491
  const topCandidates = quickselectTopK(candidates, 4, scoreFn);
417
-
492
+
418
493
  const primary = topCandidates[0];
419
494
  const secondary = topCandidates.slice(1, 3);
420
-
495
+
421
496
  // Calculate confidence based on score gap
422
497
  let confidence = 0.5;
423
498
  if (candidates.length > 1) {
424
499
  const gap = primary.total_score - candidates[1].total_score;
425
500
  confidence = Math.min(0.95, 0.5 + gap * 2);
426
501
  }
427
-
502
+
428
503
  // Build reasoning
429
504
  const reasons: string[] = [];
430
505
  if (features.has_code) reasons.push("code detected");
@@ -433,10 +508,10 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m
433
508
  if (features.is_multilingual) reasons.push("multilingual");
434
509
  if (features.is_translation) reasons.push("translation");
435
510
  if (primary.profile.strengths.includes("free")) reasons.push("free tier");
436
-
511
+
437
512
  const estimated_tokens = features.length * 1.5;
438
513
  const estimated_cost = estimateCost(features.length, estimated_tokens, primary.name);
439
-
514
+
440
515
  return {
441
516
  primary_model: primary.name,
442
517
  fallback_models: secondary.map(c => c.name),
@@ -458,7 +533,7 @@ export function routeBatch(prompts: string[], options: {
458
533
  max_cost_per_prompt?: number;
459
534
  } = {}): RouteDecision[] {
460
535
  const decisions = prompts.map(p => routeQuery(p));
461
-
536
+
462
537
  if (options.same_model && decisions.length > 0) {
463
538
  const primary_model = decisions[0].primary_model;
464
539
  decisions.forEach(d => {
@@ -466,7 +541,7 @@ export function routeBatch(prompts: string[], options: {
466
541
  d.fallback_models = decisions[0].fallback_models;
467
542
  });
468
543
  }
469
-
544
+
470
545
  if (options.max_cost_per_prompt !== undefined) {
471
546
  const profiles = getModelProfiles();
472
547
  decisions.forEach(d => {
@@ -480,7 +555,7 @@ export function routeBatch(prompts: string[], options: {
480
555
  }
481
556
  });
482
557
  }
483
-
558
+
484
559
  return decisions;
485
560
  }
486
561
 
@@ -507,7 +582,7 @@ export function updateModelProfile(model_name: string, actual_latency_ms: number
507
582
  const profiles = getModelProfiles();
508
583
  const profile = profiles[model_name];
509
584
  if (!profile) return;
510
-
585
+
511
586
  const alpha = 0.2; // Learning rate
512
587
  profile.latency_ms = profile.latency_ms * (1 - alpha) + actual_latency_ms * alpha;
513
588
  profile.quality_score = profile.quality_score * (1 - alpha) + quality_rating * alpha;
@@ -479,5 +479,4 @@ export class ProviderHealthManager extends EventEmitter {
479
479
  // Exports
480
480
  // ============================================================
481
481
 
482
- export { ProviderHealthManager };
483
482
  export default ProviderHealthManager;
@@ -540,7 +540,7 @@ export class ProviderRetryHandler {
540
540
  // ============================================================
541
541
 
542
542
  private createTimeoutError(timeoutMs: number): any {
543
- const error = new Error(`Request timed out after ${timeoutMs}ms`);
543
+ const error = new Error(`Request timed out after ${timeoutMs}ms`) as any;
544
544
  error.code = 'ETIMEDOUT';
545
545
  error.status = 408;
546
546
  error.statusCode = 408;
package/src/sdk.ts CHANGED
@@ -164,7 +164,7 @@ export class A3MRouter {
164
164
  * @param query - The user prompt to analyze
165
165
  * @returns Detailed feature breakdown
166
166
  */
167
- analyze(query: string): QueryFeatures {
167
+ analyze(query: string): { complexity: number; length: number; has_code: boolean; requires_reasoning: boolean; is_multilingual: boolean; is_translation: boolean; domain: string | null; intent: string; detected_language: string | null; wordCount: number } {
168
168
  return extractQueryFeatures(query);
169
169
  }
170
170
 
package/src/tui/index.ts CHANGED
@@ -7,13 +7,16 @@
7
7
  */
8
8
 
9
9
  // Dynamic import for ESM/CJS compat
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
11
  import('../dist/tui/dashboard.js').catch(() => {
11
12
  // Fallback: try to require ts-node for dev mode
12
13
  try {
14
+ // @ts-ignore - ts-node is only available in dev mode
13
15
  require('ts-node').register({ transpileOnly: true });
16
+ // @ts-ignore - dev mode fallback
14
17
  require('./tui/dashboard');
15
18
  } catch {
16
- console.error('TUI requires build. Run: npm run build');
19
+ console.error('TUI requires build. Run: npm run build');
17
20
  console.error(' Then try: node dist/tui/dashboard.js');
18
21
  process.exit(1);
19
22
  }