perf: cache vip98k reference staging
This commit is contained in:
parent
2fb53b4279
commit
08ed9ac3ef
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
105
lib/ai/vip98k-runtime-config.ts
Normal file
105
lib/ai/vip98k-runtime-config.ts
Normal 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
|
||||
);
|
||||
}
|
||||
@ -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},
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user