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" }); 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: preset.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; });