mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 17:44:06 +00:00
fix(video): bound remaining provider downloads
This commit is contained in:
@@ -6,7 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Providers: bound generated video downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, and FAL providers, and bound generated FAL image downloads.
|
||||
- Providers: bound generated video downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, and Google providers, and bound generated FAL image downloads.
|
||||
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
|
||||
|
||||
## 2026.5.28
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -174,6 +172,38 @@ describe("google video generation provider", () => {
|
||||
expect(httpOptions).not.toHaveProperty("apiVersion");
|
||||
});
|
||||
|
||||
it("rejects inline video bytes that exceed the configured media cap", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{
|
||||
video: {
|
||||
videoBytes: Buffer.from("too-large").toString("base64"),
|
||||
mimeType: "video/mp4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "A tiny robot watering a windowsill garden",
|
||||
cfg: { agents: { defaults: { mediaMaxMb: 0.000001 } } },
|
||||
durationSeconds: 3,
|
||||
}),
|
||||
).rejects.toThrow("Google generated video download exceeds 1 bytes");
|
||||
});
|
||||
|
||||
it("strips /v1beta suffix from configured baseUrl before passing to GoogleGenAI SDK", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
@@ -256,7 +286,7 @@ describe("google video generation provider", () => {
|
||||
expect(result.videos[0]?.mimeType).toBe("video/mp4");
|
||||
});
|
||||
|
||||
it("stages SDK file downloads before finalizing generated video bytes", async () => {
|
||||
it("rejects direct video uri downloads that exceed the configured media cap", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
@@ -268,16 +298,64 @@ describe("google video generation provider", () => {
|
||||
generatedVideos: [
|
||||
{
|
||||
video: {
|
||||
name: "files/generated-video",
|
||||
uri: "https://generativelanguage.googleapis.com/v1beta/files/generated-video:download?alt=media",
|
||||
mimeType: "video/mp4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
downloadMock.mockImplementation(async ({ downloadPath }: { downloadPath: string }) => {
|
||||
await writeFile(downloadPath, "sdk-video");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response("too-large", {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { "content-type": "video/mp4" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "A tiny robot watering a windowsill garden",
|
||||
cfg: { agents: { defaults: { mediaMaxMb: 0.000001 } } },
|
||||
durationSeconds: 3,
|
||||
}),
|
||||
).rejects.toThrow("Google generated video download exceeds 1 bytes");
|
||||
});
|
||||
|
||||
it("downloads SDK file handles through the bounded REST media endpoint", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{
|
||||
video: {
|
||||
uri: "files/generated-video",
|
||||
mimeType: "video/mp4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const fetchMock = vi.fn(async () => {
|
||||
return new Response("sdk-video", {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { "content-type": "video/mp4" },
|
||||
});
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
const result = await provider.generateVideo({
|
||||
@@ -288,14 +366,58 @@ describe("google video generation provider", () => {
|
||||
durationSeconds: 3,
|
||||
});
|
||||
|
||||
const [{ downloadPath }] = downloadMock.mock.calls[0] ?? [{}];
|
||||
const downloadBaseName = path.basename(String(downloadPath));
|
||||
expect(downloadBaseName).toContain("video-1.mp4");
|
||||
expect(downloadBaseName).toMatch(/\.part$/);
|
||||
expect(fetchInputUrl(fetchMock, 0)).toBe(
|
||||
"https://generativelanguage.googleapis.com/v1beta/files/generated-video:download?alt=media&key=google-key",
|
||||
);
|
||||
expect(downloadMock).not.toHaveBeenCalled();
|
||||
expect(result.videos[0]?.buffer).toEqual(Buffer.from("sdk-video"));
|
||||
expect(result.videos[0]?.fileName).toBe("video-1.mp4");
|
||||
});
|
||||
|
||||
it("rejects SDK file-handle downloads that exceed the configured media cap", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
source: "env",
|
||||
mode: "api-key",
|
||||
});
|
||||
generateVideosMock.mockResolvedValue({
|
||||
done: true,
|
||||
response: {
|
||||
generatedVideos: [
|
||||
{
|
||||
video: {
|
||||
uri: "files/generated-video",
|
||||
mimeType: "video/mp4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response("too-large", {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: { "content-type": "video/mp4" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const provider = buildGoogleVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "google",
|
||||
model: "veo-3.1-fast-generate-preview",
|
||||
prompt: "A tiny robot watering a windowsill garden",
|
||||
cfg: { agents: { defaults: { mediaMaxMb: 0.000001 } } },
|
||||
durationSeconds: 3,
|
||||
}),
|
||||
).rejects.toThrow("Google generated video download exceeds 1 bytes");
|
||||
expect(downloadMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to REST predictLongRunning when text-only SDK video generation returns 404", async () => {
|
||||
vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
|
||||
apiKey: "google-key",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import {
|
||||
createProviderOperationDeadline,
|
||||
@@ -7,10 +5,9 @@ import {
|
||||
resolveProviderOperationTimeoutMs,
|
||||
waitProviderOperationPollInterval,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
VideoGenerationProvider,
|
||||
@@ -29,6 +26,7 @@ import { createGoogleGenAI, type GoogleGenAIClient } from "./google-genai-runtim
|
||||
const DEFAULT_TIMEOUT_MS = 180_000;
|
||||
const POLL_INTERVAL_MS = 10_000;
|
||||
const MAX_POLL_ATTEMPTS = 120;
|
||||
const DEFAULT_GENERATED_VIDEO_MAX_BYTES = 16 * 1024 * 1024;
|
||||
const GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE =
|
||||
"Google video generation response missing generated videos";
|
||||
|
||||
@@ -37,6 +35,20 @@ function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): strin
|
||||
return configured ? resolveGoogleGenerativeAiApiOrigin(configured) : undefined;
|
||||
}
|
||||
|
||||
function resolveGeneratedVideoMaxBytes(req: VideoGenerationRequest): number {
|
||||
const configured = req.cfg.agents?.defaults?.mediaMaxMb;
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||
return Math.floor(configured * 1024 * 1024);
|
||||
}
|
||||
return DEFAULT_GENERATED_VIDEO_MAX_BYTES;
|
||||
}
|
||||
|
||||
function assertGeneratedVideoBufferWithinLimit(buffer: Buffer, maxBytes: number): void {
|
||||
if (buffer.length > maxBytes) {
|
||||
throw new Error(`Google generated video download exceeds ${maxBytes} bytes`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGoogleVideoRestBaseUrl(configuredBaseUrl?: string): string {
|
||||
return `${configuredBaseUrl ?? "https://generativelanguage.googleapis.com"}/v1beta`;
|
||||
}
|
||||
@@ -148,42 +160,6 @@ function resolveInputVideo(req: VideoGenerationRequest) {
|
||||
};
|
||||
}
|
||||
|
||||
async function downloadGeneratedVideo(params: {
|
||||
client: GoogleGenAIClient;
|
||||
file: unknown;
|
||||
index: number;
|
||||
}): Promise<GeneratedVideoAsset> {
|
||||
return await withTempWorkspace(
|
||||
{ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "openclaw-google-video-" },
|
||||
async ({ dir: tempDir }) => {
|
||||
const fileName = `video-${params.index + 1}.mp4`;
|
||||
const downloadPath = path.join(tempDir, fileName);
|
||||
await writeExternalFileWithinRoot({
|
||||
rootDir: tempDir,
|
||||
path: fileName,
|
||||
write: async (downloadPath) => {
|
||||
await executeProviderOperationWithRetry({
|
||||
provider: "google",
|
||||
stage: "download",
|
||||
operation: async () => {
|
||||
await params.client.files.download({
|
||||
file: params.file as never,
|
||||
downloadPath,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const buffer = await readFile(downloadPath);
|
||||
return {
|
||||
buffer,
|
||||
mimeType: "video/mp4",
|
||||
fileName: `video-${params.index + 1}.mp4`,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGoogleGeneratedVideoDownloadUrl(params: {
|
||||
uri: string | undefined;
|
||||
apiKey: string;
|
||||
@@ -222,12 +198,31 @@ function resolveGoogleGeneratedVideoDownloadUrl(params: {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function resolveGoogleGeneratedVideoFileDownloadUrl(params: {
|
||||
file: unknown;
|
||||
apiKey: string;
|
||||
configuredBaseUrl?: string;
|
||||
}): string | undefined {
|
||||
const resource = params.file as { name?: unknown; uri?: unknown } | undefined;
|
||||
const name = normalizeOptionalString(resource?.name) ?? normalizeOptionalString(resource?.uri);
|
||||
if (!name || !/^files\/[^/?#]+$/u.test(name)) {
|
||||
return undefined;
|
||||
}
|
||||
const baseUrl = resolveGoogleVideoRestBaseUrl(params.configuredBaseUrl);
|
||||
const url = new URL(`${baseUrl}/${name}:download`);
|
||||
url.searchParams.set("alt", "media");
|
||||
url.searchParams.set("key", params.apiKey);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function downloadGeneratedVideoFromUri(params: {
|
||||
uri: string | undefined;
|
||||
apiKey: string;
|
||||
configuredBaseUrl?: string;
|
||||
mimeType?: string;
|
||||
index: number;
|
||||
maxBytes: number;
|
||||
timeoutMs: number;
|
||||
}): Promise<GeneratedVideoAsset | undefined> {
|
||||
const downloadUrl = resolveGoogleGeneratedVideoDownloadUrl({
|
||||
uri: params.uri,
|
||||
@@ -243,6 +238,7 @@ async function downloadGeneratedVideoFromUri(params: {
|
||||
operation: async () => {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: downloadUrl,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
@@ -250,7 +246,13 @@ async function downloadGeneratedVideoFromUri(params: {
|
||||
`Failed to download Google generated video: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const buffer = await readResponseWithLimit(response, params.maxBytes, {
|
||||
chunkTimeoutMs: params.timeoutMs,
|
||||
onOverflow: ({ maxBytes }) =>
|
||||
new Error(`Google generated video download exceeds ${maxBytes} bytes`),
|
||||
onIdleTimeout: ({ chunkTimeoutMs }) =>
|
||||
new Error(`Google generated video download stalled after ${chunkTimeoutMs}ms`),
|
||||
});
|
||||
return {
|
||||
buffer,
|
||||
mimeType:
|
||||
@@ -545,14 +547,17 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
|
||||
if (generatedVideos.length === 0) {
|
||||
throw new Error(GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE);
|
||||
}
|
||||
const maxVideoBytes = resolveGeneratedVideoMaxBytes(req);
|
||||
const videos = await Promise.all(
|
||||
generatedVideos.map(async (entry, index) => {
|
||||
const inline = entry.video as
|
||||
| { videoBytes?: string; uri?: string; mimeType?: string }
|
||||
| undefined;
|
||||
if (inline?.videoBytes) {
|
||||
const buffer = Buffer.from(inline.videoBytes, "base64");
|
||||
assertGeneratedVideoBufferWithinLimit(buffer, maxVideoBytes);
|
||||
return {
|
||||
buffer: Buffer.from(inline.videoBytes, "base64"),
|
||||
buffer,
|
||||
mimeType: normalizeOptionalString(inline.mimeType) || "video/mp4",
|
||||
fileName: `video-${index + 1}.mp4`,
|
||||
};
|
||||
@@ -563,6 +568,11 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
|
||||
configuredBaseUrl,
|
||||
mimeType: inline?.mimeType,
|
||||
index,
|
||||
maxBytes: maxVideoBytes,
|
||||
timeoutMs: resolveProviderOperationTimeoutMs({
|
||||
deadline,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
}),
|
||||
});
|
||||
if (directDownload) {
|
||||
return directDownload;
|
||||
@@ -570,11 +580,26 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
|
||||
if (!inline) {
|
||||
throw new Error("Google generated video missing file handle");
|
||||
}
|
||||
return await downloadGeneratedVideo({
|
||||
client,
|
||||
file: inline,
|
||||
const fileDownload = await downloadGeneratedVideoFromUri({
|
||||
uri: resolveGoogleGeneratedVideoFileDownloadUrl({
|
||||
file: inline,
|
||||
apiKey,
|
||||
configuredBaseUrl,
|
||||
}),
|
||||
apiKey,
|
||||
configuredBaseUrl,
|
||||
mimeType: inline.mimeType,
|
||||
index,
|
||||
maxBytes: maxVideoBytes,
|
||||
timeoutMs: resolveProviderOperationTimeoutMs({
|
||||
deadline,
|
||||
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
|
||||
}),
|
||||
});
|
||||
if (!fileDownload) {
|
||||
throw new Error("Google generated video missing bounded download URL");
|
||||
}
|
||||
return fileDownload;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -62,10 +62,10 @@ function releasedJson(value: unknown) {
|
||||
|
||||
function releasedVideo(params: { contentType: string; bytes: string }) {
|
||||
return {
|
||||
response: {
|
||||
headers: new Headers({ "content-type": params.contentType }),
|
||||
arrayBuffer: async () => Buffer.from(params.bytes),
|
||||
},
|
||||
response: new Response(Buffer.from(params.bytes), {
|
||||
status: 200,
|
||||
headers: { "content-type": params.contentType },
|
||||
}),
|
||||
release: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
@@ -553,6 +553,36 @@ describe("openrouter video generation provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns unsigned URL-only videos when downloads exceed the configured media cap", async () => {
|
||||
postJsonRequestMock.mockResolvedValue(
|
||||
releasedJson({
|
||||
id: "job-123",
|
||||
polling_url: "/api/v1/videos/job-123",
|
||||
status: "completed",
|
||||
unsigned_urls: ["https://cdn.openrouter.test/video.mp4"],
|
||||
}),
|
||||
);
|
||||
fetchWithTimeoutGuardedMock.mockResolvedValueOnce(
|
||||
releasedVideo({ contentType: "video/mp4", bytes: "too-large" }),
|
||||
);
|
||||
|
||||
const provider = buildOpenRouterVideoGenerationProvider();
|
||||
const result = await provider.generateVideo({
|
||||
provider: "openrouter",
|
||||
model: "google/veo-3.1",
|
||||
prompt: "A glass cube reflects a neon skyline",
|
||||
cfg: { agents: { defaults: { mediaMaxMb: 0.000001 } } } as never,
|
||||
});
|
||||
|
||||
expect(result.videos).toEqual([
|
||||
{
|
||||
url: "https://cdn.openrouter.test/video.mp4",
|
||||
mimeType: "video/mp4",
|
||||
fileName: "video-1.mp4",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects malformed numeric seed values before submitting video jobs", async () => {
|
||||
const provider = buildOpenRouterVideoGenerationProvider();
|
||||
await expect(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sanitizeConfiguredModelProviderRequest,
|
||||
waitProviderOperationPollInterval,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type {
|
||||
GeneratedVideoAsset,
|
||||
@@ -32,6 +33,7 @@ const DEFAULT_TIMEOUT_MS = 600_000;
|
||||
const DEFAULT_HTTP_TIMEOUT_MS = 60_000;
|
||||
const POLL_INTERVAL_MS = 5_000;
|
||||
const MAX_POLL_ATTEMPTS = 120;
|
||||
const DEFAULT_GENERATED_VIDEO_MAX_BYTES = 16 * 1024 * 1024;
|
||||
const SUPPORTED_ASPECT_RATIOS = ["16:9", "9:16"] as const;
|
||||
const OPENROUTER_VIDEO_MALFORMED_RESPONSE = "OpenRouter video generation response malformed";
|
||||
const SUPPORTED_DURATION_SECONDS = [4, 6, 8] as const;
|
||||
@@ -349,13 +351,36 @@ function resolveOpenRouterContentUrl(params: { baseUrl: string; jobId: string })
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDeliverableOpenRouterVideoUrl(value: string | undefined): string | undefined {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const url = new URL(normalized);
|
||||
return url.protocol === "https:" || url.protocol === "http:" ? normalized : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGeneratedVideoMaxBytes(req: VideoGenerationRequest): number {
|
||||
const configured = req.cfg.agents?.defaults?.mediaMaxMb;
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||
return Math.floor(configured * 1024 * 1024);
|
||||
}
|
||||
return DEFAULT_GENERATED_VIDEO_MAX_BYTES;
|
||||
}
|
||||
|
||||
async function downloadOpenRouterVideo(params: {
|
||||
url: string;
|
||||
deliveryUrl?: string;
|
||||
baseUrl: string;
|
||||
headers: Headers;
|
||||
timeoutMs: number;
|
||||
allowPrivateNetwork: boolean;
|
||||
dispatcherPolicy: OpenRouterVideoDispatcherPolicy;
|
||||
maxBytes: number;
|
||||
}): Promise<GeneratedVideoAsset> {
|
||||
const { response, release } = await fetchOpenRouterVideoGet({
|
||||
...params,
|
||||
@@ -364,11 +389,30 @@ async function downloadOpenRouterVideo(params: {
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "OpenRouter generated video download failed");
|
||||
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const fileName = `video-1.${extensionForMime(mimeType)?.slice(1) ?? "mp4"}`;
|
||||
let exceededMaxBytes = false;
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await readResponseWithLimit(response, params.maxBytes, {
|
||||
onOverflow: ({ maxBytes }) => {
|
||||
exceededMaxBytes = true;
|
||||
return new Error(`OpenRouter generated video download exceeds ${maxBytes} bytes`);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (exceededMaxBytes && params.deliveryUrl) {
|
||||
return {
|
||||
url: params.deliveryUrl,
|
||||
mimeType,
|
||||
fileName,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
buffer,
|
||||
mimeType,
|
||||
fileName: `video-1.${extensionForMime(mimeType)?.slice(1) ?? "mp4"}`,
|
||||
fileName,
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
@@ -504,11 +548,12 @@ export function buildOpenRouterVideoGenerationProvider(): VideoGenerationProvide
|
||||
dispatcherPolicy,
|
||||
});
|
||||
const completedJobId = normalizeOptionalString(completed.id) ?? jobId;
|
||||
const unsignedUrl = completed.unsigned_urls?.find((url) => normalizeOptionalString(url));
|
||||
const videoUrl =
|
||||
completed.unsigned_urls?.find((url) => normalizeOptionalString(url)) ??
|
||||
resolveOpenRouterContentUrl({ baseUrl, jobId: completedJobId });
|
||||
unsignedUrl ?? resolveOpenRouterContentUrl({ baseUrl, jobId: completedJobId });
|
||||
const video = await downloadOpenRouterVideo({
|
||||
url: videoUrl,
|
||||
deliveryUrl: resolveDeliverableOpenRouterVideoUrl(unsignedUrl),
|
||||
baseUrl,
|
||||
headers,
|
||||
timeoutMs: resolveProviderOperationTimeoutMs({
|
||||
@@ -517,6 +562,7 @@ export function buildOpenRouterVideoGenerationProvider(): VideoGenerationProvide
|
||||
}),
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
maxBytes: resolveGeneratedVideoMaxBytes(req),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user