139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
import "server-only";
|
|
|
|
import type { DiscountableImageGenConfigLike } from "@/lib/ai/image-model-pricing";
|
|
import {
|
|
getImageCreditCostFromConfig,
|
|
getImageModelCreditDiscountLabel,
|
|
} from "@/lib/ai/image-model-pricing";
|
|
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
|
|
|
|
export type StyleProModelEntry = Awaited<
|
|
ReturnType<typeof getEnabledAiModelsWithProvider>
|
|
>[number];
|
|
|
|
export type PublicStyleProModelOption = {
|
|
id: string;
|
|
modelId: string;
|
|
displayName: string;
|
|
avatarUrl: string | null;
|
|
providerName: string;
|
|
providerType: string;
|
|
isDefault: boolean;
|
|
unitCost: number;
|
|
creditDiscountLabel: string | null;
|
|
};
|
|
|
|
const VIDEO_PROVIDER_TYPES = new Set([
|
|
"grsai-video",
|
|
"qianwen-video",
|
|
"doubao-video",
|
|
"ruxa-video",
|
|
"volcano-video",
|
|
"fal-video",
|
|
"runway",
|
|
]);
|
|
|
|
const VIDEO_MODEL_KEYWORDS = [
|
|
"/generate/video/",
|
|
"video",
|
|
"veo",
|
|
"sora",
|
|
"kling",
|
|
"hailuo",
|
|
"seedance",
|
|
"runway",
|
|
"gen-3",
|
|
"gen-4",
|
|
"gen 3",
|
|
"gen 4",
|
|
"wan-2.",
|
|
"wan 2.",
|
|
"t2v",
|
|
"i2v",
|
|
];
|
|
|
|
function hasVideoGenerationConfig(value: unknown) {
|
|
if (!value || typeof value !== "object") return false;
|
|
const config = value as Record<string, unknown>;
|
|
return (
|
|
"creditCostPerVideo" in config ||
|
|
"creditCostPerVideoByDuration" in config ||
|
|
"creditCostPerVideoByResolution" in config ||
|
|
"videoDurationOptions" in config ||
|
|
"videoResolutionOptions" in config
|
|
);
|
|
}
|
|
|
|
function hasVideoCapabilities(value: unknown) {
|
|
if (!value || typeof value !== "object") return false;
|
|
const serialized = JSON.stringify(value).toLowerCase();
|
|
return serialized.includes("text-to-video") || serialized.includes("image-to-video");
|
|
}
|
|
|
|
function isVideoLikeModel(entry: StyleProModelEntry) {
|
|
if (VIDEO_PROVIDER_TYPES.has(entry.provider.type)) return true;
|
|
if (
|
|
hasVideoGenerationConfig(entry.model.imageGenConfig) ||
|
|
hasVideoCapabilities(entry.model.capabilities) ||
|
|
hasVideoCapabilities(entry.model.defaultParams)
|
|
) {
|
|
return true;
|
|
}
|
|
const searchable = [
|
|
entry.provider.type,
|
|
entry.provider.name,
|
|
entry.model.modelId,
|
|
entry.model.displayName,
|
|
entry.model.description,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")
|
|
.toLowerCase();
|
|
return VIDEO_MODEL_KEYWORDS.some((keyword) => searchable.includes(keyword));
|
|
}
|
|
|
|
export function isStyleProImageModel(entry: StyleProModelEntry) {
|
|
return Boolean(entry.model.supportsImageGen) && !isVideoLikeModel(entry);
|
|
}
|
|
|
|
export async function getStyleProImageModelEntries(options?: {
|
|
includeHidden?: boolean;
|
|
}) {
|
|
const models = await getEnabledAiModelsWithProvider(
|
|
false,
|
|
options?.includeHidden === true
|
|
);
|
|
return models.filter(isStyleProImageModel);
|
|
}
|
|
|
|
export function findStyleProImageModelByRef(
|
|
entries: StyleProModelEntry[],
|
|
modelRef?: string | null
|
|
) {
|
|
const normalized = String(modelRef || "").trim();
|
|
if (!normalized) return null;
|
|
return (
|
|
entries.find(
|
|
({ model }) => model.id === normalized || model.modelId === normalized
|
|
) || null
|
|
);
|
|
}
|
|
|
|
export function toPublicStyleProModelOption(
|
|
entry: StyleProModelEntry
|
|
): PublicStyleProModelOption {
|
|
const imageGenConfig = (entry.model.imageGenConfig ||
|
|
null) as DiscountableImageGenConfigLike | null;
|
|
return {
|
|
id: entry.model.id,
|
|
modelId: entry.model.modelId,
|
|
displayName: entry.model.displayName,
|
|
avatarUrl: entry.model.avatarUrl || null,
|
|
providerName: entry.provider.name,
|
|
providerType: entry.provider.type,
|
|
isDefault: entry.model.isDefault,
|
|
unitCost: getImageCreditCostFromConfig(imageGenConfig),
|
|
creditDiscountLabel: getImageModelCreditDiscountLabel(imageGenConfig),
|
|
};
|
|
}
|