openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
510 lines (509 loc) • 22.1 kB
JavaScript
import { b as uniqueValues } from "./string-normalization-DsCfAx8q.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { o as resolveAgentModelTimeoutMsValue } from "./model-input-BuGMCNOz.js";
import "./provider-env-vars-B8XgJCSL.js";
import { a as resolveCapabilityModelCandidates, d as throwCapabilityGenerationFailure, i as recordCapabilityCandidateFailure, l as resolveMediaProviderRequestTimeoutMs, n as buildNoCapabilityModelConfiguredMessage, t as buildMediaGenerationNormalizationMetadata } from "./runtime-shared-DRaKQShq.js";
import { o as listVideoGenerationProviders, r as getVideoGenerationProvider, s as parseGenerationModelRef } from "./registry-BFqDasrd.js";
import { n as hasMediaNormalizationEntry, t as resolveMediaGeometryOverrides } from "./geometry-normalization-ecjI7wns.js";
//#region src/video-generation/capabilities.ts
function resolveVideoGenerationMode(params) {
const inputImageCount = params.inputImageCount ?? 0;
const inputVideoCount = params.inputVideoCount ?? 0;
if (inputImageCount > 0 && inputVideoCount > 0) return null;
if (inputVideoCount > 0) return "videoToVideo";
if (inputImageCount > 0) return "imageToVideo";
return "generate";
}
function listSupportedVideoGenerationModes(provider) {
const modes = ["generate"];
if (provider.capabilities.imageToVideo?.enabled) modes.push("imageToVideo");
if (provider.capabilities.videoToVideo?.enabled) modes.push("videoToVideo");
return modes;
}
function resolveVideoGenerationModeCapabilities(params) {
const inputImageCount = params.inputImageCount ?? 0;
const inputVideoCount = params.inputVideoCount ?? 0;
const mode = resolveVideoGenerationMode(params);
const capabilities = params.provider?.capabilities;
const withModelLimits = (caps) => {
const model = params.model?.trim();
if (!caps || !model) return caps;
const maxInputImages = caps.maxInputImagesByModel?.[model];
const maxInputVideos = caps.maxInputVideosByModel?.[model];
const maxInputAudios = caps.maxInputAudiosByModel?.[model];
if (typeof maxInputImages !== "number" && typeof maxInputVideos !== "number" && typeof maxInputAudios !== "number") return caps;
return {
...caps,
...typeof maxInputImages === "number" ? { maxInputImages } : {},
...typeof maxInputVideos === "number" ? { maxInputVideos } : {},
...typeof maxInputAudios === "number" ? { maxInputAudios } : {}
};
};
if (!capabilities) return {
mode,
capabilities: void 0
};
if (mode === "generate") return {
mode,
capabilities: withModelLimits(capabilities.generate)
};
if (mode === "imageToVideo") return {
mode,
capabilities: withModelLimits(capabilities.imageToVideo)
};
if (mode === "videoToVideo") return {
mode,
capabilities: withModelLimits(capabilities.videoToVideo)
};
const videoToVideoCapabilities = withModelLimits(capabilities.videoToVideo);
if (inputImageCount > 0 && inputVideoCount > 0 && videoToVideoCapabilities?.enabled && (videoToVideoCapabilities.maxInputImages ?? 0) > 0) return {
mode,
capabilities: videoToVideoCapabilities
};
return {
mode,
capabilities: void 0
};
}
//#endregion
//#region src/video-generation/capability-overlays.ts
function isVideoGenerationTransformCapabilities(capabilities) {
return Boolean(capabilities && "enabled" in capabilities);
}
function buildVideoGenerationCapabilityFailure(params) {
const { providerId, model, provider, inputImageCount, inputVideoCount, inputAudioCount } = params;
const label = `${providerId}/${model}`;
const { mode, capabilities } = resolveVideoGenerationModeCapabilities({
provider,
model,
inputImageCount,
inputVideoCount
});
const catalogModes = provider.catalogByModel?.[model]?.modes;
if (mode && catalogModes && !catalogModes.includes(mode)) return `${label} does not support ${mode === "generate" ? "text-to-video generation" : mode === "imageToVideo" ? "image-to-video generation" : "video-to-video generation"}; skipping`;
if (inputImageCount > 0 || inputVideoCount > 0) {
const visualLabel = inputImageCount > 0 && inputVideoCount > 0 ? "combined image/video reference inputs" : inputImageCount > 0 ? "reference image inputs" : "reference video inputs";
if (!capabilities || !isVideoGenerationTransformCapabilities(capabilities)) return `${label} does not support ${visualLabel}; skipping to avoid silent reference drop`;
if (!capabilities.enabled) return `${label} does not support ${visualLabel}; skipping to avoid silent reference drop`;
}
if (inputImageCount > 0) {
const maxImages = capabilities?.maxInputImages ?? provider.capabilities.maxInputImages ?? 0;
if (inputImageCount > maxImages) return maxImages === 0 ? `${label} does not support reference image inputs; skipping to avoid silent image drop` : `${label} supports at most ${maxImages} reference image(s), ${inputImageCount} requested; skipping`;
}
if (inputVideoCount > 0) {
const maxVideos = capabilities?.maxInputVideos ?? provider.capabilities.maxInputVideos ?? 0;
if (inputVideoCount > maxVideos) return maxVideos === 0 ? `${label} does not support reference video inputs; skipping to avoid silent video drop` : `${label} supports at most ${maxVideos} reference video(s), ${inputVideoCount} requested; skipping`;
}
if (inputAudioCount > 0) {
const maxAudio = capabilities?.maxInputAudios ?? provider.capabilities.maxInputAudios ?? 0;
if (inputAudioCount > maxAudio) return maxAudio === 0 ? `${label} does not support reference audio inputs; skipping to avoid silent audio drop` : `${label} supports at most ${maxAudio} reference audio(s), ${inputAudioCount} requested; skipping`;
}
}
function mergeVideoGenerationCapabilities(base, overlay) {
const overlayOptions = overlay.providerOptions;
const mergedProviderOptions = Object.hasOwn(overlay, "providerOptions") && overlayOptions && Object.keys(overlayOptions).length === 0 ? overlayOptions : base.providerOptions || overlayOptions ? {
...base.providerOptions,
...overlayOptions
} : void 0;
return {
...base,
...overlay,
...mergedProviderOptions ? { providerOptions: mergedProviderOptions } : {}
};
}
function mergeVideoGenerationModeCapabilities(base, overlay) {
if (!overlay) return base;
if (!base) return overlay;
return mergeVideoGenerationCapabilities(base, overlay);
}
function mergeVideoGenerationProviderCapabilities(base, overlay) {
return {
...mergeVideoGenerationCapabilities(base, overlay),
generate: mergeVideoGenerationModeCapabilities(base.generate, overlay.generate),
imageToVideo: mergeVideoGenerationModeCapabilities(base.imageToVideo, overlay.imageToVideo),
videoToVideo: mergeVideoGenerationModeCapabilities(base.videoToVideo, overlay.videoToVideo)
};
}
async function resolveProviderWithModelCapabilities(params) {
if (!params.provider.resolveModelCapabilities) return params.provider;
try {
const modelCapabilities = await params.provider.resolveModelCapabilities({
provider: params.providerId,
model: params.model,
cfg: params.cfg,
agentDir: params.agentDir,
authStore: params.authStore,
...params.timeoutMs !== void 0 ? { timeoutMs: params.timeoutMs } : {}
});
if (!modelCapabilities) return params.provider;
return {
...params.provider,
capabilities: mergeVideoGenerationProviderCapabilities(params.provider.capabilities, modelCapabilities)
};
} catch (err) {
params.log.debug(`video-generation model capability lookup failed for ${params.providerId}/${params.model}: ${err instanceof Error ? err.message : String(err)}`);
return params.provider;
}
}
//#endregion
//#region src/video-generation/duration-support.ts
function normalizeSupportedDurationValues(values) {
if (!Array.isArray(values) || values.length === 0) return;
const normalized = uniqueValues(values).filter((value) => Number.isFinite(value) && value > 0).map((value) => Math.round(value)).filter((value) => value > 0).toSorted((left, right) => left - right);
return normalized.length > 0 ? normalized : void 0;
}
function resolveVideoGenerationSupportedDurations(params) {
const { capabilities: caps } = resolveVideoGenerationModeCapabilities({
provider: params.provider,
model: params.model,
inputImageCount: params.inputImageCount,
inputVideoCount: params.inputVideoCount
});
const model = params.model?.trim();
return normalizeSupportedDurationValues((model && caps?.supportedDurationSecondsByModel ? caps.supportedDurationSecondsByModel[model] : void 0) ?? caps?.supportedDurationSeconds);
}
function normalizeVideoGenerationDuration(params) {
if (typeof params.durationSeconds !== "number" || !Number.isFinite(params.durationSeconds)) return;
const rounded = Math.max(1, Math.round(params.durationSeconds));
const supported = resolveVideoGenerationSupportedDurations(params);
if (!supported || supported.length === 0) return rounded;
return supported.reduce((best, current) => {
const currentDistance = Math.abs(current - rounded);
const bestDistance = Math.abs(best - rounded);
if (currentDistance < bestDistance) return current;
if (currentDistance === bestDistance && current > best) return current;
return best;
});
}
//#endregion
//#region src/video-generation/normalization.ts
const VIDEO_RESOLUTION_ORDER = [
"360P",
"480P",
"540P",
"720P",
"768P",
"1080P"
];
function resolveVideoGenerationOverrides(params) {
const { capabilities: caps } = resolveVideoGenerationModeCapabilities({
provider: params.provider,
model: params.model,
inputImageCount: params.inputImageCount,
inputVideoCount: params.inputVideoCount
});
const sanitized = resolveMediaGeometryOverrides({
size: params.size,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
capabilities: caps,
resolutionOrder: VIDEO_RESOLUTION_ORDER,
reportUnrecognizedOverrides: true,
useAspectRatioForRequestedSize: true
});
const ignoredOverrides = sanitized.ignoredOverrides;
const normalization = sanitized.normalization;
let { audio, watermark } = params;
if (caps && typeof audio === "boolean" && !caps.supportsAudio) {
ignoredOverrides.push({
key: "audio",
value: audio
});
audio = void 0;
}
if (caps && typeof watermark === "boolean" && !caps.supportsWatermark) {
ignoredOverrides.push({
key: "watermark",
value: watermark
});
watermark = void 0;
}
const requestedDurationSeconds = typeof params.durationSeconds === "number" && Number.isFinite(params.durationSeconds) ? Math.max(1, Math.round(params.durationSeconds)) : void 0;
const durationSeconds = normalizeVideoGenerationDuration({
provider: params.provider,
model: params.model,
durationSeconds: requestedDurationSeconds,
inputImageCount: params.inputImageCount ?? 0,
inputVideoCount: params.inputVideoCount ?? 0
});
const supportedDurationSeconds = resolveVideoGenerationSupportedDurations({
provider: params.provider,
model: params.model,
inputImageCount: params.inputImageCount ?? 0,
inputVideoCount: params.inputVideoCount ?? 0
});
if (typeof requestedDurationSeconds === "number" && typeof durationSeconds === "number" && requestedDurationSeconds !== durationSeconds) normalization.durationSeconds = {
requested: requestedDurationSeconds,
applied: durationSeconds,
...supportedDurationSeconds?.length ? { supportedValues: supportedDurationSeconds } : {}
};
return {
size: sanitized.size,
aspectRatio: sanitized.aspectRatio,
resolution: sanitized.resolution,
durationSeconds,
supportedDurationSeconds,
audio,
watermark,
ignoredOverrides,
normalization: hasMediaNormalizationEntry(normalization.size) || hasMediaNormalizationEntry(normalization.aspectRatio) || hasMediaNormalizationEntry(normalization.resolution) || hasMediaNormalizationEntry(normalization.durationSeconds) ? normalization : void 0
};
}
//#endregion
//#region src/video-generation/runtime.ts
const log = createSubsystemLogger("video-generation");
const MODEL_CAPABILITY_LOOKUP_TIMEOUT_MS = 5e3;
const SUPPORTED_DURATIONS_HINT = Symbol.for("openclaw.videoGeneration.supportedDurations");
/**
* Validate agent-supplied providerOptions against the candidate's declared
* schema. Returns a human-readable skip reason when the candidate cannot
* accept the supplied options, or undefined when everything checks out.
*
* Backward-compatible behavior:
* - Provider declares no schema (undefined): pass options through as-is.
* The provider receives them and may silently ignore unknown keys. This is
* the safe default for legacy / not-yet-migrated providers.
* - Provider explicitly declares an empty schema ({}): rejects any options.
* This is the opt-in signal that the provider has been audited and truly
* supports no options.
* - Provider declares a typed schema: validates each key name and value type,
* skipping the candidate on any mismatch.
*/
function validateProviderOptionsAgainstDeclaration(params) {
const { providerId, model, providerOptions, declaration } = params;
const keys = Object.keys(providerOptions);
if (keys.length === 0) return;
if (declaration === void 0) return;
if (Object.keys(declaration).length === 0) return `${providerId}/${model} does not accept providerOptions (caller supplied: ${keys.join(", ")}); skipping`;
const unknown = keys.filter((key) => !Object.hasOwn(declaration, key));
if (unknown.length > 0) {
const accepted = Object.keys(declaration).join(", ");
return `${providerId}/${model} does not accept providerOptions keys: ${unknown.join(", ")} (accepted: ${accepted}); skipping`;
}
for (const key of keys) {
const expected = declaration[key];
const value = providerOptions[key];
const actual = typeof value;
if (expected === "number" && (actual !== "number" || !Number.isFinite(value))) return `${providerId}/${model} expects providerOptions.${key} to be a finite number, got ${actual}; skipping`;
if (expected === "boolean" && actual !== "boolean") return `${providerId}/${model} expects providerOptions.${key} to be a boolean, got ${actual}; skipping`;
if (expected === "string" && actual !== "string") return `${providerId}/${model} expects providerOptions.${key} to be a string, got ${actual}; skipping`;
}
}
function buildNoVideoGenerationModelConfiguredMessage(cfg, deps) {
const listProviders = deps.listProviders ?? listVideoGenerationProviders;
return buildNoCapabilityModelConfiguredMessage({
capabilityLabel: "video-generation",
modelConfigKey: "mediaModels.video",
providers: listProviders(cfg),
getProviderEnvVars: deps.getProviderEnvVars
});
}
function listRuntimeVideoGenerationProviders(params, deps = {}) {
return (deps.listProviders ?? listVideoGenerationProviders)(params?.config);
}
async function generateVideo(params, deps = {}) {
const getProvider = deps.getProvider ?? getVideoGenerationProvider;
const listProviders = deps.listProviders ?? listVideoGenerationProviders;
const logger = deps.log ?? log;
const requestedTimeoutMs = params.timeoutMs ?? resolveAgentModelTimeoutMsValue(params.cfg.agents?.defaults?.mediaModels?.video);
const candidates = resolveCapabilityModelCandidates({
cfg: params.cfg,
modelConfig: params.cfg.agents?.defaults?.mediaModels?.video,
modelOverride: params.modelOverride,
parseModelRef: parseGenerationModelRef,
agentDir: params.agentDir,
listProviders,
autoProviderFallback: params.autoProviderFallback
});
if (candidates.length === 0) throw new Error(buildNoVideoGenerationModelConfiguredMessage(params.cfg, deps));
const attempts = [];
let lastError;
let skipWarnEmitted = false;
const warnOnFirstSkip = (reason) => {
if (!skipWarnEmitted) {
skipWarnEmitted = true;
logger.warn(`video-generation candidate skipped: ${reason}`);
}
};
for (const candidate of candidates) {
const provider = getProvider(candidate.provider, params.cfg);
if (!provider) {
const error = `No video-generation provider registered for ${candidate.provider}`;
attempts.push({
provider: candidate.provider,
model: candidate.model,
error
});
lastError = new Error(error);
continue;
}
const timeoutMs = resolveMediaProviderRequestTimeoutMs({
timeoutMs: requestedTimeoutMs,
providerDefaultTimeoutMs: provider.defaultTimeoutMs
});
const activeProvider = await resolveProviderWithModelCapabilities({
provider,
providerId: candidate.provider,
model: candidate.model,
cfg: params.cfg,
agentDir: params.agentDir,
authStore: params.authStore,
timeoutMs: MODEL_CAPABILITY_LOOKUP_TIMEOUT_MS,
log: logger
});
const inputImageCount = params.inputImages?.length ?? 0;
const inputVideoCount = params.inputVideos?.length ?? 0;
const inputAudioCount = params.inputAudios?.length ?? 0;
const capabilityMismatch = buildVideoGenerationCapabilityFailure({
providerId: candidate.provider,
model: candidate.model,
provider: activeProvider,
inputImageCount,
inputVideoCount,
inputAudioCount
});
if (capabilityMismatch) {
attempts.push({
provider: candidate.provider,
model: candidate.model,
error: capabilityMismatch
});
lastError = new Error(capabilityMismatch);
warnOnFirstSkip(capabilityMismatch);
logger.debug(`video-generation candidate skipped (mode or reference capability): ${candidate.provider}/${candidate.model}`);
continue;
}
if (params.providerOptions && typeof params.providerOptions === "object" && Object.keys(params.providerOptions).length > 0) {
const { capabilities: optCaps } = resolveVideoGenerationModeCapabilities({
provider: activeProvider,
model: candidate.model,
inputImageCount,
inputVideoCount
});
const declaredOptions = optCaps?.providerOptions ?? activeProvider.capabilities.providerOptions ?? void 0;
const mismatch = validateProviderOptionsAgainstDeclaration({
providerId: candidate.provider,
model: candidate.model,
providerOptions: params.providerOptions,
declaration: declaredOptions
});
if (mismatch) {
attempts.push({
provider: candidate.provider,
model: candidate.model,
error: mismatch
});
lastError = new Error(mismatch);
warnOnFirstSkip(mismatch);
logger.debug(`video-generation candidate skipped (providerOptions): ${candidate.provider}/${candidate.model}`);
continue;
}
}
const supportedDurations = resolveVideoGenerationSupportedDurations({
provider: activeProvider,
model: candidate.model,
inputImageCount,
inputVideoCount
});
const requestedDuration = params.durationSeconds;
if (typeof requestedDuration === "number" && Number.isFinite(requestedDuration)) {
const { capabilities: durCaps } = resolveVideoGenerationModeCapabilities({
provider: activeProvider,
model: candidate.model,
inputImageCount,
inputVideoCount
});
const maxDuration = durCaps?.maxDurationSeconds ?? activeProvider.capabilities.maxDurationSeconds;
if (!supportedDurations && typeof maxDuration === "number" && Math.round(requestedDuration) > maxDuration) {
const error = `${candidate.provider}/${candidate.model} supports at most ${maxDuration}s per video, ${requestedDuration}s requested; skipping`;
attempts.push({
provider: candidate.provider,
model: candidate.model,
error
});
lastError = new Error(error);
warnOnFirstSkip(error);
logger.debug(`video-generation candidate skipped (duration capability): ${candidate.provider}/${candidate.model}`);
continue;
}
}
try {
const sanitized = resolveVideoGenerationOverrides({
provider: activeProvider,
model: candidate.model,
size: params.size,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
durationSeconds: params.durationSeconds,
audio: params.audio,
watermark: params.watermark,
inputImageCount,
inputVideoCount
});
const generationRequest = {
provider: candidate.provider,
model: candidate.model,
prompt: params.prompt,
cfg: params.cfg,
agentDir: params.agentDir,
authStore: params.authStore,
size: sanitized.size,
aspectRatio: sanitized.aspectRatio,
resolution: sanitized.resolution,
durationSeconds: sanitized.durationSeconds,
audio: sanitized.audio,
watermark: sanitized.watermark,
inputImages: params.inputImages,
inputVideos: params.inputVideos,
inputAudios: params.inputAudios,
providerOptions: params.providerOptions,
...timeoutMs !== void 0 ? { timeoutMs } : {}
};
if (supportedDurations) generationRequest[SUPPORTED_DURATIONS_HINT] = supportedDurations;
const result = await provider.generateVideo(generationRequest);
if (!Array.isArray(result.videos) || result.videos.length === 0) throw new Error("Video generation provider returned no videos.");
return {
videos: result.videos.map((video, index) => {
if (video.buffer?.byteLength === 0) {
if (video.url) {
const { buffer: _emptyBuffer, ...urlOnlyVideo } = video;
return urlOnlyVideo;
}
throw new Error(`Video generation provider returned an empty video buffer at index ${index}.`);
}
if (!video.buffer && !video.url) throw new Error(`Video generation provider returned an undeliverable asset at index ${index}: neither buffer nor url is set.`);
return video;
}),
provider: candidate.provider,
model: result.model ?? candidate.model,
attempts,
normalization: sanitized.normalization,
ignoredOverrides: sanitized.ignoredOverrides,
metadata: {
...result.metadata,
...buildMediaGenerationNormalizationMetadata({
normalization: sanitized.normalization,
requestedSizeForDerivedAspectRatio: params.size,
includeSupportedDurationSeconds: true
})
}
};
} catch (err) {
lastError = err;
recordCapabilityCandidateFailure({
attempts,
provider: candidate.provider,
model: candidate.model,
error: err
});
logger.debug(`video-generation candidate failed: ${candidate.provider}/${candidate.model}`);
}
}
return throwCapabilityGenerationFailure({
capabilityLabel: "video generation",
attempts,
lastError
});
}
//#endregion
export { listRuntimeVideoGenerationProviders as n, listSupportedVideoGenerationModes as r, generateVideo as t };