110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
import "server-only";
|
|
|
|
export type ProviderHealthEventType = "success" | "timeout" | "quota" | "error";
|
|
|
|
export interface ProviderHealthSnapshot {
|
|
providerKey: string;
|
|
providerType?: string;
|
|
lastEventType: ProviderHealthEventType;
|
|
lastError?: string;
|
|
consecutiveFailures: number;
|
|
lastSuccessAt?: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
const providerHealthMap = new Map<string, ProviderHealthSnapshot>();
|
|
|
|
function normalizeErrorMessage(error: unknown) {
|
|
return error instanceof Error ? String(error.message || "").trim() : String(error || "").trim();
|
|
}
|
|
|
|
export function classifyProviderHealthEvent(error: unknown): ProviderHealthEventType {
|
|
const normalized = normalizeErrorMessage(error).toLowerCase();
|
|
|
|
if (
|
|
normalized.includes("token quota is not enough") ||
|
|
normalized.includes("pre_consume_token_quota_failed") ||
|
|
normalized.includes("quota is not enough")
|
|
) {
|
|
return "quota";
|
|
}
|
|
|
|
if (
|
|
normalized.includes("timeout") ||
|
|
normalized.includes("timed out") ||
|
|
normalized.includes("body timeout") ||
|
|
normalized.includes("headers timeout")
|
|
) {
|
|
return "timeout";
|
|
}
|
|
|
|
return "error";
|
|
}
|
|
|
|
export function recordProviderHealthSuccess(params: {
|
|
providerKey?: string;
|
|
providerType?: string;
|
|
}) {
|
|
const providerKey = String(params.providerKey || "").trim();
|
|
if (!providerKey) return;
|
|
|
|
const existing = providerHealthMap.get(providerKey);
|
|
providerHealthMap.set(providerKey, {
|
|
providerKey,
|
|
providerType: params.providerType || existing?.providerType,
|
|
lastEventType: "success",
|
|
consecutiveFailures: 0,
|
|
lastError: undefined,
|
|
lastSuccessAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
export function recordProviderHealthFailure(params: {
|
|
providerKey?: string;
|
|
providerType?: string;
|
|
error: unknown;
|
|
}) {
|
|
const providerKey = String(params.providerKey || "").trim();
|
|
if (!providerKey) return;
|
|
|
|
const existing = providerHealthMap.get(providerKey);
|
|
providerHealthMap.set(providerKey, {
|
|
providerKey,
|
|
providerType: params.providerType || existing?.providerType,
|
|
lastEventType: classifyProviderHealthEvent(params.error),
|
|
lastError: normalizeErrorMessage(params.error).slice(0, 400),
|
|
consecutiveFailures:
|
|
existing?.lastEventType === "success"
|
|
? 1
|
|
: Math.max(1, (existing?.consecutiveFailures || 0) + 1),
|
|
lastSuccessAt: existing?.lastSuccessAt,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
export function getProviderHealthSnapshot(providerKey?: string) {
|
|
const normalized = String(providerKey || "").trim();
|
|
if (!normalized) return null;
|
|
return providerHealthMap.get(normalized) || null;
|
|
}
|
|
|
|
export function shouldAvoidProvider(snapshot?: ProviderHealthSnapshot | null) {
|
|
if (!snapshot) return false;
|
|
|
|
const ageMs = Date.now() - snapshot.updatedAt;
|
|
if (snapshot.lastEventType === "quota") {
|
|
return ageMs <= 15 * 60 * 1000;
|
|
}
|
|
|
|
if (snapshot.lastEventType === "timeout") {
|
|
return snapshot.consecutiveFailures >= 2 && ageMs <= 5 * 60 * 1000;
|
|
}
|
|
|
|
if (snapshot.lastEventType === "error") {
|
|
return snapshot.consecutiveFailures >= 3 && ageMs <= 5 * 60 * 1000;
|
|
}
|
|
|
|
return false;
|
|
}
|