ai-chatbot/lib/ai/reference-image-capability.ts

107 lines
3.4 KiB
TypeScript

type ModelDescriptor = {
displayName?: unknown;
modelId?: unknown;
supportsVision?: boolean | null;
capabilities?: unknown;
defaultParams?: unknown;
};
type ProviderDescriptor = {
type?: unknown;
};
const BFL_REFERENCE_IMAGE_MODEL_REGEX =
/^(?:bfl\/)?(?:flux-2-(?:max|flex|pro(?:-preview)?|klein-(?:4b|9b)(?:-preview)?)|flux-kontext-(?:pro|max))$/i;
const REFERENCE_IMAGE_MODE_REGEX =
/^(?:image-edit|image-to-image|img2img|reference-images?|image-reference|reference-image)$/i;
const GEMINI_TEXT_ONLY_IMAGE_MODEL_REGEX = /^(?:nano-banana-pro|nano-banana-2)$/i;
function readStringField(source: unknown, key: string) {
if (!source || typeof source !== "object" || !(key in source)) return "";
return String((source as Record<string, unknown>)[key] || "").trim();
}
function normalizeBflModelId(value: unknown) {
const trimmed = String(value || "").trim();
if (!trimmed) return "";
return trimmed
.replace(/^https?:\/\/[^/]+/i, "")
.replace(/^\/?(?:bfl\/)?v1\//i, "")
.replace(/^\/+/, "")
.replace(/[?#].*$/, "");
}
function readStringArrayField(source: unknown, key: string) {
if (!source || typeof source !== "object" || !(key in source)) return [];
const value = (source as Record<string, unknown>)[key];
if (!Array.isArray(value)) return [];
return value.map((item) => String(item || "").trim()).filter(Boolean);
}
function hasReferenceImageMode(value: unknown) {
if (!value || typeof value !== "object") return false;
const supportedModes = [
...readStringArrayField(value, "supportedModes"),
...readStringArrayField(value, "modes"),
];
return supportedModes.some((mode) => REFERENCE_IMAGE_MODE_REGEX.test(mode));
}
export function isKnownReferenceImageModel(
model: ModelDescriptor | null | undefined,
provider?: ProviderDescriptor | null
) {
const providerType = String(provider?.type || "").trim();
const candidates = [
model?.displayName,
model?.modelId,
readStringField(model?.defaultParams, "endpoint"),
readStringField(model?.defaultParams, "submitEndpoint"),
readStringField(model?.capabilities, "endpoint"),
];
const capabilitiesEndpoints =
model?.capabilities &&
typeof model.capabilities === "object" &&
"endpoints" in model.capabilities
? (model.capabilities as { endpoints?: unknown }).endpoints
: undefined;
candidates.push(readStringField(capabilitiesEndpoints, "submit"));
const matchesKnownBflModel = candidates
.map(normalizeBflModelId)
.some((modelId) => BFL_REFERENCE_IMAGE_MODEL_REGEX.test(modelId));
if (matchesKnownBflModel) return true;
if (providerType === "bfl-image") {
return hasReferenceImageMode(model?.capabilities) || hasReferenceImageMode(model?.defaultParams);
}
return false;
}
export function modelLacksReferenceImageInputSupport(
model: ModelDescriptor | null | undefined,
provider?: ProviderDescriptor | null
) {
if (isKnownReferenceImageModel(model, provider)) return false;
const providerType = String(provider?.type || "").trim().toLowerCase();
const modelId = String(model?.modelId || "").trim();
if (
providerType === "gemini" &&
GEMINI_TEXT_ONLY_IMAGE_MODEL_REGEX.test(modelId)
) {
return true;
}
return model?.supportsVision === false;
}
export function modelSupportsReferenceImageInput(
model: ModelDescriptor | null | undefined,
provider?: ProviderDescriptor | null
) {
return !modelLacksReferenceImageInputSupport(model, provider);
}