1- import { readFile } from "node:fs/promises" ;
2- import path from "node:path" ;
31import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime" ;
42import {
53 createProviderOperationDeadline ,
64 executeProviderOperationWithRetry ,
75 resolveProviderOperationTimeoutMs ,
86 waitProviderOperationPollInterval ,
97} from "openclaw/plugin-sdk/provider-http" ;
10- import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security -runtime" ;
8+ import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit -runtime" ;
119import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime" ;
1210import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime" ;
13- import { resolvePreferredOpenClawTmpDir , withTempWorkspace } from "openclaw/plugin-sdk/temp-path" ;
1411import type {
1512 GeneratedVideoAsset ,
1613 VideoGenerationProvider ,
@@ -29,6 +26,7 @@ import { createGoogleGenAI, type GoogleGenAIClient } from "./google-genai-runtim
2926const DEFAULT_TIMEOUT_MS = 180_000 ;
3027const POLL_INTERVAL_MS = 10_000 ;
3128const MAX_POLL_ATTEMPTS = 120 ;
29+ const DEFAULT_GENERATED_VIDEO_MAX_BYTES = 16 * 1024 * 1024 ;
3230const GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE =
3331 "Google video generation response missing generated videos" ;
3432
@@ -37,6 +35,20 @@ function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): strin
3735 return configured ? resolveGoogleGenerativeAiApiOrigin ( configured ) : undefined ;
3836}
3937
38+ function resolveGeneratedVideoMaxBytes ( req : VideoGenerationRequest ) : number {
39+ const configured = req . cfg . agents ?. defaults ?. mediaMaxMb ;
40+ if ( typeof configured === "number" && Number . isFinite ( configured ) && configured > 0 ) {
41+ return Math . floor ( configured * 1024 * 1024 ) ;
42+ }
43+ return DEFAULT_GENERATED_VIDEO_MAX_BYTES ;
44+ }
45+
46+ function assertGeneratedVideoBufferWithinLimit ( buffer : Buffer , maxBytes : number ) : void {
47+ if ( buffer . length > maxBytes ) {
48+ throw new Error ( `Google generated video download exceeds ${ maxBytes } bytes` ) ;
49+ }
50+ }
51+
4052function resolveGoogleVideoRestBaseUrl ( configuredBaseUrl ?: string ) : string {
4153 return `${ configuredBaseUrl ?? "https://generativelanguage.googleapis.com" } /v1beta` ;
4254}
@@ -148,42 +160,6 @@ function resolveInputVideo(req: VideoGenerationRequest) {
148160 } ;
149161}
150162
151- async function downloadGeneratedVideo ( params : {
152- client : GoogleGenAIClient ;
153- file : unknown ;
154- index : number ;
155- } ) : Promise < GeneratedVideoAsset > {
156- return await withTempWorkspace (
157- { rootDir : resolvePreferredOpenClawTmpDir ( ) , prefix : "openclaw-google-video-" } ,
158- async ( { dir : tempDir } ) => {
159- const fileName = `video-${ params . index + 1 } .mp4` ;
160- const downloadPath = path . join ( tempDir , fileName ) ;
161- await writeExternalFileWithinRoot ( {
162- rootDir : tempDir ,
163- path : fileName ,
164- write : async ( downloadPath ) => {
165- await executeProviderOperationWithRetry ( {
166- provider : "google" ,
167- stage : "download" ,
168- operation : async ( ) => {
169- await params . client . files . download ( {
170- file : params . file as never ,
171- downloadPath,
172- } ) ;
173- } ,
174- } ) ;
175- } ,
176- } ) ;
177- const buffer = await readFile ( downloadPath ) ;
178- return {
179- buffer,
180- mimeType : "video/mp4" ,
181- fileName : `video-${ params . index + 1 } .mp4` ,
182- } ;
183- } ,
184- ) ;
185- }
186-
187163function resolveGoogleGeneratedVideoDownloadUrl ( params : {
188164 uri : string | undefined ;
189165 apiKey : string ;
@@ -222,12 +198,31 @@ function resolveGoogleGeneratedVideoDownloadUrl(params: {
222198 return url . toString ( ) ;
223199}
224200
201+ function resolveGoogleGeneratedVideoFileDownloadUrl ( params : {
202+ file : unknown ;
203+ apiKey : string ;
204+ configuredBaseUrl ?: string ;
205+ } ) : string | undefined {
206+ const resource = params . file as { name ?: unknown ; uri ?: unknown } | undefined ;
207+ const name = normalizeOptionalString ( resource ?. name ) ?? normalizeOptionalString ( resource ?. uri ) ;
208+ if ( ! name || ! / ^ f i l e s \/ [ ^ / ? # ] + $ / u. test ( name ) ) {
209+ return undefined ;
210+ }
211+ const baseUrl = resolveGoogleVideoRestBaseUrl ( params . configuredBaseUrl ) ;
212+ const url = new URL ( `${ baseUrl } /${ name } :download` ) ;
213+ url . searchParams . set ( "alt" , "media" ) ;
214+ url . searchParams . set ( "key" , params . apiKey ) ;
215+ return url . toString ( ) ;
216+ }
217+
225218async function downloadGeneratedVideoFromUri ( params : {
226219 uri : string | undefined ;
227220 apiKey : string ;
228221 configuredBaseUrl ?: string ;
229222 mimeType ?: string ;
230223 index : number ;
224+ maxBytes : number ;
225+ timeoutMs : number ;
231226} ) : Promise < GeneratedVideoAsset | undefined > {
232227 const downloadUrl = resolveGoogleGeneratedVideoDownloadUrl ( {
233228 uri : params . uri ,
@@ -243,14 +238,21 @@ async function downloadGeneratedVideoFromUri(params: {
243238 operation : async ( ) => {
244239 const { response, release } = await fetchWithSsrFGuard ( {
245240 url : downloadUrl ,
241+ timeoutMs : params . timeoutMs ,
246242 } ) ;
247243 try {
248244 if ( ! response . ok ) {
249245 throw new Error (
250246 `Failed to download Google generated video: ${ response . status } ${ response . statusText } ` ,
251247 ) ;
252248 }
253- const buffer = Buffer . from ( await response . arrayBuffer ( ) ) ;
249+ const buffer = await readResponseWithLimit ( response , params . maxBytes , {
250+ chunkTimeoutMs : params . timeoutMs ,
251+ onOverflow : ( { maxBytes } ) =>
252+ new Error ( `Google generated video download exceeds ${ maxBytes } bytes` ) ,
253+ onIdleTimeout : ( { chunkTimeoutMs } ) =>
254+ new Error ( `Google generated video download stalled after ${ chunkTimeoutMs } ms` ) ,
255+ } ) ;
254256 return {
255257 buffer,
256258 mimeType :
@@ -545,14 +547,17 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
545547 if ( generatedVideos . length === 0 ) {
546548 throw new Error ( GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE ) ;
547549 }
550+ const maxVideoBytes = resolveGeneratedVideoMaxBytes ( req ) ;
548551 const videos = await Promise . all (
549552 generatedVideos . map ( async ( entry , index ) => {
550553 const inline = entry . video as
551554 | { videoBytes ?: string ; uri ?: string ; mimeType ?: string }
552555 | undefined ;
553556 if ( inline ?. videoBytes ) {
557+ const buffer = Buffer . from ( inline . videoBytes , "base64" ) ;
558+ assertGeneratedVideoBufferWithinLimit ( buffer , maxVideoBytes ) ;
554559 return {
555- buffer : Buffer . from ( inline . videoBytes , "base64" ) ,
560+ buffer,
556561 mimeType : normalizeOptionalString ( inline . mimeType ) || "video/mp4" ,
557562 fileName : `video-${ index + 1 } .mp4` ,
558563 } ;
@@ -563,18 +568,38 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
563568 configuredBaseUrl,
564569 mimeType : inline ?. mimeType ,
565570 index,
571+ maxBytes : maxVideoBytes ,
572+ timeoutMs : resolveProviderOperationTimeoutMs ( {
573+ deadline,
574+ defaultTimeoutMs : DEFAULT_TIMEOUT_MS ,
575+ } ) ,
566576 } ) ;
567577 if ( directDownload ) {
568578 return directDownload ;
569579 }
570580 if ( ! inline ) {
571581 throw new Error ( "Google generated video missing file handle" ) ;
572582 }
573- return await downloadGeneratedVideo ( {
574- client,
575- file : inline ,
583+ const fileDownload = await downloadGeneratedVideoFromUri ( {
584+ uri : resolveGoogleGeneratedVideoFileDownloadUrl ( {
585+ file : inline ,
586+ apiKey,
587+ configuredBaseUrl,
588+ } ) ,
589+ apiKey,
590+ configuredBaseUrl,
591+ mimeType : inline . mimeType ,
576592 index,
593+ maxBytes : maxVideoBytes ,
594+ timeoutMs : resolveProviderOperationTimeoutMs ( {
595+ deadline,
596+ defaultTimeoutMs : DEFAULT_TIMEOUT_MS ,
597+ } ) ,
577598 } ) ;
599+ if ( ! fileDownload ) {
600+ throw new Error ( "Google generated video missing bounded download URL" ) ;
601+ }
602+ return fileDownload ;
578603 } ) ,
579604 ) ;
580605 return {
0 commit comments