diff --git a/scripts/sync-vip98k-nano-banana-21.ts b/scripts/sync-vip98k-nano-banana-21.ts new file mode 100644 index 000000000..af28c28ec --- /dev/null +++ b/scripts/sync-vip98k-nano-banana-21.ts @@ -0,0 +1,136 @@ +import dotenv from "dotenv"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import postgres from "postgres"; +import { + buildVip98kModelPresets, + fetchVip98kCatalog, +} from "../lib/ai/vip98k-models"; + +const MODEL_ID = "nano-banana-2.1"; +dotenv.config({ path: ".env.local", quiet: true }); + +async function main() { + if (!process.env.POSTGRES_URL) throw new Error("POSTGRES_URL is required"); + const sql = postgres(process.env.POSTGRES_URL, { max: 1, prepare: false }); + try { + const providers = await sql` + select id, "baseUrl", "apiKey" from "AiProvider" + where type = 'vip98k' and "isEnabled" = true + `; + const candidates = providers.filter( + (provider) => new URL(provider.baseUrl).hostname === "api.vip98k.com", + ); + if (candidates.length !== 1) { + throw new Error("Expected exactly one enabled api.vip98k.com provider"); + } + const provider = candidates[0]!; + const catalog = await fetchVip98kCatalog({ + baseURL: provider.baseUrl, + apiKey: provider.apiKey, + }); + const model = catalog.find( + (item) => (item.modelId || item.id || item.name) === MODEL_ID, + ); + if (!model) throw new Error("Model is absent from the authorized catalog"); + const preset = buildVip98kModelPresets([model])[0]; + if (!preset || preset.isHidden || !preset.supportsVision) { + throw new Error("Published model does not qualify for reference-image generation"); + } + + // Use the published asynchronous contract for both text and reference input. + // This preserves native resolution and aspect ratio instead of prompt hints. + const metadata = preset.capabilities.vip98k as { + invocation: Record; + invocations: Array<{ + kind: string; + path: string; + requestExample: Record; + inputSchema?: { properties?: Record }; + }>; + }; + const invocation = metadata.invocations.find( + (item) => item.kind === "task" && item.path === "/extensions/v1/tasks", + ); + const fields = invocation?.inputSchema?.properties; + if ( + !invocation || !fields?.file_urls || !fields.aspect_ratio || + !["1K", "2K", "4K"].every((size) => fields.resolution?.enum?.includes(size)) + ) { + throw new Error("Expected asynchronous multi-reference and 1K/2K/4K contract"); + } + const taskMetadata = { ...metadata, invocation, invocations: [invocation] }; + const capabilities = { ...preset.capabilities, vip98k: taskMetadata }; + const imageGenConfig = { + ...preset.imageGenConfig, + supportsQuality: false, + vip98k: taskMetadata, + }; + const { quality: _quality, ...defaultParams } = preset.defaultParams; + const existing = await sql` + select * from "AiModel" + where "providerId" = ${provider.id} and "modelId" = ${MODEL_ID} + `; + if (existing.length > 1) throw new Error("Duplicate model rows require reconciliation"); + const summary = { + modelId: MODEL_ID, + providerId: provider.id, + apply: process.argv.includes("--apply"), + exists: existing.length === 1, + sizes: capabilities.supportedImageSizes, + supportsReferenceImages: preset.supportsVision, + invocationPath: invocation.path, + }; + if (!summary.apply) return console.log(JSON.stringify(summary, null, 2)); + + const backupIndex = process.argv.indexOf("--backup-dir"); + if (backupIndex < 0 || !process.argv[backupIndex + 1]) { + throw new Error("--backup-dir is required when applying configuration"); + } + const backupDir = process.argv[backupIndex + 1]!; + mkdirSync(backupDir, { recursive: true, mode: 0o700 }); + writeFileSync(join(backupDir, `model-before-${Date.now()}.json`), JSON.stringify(existing), { mode: 0o600 }); + const asJson = (value: unknown) => sql.json(JSON.parse(JSON.stringify(value))); + const id = await sql.begin(async (tx) => { + await tx`select pg_advisory_xact_lock(hashtext(${provider.id + ":" + MODEL_ID}))`; + const rows = await tx` + select id from "AiModel" + where "providerId" = ${provider.id} and "modelId" = ${MODEL_ID} + for update + `; + if (rows.length > 1) throw new Error("Duplicate model rows"); + if (!rows.length) { + const [created] = await tx` + insert into "AiModel" ( + "providerId", "modelId", "displayName", description, + "supportsVision", "supportsImageGen", "isEnabled", "isHidden", + "isDefault", "sortOrder", capabilities, "defaultParams", "imageGenConfig" + ) values ( + ${provider.id}, ${MODEL_ID}, ${preset.displayName}, ${preset.description}, + true, true, true, false, false, 106, + ${asJson(capabilities)}, ${asJson(defaultParams)}, ${asJson(imageGenConfig)} + ) returning id + `; + return created!.id; + } + await tx` + update "AiModel" set + "displayName" = ${preset.displayName}, description = ${preset.description}, + "supportsVision" = true, "supportsImageGen" = true, + "isEnabled" = true, "isHidden" = false, + capabilities = ${asJson(capabilities)}, "defaultParams" = ${asJson(defaultParams)}, + "imageGenConfig" = ${asJson(imageGenConfig)}, "updatedAt" = now() + where id = ${rows[0]!.id} + `; + return rows[0]!.id; + }); + console.log(JSON.stringify({ ...summary, id, enabled: true, visible: true }, null, 2)); + } finally { + await sql.end(); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : "Model synchronization failed"); + process.exitCode = 1; +});