@sparkleideas/plugins 3.0.0-alpha.10
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 +401 -0
- package/__tests__/collection-manager.test.ts +332 -0
- package/__tests__/dependency-graph.test.ts +434 -0
- package/__tests__/enhanced-plugin-registry.test.ts +488 -0
- package/__tests__/plugin-registry.test.ts +368 -0
- package/__tests__/ruvector-bridge.test.ts +2429 -0
- package/__tests__/ruvector-integration.test.ts +1602 -0
- package/__tests__/ruvector-migrations.test.ts +1099 -0
- package/__tests__/ruvector-quantization.test.ts +846 -0
- package/__tests__/ruvector-streaming.test.ts +1088 -0
- package/__tests__/sdk.test.ts +325 -0
- package/__tests__/security.test.ts +348 -0
- package/__tests__/utils/ruvector-test-utils.ts +860 -0
- package/examples/plugin-creator/index.ts +636 -0
- package/examples/plugin-creator/plugin-creator.test.ts +312 -0
- package/examples/ruvector/README.md +288 -0
- package/examples/ruvector/attention-patterns.ts +394 -0
- package/examples/ruvector/basic-usage.ts +288 -0
- package/examples/ruvector/docker-compose.yml +75 -0
- package/examples/ruvector/gnn-analysis.ts +501 -0
- package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
- package/examples/ruvector/init-db.sql +119 -0
- package/examples/ruvector/quantization.ts +680 -0
- package/examples/ruvector/self-learning.ts +447 -0
- package/examples/ruvector/semantic-search.ts +576 -0
- package/examples/ruvector/streaming-large-data.ts +507 -0
- package/examples/ruvector/transactions.ts +594 -0
- package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
- package/examples/ruvector-plugins/index.ts +79 -0
- package/examples/ruvector-plugins/intent-router.ts +354 -0
- package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
- package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
- package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
- package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
- package/examples/ruvector-plugins/shared/index.ts +20 -0
- package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
- package/examples/ruvector-plugins/sona-learning.ts +445 -0
- package/package.json +97 -0
- package/src/collections/collection-manager.ts +661 -0
- package/src/collections/index.ts +56 -0
- package/src/collections/official/index.ts +1040 -0
- package/src/core/base-plugin.ts +416 -0
- package/src/core/plugin-interface.ts +215 -0
- package/src/hooks/index.ts +685 -0
- package/src/index.ts +378 -0
- package/src/integrations/agentic-flow.ts +743 -0
- package/src/integrations/index.ts +88 -0
- package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
- package/src/integrations/ruvector/attention-advanced.ts +1040 -0
- package/src/integrations/ruvector/attention-executor.ts +782 -0
- package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
- package/src/integrations/ruvector/attention.ts +1063 -0
- package/src/integrations/ruvector/gnn.ts +3050 -0
- package/src/integrations/ruvector/hyperbolic.ts +1948 -0
- package/src/integrations/ruvector/index.ts +394 -0
- package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
- package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
- package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
- package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
- package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
- package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
- package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
- package/src/integrations/ruvector/migrations/index.ts +35 -0
- package/src/integrations/ruvector/migrations/migrations.ts +647 -0
- package/src/integrations/ruvector/quantization.ts +2036 -0
- package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
- package/src/integrations/ruvector/self-learning.ts +2376 -0
- package/src/integrations/ruvector/streaming.ts +1737 -0
- package/src/integrations/ruvector/types.ts +1945 -0
- package/src/providers/index.ts +643 -0
- package/src/registry/dependency-graph.ts +568 -0
- package/src/registry/enhanced-plugin-registry.ts +994 -0
- package/src/registry/plugin-registry.ts +604 -0
- package/src/sdk/index.ts +563 -0
- package/src/security/index.ts +594 -0
- package/src/types/index.ts +446 -0
- package/src/workers/index.ts +700 -0
- package/tmp.json +0 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +23 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Provider Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Provides unified interface for LLM providers in the plugin system.
|
|
5
|
+
* Enables multi-provider support, fallback chains, and cost optimization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import type {
|
|
10
|
+
LLMProviderDefinition,
|
|
11
|
+
LLMCapability,
|
|
12
|
+
LLMRequest,
|
|
13
|
+
LLMResponse,
|
|
14
|
+
LLMMessage,
|
|
15
|
+
LLMTool,
|
|
16
|
+
LLMToolCall,
|
|
17
|
+
RateLimitConfig,
|
|
18
|
+
CostConfig,
|
|
19
|
+
ILogger,
|
|
20
|
+
IEventBus,
|
|
21
|
+
} from '../types/index.js';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Provider Events
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export const PROVIDER_EVENTS = {
|
|
28
|
+
REGISTERED: 'provider:registered',
|
|
29
|
+
UNREGISTERED: 'provider:unregistered',
|
|
30
|
+
REQUEST_START: 'provider:request-start',
|
|
31
|
+
REQUEST_COMPLETE: 'provider:request-complete',
|
|
32
|
+
REQUEST_ERROR: 'provider:request-error',
|
|
33
|
+
RATE_LIMITED: 'provider:rate-limited',
|
|
34
|
+
FALLBACK: 'provider:fallback',
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export type ProviderEvent = typeof PROVIDER_EVENTS[keyof typeof PROVIDER_EVENTS];
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Provider Interface
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
export interface ILLMProvider {
|
|
44
|
+
readonly definition: LLMProviderDefinition;
|
|
45
|
+
|
|
46
|
+
complete(request: LLMRequest): Promise<LLMResponse>;
|
|
47
|
+
stream?(request: LLMRequest): AsyncIterable<Partial<LLMResponse>>;
|
|
48
|
+
embed?(texts: string[]): Promise<number[][]>;
|
|
49
|
+
|
|
50
|
+
healthCheck(): Promise<{ healthy: boolean; latencyMs: number }>;
|
|
51
|
+
getRateLimitStatus(): RateLimitStatus;
|
|
52
|
+
getCostEstimate(request: LLMRequest): number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RateLimitStatus {
|
|
56
|
+
requestsRemaining: number;
|
|
57
|
+
tokensRemaining: number;
|
|
58
|
+
resetAt: Date;
|
|
59
|
+
isLimited: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Provider Registry
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export interface ProviderRegistryConfig {
|
|
67
|
+
logger?: ILogger;
|
|
68
|
+
eventBus?: IEventBus;
|
|
69
|
+
defaultProvider?: string;
|
|
70
|
+
fallbackChain?: string[];
|
|
71
|
+
costOptimization?: boolean;
|
|
72
|
+
retryConfig?: RetryConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RetryConfig {
|
|
76
|
+
maxRetries: number;
|
|
77
|
+
initialDelayMs: number;
|
|
78
|
+
maxDelayMs: number;
|
|
79
|
+
backoffMultiplier: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ProviderEntry {
|
|
83
|
+
readonly provider: ILLMProvider;
|
|
84
|
+
readonly registeredAt: Date;
|
|
85
|
+
requestCount: number;
|
|
86
|
+
errorCount: number;
|
|
87
|
+
totalTokensUsed: number;
|
|
88
|
+
totalCost: number;
|
|
89
|
+
lastUsed?: Date;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ProviderRegistryStats {
|
|
93
|
+
totalProviders: number;
|
|
94
|
+
totalRequests: number;
|
|
95
|
+
totalErrors: number;
|
|
96
|
+
totalTokensUsed: number;
|
|
97
|
+
totalCost: number;
|
|
98
|
+
providerStats: Record<string, {
|
|
99
|
+
requests: number;
|
|
100
|
+
errors: number;
|
|
101
|
+
tokensUsed: number;
|
|
102
|
+
cost: number;
|
|
103
|
+
avgLatency: number;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Central registry for LLM provider management.
|
|
109
|
+
*/
|
|
110
|
+
export class ProviderRegistry extends EventEmitter {
|
|
111
|
+
private readonly providers = new Map<string, ProviderEntry>();
|
|
112
|
+
private readonly config: ProviderRegistryConfig;
|
|
113
|
+
private readonly latencyTracking = new Map<string, number[]>();
|
|
114
|
+
|
|
115
|
+
constructor(config?: ProviderRegistryConfig) {
|
|
116
|
+
super();
|
|
117
|
+
this.config = {
|
|
118
|
+
costOptimization: false,
|
|
119
|
+
retryConfig: {
|
|
120
|
+
maxRetries: 3,
|
|
121
|
+
initialDelayMs: 1000,
|
|
122
|
+
maxDelayMs: 30000,
|
|
123
|
+
backoffMultiplier: 2,
|
|
124
|
+
},
|
|
125
|
+
...config,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Register a provider.
|
|
131
|
+
*/
|
|
132
|
+
register(provider: ILLMProvider): void {
|
|
133
|
+
const name = provider.definition.name;
|
|
134
|
+
|
|
135
|
+
if (this.providers.has(name)) {
|
|
136
|
+
throw new Error(`Provider ${name} already registered`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const entry: ProviderEntry = {
|
|
140
|
+
provider,
|
|
141
|
+
registeredAt: new Date(),
|
|
142
|
+
requestCount: 0,
|
|
143
|
+
errorCount: 0,
|
|
144
|
+
totalTokensUsed: 0,
|
|
145
|
+
totalCost: 0,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.providers.set(name, entry);
|
|
149
|
+
this.latencyTracking.set(name, []);
|
|
150
|
+
this.emit(PROVIDER_EVENTS.REGISTERED, { provider: name });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Unregister a provider.
|
|
155
|
+
*/
|
|
156
|
+
unregister(name: string): boolean {
|
|
157
|
+
const removed = this.providers.delete(name);
|
|
158
|
+
if (removed) {
|
|
159
|
+
this.latencyTracking.delete(name);
|
|
160
|
+
this.emit(PROVIDER_EVENTS.UNREGISTERED, { provider: name });
|
|
161
|
+
}
|
|
162
|
+
return removed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get a provider by name.
|
|
167
|
+
*/
|
|
168
|
+
get(name: string): ILLMProvider | undefined {
|
|
169
|
+
return this.providers.get(name)?.provider;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the best available provider based on criteria.
|
|
174
|
+
*/
|
|
175
|
+
getBest(options?: {
|
|
176
|
+
capabilities?: LLMCapability[];
|
|
177
|
+
model?: string;
|
|
178
|
+
preferCheaper?: boolean;
|
|
179
|
+
}): ILLMProvider | undefined {
|
|
180
|
+
let candidates = Array.from(this.providers.values());
|
|
181
|
+
|
|
182
|
+
// Filter by capabilities
|
|
183
|
+
if (options?.capabilities) {
|
|
184
|
+
candidates = candidates.filter(e =>
|
|
185
|
+
options.capabilities!.every(cap =>
|
|
186
|
+
e.provider.definition.capabilities.includes(cap)
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Filter by model support
|
|
192
|
+
if (options?.model) {
|
|
193
|
+
candidates = candidates.filter(e =>
|
|
194
|
+
e.provider.definition.models.includes(options.model!)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Filter by rate limit availability
|
|
199
|
+
candidates = candidates.filter(e => !e.provider.getRateLimitStatus().isLimited);
|
|
200
|
+
|
|
201
|
+
if (candidates.length === 0) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Sort by preference
|
|
206
|
+
if (options?.preferCheaper || this.config.costOptimization) {
|
|
207
|
+
candidates.sort((a, b) => {
|
|
208
|
+
const costA = a.provider.definition.costPerToken?.input ?? 0;
|
|
209
|
+
const costB = b.provider.definition.costPerToken?.input ?? 0;
|
|
210
|
+
return costA - costB;
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
// Sort by success rate
|
|
214
|
+
candidates.sort((a, b) => {
|
|
215
|
+
const rateA = a.requestCount > 0 ? (a.requestCount - a.errorCount) / a.requestCount : 1;
|
|
216
|
+
const rateB = b.requestCount > 0 ? (b.requestCount - b.errorCount) / b.requestCount : 1;
|
|
217
|
+
return rateB - rateA;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return candidates[0]?.provider;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Execute a request with automatic provider selection and fallback.
|
|
226
|
+
*/
|
|
227
|
+
async execute(request: LLMRequest): Promise<LLMResponse> {
|
|
228
|
+
const provider = this.getBest({ model: request.model });
|
|
229
|
+
|
|
230
|
+
if (!provider) {
|
|
231
|
+
throw new Error(`No available provider for model ${request.model}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return this.executeWithProvider(provider.definition.name, request);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Execute a request on a specific provider with retry.
|
|
239
|
+
*/
|
|
240
|
+
async executeWithProvider(providerName: string, request: LLMRequest): Promise<LLMResponse> {
|
|
241
|
+
const entry = this.providers.get(providerName);
|
|
242
|
+
if (!entry) {
|
|
243
|
+
throw new Error(`Provider ${providerName} not found`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const retryConfig = this.config.retryConfig!;
|
|
247
|
+
let lastError: Error | null = null;
|
|
248
|
+
let delay = retryConfig.initialDelayMs;
|
|
249
|
+
|
|
250
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
251
|
+
if (attempt > 0) {
|
|
252
|
+
await this.delay(delay);
|
|
253
|
+
delay = Math.min(delay * retryConfig.backoffMultiplier, retryConfig.maxDelayMs);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
this.emit(PROVIDER_EVENTS.REQUEST_START, {
|
|
258
|
+
provider: providerName,
|
|
259
|
+
model: request.model,
|
|
260
|
+
attempt,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const startTime = Date.now();
|
|
264
|
+
const response = await entry.provider.complete(request);
|
|
265
|
+
const latency = Date.now() - startTime;
|
|
266
|
+
|
|
267
|
+
// Update metrics
|
|
268
|
+
entry.requestCount++;
|
|
269
|
+
entry.lastUsed = new Date();
|
|
270
|
+
entry.totalTokensUsed += response.usage.totalTokens;
|
|
271
|
+
entry.totalCost += entry.provider.getCostEstimate(request);
|
|
272
|
+
|
|
273
|
+
// Track latency
|
|
274
|
+
const latencies = this.latencyTracking.get(providerName)!;
|
|
275
|
+
latencies.push(latency);
|
|
276
|
+
if (latencies.length > 100) latencies.shift();
|
|
277
|
+
|
|
278
|
+
this.emit(PROVIDER_EVENTS.REQUEST_COMPLETE, {
|
|
279
|
+
provider: providerName,
|
|
280
|
+
model: request.model,
|
|
281
|
+
latencyMs: latency,
|
|
282
|
+
tokensUsed: response.usage.totalTokens,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return response;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
288
|
+
entry.errorCount++;
|
|
289
|
+
|
|
290
|
+
this.emit(PROVIDER_EVENTS.REQUEST_ERROR, {
|
|
291
|
+
provider: providerName,
|
|
292
|
+
model: request.model,
|
|
293
|
+
error: lastError.message,
|
|
294
|
+
attempt,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Check if we should try fallback
|
|
298
|
+
if (attempt === retryConfig.maxRetries && this.config.fallbackChain) {
|
|
299
|
+
const fallbackResult = await this.tryFallback(request, providerName);
|
|
300
|
+
if (fallbackResult) {
|
|
301
|
+
return fallbackResult;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
throw lastError ?? new Error('Unknown error during provider execution');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async tryFallback(
|
|
311
|
+
request: LLMRequest,
|
|
312
|
+
failedProvider: string
|
|
313
|
+
): Promise<LLMResponse | null> {
|
|
314
|
+
const fallbackChain = this.config.fallbackChain ?? [];
|
|
315
|
+
|
|
316
|
+
for (const fallbackName of fallbackChain) {
|
|
317
|
+
if (fallbackName === failedProvider) continue;
|
|
318
|
+
|
|
319
|
+
const fallbackEntry = this.providers.get(fallbackName);
|
|
320
|
+
if (!fallbackEntry) continue;
|
|
321
|
+
|
|
322
|
+
// Check if fallback supports the model
|
|
323
|
+
if (!fallbackEntry.provider.definition.models.includes(request.model)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check rate limit
|
|
328
|
+
if (fallbackEntry.provider.getRateLimitStatus().isLimited) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
this.emit(PROVIDER_EVENTS.FALLBACK, {
|
|
334
|
+
from: failedProvider,
|
|
335
|
+
to: fallbackName,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return await this.executeWithProvider(fallbackName, request);
|
|
339
|
+
} catch {
|
|
340
|
+
// Try next fallback
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private delay(ms: number): Promise<void> {
|
|
349
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* List all registered providers.
|
|
354
|
+
*/
|
|
355
|
+
list(): LLMProviderDefinition[] {
|
|
356
|
+
return Array.from(this.providers.values()).map(e => e.provider.definition);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get provider statistics.
|
|
361
|
+
*/
|
|
362
|
+
getStats(): ProviderRegistryStats {
|
|
363
|
+
let totalRequests = 0;
|
|
364
|
+
let totalErrors = 0;
|
|
365
|
+
let totalTokensUsed = 0;
|
|
366
|
+
let totalCost = 0;
|
|
367
|
+
const providerStats: ProviderRegistryStats['providerStats'] = {};
|
|
368
|
+
|
|
369
|
+
for (const [name, entry] of this.providers) {
|
|
370
|
+
totalRequests += entry.requestCount;
|
|
371
|
+
totalErrors += entry.errorCount;
|
|
372
|
+
totalTokensUsed += entry.totalTokensUsed;
|
|
373
|
+
totalCost += entry.totalCost;
|
|
374
|
+
|
|
375
|
+
const latencies = this.latencyTracking.get(name) ?? [];
|
|
376
|
+
const avgLatency = latencies.length > 0
|
|
377
|
+
? latencies.reduce((a, b) => a + b, 0) / latencies.length
|
|
378
|
+
: 0;
|
|
379
|
+
|
|
380
|
+
providerStats[name] = {
|
|
381
|
+
requests: entry.requestCount,
|
|
382
|
+
errors: entry.errorCount,
|
|
383
|
+
tokensUsed: entry.totalTokensUsed,
|
|
384
|
+
cost: entry.totalCost,
|
|
385
|
+
avgLatency,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
totalProviders: this.providers.size,
|
|
391
|
+
totalRequests,
|
|
392
|
+
totalErrors,
|
|
393
|
+
totalTokensUsed,
|
|
394
|
+
totalCost,
|
|
395
|
+
providerStats,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Health check all providers.
|
|
401
|
+
*/
|
|
402
|
+
async healthCheck(): Promise<Map<string, { healthy: boolean; latencyMs: number }>> {
|
|
403
|
+
const results = new Map<string, { healthy: boolean; latencyMs: number }>();
|
|
404
|
+
|
|
405
|
+
for (const [name, entry] of this.providers) {
|
|
406
|
+
try {
|
|
407
|
+
results.set(name, await entry.provider.healthCheck());
|
|
408
|
+
} catch {
|
|
409
|
+
results.set(name, { healthy: false, latencyMs: -1 });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// Base Provider Implementation
|
|
419
|
+
// ============================================================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Abstract base class for LLM providers.
|
|
423
|
+
*/
|
|
424
|
+
export abstract class BaseLLMProvider implements ILLMProvider {
|
|
425
|
+
readonly definition: LLMProviderDefinition;
|
|
426
|
+
protected rateLimitState: {
|
|
427
|
+
requestsInWindow: number;
|
|
428
|
+
tokensInWindow: number;
|
|
429
|
+
windowStart: Date;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
constructor(definition: LLMProviderDefinition) {
|
|
433
|
+
this.definition = definition;
|
|
434
|
+
this.rateLimitState = {
|
|
435
|
+
requestsInWindow: 0,
|
|
436
|
+
tokensInWindow: 0,
|
|
437
|
+
windowStart: new Date(),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
abstract complete(request: LLMRequest): Promise<LLMResponse>;
|
|
442
|
+
|
|
443
|
+
stream?(request: LLMRequest): AsyncIterable<Partial<LLMResponse>>;
|
|
444
|
+
embed?(texts: string[]): Promise<number[][]>;
|
|
445
|
+
|
|
446
|
+
async healthCheck(): Promise<{ healthy: boolean; latencyMs: number }> {
|
|
447
|
+
const start = Date.now();
|
|
448
|
+
try {
|
|
449
|
+
// Simple ping test
|
|
450
|
+
await this.complete({
|
|
451
|
+
model: this.definition.models[0],
|
|
452
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
453
|
+
maxTokens: 5,
|
|
454
|
+
});
|
|
455
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
456
|
+
} catch {
|
|
457
|
+
return { healthy: false, latencyMs: Date.now() - start };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
getRateLimitStatus(): RateLimitStatus {
|
|
462
|
+
const config = this.definition.rateLimit;
|
|
463
|
+
if (!config) {
|
|
464
|
+
return {
|
|
465
|
+
requestsRemaining: Infinity,
|
|
466
|
+
tokensRemaining: Infinity,
|
|
467
|
+
resetAt: new Date(Date.now() + 60000),
|
|
468
|
+
isLimited: false,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check if window has reset
|
|
473
|
+
const windowMs = 60000; // 1 minute window
|
|
474
|
+
const now = new Date();
|
|
475
|
+
if (now.getTime() - this.rateLimitState.windowStart.getTime() > windowMs) {
|
|
476
|
+
this.rateLimitState = {
|
|
477
|
+
requestsInWindow: 0,
|
|
478
|
+
tokensInWindow: 0,
|
|
479
|
+
windowStart: now,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const requestsRemaining = config.requestsPerMinute - this.rateLimitState.requestsInWindow;
|
|
484
|
+
const tokensRemaining = config.tokensPerMinute - this.rateLimitState.tokensInWindow;
|
|
485
|
+
const resetAt = new Date(this.rateLimitState.windowStart.getTime() + windowMs);
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
requestsRemaining,
|
|
489
|
+
tokensRemaining,
|
|
490
|
+
resetAt,
|
|
491
|
+
isLimited: requestsRemaining <= 0 || tokensRemaining <= 0,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getCostEstimate(request: LLMRequest): number {
|
|
496
|
+
const costConfig = this.definition.costPerToken;
|
|
497
|
+
if (!costConfig) return 0;
|
|
498
|
+
|
|
499
|
+
// Rough token estimate: ~4 chars per token
|
|
500
|
+
const inputTokens = request.messages.reduce(
|
|
501
|
+
(sum, m) => sum + Math.ceil(m.content.length / 4),
|
|
502
|
+
0
|
|
503
|
+
);
|
|
504
|
+
const outputTokens = request.maxTokens ?? 1000;
|
|
505
|
+
|
|
506
|
+
return (inputTokens * costConfig.input) + (outputTokens * costConfig.output);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
protected updateRateLimits(tokensUsed: number): void {
|
|
510
|
+
this.rateLimitState.requestsInWindow++;
|
|
511
|
+
this.rateLimitState.tokensInWindow += tokensUsed;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ============================================================================
|
|
516
|
+
// Provider Factory
|
|
517
|
+
// ============================================================================
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Factory for creating provider definitions.
|
|
521
|
+
*/
|
|
522
|
+
export class ProviderFactory {
|
|
523
|
+
/**
|
|
524
|
+
* Create an Anthropic Claude provider definition.
|
|
525
|
+
*/
|
|
526
|
+
static createClaude(options?: {
|
|
527
|
+
displayName?: string;
|
|
528
|
+
models?: string[];
|
|
529
|
+
rateLimit?: RateLimitConfig;
|
|
530
|
+
costPerToken?: CostConfig;
|
|
531
|
+
}): LLMProviderDefinition {
|
|
532
|
+
return {
|
|
533
|
+
name: 'anthropic',
|
|
534
|
+
displayName: options?.displayName ?? 'Anthropic Claude',
|
|
535
|
+
models: options?.models ?? [
|
|
536
|
+
'claude-opus-4-5-20251101',
|
|
537
|
+
'claude-sonnet-4-20250514',
|
|
538
|
+
'claude-3-5-haiku-20241022',
|
|
539
|
+
],
|
|
540
|
+
capabilities: [
|
|
541
|
+
'completion',
|
|
542
|
+
'chat',
|
|
543
|
+
'streaming',
|
|
544
|
+
'function-calling',
|
|
545
|
+
'vision',
|
|
546
|
+
'code-generation',
|
|
547
|
+
],
|
|
548
|
+
rateLimit: options?.rateLimit ?? {
|
|
549
|
+
requestsPerMinute: 50,
|
|
550
|
+
tokensPerMinute: 100000,
|
|
551
|
+
},
|
|
552
|
+
costPerToken: options?.costPerToken ?? {
|
|
553
|
+
input: 0.000003,
|
|
554
|
+
output: 0.000015,
|
|
555
|
+
currency: 'USD',
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Create an OpenAI provider definition.
|
|
562
|
+
*/
|
|
563
|
+
static createOpenAI(options?: {
|
|
564
|
+
displayName?: string;
|
|
565
|
+
models?: string[];
|
|
566
|
+
rateLimit?: RateLimitConfig;
|
|
567
|
+
costPerToken?: CostConfig;
|
|
568
|
+
}): LLMProviderDefinition {
|
|
569
|
+
return {
|
|
570
|
+
name: 'openai',
|
|
571
|
+
displayName: options?.displayName ?? 'OpenAI',
|
|
572
|
+
models: options?.models ?? [
|
|
573
|
+
'gpt-4o',
|
|
574
|
+
'gpt-4o-mini',
|
|
575
|
+
'gpt-4-turbo',
|
|
576
|
+
'gpt-3.5-turbo',
|
|
577
|
+
],
|
|
578
|
+
capabilities: [
|
|
579
|
+
'completion',
|
|
580
|
+
'chat',
|
|
581
|
+
'streaming',
|
|
582
|
+
'function-calling',
|
|
583
|
+
'vision',
|
|
584
|
+
'embeddings',
|
|
585
|
+
'code-generation',
|
|
586
|
+
],
|
|
587
|
+
rateLimit: options?.rateLimit ?? {
|
|
588
|
+
requestsPerMinute: 60,
|
|
589
|
+
tokensPerMinute: 150000,
|
|
590
|
+
},
|
|
591
|
+
costPerToken: options?.costPerToken ?? {
|
|
592
|
+
input: 0.00001,
|
|
593
|
+
output: 0.00003,
|
|
594
|
+
currency: 'USD',
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Create a local/self-hosted provider definition.
|
|
601
|
+
*/
|
|
602
|
+
static createLocal(options: {
|
|
603
|
+
name: string;
|
|
604
|
+
displayName: string;
|
|
605
|
+
models: string[];
|
|
606
|
+
capabilities: LLMCapability[];
|
|
607
|
+
endpoint?: string;
|
|
608
|
+
}): LLMProviderDefinition {
|
|
609
|
+
return {
|
|
610
|
+
name: options.name,
|
|
611
|
+
displayName: options.displayName,
|
|
612
|
+
models: options.models,
|
|
613
|
+
capabilities: options.capabilities,
|
|
614
|
+
// No rate limits for local
|
|
615
|
+
rateLimit: undefined,
|
|
616
|
+
// No cost for local
|
|
617
|
+
costPerToken: undefined,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Create a custom provider definition.
|
|
623
|
+
*/
|
|
624
|
+
static createCustom(definition: LLMProviderDefinition): LLMProviderDefinition {
|
|
625
|
+
return { ...definition };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// Exports
|
|
631
|
+
// ============================================================================
|
|
632
|
+
|
|
633
|
+
export type {
|
|
634
|
+
LLMProviderDefinition,
|
|
635
|
+
LLMCapability,
|
|
636
|
+
LLMRequest,
|
|
637
|
+
LLMResponse,
|
|
638
|
+
LLMMessage,
|
|
639
|
+
LLMTool,
|
|
640
|
+
LLMToolCall,
|
|
641
|
+
RateLimitConfig,
|
|
642
|
+
CostConfig,
|
|
643
|
+
};
|