feat(models): add targeted VIP98K nano-banana-2.1 synchronization

This commit is contained in:
134 2026-10-12 16:53:42 +08:00
parent c0cbc34911
commit 3750e89ed1

View File

@ -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<string, unknown>;
invocations: Array<{
kind: string;
path: string;
requestExample: Record<string, unknown>;
inputSchema?: { properties?: Record<string, { enum?: string[] }> };
}>;
};
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;
});