fix: reconcile VIP98K media catalog
This commit is contained in:
parent
712d4350a0
commit
f3f1fdafbd
@ -7691,7 +7691,7 @@ export function AdminSettingsClient() {
|
||||
if (
|
||||
!options?.skipConfirm &&
|
||||
!confirm(
|
||||
"将从 VIP98K 模型目录同步全部模型,并按图片、视频、3D 世界、文本、图片工具、OCR、音频工具分类。图片模型会配置比例、1K/2K/4K 和多参考图能力。\n\n继续吗?"
|
||||
"将同步 VIP98K 当前可用目录,停用服务商已关闭的模型,并排除纯文生图和纯文生视频模型。图片编辑、图生视频、参考媒体、多模态、3D 与工具模型会保留;你此前手动删除的模型不会恢复。\n\n继续吗?"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@ -7710,8 +7710,9 @@ export function AdminSettingsClient() {
|
||||
const totalSynced =
|
||||
Number(data.addedCount || 0) + Number(data.updatedCount || 0);
|
||||
const totalCatalog = Number(data.counts?.total || totalSynced);
|
||||
const totalDisabled = Number(data.disabledCount || 0);
|
||||
toast.success(
|
||||
`已同步 ${totalSynced} 个 VIP98K 模型(目录共 ${totalCatalog} 个)`
|
||||
`已同步 ${totalSynced} 个 VIP98K 模型,停用 ${totalDisabled} 个旧模型(可用目录 ${totalCatalog} 个)`
|
||||
);
|
||||
mutateModels();
|
||||
if (data.errors?.length) {
|
||||
|
||||
@ -75,6 +75,7 @@ export async function POST(request: Request) {
|
||||
body.modelIds.map((item: unknown) => String(item || "").trim()).filter(Boolean),
|
||||
)
|
||||
: null;
|
||||
const preserveDeletedModels = body?.preserveDeletedModels !== false;
|
||||
if (!providerId) {
|
||||
return NextResponse.json({ error: "Provider ID is required" }, { status: 400 });
|
||||
}
|
||||
@ -91,14 +92,38 @@ export async function POST(request: Request) {
|
||||
baseURL: provider.baseUrl,
|
||||
apiKey: provider.apiKey,
|
||||
});
|
||||
const existingModels = await db.query.aiModel.findMany({
|
||||
where: eq(aiModel.providerId, providerId),
|
||||
});
|
||||
const existingModelIds = new Set(existingModels.map((model) => model.modelId));
|
||||
const preserveExistingCatalog =
|
||||
requestedModelIds === null &&
|
||||
preserveDeletedModels &&
|
||||
existingModels.length > 0;
|
||||
const presets = requestedModelIds
|
||||
? catalogPresets.filter((preset) => requestedModelIds.has(preset.modelId))
|
||||
: catalogPresets;
|
||||
: preserveExistingCatalog
|
||||
? catalogPresets.filter((preset) => existingModelIds.has(preset.modelId))
|
||||
: catalogPresets;
|
||||
|
||||
const addedModels: AiModel[] = [];
|
||||
const updatedModels: AiModel[] = [];
|
||||
const disabledModels: AiModel[] = [];
|
||||
const errors: Array<{ modelId: string; error: string }> = [];
|
||||
|
||||
if (requestedModelIds === null) {
|
||||
const catalogModelIds = new Set(catalogPresets.map((preset) => preset.modelId));
|
||||
for (const existing of existingModels) {
|
||||
if (catalogModelIds.has(existing.modelId)) continue;
|
||||
const [disabled] = await db
|
||||
.update(aiModel)
|
||||
.set({ isEnabled: false, isHidden: true, updatedAt: new Date() })
|
||||
.where(eq(aiModel.id, existing.id))
|
||||
.returning();
|
||||
if (disabled) disabledModels.push(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
for (const preset of presets) {
|
||||
try {
|
||||
const existing = await db.query.aiModel.findFirst({
|
||||
@ -113,7 +138,8 @@ export async function POST(request: Request) {
|
||||
description: preset.description,
|
||||
supportsVision: preset.supportsVision,
|
||||
supportsImageGen: preset.supportsImageGen,
|
||||
isEnabled: true,
|
||||
isEnabled:
|
||||
requestedModelIds !== null ? true : (existing?.isEnabled ?? true),
|
||||
isHidden: preset.isHidden,
|
||||
sortOrder: preset.sortOrder,
|
||||
capabilities: preset.capabilities,
|
||||
@ -161,8 +187,10 @@ export async function POST(request: Request) {
|
||||
success: true,
|
||||
addedCount: addedModels.length,
|
||||
updatedCount: updatedModels.length,
|
||||
disabledCount: disabledModels.length,
|
||||
addedModels,
|
||||
updatedModels,
|
||||
disabledModelIds: disabledModels.map((model) => model.modelId),
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
counts: {
|
||||
requested: presets.length,
|
||||
|
||||
@ -3,10 +3,17 @@ import "server-only";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import sharp from "sharp";
|
||||
import { createOpenAIImage } from "@/lib/ai/providers/openai-image";
|
||||
import {
|
||||
resolveVip98kTaskInvocation,
|
||||
type Vip98kTaskInvocation,
|
||||
} from "@/lib/ai/vip98k-task-invocation";
|
||||
import { VIP98K_BASE_URL, normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models";
|
||||
import { createLogger } from "@/lib/observability/logger";
|
||||
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
|
||||
const logger = createLogger("vip98k-provider");
|
||||
|
||||
interface Vip98kConfig {
|
||||
apiKey: string;
|
||||
baseURL?: string | null;
|
||||
@ -30,15 +37,7 @@ 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 Vip98kInvocation = Vip98kTaskInvocation;
|
||||
|
||||
type UploadedImage = {
|
||||
thumbnail: string;
|
||||
@ -491,14 +490,38 @@ export async function submitVip98kExternalTask(params: {
|
||||
imageParams: params.imageParams,
|
||||
modelConfig: params.modelConfig || {},
|
||||
};
|
||||
const invocation = getVip98kInvocation(taskParams.modelConfig);
|
||||
const invocation = resolveVip98kTaskInvocation(
|
||||
taskParams.modelId,
|
||||
getVip98kInvocation(taskParams.modelConfig)
|
||||
);
|
||||
const body = buildTaskBody(taskParams, invocation);
|
||||
const imageBindingKeys = [
|
||||
"images",
|
||||
"imageUrls",
|
||||
"referenceImages",
|
||||
"reference_images",
|
||||
"image",
|
||||
"imageUrl",
|
||||
"image_url",
|
||||
"video_clips",
|
||||
].filter((key) => key in body);
|
||||
logger.info("task submit audit", {
|
||||
modelId: taskParams.modelId,
|
||||
kind: taskParams.kind,
|
||||
invocationKind: invocation?.kind || "task",
|
||||
invocationPath: invocation?.path || "/api/external/task",
|
||||
imageCount: imageUrls.length,
|
||||
videoCount: videoUrls.length,
|
||||
imageBindingKeys,
|
||||
bodyKeys: Object.keys(body).sort(),
|
||||
});
|
||||
const submitted = await submitTask({
|
||||
apiKey: params.apiKey,
|
||||
baseURL: params.baseURL,
|
||||
path: invocation?.path,
|
||||
method: invocation?.method,
|
||||
contentType: invocation?.contentType,
|
||||
body: buildTaskBody(taskParams, invocation),
|
||||
body,
|
||||
});
|
||||
const resultPath = submitted.resultPath || invocation?.resultPath || "";
|
||||
return {
|
||||
@ -653,8 +676,12 @@ function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation |
|
||||
}
|
||||
|
||||
if (params.imageUrls.length > 0) {
|
||||
let hasImageBinding = false;
|
||||
const setImageField = (key: string, value: unknown) => {
|
||||
if (key in body || !invocation) body[key] = value;
|
||||
if (key in body || !invocation) {
|
||||
body[key] = value;
|
||||
hasImageBinding = true;
|
||||
}
|
||||
};
|
||||
setImageField("images", params.imageUrls);
|
||||
setImageField("imageUrls", params.imageUrls);
|
||||
@ -670,6 +697,7 @@ function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation |
|
||||
if (Array.isArray(body.video_clips)) {
|
||||
const template = toRecord(body.video_clips[0]) || {};
|
||||
body.video_clips = params.imageUrls.map((url) => ({ ...template, url }));
|
||||
hasImageBinding = true;
|
||||
}
|
||||
if (params.imageUrls[1]) {
|
||||
setImageField("lastFrameImage", params.imageUrls[1]);
|
||||
@ -677,6 +705,12 @@ function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation |
|
||||
setImageField("endImageUrl", params.imageUrls[1]);
|
||||
setImageField("end_image_url", params.imageUrls[1]);
|
||||
}
|
||||
if (
|
||||
!hasImageBinding &&
|
||||
(params.kind === "image" || params.kind === "image-tool")
|
||||
) {
|
||||
body.images = params.imageUrls;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.videoUrls.length > 0) {
|
||||
|
||||
@ -57,6 +57,11 @@ export interface Vip98kCatalogModel {
|
||||
pricing?: unknown;
|
||||
price?: unknown;
|
||||
status?: string;
|
||||
available?: boolean;
|
||||
isAvailable?: boolean;
|
||||
enabled?: boolean;
|
||||
isEnabled?: boolean;
|
||||
is_active?: boolean;
|
||||
invocation?: Vip98kInvocationContract;
|
||||
taskTypes?: unknown;
|
||||
task_types?: unknown;
|
||||
@ -300,6 +305,95 @@ function resolvePresetType(model: Vip98kCatalogModel): Vip98kCatalogModelType {
|
||||
return catalogType;
|
||||
}
|
||||
|
||||
const UNAVAILABLE_STATUS_TOKENS = [
|
||||
"disabled",
|
||||
"unavailable",
|
||||
"closed",
|
||||
"offline",
|
||||
"deprecated",
|
||||
"removed",
|
||||
"retired",
|
||||
"suspended",
|
||||
] as const;
|
||||
|
||||
function getInvocationRequestKeys(model: Vip98kCatalogModel) {
|
||||
const invocation = toRecord(model.invocation);
|
||||
return Object.keys(toRecord(invocation?.request_example) || {}).map((key) =>
|
||||
key.trim().toLowerCase().replace(/_/g, "-"),
|
||||
);
|
||||
}
|
||||
|
||||
function hasMediaReferenceInput(
|
||||
model: Vip98kCatalogModel,
|
||||
type: Vip98kCatalogModelType,
|
||||
) {
|
||||
const capabilities = getCapabilities(model);
|
||||
const inputModes = getInputModes(model).map((mode) => mode.replace(/_/g, "-"));
|
||||
const requestKeys = getInvocationRequestKeys(model);
|
||||
const modelId = getModelId(model).toLowerCase();
|
||||
const mediaInputModes = new Set([
|
||||
"image",
|
||||
"images",
|
||||
"url",
|
||||
"file",
|
||||
"video",
|
||||
"audio",
|
||||
"base64",
|
||||
]);
|
||||
const hasMediaCapability = capabilities.some((capability) => {
|
||||
if (capability.startsWith("text-to-")) return false;
|
||||
if (type === "image") {
|
||||
return capability.endsWith("-to-image") || capability.includes("image-edit");
|
||||
}
|
||||
return (
|
||||
capability.endsWith("-to-video") ||
|
||||
capability.startsWith("video-") ||
|
||||
capability.includes("video-edit")
|
||||
);
|
||||
});
|
||||
const hasMediaRequestField = requestKeys.some((key) =>
|
||||
/(?:image|video|audio|reference|first-?frame|last-?frame|file|url)/.test(key),
|
||||
);
|
||||
const hasMediaModelPath =
|
||||
type === "image"
|
||||
? /(?:image-to-image|reference-to-image|\/edit(?:$|[\/-]))/.test(modelId)
|
||||
: /(?:image-to-video|video-to-video|reference-to-video|first-frame|start-end|video-edit|video-extend)/.test(
|
||||
modelId,
|
||||
);
|
||||
|
||||
return (
|
||||
hasMediaCapability ||
|
||||
inputModes.some((mode) => mediaInputModes.has(mode)) ||
|
||||
hasMediaRequestField ||
|
||||
hasMediaModelPath
|
||||
);
|
||||
}
|
||||
|
||||
export function isVip98kCatalogModelAvailable(model: Vip98kCatalogModel) {
|
||||
const status = String(model.status || "").trim().toLowerCase();
|
||||
if (UNAVAILABLE_STATUS_TOKENS.some((token) => status.includes(token))) {
|
||||
return false;
|
||||
}
|
||||
return !(
|
||||
model.available === false ||
|
||||
model.isAvailable === false ||
|
||||
model.enabled === false ||
|
||||
model.isEnabled === false ||
|
||||
model.is_active === false
|
||||
);
|
||||
}
|
||||
|
||||
export function isVip98kCatalogModelEligible(model: Vip98kCatalogModel) {
|
||||
if (!getModelId(model) || !isVip98kCatalogModelAvailable(model)) return false;
|
||||
const type = resolvePresetType(model);
|
||||
if (type !== "image" && type !== "video") return true;
|
||||
return hasMediaReferenceInput(model, type);
|
||||
}
|
||||
|
||||
export function filterVip98kCatalogModels(models: Vip98kCatalogModel[]) {
|
||||
return models.filter(isVip98kCatalogModelEligible);
|
||||
}
|
||||
|
||||
function normalizeImageSize(value: string): string | null {
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (normalized === "1K" || normalized === "2K" || normalized === "4K") {
|
||||
@ -767,7 +861,7 @@ function createToolPreset(
|
||||
export function buildVip98kModelPresets(
|
||||
models: Vip98kCatalogModel[],
|
||||
): Vip98kModelPreset[] {
|
||||
return models
|
||||
return filterVip98kCatalogModels(models)
|
||||
.map((model, index) => {
|
||||
const modelId = getModelId(model);
|
||||
if (!modelId) return null;
|
||||
|
||||
67
lib/ai/vip98k-task-invocation.ts
Normal file
67
lib/ai/vip98k-task-invocation.ts
Normal file
@ -0,0 +1,67 @@
|
||||
export type Vip98kTaskInvocation = {
|
||||
kind: string;
|
||||
method: string;
|
||||
path: string;
|
||||
contentType: string;
|
||||
requestExample: Record<string, unknown>;
|
||||
resultMethod: string;
|
||||
resultPath: string;
|
||||
};
|
||||
|
||||
type KnownImageInput = "images" | "video_clips";
|
||||
|
||||
const KNOWN_IMAGE_TOOL_INPUTS: Record<string, KnownImageInput> = {
|
||||
"google/nano-banana-2/reference-to-image": "video_clips",
|
||||
"google/nano-banana-pro/edit": "images",
|
||||
"openai/gpt-image-2/edit": "images",
|
||||
};
|
||||
|
||||
function getInvocationToolId(invocation: Vip98kTaskInvocation | null) {
|
||||
if (!invocation || invocation.kind !== "tool") return null;
|
||||
const query = invocation.path.split("?", 2)[1] || "";
|
||||
return new URLSearchParams(query).get("tool_id");
|
||||
}
|
||||
|
||||
function createKnownImageToolInvocation(
|
||||
modelId: string,
|
||||
imageInput: KnownImageInput
|
||||
): Vip98kTaskInvocation {
|
||||
const encodedModelId = encodeURIComponent(modelId);
|
||||
const requestExample =
|
||||
imageInput === "video_clips"
|
||||
? {
|
||||
prompt: "",
|
||||
video_clips: [{ fps: 1, url: "", ends: 0, start: 0 }],
|
||||
}
|
||||
: { prompt: "", images: [""] };
|
||||
|
||||
return {
|
||||
kind: "tool",
|
||||
method: "POST",
|
||||
path: `/api/external/tools/generate?tool_id=${encodedModelId}`,
|
||||
contentType: "application/json",
|
||||
requestExample,
|
||||
resultMethod: "GET",
|
||||
resultPath: `/api/external/tools/result?tool_id=${encodedModelId}&taskId={task_id}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A frontend alias may resolve to a different VIP98K tool at runtime. In that
|
||||
* case the selected model's invocation cannot be reused because its endpoint
|
||||
* and reference-image field belong to the alias rather than the resolved tool.
|
||||
*/
|
||||
export function resolveVip98kTaskInvocation(
|
||||
modelId: string,
|
||||
configuredInvocation: Vip98kTaskInvocation | null
|
||||
) {
|
||||
const normalizedModelId = modelId.trim().toLowerCase();
|
||||
const knownImageInput = KNOWN_IMAGE_TOOL_INPUTS[normalizedModelId];
|
||||
if (!knownImageInput) return configuredInvocation;
|
||||
|
||||
if (getInvocationToolId(configuredInvocation)?.toLowerCase() === normalizedModelId) {
|
||||
return configuredInvocation;
|
||||
}
|
||||
|
||||
return createKnownImageToolInvocation(modelId, knownImageInput);
|
||||
}
|
||||
@ -46,12 +46,14 @@ export default defineConfig({
|
||||
/routes\/video-mark-prompt\.test\.ts/,
|
||||
/routes\/video-subtitle-erase-tools\.test\.ts/,
|
||||
/routes\/home-sidebar-auth\.test\.ts/,
|
||||
/routes\/model-classification\.test\.ts/,
|
||||
/routes\/openclaw-client-errors\.test\.ts/,
|
||||
/routes\/openclaw-config\.test\.ts/,
|
||||
/routes\/openclaw-durable-task-state\.test\.ts/,
|
||||
/routes\/openclaw-request-state\.test\.ts/,
|
||||
/routes\/openclaw-video-attachments\.test\.ts/,
|
||||
/routes\/openapi-access-key\.test\.ts/,
|
||||
/routes\/vip98k-reference-routing\.test\.ts/,
|
||||
],
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
@ -9,7 +9,11 @@ import {
|
||||
isImageGenerationModelDescriptor,
|
||||
isVideoModelDescriptor,
|
||||
} from "@/lib/ai/model-classification";
|
||||
import { buildVip98kModelPresets } from "@/lib/ai/vip98k-models";
|
||||
import {
|
||||
buildVip98kModelPresets,
|
||||
isVip98kCatalogModelEligible,
|
||||
isVip98kImageModelVisible,
|
||||
} from "@/lib/ai/vip98k-models";
|
||||
|
||||
const atlasHunyuan3D = {
|
||||
model: "tencent/hunyuan3d-pro/text-to-3d",
|
||||
@ -196,7 +200,8 @@ test.describe("AI model media classification", () => {
|
||||
modelId: "mislabelled-video",
|
||||
displayName: "Mislabelled video",
|
||||
type: "image",
|
||||
capabilities: ["text_to_video"],
|
||||
capabilities: ["text_to_video", "image_to_video"],
|
||||
inputModes: ["text", "image"],
|
||||
},
|
||||
{
|
||||
modelId: "enhance",
|
||||
@ -264,7 +269,7 @@ test.describe("AI model media classification", () => {
|
||||
});
|
||||
|
||||
test("only shows VIP98K image models verified as released in 2026 or later", () => {
|
||||
const presets = buildVip98kModelPresets([
|
||||
const models = [
|
||||
{
|
||||
modelId: "black-forest-labs/flux-dev",
|
||||
displayName: "Flux Dev",
|
||||
@ -291,13 +296,60 @@ test.describe("AI model media classification", () => {
|
||||
capabilities: ["text_to_image"],
|
||||
releasedAt: "2025-12-15",
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
expect(presets.find((item) => item.modelId === "black-forest-labs/flux-dev")?.isHidden).toBe(true);
|
||||
expect(isVip98kImageModelVisible(models[0])).toBe(false);
|
||||
expect(isVip98kImageModelVisible(models[1])).toBe(true);
|
||||
expect(isVip98kImageModelVisible(models[2])).toBe(true);
|
||||
expect(isVip98kImageModelVisible(models[3])).toBe(false);
|
||||
});
|
||||
|
||||
test("excludes pure text image/video models but keeps reference and 3D models", () => {
|
||||
expect(
|
||||
presets.find((item) => item.modelId === "google/nano-banana-2/text-to-image")?.isHidden,
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "openai/gpt-image-2/text-to-image",
|
||||
type: "image",
|
||||
capabilities: ["text-to-image"],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "google/veo/text-to-video",
|
||||
type: "video",
|
||||
capabilities: ["text-to-video"],
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "google/nano-banana-2/edit",
|
||||
type: "image",
|
||||
capabilities: ["text-to-image", "image-to-image"],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "bytedance/seedance/image-to-video",
|
||||
type: "video",
|
||||
capabilities: ["text-to-video", "image-to-video"],
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "tencent/hunyuan3d-pro/text-to-3d",
|
||||
type: "image",
|
||||
capabilities: ["text-to-3d"],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("excludes catalog models explicitly marked unavailable", () => {
|
||||
expect(
|
||||
isVip98kCatalogModelEligible({
|
||||
modelId: "google/nano-banana-2/edit",
|
||||
type: "image",
|
||||
capabilities: ["image-to-image"],
|
||||
status: "closed",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(presets.find((item) => item.modelId === "future-image-with-date")?.isHidden).toBe(false);
|
||||
expect(presets.find((item) => item.modelId === "old-image-with-date")?.isHidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
62
tests/routes/vip98k-reference-routing.test.ts
Normal file
62
tests/routes/vip98k-reference-routing.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
resolveVip98kTaskInvocation,
|
||||
type Vip98kTaskInvocation,
|
||||
} from "@/lib/ai/vip98k-task-invocation";
|
||||
|
||||
const textToImageInvocation: Vip98kTaskInvocation = {
|
||||
kind: "tool",
|
||||
method: "POST",
|
||||
path: "/api/external/tools/generate?tool_id=google%2Fnano-banana-2%2Ftext-to-image",
|
||||
contentType: "application/json",
|
||||
requestExample: { prompt: "Generate an image" },
|
||||
resultMethod: "GET",
|
||||
resultPath:
|
||||
"/api/external/tools/result?tool_id=google%2Fnano-banana-2%2Ftext-to-image&taskId={task_id}",
|
||||
};
|
||||
|
||||
test.describe("VIP98K reference-image routing", () => {
|
||||
test("replaces a text-to-image alias invocation with the resolved reference tool", () => {
|
||||
const invocation = resolveVip98kTaskInvocation(
|
||||
"google/nano-banana-2/reference-to-image",
|
||||
textToImageInvocation
|
||||
);
|
||||
|
||||
expect(invocation?.path).toContain(
|
||||
"tool_id=google%2Fnano-banana-2%2Freference-to-image"
|
||||
);
|
||||
expect(invocation?.resultPath).toContain(
|
||||
"tool_id=google%2Fnano-banana-2%2Freference-to-image"
|
||||
);
|
||||
expect(invocation?.requestExample).toMatchObject({
|
||||
prompt: "",
|
||||
video_clips: [{ fps: 1, url: "", ends: 0, start: 0 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the catalog invocation when it already targets the resolved tool", () => {
|
||||
const catalogInvocation: Vip98kTaskInvocation = {
|
||||
...textToImageInvocation,
|
||||
path: "/api/external/tools/generate?tool_id=google%2Fnano-banana-2%2Freference-to-image",
|
||||
resultPath:
|
||||
"/api/external/tools/result?tool_id=google%2Fnano-banana-2%2Freference-to-image&taskId={task_id}",
|
||||
requestExample: {
|
||||
prompt: "Catalog prompt",
|
||||
video_clips: [{ fps: 2, url: "catalog", ends: 1, start: 0 }],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveVip98kTaskInvocation(
|
||||
"google/nano-banana-2/reference-to-image",
|
||||
catalogInvocation
|
||||
)
|
||||
).toBe(catalogInvocation);
|
||||
});
|
||||
|
||||
test("does not rewrite unrelated tools", () => {
|
||||
expect(
|
||||
resolveVip98kTaskInvocation("other/image-model", textToImageInvocation)
|
||||
).toBe(textToImageInvocation);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user