1239 lines
39 KiB
TypeScript
1239 lines
39 KiB
TypeScript
import { canonicalVip98kVideoRatio, vip98kVideoDurationBounds } from "./vip98k-video-parameters";
|
||
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 || toRecord((model as any).duration)?.allowed_seconds,
|
||
);
|
||
}
|
||
|
||
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 invocation = getInvocationCandidates(model)[0];
|
||
const schema = toRecord(toRecord(invocation?.input_schema)?.properties) || {};
|
||
const schemaResolutions = parseStringArray(toRecord(schema.resolution)?.enum);
|
||
const supportedResolutions = schemaResolutions.length ? schemaResolutions.map(normalizeVideoResolution).filter((value): value is string => Boolean(value)) : getSupportedVideoResolutions(model);
|
||
const bounds = vip98kVideoDurationBounds(invocation?.input_schema);
|
||
const durations = bounds.durations || (bounds.min > 0 && bounds.max <= 120
|
||
? Array.from({ length: Math.floor(bounds.max) - Math.ceil(bounds.min) + 1 }, (_, index) => Math.ceil(bounds.min) + index)
|
||
: getDurations(model));
|
||
const defaultDuration =
|
||
parseNumber(model.defaultDuration || model.default_duration) ||
|
||
durations[durations.length - 1] ||
|
||
8;
|
||
const keys = new Set(getInvocationRequestKeys(model));
|
||
const imageModes = parseStringArray(toRecord(schema.mode_image)?.enum);
|
||
const supportsReferenceImages = capabilities.includes("image-to-video") || keys.has("file-urls") || keys.has("image") || keys.has("image-url");
|
||
const supportsFramePair = imageModes.includes("frame");
|
||
const supportsIngredients = imageModes.includes("ingredient");
|
||
const supportedModes = [
|
||
...(capabilities.includes("text-to-video") || keys.has("prompt") ? ["text-to-video"] : []),
|
||
...(supportsReferenceImages ? ["first-frame"] : []),
|
||
...(supportsFramePair ? ["first-last-frame"] : []),
|
||
...(supportsIngredients ? ["reference-images"] : []),
|
||
];
|
||
const ratios = parseStringArray(toRecord(schema.aspect_ratio)?.enum).map(canonicalVip98kVideoRatio);
|
||
const maxReferenceImages = Number(toRecord(schema.file_urls)?.maxItems) || (supportsReferenceImages ? 1 : 0);
|
||
|
||
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: ratios.length ? ratios : [...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,
|
||
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 [];
|
||
}
|
||
|
||
export 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);
|
||
}
|