ai-chatbot/lib/ai/vip98k-models.ts
2026-08-10 23:09:26 +08:00

1010 lines
30 KiB
TypeScript

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;
request_example?: 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;
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_SIZES = ["1K", "2K", "4K"] as const;
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(?:$|[-/])/,
/^gpt-image-2(?:$|[-/])/,
/^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(?:$|[-/])/,
];
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) {
const invocation = toRecord(model.invocation);
return Object.keys(toRecord(invocation?.request_example) || {}).map((key) =>
key.trim().toLowerCase().replace(/_/g, "-"),
);
}
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,
);
return (
hasMediaCapability ||
inputModes.some((mode) => mediaInputModes.has(mode)) ||
hasMediaRequestField ||
hasMediaModelPath
);
}
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" && type !== "video") return true;
return hasMediaReferenceInput(model, type);
}
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`;
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) {
const sizes = getSupportedResolutions(model)
.map(normalizeImageSize)
.filter((value): value is string => Boolean(value));
return sizes.length ? sizes : [...DEFAULT_IMAGE_SIZES];
}
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 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);
return {
modelType: type,
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"),
requestExample: toRecord(invocation.request_example) || {},
resultMethod: String(invocation.result_method || "GET"),
resultPath: String(invocation.result_path || ""),
}
: null,
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 supportsReferenceImages =
capabilities.includes("image-to-image") ||
inputModes.includes("url") ||
inputModes.includes("file");
const supportedModes = [
...(capabilities.includes("text-to-image") || capabilities.length === 0
? ["text-to-image"]
: []),
...(supportsReferenceImages ? ["image-to-image", "reference-images"] : []),
];
const maxReferenceImages = supportsReferenceImages ? 8 : 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,
supportedAspectRatios: [...DEFAULT_ASPECT_RATIOS],
supportsMultiImageReference: supportsReferenceImages,
maxReferenceImages,
vip98k: getVip98kMetadata(model, "image"),
},
defaultParams: {
provider: "vip98k",
kind: "image",
endpoint: modelId,
apiFormat: "openai-images-compatible",
taskApi: "openai-images",
supportedModes,
imageSize: defaultImageSize,
aspectRatio: "auto",
imageCount: 1,
},
imageGenConfig: {
maxImages: 1,
maxReferenceImages,
supportsSize: true,
supports1K: supportedImageSizes.includes("1K"),
supports2K: supportedImageSizes.includes("2K"),
supports4K: supportedImageSizes.includes("4K"),
supportedImageSizes,
supportedAspectRatios: [...DEFAULT_ASPECT_RATIOS],
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 [authorizedResponse, publicResponse] = await Promise.all([
fetch(`${baseURL}/api/external/task/models`, {
cache: "no-store",
headers: {
Authorization: `Bearer ${params.apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(60_000),
}),
fetch(`${baseURL}/api/v1/public/models`, {
cache: "no-store",
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(60_000),
}),
]);
const [authorizedPayload, publicPayload] = await Promise.all([
parseJsonResponse(authorizedResponse),
parseJsonResponse(publicResponse),
]);
if (!authorizedResponse.ok) {
throw new Error(
`${extractErrorMessage(authorizedPayload)} (HTTP ${authorizedResponse.status})`,
);
}
if (!publicResponse.ok) {
throw new Error(
`VIP98K public invocation catalog failed (HTTP ${publicResponse.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");
}
if (publicModels.length === 0) {
throw new Error("VIP98K public invocation catalog is empty");
}
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;
return {
...publicModel,
...model,
invocation: publicModel.invocation,
taskTypes: model.taskTypes || model.task_types || publicModel.taskTypes || publicModel.task_types,
capabilities: model.capabilities || publicModel.capabilities,
};
});
}
export async function fetchVip98kModelPresets(params: {
baseURL?: string | null;
apiKey: string;
}) {
const models = await fetchVip98kCatalog(params);
return buildVip98kModelPresets(models);
}