openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
331 lines (330 loc) • 13.8 kB
JavaScript
import { a as XAI_VIDEO_ASPECT_RATIOS, o as XAI_VIDEO_DEFAULT_TIMEOUT_MS, p as isXaiVideo15Model, t as DEFAULT_XAI_VIDEO_BASE_URL } from "./capability-provider-metadata-factory-Hx0z45WX.mjs";
import { n as createXaiVideoGenerationProviderMetadata } from "./capability-provider-metadata-BM9IhPac.mjs";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { toImageDataUrl } from "openclaw/plugin-sdk/image-generation";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import { assertOkOrThrowHttpError, createProviderOperationDeadline, createProviderOperationTimeoutResolver, executeProviderOperationWithRetry, fetchWithTimeoutGuarded, pollProviderOperation, postJsonRequest, readProviderJsonResponse, resolveProviderHttpRequestConfig, resolveProviderOperationTimeoutMs, sanitizeConfiguredModelProviderRequest, waitProviderOperationPollInterval } from "openclaw/plugin-sdk/provider-http";
import { downloadGeneratedVideoAsset, resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
//#region extensions/xai/video-generation-transport.ts
function resolveXaiVideoFetchTimeoutMs(timeoutMs, defaultTimeoutMs) {
const resolved = typeof timeoutMs === "function" ? timeoutMs() : timeoutMs;
return typeof resolved === "number" && Number.isFinite(resolved) && resolved > 0 ? resolved : defaultTimeoutMs;
}
async function fetchXaiVideoResponse(params) {
return await executeProviderOperationWithRetry({
provider: "xai",
stage: params.stage,
operation: async () => {
const result = await fetchWithTimeoutGuarded(params.url, params.init, resolveXaiVideoFetchTimeoutMs(params.timeoutMs, params.defaultTimeoutMs), params.fetchFn, {
...params.allowPrivateNetwork ? { ssrfPolicy: { allowPrivateNetwork: true } } : {},
...params.dispatcherPolicy ? { dispatcherPolicy: params.dispatcherPolicy } : {},
auditContext: params.auditContext
});
try {
await assertOkOrThrowHttpError(result.response, params.requestFailedMessage);
return result;
} catch (error) {
await result.release();
throw error;
}
}
});
}
async function downloadXaiVideo(params) {
return await downloadGeneratedVideoAsset({
url: params.url,
timeoutMs: params.timeoutMs ?? params.defaultTimeoutMs,
defaultTimeoutMs: params.defaultTimeoutMs,
fetchFn: params.fetchFn,
provider: "xai",
label: "xAI generated video download",
requestFailedMessage: "xAI generated video download failed",
maxBytes: params.maxBytes,
validateBinaryResponse: true,
chunkTimeoutMs: 0,
fetchResponse: async ({ timeoutMs }) => await fetchXaiVideoResponse({
url: params.url,
stage: "download",
requestFailedMessage: "xAI generated video download failed",
auditContext: "xai-video-download",
init: { method: "GET" },
timeoutMs,
defaultTimeoutMs: params.defaultTimeoutMs,
allowPrivateNetwork: params.allowPrivateNetwork,
dispatcherPolicy: params.dispatcherPolicy,
fetchFn: params.fetchFn
})
});
}
//#endregion
//#region extensions/xai/video-generation-provider.ts
const POLL_INTERVAL_MS = 5e3;
const MAX_POLL_ATTEMPTS = 120;
const XAI_VIDEO_MALFORMED_RESPONSE = "xAI video generation response malformed";
const XAI_VIDEO_TERMINAL_FAILURE_STATUSES = /* @__PURE__ */ new Set([
"failed",
"error",
"expired",
"cancelled"
]);
const XAI_VIDEO_DEFAULT_DURATION_SECONDS = 8;
const XAI_VIDEO_DEFAULT_ASPECT_RATIO = "16:9";
const XAI_VIDEO_DEFAULT_RESOLUTION = "480p";
async function readXaiVideoJson(response) {
let payload;
try {
payload = await readProviderJsonResponse(response, "xAI video generation response");
} catch (error) {
if (error instanceof Error && error.message.endsWith(": malformed JSON response")) throw new Error(XAI_VIDEO_MALFORMED_RESPONSE, { cause: error });
throw error;
}
if (!isRecord(payload)) throw new Error(XAI_VIDEO_MALFORMED_RESPONSE);
return payload;
}
function xaiErrorMessage(payload) {
const error = payload.error;
if (error === void 0 || error === null) return;
if (!isRecord(error)) throw new Error(XAI_VIDEO_MALFORMED_RESPONSE);
return normalizeOptionalString(error.message);
}
function readXaiStatusResponse(payload) {
const video = payload.video;
if (video !== void 0 && video !== null && !isRecord(video)) throw new Error(XAI_VIDEO_MALFORMED_RESPONSE);
return {
status: normalizeOptionalString(payload.status) ?? "",
videoUrl: isRecord(video) ? normalizeOptionalString(video.url) : void 0,
errorMessage: xaiErrorMessage(payload)
};
}
function resolveImageUrl(input) {
const inputUrl = normalizeOptionalString(input.url);
if (inputUrl) return inputUrl;
if (!input.buffer) throw new Error("xAI image-to-video input is missing image data.");
return toImageDataUrl({
...input,
buffer: input.buffer,
defaultMimeType: "image/png"
});
}
function isReferenceImage(input) {
return normalizeOptionalString(input.role)?.toLowerCase() === "reference_image";
}
function isFirstFrameImage(input) {
const role = normalizeOptionalString(input.role)?.toLowerCase();
return role === void 0 || role === "first_frame";
}
function validateXaiVideo15Request(req) {
if (!isXaiVideo15Model(req.model)) return;
if ((req.inputVideos?.length ?? 0) > 0) throw new Error("xAI grok-imagine-video-1.5 does not support video inputs.");
const [inputImage, ...additionalImages] = req.inputImages ?? [];
if (!inputImage || additionalImages.length > 0) throw new Error("xAI grok-imagine-video-1.5 requires exactly one first-frame image.");
if (!isFirstFrameImage(inputImage)) throw new Error("xAI grok-imagine-video-1.5 supports only an ordinary or first_frame image.");
}
function resolveInputVideoUrl(input) {
if (!input) return;
const url = normalizeOptionalString(input.url);
if (url) return url;
if (input.buffer) throw new Error("xAI video editing currently requires a remote mp4 URL input.");
throw new Error("xAI video editing input is missing video data.");
}
function resolveDurationSeconds(params) {
if (typeof params.durationSeconds !== "number" || !Number.isFinite(params.durationSeconds)) return;
const rounded = Math.round(params.durationSeconds);
return Math.max(params.min ?? 1, Math.min(params.max ?? 15, rounded));
}
function resolveAspectRatio(value) {
const trimmed = normalizeOptionalString(value);
if (!trimmed || !XAI_VIDEO_ASPECT_RATIOS.has(trimmed)) return;
return trimmed;
}
function resolveResolution(value, options) {
if (typeof value !== "string") return;
const normalized = value.trim().toLowerCase();
if (normalized === "480p") return "480p";
if (normalized === "720p") return "720p";
if (normalized === "1080p") return options?.allow1080p ? "1080p" : "720p";
}
function resolveXaiVideoMode(req) {
const hasVideoInput = (req.inputVideos?.length ?? 0) > 0;
if (!hasVideoInput && (req.inputImages ?? []).some(isReferenceImage)) return "referenceToVideo";
if (!hasVideoInput) return "generate";
return typeof resolveDurationSeconds({
durationSeconds: req.durationSeconds,
min: 2,
max: 10
}) === "number" ? "extend" : "edit";
}
function prepareCreateRequest(req) {
validateXaiVideo15Request(req);
const inputImages = req.inputImages ?? [];
const hasReferenceImages = inputImages.some(isReferenceImage);
if (hasReferenceImages && !inputImages.every(isReferenceImage)) throw new Error("xAI reference-image video generation requires every image role to be reference_image.");
if (!hasReferenceImages && inputImages.length > 1) throw new Error("xAI image-to-video generation supports at most one first-frame image.");
if (hasReferenceImages && inputImages.length > 7) throw new Error("xAI reference-image video generation supports at most 7 reference images.");
if ((req.inputVideos?.length ?? 0) > 1) throw new Error("xAI video generation supports at most one input video.");
if ((req.inputImages?.length ?? 0) > 0 && (req.inputVideos?.length ?? 0) > 0) throw new Error("xAI video generation does not support image and video inputs together.");
const mode = resolveXaiVideoMode(req);
const body = {
model: normalizeOptionalString(req.model) ?? "grok-imagine-video",
prompt: req.prompt
};
if (mode === "generate" || mode === "referenceToVideo") {
const isVideo15 = mode === "generate" && isXaiVideo15Model(req.model);
const inputImage = mode === "generate" ? inputImages[0] : void 0;
const imageUrl = inputImage ? resolveImageUrl(inputImage) : void 0;
if (mode === "referenceToVideo") body.reference_images = inputImages.map((image) => ({ url: resolveImageUrl(image) }));
else if (imageUrl) body.image = { url: imageUrl };
body.duration = resolveDurationSeconds({
durationSeconds: req.durationSeconds,
min: 1,
max: mode === "generate" ? 15 : 10
}) ?? XAI_VIDEO_DEFAULT_DURATION_SECONDS;
const aspectRatio = resolveAspectRatio(req.aspectRatio);
if (aspectRatio || !imageUrl) body.aspect_ratio = aspectRatio ?? XAI_VIDEO_DEFAULT_ASPECT_RATIO;
body.resolution = resolveResolution(req.resolution, { allow1080p: isVideo15 }) ?? XAI_VIDEO_DEFAULT_RESOLUTION;
return {
body,
mode,
endpoint: "/videos/generations"
};
}
body.video = { url: resolveInputVideoUrl(req.inputVideos?.[0]) };
if (mode === "extend") {
const duration = resolveDurationSeconds({
durationSeconds: req.durationSeconds,
min: 2,
max: 10
});
if (typeof duration === "number") body.duration = duration;
}
return {
body,
mode,
endpoint: mode === "edit" ? "/videos/edits" : "/videos/extensions"
};
}
function buildXaiVideoGenerationProvider() {
return {
...createXaiVideoGenerationProviderMetadata(),
async generateVideo(req) {
const { body, endpoint, mode } = prepareCreateRequest(req);
const auth = await resolveApiKeyForProvider({
provider: "xai",
cfg: req.cfg,
agentDir: req.agentDir,
store: req.authStore
});
if (!auth.apiKey) throw new Error("xAI API key missing");
const fetchFn = fetch;
const deadline = createProviderOperationDeadline({
timeoutMs: req.timeoutMs,
label: "xAI video generation"
});
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({
baseUrl: normalizeOptionalString(req.cfg?.models?.providers?.xai?.baseUrl) ?? "https://api.x.ai/v1",
defaultBaseUrl: DEFAULT_XAI_VIDEO_BASE_URL,
defaultHeaders: {
Authorization: `Bearer ${auth.apiKey}`,
"Content-Type": "application/json"
},
request: sanitizeConfiguredModelProviderRequest(req.cfg?.models?.providers?.xai?.request),
provider: "xai",
capability: "video",
transport: "http"
});
const submitHeaders = new Headers(headers);
submitHeaders.set("x-idempotency-key", crypto.randomUUID());
const { response, release } = await postJsonRequest({
url: `${baseUrl}${endpoint}`,
headers: submitHeaders,
body,
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS
}),
fetchFn,
allowPrivateNetwork,
dispatcherPolicy
});
try {
await assertOkOrThrowHttpError(response, "xAI video generation failed");
const submitted = await readXaiVideoJson(response);
const submitError = xaiErrorMessage(submitted);
const requestId = normalizeOptionalString(submitted.request_id);
if (!requestId) throw new Error(submitError ?? "xAI video generation response missing request_id");
const pollDeadline = createProviderOperationDeadline({
timeoutMs: resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS
}),
label: `xAI video generation request ${requestId}`
});
const completed = await pollProviderOperation({
maxAttempts: MAX_POLL_ATTEMPTS,
timeoutMessage: `xAI video generation task ${requestId} did not finish in time`,
wait: () => waitProviderOperationPollInterval({
deadline: pollDeadline,
pollIntervalMs: POLL_INTERVAL_MS
}),
read: async () => {
const { response: pollResponse, release: releasePoll } = await fetchXaiVideoResponse({
url: `${baseUrl}/videos/${requestId}`,
stage: "poll",
requestFailedMessage: "xAI video status request failed",
auditContext: "xai-video-status",
init: {
method: "GET",
headers
},
timeoutMs: createProviderOperationTimeoutResolver({
deadline: pollDeadline,
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS
}),
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS,
allowPrivateNetwork,
dispatcherPolicy,
fetchFn
});
try {
return readXaiStatusResponse(await readXaiVideoJson(pollResponse));
} finally {
await releasePoll();
}
},
isComplete: (payload) => payload.status.toLowerCase() === "done",
getFailureMessage: (payload) => {
const status = payload.status.toLowerCase();
return XAI_VIDEO_TERMINAL_FAILURE_STATUSES.has(status) ? payload.errorMessage ?? `xAI video generation ${status}` : void 0;
}
});
const videoUrl = completed.videoUrl;
if (!videoUrl) throw new Error(XAI_VIDEO_MALFORMED_RESPONSE);
return {
videos: [await downloadXaiVideo({
url: videoUrl,
timeoutMs: createProviderOperationTimeoutResolver({
deadline,
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS
}),
defaultTimeoutMs: XAI_VIDEO_DEFAULT_TIMEOUT_MS,
allowPrivateNetwork,
dispatcherPolicy,
fetchFn,
maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "video")
})],
model: normalizeOptionalString(req.model) ?? "grok-imagine-video",
metadata: {
requestId,
status: completed.status,
videoUrl,
mode
}
};
} finally {
await release();
}
}
};
}
//#endregion
export { buildXaiVideoGenerationProvider };