adaptive-memory-multi-model-router 2.2.5 → 2.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +149 -116
  2. package/README.md.bak +836 -0
  3. package/assets/benchmark-results.png +0 -0
  4. package/assets/complexity-scoring-v2.png +0 -0
  5. package/assets/complexity-scoring.png +0 -0
  6. package/assets/cost-comparison-chart.png +0 -0
  7. package/assets/cost-comparison-v2.png +0 -0
  8. package/assets/feature-comparison-v2.png +0 -0
  9. package/assets/feature-comparison-v3.png +0 -0
  10. package/assets/provider-health-chart.png +0 -0
  11. package/assets/provider-health-v2.png +0 -0
  12. package/assets/routing-flow-v2.png +0 -0
  13. package/assets/routing-flow-v3.png +0 -0
  14. package/assets/routing-flow.png +0 -0
  15. package/assets/tier-distribution.png +0 -0
  16. package/benchmark-results.json +620 -46
  17. package/dist/cache/cacheKeyGenerator.d.ts +67 -0
  18. package/dist/cache/cacheKeyGenerator.d.ts.map +1 -0
  19. package/dist/cache/cacheKeyGenerator.js +211 -0
  20. package/dist/cache/cacheKeyGenerator.js.map +1 -0
  21. package/dist/cli.js +0 -0
  22. package/dist/cost/preCallCostEstimator.d.ts +114 -0
  23. package/dist/cost/preCallCostEstimator.d.ts.map +1 -0
  24. package/dist/cost/preCallCostEstimator.js +256 -0
  25. package/dist/cost/preCallCostEstimator.js.map +1 -0
  26. package/dist/inference/speculativeDecoding.d.ts +133 -0
  27. package/dist/inference/speculativeDecoding.d.ts.map +1 -0
  28. package/dist/inference/speculativeDecoding.js +276 -0
  29. package/dist/inference/speculativeDecoding.js.map +1 -0
  30. package/dist/providers/providerHealth.d.ts +117 -0
  31. package/dist/providers/providerHealth.d.ts.map +1 -0
  32. package/dist/providers/providerHealth.js +309 -0
  33. package/dist/providers/providerHealth.js.map +1 -0
  34. package/dist/routing/difficultyClassifier.d.ts +79 -0
  35. package/dist/routing/difficultyClassifier.d.ts.map +1 -0
  36. package/dist/routing/difficultyClassifier.js +329 -0
  37. package/dist/routing/difficultyClassifier.js.map +1 -0
  38. package/dist/sdk.d.ts +125 -0
  39. package/dist/sdk.d.ts.map +1 -0
  40. package/dist/sdk.js.map +1 -0
  41. package/package.json +2 -322
  42. package/scripts/run-mmlu-benchmark.js +176 -0
  43. package/scripts/run-provider-benchmark.js +244 -0
  44. package/src/cache/cacheKeyGenerator.ts +242 -0
  45. package/src/cost/preCallCostEstimator.ts +345 -0
  46. package/src/inference/speculativeDecoding.ts +373 -0
  47. package/src/providers/providerHealth.ts +397 -0
  48. package/src/routing/difficultyClassifier.ts +420 -0
  49. package/test/provider-test.js +69 -90
  50. package/test.js +43 -69
  51. package/test.js.bak +376 -0
  52. package/src/skills/__tests__/skill_manager.test.ts +0 -328
