137 lines
5.5 KiB
TypeScript
137 lines
5.5 KiB
TypeScript
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<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: 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;
|
|
});
|