perf: cache vip98k reference staging

This commit is contained in:
134 2026-09-07 20:48:55 +08:00
parent 5d93e03158
commit d10068874e
5 changed files with 481 additions and 36 deletions

View File

@ -1,10 +1,14 @@
import "server-only";
import { randomUUID } from "node:crypto";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import {
GetObjectCommand,
HeadObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
import { createOpenAI } from "@ai-sdk/openai";
import sharp from "sharp";
import { getDefaultStorageConfig } from "@/lib/db/queries";
import { getDefaultStorageConfig, getSystemSetting } from "@/lib/db/queries";
import {
bindVip98kImages,
resolveVip98kImageTaskSize,
@ -13,12 +17,22 @@ import {
} from "@/lib/ai/vip98k-task-invocation";
import { VIP98K_BASE_URL, normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models";
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
import {
defaultVip98kRuntimeConfig,
normalizeVip98kRuntimeConfig,
resolveVip98kSubmissionTimeoutMs,
VIP98K_RUNTIME_CONFIG_KEY,
type Vip98kRuntimeConfig,
} from "@/lib/ai/vip98k-runtime-config";
import {
addVip98kReferenceRetryToken,
getStorageObjectKeyFromPublicUrl,
getVip98kReferenceStagingObjectKey,
getVip98kStagedMediaRetryDelay,
getVip98kSubmissionRetryDelay,
isVip98kStagedReferenceUrl,
shouldStageVip98kReferenceUrl,
VIP98K_REFERENCE_STAGING_KEY_PREFIX,
} from "@/lib/ai/vip98k-reference-staging";
import { createLogger } from "@/lib/observability/logger";
import { createDeferredUploadedImage } from "@/lib/storage/image-archive";
@ -71,10 +85,10 @@ const DEFAULT_POLL_INTERVAL_SECONDS = 8;
const DEFAULT_MAX_POLL_SECONDS = 600;
const REFERENCE_DOWNLOAD_ATTEMPTS = 3;
const REFERENCE_DOWNLOAD_TIMEOUT_MS = 120_000;
const REFERENCE_CACHE_LOOKUP_TIMEOUT_MS = 15_000;
const MAX_REFERENCE_DOWNLOAD_BYTES = IMAGE_UPLOAD_MAX_BYTES;
const REFERENCE_STAGING_CACHE_MS = 60_000;
const REFERENCE_STAGING_KEY_PREFIX = "uploads/vip98k-";
const MAX_SUBMISSION_ATTEMPTS = 3;
const RUNTIME_CONFIG_CACHE_MS = 60_000;
type ReferenceStagingConfig = {
sourcePublicUrl: string;
@ -91,7 +105,14 @@ class Vip98kReferenceTooLargeError extends Error {
let referenceStagingConfigCache:
| { expiresAt: number; value: ReferenceStagingConfig | null }
| undefined;
let runtimeConfigCache:
| { expiresAt: number; value: Vip98kRuntimeConfig }
| undefined;
const referenceStagingTasks = new Map<string, Promise<string>>();
const referenceStagingResultCache = new Map<
string,
{ cachedAt: number; url: string }
>();
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@ -522,6 +543,28 @@ async function resolveReferenceStagingConfig() {
return value;
}
async function getVip98kRuntimeConfig() {
const now = Date.now();
if (runtimeConfigCache?.expiresAt && runtimeConfigCache.expiresAt > now) {
return runtimeConfigCache.value;
}
let value = defaultVip98kRuntimeConfig;
try {
const setting = await getSystemSetting({ key: VIP98K_RUNTIME_CONFIG_KEY });
value = normalizeVip98kRuntimeConfig(setting?.value);
} catch (error) {
logger.warn("runtime config lookup failed; using defaults", {
message: error instanceof Error ? error.message : String(error),
});
}
runtimeConfigCache = {
expiresAt: now + RUNTIME_CONFIG_CACHE_MS,
value,
};
return value;
}
function getUrlHost(value: string) {
try {
return new URL(value).host;
@ -530,11 +573,96 @@ function getUrlHost(value: string) {
}
}
function isStagedReferenceUrl(value: string) {
function getReferenceStagingCacheKey(
mediaUrl: string,
stagingConfig: ReferenceStagingConfig,
) {
return [
stagingConfig.storageConfig.endpoint,
stagingConfig.storageConfig.bucket,
mediaUrl,
].join("|");
}
function getCachedStagedReference(
cacheKey: string,
runtimeConfig: Vip98kRuntimeConfig,
) {
const cached = referenceStagingResultCache.get(cacheKey);
if (!cached) return null;
if (cached.cachedAt + runtimeConfig.referenceCacheTtlMs <= Date.now()) {
referenceStagingResultCache.delete(cacheKey);
return null;
}
referenceStagingResultCache.delete(cacheKey);
referenceStagingResultCache.set(cacheKey, cached);
return cached.url;
}
function cacheStagedReference(
cacheKey: string,
stagedUrl: string,
runtimeConfig: Vip98kRuntimeConfig,
) {
referenceStagingResultCache.delete(cacheKey);
while (
referenceStagingResultCache.size >= runtimeConfig.referenceCacheMaxEntries
) {
const oldestKey = referenceStagingResultCache.keys().next().value;
if (typeof oldestKey !== "string") break;
referenceStagingResultCache.delete(oldestKey);
}
referenceStagingResultCache.set(cacheKey, {
cachedAt: Date.now(),
url: stagedUrl,
});
}
function isObjectNotFound(error: unknown) {
if (!error || typeof error !== "object") return false;
const record = error as {
name?: unknown;
Code?: unknown;
code?: unknown;
$metadata?: { httpStatusCode?: unknown };
};
return (
record.$metadata?.httpStatusCode === 404 ||
[record.name, record.Code, record.code].some((value) =>
/notfound|nosuchkey|no such key/i.test(String(value || "")),
)
);
}
async function findStagedReference(
key: string,
stagingConfig: ReferenceStagingConfig,
runtimeConfig: Vip98kRuntimeConfig,
) {
try {
return new URL(value).pathname.startsWith(`/${REFERENCE_STAGING_KEY_PREFIX}`);
} catch {
return false;
const response = await createS3Client(stagingConfig.storageConfig).send(
new HeadObjectCommand({
Bucket: stagingConfig.storageConfig.bucket,
Key: key,
}),
{ abortSignal: AbortSignal.timeout(REFERENCE_CACHE_LOOKUP_TIMEOUT_MS) },
);
if (
response.LastModified &&
Date.now() - response.LastModified.getTime() >
runtimeConfig.referenceCacheTtlMs
) {
return null;
}
return buildS3PublicUrl(stagingConfig.storageConfig, key);
} catch (error) {
if (!isObjectNotFound(error)) {
logger.warn("reference staging cache lookup failed; rebuilding", {
targetHost: getUrlHost(stagingConfig.sourcePublicUrl),
message: error instanceof Error ? error.message : String(error),
});
}
return null;
}
}
@ -647,10 +775,13 @@ async function downloadReferenceImageFromStorage(
async function uploadReferenceImage(
buffer: Buffer,
stagingConfig: ReferenceStagingConfig,
objectKey?: string,
) {
const key = `${REFERENCE_STAGING_KEY_PREFIX}${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}.jpg`;
const key =
objectKey ||
`${VIP98K_REFERENCE_STAGING_KEY_PREFIX}${Date.now()}-${Math.random()
.toString(36)
.slice(2, 8)}.jpg`;
await createS3Client(stagingConfig.storageConfig).send(
new PutObjectCommand({
Bucket: stagingConfig.storageConfig.bucket,
@ -690,25 +821,67 @@ async function normalizeReferenceImage(buffer: Buffer, mimeType: string) {
async function stageReferenceImage(
mediaUrl: string,
stagingConfig: ReferenceStagingConfig,
runtimeConfig: Vip98kRuntimeConfig,
cacheKey: string,
) {
const startedAt = Date.now();
const objectKey = getVip98kReferenceStagingObjectKey(mediaUrl);
const cacheLookupStartedAt = Date.now();
const existingUrl = await findStagedReference(
objectKey,
stagingConfig,
runtimeConfig,
);
const cacheLookupMs = Date.now() - cacheLookupStartedAt;
if (existingUrl) {
cacheStagedReference(cacheKey, existingUrl, runtimeConfig);
logger.info("reference staging object cache hit", {
sourceHost: getUrlHost(mediaUrl),
targetHost: getUrlHost(existingUrl),
cacheLookupMs,
totalMs: Date.now() - startedAt,
});
return existingUrl;
}
const downloadStartedAt = Date.now();
const downloaded =
(await downloadReferenceImageFromStorage(mediaUrl, stagingConfig)) ||
(await downloadReferenceImage(mediaUrl));
const downloadMs = Date.now() - downloadStartedAt;
const normalizeStartedAt = Date.now();
const normalized = await normalizeReferenceImage(
downloaded.buffer,
downloaded.mimeType,
);
const uploadedUrl = await uploadReferenceImage(normalized.buffer, stagingConfig);
const normalizeMs = Date.now() - normalizeStartedAt;
const uploadStartedAt = Date.now();
const uploadedUrl = await uploadReferenceImage(
normalized.buffer,
stagingConfig,
objectKey,
);
const uploadMs = Date.now() - uploadStartedAt;
cacheStagedReference(cacheKey, uploadedUrl, runtimeConfig);
logger.info("reference image staged", {
sourceHost: getUrlHost(mediaUrl),
targetHost: getUrlHost(uploadedUrl),
sourceBytes: downloaded.buffer.byteLength,
stagedBytes: normalized.buffer.byteLength,
cacheLookupMs,
downloadMs,
normalizeMs,
uploadMs,
totalMs: Date.now() - startedAt,
});
return uploadedUrl;
}
async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "video") {
async function ensurePublicMediaUrl(
mediaUrl: string,
mediaKind: "image" | "video",
runtimeConfig: Vip98kRuntimeConfig,
) {
const normalized = String(mediaUrl || "").trim();
if (/^https?:\/\//i.test(normalized)) {
if (mediaKind !== "image") return normalized;
@ -718,7 +891,7 @@ async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "vide
: null;
if (
!stagingConfig ||
sourceObjectKey?.startsWith(REFERENCE_STAGING_KEY_PREFIX) ||
sourceObjectKey?.startsWith(VIP98K_REFERENCE_STAGING_KEY_PREFIX) ||
!shouldStageVip98kReferenceUrl({
mediaUrl: normalized,
sourcePublicUrl: stagingConfig.sourcePublicUrl,
@ -727,12 +900,26 @@ async function ensurePublicMediaUrl(mediaUrl: string, mediaKind: "image" | "vide
return normalized;
}
const existingTask = referenceStagingTasks.get(normalized);
const cacheKey = getReferenceStagingCacheKey(normalized, stagingConfig);
const cachedUrl = getCachedStagedReference(cacheKey, runtimeConfig);
if (cachedUrl) {
logger.info("reference staging memory cache hit", {
sourceHost: getUrlHost(normalized),
targetHost: getUrlHost(cachedUrl),
});
return cachedUrl;
}
const existingTask = referenceStagingTasks.get(cacheKey);
if (existingTask) return existingTask;
const task = stageReferenceImage(normalized, stagingConfig).finally(() => {
referenceStagingTasks.delete(normalized);
const task = stageReferenceImage(
normalized,
stagingConfig,
runtimeConfig,
cacheKey,
).finally(() => {
referenceStagingTasks.delete(cacheKey);
});
referenceStagingTasks.set(normalized, task);
referenceStagingTasks.set(cacheKey, task);
return task;
}
if (!normalized.startsWith("data:")) return normalized;
@ -824,10 +1011,14 @@ function getConfiguredPollSeconds(config: Vip98kModelRuntimeConfig, key: string,
async function submitTask(params: {
apiKey: string;
baseURL?: string | null;
modelId: string;
path?: string | null;
method?: string | null;
contentType?: string | null;
body: Record<string, unknown>;
submissionTimeoutMs: number;
maxSubmissionAttempts: number;
submissionRetryBaseDelayMs: number;
}) {
const baseURL = normalizeVip98kBaseUrl(params.baseURL);
const path = String(params.path || "/api/external/task/generate").trim();
@ -845,14 +1036,17 @@ async function submitTask(params: {
const candidates = Array.isArray(value) ? value : [value];
return candidates.filter(
(candidate): candidate is string =>
typeof candidate === "string" && isStagedReferenceUrl(candidate),
typeof candidate === "string" &&
isVip98kStagedReferenceUrl(candidate),
);
});
const maxAttempts = MAX_SUBMISSION_ATTEMPTS;
const maxAttempts = params.maxSubmissionAttempts;
let refreshStagedReferences = false;
let idempotencyKey = randomUUID();
const submissionStartedAt = Date.now();
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const attemptStartedAt = Date.now();
const requestBody = refreshStagedReferences
? Object.fromEntries(
Object.entries(params.body).map(([key, value]) => {
@ -860,7 +1054,8 @@ async function submitTask(params: {
return [key, value];
}
const addRetryToken = (candidate: unknown) =>
typeof candidate === "string" && isStagedReferenceUrl(candidate)
typeof candidate === "string" &&
isVip98kStagedReferenceUrl(candidate)
? addVip98kReferenceRetryToken(
candidate,
`${Date.now()}-${attempt}`,
@ -883,13 +1078,28 @@ async function submitTask(params: {
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(120_000),
signal: AbortSignal.timeout(params.submissionTimeoutMs),
});
} catch (error) {
if (attempt >= maxAttempts) throw error;
const retryDelayMs = attempt * 5_000;
if (attempt >= maxAttempts) {
logger.error("provider submission transport failed", {
modelId: params.modelId,
path,
attempt,
attemptDurationMs: Date.now() - attemptStartedAt,
totalDurationMs: Date.now() - submissionStartedAt,
submissionTimeoutMs: params.submissionTimeoutMs,
message: error instanceof Error ? error.message : String(error),
});
throw error;
}
const retryDelayMs = attempt * params.submissionRetryBaseDelayMs;
logger.warn("provider submission transport failed; retrying idempotently", {
modelId: params.modelId,
path,
attempt,
attemptDurationMs: Date.now() - attemptStartedAt,
submissionTimeoutMs: params.submissionTimeoutMs,
retryDelayMs,
message: error instanceof Error ? error.message : String(error),
});
@ -898,6 +1108,14 @@ async function submitTask(params: {
continue;
}
const payload = await parseJsonResponse(response);
logger.info("provider submission response received", {
modelId: params.modelId,
path,
attempt,
status: response.status,
attemptDurationMs: Date.now() - attemptStartedAt,
totalDurationMs: Date.now() - submissionStartedAt,
});
if (response.ok) {
return {
payload,
@ -917,6 +1135,8 @@ async function submitTask(params: {
});
if (stagedMediaRetryDelayMs !== null) {
logger.warn("staged reference not ready for provider; retrying submission", {
modelId: params.modelId,
path,
attempt,
retryDelayMs: stagedMediaRetryDelayMs,
stagedReferenceCount: stagedReferences.length,
@ -939,6 +1159,8 @@ async function submitTask(params: {
});
if (reconciliationRetryDelayMs !== null) {
logger.warn("provider is reconciling task submission; retrying", {
modelId: params.modelId,
path,
attempt,
retryDelayMs: reconciliationRetryDelayMs,
});
@ -980,14 +1202,26 @@ export async function submitVip98kExternalTask(params: {
imageParams?: Record<string, unknown>;
modelConfig?: Vip98kModelRuntimeConfig;
}): Promise<Vip98kExternalTaskSubmission> {
const runtimeConfig = await getVip98kRuntimeConfig();
const referencePreparationStartedAt = Date.now();
const [imageUrls, videoUrls] = await Promise.all([
Promise.all(
(params.imageUrls || []).map((url) => ensurePublicMediaUrl(url, "image")),
(params.imageUrls || []).map((url) =>
ensurePublicMediaUrl(url, "image", runtimeConfig),
),
),
Promise.all(
(params.videoUrls || []).map((url) => ensurePublicMediaUrl(url, "video")),
(params.videoUrls || []).map((url) =>
ensurePublicMediaUrl(url, "video", runtimeConfig),
),
),
]);
logger.info("reference preparation completed", {
modelId: params.modelId,
imageCount: imageUrls.length,
videoCount: videoUrls.length,
durationMs: Date.now() - referencePreparationStartedAt,
});
const taskParams: Vip98kTaskParams = {
apiKey: params.apiKey,
baseURL: params.baseURL,
@ -1025,15 +1259,27 @@ export async function submitVip98kExternalTask(params: {
videoCount: videoUrls.length,
imageBindingKeys,
outputConstraintFallbacks,
submissionTimeoutMs: resolveVip98kSubmissionTimeoutMs(
params.modelId,
runtimeConfig,
),
maxSubmissionAttempts: runtimeConfig.maxSubmissionAttempts,
bodyKeys: Object.keys(body).sort(),
});
const submitted = await submitTask({
apiKey: params.apiKey,
baseURL: params.baseURL,
modelId: params.modelId,
path: invocation?.path,
method: invocation?.method,
contentType: invocation?.contentType,
body,
submissionTimeoutMs: resolveVip98kSubmissionTimeoutMs(
params.modelId,
runtimeConfig,
),
maxSubmissionAttempts: runtimeConfig.maxSubmissionAttempts,
submissionRetryBaseDelayMs: runtimeConfig.submissionRetryBaseDelayMs,
});
const resultPath = submitted.resultPath || invocation?.resultPath || "";
return {
@ -1116,6 +1362,13 @@ async function waitForTask(params: {
["succeeded", "success", "completed", "done", "finished"].includes(status) ||
(done && outputs.length > 0)
) {
logger.info("task polling completed", {
taskId: params.taskId,
modelId: params.modelId,
status,
outputCount: outputs.length,
durationMs: Date.now() - startedAt,
});
return { payload, outputs };
}
if (["failed", "error", "timeout", "canceled", "cancelled"].includes(status)) {
@ -1134,6 +1387,12 @@ async function waitForTask(params: {
}
}
logger.error("task polling timed out", {
taskId: params.taskId,
modelId: params.modelId,
durationMs: Date.now() - startedAt,
timeoutMs,
});
throw new Error("VIP98K task timed out; it may still be running");
}
@ -1365,7 +1624,13 @@ async function runTask(params: Vip98kTaskParams) {
throw new Error("VIP98K task completed but returned no image URL");
}
const imageSize = String(params.imageParams?.imageSize || "2K").trim() || "2K";
const archiveStartedAt = Date.now();
const archived = await Promise.all(outputUrls.slice(0, 1).map(archiveOutputImage));
logger.info("result archive completed", {
modelId: params.modelId,
outputCount: archived.length,
durationMs: Date.now() - archiveStartedAt,
});
return formatImageMarkdown(archived, imageSize);
}
@ -1415,22 +1680,14 @@ export function createVip98k({ apiKey, baseURL }: Vip98kConfig) {
const { prompt, imageUrls, videoUrls } = extractPromptAndMedia(
options.messages,
);
const [publicImageUrls, publicVideoUrls] = await Promise.all([
Promise.all(
imageUrls.map((url) => ensurePublicMediaUrl(url, "image")),
),
Promise.all(
videoUrls.map((url) => ensurePublicMediaUrl(url, "video")),
),
]);
const text = await runTask({
apiKey,
baseURL: rootBaseURL,
modelId: activeModelId,
kind: activeKind,
prompt,
imageUrls: publicImageUrls,
videoUrls: publicVideoUrls,
imageUrls,
videoUrls,
videoParams: options.videoParams,
imageParams: options.imageParams,
modelConfig: runtimeConfig,

View File

@ -1,5 +1,9 @@
import { createHash } from "node:crypto";
import { isMediaInputFailure } from "@/lib/ai/runtime/request-failure-classification";
export const VIP98K_REFERENCE_STAGING_KEY_PREFIX = "uploads/vip98k-";
const VIP98K_REFERENCE_CACHE_OBJECT_PREFIX = "uploads/vip98k-cache";
function parseUrl(value: string) {
try {
return new URL(String(value || "").trim());
@ -8,6 +12,24 @@ function parseUrl(value: string) {
}
}
export function isVip98kStagedReferenceUrl(value: string) {
const parsed = parseUrl(value);
return Boolean(
parsed?.pathname.includes(`/${VIP98K_REFERENCE_STAGING_KEY_PREFIX}`),
);
}
export function getVip98kReferenceStagingObjectKey(mediaUrl: string) {
const parsed = parseUrl(mediaUrl);
if (parsed) {
parsed.hash = "";
parsed.searchParams.delete("sg_media_retry");
}
const cacheIdentity = parsed?.toString() || mediaUrl;
const digest = createHash("sha256").update(cacheIdentity).digest("hex");
return `${VIP98K_REFERENCE_CACHE_OBJECT_PREFIX}/${digest}.jpg`;
}
export function isUrlWithinPublicBase(mediaUrl: string, publicBaseUrl: string) {
const media = parseUrl(mediaUrl);
const base = parseUrl(publicBaseUrl);

View File

@ -0,0 +1,105 @@
export const VIP98K_RUNTIME_CONFIG_KEY = "vip98k_runtime_config";
export type Vip98kRuntimeConfig = {
submissionTimeoutMs: number;
modelSubmissionTimeoutMs: Record<string, number>;
maxSubmissionAttempts: number;
submissionRetryBaseDelayMs: number;
referenceCacheTtlMs: number;
referenceCacheMaxEntries: number;
};
export const defaultVip98kRuntimeConfig: Vip98kRuntimeConfig = {
submissionTimeoutMs: 120_000,
modelSubmissionTimeoutMs: {
"nano-banana-pro": 240_000,
},
maxSubmissionAttempts: 3,
submissionRetryBaseDelayMs: 5_000,
referenceCacheTtlMs: 7 * 24 * 60 * 60 * 1000,
referenceCacheMaxEntries: 2_000,
};
function clampInteger(
value: unknown,
fallback: number,
minimum: number,
maximum: number,
) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(maximum, Math.max(minimum, Math.round(numeric)));
}
function normalizeModelTimeouts(value: unknown) {
const configured =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const normalized: Record<string, number> = {
...defaultVip98kRuntimeConfig.modelSubmissionTimeoutMs,
};
for (const [modelId, timeout] of Object.entries(configured)) {
const normalizedModelId = modelId.trim().toLowerCase();
if (!normalizedModelId || normalizedModelId.length > 160) continue;
normalized[normalizedModelId] = clampInteger(
timeout,
defaultVip98kRuntimeConfig.submissionTimeoutMs,
30_000,
10 * 60_000,
);
}
return normalized;
}
export function normalizeVip98kRuntimeConfig(value: unknown): Vip98kRuntimeConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
return {
submissionTimeoutMs: clampInteger(
raw.submissionTimeoutMs,
defaultVip98kRuntimeConfig.submissionTimeoutMs,
30_000,
10 * 60_000,
),
modelSubmissionTimeoutMs: normalizeModelTimeouts(
raw.modelSubmissionTimeoutMs,
),
maxSubmissionAttempts: clampInteger(
raw.maxSubmissionAttempts,
defaultVip98kRuntimeConfig.maxSubmissionAttempts,
1,
5,
),
submissionRetryBaseDelayMs: clampInteger(
raw.submissionRetryBaseDelayMs,
defaultVip98kRuntimeConfig.submissionRetryBaseDelayMs,
1_000,
60_000,
),
referenceCacheTtlMs: clampInteger(
raw.referenceCacheTtlMs,
defaultVip98kRuntimeConfig.referenceCacheTtlMs,
5 * 60_000,
30 * 24 * 60 * 60_000,
),
referenceCacheMaxEntries: clampInteger(
raw.referenceCacheMaxEntries,
defaultVip98kRuntimeConfig.referenceCacheMaxEntries,
100,
10_000,
),
};
}
export function resolveVip98kSubmissionTimeoutMs(
modelId: string,
config: Vip98kRuntimeConfig,
) {
return (
config.modelSubmissionTimeoutMs[modelId.trim().toLowerCase()] ||
config.submissionTimeoutMs
);
}

View File

@ -4,6 +4,10 @@ import {
fetchVip98kModelPresets,
type Vip98kModelPreset,
} from "../lib/ai/vip98k-models";
import {
defaultVip98kRuntimeConfig,
VIP98K_RUNTIME_CONFIG_KEY,
} from "../lib/ai/vip98k-runtime-config";
dotenv.config({ path: ".env.local" });
dotenv.config();
@ -140,6 +144,15 @@ async function main() {
if (!apply) return;
await sql.begin(async (tx) => {
await tx`
insert into "SystemSettings" (key, value, "updatedAt")
values (
${VIP98K_RUNTIME_CONFIG_KEY},
${tx.json(toJsonValue(defaultVip98kRuntimeConfig))},
now()
)
on conflict (key) do nothing
`;
await tx`
update "AiProvider"
set "baseUrl" = 'https://api.vip98k.com', "apiKey" = ${vip98kApiKey},

View File

@ -13,11 +13,17 @@ import {
import {
addVip98kReferenceRetryToken,
getStorageObjectKeyFromPublicUrl,
getVip98kReferenceStagingObjectKey,
getVip98kStagedMediaRetryDelay,
getVip98kSubmissionRetryDelay,
isVip98kStagedReferenceUrl,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
import {
normalizeVip98kRuntimeConfig,
resolveVip98kSubmissionTimeoutMs,
} from "@/lib/ai/vip98k-runtime-config";
const textToImageInvocation: Vip98kTaskInvocation = {
kind: "tool",
@ -31,6 +37,48 @@ const textToImageInvocation: Vip98kTaskInvocation = {
};
test.describe("VIP98K reference-image routing", () => {
test("uses a stable cross-process cache key for staged references", () => {
const source = "https://storage.example/uploads/reference.png";
const key = getVip98kReferenceStagingObjectKey(source);
expect(key).toMatch(/^uploads\/vip98k-cache\/[a-f0-9]{64}\.jpg$/);
expect(
getVip98kReferenceStagingObjectKey(`${source}?sg_media_retry=retry-2`),
).toBe(key);
expect(
isVip98kStagedReferenceUrl(
`https://minio.example/bucket/${key}`,
),
).toBe(true);
});
test("loads bounded database runtime settings with a Pro timeout override", () => {
const defaults = normalizeVip98kRuntimeConfig(null);
expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", defaults)).toBe(
120_000,
);
expect(resolveVip98kSubmissionTimeoutMs("nano-banana-pro", defaults)).toBe(
240_000,
);
expect(defaults.referenceCacheTtlMs).toBe(7 * 24 * 60 * 60 * 1000);
const config = normalizeVip98kRuntimeConfig({
submissionTimeoutMs: 90_000,
modelSubmissionTimeoutMs: { "nano-banana-pro": 300_000 },
maxSubmissionAttempts: 20,
referenceCacheTtlMs: 1,
});
expect(config.maxSubmissionAttempts).toBe(5);
expect(config.referenceCacheTtlMs).toBe(5 * 60_000);
expect(resolveVip98kSubmissionTimeoutMs("nano-banana-2", config)).toBe(
90_000,
);
expect(resolveVip98kSubmissionTimeoutMs("NANO-BANANA-PRO", config)).toBe(
300_000,
);
});
test("identifies default-storage references that need provider staging", () => {
expect(
shouldStageVip98kReferenceUrl({