feat(sse): add native xAI Grok Imagine video generation provider

OmniRoute's /v1/videos surface already supported 10 provider formats
(vertex-veo, google-flow, comfyui, sdwebui-video, kie-video, runwayml,
haiper-video, veoaifree-web, leonardo-video, dashscope-video), but xAI
had no native entry — Grok Imagine was only reachable indirectly through
the kie proxy market (kie's "grok-imagine/text-to-video" models), which
requires a separate kie.ai account and bills through kie.

This registers xai as a first-class video provider that talks to
api.x.ai/v1/videos directly, reusing the stored xai Bearer apiKey that
the existing image-generation "xai" entry in imageRegistry.ts already
uses — no new credential flow. The new xai-video handler format mirrors
the DashScope create+poll shape, adapted to xAI's request_id / status
("pending" | "processing" | "done" | "failed") job model.

User-visible effect: `xai/grok-imagine-video` works on
POST /v1/videos/generations against a user's own xAI key.

Co-authored-by: ann <daohuyentfqn2l@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2593
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-15 01:50:50 -03:00
parent af0c72fba5
commit 1579f960be
3 changed files with 391 additions and 0 deletions

View File

@@ -205,6 +205,19 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
format: "dashscope-video",
models: [{ id: "wan2.7-t2v", name: "Wan 2.7 T2V" }],
},
xai: {
id: "xai",
// xAI Grok Imagine async video-generation API. Reuses the stored xai
// provider Bearer apiKey (same credential the image-generation "xai"
// entry in imageRegistry.ts already uses) — no separate credential flow.
baseUrl: "https://api.x.ai/v1/videos",
statusUrl: "https://api.x.ai/v1/videos",
authType: "apikey",
authHeader: "bearer",
format: "xai-video",
models: [{ id: "grok-imagine-video", name: "Grok Imagine Video" }],
},
};
/**

View File

@@ -112,6 +112,10 @@ export async function handleVideoGeneration({ body, credentials, log }) {
});
}
if (providerConfig.format === "xai-video") {
return handleXaiVideoGeneration({ model, provider, providerConfig, body, credentials, log });
}
return {
success: false,
status: 400,
@@ -271,6 +275,144 @@ 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,236 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-video-xai-"));
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
const { VIDEO_PROVIDERS } = await import("../../open-sse/config/videoRegistry.ts");
// Makes poll-interval waits resolve instantly so tests don't sleep.
function immediateTimeout(callback, _ms, ...args) {
if (typeof callback === "function") callback(...args);
return 0;
}
const CREATE_URL = "https://api.x.ai/v1/videos/generations";
const POLL_URL_PREFIX = "https://api.x.ai/v1/videos/";
function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
test("VIDEO_PROVIDERS exposes the xai grok-imagine-video entry", () => {
assert.ok(VIDEO_PROVIDERS.xai, "xai video provider is registered");
assert.equal(VIDEO_PROVIDERS.xai.format, "xai-video");
assert.ok(
VIDEO_PROVIDERS.xai.models.some((m) => m.id === "grok-imagine-video"),
"grok-imagine-video is listed"
);
});
test("handleVideoGeneration creates + polls an xAI Grok Imagine video job and returns mp4 URL", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
let createRequest;
let pollRequestCount = 0;
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = async (url, options = {}) => {
const stringUrl = String(url);
if (stringUrl === CREATE_URL) {
createRequest = {
url: stringUrl,
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return jsonResponse({ request_id: "xai-req-1", status: "pending" });
}
if (stringUrl === `${POLL_URL_PREFIX}xai-req-1`) {
pollRequestCount += 1;
if (pollRequestCount === 1) {
return jsonResponse({ request_id: "xai-req-1", status: "processing", progress: 40 });
}
return jsonResponse({
request_id: "xai-req-1",
status: "done",
progress: 100,
video: { url: "https://videos.x.ai/xai-req-1.mp4" },
});
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
try {
const result = await handleVideoGeneration({
body: {
model: "xai/grok-imagine-video",
prompt: "a cinematic tracking shot through a neon city at night",
duration: 6,
},
credentials: { apiKey: "xai-key" },
log: null,
});
// Create request shape
assert.equal(createRequest.headers["Authorization"], "Bearer xai-key");
assert.equal(createRequest.body.model, "grok-imagine-video");
assert.equal(
createRequest.body.prompt,
"a cinematic tracking shot through a neon city at night"
);
assert.equal(createRequest.body.duration, 6);
// Polled at least once past "processing" before terminal "done"
assert.ok(pollRequestCount >= 2);
// Response shape
assert.equal(result.success, true);
assert.equal(result.data.data[0].url, "https://videos.x.ai/xai-req-1.mp4");
assert.equal(result.data.data[0].format, "mp4");
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("handleVideoGeneration rejects xAI video requests without credentials", async () => {
const result = await handleVideoGeneration({
body: { model: "xai/grok-imagine-video", prompt: "x" },
credentials: null,
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 401);
assert.match(result.error, /xAI API key is required/);
});
test("handleVideoGeneration surfaces a 502 when xAI returns no request_id", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
jsonResponse({ error: { message: "Invalid API key" } }, 401);
try {
const result = await handleVideoGeneration({
body: { model: "xai/grok-imagine-video", prompt: "x" },
credentials: { apiKey: "bad-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 502);
assert.equal(result.error, "Invalid API key");
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleVideoGeneration returns 502 when the xAI job status is failed", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = async (url) => {
const stringUrl = String(url);
if (stringUrl === CREATE_URL) {
return jsonResponse({ request_id: "xai-fail", status: "pending" });
}
if (stringUrl === `${POLL_URL_PREFIX}xai-fail`) {
return jsonResponse({
request_id: "xai-fail",
status: "failed",
error: "content policy violation",
});
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
try {
const result = await handleVideoGeneration({
body: { model: "xai/grok-imagine-video", prompt: "x" },
credentials: { apiKey: "xai-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 502);
assert.equal(result.error, "content policy violation");
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("handleVideoGeneration returns 504 when the xAI job never completes", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const originalNow = Date.now;
globalThis.setTimeout = immediateTimeout;
let nowCalls = 0;
Date.now = () => {
nowCalls += 1;
return nowCalls === 1 ? 1000 : nowCalls === 2 ? 2000 : 1_000_000;
};
globalThis.fetch = async (url) => {
const stringUrl = String(url);
if (stringUrl === CREATE_URL) {
return jsonResponse({ request_id: "xai-stuck", status: "pending" });
}
if (stringUrl === `${POLL_URL_PREFIX}xai-stuck`) {
return jsonResponse({ request_id: "xai-stuck", status: "processing", progress: 10 });
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
try {
const result = await handleVideoGeneration({
body: {
model: "xai/grok-imagine-video",
prompt: "x",
timeout_ms: 5000,
poll_interval_ms: 100,
},
credentials: { apiKey: "xai-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 504);
assert.match(result.error, /timed out/);
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
Date.now = originalNow;
}
});
test("handleVideoGeneration never leaks a stack trace in xAI video error responses", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
throw new Error("connect ECONNREFUSED 127.0.0.1:443\n at TCPConnectWrap.afterConnect");
};
try {
const result = await handleVideoGeneration({
body: { model: "xai/grok-imagine-video", prompt: "x" },
credentials: { apiKey: "xai-key" },
log: null,
});
assert.equal(result.success, false);
assert.ok(!String(result.error).includes("at TCPConnectWrap"));
} finally {
globalThis.fetch = originalFetch;
}
});