ai.matey.core 0.2.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/bridge.js +657 -0
  3. package/dist/cjs/bridge.js.map +1 -0
  4. package/dist/cjs/capability-inference.js +349 -0
  5. package/dist/cjs/capability-inference.js.map +1 -0
  6. package/dist/cjs/capability-matcher.js +216 -0
  7. package/dist/cjs/capability-matcher.js.map +1 -0
  8. package/dist/cjs/index.js +31 -0
  9. package/dist/cjs/index.js.map +1 -0
  10. package/dist/cjs/middleware-stack.js +256 -0
  11. package/dist/cjs/middleware-stack.js.map +1 -0
  12. package/dist/cjs/model-pricing.js +350 -0
  13. package/dist/cjs/model-pricing.js.map +1 -0
  14. package/dist/cjs/model-translation.js +171 -0
  15. package/dist/cjs/model-translation.js.map +1 -0
  16. package/dist/cjs/router.js +1388 -0
  17. package/dist/cjs/router.js.map +1 -0
  18. package/dist/esm/bridge.js +652 -0
  19. package/dist/esm/bridge.js.map +1 -0
  20. package/dist/esm/capability-inference.js +343 -0
  21. package/dist/esm/capability-inference.js.map +1 -0
  22. package/dist/esm/capability-matcher.js +210 -0
  23. package/dist/esm/capability-matcher.js.map +1 -0
  24. package/dist/esm/index.js +15 -0
  25. package/dist/esm/index.js.map +1 -0
  26. package/dist/esm/middleware-stack.js +250 -0
  27. package/dist/esm/middleware-stack.js.map +1 -0
  28. package/dist/esm/model-pricing.js +340 -0
  29. package/dist/esm/model-pricing.js.map +1 -0
  30. package/dist/esm/model-translation.js +163 -0
  31. package/dist/esm/model-translation.js.map +1 -0
  32. package/dist/esm/router.js +1383 -0
  33. package/dist/esm/router.js.map +1 -0
  34. package/dist/types/bridge.d.ts +254 -0
  35. package/dist/types/bridge.d.ts.map +1 -0
  36. package/dist/types/capability-inference.d.ts +35 -0
  37. package/dist/types/capability-inference.d.ts.map +1 -0
  38. package/dist/types/capability-matcher.d.ts +104 -0
  39. package/dist/types/capability-matcher.d.ts.map +1 -0
  40. package/dist/types/index.d.ts +15 -0
  41. package/dist/types/index.d.ts.map +1 -0
  42. package/dist/types/middleware-stack.d.ts +102 -0
  43. package/dist/types/middleware-stack.d.ts.map +1 -0
  44. package/dist/types/model-pricing.d.ts +81 -0
  45. package/dist/types/model-pricing.d.ts.map +1 -0
  46. package/dist/types/model-translation.d.ts +171 -0
  47. package/dist/types/model-translation.d.ts.map +1 -0
  48. package/dist/types/router.d.ts +287 -0
  49. package/dist/types/router.d.ts.map +1 -0
  50. package/package.json +70 -0
  51. package/readme.md +34 -0
