responses-proxy 0.1.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 (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,377 @@
1
+ import { fetchProviderUsage } from "./provider-usage.js";
2
+ export class RoutingEngine {
3
+ providerRepository;
4
+ healthCheckCache;
5
+ healthCacheTtl;
6
+ constructor(providerRepository, healthCheckCache = new Map(), healthCacheTtl = 30000 // 30 seconds
7
+ ) {
8
+ this.providerRepository = providerRepository;
9
+ this.healthCheckCache = healthCheckCache;
10
+ this.healthCacheTtl = healthCacheTtl;
11
+ }
12
+ /**
13
+ * Select the best provider for a request using multi-tier routing
14
+ */
15
+ async selectProvider(combo, request) {
16
+ const startTime = Date.now();
17
+ let fallbackCount = 0;
18
+ let retryCount = 0;
19
+ // Get enabled tiers sorted by priority
20
+ const enabledTiers = combo.tiers
21
+ .filter(tier => tier.isEnabled)
22
+ .sort((a, b) => a.priority - b.priority);
23
+ if (enabledTiers.length === 0) {
24
+ return {
25
+ success: false,
26
+ error: 'No enabled tiers available',
27
+ selectionTime: Date.now() - startTime,
28
+ fallbackCount,
29
+ retryCount
30
+ };
31
+ }
32
+ // Try each tier in priority order
33
+ for (let tierIndex = 0; tierIndex < enabledTiers.length; tierIndex++) {
34
+ const tier = enabledTiers[tierIndex];
35
+ try {
36
+ // Get eligible providers for this tier
37
+ const eligibleProviders = await this.getEligibleProviders(tier, request);
38
+ if (eligibleProviders.length > 0) {
39
+ // Select provider based on load balancing strategy
40
+ const selectedProvider = await this.selectFromTier(eligibleProviders, combo.policies.loadBalancing, request);
41
+ if (selectedProvider) {
42
+ return {
43
+ success: true,
44
+ provider: selectedProvider.provider,
45
+ tier: tier.name,
46
+ selectionTime: Date.now() - startTime,
47
+ eligibilityScore: selectedProvider.eligibilityScore,
48
+ fallbackCount,
49
+ retryCount
50
+ };
51
+ }
52
+ }
53
+ // If this isn't the last tier, wait for fallback delay
54
+ if (tierIndex < enabledTiers.length - 1) {
55
+ fallbackCount++;
56
+ if (tier.fallbackDelay > 0) {
57
+ await new Promise(resolve => setTimeout(resolve, tier.fallbackDelay));
58
+ }
59
+ }
60
+ }
61
+ catch (error) {
62
+ console.error(`Error selecting from tier ${tier.name}:`, error);
63
+ // Continue to next tier on error
64
+ fallbackCount++;
65
+ }
66
+ }
67
+ return {
68
+ success: false,
69
+ error: 'No eligible providers available in any tier',
70
+ selectionTime: Date.now() - startTime,
71
+ fallbackCount,
72
+ retryCount
73
+ };
74
+ }
75
+ /**
76
+ * Get eligible providers for a tier based on health and configuration
77
+ */
78
+ async getEligibleProviders(tier, request) {
79
+ const eligibleProviders = [];
80
+ for (const binding of tier.providers) {
81
+ if (!binding.isEnabled) {
82
+ continue;
83
+ }
84
+ try {
85
+ // Get provider from repository
86
+ const provider = this.providerRepository.getProvider(binding.providerId);
87
+ if (!provider) {
88
+ console.warn(`Provider ${binding.providerId} not found`);
89
+ continue;
90
+ }
91
+ // Get provider health
92
+ const health = await this.getProviderHealth(binding.providerId);
93
+ // Calculate eligibility score
94
+ const eligibilityScore = this.calculateEligibilityScore(provider, health, request, tier);
95
+ // Check if provider meets minimum eligibility threshold
96
+ const minThreshold = this.getMinEligibilityThreshold(tier, request);
97
+ if (eligibilityScore >= minThreshold) {
98
+ eligibleProviders.push({
99
+ binding,
100
+ provider,
101
+ health,
102
+ eligibilityScore
103
+ });
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.error(`Error evaluating provider ${binding.providerId}:`, error);
108
+ // Skip this provider on error
109
+ }
110
+ }
111
+ return eligibleProviders;
112
+ }
113
+ /**
114
+ * Select a provider from eligible providers based on load balancing strategy
115
+ */
116
+ async selectFromTier(eligibleProviders, strategy, request) {
117
+ if (eligibleProviders.length === 0) {
118
+ return null;
119
+ }
120
+ if (eligibleProviders.length === 1) {
121
+ return eligibleProviders[0];
122
+ }
123
+ switch (strategy) {
124
+ case 'weighted':
125
+ return this.selectByWeight(eligibleProviders);
126
+ case 'health_based':
127
+ return this.selectByHealth(eligibleProviders);
128
+ case 'cost_optimized':
129
+ return this.selectByCost(eligibleProviders);
130
+ case 'round_robin':
131
+ return this.selectRoundRobin(eligibleProviders, request);
132
+ case 'least_connections':
133
+ return this.selectLeastConnections(eligibleProviders);
134
+ case 'random':
135
+ return this.selectRandom(eligibleProviders);
136
+ default:
137
+ // Default to weighted selection
138
+ return this.selectByWeight(eligibleProviders);
139
+ }
140
+ }
141
+ /**
142
+ * Calculate eligibility score for a provider (0-100)
143
+ */
144
+ calculateEligibilityScore(provider, health, request, tier) {
145
+ let score = 100;
146
+ // Response time factor (20% weight)
147
+ if (health.averageResponseTime > 5000) {
148
+ score -= 20;
149
+ }
150
+ else if (health.averageResponseTime > 2000) {
151
+ score -= 10;
152
+ }
153
+ else if (health.averageResponseTime > 1000) {
154
+ score -= 5;
155
+ }
156
+ // Error rate factor (30% weight)
157
+ if (health.errorRate > 0.1) {
158
+ score -= 30;
159
+ }
160
+ else if (health.errorRate > 0.05) {
161
+ score -= 15;
162
+ }
163
+ else if (health.errorRate > 0.02) {
164
+ score -= 8;
165
+ }
166
+ // Quota availability factor (25% weight)
167
+ if (health.quotaUsagePercent > 95) {
168
+ score -= 25;
169
+ }
170
+ else if (health.quotaUsagePercent > 80) {
171
+ score -= 10;
172
+ }
173
+ else if (health.quotaUsagePercent > 60) {
174
+ score -= 5;
175
+ }
176
+ // Account status factor (25% weight)
177
+ if (!health.hasValidAccounts) {
178
+ score -= 25;
179
+ }
180
+ else if (health.accountsNearExpiry) {
181
+ score -= 10;
182
+ }
183
+ // Health threshold check
184
+ if (tier.healthThreshold && tier.healthThreshold.length > 0) {
185
+ const providerHealthStatus = this.getProviderHealthStatus(health);
186
+ if (!tier.healthThreshold.includes(providerHealthStatus)) {
187
+ score -= 50; // Significant penalty for not meeting health threshold
188
+ }
189
+ }
190
+ // Priority boost for high priority requests
191
+ if (request.priority === 'high' && provider.capabilities.usageCheckEnabled) {
192
+ score += 5;
193
+ }
194
+ return Math.max(0, Math.min(100, score));
195
+ }
196
+ /**
197
+ * Get provider health status category
198
+ */
199
+ getProviderHealthStatus(health) {
200
+ if (!health.isHealthy || health.errorRate > 0.1 || health.averageResponseTime > 5000) {
201
+ return 'degraded';
202
+ }
203
+ if (health.quotaUsagePercent > 90) {
204
+ return 'rate_limited';
205
+ }
206
+ return 'healthy';
207
+ }
208
+ /**
209
+ * Get minimum eligibility threshold based on tier and request
210
+ */
211
+ getMinEligibilityThreshold(tier, request) {
212
+ // Higher tiers have higher standards
213
+ switch (tier.tier) {
214
+ case 'subscription':
215
+ return 70;
216
+ case 'cheap':
217
+ return 50;
218
+ case 'free':
219
+ return 30;
220
+ case 'custom':
221
+ return 40;
222
+ default:
223
+ return 50;
224
+ }
225
+ }
226
+ /**
227
+ * Get cached provider health or fetch fresh data
228
+ */
229
+ async getProviderHealth(providerId) {
230
+ const cached = this.healthCheckCache.get(providerId);
231
+ const now = Date.now();
232
+ if (cached && (now - cached.lastChecked) < this.healthCacheTtl) {
233
+ return cached;
234
+ }
235
+ // Fetch fresh health data
236
+ const health = await this.fetchProviderHealth(providerId);
237
+ this.healthCheckCache.set(providerId, health);
238
+ return health;
239
+ }
240
+ /**
241
+ * Fetch provider health from various sources
242
+ */
243
+ async fetchProviderHealth(providerId) {
244
+ const provider = this.providerRepository.getProvider(providerId);
245
+ if (!provider) {
246
+ throw new Error(`Provider ${providerId} not found`);
247
+ }
248
+ let quotaUsagePercent = 0;
249
+ let isHealthy = true;
250
+ let averageResponseTime = 1000; // Default 1s
251
+ let errorRate = 0;
252
+ // Check provider usage if enabled
253
+ if (provider.capabilities.usageCheckEnabled && provider.capabilities.usageCheckUrl) {
254
+ try {
255
+ const usage = await fetchProviderUsage({
256
+ apiKey: provider.providerApiKeys[0],
257
+ requestId: `route-health-${providerId}-${Date.now()}`,
258
+ logger: {
259
+ info: () => { },
260
+ warn: () => { },
261
+ error: () => { },
262
+ debug: () => { },
263
+ trace: () => { },
264
+ fatal: () => { },
265
+ silent: () => { },
266
+ },
267
+ timeoutMs: 5000,
268
+ url: provider.capabilities.usageCheckUrl,
269
+ });
270
+ if (usage && usage.limit !== undefined && usage.used !== undefined) {
271
+ quotaUsagePercent = (usage.used / usage.limit) * 100;
272
+ isHealthy = quotaUsagePercent < 95;
273
+ }
274
+ }
275
+ catch (error) {
276
+ console.warn(`Failed to check usage for provider ${providerId}:`, error);
277
+ // Assume degraded if we can't check usage
278
+ isHealthy = false;
279
+ errorRate = 0.1;
280
+ }
281
+ }
282
+ // Check account status based on auth mode
283
+ let hasValidAccounts = true;
284
+ let accountsNearExpiry = false;
285
+ if (provider.authMode === 'chatgpt_oauth') {
286
+ // TODO: Check ChatGPT OAuth account status
287
+ // For now, assume valid if provider has chatgptAccountId
288
+ hasValidAccounts = !!provider.chatgptAccountId;
289
+ }
290
+ else if (provider.authMode === 'kiro') {
291
+ // TODO: Check Kiro account status
292
+ // For now, assume valid
293
+ hasValidAccounts = true;
294
+ }
295
+ else if (provider.authMode === 'api_key') {
296
+ // Check if provider has API keys
297
+ hasValidAccounts = provider.providerApiKeys.length > 0;
298
+ }
299
+ return {
300
+ providerId,
301
+ isHealthy,
302
+ averageResponseTime,
303
+ errorRate,
304
+ quotaUsagePercent,
305
+ hasValidAccounts,
306
+ accountsNearExpiry,
307
+ lastChecked: Date.now()
308
+ };
309
+ }
310
+ // Load balancing strategy implementations
311
+ selectByWeight(providers) {
312
+ const totalWeight = providers.reduce((sum, p) => sum + p.binding.weight, 0);
313
+ const random = Math.random() * totalWeight;
314
+ let currentWeight = 0;
315
+ for (const provider of providers) {
316
+ currentWeight += provider.binding.weight;
317
+ if (random <= currentWeight) {
318
+ return provider;
319
+ }
320
+ }
321
+ return providers[0]; // Fallback
322
+ }
323
+ selectByHealth(providers) {
324
+ // Sort by eligibility score (highest first)
325
+ const sorted = [...providers].sort((a, b) => b.eligibilityScore - a.eligibilityScore);
326
+ return sorted[0];
327
+ }
328
+ selectByCost(providers) {
329
+ // For now, prefer providers in 'cheap' or 'free' tiers
330
+ // TODO: Implement actual cost calculation based on provider pricing
331
+ const cheapProviders = providers.filter(p => p.provider.name.toLowerCase().includes('free') ||
332
+ p.provider.name.toLowerCase().includes('cheap'));
333
+ if (cheapProviders.length > 0) {
334
+ return this.selectByHealth(cheapProviders);
335
+ }
336
+ return this.selectByHealth(providers);
337
+ }
338
+ selectRoundRobin(providers, request) {
339
+ // Simple round-robin based on request hash
340
+ const hash = this.hashString(request.route + (request.clientRoute || ''));
341
+ const index = hash % providers.length;
342
+ return providers[index];
343
+ }
344
+ selectLeastConnections(providers) {
345
+ // TODO: Implement actual connection tracking
346
+ // For now, fall back to health-based selection
347
+ return this.selectByHealth(providers);
348
+ }
349
+ selectRandom(providers) {
350
+ const index = Math.floor(Math.random() * providers.length);
351
+ return providers[index];
352
+ }
353
+ hashString(str) {
354
+ let hash = 0;
355
+ for (let i = 0; i < str.length; i++) {
356
+ const char = str.charCodeAt(i);
357
+ hash = ((hash << 5) - hash) + char;
358
+ hash = hash & hash; // Convert to 32-bit integer
359
+ }
360
+ return Math.abs(hash);
361
+ }
362
+ /**
363
+ * Clear health cache (useful for testing or forced refresh)
364
+ */
365
+ clearHealthCache() {
366
+ this.healthCheckCache.clear();
367
+ }
368
+ /**
369
+ * Get current health cache stats
370
+ */
371
+ getHealthCacheStats() {
372
+ return {
373
+ size: this.healthCheckCache.size,
374
+ entries: Array.from(this.healthCheckCache.keys())
375
+ };
376
+ }
377
+ }
@@ -0,0 +1,155 @@
1
+ import { resolveProviderForRequest } from "./provider-routing.js";
2
+ /**
3
+ * Enhanced provider resolution that uses routing combos when available,
4
+ * falling back to the original simple provider selection logic.
5
+ */
6
+ export async function resolveProviderWithRouting(request, context) {
7
+ const { clientRoute, providers, providerHint, requestId, startedAt } = request;
8
+ const { routingComboRepository, routingEngine, healthService } = context;
9
+ // If explicit provider is requested, honor it directly (bypass routing combos)
10
+ if (providerHint.providerId || providerHint.providerName) {
11
+ const resolution = resolveProviderForRequest({
12
+ providers,
13
+ explicitProviderId: providerHint.providerId,
14
+ explicitProviderName: providerHint.providerName,
15
+ });
16
+ if ("error" in resolution) {
17
+ return { error: resolution.error };
18
+ }
19
+ return {
20
+ provider: resolution.provider,
21
+ matchReason: "explicit_provider",
22
+ selectionTime: Date.now() - startedAt
23
+ };
24
+ }
25
+ try {
26
+ // Check if client route has a routing combo assigned
27
+ const assignedComboId = await routingComboRepository.getClientRouteCombo(clientRoute);
28
+ if (assignedComboId) {
29
+ // Use routing combo for provider selection
30
+ const combo = await routingComboRepository.getComboById(assignedComboId);
31
+ if (combo && combo.isActive) {
32
+ const routingResult = await routingEngine.selectProvider(combo, {
33
+ route: typeof request.headers["x-proxy-route"] === "string" ? request.headers["x-proxy-route"] : "",
34
+ clientRoute,
35
+ startTime: startedAt,
36
+ priority: request.headers["x-priority"] === "high" ? "high" : "normal",
37
+ });
38
+ if (routingResult.success && routingResult.provider) {
39
+ return {
40
+ provider: routingResult.provider,
41
+ matchReason: "routing_combo",
42
+ routingComboId: combo.id,
43
+ tierName: routingResult.tier,
44
+ selectionTime: routingResult.selectionTime,
45
+ fallbackCount: routingResult.fallbackCount
46
+ };
47
+ }
48
+ // If routing combo failed, fall back to simple selection
49
+ console.warn(`Routing combo ${assignedComboId} failed for client route ${clientRoute}: ${routingResult.error}`);
50
+ }
51
+ }
52
+ // Check for default routing combo if no specific assignment
53
+ if (!assignedComboId) {
54
+ const defaultCombo = await routingComboRepository.getDefaultCombo();
55
+ if (defaultCombo && defaultCombo.isActive) {
56
+ const routingResult = await routingEngine.selectProvider(defaultCombo, {
57
+ route: typeof request.headers["x-proxy-route"] === "string" ? request.headers["x-proxy-route"] : "",
58
+ clientRoute,
59
+ startTime: startedAt,
60
+ priority: request.headers["x-priority"] === "high" ? "high" : "normal",
61
+ });
62
+ if (routingResult.success && routingResult.provider) {
63
+ return {
64
+ provider: routingResult.provider,
65
+ matchReason: "routing_combo",
66
+ routingComboId: defaultCombo.id,
67
+ tierName: routingResult.tier,
68
+ selectionTime: routingResult.selectionTime,
69
+ fallbackCount: routingResult.fallbackCount
70
+ };
71
+ }
72
+ }
73
+ }
74
+ // Fall back to original simple provider selection
75
+ const fallbackResolution = resolveProviderForRequest({
76
+ providers,
77
+ explicitProviderId: undefined,
78
+ explicitProviderName: undefined,
79
+ });
80
+ if ("error" in fallbackResolution) {
81
+ return { error: fallbackResolution.error };
82
+ }
83
+ return {
84
+ provider: fallbackResolution.provider,
85
+ matchReason: "fallback",
86
+ selectionTime: Date.now() - startedAt
87
+ };
88
+ }
89
+ catch (error) {
90
+ console.error(`Error in routing integration for client route ${clientRoute}:`, error);
91
+ // Fall back to original simple provider selection on any error
92
+ const fallbackResolution = resolveProviderForRequest({
93
+ providers,
94
+ explicitProviderId: undefined,
95
+ explicitProviderName: undefined,
96
+ });
97
+ if ("error" in fallbackResolution) {
98
+ return { error: fallbackResolution.error };
99
+ }
100
+ return {
101
+ provider: fallbackResolution.provider,
102
+ matchReason: "fallback",
103
+ selectionTime: Date.now() - startedAt
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Record request result for health tracking
109
+ */
110
+ export function recordRequestResult(context, providerId, responseTime, isError) {
111
+ try {
112
+ context.healthService.recordRequestResult(providerId, responseTime, isError);
113
+ }
114
+ catch (error) {
115
+ console.error(`Failed to record request result for provider ${providerId}:`, error);
116
+ }
117
+ }
118
+ /**
119
+ * Get routing combo assignment for a client route
120
+ */
121
+ export async function getClientRouteCombo(context, clientRoute) {
122
+ try {
123
+ return await context.routingComboRepository.getClientRouteCombo(clientRoute);
124
+ }
125
+ catch (error) {
126
+ console.error(`Failed to get routing combo for client route ${clientRoute}:`, error);
127
+ return null;
128
+ }
129
+ }
130
+ /**
131
+ * Assign a routing combo to a client route
132
+ */
133
+ export async function assignClientRouteCombo(context, clientRoute, comboId) {
134
+ try {
135
+ await context.routingComboRepository.assignClientRouteCombo(clientRoute, comboId);
136
+ return true;
137
+ }
138
+ catch (error) {
139
+ console.error(`Failed to assign routing combo ${comboId} to client route ${clientRoute}:`, error);
140
+ return false;
141
+ }
142
+ }
143
+ /**
144
+ * Remove routing combo assignment from a client route
145
+ */
146
+ export async function unassignClientRouteCombo(context, clientRoute) {
147
+ try {
148
+ await context.routingComboRepository.unassignClientRouteCombo(clientRoute);
149
+ return true;
150
+ }
151
+ catch (error) {
152
+ console.error(`Failed to unassign routing combo from client route ${clientRoute}:`, error);
153
+ return false;
154
+ }
155
+ }