@@ -0,0 +1,345 @@
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
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * A3M Router - Speculative Decoding Interface
3
+ *
4
+ * Interface for integrating speculative decoding.
5
+ * Currently a stub/interface for future Medusa/Lookahead integration.
6
+ *
7
+ * Speculative decoding uses a smaller "draft" model to predict
8
+ * multiple tokens ahead, which are then verified in parallel by
9
+ * the main model. This can provide 2-3x speedup in generation.
10
+ *
11
+ * Usage:
12
+ * const specDec = new SpeculativeDecoding();
13
+ * if (specDec.shouldUse(true)) {
14
+ * const draftModel = specDec.getDraftModel('medusa');
15
+ * // ... use draft model to generate and verify
16
+ * }
17
+ */
18
+
19
+ import { ProviderTier } from '../providers/providerConfig';
20
+
21
+ // ============================================================
22
+ // Types
23
+ // ============================================================
24
+
25
+ export type DraftModelType = 'medusa' | 'lookahead' | 'eagle' | 'spark';
26
+
27
+ export interface SpeculativeConfig {
28
+ /** Enable speculative decoding */
29
+ enabled: boolean;
30
+ /** Draft model type */
31
+ draftModelType?: DraftModelType;
32
+ /** Number of speculative tokens to generate */
33
+ speculationWindow?: number;
34
+ /** Temperature for draft model */
35
+ temperature?: number;
36
+ /** Provider to use for draft model */
37
+ provider?: string;
38
+ /** Model to use for draft model */
39
+ model?: string;
40
+ }
41
+
42
+ export interface DraftModel {
43
+ /** Model identifier */
44
+ id: string;
45
+ /** Model type */
46
+ type: DraftModelType;
47
+ /** Provider for this model */
48
+ provider: string;
49
+ /** Model size description */
50
+ size: string;
51
+ /** Supported speculation windows */
52
+ supportedWindows: number[];
53
+ }
54
+
55
+ export interface SpeculativeResult {
56
+ /** Whether speculative decoding was used */
57
+ used: boolean;
58
+ /** Number of tokens in draft */
59
+ draftTokens: number;
60
+ /** Number of tokens accepted */
61
+ acceptedTokens: number;
62
+ /** Acceptance rate */
63
+ acceptanceRate: number;
64
+ /** Time saved (ms, estimated) */
65
+ estimatedTimeSaved: number;
66
+ }
67
+
68
+ export interface VerificationResult {
69
+ /** All tokens verified successfully */
70
+ allAccepted: boolean;
71
+ /** Indices of accepted tokens */
72
+ acceptedIndices: number[];
73
+ /** Indices of rejected tokens */
74
+ rejectedIndices: number[];
75
+ /** Actual tokens to use (may differ from draft) */
76
+ actualTokens: string[];
77
+ /** Number of tokens to rewind */
78
+ rewindCount: number;
79
+ }
80
+
81
+ // ============================================================
82
+ // Speculative Decoding Interface
83
+ // ============================================================
84
+
85
+ export class SpeculativeDecoding {
86
+ private config: SpeculativeConfig;
87
+ private draftModels: Map<string, DraftModel> = new Map();
88
+
89
+ constructor(config?: Partial<SpeculativeConfig>) {
90
+ this.config = {
91
+ enabled: config?.enabled ?? false,
92
+ draftModelType: config?.draftModelType ?? 'medusa',
93
+ speculationWindow: config?.speculationWindow ?? 4,
94
+ temperature: config?.temperature ?? 0.0,
95
+ provider: config?.provider,
96
+ model: config?.model,
97
+ };
98
+
99
+ // Register available draft models
100
+ this.registerDraftModels();
101
+ }
102
+
103
+ /**
104
+ * Determine if speculative decoding should be used.
105
+ * Checks config, provider support, and model availability.
106
+ */
107
+ shouldUse(forceEnable?: boolean): boolean {
108
+ if (forceEnable) return true;
109
+ if (!this.config.enabled) return false;
110
+
111
+ // Speculative decoding beneficial for longer outputs
112
+ // Check if provider supports it
113
+ return this.config.enabled;
114
+ }
115
+
116
+ /**
117
+ * Get the draft model configuration.
118
+ */
119
+ getDraftModel(type?: DraftModelType): DraftModel | null {
120
+ const modelType = type || this.config.draftModelType || 'medusa';
121
+
122
+ const model = this.draftModels.get(modelType);
123
+ if (!model) {
124
+ // Return a generic configuration
125
+ return {
126
+ id: `draft-${modelType}`,
127
+ type: modelType,
128
+ provider: this.config.provider || 'auto',
129
+ size: 'small',
130
+ supportedWindows: [2, 4, 6, 8],
131
+ };
132
+ }
133
+
134
+ return model;
135
+ }
136
+
137
+ /**
138
+ * Generate draft tokens using the draft model.
139
+ * This is an async method to support API-based draft models.
140
+ */
141
+ async generateDraft(
142
+ prompt: string,
143
+ maxTokens: number
144
+ ): Promise<{ tokens: string[]; scores: number[] }> {
145
+ const draftModel = this.getDraftModel();
146
+ if (!draftModel) {
147
+ throw new Error('No draft model available');
148
+ }
149
+
150
+ // Stub: In production, this would call the draft model API
151
+ // For now, return empty draft
152
+ return {
153
+ tokens: [],
154
+ scores: [],
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Verify draft tokens against the main model.
160
+ * Returns which tokens were accepted and corrections if needed.
161
+ */
162
+ async verifyDraft(
163
+ prompt: string,
164
+ draftTokens: string[]
165
+ ): Promise<VerificationResult> {
166
+ if (draftTokens.length === 0) {
167
+ return {
168
+ allAccepted: true,
169
+ acceptedIndices: [],
170
+ rejectedIndices: [],
171
+ actualTokens: [],
172
+ rewindCount: 0,
173
+ };
174
+ }
175
+
176
+ // Stub: In production, this would verify each draft token
177
+ // For now, accept all tokens (optimistic)
178
+ return {
179
+ allAccepted: true,
180
+ acceptedIndices: draftTokens.map((_, i) => i),
181
+ rejectedIndices: [],
182
+ actualTokens: draftTokens,
183
+ rewindCount: 0,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Calculate speedup from speculative decoding results.
189
+ */
190
+ calculateSpeedup(result: SpeculativeResult): number {
191
+ if (!result.used || result.draftTokens === 0) return 1.0;
192
+
193
+ // Approximate speedup based on acceptance rate
194
+ // Higher acceptance = more speedup
195
+ const acceptanceRate = result.acceptanceRate;
196
+
197
+ // Theoretical 2-3x speedup at 90%+ acceptance
198
+ if (acceptanceRate >= 0.9) return 2.5;
199
+ if (acceptanceRate >= 0.8) return 2.0;
200
+ if (acceptanceRate >= 0.7) return 1.7;
201
+ if (acceptanceRate >= 0.5) return 1.4;
202
+ if (acceptanceRate >= 0.3) return 1.2;
203
+ return 1.1;
204
+ }
205
+
206
+ /**
207
+ * Update configuration.
208
+ */
209
+ updateConfig(updates: Partial<SpeculativeConfig>): void {
210
+ this.config = { ...this.config, ...updates };
211
+ }
212
+
213
+ /**
214
+ * Get current configuration.
215
+ */
216
+ getConfig(): SpeculativeConfig {
217
+ return { ...this.config };
218
+ }
219
+
220
+ /**
221
+ * Check if a provider supports speculative decoding.
222
+ */
223
+ supportsSpeculativeDecoding(provider: string): boolean {
224
+ // Local providers (Ollama, vLLM) typically support it
225
+ const localProviders = ['ollama', 'lmstudio', 'vllm'];
226
+ if (localProviders.includes(provider)) return true;
227
+
228
+ // Check if provider has known speculative support
229
+ const supportedProviders = ['together', 'fireworks', 'anyscale'];
230
+ return supportedProviders.includes(provider);
231
+ }
232
+
233
+ /**
234
+ * Get recommended speculation window based on model size.
235
+ */
236
+ getRecommendedWindow(modelSize: 'small' | 'medium' | 'large'): number {
237
+ switch (modelSize) {
238
+ case 'small': return 6;
239
+ case 'medium': return 4;
240
+ case 'large': return 2;
241
+ default: return 4;
242
+ }
243
+ }
244
+
245
+ // ---- Private helpers ----
246
+
247
+ private registerDraftModels(): void {
248
+ // Medusa-style models (multiple draft heads)
249
+ this.draftModels.set('medusa', {
250
+ id: 'medusa-7b',
251
+ type: 'medusa',
252
+ provider: 'auto',
253
+ size: '7B equivalent',
254
+ supportedWindows: [2, 4, 6, 8, 10],
255
+ });
256
+
257
+ // Lookahead decoding
258
+ this.draftModels.set('lookahead', {
259
+ id: 'lookahead-7b',
260
+ type: 'lookahead',
261
+ provider: 'auto',
262
+ size: '7B equivalent',
263
+ supportedWindows: [2, 4, 6],
264
+ });
265
+
266
+ // EAGLE decoding
267
+ this.draftModels.set('eagle', {
268
+ id: 'eagle-7b',
269
+ type: 'eagle',
270
+ provider: 'auto',
271
+ size: '7B equivalent',
272
+ supportedWindows: [2, 4, 6, 8],
273
+ });
274
+
275
+ // Spark (speculative decoding for transformers)
276
+ this.draftModels.set('spark', {
277
+ id: 'spark-7b',
278
+ type: 'spark',
279
+ provider: 'auto',
280
+ size: '7B equivalent',
281
+ supportedWindows: [2, 4, 6, 8, 10, 12],
282
+ });
283
+ }
284
+ }
285
+
286
+ // ============================================================
287
+ // Speculative Decoding Wrapper for LLM calls
288
+ // ============================================================
289
+
290
+ export class SpeculativeDecodingWrapper {
291
+ private specDec: SpeculativeDecoding;
292
+ private mainModelCall: (prompt: string, options?: any) => Promise<string>;
293
+
294
+ constructor(
295
+ mainModelCall: (prompt: string, options?: any) => Promise<string>,
296
+ config?: Partial<SpeculativeConfig>
297
+ ) {
298
+ this.mainModelCall = mainModelCall;
299
+ this.specDec = new SpeculativeDecoding(config);
300
+ }
301
+
302
+ /**
303
+ * Generate with speculative decoding.
304
+ */
305
+ async generate(
306
+ prompt: string,
307
+ maxTokens: number,
308
+ options?: { temperature?: number; useSpecDec?: boolean }
309
+ ): Promise<{ text: string; result: SpeculativeResult }> {
310
+ if (!this.specDec.shouldUse(options?.useSpecDec)) {
311
+ // Fallback to normal generation
312
+ const text = await this.mainModelCall(prompt, options);
313
+ return {
314
+ text,
315
+ result: {
316
+ used: false,
317
+ draftTokens: 0,
318
+ acceptedTokens: 0,
319
+ acceptanceRate: 0,
320
+ estimatedTimeSaved: 0,
321
+ },
322
+ };
323
+ }
324
+
325
+ // Speculative generation
326
+ const startTime = Date.now();
327
+ const draftTokens = await this.specDec.generateDraft(prompt, maxTokens);
328
+ const verification = await this.specDec.verifyDraft(prompt, draftTokens.tokens);
329
+
330
+ let text: string;
331
+
332
+ if (verification.allAccepted) {
333
+ // All draft tokens accepted, continue with main model
334
+ text = verification.actualTokens.join('') +
335
+ await this.mainModelCall(prompt, options);
336
+ } else {
337
+ // Some tokens rejected, need to rewind
338
+ text = verification.actualTokens.join('');
339
+ // Re-prompt with corrected context
340
+ text += await this.mainModelCall(prompt + text, options);
341
+ }
342
+
343
+ const endTime = Date.now();
344
+ const speedup = this.specDec.calculateSpeedup({
345
+ used: true,
346
+ draftTokens: draftTokens.tokens.length,
347
+ acceptedTokens: verification.acceptedIndices.length,
348
+ acceptanceRate: verification.acceptedIndices.length / draftTokens.tokens.length,
349
+ estimatedTimeSaved: endTime - startTime,
350
+ });
351
+
352
+ return {
353
+ text,
354
+ result: {
355
+ used: true,
356
+ draftTokens: draftTokens.tokens.length,
357
+ acceptedTokens: verification.acceptedIndices.length,
358
+ acceptanceRate: verification.acceptedIndices.length / draftTokens.tokens.length,
359
+ estimatedTimeSaved: Math.round((1 - 1 / speedup) * (endTime - startTime)),
360
+ },
361
+ };
362
+ }
363
+ }
364
+
365
+ // ============================================================
366
+ // Factory
367
+ // ============================================================
368
+
369
+ export function createSpeculativeDecoding(
370
+ config?: Partial<SpeculativeConfig>
371
+ ): SpeculativeDecoding {
372
+ return new SpeculativeDecoding(config);
373
+ }