160 lines
4.3 KiB
TypeScript
160 lines
4.3 KiB
TypeScript
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);
|
||
}
|
||
}
|