adaptive-memory-multi-model-router 2.2.9 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -902
- package/package.json +1 -1
- package/src/skills/__tests__/skill_manager.test.ts +328 -0
- package/assets/benchmark-results.png +0 -0
- package/assets/complexity-scoring-v2.png +0 -0
- package/assets/complexity-scoring.png +0 -0
- package/assets/cost-comparison-chart.png +0 -0
- package/assets/cost-comparison-v2.png +0 -0
- package/assets/feature-comparison-v2.png +0 -0
- package/assets/feature-comparison-v3.png +0 -0
- package/assets/provider-health-chart.png +0 -0
- package/assets/provider-health-v2.png +0 -0
- package/assets/routing-flow-v2.png +0 -0
- package/assets/routing-flow-v3.png +0 -0
- package/assets/routing-flow.png +0 -0
- package/assets/tier-distribution.png +0 -0
- package/dist/cache/cacheKeyGenerator.d.ts +0 -67
- package/dist/cache/cacheKeyGenerator.d.ts.map +0 -1
- package/dist/cache/cacheKeyGenerator.js +0 -211
- package/dist/cache/cacheKeyGenerator.js.map +0 -1
- package/dist/cost/preCallCostEstimator.d.ts +0 -114
- package/dist/cost/preCallCostEstimator.d.ts.map +0 -1
- package/dist/cost/preCallCostEstimator.js +0 -256
- package/dist/cost/preCallCostEstimator.js.map +0 -1
- package/dist/inference/speculativeDecoding.d.ts +0 -133
- package/dist/inference/speculativeDecoding.d.ts.map +0 -1
- package/dist/inference/speculativeDecoding.js +0 -276
- package/dist/inference/speculativeDecoding.js.map +0 -1
- package/dist/providers/providerHealth.d.ts +0 -117
- package/dist/providers/providerHealth.d.ts.map +0 -1
- package/dist/providers/providerHealth.js +0 -309
- package/dist/providers/providerHealth.js.map +0 -1
- package/dist/routing/difficultyClassifier.d.ts +0 -79
- package/dist/routing/difficultyClassifier.d.ts.map +0 -1
- package/dist/routing/difficultyClassifier.js +0 -329
- package/dist/routing/difficultyClassifier.js.map +0 -1
- package/dist/sdk.d.ts +0 -125
- package/docs/HN_CAMPAIGN.md +0 -785
- package/src/cache/cacheKeyGenerator.ts +0 -242
- package/src/cost/preCallCostEstimator.ts +0 -345
- package/src/inference/speculativeDecoding.ts +0 -373
- package/src/providers/providerHealth.ts +0 -397
- package/src/routing/difficultyClassifier.ts +0 -420
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A3M Router - Cross-Provider Cache Key Generator
|
|
3
|
-
*
|
|
4
|
-
* Normalizes prompts so same semantic content maps to same cache key
|
|
5
|
-
* regardless of provider-specific formatting, system prompts, etc.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const cacheKey = generateCacheKey("What is Python?", { provider: "openai" });
|
|
9
|
-
* const key2 = generateCacheKey("What is Python?", { provider: "anthropic" });
|
|
10
|
-
* // key === key2 (same semantic content = same key)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import * as crypto from 'crypto';
|
|
14
|
-
|
|
15
|
-
// ============================================================
|
|
16
|
-
// Types
|
|
17
|
-
// ============================================================
|
|
18
|
-
|
|
19
|
-
export interface CacheKeyOptions {
|
|
20
|
-
/** Target provider (affects normalization rules) */
|
|
21
|
-
provider?: string;
|
|
22
|
-
/** Target model (for model-specific normalization) */
|
|
23
|
-
model?: string;
|
|
24
|
-
/** Whether to include system prompt in normalization */
|
|
25
|
-
includeSystemPrompt?: boolean;
|
|
26
|
-
/** Custom normalization rules */
|
|
27
|
-
customRules?: NormalizationRule[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface NormalizationRule {
|
|
31
|
-
pattern: RegExp;
|
|
32
|
-
replacement: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface CacheKeyResult {
|
|
36
|
-
/** The normalized cache key string */
|
|
37
|
-
key: string;
|
|
38
|
-
/** Hash of the normalized content */
|
|
39
|
-
hash: string;
|
|
40
|
-
/** Metadata about what was normalized */
|
|
41
|
-
metadata: {
|
|
42
|
-
originalLength: number;
|
|
43
|
-
normalizedLength: number;
|
|
44
|
-
rulesApplied: number;
|
|
45
|
-
provider?: string;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ============================================================
|
|
50
|
-
// Provider-specific system prompt patterns
|
|
51
|
-
// ============================================================
|
|
52
|
-
|
|
53
|
-
const PROVIDER_SYSTEM_PATTERNS: Record<string, RegExp[]> = {
|
|
54
|
-
anthropic: [
|
|
55
|
-
/<anthropic_thinking>[\s\S]*?<\/anthropic_thinking>/gi,
|
|
56
|
-
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
57
|
-
/Human:/gi,
|
|
58
|
-
/Assistant:/gi,
|
|
59
|
-
],
|
|
60
|
-
openai: [
|
|
61
|
-
/<|im_start|>/gi,
|
|
62
|
-
/<|im_end|>/gi,
|
|
63
|
-
],
|
|
64
|
-
google: [
|
|
65
|
-
/<content>[\s\S]*?<\/content>/gi,
|
|
66
|
-
/[\Parts|thought]/gi,
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// ============================================================
|
|
71
|
-
// Core normalizer
|
|
72
|
-
// ============================================================
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Normalize text for cross-provider cache key generation.
|
|
76
|
-
* Removes provider-specific formatting while preserving semantic content.
|
|
77
|
-
*/
|
|
78
|
-
export function normalizeForCacheKey(
|
|
79
|
-
text: string,
|
|
80
|
-
options: CacheKeyOptions = {}
|
|
81
|
-
): string {
|
|
82
|
-
let normalized = text;
|
|
83
|
-
|
|
84
|
-
// Step 1: Unicode normalization (NFC)
|
|
85
|
-
normalized = normalized.normalize('NFC');
|
|
86
|
-
|
|
87
|
-
// Step 2: Collapse whitespace
|
|
88
|
-
normalized = normalized.replace(/\s+/g, ' ');
|
|
89
|
-
|
|
90
|
-
// Step 3: Remove control characters
|
|
91
|
-
normalized = normalized.replace(/[\x00-\x1F\x7F]/g, '');
|
|
92
|
-
|
|
93
|
-
// Step 4: Strip provider-specific formatting
|
|
94
|
-
if (options.provider) {
|
|
95
|
-
const patterns = PROVIDER_SYSTEM_PATTERNS[options.provider] || [];
|
|
96
|
-
for (const pattern of patterns) {
|
|
97
|
-
normalized = normalized.replace(pattern, '');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Step 5: General system/assistant role removal
|
|
102
|
-
normalized = normalized
|
|
103
|
-
.replace(/\b(system|user|assistant|human|bot)\s*:/gi, '')
|
|
104
|
-
.replace(/^(system|user|assistant|human|bot)\s*/gim, '');
|
|
105
|
-
|
|
106
|
-
// Step 6: Remove markdown formatting (often provider-specific)
|
|
107
|
-
normalized = normalized
|
|
108
|
-
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK]') // Preserve code block indicator
|
|
109
|
-
.replace(/`([^`]+)`/g, '$1') // Inline code content
|
|
110
|
-
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
|
|
111
|
-
.replace(/_([^_]+)_/g, '$1') // Italic
|
|
112
|
-
.replace(/#+\s*/g, '') // Headers
|
|
113
|
-
.replace(/^\s*[-*+]\s+/gm, '') // List bullets
|
|
114
|
-
.replace(/^\s*\d+\.\s+/gm, ''); // Numbered lists
|
|
115
|
-
|
|
116
|
-
// Step 7: Apply custom rules
|
|
117
|
-
if (options.customRules) {
|
|
118
|
-
for (const rule of options.customRules) {
|
|
119
|
-
normalized = normalized.replace(rule.pattern, rule.replacement);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Step 8: Collapse whitespace again after removals
|
|
124
|
-
normalized = normalized.replace(/\s+/g, ' ').trim();
|
|
125
|
-
|
|
126
|
-
return normalized;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Generate a deterministic cache key from a query.
|
|
131
|
-
* Same semantic content = same key across providers.
|
|
132
|
-
*/
|
|
133
|
-
export function generateCacheKey(
|
|
134
|
-
query: string,
|
|
135
|
-
options: CacheKeyOptions = {}
|
|
136
|
-
): CacheKeyResult {
|
|
137
|
-
const originalLength = query.length;
|
|
138
|
-
|
|
139
|
-
// Normalize the query
|
|
140
|
-
let normalized = normalizeForCacheKey(query, {
|
|
141
|
-
...options,
|
|
142
|
-
includeSystemPrompt: false, // Always exclude for user query matching
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Count rules that were applied (approximate)
|
|
146
|
-
let rulesApplied = 3; // Base normalizations
|
|
147
|
-
if (options.provider) rulesApplied += 2;
|
|
148
|
-
if (options.customRules) rulesApplied += options.customRules.length;
|
|
149
|
-
|
|
150
|
-
// Generate hash
|
|
151
|
-
const hash = crypto
|
|
152
|
-
.createHash('sha256')
|
|
153
|
-
.update(normalized)
|
|
154
|
-
.digest('hex')
|
|
155
|
-
.substring(0, 16); // First 16 chars = 64-bit key
|
|
156
|
-
|
|
157
|
-
// Final key format: v1:{hash}:{provider?[:model]?}
|
|
158
|
-
let key = `v1:${hash}`;
|
|
159
|
-
if (options.provider) {
|
|
160
|
-
key += `:${options.provider}`;
|
|
161
|
-
if (options.model) {
|
|
162
|
-
key += `:${options.model}`;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
key,
|
|
168
|
-
hash,
|
|
169
|
-
metadata: {
|
|
170
|
-
originalLength,
|
|
171
|
-
normalizedLength: normalized.length,
|
|
172
|
-
rulesApplied,
|
|
173
|
-
provider: options.provider,
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ============================================================
|
|
179
|
-
// SemanticCache enhancement
|
|
180
|
-
// ============================================================
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Add cross-provider cache key methods to existing SemanticCache.
|
|
184
|
-
* Call this to enhance the cache with provider-normalized lookups.
|
|
185
|
-
*/
|
|
186
|
-
export function createCacheKeyGenerator(
|
|
187
|
-
defaultOptions?: CacheKeyOptions
|
|
188
|
-
): {
|
|
189
|
-
generateKey: (query: string, options?: CacheKeyOptions) => CacheKeyResult;
|
|
190
|
-
createNormalizedMatcher: (cache: Map<string, any>) => (query: string, options?: CacheKeyOptions) => string | null;
|
|
191
|
-
} {
|
|
192
|
-
return {
|
|
193
|
-
/**
|
|
194
|
-
* Generate a cache key for a query.
|
|
195
|
-
*/
|
|
196
|
-
generateKey: (query: string, options?: CacheKeyOptions): CacheKeyResult => {
|
|
197
|
-
return generateCacheKey(query, { ...defaultOptions, ...options });
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Create a matcher function that finds existing cache entries
|
|
202
|
-
* by comparing normalized keys.
|
|
203
|
-
*/
|
|
204
|
-
createNormalizedMatcher: (cache: Map<string, any>) => {
|
|
205
|
-
return (query: string, options?: CacheKeyOptions): string | null => {
|
|
206
|
-
const { key } = generateCacheKey(query, { ...defaultOptions, ...options });
|
|
207
|
-
|
|
208
|
-
// Check exact match
|
|
209
|
-
if (cache.has(key)) {
|
|
210
|
-
return key;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Check hash-only match (v1:{hash} prefix)
|
|
214
|
-
const hashPrefix = key.split(':').slice(0, 2).join(':');
|
|
215
|
-
for (const cachedKey of cache.keys()) {
|
|
216
|
-
if (cachedKey.startsWith(hashPrefix + ':')) {
|
|
217
|
-
return cachedKey;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return null;
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ============================================================
|
|
228
|
-
// Convenience exports
|
|
229
|
-
// ============================================================
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Quick cache key generation (simplified API).
|
|
233
|
-
* Use this for simple cross-provider cache lookups.
|
|
234
|
-
*
|
|
235
|
-
* @example
|
|
236
|
-
* const key1 = toCacheKey("What is Python?", "openai");
|
|
237
|
-
* const key2 = toCacheKey("What is Python?", "anthropic");
|
|
238
|
-
* console.log(key1 === key2); // true
|
|
239
|
-
*/
|
|
240
|
-
export function toCacheKey(query: string, provider?: string): string {
|
|
241
|
-
return generateCacheKey(query, { provider }).key;
|
|
242
|
-
}
|
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A3M Router - Pre-Call Cost Estimator
|
|
3
|
-
*
|
|
4
|
-
* Estimates cost BEFORE making an API call based on input features.
|
|
5
|
-
* Uses historical data patterns and token count estimation via character ratio.
|
|
6
|
-
*
|
|
7
|
-
* No external API calls - all estimation is local.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* const estimator = new PreCallCostEstimator();
|
|
11
|
-
* const estimate = estimator.estimate({
|
|
12
|
-
* query: "What is Python?",
|
|
13
|
-
* modelTier: 'mid',
|
|
14
|
-
* provider: 'groq'
|
|
15
|
-
* });
|
|
16
|
-
* console.log(estimate); // { estimatedTokens: 24, estimatedCost: 0.0014, estimatedLatency: 200 }
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { ProviderTier } from '../providers/providerConfig';
|
|
20
|
-
|
|
21
|
-
// ============================================================
|
|
22
|
-
// Types
|
|
23
|
-
// ============================================================
|
|
24
|
-
|
|
25
|
-
export interface CostEstimateInput {
|
|
26
|
-
/** Query text to estimate tokens for */
|
|
27
|
-
query: string;
|
|
28
|
-
/** Model tier (free|cheap|mid|premium|enterprise) */
|
|
29
|
-
modelTier?: ProviderTier;
|
|
30
|
-
/** Provider ID for provider-specific cost lookup */
|
|
31
|
-
provider?: string;
|
|
32
|
-
/** Optional: explicit token count (if already known) */
|
|
33
|
-
explicitTokens?: number;
|
|
34
|
-
/** System prompt length (if using a system prompt) */
|
|
35
|
-
systemPromptLength?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface CostEstimate {
|
|
39
|
-
/** Estimated total tokens (input + output) */
|
|
40
|
-
estimatedTokens: number;
|
|
41
|
-
/** Estimated input tokens */
|
|
42
|
-
estimatedInputTokens: number;
|
|
43
|
-
/** Estimated output tokens */
|
|
44
|
-
estimatedOutputTokens: number;
|
|
45
|
-
/** Estimated cost in USD */
|
|
46
|
-
estimatedCost: number;
|
|
47
|
-
/** Estimated latency in milliseconds */
|
|
48
|
-
estimatedLatency: number;
|
|
49
|
-
/** Confidence score 0-1 */
|
|
50
|
-
confidence: number;
|
|
51
|
-
/** Breakdown of estimation */
|
|
52
|
-
breakdown: {
|
|
53
|
-
inputCostPerM: number;
|
|
54
|
-
outputCostPerM: number;
|
|
55
|
-
charToTokenRatio: number;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ProviderCostConfig {
|
|
60
|
-
input: number; // per 1M tokens
|
|
61
|
-
output: number; // per 1M tokens
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Default latency estimates per tier (ms)
|
|
65
|
-
const TIER_LATENCY: Record<ProviderTier, { min: number; max: number; avg: number }> = {
|
|
66
|
-
free: { min: 500, max: 5000, avg: 2000 },
|
|
67
|
-
cheap: { min: 100, max: 800, avg: 300 },
|
|
68
|
-
mid: { min: 200, max: 1500, avg: 600 },
|
|
69
|
-
premium:{ min: 300, max: 2000, avg: 800 },
|
|
70
|
-
enterprise: { min: 200, max: 1500, avg: 500 },
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// ============================================================
|
|
74
|
-
// PreCallCostEstimator
|
|
75
|
-
// ============================================================
|
|
76
|
-
|
|
77
|
-
export class PreCallCostEstimator {
|
|
78
|
-
// Historical data for regression: [charCount, actualTokens][] tuples
|
|
79
|
-
private historicalData: Array<{ chars: number; tokens: number }> = [];
|
|
80
|
-
// Linear regression coefficients
|
|
81
|
-
private slope = 0.25; // chars per token ratio
|
|
82
|
-
private intercept = 10;
|
|
83
|
-
// EWMA for latency estimation
|
|
84
|
-
private latencyHistory: number[] = [];
|
|
85
|
-
private ewmaAlpha = 0.3;
|
|
86
|
-
|
|
87
|
-
constructor(historicalData?: Array<{ chars: number; tokens: number }>) {
|
|
88
|
-
if (historicalData && historicalData.length > 0) {
|
|
89
|
-
this.fitLinearRegression(historicalData);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Main estimation method - estimates tokens, cost, and latency.
|
|
95
|
-
*/
|
|
96
|
-
estimate(input: CostEstimateInput): CostEstimate {
|
|
97
|
-
const {
|
|
98
|
-
query,
|
|
99
|
-
modelTier = 'mid',
|
|
100
|
-
provider,
|
|
101
|
-
explicitTokens,
|
|
102
|
-
systemPromptLength = 0,
|
|
103
|
-
} = input;
|
|
104
|
-
|
|
105
|
-
// Token estimation
|
|
106
|
-
let inputTokens: number;
|
|
107
|
-
if (explicitTokens !== undefined) {
|
|
108
|
-
inputTokens = explicitTokens;
|
|
109
|
-
} else {
|
|
110
|
-
inputTokens = this.estimateTokens(query, systemPromptLength);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Output token estimation based on query complexity
|
|
114
|
-
const outputTokens = this.estimateOutputTokens(query, inputTokens);
|
|
115
|
-
|
|
116
|
-
const totalTokens = inputTokens + outputTokens;
|
|
117
|
-
|
|
118
|
-
// Cost estimation
|
|
119
|
-
const costConfig = this.getCostConfig(provider, modelTier);
|
|
120
|
-
const estimatedCost = this.calculateCost(totalTokens, costConfig);
|
|
121
|
-
|
|
122
|
-
// Latency estimation
|
|
123
|
-
const estimatedLatency = this.estimateLatency(modelTier, inputTokens, totalTokens);
|
|
124
|
-
|
|
125
|
-
// Confidence based on amount of historical data we have
|
|
126
|
-
const confidence = this.calculateConfidence();
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
estimatedTokens: totalTokens,
|
|
130
|
-
estimatedInputTokens: inputTokens,
|
|
131
|
-
estimatedOutputTokens: outputTokens,
|
|
132
|
-
estimatedCost: Math.round(estimatedCost * 1000000) / 1000000, // 6 decimal places
|
|
133
|
-
estimatedLatency,
|
|
134
|
-
confidence,
|
|
135
|
-
breakdown: {
|
|
136
|
-
inputCostPerM: costConfig.input,
|
|
137
|
-
outputCostPerM: costConfig.output,
|
|
138
|
-
charToTokenRatio: this.slope,
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Estimate input tokens using character-to-token ratio.
|
|
145
|
-
* Uses linear regression if historical data is available.
|
|
146
|
-
*/
|
|
147
|
-
estimateTokens(text: string, systemPromptLength = 0): number {
|
|
148
|
-
const totalChars = text.length + systemPromptLength;
|
|
149
|
-
|
|
150
|
-
if (this.historicalData.length >= 5) {
|
|
151
|
-
// Use linear regression
|
|
152
|
-
return Math.max(1, Math.round(this.slope * totalChars + this.intercept));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Fallback: general English average ~4 chars per token
|
|
156
|
-
return Math.max(1, Math.round(totalChars / 4));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Estimate output tokens based on query complexity.
|
|
161
|
-
* More complex queries (code, analysis) tend to need more output.
|
|
162
|
-
*/
|
|
163
|
-
estimateOutputTokens(query: string, inputTokens: number): number {
|
|
164
|
-
const lower = query.toLowerCase();
|
|
165
|
-
|
|
166
|
-
// Base estimate: ~30% of input tokens as output
|
|
167
|
-
let multiplier = 0.3;
|
|
168
|
-
|
|
169
|
-
// Complexity adjustments
|
|
170
|
-
if (this.containsKeyword(lower, ['code', 'implement', 'function', 'class', 'algorithm'])) {
|
|
171
|
-
multiplier = 0.5; // Code needs more output
|
|
172
|
-
} else if (this.containsKeyword(lower, ['explain', 'describe', 'what is', 'how does'])) {
|
|
173
|
-
multiplier = 0.35; // Explanations need moderate output
|
|
174
|
-
} else if (this.containsKeyword(lower, ['list', 'count', 'find all'])) {
|
|
175
|
-
multiplier = 0.4; // List queries need more output
|
|
176
|
-
} else if (this.containsKeyword(lower, ['yes', 'no', 'is', 'are', 'does'])) {
|
|
177
|
-
multiplier = 0.1; // Simple questions need minimal output
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Cap to reasonable bounds
|
|
181
|
-
return Math.min(
|
|
182
|
-
Math.max(10, Math.round(inputTokens * multiplier)),
|
|
183
|
-
4000 // Max 4k output tokens
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Calculate cost in USD.
|
|
189
|
-
*/
|
|
190
|
-
calculateCost(totalTokens: number, config: ProviderCostConfig): number {
|
|
191
|
-
const inputM = this.estimateInputTokensFromTotal(totalTokens) / 1_000_000;
|
|
192
|
-
const outputM = this.estimateOutputTokensFromTotal(totalTokens) / 1_000_000;
|
|
193
|
-
return inputM * config.input + outputM * config.output;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Estimate latency in milliseconds.
|
|
198
|
-
*/
|
|
199
|
-
estimateLatency(tier: ProviderTier, inputTokens: number, totalTokens: number): number {
|
|
200
|
-
const tierLatency = TIER_LATENCY[tier];
|
|
201
|
-
|
|
202
|
-
// Base latency from tier
|
|
203
|
-
let latency = tierLatency.avg;
|
|
204
|
-
|
|
205
|
-
// Scale by token count (rough linear approximation)
|
|
206
|
-
const tokenScale = totalTokens / 100;
|
|
207
|
-
latency *= Math.max(0.5, Math.min(3, tokenScale));
|
|
208
|
-
|
|
209
|
-
// Adjust for historical EWMA if available
|
|
210
|
-
if (this.latencyHistory.length > 0) {
|
|
211
|
-
const ewmaLatency = this.calculateEwma();
|
|
212
|
-
latency = latency * 0.7 + ewmaLatency * 0.3;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return Math.round(latency);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Record actual tokens for future regression improvements.
|
|
220
|
-
*/
|
|
221
|
-
recordActualTokens(queryLength: number, actualTokens: number): void {
|
|
222
|
-
this.historicalData.push({ chars: queryLength, tokens: actualTokens });
|
|
223
|
-
|
|
224
|
-
// Keep only last 100 data points
|
|
225
|
-
if (this.historicalData.length > 100) {
|
|
226
|
-
this.historicalData.shift();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Refit regression periodically
|
|
230
|
-
if (this.historicalData.length % 10 === 0) {
|
|
231
|
-
this.fitLinearRegression(this.historicalData);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Record actual latency for EWMA updates.
|
|
237
|
-
*/
|
|
238
|
-
recordActualLatency(latencyMs: number): void {
|
|
239
|
-
this.latencyHistory.push(latencyMs);
|
|
240
|
-
if (this.latencyHistory.length > 50) {
|
|
241
|
-
this.latencyHistory.shift();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Fit linear regression to historical data.
|
|
247
|
-
* Uses ordinary least squares.
|
|
248
|
-
*/
|
|
249
|
-
fitLinearRegression(data: Array<{ chars: number; tokens: number }>): void {
|
|
250
|
-
if (data.length < 2) return;
|
|
251
|
-
|
|
252
|
-
const n = data.length;
|
|
253
|
-
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
254
|
-
|
|
255
|
-
for (const { chars, tokens } of data) {
|
|
256
|
-
sumX += chars;
|
|
257
|
-
sumY += tokens;
|
|
258
|
-
sumXY += chars * tokens;
|
|
259
|
-
sumX2 += chars * chars;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const denominator = n * sumX2 - sumX * sumX;
|
|
263
|
-
if (denominator === 0) return;
|
|
264
|
-
|
|
265
|
-
this.slope = (n * sumXY - sumX * sumY) / denominator;
|
|
266
|
-
this.intercept = (sumY - this.slope * sumX) / n;
|
|
267
|
-
|
|
268
|
-
// Sanity check
|
|
269
|
-
if (this.slope <= 0 || this.slope > 1) {
|
|
270
|
-
this.slope = 0.25; // Reset to default if outlier
|
|
271
|
-
this.intercept = 10;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ---- Private helpers ----
|
|
276
|
-
|
|
277
|
-
private getCostConfig(provider?: string, tier?: ProviderTier): ProviderCostConfig {
|
|
278
|
-
// Provider-specific costs (from providerConfig.ts patterns)
|
|
279
|
-
const providerCosts: Partial<Record<string, ProviderCostConfig>> = {
|
|
280
|
-
groq: { input: 0.59, output: 0.79 },
|
|
281
|
-
cerebras: { input: 0.60, output: 0.60 },
|
|
282
|
-
deepseek: { input: 0.14, output: 0.28 },
|
|
283
|
-
deepinfra: { input: 0.05, output: 0.05 },
|
|
284
|
-
together: { input: 0.18, output: 0.18 },
|
|
285
|
-
fireworks: { input: 0.20, output: 0.20 },
|
|
286
|
-
mistral: { input: 0.20, output: 0.60 },
|
|
287
|
-
openai: { input: 2.50, output: 10.00 },
|
|
288
|
-
anthropic: { input: 3.00, output: 15.00 },
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
if (provider && providerCosts[provider]) {
|
|
292
|
-
return providerCosts[provider]!;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Tier fallback
|
|
296
|
-
const tierCosts: Record<ProviderTier, ProviderCostConfig> = {
|
|
297
|
-
free: { input: 0, output: 0 },
|
|
298
|
-
cheap: { input: 0.20, output: 0.40 },
|
|
299
|
-
mid: { input: 1.00, output: 3.00 },
|
|
300
|
-
premium: { input: 3.00, output: 12.00 },
|
|
301
|
-
enterprise: { input: 5.00, output: 20.00 },
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
return tier ? tierCosts[tier] : tierCosts.mid;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private containsKeyword(text: string, keywords: string[]): boolean {
|
|
308
|
-
return keywords.some(kw => text.includes(kw));
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private calculateEwma(): number {
|
|
312
|
-
if (this.latencyHistory.length === 0) return 0;
|
|
313
|
-
let ewma = this.latencyHistory[0];
|
|
314
|
-
for (let i = 1; i < this.latencyHistory.length; i++) {
|
|
315
|
-
ewma = this.ewmaAlpha * this.latencyHistory[i] + (1 - this.ewmaAlpha) * ewma;
|
|
316
|
-
}
|
|
317
|
-
return ewma;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private calculateConfidence(): number {
|
|
321
|
-
// More historical data = higher confidence
|
|
322
|
-
const dataFactor = Math.min(this.historicalData.length / 50, 1.0);
|
|
323
|
-
// More latency history = higher confidence
|
|
324
|
-
const latencyFactor = Math.min(this.latencyHistory.length / 20, 1.0);
|
|
325
|
-
return Math.round((dataFactor * 0.6 + latencyFactor * 0.4) * 100) / 100;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private estimateInputTokensFromTotal(total: number): number {
|
|
329
|
-
return Math.round(total * 0.7); // Assume 70% input
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private estimateOutputTokensFromTotal(total: number): number {
|
|
333
|
-
return Math.round(total * 0.3); // Assume 30% output
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ============================================================
|
|
338
|
-
// Factory
|
|
339
|
-
// ============================================================
|
|
340
|
-
|
|
341
|
-
export function createPreCallCostEstimator(
|
|
342
|
-
historicalData?: Array<{ chars: number; tokens: number }>
|
|
343
|
-
): PreCallCostEstimator {
|
|
344
|
-
return new PreCallCostEstimator(historicalData);
|
|
345
|
-
}
|