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,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
|
+
}
|