fix(sse): extract xAI Grok Imagine video handler to fix file-size ratchet

videoGeneration.ts grew to 1407 lines (frozen cap 1265) after adding the
Grok Imagine handler. Extract handleXaiVideoGeneration into a co-located
module (open-sse/handlers/videoGeneration/xaiGrokImagineHandler.ts),
following the googleFlowHandler.ts precedent — same pattern already used
for the Google Flow video handler. File now sits at 1261 lines, under cap.
No behavior change; existing tests (video-xai-grok-imagine.test.ts) cover
the handler through the public handleVideoGeneration() entry point and
pass unmodified.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-15 11:22:43 -03:00
parent 050767cd10
commit 46345f1cce
2 changed files with 153 additions and 150 deletions

View File

@@ -1,24 +1,17 @@
/**
* Video Generation Handler
*
* Handles POST /v1/videos/generations requests.
* Proxies to upstream video generation providers.
*
* Supported provider formats:
* - ComfyUI: submit AnimateDiff/SVD workflow → poll → fetch video
* - SD WebUI: POST to AnimateDiff extension endpoint
*
* Response format (OpenAI-like):
* {
* "created": 1234567890,
* "data": [{ "b64_json": "...", "format": "mp4" }]
* }
* Handles POST /v1/videos/generations requests. Proxies to upstream video
* generation providers (ComfyUI AnimateDiff/SVD, SD WebUI AnimateDiff, and
* more — see the per-format handlers below). Response format (OpenAI-like):
* { "created": 1234567890, "data": [{ "b64_json": "...", "format": "mp4" }] }
*/
import { getVideoProvider, parseVideoModel } from "../config/videoRegistry.ts";
import { kieExecutor } from "../executors/kie.ts";
import { vertexGenerateVideo } from "../executors/vertexMedia.ts";
import { handleGoogleFlowVideoGeneration } from "./videoGeneration/googleFlowHandler.ts";
import { handleXaiVideoGeneration } from "./videoGeneration/xaiGrokImagineHandler.ts";
import { getExecutor } from "../executors/index.ts";
import { isJsonObject, parseKieResultJson } from "../utils/kieTask.ts";
import {
@@ -275,144 +268,6 @@ async function handleDashscopeVideoGeneration({
}
}
/**
* xAI Grok Imagine video generation: create async job → poll → MP4.
* Reuses the stored xai provider Bearer apiKey (same credential the
* image-generation "xai" entry in imageRegistry.ts already uses) — no
* separate credential flow. Mirrors the DashScope create+poll shape above,
* adapted to xAI's request_id / status ("pending"|"processing"|"done"|"failed")
* job shape (https://docs.x.ai/developers/rest-api-reference/inference/videos).
*/
async function handleXaiVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: { baseUrl: string; statusUrl?: string };
body: Record<string, unknown> & {
prompt?: unknown;
image?: unknown;
duration?: unknown;
aspect_ratio?: unknown;
resolution?: unknown;
timeout_ms?: unknown;
poll_interval_ms?: unknown;
};
credentials?: { apiKey?: string; accessToken?: string } | null;
log?: {
info: (scope: string, message: string) => void;
error: (scope: string, message: string) => void;
} | null;
}) {
const startTime = Date.now();
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
const pollIntervalMs = Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500;
const token = credentials?.apiKey || credentials?.accessToken;
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
const statusUrl = (providerConfig.statusUrl || baseUrl).replace(/\/$/, "");
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (!token) {
return { success: false, status: 401, error: "xAI API key is required" };
}
const payload: Record<string, unknown> = { model, prompt };
if (typeof body.image === "string") payload.image = body.image;
if (body.duration != null) payload.duration = Number(body.duration);
if (typeof body.aspect_ratio === "string") payload.aspect_ratio = body.aspect_ratio;
if (typeof body.resolution === "string") payload.resolution = body.resolution;
if (log) {
log.info("VIDEO", `${provider}/${model} (xai-video) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
// Step 1: create async job
const createRes = await fetch(`${baseUrl}/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const createData = await createRes.json().catch(() => ({}));
const requestId = createData?.request_id;
if (!requestId) {
const errorMessage =
createData?.error?.message ||
createData?.message ||
"xAI video generation did not return request_id";
if (log) {
log.error("VIDEO", `xAI createJob failed: ${JSON.stringify(createData)}`);
}
return { success: false, status: 502, error: String(errorMessage) };
}
// Step 2: poll statusUrl/{request_id} until terminal
const deadline = startTime + timeoutMs;
let lastStatus = "pending";
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
const pollRes = await fetch(`${statusUrl}/${requestId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const pollData = await pollRes.json().catch(() => ({}));
lastStatus = pollData?.status || "pending";
if (lastStatus === "done") {
const videoUrl = pollData?.video?.url;
if (!videoUrl) {
return {
success: false,
status: 502,
error: "xAI video job done but no video.url",
};
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { videos_count: 1 },
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ url: videoUrl, format: "mp4" }],
},
};
}
if (lastStatus === "failed") {
const errorMessage = pollData?.error || "xAI video job failed";
return { success: false, status: 502, error: String(errorMessage) };
}
// pending / processing → keep polling
}
return {
success: false,
status: 504,
error: `xAI video job ${requestId} timed out (status: ${lastStatus})`,
};
} catch (err: unknown) {
return {
success: false,
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
error: sanitizeErrorMessage(err) || "Video provider error",
};
}
}
// Map OmniRoute size/aspect_ratio → Alibaba DashScope "WxH" (1280*720).
// Accepts "1280*720", "1280x720", or a ratio "16:9". Returns undefined if unparseable
// (then omitted from the payload so DashScope applies its own default).

View File

@@ -0,0 +1,148 @@
/**
* xAI Grok Imagine video generation: create async job → poll → MP4.
* Reuses the stored xai provider Bearer apiKey (same credential the
* image-generation "xai" entry in imageRegistry.ts already uses) — no
* separate credential flow. Mirrors the DashScope create+poll shape in
* videoGeneration.ts, adapted to xAI's request_id / status
* ("pending"|"processing"|"done"|"failed") job shape
* (https://docs.x.ai/developers/rest-api-reference/inference/videos).
*/
import { isJsonObject } from "../../utils/kieTask.ts";
import { saveCallLog } from "@/lib/usageDb";
import { sanitizeErrorMessage } from "../../utils/error.ts";
interface XaiVideoBody {
prompt?: unknown;
image?: unknown;
duration?: unknown;
aspect_ratio?: unknown;
resolution?: unknown;
timeout_ms?: unknown;
poll_interval_ms?: unknown;
[key: string]: unknown;
}
interface XaiVideoLog {
info: (scope: string, message: string) => void;
error: (scope: string, message: string) => void;
}
export async function handleXaiVideoGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
}: {
model: string;
provider: string;
providerConfig: { baseUrl: string; statusUrl?: string };
body: XaiVideoBody;
credentials?: { apiKey?: string; accessToken?: string } | null;
log?: XaiVideoLog | null;
}) {
const startTime = Date.now();
const timeoutMs = Number(body.timeout_ms) > 0 ? Number(body.timeout_ms) : 300000;
const pollIntervalMs = Number(body.poll_interval_ms) > 0 ? Number(body.poll_interval_ms) : 2500;
const token = credentials?.apiKey || credentials?.accessToken;
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
const statusUrl = (providerConfig.statusUrl || baseUrl).replace(/\/$/, "");
const prompt = typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? "");
if (!token) {
return { success: false, status: 401, error: "xAI API key is required" };
}
const payload: Record<string, unknown> = { model, prompt };
if (typeof body.image === "string") payload.image = body.image;
if (body.duration != null) payload.duration = Number(body.duration);
if (typeof body.aspect_ratio === "string") payload.aspect_ratio = body.aspect_ratio;
if (typeof body.resolution === "string") payload.resolution = body.resolution;
if (log) {
log.info("VIDEO", `${provider}/${model} (xai-video) | prompt: "${prompt.slice(0, 60)}..."`);
}
try {
// Step 1: create async job
const createRes = await fetch(`${baseUrl}/generations`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const createData = await createRes.json().catch(() => ({}));
const requestId = createData?.request_id;
if (!requestId) {
const errorMessage =
createData?.error?.message ||
createData?.message ||
"xAI video generation did not return request_id";
if (log) {
log.error("VIDEO", `xAI createJob failed: ${JSON.stringify(createData)}`);
}
return { success: false, status: 502, error: String(errorMessage) };
}
// Step 2: poll statusUrl/{request_id} until terminal
const deadline = startTime + timeoutMs;
let lastStatus = "pending";
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
const pollRes = await fetch(`${statusUrl}/${requestId}`, {
headers: { Authorization: `Bearer ${token}` },
});
const pollData = await pollRes.json().catch(() => ({}));
lastStatus = pollData?.status || "pending";
if (lastStatus === "done") {
const videoUrl = pollData?.video?.url;
if (!videoUrl) {
return {
success: false,
status: 502,
error: "xAI video job done but no video.url",
};
}
saveCallLog({
method: "POST",
path: "/v1/videos/generations",
status: 200,
model: `${provider}/${model}`,
provider,
duration: Date.now() - startTime,
responseBody: { videos_count: 1 },
}).catch(() => {});
return {
success: true,
data: {
created: Math.floor(Date.now() / 1000),
data: [{ url: videoUrl, format: "mp4" }],
},
};
}
if (lastStatus === "failed") {
const errorMessage = pollData?.error || "xAI video job failed";
return { success: false, status: 502, error: String(errorMessage) };
}
// pending / processing → keep polling
}
return {
success: false,
status: 504,
error: `xAI video job ${requestId} timed out (status: ${lastStatus})`,
};
} catch (err: unknown) {
return {
success: false,
status: isJsonObject(err) && Number.isFinite(Number(err.status)) ? Number(err.status) : 502,
error: sanitizeErrorMessage(err) || "Video provider error",
};
}
}