adaptive-memory-multi-model-router 1.2.2
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 +114 -0
- package/demo/research-demo.js +266 -0
- package/dist/cache/prefixCache.d.ts +114 -0
- package/dist/cache/prefixCache.d.ts.map +1 -0
- package/dist/cache/prefixCache.js +285 -0
- package/dist/cache/prefixCache.js.map +1 -0
- package/dist/cache/responseCache.d.ts +58 -0
- package/dist/cache/responseCache.d.ts.map +1 -0
- package/dist/cache/responseCache.js +153 -0
- package/dist/cache/responseCache.js.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cost/costTracker.d.ts +95 -0
- package/dist/cost/costTracker.d.ts.map +1 -0
- package/dist/cost/costTracker.js +240 -0
- package/dist/cost/costTracker.js.map +1 -0
- package/dist/index.d.ts +723 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/episodicMemory.d.ts +82 -0
- package/dist/memory/episodicMemory.d.ts.map +1 -0
- package/dist/memory/episodicMemory.js +145 -0
- package/dist/memory/episodicMemory.js.map +1 -0
- package/dist/orchestration/haloOrchestrator.d.ts +102 -0
- package/dist/orchestration/haloOrchestrator.d.ts.map +1 -0
- package/dist/orchestration/haloOrchestrator.js +207 -0
- package/dist/orchestration/haloOrchestrator.js.map +1 -0
- package/dist/orchestration/mctsWorkflow.d.ts +85 -0
- package/dist/orchestration/mctsWorkflow.d.ts.map +1 -0
- package/dist/orchestration/mctsWorkflow.js +210 -0
- package/dist/orchestration/mctsWorkflow.js.map +1 -0
- package/dist/providers/localProvider.d.ts +102 -0
- package/dist/providers/localProvider.d.ts.map +1 -0
- package/dist/providers/localProvider.js +338 -0
- package/dist/providers/localProvider.js.map +1 -0
- package/dist/providers/registry.d.ts +55 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +138 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/routing/advancedRouter.d.ts +68 -0
- package/dist/routing/advancedRouter.d.ts.map +1 -0
- package/dist/routing/advancedRouter.js +332 -0
- package/dist/routing/advancedRouter.js.map +1 -0
- package/dist/tools/tmlpdTools.d.ts +101 -0
- package/dist/tools/tmlpdTools.d.ts.map +1 -0
- package/dist/tools/tmlpdTools.js +368 -0
- package/dist/tools/tmlpdTools.js.map +1 -0
- package/dist/utils/batchProcessor.d.ts +96 -0
- package/dist/utils/batchProcessor.d.ts.map +1 -0
- package/dist/utils/batchProcessor.js +170 -0
- package/dist/utils/batchProcessor.js.map +1 -0
- package/dist/utils/compression.d.ts +61 -0
- package/dist/utils/compression.d.ts.map +1 -0
- package/dist/utils/compression.js +281 -0
- package/dist/utils/compression.js.map +1 -0
- package/dist/utils/reliability.d.ts +74 -0
- package/dist/utils/reliability.d.ts.map +1 -0
- package/dist/utils/reliability.js +177 -0
- package/dist/utils/reliability.js.map +1 -0
- package/dist/utils/speculativeDecoding.d.ts +117 -0
- package/dist/utils/speculativeDecoding.d.ts.map +1 -0
- package/dist/utils/speculativeDecoding.js +246 -0
- package/dist/utils/speculativeDecoding.js.map +1 -0
- package/dist/utils/tokenUtils.d.ts +50 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -0
- package/dist/utils/tokenUtils.js +124 -0
- package/dist/utils/tokenUtils.js.map +1 -0
- package/examples/QUICKSTART.md +183 -0
- package/notebooks/quickstart.ipynb +157 -0
- package/package.json +83 -0
- package/python/examples.py +53 -0
- package/python/integrations.py +330 -0
- package/python/setup.py +28 -0
- package/python/tmlpd.py +369 -0
- package/qna/REDDIT_GAP_ANALYSIS.md +299 -0
- package/qna/TMLPD_QNA.md +751 -0
- package/rust/tmlpd.h +268 -0
- package/skill/SKILL.md +238 -0
- package/src/cache/prefixCache.ts +365 -0
- package/src/cache/responseCache.ts +147 -0
- package/src/cost/costTracker.ts +302 -0
- package/src/index.ts +224 -0
- package/src/memory/episodicMemory.ts +185 -0
- package/src/orchestration/haloOrchestrator.ts +266 -0
- package/src/orchestration/mctsWorkflow.ts +262 -0
- package/src/providers/localProvider.ts +406 -0
- package/src/providers/registry.ts +164 -0
- package/src/routing/advancedRouter.ts +406 -0
- package/src/tools/tmlpdTools.ts +433 -0
- package/src/utils/batchProcessor.ts +232 -0
- package/src/utils/compression.ts +325 -0
- package/src/utils/reliability.ts +221 -0
- package/src/utils/speculativeDecoding.ts +344 -0
- package/src/utils/tokenUtils.ts +145 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMLPD PI Tools
|
|
3
|
+
*
|
|
4
|
+
* Main tools exposed to the PI agent via the MCP bridge.
|
|
5
|
+
* Features: streaming, caching, cost tracking, reliability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ResponseCache, CacheConfig } from "../cache/responseCache";
|
|
9
|
+
import { CostTracker, BudgetConfig, CostSummary } from "../cost/costTracker";
|
|
10
|
+
import { ProviderRegistry } from "../providers/registry";
|
|
11
|
+
import { CircuitBreaker, withRetry, RetryConfig, DEFAULT_RETRY_CONFIG } from "../utils/reliability";
|
|
12
|
+
import * as https from "https";
|
|
13
|
+
import * as http from "http";
|
|
14
|
+
|
|
15
|
+
export interface TMLPDConfig {
|
|
16
|
+
cache?: Partial<CacheConfig>;
|
|
17
|
+
budget?: BudgetConfig;
|
|
18
|
+
retry?: Partial<RetryConfig>;
|
|
19
|
+
maxConcurrent?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ExecuteResult {
|
|
23
|
+
success: boolean;
|
|
24
|
+
content?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
model: string;
|
|
27
|
+
provider: string;
|
|
28
|
+
tokens?: number;
|
|
29
|
+
cost?: number;
|
|
30
|
+
cached?: boolean;
|
|
31
|
+
duration_ms?: number;
|
|
32
|
+
attempts?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParallelResult {
|
|
36
|
+
responses: ExecuteResult[];
|
|
37
|
+
total_models: number;
|
|
38
|
+
successful_models: number;
|
|
39
|
+
total_cost: number;
|
|
40
|
+
duration_ms: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface StreamingConfig {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
chunk_size?: number;
|
|
46
|
+
on_chunk?: (chunk: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class TMLPDTools {
|
|
50
|
+
private cache: ResponseCache;
|
|
51
|
+
private costTracker: CostTracker;
|
|
52
|
+
private registry: ProviderRegistry;
|
|
53
|
+
private circuitBreakers: Map<string, CircuitBreaker> = new Map();
|
|
54
|
+
private retryConfig: RetryConfig;
|
|
55
|
+
private maxConcurrent: number;
|
|
56
|
+
|
|
57
|
+
constructor(config: TMLPDConfig = {}) {
|
|
58
|
+
this.cache = new ResponseCache(config.cache);
|
|
59
|
+
this.costTracker = new CostTracker(config.budget);
|
|
60
|
+
this.registry = new ProviderRegistry();
|
|
61
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retry };
|
|
62
|
+
this.maxConcurrent = config.maxConcurrent || 5;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get cost summary
|
|
67
|
+
*/
|
|
68
|
+
getCostSummary(): CostSummary {
|
|
69
|
+
return this.costTracker.getSummary();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get remaining budget
|
|
74
|
+
*/
|
|
75
|
+
getRemainingBudget(): { daily: number | null; monthly: number | null; per_model: Record<string, number> } {
|
|
76
|
+
return this.costTracker.getRemainingBudget();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get cache stats
|
|
81
|
+
*/
|
|
82
|
+
getCacheStats(): { hits: number; misses: number; size: number; hit_rate: number } {
|
|
83
|
+
return this.cache.getStats();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get provider status
|
|
88
|
+
*/
|
|
89
|
+
getProviderStatus(): Record<string, any> {
|
|
90
|
+
return this.registry.getStatus();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Execute single prompt with optional streaming
|
|
95
|
+
*/
|
|
96
|
+
async execute(
|
|
97
|
+
prompt: string,
|
|
98
|
+
model?: string,
|
|
99
|
+
streaming?: StreamingConfig
|
|
100
|
+
): Promise<ExecuteResult> {
|
|
101
|
+
const selectedModel = model || this.registry.selectModel();
|
|
102
|
+
if (!selectedModel) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: "No providers available",
|
|
106
|
+
model: "unknown",
|
|
107
|
+
provider: "unknown",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check cache first
|
|
112
|
+
const cached = this.cache.get(prompt, selectedModel);
|
|
113
|
+
if (cached) {
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
content: cached.content,
|
|
117
|
+
model: cached.model,
|
|
118
|
+
provider: cached.provider,
|
|
119
|
+
tokens: cached.tokens,
|
|
120
|
+
cost: 0, // Cache hit = no cost
|
|
121
|
+
cached: true,
|
|
122
|
+
duration_ms: 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const provider = selectedModel.split("/")[0];
|
|
127
|
+
const breaker = this.getCircuitBreaker(provider);
|
|
128
|
+
|
|
129
|
+
const startTime = Date.now();
|
|
130
|
+
const { result, error, attempts, circuit_tripped } = await withRetry(
|
|
131
|
+
async () => {
|
|
132
|
+
return this.executeRequest(selectedModel, streaming);
|
|
133
|
+
},
|
|
134
|
+
this.retryConfig,
|
|
135
|
+
breaker
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const duration_ms = Date.now() - startTime;
|
|
139
|
+
|
|
140
|
+
if (!result) {
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: error?.message || "Execution failed",
|
|
144
|
+
model: selectedModel,
|
|
145
|
+
provider,
|
|
146
|
+
attempts,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Record cost
|
|
151
|
+
const costSnapshot = this.costTracker.record(
|
|
152
|
+
provider,
|
|
153
|
+
selectedModel,
|
|
154
|
+
result.input_tokens || 0,
|
|
155
|
+
result.output_tokens || 0
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Cache successful response
|
|
159
|
+
if (result.content) {
|
|
160
|
+
this.cache.set(prompt, selectedModel, {
|
|
161
|
+
content: result.content,
|
|
162
|
+
model: selectedModel,
|
|
163
|
+
provider,
|
|
164
|
+
tokens: costSnapshot.input_tokens + costSnapshot.output_tokens,
|
|
165
|
+
cost: costSnapshot.total_cost,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
content: result.content,
|
|
172
|
+
model: selectedModel,
|
|
173
|
+
provider,
|
|
174
|
+
tokens: costSnapshot.input_tokens + costSnapshot.output_tokens,
|
|
175
|
+
cost: costSnapshot.total_cost,
|
|
176
|
+
duration_ms,
|
|
177
|
+
attempts,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Execute parallel across multiple models
|
|
183
|
+
*/
|
|
184
|
+
async executeParallel(
|
|
185
|
+
prompt: string,
|
|
186
|
+
models?: string[],
|
|
187
|
+
streaming?: StreamingConfig
|
|
188
|
+
): Promise<ParallelResult> {
|
|
189
|
+
const selectedModels = models || this.registry.getReadyProviders().slice(0, 3).map(p => `${p}/default`);
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
|
|
192
|
+
const results: ExecuteResult[] = [];
|
|
193
|
+
let total_cost = 0;
|
|
194
|
+
|
|
195
|
+
// Execute with concurrency limit
|
|
196
|
+
for (let i = 0; i < selectedModels.length; i += this.maxConcurrent) {
|
|
197
|
+
const batch = selectedModels.slice(i, i + this.maxConcurrent);
|
|
198
|
+
const batchResults = await Promise.all(
|
|
199
|
+
batch.map((model) => this.execute(prompt, model, streaming))
|
|
200
|
+
);
|
|
201
|
+
results.push(...batchResults);
|
|
202
|
+
|
|
203
|
+
for (const r of batchResults) {
|
|
204
|
+
if (r.success && r.cost) {
|
|
205
|
+
total_cost += r.cost;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const duration_ms = Date.now() - startTime;
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
responses: results,
|
|
214
|
+
total_models: selectedModels.length,
|
|
215
|
+
successful_models: results.filter((r) => r.success).length,
|
|
216
|
+
total_cost,
|
|
217
|
+
duration_ms,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get circuit breaker for provider
|
|
223
|
+
*/
|
|
224
|
+
private getCircuitBreaker(provider: string): CircuitBreaker {
|
|
225
|
+
if (!this.circuitBreakers.has(provider)) {
|
|
226
|
+
this.circuitBreakers.set(provider, new CircuitBreaker());
|
|
227
|
+
}
|
|
228
|
+
return this.circuitBreakers.get(provider)!;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Execute HTTP request to provider
|
|
233
|
+
*/
|
|
234
|
+
private async executeRequest(
|
|
235
|
+
model: string,
|
|
236
|
+
streaming?: StreamingConfig
|
|
237
|
+
): Promise<{ content: string; input_tokens: number; output_tokens: number }> {
|
|
238
|
+
const provider = model.split("/")[0];
|
|
239
|
+
const providerConfig = this.registry.getStatus().providers[provider];
|
|
240
|
+
|
|
241
|
+
if (!providerConfig?.ready) {
|
|
242
|
+
throw new Error(`Provider ${provider} is not available`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Build request based on provider mode
|
|
246
|
+
if (providerConfig.mode === "gemini") {
|
|
247
|
+
return this.executeGemini(provider, model);
|
|
248
|
+
} else if (providerConfig.mode === "anthropic") {
|
|
249
|
+
return this.executeAnthropic(provider, model);
|
|
250
|
+
} else {
|
|
251
|
+
return this.executeOpenAI(provider, model);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async executeOpenAI(
|
|
256
|
+
provider: string,
|
|
257
|
+
model: string
|
|
258
|
+
): Promise<{ content: string; input_tokens: number; output_tokens: number }> {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const status = this.registry.getStatus().providers[provider];
|
|
261
|
+
const apiKey = process.env[`${provider.toUpperCase()}_API_KEY`] || "";
|
|
262
|
+
const baseUrl = process.env[`${provider.toUpperCase()}_OPENAI_BASE_URL`] || "";
|
|
263
|
+
|
|
264
|
+
const payload = JSON.stringify({
|
|
265
|
+
model: model.split("/")[1],
|
|
266
|
+
messages: [{ role: "user", content: "Placeholder" }], // Will be replaced by actual prompt via closure
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const url = new URL(`${baseUrl}/chat/completions`);
|
|
270
|
+
const options = {
|
|
271
|
+
hostname: url.hostname,
|
|
272
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
273
|
+
path: url.pathname,
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: {
|
|
276
|
+
"Content-Type": "application/json",
|
|
277
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
278
|
+
"User-Agent": "TMLPD-Pi/1.0",
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const protocol = url.protocol === "https:" ? https : http;
|
|
283
|
+
const req = protocol.request(options, (res) => {
|
|
284
|
+
let data = "";
|
|
285
|
+
res.on("data", (chunk) => (data += chunk));
|
|
286
|
+
res.on("end", () => {
|
|
287
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
288
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const json = JSON.parse(data);
|
|
293
|
+
const content = json.choices?.[0]?.message?.content || "";
|
|
294
|
+
const tokens = json.usage?.total_tokens || 0;
|
|
295
|
+
resolve({ content, input_tokens: Math.floor(tokens * 0.3), output_tokens: Math.floor(tokens * 0.7) });
|
|
296
|
+
} catch {
|
|
297
|
+
reject(new Error("Invalid JSON response"));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
req.on("error", reject);
|
|
303
|
+
req.write(payload);
|
|
304
|
+
req.end();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async executeAnthropic(
|
|
309
|
+
provider: string,
|
|
310
|
+
model: string
|
|
311
|
+
): Promise<{ content: string; input_tokens: number; output_tokens: number }> {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const apiKey = process.env[`${provider.toUpperCase()}_API_KEY`] || "";
|
|
314
|
+
const baseUrl = process.env[`${provider.toUpperCase()}_BASE_URL`] || "";
|
|
315
|
+
|
|
316
|
+
const payload = JSON.stringify({
|
|
317
|
+
model: model.split("/")[1],
|
|
318
|
+
max_tokens: 4096,
|
|
319
|
+
messages: [{ role: "user", content: "Placeholder" }],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const url = new URL(baseUrl);
|
|
323
|
+
const options = {
|
|
324
|
+
hostname: url.hostname,
|
|
325
|
+
port: url.port || 443,
|
|
326
|
+
path: url.pathname,
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: {
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
"x-api-key": apiKey,
|
|
331
|
+
"anthropic-version": "2023-06-01",
|
|
332
|
+
"User-Agent": "TMLPD-Pi/1.0",
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const req = https.request(options, (res) => {
|
|
337
|
+
let data = "";
|
|
338
|
+
res.on("data", (chunk) => (data += chunk));
|
|
339
|
+
res.on("end", () => {
|
|
340
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
341
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const json = JSON.parse(data);
|
|
346
|
+
const content = json.content?.[0]?.text || "";
|
|
347
|
+
const tokens = json.usage?.output_tokens || 0;
|
|
348
|
+
resolve({ content, input_tokens: Math.floor(tokens * 0.3), output_tokens: Math.floor(tokens * 0.7) });
|
|
349
|
+
} catch {
|
|
350
|
+
reject(new Error("Invalid JSON response"));
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
req.on("error", reject);
|
|
356
|
+
req.write(payload);
|
|
357
|
+
req.end();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async executeGemini(
|
|
362
|
+
provider: string,
|
|
363
|
+
model: string
|
|
364
|
+
): Promise<{ content: string; input_tokens: number; output_tokens: number }> {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
const apiKey = process.env.GOOGLE_API_KEY || "";
|
|
367
|
+
const baseUrl = "https://generativelanguage.googleapis.com/v1beta";
|
|
368
|
+
|
|
369
|
+
const payload = JSON.stringify({
|
|
370
|
+
contents: [{ parts: [{ text: "Placeholder" }] }],
|
|
371
|
+
generationConfig: { maxOutputTokens: 4096 },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const url = new URL(`${baseUrl}/models/${model.split("/")[1]}:generateContent`);
|
|
375
|
+
url.searchParams.set("key", apiKey);
|
|
376
|
+
|
|
377
|
+
const options = {
|
|
378
|
+
hostname: url.hostname,
|
|
379
|
+
port: url.port || 443,
|
|
380
|
+
path: url.pathname + url.search,
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
"User-Agent": "TMLPD-Pi/1.0",
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const req = https.request(options, (res) => {
|
|
389
|
+
let data = "";
|
|
390
|
+
res.on("data", (chunk) => (data += chunk));
|
|
391
|
+
res.on("end", () => {
|
|
392
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
393
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const json = JSON.parse(data);
|
|
398
|
+
const content = json.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
399
|
+
resolve({ content, input_tokens: 100, output_tokens: 100 });
|
|
400
|
+
} catch {
|
|
401
|
+
reject(new Error("Invalid JSON response"));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
req.on("error", reject);
|
|
407
|
+
req.write(payload);
|
|
408
|
+
req.end();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Factory Function
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
let _defaultInstance: TMLPDTools | null = null;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a TMLPD instance
|
|
420
|
+
*/
|
|
421
|
+
export function createTMLPD(config?: TMLPDConfig): TMLPDTools {
|
|
422
|
+
return new TMLPDTools(config);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Get default singleton instance
|
|
427
|
+
*/
|
|
428
|
+
export function getDefault(): TMLPDTools {
|
|
429
|
+
if (!_defaultInstance) {
|
|
430
|
+
_defaultInstance = new TMLPDTools();
|
|
431
|
+
}
|
|
432
|
+
return _defaultInstance;
|
|
433
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TMLPD Batch Processing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Queue and process multiple prompts with:
|
|
5
|
+
* - Concurrency control
|
|
6
|
+
* - Priority scheduling
|
|
7
|
+
* - Progress callbacks
|
|
8
|
+
* - Rate limiting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { nanoid } from "nanoid";
|
|
12
|
+
|
|
13
|
+
export interface BatchItem {
|
|
14
|
+
id: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
priority: "high" | "normal" | "low";
|
|
18
|
+
callback?: (result: BatchResult) => void;
|
|
19
|
+
metadata?: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BatchResult {
|
|
23
|
+
id: string;
|
|
24
|
+
success: boolean;
|
|
25
|
+
content?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
model: string;
|
|
28
|
+
cost: number;
|
|
29
|
+
duration_ms: number;
|
|
30
|
+
cached: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BatchOptions {
|
|
34
|
+
concurrency?: number; // Max parallel executions
|
|
35
|
+
model?: string; // Default model for all
|
|
36
|
+
stop_on_error?: boolean; // Stop batch on first error
|
|
37
|
+
rate_limit?: {
|
|
38
|
+
requests_per_minute?: number;
|
|
39
|
+
tokens_per_minute?: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BatchProgress {
|
|
44
|
+
total: number;
|
|
45
|
+
completed: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
in_progress: number;
|
|
48
|
+
total_cost: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ProgressCallback = (progress: BatchProgress, result?: BatchResult) => void;
|
|
52
|
+
|
|
53
|
+
export class BatchProcessor {
|
|
54
|
+
private queue: BatchItem[] = [];
|
|
55
|
+
private results: BatchResult[] = [];
|
|
56
|
+
private options: Required<BatchOptions>;
|
|
57
|
+
private executing: Set<string> = new Set();
|
|
58
|
+
private progressCallbacks: ProgressCallback[] = [];
|
|
59
|
+
|
|
60
|
+
constructor(options: BatchOptions = {}) {
|
|
61
|
+
this.options = {
|
|
62
|
+
concurrency: options.concurrency || 5,
|
|
63
|
+
model: options.model || "gpt-4o",
|
|
64
|
+
stop_on_error: options.stop_on_error || false,
|
|
65
|
+
rate_limit: options.rate_limit || {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add item to batch queue.
|
|
71
|
+
*/
|
|
72
|
+
add(item: Omit<BatchItem, "id">): string {
|
|
73
|
+
const id = nanoid(8);
|
|
74
|
+
this.queue.push({
|
|
75
|
+
...item,
|
|
76
|
+
id,
|
|
77
|
+
priority: item.priority || "normal"
|
|
78
|
+
});
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add multiple items.
|
|
84
|
+
*/
|
|
85
|
+
addBatch(items: Array<Omit<BatchItem, "id">>): string[] {
|
|
86
|
+
return items.map(item => this.add(item));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register progress callback.
|
|
91
|
+
*/
|
|
92
|
+
onProgress(callback: ProgressCallback): void {
|
|
93
|
+
this.progressCallbacks.push(callback);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get current progress.
|
|
98
|
+
*/
|
|
99
|
+
getProgress(): BatchProgress {
|
|
100
|
+
return {
|
|
101
|
+
total: this.results.length + this.queue.length + this.executing.size,
|
|
102
|
+
completed: this.results.filter(r => r.success).length,
|
|
103
|
+
failed: this.results.filter(r => !r.success).length,
|
|
104
|
+
in_progress: this.executing.size,
|
|
105
|
+
total_cost: this.results.reduce((sum, r) => sum + r.cost, 0)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Execute batch with concurrency control.
|
|
111
|
+
*/
|
|
112
|
+
async execute(
|
|
113
|
+
executor: (item: BatchItem) => Promise<BatchResult>
|
|
114
|
+
): Promise<BatchResult[]> {
|
|
115
|
+
// Sort by priority
|
|
116
|
+
this.queue.sort((a, b) => {
|
|
117
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
118
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const total = this.queue.length;
|
|
122
|
+
let index = 0;
|
|
123
|
+
|
|
124
|
+
// Process with concurrency limit
|
|
125
|
+
while (index < this.queue.length || this.executing.size > 0) {
|
|
126
|
+
// Launch up to concurrency limit
|
|
127
|
+
while (index < this.queue.length && this.executing.size < this.options.concurrency) {
|
|
128
|
+
const item = this.queue[index++];
|
|
129
|
+
this.executing.add(item.id);
|
|
130
|
+
|
|
131
|
+
// Execute with promise tracking
|
|
132
|
+
executor(item).then(result => {
|
|
133
|
+
this.results.push(result);
|
|
134
|
+
this.executing.delete(item.id);
|
|
135
|
+
|
|
136
|
+
// Notify progress
|
|
137
|
+
const progress = this.getProgress();
|
|
138
|
+
for (const cb of this.progressCallbacks) {
|
|
139
|
+
cb(progress, result);
|
|
140
|
+
}
|
|
141
|
+
}).catch(error => {
|
|
142
|
+
const result: BatchResult = {
|
|
143
|
+
id: item.id,
|
|
144
|
+
success: false,
|
|
145
|
+
error: error.message,
|
|
146
|
+
model: item.model || this.options.model,
|
|
147
|
+
cost: 0,
|
|
148
|
+
duration_ms: 0,
|
|
149
|
+
cached: false
|
|
150
|
+
};
|
|
151
|
+
this.results.push(result);
|
|
152
|
+
this.executing.delete(item.id);
|
|
153
|
+
|
|
154
|
+
const progress = this.getProgress();
|
|
155
|
+
for (const cb of this.progressCallbacks) {
|
|
156
|
+
cb(progress, result);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check stop_on_error
|
|
160
|
+
if (this.options.stop_on_error) {
|
|
161
|
+
// Cancel remaining items
|
|
162
|
+
this.queue = [];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Rate limiting: wait between launches
|
|
167
|
+
if (this.options.rate_limit.requests_per_minute) {
|
|
168
|
+
const delay = 60000 / this.options.rate_limit.requests_per_minute;
|
|
169
|
+
await this.sleep(delay);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Wait for at least one to complete
|
|
174
|
+
if (this.executing.size > 0) {
|
|
175
|
+
await this.waitForCompletion();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.results;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear queue and results.
|
|
184
|
+
*/
|
|
185
|
+
reset(): void {
|
|
186
|
+
this.queue = [];
|
|
187
|
+
this.results = [];
|
|
188
|
+
this.executing.clear();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get queue size.
|
|
193
|
+
*/
|
|
194
|
+
size(): number {
|
|
195
|
+
return this.queue.length + this.executing.size + this.results.length;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private sleep(ms: number): Promise<void> {
|
|
199
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async waitForCompletion(): Promise<void> {
|
|
203
|
+
await new Promise(resolve => {
|
|
204
|
+
const check = () => {
|
|
205
|
+
if (this.executing.size === 0) {
|
|
206
|
+
resolve(null);
|
|
207
|
+
} else {
|
|
208
|
+
setTimeout(check, 50);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
check();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Helper function for simple batch execution.
|
|
218
|
+
*/
|
|
219
|
+
export async function executeBatch(
|
|
220
|
+
items: Array<{ prompt: string; model?: string; priority?: "high" | "normal" | "low" }>,
|
|
221
|
+
executor: (prompt: string, model?: string) => Promise<BatchResult>,
|
|
222
|
+
options: BatchOptions = {}
|
|
223
|
+
): Promise<BatchResult[]> {
|
|
224
|
+
const processor = new BatchProcessor(options);
|
|
225
|
+
processor.addBatch(items.map(i => ({ ...i, priority: i.priority || "normal" })));
|
|
226
|
+
return processor.execute(async (item) => executor(item.prompt, item.model));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default {
|
|
230
|
+
BatchProcessor,
|
|
231
|
+
executeBatch
|
|
232
|
+
};
|