272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import dotenv from "dotenv";
|
|
import postgres from "postgres";
|
|
import {
|
|
fetchVip98kModelPresets,
|
|
type Vip98kModelPreset,
|
|
} from "../lib/ai/vip98k-models";
|
|
|
|
dotenv.config({ path: ".env.local" });
|
|
dotenv.config();
|
|
|
|
const ALLOWED_IMAGE_PROVIDER_TYPES = ["alibaba-image", "vip98k"] as const;
|
|
|
|
function hasFlag(name: string) {
|
|
return process.argv.includes(name);
|
|
}
|
|
|
|
function toJsonValue(value: unknown): postgres.JSONValue {
|
|
return JSON.parse(JSON.stringify(value)) as postgres.JSONValue;
|
|
}
|
|
|
|
function isQualifiedVip98kImagePreset(preset: Vip98kModelPreset) {
|
|
return preset.supportsImageGen && !preset.isHidden;
|
|
}
|
|
|
|
async function main() {
|
|
const apply = hasFlag("--apply");
|
|
const databaseUrl = String(process.env.POSTGRES_URL || "").trim();
|
|
if (!databaseUrl) throw new Error("POSTGRES_URL is required");
|
|
|
|
const sql = postgres(databaseUrl, { prepare: false, max: 1 });
|
|
try {
|
|
const providers = await sql<
|
|
Array<{
|
|
id: string;
|
|
type: string;
|
|
name: string;
|
|
apiKey: string | null;
|
|
baseUrl: string | null;
|
|
isEnabled: boolean;
|
|
}>
|
|
>`select id, type, name, "apiKey", "baseUrl", "isEnabled" from "AiProvider" order by "createdAt"`;
|
|
const vip98kProviders = providers.filter((provider) => provider.type === "vip98k");
|
|
const alibabaProviders = providers.filter(
|
|
(provider) => provider.type === "alibaba-image",
|
|
);
|
|
if (vip98kProviders.length !== 1) {
|
|
throw new Error(`Expected one VIP98K provider, found ${vip98kProviders.length}`);
|
|
}
|
|
if (alibabaProviders.length === 0) {
|
|
throw new Error("Alibaba official image provider is missing");
|
|
}
|
|
const vip98kProvider = vip98kProviders[0];
|
|
if (!vip98kProvider) throw new Error("VIP98K provider is missing");
|
|
const vip98kApiKey = String(
|
|
process.env.VIP98K_API_KEY || vip98kProvider.apiKey || "",
|
|
).trim();
|
|
if (!vip98kApiKey) throw new Error("VIP98K API key is not configured");
|
|
const vip98kBaseUrl = String(
|
|
process.env.VIP98K_BASE_URL ||
|
|
vip98kProvider.baseUrl ||
|
|
"https://api.vip98k.com",
|
|
).trim();
|
|
const presets = await fetchVip98kModelPresets({
|
|
baseURL: vip98kBaseUrl,
|
|
apiKey: vip98kApiKey,
|
|
});
|
|
const qualifiedVip98kPresets = presets.filter(isQualifiedVip98kImagePreset);
|
|
if (qualifiedVip98kPresets.length === 0) {
|
|
throw new Error("VIP98K catalog contains no qualified image model");
|
|
}
|
|
const existingVip98kModels = await sql<
|
|
Array<{
|
|
id: string;
|
|
modelId: string;
|
|
}>
|
|
>`
|
|
select id, "modelId"
|
|
from "AiModel"
|
|
where "providerId" = ${vip98kProvider.id}::uuid
|
|
`;
|
|
const existingVip98kByModelId = new Map(
|
|
existingVip98kModels.map((model) => [model.modelId, model]),
|
|
);
|
|
|
|
const disallowedImageModels = await sql<
|
|
Array<{ id: string; providerType: string; modelId: string }>
|
|
>`
|
|
select m.id, p.type as "providerType", m."modelId"
|
|
from "AiModel" m join "AiProvider" p on p.id = m."providerId"
|
|
where m."supportsImageGen"
|
|
and p.type != all(${ALLOWED_IMAGE_PROVIDER_TYPES}::text[])
|
|
order by p.type, m."modelId"
|
|
`;
|
|
const staleVip98kImageModels = await sql<Array<{ id: string; modelId: string }>>`
|
|
select m.id, m."modelId"
|
|
from "AiModel" m
|
|
where m."providerId" = ${vip98kProvider.id}::uuid
|
|
and m."supportsImageGen"
|
|
and m."modelId" != all(${qualifiedVip98kPresets.map((preset) => preset.modelId)}::text[])
|
|
order by m."modelId"
|
|
`;
|
|
const imageOnlyProviders = await sql<Array<{ id: string; type: string; name: string }>>`
|
|
select p.id, p.type, p.name
|
|
from "AiProvider" p
|
|
where p.type != all(${ALLOWED_IMAGE_PROVIDER_TYPES}::text[])
|
|
and (
|
|
p.type like '%-image'
|
|
or p.type in ('bananaimage', 'gemini', 'huanke-gemini')
|
|
or (
|
|
exists (select 1 from "AiModel" m where m."providerId" = p.id and m."supportsImageGen")
|
|
and not exists (
|
|
select 1 from "AiModel" m
|
|
where m."providerId" = p.id and not m."supportsImageGen" and m."isEnabled"
|
|
)
|
|
)
|
|
)
|
|
order by p.type, p.name
|
|
`;
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
mode: apply ? "apply" : "dry-run",
|
|
allowedImageProviderTypes: ALLOWED_IMAGE_PROVIDER_TYPES,
|
|
qualifiedVip98kModels: qualifiedVip98kPresets.map((preset) => preset.modelId),
|
|
protectedVip98kModels: qualifiedVip98kPresets.map(
|
|
(preset) => preset.modelId,
|
|
),
|
|
disallowedImageModelsToDisable: disallowedImageModels.length,
|
|
staleVip98kImageModelsToDisable: staleVip98kImageModels.length,
|
|
imageOnlyProvidersToDisable: imageOnlyProviders.map((provider) => ({
|
|
type: provider.type,
|
|
name: provider.name,
|
|
})),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
if (!apply) return;
|
|
|
|
await sql.begin(async (tx) => {
|
|
await tx`
|
|
update "AiProvider"
|
|
set "baseUrl" = 'https://api.vip98k.com', "apiKey" = ${vip98kApiKey},
|
|
"isEnabled" = true, "updatedAt" = now()
|
|
where id = ${vip98kProvider.id}::uuid
|
|
`;
|
|
await tx`
|
|
update "AiProvider" set "isEnabled" = true, "updatedAt" = now()
|
|
where type = 'alibaba-image'
|
|
`;
|
|
await tx`
|
|
update "AiModel" m set
|
|
"isEnabled" = true, "excludeFromHealthBan" = true,
|
|
"healthStatus" = 'healthy', "healthConsecutiveFailures" = 0,
|
|
"healthLastEventType" = null, "healthLastError" = null,
|
|
"healthAutoDisabledAt" = null, "healthAutoDisabledReason" = null,
|
|
"healthRecoverySuccessStreak" = 0,
|
|
"healthObservationStartedAt" = null,
|
|
"healthUpdatedAt" = now(), "updatedAt" = now()
|
|
from "AiProvider" p
|
|
where p.id = m."providerId" and p.type = 'alibaba-image'
|
|
and m."supportsImageGen" and not m."isHidden"
|
|
`;
|
|
|
|
for (const preset of qualifiedVip98kPresets) {
|
|
const capabilities = tx.json(toJsonValue(preset.capabilities));
|
|
const defaultParams = tx.json(toJsonValue(preset.defaultParams));
|
|
const imageGenConfig = preset.imageGenConfig
|
|
? tx.json(toJsonValue(preset.imageGenConfig))
|
|
: null;
|
|
const existing = existingVip98kByModelId.get(preset.modelId);
|
|
if (existing) {
|
|
await tx`
|
|
update "AiModel" set
|
|
"displayName" = ${preset.displayName}, description = ${preset.description},
|
|
"supportsVision" = ${preset.supportsVision}, "supportsImageGen" = true,
|
|
"isEnabled" = true, "isHidden" = false,
|
|
"excludeFromHealthBan" = true, "healthStatus" = 'healthy',
|
|
"healthConsecutiveFailures" = 0, "healthLastEventType" = null,
|
|
"healthLastError" = null, "healthAutoDisabledAt" = null,
|
|
"healthAutoDisabledReason" = null, "healthRecoverySuccessStreak" = 0,
|
|
"healthObservationStartedAt" = null, "healthUpdatedAt" = now(),
|
|
"sortOrder" = ${preset.sortOrder},
|
|
capabilities = ${capabilities}, "defaultParams" = ${defaultParams},
|
|
"imageGenConfig" = ${imageGenConfig}, "fallbackModelId1" = null,
|
|
"fallbackModelId2" = null, "updatedAt" = now()
|
|
where id = ${existing.id}::uuid
|
|
`;
|
|
} else {
|
|
await tx`
|
|
insert into "AiModel" (
|
|
"providerId", "modelId", "displayName", description, "supportsVision",
|
|
"supportsImageGen", "isEnabled", "isHidden", "sortOrder", capabilities,
|
|
"defaultParams", "imageGenConfig", "isDefault", "isDefaultTextEditModel",
|
|
"excludeFromHealthBan", "healthStatus"
|
|
) values (
|
|
${vip98kProvider.id}::uuid, ${preset.modelId}, ${preset.displayName},
|
|
${preset.description}, ${preset.supportsVision}, true, true, false,
|
|
${preset.sortOrder}, ${capabilities}, ${defaultParams}, ${imageGenConfig},
|
|
false, false, true, 'healthy'
|
|
)
|
|
`;
|
|
}
|
|
}
|
|
|
|
await tx`
|
|
update "AiModel" m set
|
|
"isEnabled" = false, "isHidden" = true, "isDefault" = false,
|
|
"isDefaultTextEditModel" = false, "updatedAt" = now()
|
|
from "AiProvider" p
|
|
where p.id = m."providerId" and m."supportsImageGen"
|
|
and p.type != all(${ALLOWED_IMAGE_PROVIDER_TYPES}::text[])
|
|
`;
|
|
await tx`
|
|
update "AiModel" set
|
|
"isEnabled" = false, "isHidden" = true, "isDefault" = false,
|
|
"isDefaultTextEditModel" = false, "updatedAt" = now()
|
|
where "providerId" = ${vip98kProvider.id}::uuid and "supportsImageGen"
|
|
and "modelId" != all(${qualifiedVip98kPresets.map((preset) => preset.modelId)}::text[])
|
|
`;
|
|
if (imageOnlyProviders.length > 0) {
|
|
await tx`
|
|
update "AiProvider" set "isEnabled" = false, "updatedAt" = now()
|
|
where id = any(${imageOnlyProviders.map((provider) => provider.id)}::uuid[])
|
|
`;
|
|
}
|
|
});
|
|
|
|
const invalidModels = await sql<
|
|
Array<{ providerType: string; modelId: string }>
|
|
>`
|
|
select p.type as "providerType", m."modelId"
|
|
from "AiModel" m join "AiProvider" p on p.id = m."providerId"
|
|
where m."supportsImageGen" and m."isEnabled"
|
|
and p.type != all(${ALLOWED_IMAGE_PROVIDER_TYPES}::text[])
|
|
`;
|
|
const activeVip98kModels = await sql<Array<{ modelId: string }>>`
|
|
select m."modelId"
|
|
from "AiModel" m join "AiProvider" p on p.id = m."providerId"
|
|
where p.type = 'vip98k' and p."isEnabled" and m."supportsImageGen"
|
|
and m."isEnabled" and not m."isHidden"
|
|
order by m."modelId"
|
|
`;
|
|
if (invalidModels.length > 0) {
|
|
throw new Error("Disallowed image models remain enabled after apply");
|
|
}
|
|
if (activeVip98kModels.length !== qualifiedVip98kPresets.length) {
|
|
throw new Error("VIP98K active image model count does not match qualified catalog");
|
|
}
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
success: true,
|
|
activeVip98kModels: activeVip98kModels.map((model) => model.modelId),
|
|
disabledImageModelCount: disallowedImageModels.length + staleVip98kImageModels.length,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
} finally {
|
|
await sql.end();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|