951 lines
28 KiB
TypeScript
951 lines
28 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;
|
|
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;
|
|
}
|
|
|
|
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 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);
|
|
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 models
|
|
.map((model, index) => {
|
|
const modelId = getModelId(model);
|
|
if (!modelId) return null;
|
|
if (isVip98kTextToImageCatalogModel(model)) 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);
|
|
}
|