ai-chatbot/lib/ai/runtime/authentication-failover.ts

53 lines
2.4 KiB
TypeScript

import type { LanguageModelV2 } from "@ai-sdk/provider";
export function isProviderAuthenticationFailure(error: unknown, depth = 0): boolean {
if (!error || depth > 3) return false;
const value = typeof error === "object" ? error as Record<string, unknown> : {};
const status = Number(value.statusCode || value.status || value.httpStatus);
const message = String(value.message || (typeof error === "string" ? error : "")).toLowerCase();
return status === 401 || /\b401\b/.test(message) ||
/invalid authentication|invalid[ _]api[ _]key|authentication[_ ](?:failed|error)|密钥鉴权失败/.test(message) ||
isProviderAuthenticationFailure(value.cause, depth + 1);
}
export function isCredentialFailureBan(reason?: string | null) {
return /^provider_(?:authentication_failed|access_forbidden)\b/.test(String(reason || ""));
}
// Retry only an explicitly rejected authentication request, before the provider
// returns a stream. Never replay accepted streams, tools, timeouts or 5xx POSTs.
export function withAuthenticationFailover(params: {
primary: LanguageModelV2;
onAuthenticationFailure: (error: unknown) => Promise<void>;
resolveFallback: () => Promise<LanguageModelV2 | null>;
}): LanguageModelV2 {
let active = params.primary;
let recovery: Promise<LanguageModelV2 | null> | undefined;
const invoke = async <T>(call: (model: LanguageModelV2) => PromiseLike<T>, signal?: AbortSignal): Promise<T> => {
const attempted = active;
try {
return await call(attempted);
} catch (error) {
if (signal?.aborted || attempted !== params.primary || !isProviderAuthenticationFailure(error)) throw error;
recovery ||= (async () => {
await params.onAuthenticationFailure(error);
const fallback = await params.resolveFallback();
if (fallback && fallback !== params.primary) active = fallback;
return fallback;
})();
const fallback = await recovery;
if (!fallback || fallback === attempted) throw error;
signal?.throwIfAborted();
return await call(fallback);
}
};
return {
specificationVersion: "v2",
get provider() { return active.provider; },
get modelId() { return active.modelId; },
get supportedUrls() { return active.supportedUrls; },
doGenerate: (options) => invoke((model) => model.doGenerate(options), options.abortSignal),
doStream: (options) => invoke((model) => model.doStream(options), options.abortSignal),
};
}