ai-chatbot/lib/ai/vip98k-models.ts

1229 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
hasVerifiedVip98kGptImage25ResolutionTiers,
hasVerifiedVip98kOpenAiImageArrayContract,
normalizeVip98kInvocationHeaders,
} from "@/lib/ai/vip98k-task-invocation";
export const VIP98K_BASE_URL = "https://api.vip98k.com";
export type Vip98kCatalogModelType =
| "image"
| "video"
| "image_tool"
| "document_tool"
| "model"
| "audio"
| "world"
| string;
export interface Vip98kInvocationContract {
kind?: "chat" | "task" | "tool" | string;
method?: string;
path?: string;
content_type?: string;
headers?: Record<string, unknown>;
request_example?: Record<string, unknown>;
input_schema?: Record<string, unknown>;
result_method?: string;
result_path?: string;
}
export interface Vip98kCatalogModel {
modelId?: string;
id?: string;
name?: string;
displayName?: string;
display_name?: string;
description?: string;
type?: Vip98kCatalogModelType;
modelType?: Vip98kCatalogModelType;
model_type?: Vip98kCatalogModelType;
category?: Vip98kCatalogModelType;
capabilities?: unknown;
inputModes?: unknown;
input_modes?: unknown;
outputModes?: unknown;
output_modes?: unknown;
supportedResolutions?: unknown;
supported_resolutions?: unknown;
resolutions?: unknown;
durations?: unknown;
durationOptions?: unknown;
duration_options?: unknown;
defaultDuration?: unknown;
default_duration?: unknown;
supportsAsync?: unknown;
supports_async?: unknown;
pollIntervalSeconds?: unknown;
poll_interval_seconds?: unknown;
maxPollSeconds?: unknown;
max_poll_seconds?: unknown;
resultTTLSeconds?: unknown;
result_ttl_seconds?: unknown;
billingMode?: string;
billing_mode?: string;
pricing?: unknown;
price?: unknown;
status?: string;
available?: boolean;
isAvailable?: boolean;
enabled?: boolean;
isEnabled?: boolean;
is_active?: boolean;
invocation?: Vip98kInvocationContract;
invocations?: Vip98kInvocationContract[];
serviceOffers?: unknown;
service_offers?: unknown;
taskTypes?: unknown;
task_types?: unknown;
releasedAt?: unknown;
released_at?: unknown;
releaseDate?: unknown;
release_date?: unknown;
releaseYear?: unknown;
release_year?: unknown;
publishedAt?: unknown;
published_at?: unknown;
launchDate?: unknown;
launch_date?: unknown;
[key: string]: unknown;
}
export interface Vip98kModelPreset {
modelId: string;
displayName: string;
description: string;
type: Vip98kCatalogModelType;
supportsVision: boolean;
supportsImageGen: boolean;
isHidden: boolean;
capabilities: Record<string, unknown>;
defaultParams: Record<string, unknown>;
imageGenConfig: Record<string, unknown> | null;
sortOrder: number;
}
const DEFAULT_ASPECT_RATIOS = [
"auto",
"1:1",
"4:3",
"3:4",
"16:9",
"9:16",
"3:2",
"2:3",
"4:5",
"5:4",
] as const;
const DEFAULT_VIDEO_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4"] as const;
const DEFAULT_IMAGE_SIZE = "1K";
const TYPE_SORT_ORDER: Record<string, number> = {
image: 100,
video: 200,
world: 300,
model_3d: 320,
model: 400,
image_tool: 800,
audio: 900,
document_tool: 950,
};
const IMAGE_TOOL_MODEL_IDS = new Set([
"background-generator",
"clothing-segmentation",
"colorize",
"compress",
"enhance",
"face-swap-image",
"head-cutout",
"id-photo",
"image-expansion",
"image-upscaler",
"photo-cleanup",
"remove-background",
"remove-object",
"remove-watermark",
"smart-crop",
"youchuan/v8.1/remove-background",
]);
const VIDEO_CAPABILITY_KEYS = new Set([
"audio-to-video",
"first-frame-to-video",
"image-to-video",
"reference-to-video",
"start-end-frame-to-video",
"text-to-video",
"video-extend",
"video-to-video",
]);
const IMAGE_CAPABILITY_KEYS = new Set([
"image-to-image",
"reference-to-image",
"text-to-image",
]);
const IMAGE_TOOL_OPERATION_BY_MODEL_ID: Record<string, string> = {
"background-generator": "background-generation",
"clothing-segmentation": "segmentation",
colorize: "colorize",
compress: "compress",
enhance: "enhance",
"face-swap-image": "face-swap",
"head-cutout": "remove-background",
"id-photo": "id-photo",
"image-expansion": "extend",
"image-upscaler": "enhance",
"photo-cleanup": "cleanup",
"remove-background": "remove-background",
"remove-object": "remove-object",
"remove-watermark": "remove-watermark",
"smart-crop": "smart-crop",
"youchuan/v8.1/remove-background": "remove-background",
};
// The VIP98K catalog currently has no release-date field. Unknown image models stay
// available to backend routing but remain hidden until their 2026+ release is verified.
const VERIFIED_2026_IMAGE_MODEL_PATTERNS = [
/^nano-banana-2(?:$|[-/])/,
/^nano-banana-fast$/,
/^nano-banana-pro$/,
/^gpt-image-2(?:$|[-/])/,
/^gpt-image-2\.5$/,
/^gpt-image-2\.5-flare$/,
/^alibaba\/wan-2\.(?:6|7)(?:$|[-/])/,
/^black-forest-labs\/flux-2(?:$|[-/])/,
/^bytedance\/seedream-v5(?:$|[.\-/])/,
/^google\/nano-banana-2(?:$|[-/])/,
/^hidream-o1-1\.5(?:$|[-/])/,
/^ideogram\/v4(?:$|[-/])/,
/^krea-2(?:$|[-/])/,
/^microsoft\/mai-image-2\.5(?:$|[-/])/,
/^nvidia\/cosmos-3(?:$|[-/])/,
/^openai\/gpt-image-2(?:$|[-/])/,
/^qwen\/qwen-image-2\.0(?:$|[-/])/,
/^youchuan\/v8\.1(?:$|[-/])/,
];
// These catalog entries currently expose array-shaped request metadata but do
// not provide reliable multi-reference editing. Keep them out of image pickers
// until the provider publishes and verifies a real multi-image contract.
const VIP98K_MULTI_IMAGE_MODEL_EXCLUSIONS = new Set([
"nano-banana-2-lite",
"nano-banana-fast",
]);
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function parseStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return Array.from(
new Set(
value
.map((item) => String(item || "").trim())
.filter(Boolean),
),
);
}
function parseNumberArray(value: unknown): number[] {
if (!Array.isArray(value)) return [];
return Array.from(
new Set(
value
.map((item) => Number(item))
.filter((item) => Number.isFinite(item) && item > 0),
),
).sort((a, b) => a - b);
}
function parseNumber(value: unknown): number | undefined {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : undefined;
}
function normalizeCatalogType(model: Vip98kCatalogModel): Vip98kCatalogModelType {
return String(
model.type || model.modelType || model.model_type || model.category || "model",
)
.trim()
.toLowerCase();
}
function getReleaseYear(model: Vip98kCatalogModel): number | null {
const candidates = [
model.releaseYear,
model.release_year,
model.releasedAt,
model.released_at,
model.releaseDate,
model.release_date,
model.publishedAt,
model.published_at,
model.launchDate,
model.launch_date,
];
for (const candidate of candidates) {
const match = String(candidate || "").match(/(?:19|20)\d{2}/);
if (match) return Number(match[0]);
}
const datedModelId = getModelId(model).match(/(?:^|[^\d])((?:19|20)\d{2})\d{4}(?:[^\d]|$)/);
return datedModelId?.[1] ? Number(datedModelId[1]) : null;
}
export function isVip98kImageModelVisible(model: Vip98kCatalogModel) {
const releaseYear = getReleaseYear(model);
if (releaseYear !== null) return releaseYear >= 2026;
const modelId = getModelId(model).toLowerCase();
return VERIFIED_2026_IMAGE_MODEL_PATTERNS.some((pattern) => pattern.test(modelId));
}
function resolvePresetType(model: Vip98kCatalogModel): Vip98kCatalogModelType {
const catalogType = normalizeCatalogType(model);
const modelId = getModelId(model).toLowerCase();
const capabilities = getCapabilities(model);
if (
capabilities.includes("image-to-3d") ||
capabilities.includes("text-to-3d")
) {
return "model_3d";
}
if (catalogType === "world") return "world";
if (catalogType === "image_tool" || IMAGE_TOOL_MODEL_IDS.has(modelId)) {
return "image_tool";
}
if (
catalogType === "video" ||
capabilities.some(
(capability) =>
VIDEO_CAPABILITY_KEYS.has(capability) ||
capability.endsWith("-to-video") ||
capability.startsWith("video-"),
)
) {
return "video";
}
if (
catalogType === "image" ||
capabilities.some(
(capability) =>
IMAGE_CAPABILITY_KEYS.has(capability) || capability.endsWith("-to-image"),
)
) {
return "image";
}
return catalogType;
}
const UNAVAILABLE_STATUS_TOKENS = [
"disabled",
"unavailable",
"closed",
"offline",
"deprecated",
"removed",
"retired",
"suspended",
] as const;
function getInvocationRequestKeys(model: Vip98kCatalogModel) {
return getInvocationCandidates(model).flatMap((invocation) => {
const schemaProperties = toRecord(toRecord(invocation.input_schema)?.properties);
return [
...Object.keys(toRecord(invocation.request_example) || {}),
...Object.keys(schemaProperties || {}),
].map(normalizeInvocationRequestKey);
});
}
function normalizeInvocationRequestKey(key: string) {
return key
.trim()
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.toLowerCase()
.replace(/_/g, "-");
}
function getInvocationCandidates(model: Vip98kCatalogModel) {
const configured = Array.isArray(model.invocations) ? model.invocations : [];
// The singular invocation is the provider-selected route and may carry an
// offer-specific X-Relay header. Keep it ahead of generic alternatives.
const candidates = [...(model.invocation ? [model.invocation] : []), ...configured];
return candidates.filter(
(candidate, index, items) =>
Boolean(candidate && typeof candidate === "object") &&
items.findIndex(
(item) =>
String(item?.method || "POST") === String(candidate?.method || "POST") &&
String(item?.path || "") === String(candidate?.path || "") &&
JSON.stringify(item?.request_example || {}) ===
JSON.stringify(candidate?.request_example || {}) &&
JSON.stringify(item?.input_schema || null) ===
JSON.stringify(candidate?.input_schema || null),
) === index,
);
}
function hasMediaReferenceInput(
model: Vip98kCatalogModel,
type: Vip98kCatalogModelType,
) {
const capabilities = getCapabilities(model);
const inputModes = getInputModes(model).map((mode) => mode.replace(/_/g, "-"));
const requestKeys = getInvocationRequestKeys(model);
const modelId = getModelId(model).toLowerCase();
const mediaInputModes = new Set([
"image",
"images",
"url",
"file",
"video",
"audio",
"base64",
]);
const hasMediaCapability = capabilities.some((capability) => {
if (capability.startsWith("text-to-")) return false;
if (type === "image") {
return capability.endsWith("-to-image") || capability.includes("image-edit");
}
return (
capability.endsWith("-to-video") ||
capability.startsWith("video-") ||
capability.includes("video-edit")
);
});
const hasMediaRequestField = requestKeys.some((key) =>
/(?:image|video|audio|reference|first-?frame|last-?frame|file|url)/.test(key),
);
const hasMediaModelPath =
type === "image"
? /(?:image-to-image|reference-to-image|\/edit(?:$|[\/-]))/.test(modelId)
: /(?:image-to-video|video-to-video|reference-to-video|first-frame|start-end|video-edit|video-extend)/.test(
modelId,
);
// The V2 invocation contract is the executable source of truth. Authorized
// catalog metadata can advertise broad multimodal capability even when the
// published request body only accepts a text prompt.
if (type === "image" && getInvocationCandidates(model).length > 0) {
return (
hasMediaRequestField ||
hasVerifiedVip98kOpenAiImageArrayContract(modelId)
);
}
return (
hasMediaCapability ||
inputModes.some((mode) => mediaInputModes.has(mode)) ||
hasMediaRequestField ||
hasMediaModelPath
);
}
function hasMultiImageReferenceInput(model: Vip98kCatalogModel) {
if (
hasVerifiedVip98kOpenAiImageArrayContract(getModelId(model).toLowerCase())
) {
return true;
}
const multiImageKeys = new Set([
"file-urls",
"images",
"image-urls",
"reference-images",
"video-clips",
]);
return getInvocationCandidates(model).some((invocation) => {
const requestExample = toRecord(invocation.request_example) || {};
const schemaProperties =
toRecord(toRecord(invocation.input_schema)?.properties) || {};
const keys = new Set([
...Object.keys(requestExample),
...Object.keys(schemaProperties),
]);
return Array.from(keys).some((key) => {
const normalizedKey = normalizeInvocationRequestKey(key);
if (normalizedKey !== "image" && !multiImageKeys.has(normalizedKey)) {
return false;
}
const schema = toRecord(schemaProperties[key]);
return (
Array.isArray(requestExample[key]) ||
String(schema?.type || "").trim().toLowerCase() === "array"
);
});
});
}
export function isVip98kMultiImageEditCatalogModel(model: Vip98kCatalogModel) {
if (
VIP98K_MULTI_IMAGE_MODEL_EXCLUSIONS.has(getModelId(model).toLowerCase())
) {
return false;
}
return (
resolvePresetType(model) === "image" &&
hasMediaReferenceInput(model, "image") &&
hasMultiImageReferenceInput(model)
);
}
export function isVip98kCatalogModelAvailable(model: Vip98kCatalogModel) {
const status = String(model.status || "").trim().toLowerCase();
if (UNAVAILABLE_STATUS_TOKENS.some((token) => status.includes(token))) {
return false;
}
return !(
model.available === false ||
model.isAvailable === false ||
model.enabled === false ||
model.isEnabled === false ||
model.is_active === false
);
}
export function isVip98kCatalogModelEligible(model: Vip98kCatalogModel) {
if (!getModelId(model) || !isVip98kCatalogModelAvailable(model)) return false;
const type = resolvePresetType(model);
if (type === "image") {
return isVip98kMultiImageEditCatalogModel(model);
}
if (type === "video") return hasMediaReferenceInput(model, type);
return true;
}
export function filterVip98kCatalogModels(models: Vip98kCatalogModel[]) {
return models.filter(isVip98kCatalogModelEligible);
}
function normalizeImageSize(value: string): string | null {
const normalized = value.trim().toUpperCase();
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
return normalized;
}
const kMatch = normalized.match(/^(\d+(?:\.\d+)?)\s*K$/);
if (kMatch?.[1]) return `${Number(kMatch[1]).toString()}K`;
const pixelMatch = normalized.match(/^(\d+)\s*[X*×]\s*(\d+)$/);
if (pixelMatch?.[1] && pixelMatch[2]) {
const longestEdge = Math.max(Number(pixelMatch[1]), Number(pixelMatch[2]));
if (longestEdge <= 1024) return "1K";
if (longestEdge <= 2048) return "2K";
if (longestEdge <= 4096) return "4K";
}
return null;
}
function normalizeVideoResolution(value: string): string | null {
const normalized = value.trim().toLowerCase();
if (normalized === "720p" || normalized === "1080p" || normalized === "4k") {
return normalized;
}
if (normalized === "720" || normalized === "720P".toLowerCase()) return "720p";
if (normalized === "1080" || normalized === "1080P".toLowerCase()) return "1080p";
if (normalized === "2160p" || normalized === "2160" || normalized === "4K".toLowerCase()) {
return "4k";
}
return normalized || null;
}
function getSupportedResolutions(model: Vip98kCatalogModel) {
return parseStringArray(
model.supportedResolutions || model.supported_resolutions || model.resolutions,
);
}
function getSupportedImageSizes(model: Vip98kCatalogModel) {
if (hasVerifiedVip98kGptImage25ResolutionTiers(getModelId(model))) {
return ["1K", "2K", "4K"];
}
const schemaSizes = getInvocationCandidates(model).flatMap((invocation) => {
const properties =
toRecord(toRecord(invocation.input_schema)?.properties) || {};
return ["size", "image_size", "imageSize", "resolution"].flatMap(
(key) => parseStringArray(toRecord(properties[key])?.enum),
);
});
const sizes = [...getSupportedResolutions(model), ...schemaSizes]
.map(normalizeImageSize)
.filter((value): value is string => Boolean(value));
return sizes.length ? Array.from(new Set(sizes)) : [DEFAULT_IMAGE_SIZE];
}
function getSupportedVideoResolutions(model: Vip98kCatalogModel) {
const resolutions = getSupportedResolutions(model)
.map(normalizeVideoResolution)
.filter((value): value is string => Boolean(value));
return resolutions.length ? resolutions : ["720p"];
}
function getDurations(model: Vip98kCatalogModel) {
return parseNumberArray(
model.durations || model.durationOptions || model.duration_options,
);
}
function getCapabilities(model: Vip98kCatalogModel) {
return parseStringArray(model.capabilities).map((item) =>
item.trim().toLowerCase().replace(/_/g, "-"),
);
}
function normalizeMode(value: unknown) {
return String(value || "").trim().toLowerCase().replace(/_/g, "-");
}
function getStoredSupportedModes(value: unknown) {
const record = toRecord(value);
return parseStringArray(
record?.supportedModes || record?.supported_modes,
).map(normalizeMode);
}
export function hasVip98kTextToImageMode(input: {
modelId?: string | null;
capabilities?: unknown;
defaultParams?: unknown;
}) {
const modes = new Set([
...getStoredSupportedModes(input.capabilities),
...getStoredSupportedModes(input.defaultParams),
]);
return (
modes.has("text-to-image") ||
normalizeMode(input.modelId).includes("text-to-image")
);
}
export function isVip98kTextToImageCatalogModel(
model: Vip98kCatalogModel,
) {
if (resolvePresetType(model) !== "image") return false;
const capabilities = getCapabilities(model);
return capabilities.length === 0 || capabilities.includes("text-to-image");
}
function getInputModes(model: Vip98kCatalogModel) {
return parseStringArray(model.inputModes || model.input_modes).map((item) =>
item.trim().toLowerCase(),
);
}
function getOutputModes(model: Vip98kCatalogModel) {
return parseStringArray(model.outputModes || model.output_modes).map((item) =>
item.trim().toLowerCase(),
);
}
function getDisplayName(model: Vip98kCatalogModel, modelId: string) {
return String(
model.displayName || model.display_name || model.name || modelId,
).trim();
}
function getModelId(model: Vip98kCatalogModel) {
return String(model.modelId || model.id || model.name || "").trim();
}
function getEffectivePrice(model: Vip98kCatalogModel) {
const pricing = toRecord(model.pricing);
return parseNumber(
model.price ||
pricing?.effectivePrice ||
pricing?.effective_price ||
pricing?.price ||
pricing?.requestPrice ||
pricing?.request_price,
);
}
function getBillingMode(model: Vip98kCatalogModel) {
return String(model.billingMode || model.billing_mode || "per_request").trim();
}
function getVip98kMetadata(model: Vip98kCatalogModel, type: string) {
const invocation = toRecord(model.invocation);
const invocations = getInvocationCandidates(model);
const normalizeInvocation = (value: Vip98kInvocationContract) => ({
kind: String(value.kind || ""),
method: String(value.method || "POST"),
path: String(value.path || ""),
contentType: String(value.content_type || "application/json"),
headers: normalizeVip98kInvocationHeaders(value.headers),
requestExample: toRecord(value.request_example) || {},
inputSchema: toRecord(value.input_schema),
resultMethod: String(value.result_method || "GET"),
resultPath: String(value.result_path || ""),
});
return {
modelType: type,
...(type === "image" && hasVerifiedVip98kOpenAiImageArrayContract(getModelId(model))
? { resolutionSizing: "gpt-area" }
: {}),
taskApi:
String(invocation?.kind || "").toLowerCase() === "tool"
? "external-tool"
: "external-task",
invocation: invocation
? {
kind: String(invocation.kind || ""),
method: String(invocation.method || "POST"),
path: String(invocation.path || ""),
contentType: String(invocation.content_type || "application/json"),
headers: normalizeVip98kInvocationHeaders(invocation.headers),
requestExample: toRecord(invocation.request_example) || {},
inputSchema: toRecord(invocation.input_schema),
resultMethod: String(invocation.result_method || "GET"),
resultPath: String(invocation.result_path || ""),
}
: null,
invocations: invocations.map(normalizeInvocation),
supportsAsync: Boolean(model.supportsAsync ?? model.supports_async ?? true),
pollIntervalSeconds: parseNumber(
model.pollIntervalSeconds || model.poll_interval_seconds,
) || 8,
maxPollSeconds: parseNumber(model.maxPollSeconds || model.max_poll_seconds) || 600,
resultTTLSeconds: parseNumber(
model.resultTTLSeconds || model.result_ttl_seconds,
),
billingMode: getBillingMode(model),
effectivePrice: getEffectivePrice(model),
pricing: model.pricing || null,
status: model.status || null,
};
}
function createImagePreset(
model: Vip98kCatalogModel,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
const capabilities = getCapabilities(model);
const inputModes = getInputModes(model);
const outputModes = getOutputModes(model);
const supportedImageSizes = getSupportedImageSizes(model);
const invocationKeys = new Set(getInvocationRequestKeys(model));
const supportsReferenceImages = hasMediaReferenceInput(model, "image");
const supportsMultiImageReference = hasMultiImageReferenceInput(model);
const hasVerifiedResolutionTiers =
hasVerifiedVip98kGptImage25ResolutionTiers(modelId);
const supportsNativeSize = [
"size",
"image-size",
"resolution",
"width",
"height",
].some((key) => invocationKeys.has(key)) || hasVerifiedResolutionTiers;
const supportsNativeAspectRatio = [
"ratio",
"aspect-ratio",
].some((key) => invocationKeys.has(key)) || hasVerifiedVip98kOpenAiImageArrayContract(modelId);
const supportsQuality = invocationKeys.has("quality");
// Flare repeatedly returns 3:4/4:3 for 4:5/5:4, even with aspect_ratio supplied.
const supportedAspectRatios = supportsNativeAspectRatio
? DEFAULT_ASPECT_RATIOS.filter((ratio) => modelId !== "gpt-image-2.5-flare" || !["4:5", "5:4"].includes(ratio))
: ["auto"];
const supportedModes = [
...(capabilities.includes("text-to-image") || capabilities.length === 0
? ["text-to-image"]
: []),
...(supportsReferenceImages ? ["image-to-image", "reference-images"] : []),
];
const maxReferenceImages = supportsMultiImageReference
? 8
: supportsReferenceImages
? 1
: 0;
const defaultImageSize =
supportedImageSizes.find((size) => size === "2K") ||
supportedImageSizes[0] ||
"1K";
return {
modelId,
displayName,
description: model.description || `VIP98K image model (${modelId})`,
type: "image",
supportsVision: supportsReferenceImages,
supportsImageGen: true,
isHidden: !isVip98kImageModelVisible(model),
capabilities: {
provider: "vip98k",
kind: "image",
mediaType: "image",
modelType: "image",
outputType: "image",
supportedModes,
inputModes,
outputModes,
supportedImageSizes: supportsNativeSize ? supportedImageSizes : [],
supportedAspectRatios,
supportsMultiImageReference,
maxReferenceImages,
vip98k: getVip98kMetadata(model, "image"),
},
defaultParams: {
provider: "vip98k",
kind: "image",
endpoint: modelId,
apiFormat: "vip98k-external-task",
taskApi: "external-task",
supportedModes,
...(supportsNativeSize ? { imageSize: defaultImageSize } : {}),
...(supportsQuality ? { quality: "auto" } : {}),
aspectRatio: "auto",
imageCount: 1,
},
imageGenConfig: {
maxImages: 1,
maxReferenceImages,
supportsSize: supportsNativeSize,
supportsQuality,
supports1K: supportsNativeSize && supportedImageSizes.includes("1K"),
supports2K: supportsNativeSize && supportedImageSizes.includes("2K"),
supports4K: supportsNativeSize && supportedImageSizes.includes("4K"),
supportedImageSizes: supportsNativeSize ? supportedImageSizes : [],
supportedAspectRatios,
supportsPromptExtend: false,
supportsNegativePrompt: false,
supportsWatermark: false,
supportsSeed: false,
creditCostPerImage: 0,
creditCostMultiplierBySize: {
"1K": 1,
"2K": 1,
"4K": 1,
},
enableLoadingPlaceholder: true,
loadingColor: "yellow",
vip98k: getVip98kMetadata(model, "image"),
},
sortOrder: TYPE_SORT_ORDER.image + index,
};
}
function createVideoPreset(
model: Vip98kCatalogModel,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
const capabilities = getCapabilities(model);
const inputModes = getInputModes(model);
const outputModes = getOutputModes(model);
const supportedResolutions = getSupportedVideoResolutions(model);
const durations = getDurations(model);
const defaultDuration =
parseNumber(model.defaultDuration || model.default_duration) ||
durations[durations.length - 1] ||
8;
const supportsReferenceImages =
capabilities.includes("image-to-video") ||
inputModes.includes("url") ||
inputModes.includes("file");
const supportedModes = [
...(capabilities.includes("text-to-video") || capabilities.length === 0
? ["text-to-video"]
: []),
...(supportsReferenceImages ? ["image-to-video", "first-frame"] : []),
];
return {
modelId,
displayName,
description: model.description || `VIP98K video model (${modelId})`,
type: "video",
supportsVision: supportsReferenceImages,
supportsImageGen: false,
isHidden: false,
capabilities: {
provider: "vip98k",
kind: "video",
mediaType: "video",
modelType: "video",
outputType: "video",
supportedModes,
inputModes,
outputModes,
supportedResolutions,
supportedRatios: [...DEFAULT_VIDEO_RATIOS],
durationOptions: durations,
durationRange: durations.length
? { min: durations[0], max: durations[durations.length - 1] }
: { min: defaultDuration, max: defaultDuration },
defaultDuration,
supportWatermark: false,
supportCameraFixed: false,
maxReferenceImages: supportsReferenceImages ? 4 : 0,
vip98k: getVip98kMetadata(model, "video"),
},
defaultParams: {
provider: "vip98k",
kind: "video",
endpoint: modelId,
taskApi: "external-task",
supportedModes,
duration: defaultDuration,
resolution: supportedResolutions[0] || "720p",
ratio: "16:9",
},
imageGenConfig: {
creditCostPerVideo: 0,
videoDurationOptions: durations,
creditCostPerVideoByDuration: Object.fromEntries(
durations.map((duration) => [String(duration), 0]),
),
creditCostPerVideoByResolution: Object.fromEntries(
supportedResolutions.map((resolution) => [resolution, 0]),
),
enableLoadingPlaceholder: true,
loadingColor: "blue",
vip98k: getVip98kMetadata(model, "video"),
},
sortOrder: TYPE_SORT_ORDER.video + index,
};
}
function createWorldPreset(
model: Vip98kCatalogModel,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
return {
modelId,
displayName,
description: model.description || `VIP98K 3D world model (${modelId})`,
type: "world",
supportsVision: false,
supportsImageGen: false,
isHidden: false,
capabilities: {
provider: "vip98k",
kind: "world",
mediaType: "world",
modelType: "world-generation",
outputType: "3d",
supportedModes: ["world-generation", "text-to-3d"],
vip98k: getVip98kMetadata(model, "world"),
},
defaultParams: {
provider: "vip98k",
kind: "world",
endpoint: modelId,
taskApi: "external-task",
},
imageGenConfig: null,
sortOrder: TYPE_SORT_ORDER.world + index,
};
}
function createThreeDPreset(
model: Vip98kCatalogModel,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
const catalogCapabilities = getCapabilities(model);
const supportsImageInput = catalogCapabilities.includes("image-to-3d");
const supportedModes = [
...(catalogCapabilities.includes("text-to-3d") ? ["text-to-3d"] : []),
...(supportsImageInput ? ["image-to-3d"] : []),
];
return {
modelId,
displayName,
description: model.description || `VIP98K 3D model (${modelId})`,
type: "model_3d",
supportsVision: supportsImageInput,
supportsImageGen: false,
// Keep the dedicated Tripo 3D integration as the only frontend 3D route.
isHidden: true,
capabilities: {
provider: "vip98k",
kind: "model-3d",
mediaType: "model-3d",
modelType: "model-3d",
outputType: "3d",
supportedModes,
maxReferenceImages: supportsImageInput ? 1 : 0,
vip98k: getVip98kMetadata(model, "model_3d"),
},
defaultParams: {
provider: "vip98k",
kind: "model-3d",
endpoint: modelId,
taskApi: "external-task",
supportedModes,
},
imageGenConfig: null,
sortOrder: TYPE_SORT_ORDER.model_3d + index,
};
}
function createChatPreset(
model: Vip98kCatalogModel,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
const catalogCapabilities = getCapabilities(model);
const inputModes = getInputModes(model);
const supportsVision =
catalogCapabilities.some((capability) =>
["vision", "multimodal", "image-input", "image-understanding"].includes(
capability,
),
) ||
inputModes.some((mode) => ["image", "url", "file"].includes(mode)) ||
/^(?:gpt-|claude-|qwen)/i.test(modelId);
return {
modelId,
displayName,
description: model.description || `VIP98K text model (${modelId})`,
type: "model",
supportsVision,
supportsImageGen: false,
isHidden: false,
capabilities: {
provider: "vip98k",
kind: "chat",
mediaType: "text",
modelType: "model",
outputType: "text",
apiFormat: "openai-compatible",
inputModes,
supportedModes: supportsVision ? ["text", "vision"] : ["text"],
vip98k: getVip98kMetadata(model, "model"),
},
defaultParams: {
provider: "vip98k",
kind: "chat",
endpoint: modelId,
apiFormat: "openai-compatible",
},
imageGenConfig: null,
sortOrder: TYPE_SORT_ORDER.model + index,
};
}
function createToolPreset(
model: Vip98kCatalogModel,
type: string,
index: number,
): Vip98kModelPreset {
const modelId = getModelId(model);
const displayName = getDisplayName(model, modelId);
const inputModes = getInputModes(model);
const outputModes = getOutputModes(model);
const isImageTool = type === "image_tool";
const toolOperation = IMAGE_TOOL_OPERATION_BY_MODEL_ID[modelId.toLowerCase()] || type;
return {
modelId,
displayName,
description: model.description || `VIP98K ${type} model (${modelId})`,
type,
supportsVision: isImageTool,
supportsImageGen: false,
isHidden: true,
capabilities: {
provider: "vip98k",
kind: type.replace(/_/g, "-"),
mediaType: isImageTool ? "image" : type === "audio" ? "audio" : "document",
modelType: type,
outputType: outputModes[0] || (isImageTool ? "image" : "text"),
inputModes,
outputModes,
supportedModes: getCapabilities(model),
toolOperation,
vip98k: getVip98kMetadata(model, type),
},
defaultParams: {
provider: "vip98k",
kind: type.replace(/_/g, "-"),
endpoint: modelId,
toolOperation,
taskApi: type === "audio" ? "openai-compatible" : "external-task",
},
imageGenConfig: null,
sortOrder: (TYPE_SORT_ORDER[type] || 990) + index,
};
}
export function buildVip98kModelPresets(
models: Vip98kCatalogModel[],
): Vip98kModelPreset[] {
return filterVip98kCatalogModels(models)
.map((model, index) => {
const modelId = getModelId(model);
if (!modelId) return null;
const type = resolvePresetType(model);
if (type === "image") return createImagePreset(model, index);
if (type === "video") return createVideoPreset(model, index);
if (type === "world") return createWorldPreset(model, index);
if (type === "model_3d") return createThreeDPreset(model, index);
if (type === "model") return createChatPreset(model, index);
return createToolPreset(model, type, index);
})
.filter((item): item is Vip98kModelPreset => Boolean(item))
.sort((a, b) => a.sortOrder - b.sortOrder || a.displayName.localeCompare(b.displayName));
}
export function normalizeVip98kBaseUrl(baseURL?: string | null) {
const normalized = String(baseURL || VIP98K_BASE_URL).trim().replace(/\/+$/, "");
if (!normalized) return VIP98K_BASE_URL;
if (/\/v1$/i.test(normalized)) return normalized.replace(/\/v1$/i, "");
if (/\/api\/external\/task$/i.test(normalized)) {
return normalized.replace(/\/api\/external\/task$/i, "");
}
return normalized;
}
function extractCatalogModels(payload: unknown): Vip98kCatalogModel[] {
const record = toRecord(payload);
const data = record?.data;
const dataRecord = toRecord(data);
const candidates = [
dataRecord?.models,
dataRecord?.items,
dataRecord?.list,
data,
record?.models,
record?.items,
];
for (const candidate of candidates) {
if (Array.isArray(candidate)) return candidate as Vip98kCatalogModel[];
}
return [];
}
function extractPublicCatalogModels(payload: unknown): Vip98kCatalogModel[] {
const record = toRecord(payload);
const data = toRecord(record?.data);
const sections = Array.isArray(data?.sections) ? data.sections : [];
return sections.flatMap((section) => {
const sectionRecord = toRecord(section);
const models = Array.isArray(sectionRecord?.models) ? sectionRecord.models : [];
return models.filter((model): model is Vip98kCatalogModel => Boolean(toRecord(model)));
});
}
async function parseJsonResponse(response: Response) {
const text = await response.text();
if (!text.trim()) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
function extractErrorMessage(payload: unknown, fallback = "VIP98K model catalog request failed") {
if (typeof payload === "string" && payload.trim()) return payload.trim();
const record = toRecord(payload);
const data = toRecord(record?.data);
const error = toRecord(record?.error);
const value =
data?.message ||
data?.error ||
error?.message ||
record?.message ||
record?.error ||
fallback;
return typeof value === "string" && value.trim() ? value.trim() : fallback;
}
export async function fetchVip98kCatalog(params: {
baseURL?: string | null;
apiKey: string;
}): Promise<Vip98kCatalogModel[]> {
const baseURL = normalizeVip98kBaseUrl(params.baseURL);
const publicResponse = await fetch(`${baseURL}/api/v2/catalog/models`, {
cache: "no-store",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(60_000),
});
const publicPayload = await parseJsonResponse(publicResponse);
if (!publicResponse.ok) {
throw new Error(
`VIP98K public invocation catalog failed (HTTP ${publicResponse.status})`,
);
}
const publicRecord = toRecord(publicPayload);
const publicData = toRecord(publicRecord?.data);
const discovery = toRecord(publicData?.discovery);
const discoveredAuthorizedPath = String(
discovery?.authorized_models_path || "/v1/models",
).trim();
const authorizedPath =
discoveredAuthorizedPath.startsWith("/") &&
!discoveredAuthorizedPath.startsWith("//")
? discoveredAuthorizedPath
: "/v1/models";
const authorizedResponse = await fetch(`${baseURL}${authorizedPath}`, {
cache: "no-store",
headers: {
Authorization: `Bearer ${params.apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(60_000),
});
const authorizedPayload = await parseJsonResponse(authorizedResponse);
if (!authorizedResponse.ok) {
throw new Error(
`${extractErrorMessage(authorizedPayload)} (HTTP ${authorizedResponse.status})`,
);
}
const authorizedModels = extractCatalogModels(authorizedPayload);
const publicModels = extractPublicCatalogModels(publicPayload);
if (authorizedModels.length === 0) {
throw new Error("VIP98K model catalog is empty or has an unsupported response shape");
}
const publicById = new Map(
publicModels.map((model) => [getModelId(model).toLowerCase(), model]),
);
return authorizedModels.map((model) => {
const publicModel = publicById.get(getModelId(model).toLowerCase());
if (!publicModel) return model;
const authorizedResolutions = getSupportedResolutions(model);
const publicResolutions = getSupportedResolutions(publicModel);
return {
...publicModel,
...model,
invocation: publicModel.invocation,
invocations: publicModel.invocations,
serviceOffers: publicModel.serviceOffers || publicModel.service_offers,
taskTypes: model.taskTypes || model.task_types || publicModel.taskTypes || publicModel.task_types,
capabilities: model.capabilities || publicModel.capabilities,
supportedResolutions:
authorizedResolutions.length > 0
? authorizedResolutions
: publicResolutions,
};
});
}
export async function fetchVip98kModelPresets(params: {
baseURL?: string | null;
apiKey: string;
}) {
const models = await fetchVip98kCatalog(params);
return buildVip98kModelPresets(models);
}