mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Registers `deepinfra` in the video-gen registry, reusing the DeepInfra
native /v1/inference/{model} endpoint already proven for reranking in
this codebase (same host, Bearer auth, non-OpenAI response shape).
Confirmed synchronous against DeepInfra's own docs (POST {prompt} ->
{video_url, seed, request_id, inference_status}), so no polling loop
is needed. Reuses the already-registered `deepinfra` API-key provider
credential (chat) — no new credential/OAuth flow.
To keep the frozen videoGeneration.ts file-size ratchet from growing,
the new deepinfra-video adapter lives in its own co-located module
(open-sse/handlers/videoGeneration/deepinfraHandler.ts, following the
existing googleFlowHandler.ts pattern), and the pre-existing Leonardo
handler was extracted into videoGeneration/leonardoHandler.ts (pure
code move, no behavior change) to make room.
This commit is contained in:
committed by
GitHub
parent
93e217e763
commit
b0b40a3283
1
changelog.d/features/6653-deepinfra-video-provider.md
Normal file
1
changelog.d/features/6653-deepinfra-video-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(sse): add DeepInfra as a video-generation provider via its native synchronous inference endpoint (#6653)
|
||||
@@ -193,6 +193,23 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
|
||||
models: RUNWAYML_SUPPORTED_VIDEO_MODELS,
|
||||
},
|
||||
|
||||
deepinfra: {
|
||||
id: "deepinfra",
|
||||
// Native DeepInfra inference endpoint — same host/auth already proven for reranking
|
||||
// (open-sse/config/rerankRegistry.ts). Reuses the stored deepinfra provider Bearer
|
||||
// apiKey (already registered for chat) — no separate credential flow.
|
||||
baseUrl: "https://api.deepinfra.com/v1/inference",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "deepinfra-video",
|
||||
models: [
|
||||
{ id: "Wan-AI/Wan2.2-T2V-A14B", name: "Wan 2.2 T2V A14B" },
|
||||
{ id: "Wan-AI/Wan2.2-TI2V-5B", name: "Wan 2.2 TI2V 5B" },
|
||||
{ id: "Wan-AI/Wan2.7-T2V", name: "Wan 2.7 T2V" },
|
||||
{ id: "Lightricks/LTX-2.3-Distilled", name: "LTX 2.3 Distilled" },
|
||||
],
|
||||
},
|
||||
|
||||
alibaba: {
|
||||
id: "alibaba",
|
||||
alias: "ali",
|
||||
|
||||
@@ -11,6 +11,8 @@ 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 { handleDeepinfraVideoGeneration } from "./videoGeneration/deepinfraHandler.ts";
|
||||
import { handleLeonardoVideoGeneration } from "./videoGeneration/leonardoHandler.ts";
|
||||
import { handleDashscopeVideoGeneration } from "./videoGeneration/dashscopeHandler.ts";
|
||||
import { handleNovitaVideoGeneration } from "./videoGeneration/novitaHandler.ts";
|
||||
import { handleXaiVideoGeneration } from "./videoGeneration/xaiGrokImagineHandler.ts";
|
||||
@@ -106,6 +108,17 @@ export async function handleVideoGeneration({ body, credentials, log }) {
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "deepinfra-video") {
|
||||
return handleDeepinfraVideoGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "dashscope-video") {
|
||||
return handleDashscopeVideoGeneration({
|
||||
model,
|
||||
@@ -997,109 +1010,6 @@ async function handleHaiperVideoGeneration({
|
||||
return { success: false, status: 504, error: "Haiper video generation timed out" };
|
||||
}
|
||||
|
||||
async function handleLeonardoVideoGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
modelId: "phoenix",
|
||||
prompt: body.prompt,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
num_frames: 24,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { sdGenerationJob } = await res.json();
|
||||
const genId = sdGenerationJob?.generationId;
|
||||
if (!genId) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No generation ID returned",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No generation ID returned" };
|
||||
}
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
const gen = status.generations_by_pk || status;
|
||||
if (gen.status === "COMPLETE") {
|
||||
const imgUrl = gen.generated_images?.[0]?.url;
|
||||
if (imgUrl) {
|
||||
const videoRes = await fetch(imgUrl);
|
||||
const buf = await videoRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp4" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (gen.status === "FAILED") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo video generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Leonardo video generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo video generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Leonardo video generation timed out" };
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
186
open-sse/handlers/videoGeneration/deepinfraHandler.ts
Normal file
186
open-sse/handlers/videoGeneration/deepinfraHandler.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* DeepInfra native text/image-to-video generation.
|
||||
*
|
||||
* DeepInfra's `/v1/inference/{model}` endpoint is already proven in this codebase for
|
||||
* reranking (`open-sse/handlers/rerank.ts` + `open-sse/config/rerankRegistry.ts`) — same
|
||||
* host, same Bearer auth, same non-OpenAI response shape. This reuses the stored
|
||||
* `deepinfra` provider credential (already registered for chat) — no new credential flow.
|
||||
*
|
||||
* Confirmed against DeepInfra's own docs (https://deepinfra.com/<model>/api): the call is
|
||||
* synchronous — `POST {prompt}` returns `{video_url, seed, request_id, inference_status}`
|
||||
* directly, no task/poll loop like `kie-video`/`dashscope-video`.
|
||||
*/
|
||||
|
||||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
|
||||
interface DeepinfraHandlerArgs {
|
||||
model: string;
|
||||
provider: string;
|
||||
providerConfig: { baseUrl: string };
|
||||
body: Record<string, unknown> & {
|
||||
prompt?: unknown;
|
||||
negative_prompt?: unknown;
|
||||
image?: unknown;
|
||||
image_url?: unknown;
|
||||
seed?: unknown;
|
||||
};
|
||||
credentials?: { apiKey?: string; accessToken?: string } | null;
|
||||
log?: {
|
||||
info?: (scope: string, message: string) => void;
|
||||
error?: (scope: string, message: string) => void;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface DeepinfraVideoResponse {
|
||||
video_url?: unknown;
|
||||
seed?: unknown;
|
||||
request_id?: unknown;
|
||||
inference_status?: { status?: unknown; error?: unknown } | null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
/** Builds the DeepInfra `/v1/inference/{model}` request body from the OmniRoute video body. */
|
||||
/* @testonly */ export function buildDeepinfraVideoRequestBody(
|
||||
body: DeepinfraHandlerArgs["body"]
|
||||
): Record<string, unknown> {
|
||||
const payload: Record<string, unknown> = {
|
||||
prompt: typeof body.prompt === "string" ? body.prompt : String(body.prompt ?? ""),
|
||||
};
|
||||
if (typeof body.negative_prompt === "string" && body.negative_prompt) {
|
||||
payload.negative_prompt = body.negative_prompt;
|
||||
}
|
||||
const image = body.image ?? body.image_url;
|
||||
if (typeof image === "string" && image) {
|
||||
payload.image = image;
|
||||
}
|
||||
if (typeof body.seed === "number" && Number.isFinite(body.seed)) {
|
||||
payload.seed = body.seed;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** Extracts a human-readable error message from a DeepInfra error/inference_status payload. */
|
||||
/* @testonly */ export function extractDeepinfraErrorMessage(data: unknown): string | null {
|
||||
if (!isRecord(data)) return null;
|
||||
const direct = data.error ?? data.detail ?? data.message;
|
||||
if (typeof direct === "string" && direct) return direct;
|
||||
if (isRecord(direct) && typeof direct.message === "string" && direct.message) {
|
||||
return direct.message;
|
||||
}
|
||||
const status = data.inference_status;
|
||||
if (isRecord(status) && typeof status.error === "string" && status.error) {
|
||||
return status.error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface CallLogContext {
|
||||
provider: string;
|
||||
model: string;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
function logDeepinfraCall(
|
||||
ctx: CallLogContext,
|
||||
status: number,
|
||||
extra: { error?: string; responseBody?: Record<string, unknown> }
|
||||
) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status,
|
||||
model: `${ctx.provider}/${ctx.model}`,
|
||||
provider: ctx.provider,
|
||||
duration: Date.now() - ctx.startTime,
|
||||
...extra,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function buildDeepinfraFetchError(
|
||||
ctx: CallLogContext,
|
||||
status: number,
|
||||
data: unknown,
|
||||
log?: DeepinfraHandlerArgs["log"]
|
||||
) {
|
||||
const errorMessage = extractDeepinfraErrorMessage(data) || `DeepInfra returned HTTP ${status}`;
|
||||
log?.error?.("VIDEO", `${ctx.provider} deepinfra-video error ${status}: ${errorMessage}`);
|
||||
logDeepinfraCall(ctx, status, { error: errorMessage.slice(0, 500) });
|
||||
return { success: false, status, error: errorMessage };
|
||||
}
|
||||
|
||||
function buildDeepinfraSuccess(ctx: CallLogContext, videoUrl: string) {
|
||||
logDeepinfraCall(ctx, 200, { responseBody: { videos_count: 1 } });
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ url: videoUrl, format: "mp4" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeepinfraCatchError(
|
||||
ctx: CallLogContext,
|
||||
err: unknown,
|
||||
log?: DeepinfraHandlerArgs["log"]
|
||||
) {
|
||||
const errorMessage = sanitizeErrorMessage(err) || "Video provider error";
|
||||
log?.error?.("VIDEO", `${ctx.provider} deepinfra-video error: ${errorMessage}`);
|
||||
logDeepinfraCall(ctx, 502, { error: errorMessage });
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
async function fetchDeepinfraVideo(
|
||||
baseUrl: string,
|
||||
model: string,
|
||||
token: string,
|
||||
requestBody: unknown
|
||||
) {
|
||||
const res = await fetch(`${baseUrl}/${model}`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
const data: DeepinfraVideoResponse = await res.json().catch(() => ({}));
|
||||
return { res, data };
|
||||
}
|
||||
|
||||
export async function handleDeepinfraVideoGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}: DeepinfraHandlerArgs) {
|
||||
const ctx: CallLogContext = { provider, model, startTime: Date.now() };
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!token) {
|
||||
return { success: false, status: 401, error: "DeepInfra API key is required" };
|
||||
}
|
||||
|
||||
const baseUrl = providerConfig.baseUrl.replace(/\/$/, "");
|
||||
const requestBody = buildDeepinfraVideoRequestBody(body);
|
||||
const promptPreview = String(body.prompt ?? "").slice(0, 60);
|
||||
log?.info?.("VIDEO", `${provider}/${model} (deepinfra-video) | prompt: "${promptPreview}..."`);
|
||||
|
||||
try {
|
||||
const { res, data } = await fetchDeepinfraVideo(baseUrl, model, token, requestBody);
|
||||
if (!res.ok) return buildDeepinfraFetchError(ctx, res.status, data, log);
|
||||
|
||||
const videoUrl = typeof data.video_url === "string" ? data.video_url : null;
|
||||
if (!videoUrl) {
|
||||
const errorMessage =
|
||||
extractDeepinfraErrorMessage(data) || "DeepInfra video generation did not return video_url";
|
||||
return { success: false, status: 502, error: errorMessage };
|
||||
}
|
||||
|
||||
return buildDeepinfraSuccess(ctx, videoUrl);
|
||||
} catch (err: unknown) {
|
||||
return buildDeepinfraCatchError(ctx, err, log);
|
||||
}
|
||||
}
|
||||
116
open-sse/handlers/videoGeneration/leonardoHandler.ts
Normal file
116
open-sse/handlers/videoGeneration/leonardoHandler.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Leonardo AI (Phoenix) video generation: submit → poll → fetch output.
|
||||
*
|
||||
* Extracted out of the frozen `videoGeneration.ts` god-file (unchanged behavior) to make
|
||||
* room for the new DeepInfra video adapter without pushing the file-size ratchet over its
|
||||
* baseline — mirrors the existing `googleFlowHandler.ts` extraction.
|
||||
*/
|
||||
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function handleLeonardoVideoGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const token = credentials?.apiKey || "";
|
||||
const res = await fetch(providerConfig.baseUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
modelId: "phoenix",
|
||||
prompt: body.prompt,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
num_frames: 24,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: res.status,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: errorText.slice(0, 500),
|
||||
}).catch(() => {});
|
||||
return { success: false, status: res.status, error: errorText };
|
||||
}
|
||||
const { sdGenerationJob } = await res.json();
|
||||
const genId = sdGenerationJob?.generationId;
|
||||
if (!genId) {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "No generation ID returned",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "No generation ID returned" };
|
||||
}
|
||||
const deadline = Date.now() + 300000;
|
||||
while (Date.now() < deadline) {
|
||||
await sleep(5000);
|
||||
const statusRes = await fetch(`${providerConfig.baseUrl}/${genId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const status = await statusRes.json();
|
||||
const gen = status.generations_by_pk || status;
|
||||
if (gen.status === "COMPLETE") {
|
||||
const imgUrl = gen.generated_images?.[0]?.url;
|
||||
if (imgUrl) {
|
||||
const videoRes = await fetch(imgUrl);
|
||||
const buf = await videoRes.arrayBuffer();
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 200,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
}).catch(() => {});
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
data: [{ b64_json: Buffer.from(buf).toString("base64"), format: "mp4" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (gen.status === "FAILED") {
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 502,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo video generation failed",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 502, error: "Leonardo video generation failed" };
|
||||
}
|
||||
}
|
||||
saveCallLog({
|
||||
method: "POST",
|
||||
path: "/v1/videos/generations",
|
||||
status: 504,
|
||||
model: `${provider}/${model}`,
|
||||
provider,
|
||||
duration: Date.now() - startTime,
|
||||
error: "Leonardo video generation timed out",
|
||||
}).catch(() => {});
|
||||
return { success: false, status: 504, error: "Leonardo video generation timed out" };
|
||||
}
|
||||
196
tests/unit/video-deepinfra-6653.test.ts
Normal file
196
tests/unit/video-deepinfra-6653.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
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-deepinfra-"));
|
||||
|
||||
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
|
||||
const { VIDEO_PROVIDERS, parseVideoModel } = await import(
|
||||
"../../open-sse/config/videoRegistry.ts"
|
||||
);
|
||||
const {
|
||||
buildDeepinfraVideoRequestBody,
|
||||
extractDeepinfraErrorMessage,
|
||||
} = await import("../../open-sse/handlers/videoGeneration/deepinfraHandler.ts");
|
||||
|
||||
const INFERENCE_URL = "https://api.deepinfra.com/v1/inference/Wan-AI/Wan2.2-T2V-A14B";
|
||||
|
||||
function jsonResponse(body, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
test("VIDEO_PROVIDERS exposes the deepinfra deepinfra-video entry", () => {
|
||||
assert.ok(VIDEO_PROVIDERS.deepinfra, "deepinfra video provider is registered");
|
||||
assert.equal(VIDEO_PROVIDERS.deepinfra.format, "deepinfra-video");
|
||||
assert.equal(VIDEO_PROVIDERS.deepinfra.authType, "apikey");
|
||||
assert.equal(VIDEO_PROVIDERS.deepinfra.authHeader, "bearer");
|
||||
assert.equal(VIDEO_PROVIDERS.deepinfra.baseUrl, "https://api.deepinfra.com/v1/inference");
|
||||
assert.ok(
|
||||
VIDEO_PROVIDERS.deepinfra.models.some((m) => m.id === "Wan-AI/Wan2.2-T2V-A14B"),
|
||||
"Wan 2.2 T2V A14B is listed"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseVideoModel resolves provider/model strings whose model id itself contains a slash", () => {
|
||||
const parsed = parseVideoModel("deepinfra/Wan-AI/Wan2.2-T2V-A14B");
|
||||
assert.equal(parsed.provider, "deepinfra");
|
||||
assert.equal(parsed.model, "Wan-AI/Wan2.2-T2V-A14B");
|
||||
});
|
||||
|
||||
test("buildDeepinfraVideoRequestBody carries prompt/negative_prompt/image/seed", () => {
|
||||
const body = buildDeepinfraVideoRequestBody({
|
||||
prompt: "a neon city in the rain",
|
||||
negative_prompt: "blurry",
|
||||
image: "https://example.com/frame.png",
|
||||
seed: 42,
|
||||
});
|
||||
assert.deepEqual(body, {
|
||||
prompt: "a neon city in the rain",
|
||||
negative_prompt: "blurry",
|
||||
image: "https://example.com/frame.png",
|
||||
seed: 42,
|
||||
});
|
||||
});
|
||||
|
||||
test("buildDeepinfraVideoRequestBody omits optional fields when absent", () => {
|
||||
const body = buildDeepinfraVideoRequestBody({ prompt: "just a prompt" });
|
||||
assert.deepEqual(body, { prompt: "just a prompt" });
|
||||
});
|
||||
|
||||
test("extractDeepinfraErrorMessage reads string error/detail/message and inference_status.error", () => {
|
||||
assert.equal(extractDeepinfraErrorMessage({ error: "bad request" }), "bad request");
|
||||
assert.equal(extractDeepinfraErrorMessage({ detail: "invalid model" }), "invalid model");
|
||||
assert.equal(
|
||||
extractDeepinfraErrorMessage({ error: { message: "nested" } }),
|
||||
"nested"
|
||||
);
|
||||
assert.equal(
|
||||
extractDeepinfraErrorMessage({ inference_status: { error: "queue timeout" } }),
|
||||
"queue timeout"
|
||||
);
|
||||
assert.equal(extractDeepinfraErrorMessage({}), null);
|
||||
assert.equal(extractDeepinfraErrorMessage(null), null);
|
||||
});
|
||||
|
||||
test("handleVideoGeneration builds a synchronous DeepInfra request and returns the mp4 url", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedRequest;
|
||||
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
capturedRequest = {
|
||||
url: String(url),
|
||||
headers: options.headers,
|
||||
body: JSON.parse(String(options.body || "{}")),
|
||||
};
|
||||
return jsonResponse({
|
||||
video_url: "https://deepinfra-cdn.example.com/wan-out.mp4",
|
||||
seed: 7,
|
||||
request_id: "req-1",
|
||||
inference_status: { status: "succeeded" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleVideoGeneration({
|
||||
body: {
|
||||
model: "deepinfra/Wan-AI/Wan2.2-T2V-A14B",
|
||||
prompt: "a neon city in the rain",
|
||||
negative_prompt: "blurry",
|
||||
},
|
||||
credentials: { apiKey: "deepinfra-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(capturedRequest.url, INFERENCE_URL);
|
||||
assert.equal(capturedRequest.headers["Authorization"], "Bearer deepinfra-key");
|
||||
assert.equal(capturedRequest.body.prompt, "a neon city in the rain");
|
||||
assert.equal(capturedRequest.body.negative_prompt, "blurry");
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].url, "https://deepinfra-cdn.example.com/wan-out.mp4");
|
||||
assert.equal(result.data.data[0].format, "mp4");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleVideoGeneration rejects DeepInfra video requests without credentials", async () => {
|
||||
const result = await handleVideoGeneration({
|
||||
body: { model: "deepinfra/Wan-AI/Wan2.2-T2V-A14B", prompt: "x" },
|
||||
credentials: null,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.match(result.error, /DeepInfra API key is required/);
|
||||
});
|
||||
|
||||
test("handleVideoGeneration surfaces upstream HTTP errors without leaking a stack trace", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
jsonResponse({ error: "Invalid API key" }, 401);
|
||||
|
||||
try {
|
||||
const result = await handleVideoGeneration({
|
||||
body: { model: "deepinfra/Wan-AI/Wan2.2-T2V-A14B", prompt: "x" },
|
||||
credentials: { apiKey: "bad-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
assert.ok(!result.error.includes("at /"), "error must not leak a stack trace");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleVideoGeneration returns 502 when DeepInfra succeeds without a video_url", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => jsonResponse({ seed: 1, request_id: "req-2" });
|
||||
|
||||
try {
|
||||
const result = await handleVideoGeneration({
|
||||
body: { model: "deepinfra/Wan-AI/Wan2.2-T2V-A14B", prompt: "x" },
|
||||
credentials: { apiKey: "deepinfra-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.match(result.error, /did not return video_url/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleVideoGeneration sanitizes network-level failures via sanitizeErrorMessage", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error(
|
||||
"fetch failed\n at Object.fetch (/home/user/omniroute/node_modules/undici/lib/x.js:1:1)"
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleVideoGeneration({
|
||||
body: { model: "deepinfra/Wan-AI/Wan2.2-T2V-A14B", prompt: "x" },
|
||||
credentials: { apiKey: "deepinfra-key" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.ok(!result.error.includes("at Object.fetch"), "error must not leak a stack trace");
|
||||
assert.ok(!result.error.includes("/home/user"), "error must not leak an absolute path");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user