Files
2026-07-13 13:08:41 +08:00

144 lines
3.8 KiB
TypeScript

export interface ApiErrorResponse {
detail?: unknown;
message?: string;
error?: unknown;
}
const INVALID_API_KEY_MESSAGE =
"Invalid API key. Please verify your API key and try again.";
const SAFETY_BLOCK_MESSAGE =
"The request was blocked by the provider's safety system. Please revise it and try again.";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function nestedMessage(value: unknown): string | null {
if (!value) return null;
if (typeof value === "string") return value;
if (!isRecord(value)) return null;
for (const key of ["message", "detail", "error"]) {
const nested = value[key];
if (typeof nested === "string" && nested.trim()) {
return nested;
}
const message = nestedMessage(nested);
if (message) {
return message;
}
}
return null;
}
export function normalizeApiErrorDetail(detail: unknown): string | null {
if (!detail) return null;
if (typeof detail === "string") {
return detail;
}
if (Array.isArray(detail)) {
const parts = detail
.map((item) => {
if (typeof item === "string") return item;
if (isRecord(item)) {
const maybeMsg = item.msg;
const maybeLoc = item.loc;
const locPath = Array.isArray(maybeLoc)
? maybeLoc
.filter(
(value) =>
typeof value === "string" || typeof value === "number"
)
.join(".")
: "";
if (typeof maybeMsg === "string") {
return locPath ? `${locPath}: ${maybeMsg}` : maybeMsg;
}
return nestedMessage(item);
}
return null;
})
.filter((value): value is string => Boolean(value));
return parts.length ? parts.join("; ") : null;
}
if (isRecord(detail)) {
return nestedMessage(detail);
}
return String(detail);
}
function looksLikeProviderAuthError(message: string, status?: number): boolean {
const text = message.toLowerCase();
return (
/invalid[_\s-]*api[_\s-]*key/.test(text) ||
/incorrect\s+api\s+key/.test(text) ||
/api\s+key\s+(?:is\s+)?invalid/.test(text) ||
text.includes("authentication_error") ||
text.includes("invalid_api_key") ||
Boolean(status === 401 && text.includes("api key"))
);
}
function looksLikeSafetyBlock(message: string): boolean {
const text = message.toLowerCase();
return text.includes("moderation_blocked") || text.includes("safety system");
}
function looksLikeRawPayload(message: string): boolean {
const text = message.trim();
return (
text.startsWith("{") ||
text.startsWith("[") ||
text.includes("{'error'") ||
text.includes('"error"') ||
text.includes("Error code:")
);
}
export function sanitizeApiErrorMessage(
message: string,
fallbackMessage: string,
status?: number
): string {
const trimmed = message.trim();
if (!trimmed) return fallbackMessage;
if (looksLikeProviderAuthError(trimmed, status)) {
return INVALID_API_KEY_MESSAGE;
}
if (looksLikeSafetyBlock(trimmed)) {
return SAFETY_BLOCK_MESSAGE;
}
if (looksLikeRawPayload(trimmed)) {
return fallbackMessage;
}
return trimmed;
}
export function extractApiErrorMessage(
errorData: unknown,
fallbackMessage: string,
status?: number
): string {
let message: string | null = null;
if (isRecord(errorData)) {
const apiError = errorData as ApiErrorResponse;
message =
normalizeApiErrorDetail(apiError.detail) ||
(typeof apiError.message === "string" ? apiError.message : null) ||
normalizeApiErrorDetail(apiError.error);
} else {
message = normalizeApiErrorDetail(errorData);
}
return sanitizeApiErrorMessage(message || fallbackMessage, fallbackMessage, status);
}