fix(ai): honor VIP98K invocation contracts
This commit is contained in:
parent
2676d9a96f
commit
93158e0b7b
@ -30,6 +30,16 @@ type Vip98kModelRuntimeConfig = {
|
||||
imageGenConfig?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type Vip98kInvocation = {
|
||||
kind: string;
|
||||
method: string;
|
||||
path: string;
|
||||
contentType: string;
|
||||
requestExample: Record<string, unknown>;
|
||||
resultMethod: string;
|
||||
resultPath: string;
|
||||
};
|
||||
|
||||
type UploadedImage = {
|
||||
thumbnail: string;
|
||||
original: string;
|
||||
@ -75,6 +85,23 @@ function getVip98kMetadata(config: Vip98kModelRuntimeConfig) {
|
||||
return capabilitiesVip98k || imageConfigVip98k || {};
|
||||
}
|
||||
|
||||
function getVip98kInvocation(config: Vip98kModelRuntimeConfig): Vip98kInvocation | null {
|
||||
const metadata = getVip98kMetadata(config);
|
||||
const invocation = toRecord(metadata.invocation);
|
||||
if (!invocation) return null;
|
||||
const path = String(invocation.path || "").trim();
|
||||
if (!path.startsWith("/")) return null;
|
||||
return {
|
||||
kind: String(invocation.kind || "task").trim().toLowerCase(),
|
||||
method: String(invocation.method || "POST").trim().toUpperCase(),
|
||||
path,
|
||||
contentType: String(invocation.contentType || "application/json").trim(),
|
||||
requestExample: { ...(toRecord(invocation.requestExample) || {}) },
|
||||
resultMethod: String(invocation.resultMethod || "GET").trim().toUpperCase(),
|
||||
resultPath: String(invocation.resultPath || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function inferKind(modelId: string, config: Vip98kModelRuntimeConfig) {
|
||||
const capabilities = config.capabilities;
|
||||
const metadata = getVip98kMetadata(config);
|
||||
@ -391,14 +418,19 @@ function getConfiguredPollSeconds(config: Vip98kModelRuntimeConfig, key: string,
|
||||
async function submitTask(params: {
|
||||
apiKey: string;
|
||||
baseURL?: string | null;
|
||||
path?: string | null;
|
||||
method?: string | null;
|
||||
contentType?: string | null;
|
||||
body: Record<string, unknown>;
|
||||
}) {
|
||||
const baseURL = normalizeVip98kBaseUrl(params.baseURL);
|
||||
const response = await fetch(`${baseURL}/api/external/task/generate`, {
|
||||
method: "POST",
|
||||
const path = String(params.path || "/api/external/task/generate").trim();
|
||||
if (!path.startsWith("/")) throw new Error("VIP98K invocation path is invalid");
|
||||
const response = await fetch(`${baseURL}${path}`, {
|
||||
method: String(params.method || "POST").toUpperCase(),
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": params.contentType || "application/json",
|
||||
},
|
||||
body: JSON.stringify(params.body),
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
@ -459,14 +491,21 @@ export async function submitVip98kExternalTask(params: {
|
||||
imageParams: params.imageParams,
|
||||
modelConfig: params.modelConfig || {},
|
||||
};
|
||||
const invocation = getVip98kInvocation(taskParams.modelConfig);
|
||||
const submitted = await submitTask({
|
||||
apiKey: params.apiKey,
|
||||
baseURL: params.baseURL,
|
||||
body: buildTaskBody(taskParams),
|
||||
path: invocation?.path,
|
||||
method: invocation?.method,
|
||||
contentType: invocation?.contentType,
|
||||
body: buildTaskBody(taskParams, invocation),
|
||||
});
|
||||
const resultPath = submitted.resultPath || invocation?.resultPath || "";
|
||||
return {
|
||||
taskId: submitted.taskId,
|
||||
resultPath: submitted.resultPath,
|
||||
resultPath: submitted.taskId
|
||||
? resultPath.replaceAll("{task_id}", encodeURIComponent(submitted.taskId))
|
||||
: resultPath,
|
||||
outputUrls: submitted.outputs,
|
||||
payload: submitted.payload,
|
||||
};
|
||||
@ -573,15 +612,16 @@ function normalizeDuration(value: unknown) {
|
||||
return Math.round(numeric);
|
||||
}
|
||||
|
||||
function buildTaskBody(params: Vip98kTaskParams) {
|
||||
function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation | null) {
|
||||
const videoParams = params.videoParams || {};
|
||||
const imageParams = params.imageParams || {};
|
||||
const promptTitle = params.prompt.split(/\s+/).slice(0, 8).join(" ").slice(0, 80);
|
||||
const body: Record<string, unknown> = {
|
||||
model: params.modelId,
|
||||
displayName: promptTitle || params.modelId,
|
||||
prompt: params.prompt,
|
||||
};
|
||||
const body: Record<string, unknown> = { ...(invocation?.requestExample || {}) };
|
||||
if (invocation?.kind !== "tool" || "model" in body) body.model = params.modelId;
|
||||
if ("displayName" in body || invocation?.kind !== "tool") {
|
||||
body.displayName = promptTitle || params.modelId;
|
||||
}
|
||||
if ("prompt" in body || !invocation) body.prompt = params.prompt;
|
||||
|
||||
const aspectRatio =
|
||||
normalizeRatio(videoParams.ratio) ||
|
||||
@ -592,54 +632,64 @@ function buildTaskBody(params: Vip98kTaskParams) {
|
||||
const negativePrompt = String(videoParams.negativePrompt || imageParams.negativePrompt || "").trim();
|
||||
|
||||
if (aspectRatio) {
|
||||
body.ratio = aspectRatio;
|
||||
body.aspectRatio = aspectRatio;
|
||||
body.aspect_ratio = aspectRatio;
|
||||
if ("ratio" in body || !invocation) body.ratio = aspectRatio;
|
||||
if ("aspectRatio" in body || !invocation) body.aspectRatio = aspectRatio;
|
||||
if ("aspect_ratio" in body || !invocation) body.aspect_ratio = aspectRatio;
|
||||
}
|
||||
if (duration) body.duration = duration;
|
||||
if (duration && ("duration" in body || !invocation)) body.duration = duration;
|
||||
if (resolution) {
|
||||
body.resolution = resolution;
|
||||
body.imageSize = resolution;
|
||||
body.image_size = resolution;
|
||||
if ("resolution" in body || !invocation) body.resolution = resolution;
|
||||
if ("imageSize" in body || !invocation) body.imageSize = resolution;
|
||||
if ("image_size" in body || !invocation) body.image_size = resolution;
|
||||
if ("size" in body && typeof body.size === "string") body.size = resolution;
|
||||
}
|
||||
if (negativePrompt) {
|
||||
body.negativePrompt = negativePrompt;
|
||||
body.negative_prompt = negativePrompt;
|
||||
if ("negativePrompt" in body || !invocation) body.negativePrompt = negativePrompt;
|
||||
if ("negative_prompt" in body || !invocation) body.negative_prompt = negativePrompt;
|
||||
}
|
||||
if (typeof videoParams.generateAudio === "boolean") {
|
||||
body.generateAudio = videoParams.generateAudio;
|
||||
body.generate_audio = videoParams.generateAudio;
|
||||
if ("generateAudio" in body || !invocation) body.generateAudio = videoParams.generateAudio;
|
||||
if ("generate_audio" in body || !invocation) body.generate_audio = videoParams.generateAudio;
|
||||
}
|
||||
|
||||
if (params.imageUrls.length > 0) {
|
||||
body.images = params.imageUrls;
|
||||
body.imageUrls = params.imageUrls;
|
||||
body.referenceImages = params.imageUrls;
|
||||
body.reference_images = params.imageUrls;
|
||||
body.image = params.imageUrls[0];
|
||||
body.imageUrl = params.imageUrls[0];
|
||||
body.image_url = params.imageUrls[0];
|
||||
body.firstFrameImage = params.imageUrls[0];
|
||||
body.first_frame_image = params.imageUrls[0];
|
||||
body.firstFrameImageUrl = params.imageUrls[0];
|
||||
body.first_frame_image_url = params.imageUrls[0];
|
||||
const setImageField = (key: string, value: unknown) => {
|
||||
if (key in body || !invocation) body[key] = value;
|
||||
};
|
||||
setImageField("images", params.imageUrls);
|
||||
setImageField("imageUrls", params.imageUrls);
|
||||
setImageField("referenceImages", params.imageUrls);
|
||||
setImageField("reference_images", params.imageUrls);
|
||||
setImageField("image", params.imageUrls[0]);
|
||||
setImageField("imageUrl", params.imageUrls[0]);
|
||||
setImageField("image_url", params.imageUrls[0]);
|
||||
setImageField("firstFrameImage", params.imageUrls[0]);
|
||||
setImageField("first_frame_image", params.imageUrls[0]);
|
||||
setImageField("firstFrameImageUrl", params.imageUrls[0]);
|
||||
setImageField("first_frame_image_url", params.imageUrls[0]);
|
||||
if (Array.isArray(body.video_clips)) {
|
||||
const template = toRecord(body.video_clips[0]) || {};
|
||||
body.video_clips = params.imageUrls.map((url) => ({ ...template, url }));
|
||||
}
|
||||
if (params.imageUrls[1]) {
|
||||
body.lastFrameImage = params.imageUrls[1];
|
||||
body.last_frame_image = params.imageUrls[1];
|
||||
body.endImageUrl = params.imageUrls[1];
|
||||
body.end_image_url = params.imageUrls[1];
|
||||
setImageField("lastFrameImage", params.imageUrls[1]);
|
||||
setImageField("last_frame_image", params.imageUrls[1]);
|
||||
setImageField("endImageUrl", params.imageUrls[1]);
|
||||
setImageField("end_image_url", params.imageUrls[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.videoUrls.length > 0) {
|
||||
body.videos = params.videoUrls;
|
||||
body.videoUrls = params.videoUrls;
|
||||
body.referenceVideos = params.videoUrls;
|
||||
if ("videos" in body || !invocation) body.videos = params.videoUrls;
|
||||
if ("videoUrls" in body || !invocation) body.videoUrls = params.videoUrls;
|
||||
if ("referenceVideos" in body || !invocation) body.referenceVideos = params.videoUrls;
|
||||
if ("video" in body) body.video = params.videoUrls[0];
|
||||
if ("video_url" in body) body.video_url = params.videoUrls[0];
|
||||
}
|
||||
|
||||
if (params.kind === "world") {
|
||||
if (params.kind === "world" && ("mode" in body || !invocation)) {
|
||||
body.mode = "world-generation";
|
||||
} else if (params.kind === "video") {
|
||||
} else if (params.kind === "video" && ("mode" in body || !invocation)) {
|
||||
body.mode = params.imageUrls.length > 0 ? "image-to-video" : "text-to-video";
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,16 @@ export type Vip98kCatalogModelType =
|
||||
| "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;
|
||||
@ -47,6 +57,9 @@ export interface Vip98kCatalogModel {
|
||||
pricing?: unknown;
|
||||
price?: unknown;
|
||||
status?: string;
|
||||
invocation?: Vip98kInvocationContract;
|
||||
taskTypes?: unknown;
|
||||
task_types?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@ -248,9 +261,24 @@ function getBillingMode(model: Vip98kCatalogModel) {
|
||||
}
|
||||
|
||||
function getVip98kMetadata(model: Vip98kCatalogModel, type: string) {
|
||||
const invocation = toRecord(model.invocation);
|
||||
return {
|
||||
modelType: type,
|
||||
taskApi: "external-task",
|
||||
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,
|
||||
@ -646,6 +674,17 @@ function extractCatalogModels(payload: unknown): 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;
|
||||
@ -676,23 +715,58 @@ export async function fetchVip98kCatalog(params: {
|
||||
apiKey: string;
|
||||
}): Promise<Vip98kCatalogModel[]> {
|
||||
const baseURL = normalizeVip98kBaseUrl(params.baseURL);
|
||||
const response = await fetch(`${baseURL}/api/external/task/models`, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
const payload = await parseJsonResponse(response);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${extractErrorMessage(payload)} (HTTP ${response.status})`);
|
||||
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})`,
|
||||
);
|
||||
}
|
||||
const models = extractCatalogModels(payload);
|
||||
if (models.length === 0) {
|
||||
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");
|
||||
}
|
||||
return models;
|
||||
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: {
|
||||
|
||||
@ -15,6 +15,19 @@ type Vip98kCatalogModel = {
|
||||
model_id?: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
display_name?: string;
|
||||
invocation?: Vip98kInvocation;
|
||||
};
|
||||
|
||||
type Vip98kInvocation = {
|
||||
kind?: string;
|
||||
method?: string;
|
||||
path?: string;
|
||||
content_type?: string;
|
||||
request_example?: Record<string, unknown>;
|
||||
result_method?: string;
|
||||
result_path?: string;
|
||||
};
|
||||
|
||||
const CAPABILITIES = [
|
||||
@ -50,8 +63,8 @@ const DEFAULT_MODEL_IDS: Record<Capability, string[]> = {
|
||||
multimodal: ["gpt-5.4-mini", "gpt-5.4", "claude-sonnet-4-6"],
|
||||
image: ["nano-banana-2", "nano-banana-pro", "gpt-image-2-vip"],
|
||||
"multi-image-edit": [
|
||||
"google/nano-banana-2/reference-to-image",
|
||||
"google/nano-banana-pro/edit",
|
||||
"google/nano-banana-2/edit",
|
||||
"openai/gpt-image-2/edit",
|
||||
],
|
||||
rmbg: ["remove-background"],
|
||||
@ -93,25 +106,53 @@ function normalizeVip98kBaseUrl(baseUrl?: string | null) {
|
||||
}
|
||||
|
||||
async function fetchVip98kCatalog(params: { baseURL: string; apiKey: string }) {
|
||||
const payload = await parseResponse(
|
||||
await fetch(`${normalizeVip98kBaseUrl(params.baseURL)}/api/external/task/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
}),
|
||||
);
|
||||
const record = payload && typeof payload === "object" ? (payload as Record<string, any>) : {};
|
||||
const baseUrl = normalizeVip98kBaseUrl(params.baseURL);
|
||||
const [authorizedPayload, publicPayload] = await Promise.all([
|
||||
parseResponse(
|
||||
await fetch(`${baseUrl}/api/external/task/models`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
}),
|
||||
),
|
||||
parseResponse(
|
||||
await fetch(`${baseUrl}/api/v1/public/models`, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const record =
|
||||
authorizedPayload && typeof authorizedPayload === "object"
|
||||
? (authorizedPayload as Record<string, any>)
|
||||
: {};
|
||||
const data = record.data;
|
||||
const models =
|
||||
const authorized =
|
||||
(Array.isArray(data?.models) && data.models) ||
|
||||
(Array.isArray(data?.items) && data.items) ||
|
||||
(Array.isArray(data) && data) ||
|
||||
(Array.isArray(record.models) && record.models) ||
|
||||
[];
|
||||
if (models.length === 0) throw new Error("VIP98K model catalog is empty");
|
||||
return models as Vip98kCatalogModel[];
|
||||
const publicRecord =
|
||||
publicPayload && typeof publicPayload === "object"
|
||||
? (publicPayload as Record<string, any>)
|
||||
: {};
|
||||
const publicModels = (Array.isArray(publicRecord.data?.sections)
|
||||
? publicRecord.data.sections
|
||||
: []
|
||||
).flatMap((section: any) => (Array.isArray(section?.models) ? section.models : []));
|
||||
if (authorized.length === 0) throw new Error("VIP98K authorized model catalog is empty");
|
||||
if (publicModels.length === 0) throw new Error("VIP98K public invocation catalog is empty");
|
||||
const publicById = new Map(
|
||||
publicModels.map((model: Vip98kCatalogModel) => [getModelId(model).toLowerCase(), model]),
|
||||
);
|
||||
return authorized.map((model: Vip98kCatalogModel) => ({
|
||||
...(publicById.get(getModelId(model).toLowerCase()) || {}),
|
||||
...model,
|
||||
invocation: publicById.get(getModelId(model).toLowerCase())?.invocation,
|
||||
})) as Vip98kCatalogModel[];
|
||||
}
|
||||
|
||||
function sanitizeError(error: unknown) {
|
||||
@ -304,14 +345,16 @@ async function runExternalTask(params: {
|
||||
apiKey: string;
|
||||
modelId: string;
|
||||
capability: Capability;
|
||||
invocation: Vip98kInvocation;
|
||||
}) {
|
||||
const imageUrl = "https://dummyimage.com/512x512/2563eb/ffffff.png&text=VIP98K";
|
||||
const secondImageUrl = "https://dummyimage.com/512x512/dc2626/ffffff.png&text=REFERENCE";
|
||||
const body: Record<string, unknown> = {
|
||||
model: params.modelId,
|
||||
displayName: `provider-smoke-${params.capability}`,
|
||||
prompt: "Create a minimal test result with one simple object",
|
||||
};
|
||||
const body: Record<string, unknown> = JSON.parse(
|
||||
JSON.stringify(params.invocation.request_example || {}),
|
||||
);
|
||||
if ("model" in body) body.model = params.modelId;
|
||||
if ("displayName" in body) body.displayName = `provider-smoke-${params.capability}`;
|
||||
if ("prompt" in body) body.prompt = "Create a minimal test result with one simple object";
|
||||
if (
|
||||
["multi-image-edit", "rmbg", "enhance", "extend", "image-to-3d"].includes(
|
||||
params.capability,
|
||||
@ -319,40 +362,62 @@ async function runExternalTask(params: {
|
||||
) {
|
||||
const images =
|
||||
params.capability === "multi-image-edit" ? [imageUrl, secondImageUrl] : [imageUrl];
|
||||
Object.assign(body, {
|
||||
image: imageUrl,
|
||||
imageUrl,
|
||||
image_url: imageUrl,
|
||||
images,
|
||||
imageUrls: images,
|
||||
referenceImages: images,
|
||||
reference_images: images,
|
||||
});
|
||||
if ("image" in body) body.image = imageUrl;
|
||||
if ("imageUrl" in body) body.imageUrl = imageUrl;
|
||||
if ("image_url" in body) body.image_url = imageUrl;
|
||||
if ("images" in body) body.images = images;
|
||||
if ("imageUrls" in body) body.imageUrls = images;
|
||||
if ("referenceImages" in body) body.referenceImages = images;
|
||||
if ("reference_images" in body) body.reference_images = images;
|
||||
if (Array.isArray(body.video_clips)) {
|
||||
const template =
|
||||
body.video_clips[0] && typeof body.video_clips[0] === "object"
|
||||
? (body.video_clips[0] as Record<string, unknown>)
|
||||
: {};
|
||||
body.video_clips = images.map((url) => ({ ...template, url }));
|
||||
}
|
||||
if (
|
||||
params.capability === "multi-image-edit" &&
|
||||
!Array.isArray(body.images) &&
|
||||
!Array.isArray(body.referenceImages) &&
|
||||
!Array.isArray(body.reference_images) &&
|
||||
!Array.isArray(body.video_clips)
|
||||
) {
|
||||
throw new Error(`${params.modelId} official invocation does not expose multi-image input`);
|
||||
}
|
||||
}
|
||||
if (params.capability === "extend") {
|
||||
Object.assign(body, { aspectRatio: "16:9", aspect_ratio: "16:9", ratio: "16:9" });
|
||||
if ("aspectRatio" in body) body.aspectRatio = "16:9";
|
||||
if ("aspect_ratio" in body) body.aspect_ratio = "16:9";
|
||||
if ("ratio" in body) body.ratio = "16:9";
|
||||
}
|
||||
if (params.capability === "video") {
|
||||
Object.assign(body, { mode: "text-to-video", duration: 4, resolution: "480p", ratio: "16:9" });
|
||||
}
|
||||
if (params.capability === "world") body.mode = "world-generation";
|
||||
if (params.capability === "world" && "mode" in body) body.mode = "world-generation";
|
||||
|
||||
const path = String(params.invocation.path || "").trim();
|
||||
if (!path.startsWith("/")) throw new Error(`${params.modelId} has no valid invocation path`);
|
||||
|
||||
const submitted = await parseResponse(
|
||||
await fetch(`${params.baseUrl}/api/external/task/generate`, {
|
||||
method: "POST",
|
||||
await fetch(`${params.baseUrl}${path}`, {
|
||||
method: String(params.invocation.method || "POST").toUpperCase(),
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": params.invocation.content_type || "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(180_000),
|
||||
}),
|
||||
);
|
||||
const immediate = Array.from(collectUrls(submitted));
|
||||
const inputUrls = new Set([imageUrl, secondImageUrl]);
|
||||
const immediate = Array.from(collectUrls(submitted)).filter((url) => !inputUrls.has(url));
|
||||
if (immediate.length > 0) return immediate;
|
||||
const taskId = extractTaskId(submitted);
|
||||
if (!taskId) throw new Error("External task returned neither output nor task ID");
|
||||
const resultPath = extractResultPath(submitted);
|
||||
const resultPath =
|
||||
extractResultPath(submitted) ||
|
||||
String(params.invocation.result_path || "").replaceAll(
|
||||
"{task_id}",
|
||||
encodeURIComponent(taskId),
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < 1_200_000) {
|
||||
await new Promise((resolvePromise) => setTimeout(resolvePromise, 5000));
|
||||
@ -368,7 +433,7 @@ async function runExternalTask(params: {
|
||||
}),
|
||||
);
|
||||
const status = extractStatus(payload);
|
||||
const outputs = Array.from(collectUrls(payload));
|
||||
const outputs = Array.from(collectUrls(payload)).filter((url) => !inputUrls.has(url));
|
||||
if (["failed", "error", "timeout", "canceled", "cancelled"].includes(status)) {
|
||||
throw new Error(`External task failed with status ${status}`);
|
||||
}
|
||||
@ -400,6 +465,13 @@ async function main() {
|
||||
if (!modelId || !availableModelIds.has(modelId)) {
|
||||
throw new Error(`No catalog model is available for ${capability}; pass --model=<catalog-id>`);
|
||||
}
|
||||
const selectedModel = catalog.find((model) => getModelId(model) === modelId);
|
||||
if (
|
||||
!["chat", "multimodal", "image"].includes(capability) &&
|
||||
!selectedModel?.invocation?.path
|
||||
) {
|
||||
throw new Error(`${modelId} is missing its official invocation contract`);
|
||||
}
|
||||
|
||||
const ledgerPath = resolve(
|
||||
getArgValue("--ledger") ||
|
||||
@ -445,7 +517,13 @@ async function main() {
|
||||
multiImage: false,
|
||||
});
|
||||
} else {
|
||||
outputs = await runExternalTask({ baseUrl, apiKey, modelId, capability });
|
||||
outputs = await runExternalTask({
|
||||
baseUrl,
|
||||
apiKey,
|
||||
modelId,
|
||||
capability,
|
||||
invocation: selectedModel!.invocation!,
|
||||
});
|
||||
}
|
||||
const finishedAt = new Date();
|
||||
const entry: LedgerEntry = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user