@@ -0,0 +1,1388 @@
1
+ "use strict";
2
+ /**
3
+ * Router Implementation
4
+ *
5
+ * The Router manages multiple backend adapters with intelligent routing,
6
+ * fallback strategies, circuit breaker pattern, and health checking.
7
+ * It implements the BackendAdapter interface so it can be used as a backend.
8
+ *
9
+ * @module
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Router = void 0;
13
+ exports.createRouter = createRouter;
14
+ const ai_matey_errors_1 = require("ai.matey.errors");
15
+ const capability_matcher_js_1 = require("./capability-matcher.js");
16
+ const capability_inference_js_1 = require("./capability-inference.js");
17
+ // ============================================================================
18
+ // Router Implementation
19
+ // ============================================================================
20
+ /**
21
+ * Router manages multiple backend adapters with intelligent routing.
22
+ */
23
+ class Router {
24
+ metadata;
25
+ config;
26
+ backends = new Map();
27
+ modelMapping = new Map(); // model -> backend (for routing)
28
+ modelTranslationMapping = new Map(); // model -> model (for translation)
29
+ backendTranslationMappings = new Map(); // backend -> (model -> model)
30
+ modelPatterns = [];
31
+ fallbackChain = [];
32
+ roundRobinIndex = 0;
33
+ healthCheckInterval;
34
+ // Stats
35
+ stats = {
36
+ totalRequests: 0,
37
+ successfulRequests: 0,
38
+ failedRequests: 0,
39
+ totalFallbacks: 0,
40
+ parallelRequests: 0,
41
+ sinceTimestamp: Date.now(),
42
+ };
43
+ constructor(config = {}) {
44
+ this.config = {
45
+ routingStrategy: config.routingStrategy ?? 'explicit',
46
+ fallbackStrategy: config.fallbackStrategy ?? 'sequential',
47
+ defaultBackend: config.defaultBackend,
48
+ healthCheckInterval: config.healthCheckInterval ?? 0,
49
+ enableCircuitBreaker: config.enableCircuitBreaker ?? false,
50
+ circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5,
51
+ circuitBreakerTimeout: config.circuitBreakerTimeout ?? 60000,
52
+ trackLatency: config.trackLatency ?? true,
53
+ trackCost: config.trackCost ?? false,
54
+ capabilityBasedRouting: config.capabilityBasedRouting ?? false,
55
+ optimization: config.optimization ?? 'balanced',
56
+ optimizationWeights: config.optimizationWeights,
57
+ capabilityCacheDuration: config.capabilityCacheDuration ?? 3600000, // 1 hour
58
+ customRouter: config.customRouter,
59
+ customFallback: config.customFallback,
60
+ modelTranslation: config.modelTranslation ?? {
61
+ strategy: 'hybrid',
62
+ warnOnDefault: true,
63
+ strictMode: false,
64
+ },
65
+ };
66
+ this.metadata = {
67
+ name: 'router',
68
+ version: '1.0.0',
69
+ provider: 'Universal Router',
70
+ capabilities: {
71
+ streaming: true,
72
+ multiModal: true,
73
+ tools: true,
74
+ systemMessageStrategy: 'in-messages',
75
+ supportsMultipleSystemMessages: true,
76
+ },
77
+ config: this.config,
78
+ };
79
+ // Start health checking if enabled
80
+ if (this.config.healthCheckInterval && this.config.healthCheckInterval > 0) {
81
+ this.startHealthChecking();
82
+ }
83
+ }
84
+ // ==========================================================================
85
+ // Format Conversion (Not Applicable for Router)
86
+ // ==========================================================================
87
+ /**
88
+ * Convert IR request to provider format.
89
+ * Not applicable for Router - use the specific backend adapter instead.
90
+ */
91
+ fromIR(_request) {
92
+ throw new Error('fromIR() not applicable for Router - route to a specific backend first');
93
+ }
94
+ /**
95
+ * Convert provider response to IR format.
96
+ * Not applicable for Router - use the specific backend adapter instead.
97
+ */
98
+ toIR(_response, _originalRequest, _latencyMs) {
99
+ throw new Error('toIR() not applicable for Router - route to a specific backend first');
100
+ }
101
+ // ==========================================================================
102
+ // Backend Management
103
+ // ==========================================================================
104
+ /**
105
+ * Register a backend adapter.
106
+ */
107
+ register(name, adapter) {
108
+ if (this.backends.has(name)) {
109
+ throw new ai_matey_errors_1.AdapterError({
110
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
111
+ message: `Backend '${name}' is already registered`,
112
+ isRetryable: false,
113
+ provenance: { router: this.metadata.name },
114
+ });
115
+ }
116
+ this.backends.set(name, {
117
+ adapter,
118
+ isHealthy: true,
119
+ circuitBreakerState: 'closed',
120
+ consecutiveFailures: 0,
121
+ latencies: [],
122
+ totalRequests: 0,
123
+ successfulRequests: 0,
124
+ failedRequests: 0,
125
+ totalCost: 0,
126
+ });
127
+ return this;
128
+ }
129
+ /**
130
+ * Unregister a backend adapter.
131
+ */
132
+ unregister(name) {
133
+ if (!this.backends.has(name)) {
134
+ throw new ai_matey_errors_1.AdapterError({
135
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
136
+ message: `Backend '${name}' is not registered`,
137
+ isRetryable: false,
138
+ provenance: { router: this.metadata.name },
139
+ });
140
+ }
141
+ // Check if it's the default backend
142
+ if (this.config.defaultBackend === name) {
143
+ throw new ai_matey_errors_1.AdapterError({
144
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
145
+ message: `Cannot unregister default backend '${name}'`,
146
+ isRetryable: false,
147
+ provenance: { router: this.metadata.name },
148
+ });
149
+ }
150
+ // Check if it's the last backend
151
+ if (this.backends.size === 1) {
152
+ throw new ai_matey_errors_1.AdapterError({
153
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
154
+ message: `Cannot unregister last backend '${name}'`,
155
+ isRetryable: false,
156
+ provenance: { router: this.metadata.name },
157
+ });
158
+ }
159
+ this.backends.delete(name);
160
+ return this;
161
+ }
162
+ /**
163
+ * Get a registered backend adapter.
164
+ */
165
+ get(name) {
166
+ return this.backends.get(name)?.adapter;
167
+ }
168
+ /**
169
+ * Check if backend is registered.
170
+ */
171
+ has(name) {
172
+ return this.backends.has(name);
173
+ }
174
+ /**
175
+ * List all registered backend names.
176
+ */
177
+ listBackends() {
178
+ return Array.from(this.backends.keys());
179
+ }
180
+ getBackendInfo(name) {
181
+ if (name !== undefined) {
182
+ const state = this.backends.get(name);
183
+ if (!state) {
184
+ return undefined;
185
+ }
186
+ return this.createBackendInfo(name, state);
187
+ }
188
+ const infos = [];
189
+ for (const [backendName, state] of this.backends.entries()) {
190
+ infos.push(this.createBackendInfo(backendName, state));
191
+ }
192
+ return infos;
193
+ }
194
+ // ==========================================================================
195
+ // Routing Configuration
196
+ // ==========================================================================
197
+ /**
198
+ * Set fallback chain for sequential failover.
199
+ */
200
+ setFallbackChain(chain) {
201
+ // Validate all backends exist
202
+ for (const name of chain) {
203
+ if (!this.backends.has(name)) {
204
+ throw new ai_matey_errors_1.AdapterError({
205
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
206
+ message: `Backend '${name}' in fallback chain is not registered`,
207
+ isRetryable: false,
208
+ provenance: { router: this.metadata.name },
209
+ });
210
+ }
211
+ }
212
+ this.fallbackChain = [...chain];
213
+ return this;
214
+ }
215
+ /**
216
+ * Get current fallback chain.
217
+ */
218
+ getFallbackChain() {
219
+ return this.fallbackChain;
220
+ }
221
+ /**
222
+ * Set model to backend mapping.
223
+ */
224
+ setModelMapping(mapping) {
225
+ this.modelMapping.clear();
226
+ for (const [model, backend] of Object.entries(mapping)) {
227
+ if (!this.backends.has(backend)) {
228
+ throw new ai_matey_errors_1.AdapterError({
229
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
230
+ message: `Backend '${String(backend)}' in model mapping is not registered`,
231
+ isRetryable: false,
232
+ provenance: { router: this.metadata.name },
233
+ });
234
+ }
235
+ this.modelMapping.set(model, backend);
236
+ }
237
+ return this;
238
+ }
239
+ /**
240
+ * Get current model mapping.
241
+ */
242
+ getModelMapping() {
243
+ const mapping = {};
244
+ for (const [model, backend] of this.modelMapping.entries()) {
245
+ mapping[model] = backend;
246
+ }
247
+ return mapping;
248
+ }
249
+ /**
250
+ * Set model name translation mapping (for fallback scenarios).
251
+ * Maps source model names to target model names.
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * router.setModelTranslationMapping({
256
+ * 'gpt-4': 'claude-3-5-sonnet-20241022',
257
+ * 'gpt-3.5-turbo': 'claude-3-5-haiku-20241022'
258
+ * });
259
+ * ```
260
+ */
261
+ setModelTranslationMapping(mapping) {
262
+ this.modelTranslationMapping.clear();
263
+ for (const [sourceModel, targetModel] of Object.entries(mapping)) {
264
+ this.modelTranslationMapping.set(sourceModel, targetModel);
265
+ }
266
+ return this;
267
+ }
268
+ /**
269
+ * Get current model translation mapping.
270
+ */
271
+ getModelTranslationMapping() {
272
+ const mapping = {};
273
+ for (const [source, target] of this.modelTranslationMapping.entries()) {
274
+ mapping[source] = target;
275
+ }
276
+ return mapping;
277
+ }
278
+ /**
279
+ * Set backend-specific model translation mapping.
280
+ * This takes priority over global model translation mapping.
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * router.setBackendTranslationMapping('anthropic', {
285
+ * 'gpt-4': 'claude-3-5-sonnet-20241022',
286
+ * 'gpt-3.5-turbo': 'claude-3-5-haiku-20241022'
287
+ * });
288
+ * ```
289
+ */
290
+ setBackendTranslationMapping(backendName, mapping) {
291
+ // Validate backend exists
292
+ if (!this.backends.has(backendName)) {
293
+ throw new ai_matey_errors_1.AdapterError({
294
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
295
+ message: `Backend '${backendName}' is not registered`,
296
+ isRetryable: false,
297
+ provenance: { router: this.metadata.name },
298
+ });
299
+ }
300
+ // Create or get existing backend mapping
301
+ let backendMapping = this.backendTranslationMappings.get(backendName);
302
+ if (!backendMapping) {
303
+ backendMapping = new Map();
304
+ this.backendTranslationMappings.set(backendName, backendMapping);
305
+ }
306
+ // Clear and populate mapping
307
+ backendMapping.clear();
308
+ for (const [sourceModel, targetModel] of Object.entries(mapping)) {
309
+ backendMapping.set(sourceModel, targetModel);
310
+ }
311
+ return this;
312
+ }
313
+ /**
314
+ * Get backend-specific model translation mapping.
315
+ */
316
+ getBackendTranslationMapping(backendName) {
317
+ const backendMapping = this.backendTranslationMappings.get(backendName);
318
+ if (!backendMapping) {
319
+ return {};
320
+ }
321
+ const mapping = {};
322
+ for (const [source, target] of backendMapping.entries()) {
323
+ mapping[source] = target;
324
+ }
325
+ return mapping;
326
+ }
327
+ /**
328
+ * Clear all model translation mappings.
329
+ *
330
+ * @returns This router for chaining
331
+ */
332
+ clearModelTranslationMapping() {
333
+ this.modelTranslationMapping.clear();
334
+ return this;
335
+ }
336
+ /**
337
+ * Clear backend-specific model translation mappings.
338
+ *
339
+ * @param backendName Optional backend name to clear. If not provided, clears all.
340
+ * @returns This router for chaining
341
+ */
342
+ clearBackendTranslationMapping(backendName) {
343
+ if (backendName) {
344
+ const backendMapping = this.backendTranslationMappings.get(backendName);
345
+ if (backendMapping) {
346
+ backendMapping.clear();
347
+ }
348
+ }
349
+ else {
350
+ for (const mapping of this.backendTranslationMappings.values()) {
351
+ mapping.clear();
352
+ }
353
+ }
354
+ return this;
355
+ }
356
+ /**
357
+ * Set model pattern mappings.
358
+ */
359
+ setModelPatterns(patterns) {
360
+ // Validate all backends exist
361
+ for (const pattern of patterns) {
362
+ if (!this.backends.has(pattern.backend)) {
363
+ throw new ai_matey_errors_1.AdapterError({
364
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
365
+ message: `Backend '${pattern.backend}' in model pattern is not registered`,
366
+ isRetryable: false,
367
+ provenance: { router: this.metadata.name },
368
+ });
369
+ }
370
+ }
371
+ this.modelPatterns = [...patterns];
372
+ return this;
373
+ }
374
+ /**
375
+ * Get current model patterns.
376
+ */
377
+ getModelPatterns() {
378
+ return this.modelPatterns;
379
+ }
380
+ // ==========================================================================
381
+ // Routing Operations
382
+ // ==========================================================================
383
+ /**
384
+ * Select backend for a request.
385
+ */
386
+ async selectBackend(request, preferredBackend) {
387
+ const context = {
388
+ stats: this.getStats(),
389
+ metadata: request.metadata?.custom ?? {},
390
+ preferredBackend,
391
+ };
392
+ // Try explicit preference first
393
+ if (preferredBackend && this.isBackendAvailable(preferredBackend)) {
394
+ return preferredBackend;
395
+ }
396
+ // Try capability-based routing if enabled
397
+ if (this.config.capabilityBasedRouting) {
398
+ const capabilityBackend = await this.selectBackendByCapabilities(request);
399
+ if (capabilityBackend) {
400
+ return capabilityBackend;
401
+ }
402
+ }
403
+ // Apply routing strategy
404
+ const strategy = this.config.routingStrategy ?? 'explicit';
405
+ let selectedBackend = null;
406
+ switch (strategy) {
407
+ case 'explicit':
408
+ selectedBackend = this.routeExplicit(preferredBackend);
409
+ break;
410
+ case 'model-based':
411
+ selectedBackend = this.routeByModel(request);
412
+ break;
413
+ case 'cost-optimized':
414
+ selectedBackend = this.routeByCost(request);
415
+ break;
416
+ case 'latency-optimized':
417
+ selectedBackend = this.routeByLatency();
418
+ break;
419
+ case 'round-robin':
420
+ selectedBackend = this.routeRoundRobin();
421
+ break;
422
+ case 'random':
423
+ selectedBackend = this.routeRandom();
424
+ break;
425
+ case 'custom':
426
+ if (this.config.customRouter) {
427
+ selectedBackend = await this.config.customRouter(request, this.getAvailableBackends(), context);
428
+ }
429
+ break;
430
+ }
431
+ // Fallback to default backend
432
+ if (!selectedBackend && this.config.defaultBackend) {
433
+ selectedBackend = this.config.defaultBackend;
434
+ }
435
+ // Final fallback: first available backend
436
+ if (!selectedBackend) {
437
+ const available = this.getAvailableBackends();
438
+ selectedBackend = available[0] ?? null;
439
+ }
440
+ if (!selectedBackend) {
441
+ throw new ai_matey_errors_1.AdapterError({
442
+ code: ai_matey_errors_1.ErrorCode.NO_BACKEND_AVAILABLE,
443
+ message: 'No available backend for routing',
444
+ isRetryable: false,
445
+ provenance: { router: this.metadata.name },
446
+ });
447
+ }
448
+ return selectedBackend;
449
+ }
450
+ /**
451
+ * Execute request with automatic backend selection and fallback.
452
+ */
453
+ async execute(request, signal) {
454
+ this.stats.totalRequests++;
455
+ const preferredBackend = request.metadata?.custom?.backend;
456
+ const attemptedBackends = [];
457
+ try {
458
+ // Select primary backend
459
+ const primaryBackend = await this.selectBackend(request, preferredBackend);
460
+ attemptedBackends.push(primaryBackend);
461
+ // Translate model for this backend
462
+ const originalModel = request.parameters?.model ?? '';
463
+ const translationResult = this.translateModelForBackend(originalModel, primaryBackend);
464
+ // Create request with translated model
465
+ const translatedRequest = {
466
+ ...request,
467
+ parameters: {
468
+ ...request.parameters,
469
+ model: translationResult.translated,
470
+ },
471
+ };
472
+ // Try primary backend
473
+ const response = await this.executeOnBackend(primaryBackend, translatedRequest, signal);
474
+ this.stats.successfulRequests++;
475
+ return response;
476
+ }
477
+ catch (primaryError) {
478
+ // Handle fallback
479
+ if (this.config.fallbackStrategy === 'none') {
480
+ this.stats.failedRequests++;
481
+ throw primaryError;
482
+ }
483
+ try {
484
+ const response = await this.executeFallback(request, attemptedBackends, primaryError, signal);
485
+ this.stats.successfulRequests++;
486
+ this.stats.totalFallbacks++;
487
+ return response;
488
+ }
489
+ catch (fallbackError) {
490
+ this.stats.failedRequests++;
491
+ throw fallbackError;
492
+ }
493
+ }
494
+ }
495
+ /**
496
+ * Execute streaming request with automatic backend selection and fallback.
497
+ */
498
+ async *executeStream(request, signal) {
499
+ this.stats.totalRequests++;
500
+ const preferredBackend = request.metadata?.custom?.backend;
501
+ const attemptedBackends = [];
502
+ try {
503
+ // Select primary backend
504
+ const primaryBackend = await this.selectBackend(request, preferredBackend);
505
+ attemptedBackends.push(primaryBackend);
506
+ // Translate model for this backend
507
+ const originalModel = request.parameters?.model ?? '';
508
+ const translationResult = this.translateModelForBackend(originalModel, primaryBackend);
509
+ // Create request with translated model
510
+ const translatedRequest = {
511
+ ...request,
512
+ parameters: {
513
+ ...request.parameters,
514
+ model: translationResult.translated,
515
+ },
516
+ };
517
+ // Try primary backend streaming
518
+ const stream = this.executeStreamOnBackend(primaryBackend, translatedRequest, signal);
519
+ for await (const chunk of stream) {
520
+ yield chunk;
521
+ }
522
+ this.stats.successfulRequests++;
523
+ }
524
+ catch (primaryError) {
525
+ // For streaming, fallback is more complex - yield error chunk
526
+ this.stats.failedRequests++;
527
+ yield {
528
+ type: 'error',
529
+ sequence: 0,
530
+ error: {
531
+ code: primaryError instanceof ai_matey_errors_1.AdapterError ? primaryError.code : 'UNKNOWN_ERROR',
532
+ message: primaryError instanceof Error ? primaryError.message : String(primaryError),
533
+ },
534
+ };
535
+ }
536
+ }
537
+ /**
538
+ * Dispatch request to multiple backends in parallel.
539
+ */
540
+ async dispatchParallel(request, options = {}, signal) {
541
+ this.stats.totalRequests++;
542
+ this.stats.parallelRequests++;
543
+ const { backends: targetBackends, strategy = 'first', timeout, cancelOnFirstSuccess = true, } = options;
544
+ // Determine which backends to use
545
+ const backendsToUse = targetBackends && targetBackends.length > 0
546
+ ? targetBackends.filter((name) => this.isBackendAvailable(name))
547
+ : this.getAvailableBackends();
548
+ if (backendsToUse.length === 0) {
549
+ throw new ai_matey_errors_1.AdapterError({
550
+ code: ai_matey_errors_1.ErrorCode.NO_BACKEND_AVAILABLE,
551
+ message: 'No available backends for parallel dispatch',
552
+ isRetryable: false,
553
+ provenance: { router: this.metadata.name },
554
+ });
555
+ }
556
+ const startTime = Date.now();
557
+ const abortController = new AbortController();
558
+ const combinedSignal = signal
559
+ ? this.combineSignals([signal, abortController.signal])
560
+ : abortController.signal;
561
+ // Create timeout if specified
562
+ let timeoutId;
563
+ if (timeout) {
564
+ timeoutId = setTimeout(() => abortController.abort(), timeout);
565
+ }
566
+ try {
567
+ const promises = backendsToUse.map(async (backendName) => {
568
+ const backendStartTime = Date.now();
569
+ try {
570
+ const response = await this.executeOnBackend(backendName, request, combinedSignal);
571
+ return {
572
+ backend: backendName,
573
+ response,
574
+ latencyMs: Date.now() - backendStartTime,
575
+ success: true,
576
+ };
577
+ }
578
+ catch (error) {
579
+ return {
580
+ backend: backendName,
581
+ error: error instanceof ai_matey_errors_1.AdapterError ? error : this.wrapError(error),
582
+ success: false,
583
+ };
584
+ }
585
+ });
586
+ let results;
587
+ if (strategy === 'first') {
588
+ // Return first successful response
589
+ const firstSuccess = await Promise.race(promises);
590
+ if (cancelOnFirstSuccess) {
591
+ abortController.abort();
592
+ }
593
+ results = [firstSuccess];
594
+ }
595
+ else {
596
+ // Wait for all responses
597
+ results = await Promise.all(promises);
598
+ }
599
+ // Process results
600
+ const successful = results.filter((r) => r.success);
601
+ const failed = results
602
+ .filter((r) => !r.success)
603
+ .map((r) => ({
604
+ backend: r.backend,
605
+ error: r.error,
606
+ }));
607
+ if (successful.length === 0) {
608
+ throw new ai_matey_errors_1.AdapterError({
609
+ code: ai_matey_errors_1.ErrorCode.ALL_BACKENDS_FAILED,
610
+ message: `All parallel backends failed: ${failed.map((f) => f.backend).join(', ')}`,
611
+ isRetryable: true,
612
+ provenance: { router: this.metadata.name },
613
+ });
614
+ }
615
+ const firstSuccess = successful[0];
616
+ if (!firstSuccess?.response) {
617
+ throw new ai_matey_errors_1.AdapterError({
618
+ code: ai_matey_errors_1.ErrorCode.INTERNAL_ERROR,
619
+ message: 'No successful response in parallel dispatch',
620
+ isRetryable: false,
621
+ provenance: { router: this.metadata.name },
622
+ });
623
+ }
624
+ return {
625
+ response: firstSuccess.response,
626
+ allResponses: strategy === 'all'
627
+ ? successful.map((s) => ({
628
+ backend: s.backend,
629
+ response: s.response,
630
+ latencyMs: s.latencyMs,
631
+ }))
632
+ : undefined,
633
+ successfulBackends: successful.map((s) => s.backend),
634
+ failedBackends: failed,
635
+ totalTimeMs: Date.now() - startTime,
636
+ };
637
+ }
638
+ finally {
639
+ if (timeoutId) {
640
+ clearTimeout(timeoutId);
641
+ }
642
+ }
643
+ }
644
+ async checkHealth(name) {
645
+ if (name !== undefined) {
646
+ const state = this.backends.get(name);
647
+ if (!state) {
648
+ return false;
649
+ }
650
+ try {
651
+ const healthy = state.adapter.healthCheck ? await state.adapter.healthCheck() : true;
652
+ state.isHealthy = healthy;
653
+ state.lastHealthCheck = Date.now();
654
+ return healthy;
655
+ }
656
+ catch {
657
+ state.isHealthy = false;
658
+ state.lastHealthCheck = Date.now();
659
+ return false;
660
+ }
661
+ }
662
+ // Check all backends
663
+ const results = {};
664
+ const promises = Array.from(this.backends.entries()).map(async ([backendName, state]) => {
665
+ try {
666
+ const healthy = state.adapter.healthCheck ? await state.adapter.healthCheck() : true;
667
+ state.isHealthy = healthy;
668
+ state.lastHealthCheck = Date.now();
669
+ results[backendName] = healthy;
670
+ }
671
+ catch {
672
+ state.isHealthy = false;
673
+ state.lastHealthCheck = Date.now();
674
+ results[backendName] = false;
675
+ }
676
+ });
677
+ await Promise.all(promises);
678
+ return results;
679
+ }
680
+ /**
681
+ * Manually open circuit breaker for a backend.
682
+ */
683
+ openCircuitBreaker(name, timeoutMs) {
684
+ const state = this.backends.get(name);
685
+ if (!state) {
686
+ throw new ai_matey_errors_1.AdapterError({
687
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
688
+ message: `Backend '${name}' not found`,
689
+ isRetryable: false,
690
+ provenance: { router: this.metadata.name },
691
+ });
692
+ }
693
+ state.circuitBreakerState = 'open';
694
+ state.circuitOpenedAt = Date.now();
695
+ // Auto-close after timeout
696
+ if (timeoutMs ?? this.config.circuitBreakerTimeout) {
697
+ setTimeout(() => {
698
+ if (state.circuitBreakerState === 'open') {
699
+ state.circuitBreakerState = 'half-open';
700
+ }
701
+ }, timeoutMs ?? this.config.circuitBreakerTimeout);
702
+ }
703
+ }
704
+ /**
705
+ * Manually close circuit breaker for a backend.
706
+ */
707
+ closeCircuitBreaker(name) {
708
+ const state = this.backends.get(name);
709
+ if (!state) {
710
+ throw new ai_matey_errors_1.AdapterError({
711
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
712
+ message: `Backend '${name}' not found`,
713
+ isRetryable: false,
714
+ provenance: { router: this.metadata.name },
715
+ });
716
+ }
717
+ state.circuitBreakerState = 'closed';
718
+ state.consecutiveFailures = 0;
719
+ state.circuitOpenedAt = undefined;
720
+ }
721
+ /**
722
+ * Reset circuit breaker statistics.
723
+ */
724
+ resetCircuitBreaker(name) {
725
+ if (name) {
726
+ const state = this.backends.get(name);
727
+ if (state) {
728
+ state.consecutiveFailures = 0;
729
+ state.circuitBreakerState = 'closed';
730
+ state.circuitOpenedAt = undefined;
731
+ }
732
+ }
733
+ else {
734
+ for (const state of this.backends.values()) {
735
+ state.consecutiveFailures = 0;
736
+ state.circuitBreakerState = 'closed';
737
+ state.circuitOpenedAt = undefined;
738
+ }
739
+ }
740
+ }
741
+ /**
742
+ * Check if circuit breaker is open for a backend.
743
+ *
744
+ * @param name Backend name
745
+ * @returns true if circuit breaker is open, false otherwise
746
+ */
747
+ isCircuitBreakerOpen(name) {
748
+ const state = this.backends.get(name);
749
+ if (!state) {
750
+ return false;
751
+ }
752
+ return state.circuitBreakerState === 'open';
753
+ }
754
+ // ==========================================================================
755
+ // Statistics & Monitoring
756
+ // ==========================================================================
757
+ /**
758
+ * Get router statistics.
759
+ */
760
+ getStats() {
761
+ const backendStats = {};
762
+ for (const [name, state] of this.backends.entries()) {
763
+ backendStats[name] = this.calculateBackendStats(state);
764
+ }
765
+ return {
766
+ totalRequests: this.stats.totalRequests,
767
+ successfulRequests: this.stats.successfulRequests,
768
+ failedRequests: this.stats.failedRequests,
769
+ totalFallbacks: this.stats.totalFallbacks,
770
+ parallelRequests: this.stats.parallelRequests,
771
+ backendStats,
772
+ sinceTimestamp: this.stats.sinceTimestamp,
773
+ };
774
+ }
775
+ /**
776
+ * Reset router statistics.
777
+ */
778
+ resetStats() {
779
+ this.stats = {
780
+ totalRequests: 0,
781
+ successfulRequests: 0,
782
+ failedRequests: 0,
783
+ totalFallbacks: 0,
784
+ parallelRequests: 0,
785
+ sinceTimestamp: Date.now(),
786
+ };
787
+ for (const state of this.backends.values()) {
788
+ state.totalRequests = 0;
789
+ state.successfulRequests = 0;
790
+ state.failedRequests = 0;
791
+ state.latencies = [];
792
+ state.totalCost = 0;
793
+ }
794
+ }
795
+ /**
796
+ * Get statistics for specific backend.
797
+ */
798
+ getBackendStats(name) {
799
+ const state = this.backends.get(name);
800
+ if (!state) {
801
+ return undefined;
802
+ }
803
+ return this.calculateBackendStats(state);
804
+ }
805
+ // ==========================================================================
806
+ // Utility Methods
807
+ // ==========================================================================
808
+ /**
809
+ * Clone router with new configuration.
810
+ */
811
+ clone(config) {
812
+ const newRouter = new Router({ ...this.config, ...config });
813
+ // Copy backend registrations
814
+ for (const [name, state] of this.backends.entries()) {
815
+ newRouter.register(name, state.adapter);
816
+ }
817
+ // Copy model mappings
818
+ newRouter.modelMapping = new Map(this.modelMapping);
819
+ newRouter.modelPatterns = [...this.modelPatterns];
820
+ newRouter.fallbackChain = [...this.fallbackChain];
821
+ return newRouter;
822
+ }
823
+ /**
824
+ * Clean up resources.
825
+ */
826
+ dispose() {
827
+ if (this.healthCheckInterval) {
828
+ clearInterval(this.healthCheckInterval);
829
+ this.healthCheckInterval = undefined;
830
+ }
831
+ }
832
+ // ==========================================================================
833
+ // Private Helper Methods
834
+ // ==========================================================================
835
+ /**
836
+ * Execute request on specific backend.
837
+ */
838
+ async executeOnBackend(name, request, signal) {
839
+ const state = this.backends.get(name);
840
+ if (!state) {
841
+ throw new ai_matey_errors_1.AdapterError({
842
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
843
+ message: `Backend '${name}' not found`,
844
+ isRetryable: false,
845
+ provenance: { router: this.metadata.name },
846
+ });
847
+ }
848
+ // Check circuit breaker
849
+ if (this.config.enableCircuitBreaker) {
850
+ this.checkCircuitBreaker(name, state);
851
+ }
852
+ state.totalRequests++;
853
+ const startTime = Date.now();
854
+ try {
855
+ const response = await state.adapter.execute(request, signal);
856
+ // Track success
857
+ state.successfulRequests++;
858
+ state.consecutiveFailures = 0;
859
+ if (this.config.trackLatency) {
860
+ const latency = Date.now() - startTime;
861
+ state.latencies.push(latency);
862
+ // Keep only last 100 latencies
863
+ if (state.latencies.length > 100) {
864
+ state.latencies.shift();
865
+ }
866
+ }
867
+ // Track cost
868
+ if (this.config.trackCost && state.adapter.estimateCost) {
869
+ const cost = await state.adapter.estimateCost(request);
870
+ if (cost !== null) {
871
+ state.totalCost += cost;
872
+ }
873
+ }
874
+ // Update circuit breaker
875
+ if (state.circuitBreakerState === 'half-open') {
876
+ state.circuitBreakerState = 'closed';
877
+ }
878
+ return response;
879
+ }
880
+ catch (error) {
881
+ // Track failure
882
+ state.failedRequests++;
883
+ state.consecutiveFailures++;
884
+ // Update circuit breaker
885
+ if (this.config.enableCircuitBreaker &&
886
+ state.consecutiveFailures >= (this.config.circuitBreakerThreshold ?? 5)) {
887
+ this.openCircuitBreaker(name);
888
+ }
889
+ throw error;
890
+ }
891
+ }
892
+ /**
893
+ * Execute streaming request on specific backend.
894
+ */
895
+ executeStreamOnBackend(name, request, signal) {
896
+ const state = this.backends.get(name);
897
+ if (!state) {
898
+ throw new ai_matey_errors_1.AdapterError({
899
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
900
+ message: `Backend '${name}' not found`,
901
+ isRetryable: false,
902
+ provenance: { router: this.metadata.name },
903
+ });
904
+ }
905
+ // Check circuit breaker
906
+ if (this.config.enableCircuitBreaker) {
907
+ this.checkCircuitBreaker(name, state);
908
+ }
909
+ state.totalRequests++;
910
+ return state.adapter.executeStream(request, signal);
911
+ }
912
+ /**
913
+ * Execute fallback strategy.
914
+ */
915
+ async executeFallback(request, attemptedBackends, error, signal) {
916
+ const strategy = this.config.fallbackStrategy ?? 'sequential';
917
+ if (strategy === 'sequential') {
918
+ return this.fallbackSequential(request, attemptedBackends, signal);
919
+ }
920
+ else if (strategy === 'parallel') {
921
+ return this.fallbackParallel(request, attemptedBackends, signal);
922
+ }
923
+ else if (strategy === 'custom' && this.config.customFallback) {
924
+ const available = this.getAvailableBackends().filter((name) => !attemptedBackends.includes(name));
925
+ const nextBackend = await this.config.customFallback(request, attemptedBackends[attemptedBackends.length - 1], error, attemptedBackends, available);
926
+ if (nextBackend && !attemptedBackends.includes(nextBackend)) {
927
+ attemptedBackends.push(nextBackend);
928
+ return this.executeOnBackend(nextBackend, request, signal);
929
+ }
930
+ }
931
+ throw error;
932
+ }
933
+ /**
934
+ * Translate model name for a specific backend.
935
+ *
936
+ * Applies translation strategy: backend-specific exact → global exact → pattern → default → passthrough
937
+ */
938
+ translateModelForBackend(modelName, backendName) {
939
+ const strategy = this.config.modelTranslation?.strategy ?? 'hybrid';
940
+ const strictMode = this.config.modelTranslation?.strictMode ?? false;
941
+ // 0. Try backend-specific exact match (highest priority)
942
+ if (strategy !== 'none') {
943
+ const backendMapping = this.backendTranslationMappings.get(backendName);
944
+ if (backendMapping) {
945
+ const backendExactMatch = backendMapping.get(modelName);
946
+ if (backendExactMatch) {
947
+ return {
948
+ translated: backendExactMatch,
949
+ source: 'exact',
950
+ wasTranslated: true,
951
+ };
952
+ }
953
+ }
954
+ }
955
+ // 1. Try global exact match (all strategies except 'none')
956
+ if (strategy !== 'none') {
957
+ const exactMatch = this.modelTranslationMapping.get(modelName);
958
+ if (exactMatch) {
959
+ return {
960
+ translated: exactMatch,
961
+ source: 'exact',
962
+ wasTranslated: true,
963
+ };
964
+ }
965
+ }
966
+ // 2. Try pattern match (pattern and hybrid strategies)
967
+ if (strategy === 'pattern' || strategy === 'hybrid') {
968
+ // Sort patterns by priority (higher priority first)
969
+ const sortedPatterns = [...this.modelPatterns].sort((a, b) => {
970
+ const priorityA = a.priority ?? 0;
971
+ const priorityB = b.priority ?? 0;
972
+ return priorityB - priorityA;
973
+ });
974
+ for (const patternMapping of sortedPatterns) {
975
+ if (patternMapping.pattern.test(modelName)) {
976
+ // Use targetModel if specified, otherwise original model
977
+ const translated = patternMapping.targetModel ?? modelName;
978
+ return {
979
+ translated,
980
+ source: 'pattern',
981
+ wasTranslated: patternMapping.targetModel !== undefined,
982
+ };
983
+ }
984
+ }
985
+ }
986
+ // 3. Try backend default (hybrid strategy only)
987
+ if (strategy === 'hybrid') {
988
+ const backendState = this.backends.get(backendName);
989
+ const adapter = backendState?.adapter;
990
+ const defaultModel = adapter?.config?.defaultModel;
991
+ if (defaultModel) {
992
+ // TODO: Emit warning event when warnOnDefault is true
993
+ // For now, the translation result indicates wasTranslated=true
994
+ return {
995
+ translated: defaultModel,
996
+ source: 'default',
997
+ wasTranslated: true,
998
+ };
999
+ }
1000
+ }
1001
+ // 4. No translation found
1002
+ if (strictMode) {
1003
+ throw new ai_matey_errors_1.AdapterError({
1004
+ code: ai_matey_errors_1.ErrorCode.ROUTING_FAILED,
1005
+ message: `No translation found for model: ${modelName}`,
1006
+ isRetryable: false,
1007
+ provenance: { router: this.metadata.name, backend: backendName },
1008
+ });
1009
+ }
1010
+ // Return original model (passthrough)
1011
+ return {
1012
+ translated: modelName,
1013
+ source: 'none',
1014
+ wasTranslated: false,
1015
+ };
1016
+ }
1017
+ /**
1018
+ * Sequential fallback: try backends one by one.
1019
+ */
1020
+ async fallbackSequential(request, attemptedBackends, signal) {
1021
+ const available = this.getAvailableBackends().filter((name) => !attemptedBackends.includes(name));
1022
+ // Use fallback chain if configured
1023
+ const candidates = this.fallbackChain.length > 0
1024
+ ? this.fallbackChain.filter((name) => available.includes(name))
1025
+ : available;
1026
+ let lastError;
1027
+ for (const backendName of candidates) {
1028
+ try {
1029
+ attemptedBackends.push(backendName);
1030
+ // Translate model for this backend
1031
+ const originalModel = request.parameters?.model ?? '';
1032
+ const translationResult = this.translateModelForBackend(originalModel, backendName);
1033
+ // Create request with translated model
1034
+ const translatedRequest = {
1035
+ ...request,
1036
+ parameters: {
1037
+ ...request.parameters,
1038
+ model: translationResult.translated,
1039
+ },
1040
+ };
1041
+ return await this.executeOnBackend(backendName, translatedRequest, signal);
1042
+ }
1043
+ catch (error) {
1044
+ lastError = error;
1045
+ continue;
1046
+ }
1047
+ }
1048
+ throw (lastError ??
1049
+ new ai_matey_errors_1.AdapterError({
1050
+ code: ai_matey_errors_1.ErrorCode.ALL_BACKENDS_FAILED,
1051
+ message: 'All fallback backends failed',
1052
+ isRetryable: false,
1053
+ provenance: { router: this.metadata.name },
1054
+ }));
1055
+ }
1056
+ /**
1057
+ * Parallel fallback: try all remaining backends at once.
1058
+ */
1059
+ async fallbackParallel(request, attemptedBackends, signal) {
1060
+ const available = this.getAvailableBackends().filter((name) => !attemptedBackends.includes(name));
1061
+ if (available.length === 0) {
1062
+ throw new ai_matey_errors_1.AdapterError({
1063
+ code: ai_matey_errors_1.ErrorCode.NO_BACKEND_AVAILABLE,
1064
+ message: 'No available fallback backends',
1065
+ isRetryable: false,
1066
+ provenance: { router: this.metadata.name },
1067
+ });
1068
+ }
1069
+ // Create promises with model translation for each backend
1070
+ const originalModel = request.parameters?.model ?? '';
1071
+ const promises = available.map((backendName) => {
1072
+ // Translate model for this backend
1073
+ const translationResult = this.translateModelForBackend(originalModel, backendName);
1074
+ // Create request with translated model
1075
+ const translatedRequest = {
1076
+ ...request,
1077
+ parameters: {
1078
+ ...request.parameters,
1079
+ model: translationResult.translated,
1080
+ },
1081
+ };
1082
+ return this.executeOnBackend(backendName, translatedRequest, signal);
1083
+ });
1084
+ return Promise.race(promises);
1085
+ }
1086
+ /**
1087
+ * Check circuit breaker state.
1088
+ */
1089
+ checkCircuitBreaker(name, state) {
1090
+ if (state.circuitBreakerState === 'open') {
1091
+ // Check if timeout has passed
1092
+ const timeout = this.config.circuitBreakerTimeout ?? 60000;
1093
+ if (state.circuitOpenedAt && Date.now() - state.circuitOpenedAt > timeout) {
1094
+ state.circuitBreakerState = 'half-open';
1095
+ }
1096
+ else {
1097
+ throw new ai_matey_errors_1.AdapterError({
1098
+ code: ai_matey_errors_1.ErrorCode.PROVIDER_UNAVAILABLE,
1099
+ message: `Circuit breaker is open for backend '${name}'`,
1100
+ isRetryable: true,
1101
+ provenance: { router: this.metadata.name, backend: name },
1102
+ });
1103
+ }
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Get list of available backends.
1108
+ */
1109
+ getAvailableBackends() {
1110
+ const available = [];
1111
+ for (const [name, state] of this.backends.entries()) {
1112
+ if (state.isHealthy && state.circuitBreakerState !== 'open') {
1113
+ available.push(name);
1114
+ }
1115
+ }
1116
+ return available;
1117
+ }
1118
+ /**
1119
+ * Check if backend is available.
1120
+ */
1121
+ isBackendAvailable(name) {
1122
+ const state = this.backends.get(name);
1123
+ if (!state) {
1124
+ return false;
1125
+ }
1126
+ return state.isHealthy && state.circuitBreakerState !== 'open';
1127
+ }
1128
+ /**
1129
+ * Routing: explicit backend selection.
1130
+ */
1131
+ routeExplicit(preferredBackend) {
1132
+ if (preferredBackend && this.isBackendAvailable(preferredBackend)) {
1133
+ return preferredBackend;
1134
+ }
1135
+ return null;
1136
+ }
1137
+ /**
1138
+ * Routing: model-based selection.
1139
+ */
1140
+ routeByModel(request) {
1141
+ const model = request.parameters?.model;
1142
+ if (!model) {
1143
+ return null;
1144
+ }
1145
+ // Check exact mapping
1146
+ const exactMatch = this.modelMapping.get(model);
1147
+ if (exactMatch && this.isBackendAvailable(exactMatch)) {
1148
+ return exactMatch;
1149
+ }
1150
+ // Check pattern matching
1151
+ for (const pattern of this.modelPatterns) {
1152
+ if (pattern.pattern.test(model) && this.isBackendAvailable(pattern.backend)) {
1153
+ return pattern.backend;
1154
+ }
1155
+ }
1156
+ return null;
1157
+ }
1158
+ /**
1159
+ * Routing: cost-optimized selection.
1160
+ */
1161
+ routeByCost(_request) {
1162
+ if (!this.config.trackCost) {
1163
+ return null;
1164
+ }
1165
+ let bestBackend = null;
1166
+ let lowestAvgCost = Infinity;
1167
+ for (const [name, state] of this.backends.entries()) {
1168
+ if (!this.isBackendAvailable(name)) {
1169
+ continue;
1170
+ }
1171
+ const stats = this.calculateBackendStats(state);
1172
+ const avgCost = stats.averageCost ?? 0;
1173
+ if (avgCost < lowestAvgCost) {
1174
+ lowestAvgCost = avgCost;
1175
+ bestBackend = name;
1176
+ }
1177
+ }
1178
+ return bestBackend;
1179
+ }
1180
+ /**
1181
+ * Routing: latency-optimized selection.
1182
+ */
1183
+ routeByLatency() {
1184
+ if (!this.config.trackLatency) {
1185
+ return null;
1186
+ }
1187
+ let bestBackend = null;
1188
+ let lowestLatency = Infinity;
1189
+ for (const [name, state] of this.backends.entries()) {
1190
+ if (!this.isBackendAvailable(name)) {
1191
+ continue;
1192
+ }
1193
+ const stats = this.calculateBackendStats(state);
1194
+ const avgLatency = stats.averageLatencyMs;
1195
+ if (avgLatency < lowestLatency) {
1196
+ lowestLatency = avgLatency;
1197
+ bestBackend = name;
1198
+ }
1199
+ }
1200
+ return bestBackend;
1201
+ }
1202
+ /**
1203
+ * Routing: round-robin selection.
1204
+ */
1205
+ routeRoundRobin() {
1206
+ const available = this.getAvailableBackends();
1207
+ if (available.length === 0) {
1208
+ return null;
1209
+ }
1210
+ const backend = available[this.roundRobinIndex % available.length];
1211
+ this.roundRobinIndex++;
1212
+ return backend ?? null;
1213
+ }
1214
+ /**
1215
+ * Routing: random selection.
1216
+ */
1217
+ routeRandom() {
1218
+ const available = this.getAvailableBackends();
1219
+ if (available.length === 0) {
1220
+ return null;
1221
+ }
1222
+ const index = Math.floor(Math.random() * available.length);
1223
+ return available[index] ?? null;
1224
+ }
1225
+ /**
1226
+ * Select backend based on capability requirements.
1227
+ * Returns the backend name with the best matching model, or null if none match.
1228
+ */
1229
+ async selectBackendByCapabilities(request) {
1230
+ // Extract capability requirements from request metadata
1231
+ const capabilityRequirements = request.metadata?.custom?.capabilityRequirements;
1232
+ // Build requirements from config if not provided
1233
+ const requirements = {
1234
+ required: capabilityRequirements?.required,
1235
+ preferred: capabilityRequirements?.preferred,
1236
+ optimization: capabilityRequirements?.optimization ?? this.config.optimization ?? 'balanced',
1237
+ weights: capabilityRequirements?.weights ?? this.config.optimizationWeights,
1238
+ };
1239
+ // Collect available models from all backends
1240
+ const availableModels = [];
1241
+ for (const backendName of this.getAvailableBackends()) {
1242
+ const backend = this.backends.get(backendName)?.adapter;
1243
+ if (!backend) {
1244
+ continue;
1245
+ }
1246
+ // Try to get models from backend's listModels()
1247
+ if (typeof backend.listModels === 'function') {
1248
+ try {
1249
+ const result = await backend.listModels();
1250
+ for (const model of result.models) {
1251
+ availableModels.push({ model, backend: backendName });
1252
+ }
1253
+ }
1254
+ catch {
1255
+ // If listModels fails, try to infer from requested model
1256
+ const requestedModel = request.parameters?.model;
1257
+ if (requestedModel) {
1258
+ const inferredModel = {
1259
+ id: requestedModel,
1260
+ name: requestedModel,
1261
+ capabilities: (0, capability_inference_js_1.inferCapabilities)(requestedModel),
1262
+ };
1263
+ availableModels.push({ model: inferredModel, backend: backendName });
1264
+ }
1265
+ }
1266
+ }
1267
+ else {
1268
+ // Backend doesn't support listModels, try to infer from requested model
1269
+ const requestedModel = request.parameters?.model;
1270
+ if (requestedModel) {
1271
+ const inferredModel = {
1272
+ id: requestedModel,
1273
+ name: requestedModel,
1274
+ capabilities: (0, capability_inference_js_1.inferCapabilities)(requestedModel),
1275
+ };
1276
+ availableModels.push({ model: inferredModel, backend: backendName });
1277
+ }
1278
+ }
1279
+ }
1280
+ if (availableModels.length === 0) {
1281
+ return null;
1282
+ }
1283
+ // Find best match
1284
+ const bestMatch = (0, capability_matcher_js_1.findBestModel)(requirements, availableModels);
1285
+ if (!bestMatch?.meetsRequirements) {
1286
+ return null;
1287
+ }
1288
+ return bestMatch.backend;
1289
+ }
1290
+ /**
1291
+ * Calculate backend statistics.
1292
+ */
1293
+ calculateBackendStats(state) {
1294
+ const successRate = state.totalRequests > 0 ? (state.successfulRequests / state.totalRequests) * 100 : 0;
1295
+ const sortedLatencies = [...state.latencies].sort((a, b) => a - b);
1296
+ const avgLatency = sortedLatencies.length > 0
1297
+ ? sortedLatencies.reduce((a, b) => a + b, 0) / sortedLatencies.length
1298
+ : 0;
1299
+ const p50 = sortedLatencies[Math.floor(sortedLatencies.length * 0.5)] ?? 0;
1300
+ const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)] ?? 0;
1301
+ const p99 = sortedLatencies[Math.floor(sortedLatencies.length * 0.99)] ?? 0;
1302
+ const avgCost = state.totalRequests > 0 ? state.totalCost / state.totalRequests : 0;
1303
+ return {
1304
+ totalRequests: state.totalRequests,
1305
+ successfulRequests: state.successfulRequests,
1306
+ failedRequests: state.failedRequests,
1307
+ successRate,
1308
+ averageLatencyMs: avgLatency,
1309
+ p50LatencyMs: p50,
1310
+ p95LatencyMs: p95,
1311
+ p99LatencyMs: p99,
1312
+ totalCost: this.config.trackCost ? state.totalCost : undefined,
1313
+ averageCost: this.config.trackCost ? avgCost : undefined,
1314
+ };
1315
+ }
1316
+ /**
1317
+ * Create backend info from state.
1318
+ */
1319
+ createBackendInfo(name, state) {
1320
+ return {
1321
+ name,
1322
+ adapter: state.adapter,
1323
+ metadata: state.adapter.metadata,
1324
+ isHealthy: state.isHealthy,
1325
+ lastHealthCheck: state.lastHealthCheck,
1326
+ circuitBreakerState: state.circuitBreakerState,
1327
+ consecutiveFailures: state.consecutiveFailures,
1328
+ stats: this.calculateBackendStats(state),
1329
+ };
1330
+ }
1331
+ /**
1332
+ * Start periodic health checking.
1333
+ */
1334
+ startHealthChecking() {
1335
+ if (this.healthCheckInterval) {
1336
+ return;
1337
+ }
1338
+ const interval = this.config.healthCheckInterval ?? 0;
1339
+ if (interval <= 0) {
1340
+ return;
1341
+ }
1342
+ this.healthCheckInterval = setInterval(() => {
1343
+ this.checkHealth().catch(() => {
1344
+ // Ignore errors in background health checks
1345
+ });
1346
+ }, interval);
1347
+ }
1348
+ /**
1349
+ * Combine multiple abort signals.
1350
+ */
1351
+ combineSignals(signals) {
1352
+ const controller = new AbortController();
1353
+ for (const signal of signals) {
1354
+ if (signal.aborted) {
1355
+ controller.abort();
1356
+ break;
1357
+ }
1358
+ signal.addEventListener('abort', () => controller.abort(), { once: true });
1359
+ }
1360
+ return controller.signal;
1361
+ }
1362
+ /**
1363
+ * Wrap unknown error as AdapterError.
1364
+ */
1365
+ wrapError(error) {
1366
+ if (error instanceof ai_matey_errors_1.AdapterError) {
1367
+ return error;
1368
+ }
1369
+ return new ai_matey_errors_1.AdapterError({
1370
+ code: ai_matey_errors_1.ErrorCode.INTERNAL_ERROR,
1371
+ message: error instanceof Error ? error.message : String(error),
1372
+ isRetryable: false,
1373
+ provenance: { router: this.metadata.name },
1374
+ cause: error instanceof Error ? error : undefined,
1375
+ });
1376
+ }
1377
+ }
1378
+ exports.Router = Router;
1379
+ // ============================================================================
1380
+ // Factory Function
1381
+ // ============================================================================
1382
+ /**
1383
+ * Create a new Router instance.
1384
+ */
1385
+ function createRouter(config) {
1386
+ return new Router(config);
1387
+ }
1388
+ //# sourceMappingURL=router.js.map