53 lines
2.4 KiB
TypeScript
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),
|
|
};
|
|
}
|