ai-chatbot/lib/realtime/fal-capability.ts
Your Name cd91094b0f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add fal provider for realtime canvas
2026-03-31 13:36:28 +08:00

159 lines
3.8 KiB
TypeScript

import sharp from "sharp";
import { ensurePublicImageUrl } from "@/lib/realtime/image-input";
import { collectFalImageUrls, normalizeFalCredentials } from "@/lib/realtime/fal-shared";
const DEFAULT_TIMEOUT_MS = 25_000;
export type FalCapabilityCode =
| "SUPPORTED"
| "AUTH_FAILED"
| "MODEL_INVALID"
| "UPSTREAM_ERROR";
export type FalModelProbeResult = {
supported: boolean;
code: FalCapabilityCode;
message: string;
status?: number;
};
type FalProbeParams = {
apiKey: string;
modelId?: string;
timeoutMs?: number;
};
let cachedProbeImageUrl: string | null = null;
async function getProbeImageUrl() {
if (cachedProbeImageUrl) return cachedProbeImageUrl;
const buffer = await sharp({
create: {
width: 64,
height: 64,
channels: 3,
background: { r: 255, g: 255, b: 255 },
},
})
.png()
.toBuffer();
cachedProbeImageUrl = await ensurePublicImageUrl({
imageUrl: `data:image/png;base64,${buffer.toString("base64")}`,
prefix: "realtime-fal-probe",
});
return cachedProbeImageUrl;
}
function toErrorMessage(error: unknown, fallback: string) {
if (!error) return fallback;
if (typeof error === "string") return error.trim() || fallback;
if (typeof error === "object") {
const anyError = error as Record<string, unknown>;
const message = String(
anyError.message || anyError.error || anyError.detail || anyError.reason || ""
).trim();
if (message) return message;
}
if (error instanceof Error && error.message.trim()) {
return error.message.trim();
}
return fallback;
}
function isModelInvalid(message: string) {
const normalized = message.toLowerCase();
return (
normalized.includes("model") &&
(normalized.includes("not found") ||
normalized.includes("invalid") ||
normalized.includes("unknown") ||
normalized.includes("does not exist"))
);
}
export async function probeFalModelSupport(
params: FalProbeParams
): Promise<FalModelProbeResult> {
const apiKey = normalizeFalCredentials(params.apiKey);
const modelId = String(params.modelId || "").trim();
if (!apiKey) {
return {
supported: false,
code: "AUTH_FAILED",
message: "缺少 FAL API Key。",
};
}
if (!modelId) {
return {
supported: false,
code: "MODEL_INVALID",
message: "缺少 FAL 模型 ID。",
};
}
try {
const imageUrl = await getProbeImageUrl();
const fal = await import("@fal-ai/serverless-client");
fal.config({
credentials: apiKey,
});
const result = await fal.subscribe(modelId, {
input: {
prompt: "white square photo",
image_urls: [imageUrl],
},
logs: false,
timeout: Math.max(5_000, Number(params.timeoutMs || DEFAULT_TIMEOUT_MS)),
});
const urls = collectFalImageUrls(result);
if (urls.length > 0) {
return {
supported: true,
code: "SUPPORTED",
message: "模型可用。",
status: 200,
};
}
return {
supported: false,
code: "UPSTREAM_ERROR",
message: "FAL 探针已返回,但未发现可用图片输出。",
status: 502,
};
} catch (error) {
const message = toErrorMessage(error, "FAL 探针请求失败。");
if (/401|403|unauthorized|forbidden|invalid api key|authentication/i.test(message)) {
return {
supported: false,
code: "AUTH_FAILED",
message: "FAL API Key 无效或无权限,请检查后重试。",
status: 401,
};
}
if (isModelInvalid(message)) {
return {
supported: false,
code: "MODEL_INVALID",
message: `模型 ${modelId} 不存在或不可用,请更换模型后重试。`,
status: 404,
};
}
return {
supported: false,
code: "UPSTREAM_ERROR",
message,
status: 502,
};
}
}