
























@@ -13,7 +13,7 @@ import type {
1313VideoGenerationProvider,
1414VideoGenerationRequest,
1515} from "openclaw/plugin-sdk/video-generation";
16-import { resolveGoogleGenerativeAiApiOrigin } from "./api.js";
16+import { parseGeminiAuth, resolveGoogleGenerativeAiApiOrigin } from "./api.js";
1717import {
1818createGoogleVideoGenerationProviderMetadata,
1919DEFAULT_GOOGLE_VIDEO_MODEL,
@@ -26,12 +26,32 @@ import { createGoogleGenAI, type GoogleGenAIClient } from "./google-genai-runtim
2626const DEFAULT_TIMEOUT_MS = 180_000;
2727const POLL_INTERVAL_MS = 10_000;
2828const MAX_POLL_ATTEMPTS = 90;
29+const GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE =
30+"Google video generation response missing generated videos";
29313032function resolveConfiguredGoogleVideoBaseUrl(req: VideoGenerationRequest): string | undefined {
3133const configured = normalizeOptionalString(req.cfg?.models?.providers?.google?.baseUrl);
3234return configured ? resolveGoogleGenerativeAiApiOrigin(configured) : undefined;
3335}
343637+function resolveGoogleVideoRestBaseUrl(configuredBaseUrl?: string): string {
38+return `${configuredBaseUrl ?? "https://generativelanguage.googleapis.com"}/v1beta`;
39+}
40+41+function resolveGoogleVideoRestModelPath(model: string): string {
42+const trimmed = normalizeOptionalString(model) || DEFAULT_GOOGLE_VIDEO_MODEL;
43+if (trimmed.startsWith("google/models/")) {
44+return trimmed.slice("google/".length);
45+}
46+if (trimmed.startsWith("models/")) {
47+return trimmed;
48+}
49+if (trimmed.startsWith("google/")) {
50+return `models/${trimmed.slice("google/".length)}`;
51+}
52+return `models/${trimmed}`;
53+}
54+3555function parseVideoSize(size: string | undefined): { width: number; height: number } | undefined {
3656const trimmed = normalizeOptionalString(size);
3757if (!trimmed) {
@@ -220,6 +240,120 @@ async function downloadGeneratedVideoFromUri(params: {
220240};
221241}
222242243+function extractGoogleApiErrorCode(error: unknown): number | undefined {
244+const status = (error as { status?: unknown } | undefined)?.status;
245+if (typeof status === "number") {
246+return status;
247+}
248+const message = error instanceof Error ? error.message : String(error);
249+try {
250+const parsed = JSON.parse(message) as { code?: unknown; error?: { code?: unknown } };
251+const code = typeof parsed.code === "number" ? parsed.code : parsed.error?.code;
252+return typeof code === "number" ? code : undefined;
253+} catch {
254+return /\b404\b/u.test(message) ? 404 : undefined;
255+}
256+}
257+258+function extractGeneratedVideos(operation: unknown): Array<{ video?: unknown }> {
259+const response = (operation as { response?: Record<string, unknown> }).response;
260+const generatedVideos = response?.generatedVideos;
261+if (Array.isArray(generatedVideos) && generatedVideos.length > 0) {
262+return generatedVideos as Array<{ video?: unknown }>;
263+}
264+const generatedSamples = (response?.generateVideoResponse as { generatedSamples?: unknown })
265+?.generatedSamples;
266+return Array.isArray(generatedSamples) ? (generatedSamples as Array<{ video?: unknown }>) : [];
267+}
268+269+async function requestGoogleVideoJson(params: {
270+url: string;
271+method: "GET" | "POST";
272+headers: Record<string, string>;
273+deadline: ReturnType<typeof createProviderOperationDeadline>;
274+body?: unknown;
275+}): Promise<unknown> {
276+const controller = new AbortController();
277+const timeout = setTimeout(
278+() => controller.abort(),
279+resolveProviderOperationTimeoutMs({
280+deadline: params.deadline,
281+defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
282+}),
283+);
284+try {
285+const response = await fetch(params.url, {
286+method: params.method,
287+headers: params.headers,
288+ ...(params.body === undefined ? {} : { body: JSON.stringify(params.body) }),
289+signal: controller.signal,
290+});
291+const text = await response.text();
292+const payload = text ? (JSON.parse(text) as unknown) : {};
293+if (!response.ok) {
294+throw new Error(typeof payload === "string" ? payload : JSON.stringify(payload ?? null));
295+}
296+return payload;
297+} finally {
298+clearTimeout(timeout);
299+}
300+}
301+302+async function generateGoogleVideoViaRest(params: {
303+baseUrl: string;
304+headers: Record<string, string>;
305+deadline: ReturnType<typeof createProviderOperationDeadline>;
306+model: string;
307+prompt: string;
308+durationSeconds?: number;
309+aspectRatio?: "16:9" | "9:16";
310+resolution?: "720p" | "1080p";
311+audio?: boolean;
312+}): Promise<unknown> {
313+let operation = await requestGoogleVideoJson({
314+url: `${params.baseUrl}/${resolveGoogleVideoRestModelPath(params.model)}:predictLongRunning`,
315+method: "POST",
316+headers: params.headers,
317+deadline: params.deadline,
318+body: {
319+instances: [{ prompt: params.prompt }],
320+parameters: {
321+ ...(typeof params.durationSeconds === "number"
322+ ? { durationSeconds: params.durationSeconds }
323+ : {}),
324+ ...(params.aspectRatio ? { aspectRatio: params.aspectRatio } : {}),
325+ ...(params.resolution ? { resolution: params.resolution } : {}),
326+ ...(params.audio === true ? { generateAudio: true } : {}),
327+},
328+},
329+});
330+331+for (let attempt = 0; !((operation as { done?: boolean }).done ?? false); attempt += 1) {
332+if (attempt >= MAX_POLL_ATTEMPTS) {
333+throw new Error("Google video generation did not finish in time");
334+}
335+await waitProviderOperationPollInterval({
336+deadline: params.deadline,
337+pollIntervalMs: POLL_INTERVAL_MS,
338+});
339+const operationName = (operation as { name?: unknown }).name;
340+if (typeof operationName !== "string" || !operationName) {
341+throw new Error("Google video operation response missing name for polling");
342+}
343+operation = await requestGoogleVideoJson({
344+url: `${params.baseUrl}/${operationName}`,
345+method: "GET",
346+headers: params.headers,
347+deadline: params.deadline,
348+});
349+}
350+const error = (operation as { error?: unknown }).error;
351+if (error) {
352+throw new Error(JSON.stringify(error));
353+}
354+return operation;
355+}
356+223357export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
224358return {
225359 ...createGoogleVideoGenerationProviderMetadata(),
@@ -247,7 +381,14 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
247381const apiKey = auth.apiKey;
248382249383const configuredBaseUrl = resolveConfiguredGoogleVideoBaseUrl(req);
384+const restBaseUrl = resolveGoogleVideoRestBaseUrl(configuredBaseUrl);
385+const authHeaders = parseGeminiAuth(apiKey).headers;
250386const durationSeconds = resolveDurationSeconds(req.durationSeconds);
387+const model = normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL;
388+const aspectRatio = resolveAspectRatio({ aspectRatio: req.aspectRatio, size: req.size });
389+const resolution = resolveResolution({ resolution: req.resolution, size: req.size });
390+const hasReferenceInputs =
391+(req.inputImages?.length ?? 0) > 0 || (req.inputVideos?.length ?? 0) > 0;
251392const deadline = createProviderOperationDeadline({
252393timeoutMs: req.timeoutMs,
253394label: "Google video generation",
@@ -262,37 +403,70 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
262403}),
263404},
264405});
265-let operation = await client.models.generateVideos({
266-model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
267-prompt: req.prompt,
268-image: resolveInputImage(req),
269-video: resolveInputVideo(req),
270-config: {
271- ...(typeof durationSeconds === "number" ? { durationSeconds } : {}),
272- ...(resolveAspectRatio({ aspectRatio: req.aspectRatio, size: req.size })
273- ? { aspectRatio: resolveAspectRatio({ aspectRatio: req.aspectRatio, size: req.size }) }
274- : {}),
275- ...(resolveResolution({ resolution: req.resolution, size: req.size })
276- ? { resolution: resolveResolution({ resolution: req.resolution, size: req.size }) }
277- : {}),
278- ...(req.audio === true ? { generateAudio: true } : {}),
279-},
280-});
406+let usedRestFallback = false;
407+let operation;
408+try {
409+operation = await client.models.generateVideos({
410+ model,
411+prompt: req.prompt,
412+image: resolveInputImage(req),
413+video: resolveInputVideo(req),
414+config: {
415+ ...(typeof durationSeconds === "number" ? { durationSeconds } : {}),
416+ ...(aspectRatio ? { aspectRatio } : {}),
417+ ...(resolution ? { resolution } : {}),
418+ ...(req.audio === true ? { generateAudio: true } : {}),
419+},
420+});
421+} catch (error) {
422+if (hasReferenceInputs || extractGoogleApiErrorCode(error) !== 404) {
423+throw error;
424+}
425+usedRestFallback = true;
426+operation = await generateGoogleVideoViaRest({
427+baseUrl: restBaseUrl,
428+headers: authHeaders,
429+ deadline,
430+ model,
431+prompt: req.prompt,
432+ durationSeconds,
433+ aspectRatio,
434+ resolution,
435+audio: req.audio,
436+});
437+}
281438282-for (let attempt = 0; !(operation.done ?? false); attempt += 1) {
283-if (attempt >= MAX_POLL_ATTEMPTS) {
284-throw new Error("Google video generation did not finish in time");
439+if (!usedRestFallback) {
440+for (let attempt = 0; !(operation.done ?? false); attempt += 1) {
441+if (attempt >= MAX_POLL_ATTEMPTS) {
442+throw new Error("Google video generation did not finish in time");
443+}
444+await waitProviderOperationPollInterval({ deadline, pollIntervalMs: POLL_INTERVAL_MS });
445+resolveProviderOperationTimeoutMs({ deadline, defaultTimeoutMs: DEFAULT_TIMEOUT_MS });
446+operation = await client.operations.getVideosOperation({ operation });
285447}
286-await waitProviderOperationPollInterval({ deadline, pollIntervalMs: POLL_INTERVAL_MS });
287-resolveProviderOperationTimeoutMs({ deadline, defaultTimeoutMs: DEFAULT_TIMEOUT_MS });
288-operation = await client.operations.getVideosOperation({ operation });
289448}
290449if (operation.error) {
291450throw new Error(JSON.stringify(operation.error));
292451}
293-const generatedVideos = operation.response?.generatedVideos ?? [];
452+let generatedVideos = extractGeneratedVideos(operation);
453+if (generatedVideos.length === 0 && !hasReferenceInputs && !usedRestFallback) {
454+usedRestFallback = true;
455+operation = await generateGoogleVideoViaRest({
456+baseUrl: restBaseUrl,
457+headers: authHeaders,
458+ deadline,
459+ model,
460+prompt: req.prompt,
461+ durationSeconds,
462+ aspectRatio,
463+ resolution,
464+audio: req.audio,
465+});
466+generatedVideos = extractGeneratedVideos(operation);
467+}
294468if (generatedVideos.length === 0) {
295-throw new Error("Google video generation response missing generated videos");
469+throw new Error(GOOGLE_VIDEO_EMPTY_RESULT_MESSAGE);
296470}
297471const videos = await Promise.all(
298472generatedVideos.map(async (entry, index) => {
@@ -326,7 +500,7 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
326500);
327501return {
328502 videos,
329-model: normalizeOptionalString(req.model) || DEFAULT_GOOGLE_VIDEO_MODEL,
503+ model,
330504metadata: operation.name
331505 ? {
332506operationName: operation.name,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。