fix: recognize BFL reference image support
This commit is contained in:
parent
d001dba00c
commit
e7cd3d6e73
@ -1,6 +1,7 @@
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import { isFalGptImage2EditModelId } from "@/lib/ai/providers/fal-image";
|
||||
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
|
||||
import { modelLacksReferenceImageInputSupport } from "@/lib/ai/reference-image-capability";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { convertMessagesWithImages } from "./image-media";
|
||||
|
||||
@ -370,7 +371,10 @@ export async function generateImageWithFailoverCandidate(params: {
|
||||
const { candidate, messages, imageParams, fallbackImageParams } = params;
|
||||
const providerType = String(candidate.provider?.type || "");
|
||||
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
|
||||
if (chatMessagesHaveImageInput(messages) && candidate.model?.supportsVision === false) {
|
||||
if (
|
||||
chatMessagesHaveImageInput(messages) &&
|
||||
modelLacksReferenceImageInputSupport(candidate.model, candidate.provider)
|
||||
) {
|
||||
throw new Error(
|
||||
`备用模型不支持参考图/图生图输入:${candidate.model.displayName || candidate.model.id}`
|
||||
);
|
||||
|
||||
@ -14,6 +14,10 @@ import {
|
||||
getPointsSystemConfig,
|
||||
getSiteSettings,
|
||||
} from "@/lib/db/queries";
|
||||
import {
|
||||
modelLacksReferenceImageInputSupport,
|
||||
modelSupportsReferenceImageInput,
|
||||
} from "@/lib/ai/reference-image-capability";
|
||||
import { getImageToolCreditCost } from "@/lib/points/config";
|
||||
import { prepareFeaturePointsCharge } from "@/lib/points/feature-charge";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
@ -217,7 +221,10 @@ function findModelSelectionEntry(
|
||||
function isScaleAdjustDedicatedDetailModel(
|
||||
entry: AiModelWithProviderForSelection | null | undefined
|
||||
) {
|
||||
if (!entry?.model?.supportsImageGen || entry.model.supportsVision === false) {
|
||||
if (
|
||||
!entry?.model?.supportsImageGen ||
|
||||
modelLacksReferenceImageInputSupport(entry.model, entry.provider)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const label = `${entry.model.displayName || ""} ${entry.model.modelId || ""}`
|
||||
@ -237,7 +244,7 @@ function isHealthyImageModelEntry(
|
||||
) {
|
||||
return (
|
||||
Boolean(entry?.model?.supportsImageGen) &&
|
||||
entry?.model?.supportsVision !== false &&
|
||||
modelSupportsReferenceImageInput(entry?.model, entry?.provider) &&
|
||||
entry?.model?.healthStatus === "healthy"
|
||||
);
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import { acquireCanvasImageModelSlot } from "@/lib/canvas/image-concurrency";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import { resolveModelRuntimeId } from "@/lib/ai/model-runtime";
|
||||
import { modelSupportsReferenceImageInput } from "@/lib/ai/reference-image-capability";
|
||||
import { resolveLongestEdgeDimensions } from "@/lib/ai/image-size-constraints";
|
||||
|
||||
// 请求体 Schema
|
||||
@ -905,7 +906,7 @@ function buildSceneFusionCandidatePool(
|
||||
id: String(entry.model.id),
|
||||
modelId: String(entry.model.modelId || ""),
|
||||
displayName: String(entry.model.displayName || entry.model.modelId || entry.model.id),
|
||||
supportsVision: Boolean(entry.model.supportsVision),
|
||||
supportsVision: modelSupportsReferenceImageInput(entry.model, entry.provider),
|
||||
imageGenConfig: (entry.model.imageGenConfig || null) as SceneFusionImageGenConfig | null,
|
||||
excludeFromFailover: Boolean(entry.model.excludeFromFailover),
|
||||
defaultParams: (entry.model.defaultParams as Record<string, any> | null) || null,
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { isKnownReferenceImageModel } from "@/lib/ai/reference-image-capability";
|
||||
|
||||
type EnabledModelWithProvider = Awaited<
|
||||
ReturnType<typeof import("@/lib/db/queries")["getEnabledAiModelsWithProvider"]>
|
||||
@ -31,8 +32,13 @@ function hasVisionCapability(model: {
|
||||
supportsVision?: boolean | null;
|
||||
capabilities?: unknown;
|
||||
defaultParams?: unknown;
|
||||
}) {
|
||||
return Boolean(model.supportsVision) || isAiModelMultimodal(model);
|
||||
modelId?: unknown;
|
||||
}, providerType?: string | null) {
|
||||
return (
|
||||
Boolean(model.supportsVision) ||
|
||||
isKnownReferenceImageModel(model, { type: providerType }) ||
|
||||
isAiModelMultimodal(model)
|
||||
);
|
||||
}
|
||||
|
||||
const CHAT_PROVIDER_TYPES = [
|
||||
@ -86,7 +92,7 @@ function isConversationalCandidate(
|
||||
const matchesPreferredProvider = CHAT_PROVIDER_TYPES.includes(
|
||||
providerType as (typeof CHAT_PROVIDER_TYPES)[number],
|
||||
);
|
||||
const hasVision = hasVisionCapability(item.model);
|
||||
const hasVision = hasVisionCapability(item.model, providerType);
|
||||
|
||||
if (matchesPreferredProvider) {
|
||||
return requireVision ? hasVision : true;
|
||||
@ -120,7 +126,7 @@ function matchesIntentRequirements(
|
||||
if (params.intent === "vision") {
|
||||
return (
|
||||
!isVideoProviderType(String(item.provider.type || ""), item.model) &&
|
||||
hasVisionCapability(item.model)
|
||||
hasVisionCapability(item.model, item.provider.type)
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,7 +134,7 @@ function matchesIntentRequirements(
|
||||
return isVideoProviderType(String(item.provider.type || ""), item.model);
|
||||
}
|
||||
|
||||
if (params.requireVision && !hasVisionCapability(item.model)) return false;
|
||||
if (params.requireVision && !hasVisionCapability(item.model, item.provider.type)) return false;
|
||||
if (
|
||||
params.requireImageGen &&
|
||||
!isImageGenerationModelDescriptor({
|
||||
@ -271,7 +277,9 @@ export async function routeToModel(params: {
|
||||
|
||||
if (configuredModel) {
|
||||
const isImageIntent = intent === "image_edit" || intent === "image_gen";
|
||||
const matchesVision = !config.requireVision || hasVisionCapability(configuredModel.model);
|
||||
const matchesVision =
|
||||
!config.requireVision ||
|
||||
hasVisionCapability(configuredModel.model, configuredModel.provider.type);
|
||||
const matchesImageGen = !config.requireImageGen || configuredModel.model.supportsImageGen;
|
||||
const allowImageIntentWithImageGenOnly =
|
||||
isImageIntent && configuredModel.model.supportsImageGen;
|
||||
@ -347,7 +355,8 @@ export async function routeToModel(params: {
|
||||
!item.model.isSubModel &&
|
||||
item.model.isEnabled &&
|
||||
!isVideoProviderType(String(item.provider.type || ""), item.model) &&
|
||||
(item.model.supportsImageGen || hasVisionCapability(item.model))
|
||||
(item.model.supportsImageGen ||
|
||||
hasVisionCapability(item.model, item.provider.type))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
72
lib/ai/reference-image-capability.ts
Normal file
72
lib/ai/reference-image-capability.ts
Normal file
@ -0,0 +1,72 @@
|
||||
type ModelDescriptor = {
|
||||
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;
|
||||
|
||||
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(/[?#].*$/, "");
|
||||
}
|
||||
|
||||
export function isKnownReferenceImageModel(
|
||||
model: ModelDescriptor | null | undefined,
|
||||
provider?: ProviderDescriptor | null
|
||||
) {
|
||||
const providerType = String(provider?.type || "").trim();
|
||||
if (providerType !== "bfl-image") return false;
|
||||
|
||||
const candidates = [
|
||||
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"));
|
||||
|
||||
return candidates
|
||||
.map(normalizeBflModelId)
|
||||
.some((modelId) => BFL_REFERENCE_IMAGE_MODEL_REGEX.test(modelId));
|
||||
}
|
||||
|
||||
export function modelLacksReferenceImageInputSupport(
|
||||
model: ModelDescriptor | null | undefined,
|
||||
provider?: ProviderDescriptor | null
|
||||
) {
|
||||
if (isKnownReferenceImageModel(model, provider)) return false;
|
||||
return model?.supportsVision === false;
|
||||
}
|
||||
|
||||
export function modelSupportsReferenceImageInput(
|
||||
model: ModelDescriptor | null | undefined,
|
||||
provider?: ProviderDescriptor | null
|
||||
) {
|
||||
return !modelLacksReferenceImageInputSupport(model, provider);
|
||||
}
|
||||
@ -16,6 +16,7 @@ import { createRunway } from "@/lib/ai/providers/runway";
|
||||
import { createKrea } from "@/lib/ai/providers/krea";
|
||||
import { createVolcanoVision } from "@/lib/ai/providers/volcano-vision";
|
||||
import { resolveModelRuntimeId } from "@/lib/ai/model-runtime";
|
||||
import { modelLacksReferenceImageInputSupport } from "@/lib/ai/reference-image-capability";
|
||||
import { filterReachableRemoteUrls } from "@/lib/net/url-access";
|
||||
import {
|
||||
acquireCanvasImageModelSlot,
|
||||
@ -1766,7 +1767,10 @@ function findCanvasFailoverModel(params: {
|
||||
return false;
|
||||
}
|
||||
if (!item.model.supportsImageGen) return false;
|
||||
if (params.requiresImageInput && item.model.supportsVision === false) {
|
||||
if (
|
||||
params.requiresImageInput &&
|
||||
modelLacksReferenceImageInputSupport(item.model, item.provider)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!SUPPORTED_CANVAS_PROVIDER_TYPES.has(item.provider.type)) return false;
|
||||
@ -1884,7 +1888,10 @@ export async function generateCanvasImage(
|
||||
|
||||
let provider = selectedModel.provider;
|
||||
|
||||
if (requiresImageInput && selectedModel.model.supportsVision === false) {
|
||||
if (
|
||||
requiresImageInput &&
|
||||
modelLacksReferenceImageInputSupport(selectedModel.model, selectedModel.provider)
|
||||
) {
|
||||
if (request.allowFailover === false) {
|
||||
throw new Error("当前图片模型不支持参考图/图生图,请选择支持图像输入的生图模型。");
|
||||
}
|
||||
@ -1925,7 +1932,12 @@ export async function generateCanvasImage(
|
||||
for (const candidate of imageModels) {
|
||||
const candidateProvider = candidate.provider;
|
||||
if (!SUPPORTED_CANVAS_PROVIDER_TYPES.has(candidateProvider.type)) continue;
|
||||
if (requiresImageInput && candidate.model.supportsVision === false) continue;
|
||||
if (
|
||||
requiresImageInput &&
|
||||
modelLacksReferenceImageInputSupport(candidate.model, candidate.provider)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
fallbackModel = candidate;
|
||||
fallbackProvider = candidateProvider;
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user