ai-chatbot/lib/realtime/bfl-capability.ts
Your Name b5fdbd7c10
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat(realtime-canvas): migrate provider to BFL flux.2
2026-03-06 10:18:56 +08:00

160 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
const DEFAULT_BFL_MODEL_ID = "flux-2-klein-4b";
const DEFAULT_TIMEOUT_MS = 15_000;
export type BflCapabilityCode =
| "SUPPORTED"
| "AUTH_FAILED"
| "MODEL_INVALID"
| "UPSTREAM_ERROR";
export type BflModelProbeResult = {
supported: boolean;
code: BflCapabilityCode;
message: string;
status?: number;
};
type BflProbeParams = {
apiKey: string;
modelId?: string;
baseUrl?: string;
timeoutMs?: number;
};
function normalizeBaseUrl(raw: string | undefined) {
const next = String(raw || DEFAULT_BFL_BASE_URL).trim();
return (next || DEFAULT_BFL_BASE_URL).replace(/\/+$/, "");
}
function encodeModelPath(modelId: string) {
return modelId
.split("/")
.filter(Boolean)
.map((part) => encodeURIComponent(part))
.join("/");
}
function toErrorMessage(payload: any, fallback: string) {
const direct = String(
payload?.error?.message ||
payload?.error ||
payload?.message ||
payload?.detail ||
payload?.msg ||
""
).trim();
return direct || fallback;
}
function looksLikeModelInvalid(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"))
);
}
function hasTaskIdentity(payload: any) {
return Boolean(
(typeof payload?.id === "string" && payload.id.trim()) ||
(typeof payload?.polling_url === "string" && payload.polling_url.trim()) ||
(typeof payload?.response?.id === "string" && payload.response.id.trim()) ||
(typeof payload?.response?.polling_url === "string" &&
payload.response.polling_url.trim())
);
}
export async function probeBflModelSupport(params: BflProbeParams): Promise<BflModelProbeResult> {
const apiKey = String(params.apiKey || "").trim();
const modelId = String(params.modelId || DEFAULT_BFL_MODEL_ID).trim() || DEFAULT_BFL_MODEL_ID;
const baseUrl = normalizeBaseUrl(params.baseUrl);
const timeoutMs = Math.max(3_000, Number(params.timeoutMs || DEFAULT_TIMEOUT_MS));
if (!apiKey) {
return {
supported: false,
code: "AUTH_FAILED",
message: "缺少 BFL API Key。",
};
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const endpoint = `${baseUrl}/v1/${encodeModelPath(modelId)}`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"x-key": apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
prompt: "probe image generation capability",
width: 512,
height: 512,
safety_tolerance: 2,
}),
cache: "no-store",
signal: controller.signal,
});
const payload = await response.json().catch(() => null);
const message = toErrorMessage(payload, `BFL 探针请求失败(${response.status})`);
if (response.ok && hasTaskIdentity(payload)) {
return {
supported: true,
code: "SUPPORTED",
message: "模型可用。",
status: response.status,
};
}
if (response.status === 401 || response.status === 403) {
return {
supported: false,
code: "AUTH_FAILED",
message: "BFL API Key 无效或无权限,请检查后重试。",
status: response.status,
};
}
if (response.status === 404 || looksLikeModelInvalid(message)) {
return {
supported: false,
code: "MODEL_INVALID",
message: `模型 ${modelId} 不存在或不可用,请更换模型后重试。`,
status: response.status,
};
}
return {
supported: false,
code: "UPSTREAM_ERROR",
message,
status: response.status,
};
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return {
supported: false,
code: "UPSTREAM_ERROR",
message: "BFL 探针请求超时,请稍后重试。",
};
}
return {
supported: false,
code: "UPSTREAM_ERROR",
message: error instanceof Error ? error.message : "BFL 探针请求失败。",
};
} finally {
clearTimeout(timer);
}
}