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.
- package/README.md +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- 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
|
+
}
|