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