adaptive-memory-multi-model-router 2.14.21 → 2.14.23
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/.publish-tick +1 -1
- package/README.md +1 -1
- package/README.md.bak +836 -0
- package/benchmark-results.json +25 -25
- package/dist/benchmark/reproducible.d.ts.map +1 -0
- package/dist/cache/semanticCache.d.ts.map +1 -1
- package/dist/cost/costTracker.d.ts.map +1 -1
- package/dist/ensemble.d.ts +12 -2
- package/dist/ensemble.js +11 -6
- package/dist/integrations/oauth.js +3 -3
- package/dist/integrations/oauth.js.map +1 -1
- package/dist/observability/metrics.d.ts +1 -2
- package/dist/observability/metrics.js +2 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/observability/tracer.d.ts +1 -1
- package/dist/observability/tracer.js +2 -0
- package/dist/observability/tracer.js.map +1 -1
- package/dist/providers/providerConfig.d.ts.map +1 -1
- package/dist/routing/advancedRouter.d.ts +1 -0
- package/dist/routing/advancedRouter.d.ts.map +1 -1
- package/dist/routing/advancedRouter.js +77 -6
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/providerHealth.d.ts +0 -1
- package/dist/routing/providerHealth.js +3 -0
- package/dist/routing/providerHealth.js.map +1 -1
- package/dist/routing/providerRetry.js.map +1 -1
- package/dist/routing/routing/advancedRouter.js +90 -17
- package/dist/routing/utils/costUtils.js +149 -0
- package/dist/routing/utils/sorting.js +36 -0
- package/dist/sdk.d.ts +12 -1
- package/dist/server/proxyServer.d.ts.map +1 -1
- package/dist/tui/index.js +4 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/utils/costUtils.d.ts.map +1 -0
- package/dist/utils/sorting.d.ts.map +1 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/package.json +33 -9
- package/src/ensemble.ts +42 -20
- package/src/integrations/oauth.ts +3 -3
- package/src/observability/metrics.ts +1 -1
- package/src/observability/tracer.ts +1 -1
- package/src/routing/advancedRouter.ts +129 -54
- package/src/routing/providerHealth.ts +0 -1
- package/src/routing/providerRetry.ts +1 -1
- package/src/sdk.ts +1 -1
- package/src/tui/index.ts +4 -1
- package/submissions/benchmarks/ALL_PLATFORMS_SUBMISSION.md +94 -0
- package/submissions/benchmarks/LLMROUTERBENCH_SUBMISSION.md +166 -0
- package/submissions/benchmarks/MMRBENCH_SUBMISSION.md +230 -0
- package/submissions/benchmarks/ROUTERARENA_UPDATE.md +112 -0
- package/submissions/benchmarks/ROUTERBENCH_SUBMISSION.md +225 -0
- package/tsconfig.build.json +9 -2
- package/research/PUBLISH_LOG.md +0 -3
package/src/ensemble.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import {
|
|
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);
|
|
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.
|
|
154
|
-
refreshToken: tokens.
|
|
153
|
+
accessToken: tokens.accessToken,
|
|
154
|
+
refreshToken: tokens.refreshToken,
|
|
155
155
|
expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : 0,
|
|
156
|
-
tokenType: tokens.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
275
|
-
|
|
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.
|
|
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;
|
|
@@ -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):
|
|
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('
|
|
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
|
}
|