ai-chatbot/lib/ai/runtime/provider-health.ts
Your Name 9ecd91376d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: strengthen agent runtime boundaries
2026-03-16 11:45:00 +08:00

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;
}