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,229 @@
1
+ import { createHash } from "node:crypto";
2
+ export function buildPromptCacheLayout(request, options = {}) {
3
+ const inputItems = Array.isArray(request.input) ? request.input : undefined;
4
+ const split = splitInputForPromptCache(inputItems, options.summaryKeepRecentItems ?? 6);
5
+ const stableInputItems = split.stableItems;
6
+ const dynamicInputItems = split.dynamicItems;
7
+ const shouldSummarize = options.stableSummarizationEnabled === true &&
8
+ stableInputItems.length >= (options.summaryTriggerItems ?? 14) &&
9
+ dynamicInputItems.length > 0;
10
+ const summarizedStableItems = shouldSummarize
11
+ ? summarizeStableItems(stableInputItems)
12
+ : stableInputItems;
13
+ const includeStableConversationPrefix = shouldSummarize && summarizedStableItems.length > 0;
14
+ const stablePrefix = omitUndefined({
15
+ model: request.model,
16
+ instructions: mergeStableInstructions(typeof request.instructions === "string" ? request.instructions : undefined, shouldSummarize ? buildStableSummaryBlock(stableInputItems) : undefined),
17
+ tools: request.tools,
18
+ tool_choice: request.tool_choice,
19
+ parallel_tool_calls: request.parallel_tool_calls,
20
+ reasoning: request.reasoning,
21
+ text: request.text,
22
+ max_output_tokens: request.max_output_tokens,
23
+ max_tool_calls: request.max_tool_calls,
24
+ temperature: request.temperature,
25
+ top_p: request.top_p,
26
+ metadata: request.metadata,
27
+ user: request.user,
28
+ truncation: request.truncation,
29
+ include: request.include,
30
+ input: includeStableConversationPrefix ? summarizedStableItems : undefined,
31
+ });
32
+ const dynamicTail = omitUndefined({
33
+ input: includeStableConversationPrefix
34
+ ? dynamicInputItems.length > 0
35
+ ? dynamicInputItems
36
+ : request.input
37
+ : request.input,
38
+ });
39
+ const familySignature = stableStringify(omitUndefined({
40
+ model: request.model,
41
+ instructions: typeof request.instructions === "string" ? request.instructions : undefined,
42
+ tools: request.tools,
43
+ tool_choice: request.tool_choice,
44
+ parallel_tool_calls: request.parallel_tool_calls,
45
+ reasoning: request.reasoning,
46
+ text: request.text,
47
+ max_output_tokens: request.max_output_tokens,
48
+ max_tool_calls: request.max_tool_calls,
49
+ temperature: request.temperature,
50
+ top_p: request.top_p,
51
+ metadata: request.metadata,
52
+ user: request.user,
53
+ truncation: request.truncation,
54
+ include: request.include,
55
+ }));
56
+ const modelSlug = slugify(typeof request.model === "string" ? request.model : "unknown-model");
57
+ const familyId = `family:${modelSlug}:core:${shortHash(familySignature)}`;
58
+ const staticKey = `static:${familyId}:${shortHash(stableStringify(stablePrefix))}`;
59
+ const requestKey = `request:${staticKey}:${shortHash(stableStringify(dynamicTail))}`;
60
+ const promptCacheRetention = resolvePromptCacheRetention(familyId, staticKey, options);
61
+ return {
62
+ familyId,
63
+ staticKey,
64
+ requestKey,
65
+ stablePrefix,
66
+ dynamicTail,
67
+ summaryApplied: shouldSummarize,
68
+ summaryItemCount: shouldSummarize ? stableInputItems.length : 0,
69
+ promptCacheKey: options.enabled ? requestKey : undefined,
70
+ promptCacheRetention: options.enabled ? promptCacheRetention : undefined,
71
+ };
72
+ }
73
+ function splitInputForPromptCache(inputItems, keepRecentItems) {
74
+ if (!inputItems || inputItems.length === 0) {
75
+ return {
76
+ stableItems: [],
77
+ dynamicItems: [],
78
+ };
79
+ }
80
+ const minimumBoundaryIndex = Math.max(0, inputItems.length - keepRecentItems);
81
+ let boundaryIndex = minimumBoundaryIndex;
82
+ for (let index = inputItems.length - 1; index >= minimumBoundaryIndex; index -= 1) {
83
+ const item = inputItems[index];
84
+ if (isRecord(item) && item.role === "user") {
85
+ boundaryIndex = index;
86
+ break;
87
+ }
88
+ }
89
+ return {
90
+ stableItems: inputItems.slice(0, boundaryIndex),
91
+ dynamicItems: inputItems.slice(boundaryIndex),
92
+ };
93
+ }
94
+ function summarizeStableItems(items) {
95
+ const summary = buildStableSummaryBlock(items);
96
+ if (!summary) {
97
+ return items;
98
+ }
99
+ return [
100
+ {
101
+ role: "assistant",
102
+ content: [
103
+ {
104
+ type: "input_text",
105
+ text: summary,
106
+ },
107
+ ],
108
+ },
109
+ ];
110
+ }
111
+ function buildStableSummaryBlock(items) {
112
+ if (items.length === 0) {
113
+ return undefined;
114
+ }
115
+ const lines = items.map((item, index) => summarizeInputItem(item, index + 1)).filter(Boolean);
116
+ if (lines.length === 0) {
117
+ return undefined;
118
+ }
119
+ return [
120
+ "Stable conversation summary generated by responses-proxy.",
121
+ "This summary replaces older conversation history to preserve a reusable upstream prompt prefix.",
122
+ ...lines,
123
+ ].join("\n");
124
+ }
125
+ function summarizeInputItem(item, index) {
126
+ if (!isRecord(item)) {
127
+ return undefined;
128
+ }
129
+ if (typeof item.role === "string") {
130
+ const content = summarizeContent(item.content);
131
+ return content ? `${index}. ${item.role}: ${content}` : `${index}. ${item.role}`;
132
+ }
133
+ if (item.type === "function_call") {
134
+ const name = typeof item.name === "string" ? item.name : "function";
135
+ const args = typeof item.arguments === "string" ? compactText(item.arguments, 160) : "";
136
+ return `${index}. function_call ${name}${args ? ` args=${args}` : ""}`;
137
+ }
138
+ if (item.type === "function_call_output") {
139
+ const output = typeof item.output === "string" ? compactText(item.output, 160) : "";
140
+ return `${index}. function_call_output${output ? ` ${output}` : ""}`;
141
+ }
142
+ return `${index}. ${compactText(stableStringify(item), 160)}`;
143
+ }
144
+ function summarizeContent(content) {
145
+ if (typeof content === "string") {
146
+ return compactText(content, 160);
147
+ }
148
+ if (!Array.isArray(content)) {
149
+ return undefined;
150
+ }
151
+ const fragments = content
152
+ .map((part) => {
153
+ if (!isRecord(part)) {
154
+ return "";
155
+ }
156
+ if (typeof part.text === "string") {
157
+ return part.text;
158
+ }
159
+ if (typeof part.image_url === "string") {
160
+ return `[image:${compactText(part.image_url, 64)}]`;
161
+ }
162
+ return "";
163
+ })
164
+ .filter(Boolean)
165
+ .join(" ");
166
+ return fragments ? compactText(fragments, 160) : undefined;
167
+ }
168
+ function mergeStableInstructions(baseInstructions, summaryBlock) {
169
+ const parts = [baseInstructions, summaryBlock].filter((value) => typeof value === "string" && value.trim().length > 0);
170
+ if (parts.length === 0) {
171
+ return undefined;
172
+ }
173
+ return parts.join("\n\n");
174
+ }
175
+ function resolvePromptCacheRetention(familyId, staticKey, options) {
176
+ if (options.retentionByStaticKeyEnabled) {
177
+ const bestStaticMatch = [...(options.staticKeyRetentionRules ?? [])]
178
+ .filter((rule) => staticKey.startsWith(rule.prefix))
179
+ .sort((left, right) => right.prefix.length - left.prefix.length)[0];
180
+ if (bestStaticMatch) {
181
+ return bestStaticMatch.retention;
182
+ }
183
+ }
184
+ if (!options.retentionByFamilyEnabled) {
185
+ return options.defaultRetention;
186
+ }
187
+ const bestMatch = [...(options.familyRetentionRules ?? [])]
188
+ .filter((rule) => familyId.startsWith(rule.prefix))
189
+ .sort((left, right) => right.prefix.length - left.prefix.length)[0];
190
+ return bestMatch?.retention ?? options.defaultRetention;
191
+ }
192
+ function stableStringify(value) {
193
+ return JSON.stringify(sortUnknown(value));
194
+ }
195
+ function sortUnknown(value) {
196
+ if (Array.isArray(value)) {
197
+ return value.map((item) => sortUnknown(item));
198
+ }
199
+ if (!isRecord(value)) {
200
+ return value;
201
+ }
202
+ return Object.fromEntries(Object.entries(value)
203
+ .filter(([, nested]) => nested !== undefined)
204
+ .sort(([left], [right]) => left.localeCompare(right))
205
+ .map(([key, nested]) => [key, sortUnknown(nested)]));
206
+ }
207
+ function omitUndefined(value) {
208
+ return Object.fromEntries(Object.entries(value).filter(([, nested]) => nested !== undefined));
209
+ }
210
+ function shortHash(value) {
211
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
212
+ }
213
+ function slugify(value) {
214
+ return value
215
+ .trim()
216
+ .toLowerCase()
217
+ .replace(/[^a-z0-9]+/g, "-")
218
+ .replace(/^-+|-+$/g, "") || "unknown";
219
+ }
220
+ function compactText(value, maxLength) {
221
+ const compact = value.replace(/\s+/g, " ").trim();
222
+ if (compact.length <= maxLength) {
223
+ return compact;
224
+ }
225
+ return `${compact.slice(0, maxLength - 1)}…`;
226
+ }
227
+ function isRecord(value) {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
229
+ }
@@ -0,0 +1,404 @@
1
+ import { fetchProviderUsage } from "./provider-usage.js";
2
+ const DEFAULT_HEALTH_THRESHOLDS = {
3
+ responseTime: {
4
+ good: 2000, // 2s
5
+ degraded: 5000 // 5s
6
+ },
7
+ errorRate: {
8
+ good: 0.02, // 2%
9
+ degraded: 0.1 // 10%
10
+ },
11
+ quotaUsage: {
12
+ warning: 80, // 80%
13
+ critical: 95 // 95%
14
+ }
15
+ };
16
+ export class ProviderHealthService {
17
+ providerRepository;
18
+ chatGptOAuthStore;
19
+ kiroTokenStore;
20
+ healthCheckInterval;
21
+ thresholds;
22
+ healthCache = new Map();
23
+ healthCheckIntervals = new Map();
24
+ responseTimeHistory = new Map();
25
+ errorHistory = new Map();
26
+ constructor(providerRepository, chatGptOAuthStore, kiroTokenStore, healthCheckInterval = 30000, // 30 seconds
27
+ thresholds = DEFAULT_HEALTH_THRESHOLDS) {
28
+ this.providerRepository = providerRepository;
29
+ this.chatGptOAuthStore = chatGptOAuthStore;
30
+ this.kiroTokenStore = kiroTokenStore;
31
+ this.healthCheckInterval = healthCheckInterval;
32
+ this.thresholds = thresholds;
33
+ }
34
+ /**
35
+ * Start health monitoring for all providers
36
+ */
37
+ startHealthMonitoring() {
38
+ const providers = this.providerRepository.listProviders();
39
+ for (const provider of providers) {
40
+ this.startProviderHealthCheck(provider.id);
41
+ }
42
+ console.log(`Started health monitoring for ${providers.length} providers`);
43
+ }
44
+ /**
45
+ * Stop health monitoring for all providers
46
+ */
47
+ stopHealthMonitoring() {
48
+ for (const [providerId, interval] of this.healthCheckIntervals) {
49
+ clearInterval(interval);
50
+ }
51
+ this.healthCheckIntervals.clear();
52
+ console.log('Stopped health monitoring for all providers');
53
+ }
54
+ /**
55
+ * Start health checking for a specific provider
56
+ */
57
+ startProviderHealthCheck(providerId) {
58
+ // Clear existing interval if any
59
+ const existingInterval = this.healthCheckIntervals.get(providerId);
60
+ if (existingInterval) {
61
+ clearInterval(existingInterval);
62
+ }
63
+ // Perform initial health check
64
+ this.checkProviderHealth(providerId);
65
+ // Set up recurring health checks
66
+ const interval = setInterval(() => {
67
+ this.checkProviderHealth(providerId);
68
+ }, this.healthCheckInterval);
69
+ this.healthCheckIntervals.set(providerId, interval);
70
+ }
71
+ /**
72
+ * Stop health checking for a specific provider
73
+ */
74
+ stopProviderHealthCheck(providerId) {
75
+ const interval = this.healthCheckIntervals.get(providerId);
76
+ if (interval) {
77
+ clearInterval(interval);
78
+ this.healthCheckIntervals.delete(providerId);
79
+ }
80
+ }
81
+ /**
82
+ * Get current health metrics for a provider
83
+ */
84
+ getProviderHealth(providerId) {
85
+ return this.healthCache.get(providerId) || null;
86
+ }
87
+ /**
88
+ * Get health metrics for all providers
89
+ */
90
+ getAllProviderHealth() {
91
+ return new Map(this.healthCache);
92
+ }
93
+ /**
94
+ * Force a health check for a specific provider
95
+ */
96
+ async forceHealthCheck(providerId) {
97
+ return this.checkProviderHealth(providerId);
98
+ }
99
+ /**
100
+ * Record a request result for health tracking
101
+ */
102
+ recordRequestResult(providerId, responseTime, isError) {
103
+ // Update response time history
104
+ const responseHistory = this.responseTimeHistory.get(providerId) || [];
105
+ responseHistory.push(responseTime);
106
+ // Keep only last 100 response times
107
+ if (responseHistory.length > 100) {
108
+ responseHistory.shift();
109
+ }
110
+ this.responseTimeHistory.set(providerId, responseHistory);
111
+ // Update error history
112
+ const errorHistory = this.errorHistory.get(providerId) || [];
113
+ errorHistory.push({
114
+ timestamp: Date.now(),
115
+ error: isError
116
+ });
117
+ // Keep only last hour of error history
118
+ const oneHourAgo = Date.now() - (60 * 60 * 1000);
119
+ const recentErrors = errorHistory.filter(e => e.timestamp > oneHourAgo);
120
+ this.errorHistory.set(providerId, recentErrors);
121
+ // Update cached health metrics if they exist
122
+ const cachedHealth = this.healthCache.get(providerId);
123
+ if (cachedHealth) {
124
+ this.updateHealthMetricsFromHistory(providerId, cachedHealth);
125
+ }
126
+ }
127
+ /**
128
+ * Perform health check for a specific provider
129
+ */
130
+ async checkProviderHealth(providerId) {
131
+ try {
132
+ const provider = this.providerRepository.getProvider(providerId);
133
+ if (!provider) {
134
+ return {
135
+ success: false,
136
+ error: `Provider ${providerId} not found`
137
+ };
138
+ }
139
+ const now = Date.now();
140
+ // Check quota status
141
+ const quotaStatus = await this.checkProviderQuota(provider);
142
+ // Check account status
143
+ const accountStatus = await this.checkAccountStatus(provider);
144
+ // Get response time metrics from history
145
+ const responseTimeMetrics = this.calculateResponseTimeMetrics(providerId);
146
+ // Get error rate metrics from history
147
+ const errorRateMetrics = this.calculateErrorRateMetrics(providerId);
148
+ // Calculate overall health score
149
+ const healthScore = this.calculateHealthScore(responseTimeMetrics, errorRateMetrics, quotaStatus, accountStatus);
150
+ const metrics = {
151
+ providerId,
152
+ isHealthy: healthScore >= 70, // Healthy if score >= 70
153
+ healthScore,
154
+ responseTime: responseTimeMetrics,
155
+ errorRate: errorRateMetrics,
156
+ quotaStatus,
157
+ accountStatus,
158
+ lastChecked: now,
159
+ lastUpdated: now
160
+ };
161
+ // Cache the metrics
162
+ this.healthCache.set(providerId, metrics);
163
+ return {
164
+ success: true,
165
+ metrics
166
+ };
167
+ }
168
+ catch (error) {
169
+ console.error(`Health check failed for provider ${providerId}:`, error);
170
+ return {
171
+ success: false,
172
+ error: error instanceof Error ? error.message : String(error)
173
+ };
174
+ }
175
+ }
176
+ /**
177
+ * Check provider quota status
178
+ */
179
+ async checkProviderQuota(provider) {
180
+ if (!provider.capabilities.usageCheckEnabled || !provider.capabilities.usageCheckUrl) {
181
+ return {
182
+ usagePercent: 0,
183
+ remaining: -1,
184
+ limit: -1
185
+ };
186
+ }
187
+ try {
188
+ const usage = await fetchProviderUsage({
189
+ apiKey: provider.providerApiKeys[0],
190
+ requestId: `health-${provider.id}-${Date.now()}`,
191
+ logger: {
192
+ info: (...args) => console.log(...args),
193
+ warn: (...args) => console.warn(...args),
194
+ error: (...args) => console.error(...args),
195
+ debug: (...args) => console.debug(...args),
196
+ trace: (...args) => console.trace(...args),
197
+ fatal: (...args) => console.error(...args),
198
+ silent: () => { },
199
+ },
200
+ timeoutMs: 5000,
201
+ url: provider.capabilities.usageCheckUrl,
202
+ });
203
+ if (usage) {
204
+ const limit = usage.limit ?? -1;
205
+ const used = usage.used ?? 0;
206
+ const remaining = usage.remaining ?? (limit !== -1 ? limit - used : -1);
207
+ const usagePercent = limit > 0 ? (used / limit) * 100 : 0;
208
+ const resetTime = typeof usage.raw?.resetTime === 'string' ? usage.raw.resetTime : undefined;
209
+ return {
210
+ usagePercent,
211
+ remaining,
212
+ limit,
213
+ resetTime
214
+ };
215
+ }
216
+ }
217
+ catch (error) {
218
+ console.warn(`Failed to check quota for provider ${provider.id}:`, error);
219
+ }
220
+ return {
221
+ usagePercent: 0,
222
+ remaining: -1,
223
+ limit: -1
224
+ };
225
+ }
226
+ /**
227
+ * Check account status for the provider
228
+ */
229
+ async checkAccountStatus(provider) {
230
+ let hasValidAccounts = true;
231
+ let accountsNearExpiry = false;
232
+ let activeAccounts = 0;
233
+ let totalAccounts = 0;
234
+ try {
235
+ if (provider.authMode === 'chatgpt_oauth' && this.chatGptOAuthStore) {
236
+ const accounts = this.chatGptOAuthStore.listAccounts();
237
+ totalAccounts = accounts.length;
238
+ for (const account of accounts) {
239
+ if (account.accessToken && account.refreshToken) {
240
+ activeAccounts++;
241
+ // Check if token is near expiry (within 24 hours)
242
+ if (account.expiresAt) {
243
+ const expiryTime = new Date(account.expiresAt).getTime();
244
+ const twentyFourHours = 24 * 60 * 60 * 1000;
245
+ if (expiryTime - Date.now() < twentyFourHours) {
246
+ accountsNearExpiry = true;
247
+ }
248
+ }
249
+ }
250
+ }
251
+ hasValidAccounts = activeAccounts > 0;
252
+ }
253
+ else if (provider.authMode === 'kiro' && this.kiroTokenStore) {
254
+ const accounts = this.kiroTokenStore.listAccounts();
255
+ totalAccounts = accounts.length;
256
+ for (const account of accounts) {
257
+ if (account.isActive && account.accessToken) {
258
+ activeAccounts++;
259
+ // Check if Kiro token is near expiry
260
+ if (account.expiresAt) {
261
+ const expiryTime = new Date(account.expiresAt).getTime();
262
+ const twentyFourHours = 24 * 60 * 60 * 1000;
263
+ if (expiryTime - Date.now() < twentyFourHours) {
264
+ accountsNearExpiry = true;
265
+ }
266
+ }
267
+ }
268
+ }
269
+ hasValidAccounts = activeAccounts > 0;
270
+ }
271
+ else if (provider.authMode === 'api_key') {
272
+ // For API key providers, check if they have keys configured
273
+ totalAccounts = provider.providerApiKeys.length;
274
+ activeAccounts = provider.providerApiKeys.length;
275
+ hasValidAccounts = provider.providerApiKeys.length > 0;
276
+ }
277
+ }
278
+ catch (error) {
279
+ console.warn(`Failed to check account status for provider ${provider.id}:`, error);
280
+ hasValidAccounts = false;
281
+ }
282
+ return {
283
+ hasValidAccounts,
284
+ accountsNearExpiry,
285
+ activeAccounts,
286
+ totalAccounts
287
+ };
288
+ }
289
+ /**
290
+ * Calculate response time metrics from history
291
+ */
292
+ calculateResponseTimeMetrics(providerId) {
293
+ const history = this.responseTimeHistory.get(providerId) || [];
294
+ if (history.length === 0) {
295
+ return {
296
+ average: 1000, // Default 1s
297
+ p95: 1000,
298
+ p99: 1000
299
+ };
300
+ }
301
+ const sorted = [...history].sort((a, b) => a - b);
302
+ const average = history.reduce((sum, time) => sum + time, 0) / history.length;
303
+ const p95Index = Math.floor(sorted.length * 0.95);
304
+ const p99Index = Math.floor(sorted.length * 0.99);
305
+ return {
306
+ average,
307
+ p95: sorted[p95Index] || average,
308
+ p99: sorted[p99Index] || average
309
+ };
310
+ }
311
+ /**
312
+ * Calculate error rate metrics from history
313
+ */
314
+ calculateErrorRateMetrics(providerId) {
315
+ const history = this.errorHistory.get(providerId) || [];
316
+ if (history.length === 0) {
317
+ return {
318
+ rate: 0,
319
+ recentErrors: 0,
320
+ totalRequests: 0
321
+ };
322
+ }
323
+ const recentErrors = history.filter(e => e.error).length;
324
+ const totalRequests = history.length;
325
+ const rate = totalRequests > 0 ? recentErrors / totalRequests : 0;
326
+ return {
327
+ rate,
328
+ recentErrors,
329
+ totalRequests
330
+ };
331
+ }
332
+ /**
333
+ * Calculate overall health score (0-100)
334
+ */
335
+ calculateHealthScore(responseTime, errorRate, quotaStatus, accountStatus) {
336
+ let score = 100;
337
+ // Response time factor (25% weight)
338
+ if (responseTime.average > this.thresholds.responseTime.degraded) {
339
+ score -= 25;
340
+ }
341
+ else if (responseTime.average > this.thresholds.responseTime.good) {
342
+ score -= 12;
343
+ }
344
+ // Error rate factor (30% weight)
345
+ if (errorRate.rate > this.thresholds.errorRate.degraded) {
346
+ score -= 30;
347
+ }
348
+ else if (errorRate.rate > this.thresholds.errorRate.good) {
349
+ score -= 15;
350
+ }
351
+ // Quota status factor (25% weight)
352
+ if (quotaStatus.usagePercent > this.thresholds.quotaUsage.critical) {
353
+ score -= 25;
354
+ }
355
+ else if (quotaStatus.usagePercent > this.thresholds.quotaUsage.warning) {
356
+ score -= 12;
357
+ }
358
+ // Account status factor (20% weight)
359
+ if (!accountStatus.hasValidAccounts) {
360
+ score -= 20;
361
+ }
362
+ else if (accountStatus.accountsNearExpiry) {
363
+ score -= 10;
364
+ }
365
+ return Math.max(0, Math.min(100, score));
366
+ }
367
+ /**
368
+ * Update health metrics from request history
369
+ */
370
+ updateHealthMetricsFromHistory(providerId, metrics) {
371
+ metrics.responseTime = this.calculateResponseTimeMetrics(providerId);
372
+ metrics.errorRate = this.calculateErrorRateMetrics(providerId);
373
+ metrics.healthScore = this.calculateHealthScore(metrics.responseTime, metrics.errorRate, metrics.quotaStatus, metrics.accountStatus);
374
+ metrics.isHealthy = metrics.healthScore >= 70;
375
+ metrics.lastUpdated = Date.now();
376
+ }
377
+ /**
378
+ * Get health summary for all providers
379
+ */
380
+ getHealthSummary() {
381
+ const allHealth = Array.from(this.healthCache.values());
382
+ const totalProviders = allHealth.length;
383
+ if (totalProviders === 0) {
384
+ return {
385
+ totalProviders: 0,
386
+ healthyProviders: 0,
387
+ degradedProviders: 0,
388
+ unhealthyProviders: 0,
389
+ averageHealthScore: 0
390
+ };
391
+ }
392
+ const healthyProviders = allHealth.filter(h => h.healthScore >= 70).length;
393
+ const degradedProviders = allHealth.filter(h => h.healthScore >= 40 && h.healthScore < 70).length;
394
+ const unhealthyProviders = allHealth.filter(h => h.healthScore < 40).length;
395
+ const averageHealthScore = allHealth.reduce((sum, h) => sum + h.healthScore, 0) / totalProviders;
396
+ return {
397
+ totalProviders,
398
+ healthyProviders,
399
+ degradedProviders,
400
+ unhealthyProviders,
401
+ averageHealthScore: Math.round(averageHealthScore)
402
+ };
403
+ }
404
+ }