From 8b0b4ea82fe41b211e3d59c7ca607349f95d73ad Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 13 May 2026 08:42:38 +0100 Subject: [PATCH] fix(provider): retry google rest status failures --- .../google/video-generation-provider.test.ts | 70 +++++++++++++++++++ .../google/video-generation-provider.ts | 33 +++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/extensions/google/video-generation-provider.test.ts b/extensions/google/video-generation-provider.test.ts index 2dbc6fd2fc6..87e8e8c8498 100644 --- a/extensions/google/video-generation-provider.test.ts +++ b/extensions/google/video-generation-provider.test.ts @@ -106,6 +106,7 @@ describe("google video generation provider", () => { generateVideosMock.mockReset(); getVideosOperationMock.mockReset(); createGoogleGenAIMock.mockClear(); + vi.useRealTimers(); }); afterAll(() => { @@ -357,6 +358,75 @@ describe("google video generation provider", () => { expect(result.videos[0]?.buffer).toEqual(Buffer.from("rest-video")); }); + it("retries transient Google REST poll failures with empty bodies", async () => { + vi.useFakeTimers(); + vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ + apiKey: "google-key", + source: "env", + mode: "api-key", + }); + generateVideosMock.mockRejectedValue(Object.assign(new Error("sdk 404"), { status: 404 })); + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + done: false, + name: "operations/rest-123", + }), + ), + ) + .mockResolvedValueOnce(new Response("", { status: 503, statusText: "Service Unavailable" })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + done: true, + name: "operations/rest-123", + response: { + generateVideoResponse: { + generatedSamples: [ + { + video: { + uri: "https://generativelanguage.googleapis.com/v1beta/files/rest-video:download?alt=media", + mimeType: "video/mp4", + }, + }, + ], + }, + }, + }), + ), + ) + .mockResolvedValueOnce( + new Response("rest-video", { + status: 200, + statusText: "OK", + headers: { "content-type": "video/mp4" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const provider = buildGoogleVideoGenerationProvider(); + const resultPromise = provider.generateVideo({ + provider: "google", + model: "veo-3.1-fast-generate-preview", + prompt: "A tiny robot watering a windowsill garden", + cfg: {}, + durationSeconds: 3, + }); + await vi.advanceTimersByTimeAsync(10_250); + const result = await resultPromise; + + expect(fetchMock).toHaveBeenCalledTimes(4); + expect(fetchInputUrl(fetchMock, 1)).toBe( + "https://generativelanguage.googleapis.com/v1beta/operations/rest-123", + ); + expect(fetchInputUrl(fetchMock, 2)).toBe( + "https://generativelanguage.googleapis.com/v1beta/operations/rest-123", + ); + expect(result.videos[0]?.buffer).toEqual(Buffer.from("rest-video")); + }); + it("does not fall back to REST when SDK video generation with reference inputs returns 404", async () => { vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({ apiKey: "google-key", diff --git a/extensions/google/video-generation-provider.ts b/extensions/google/video-generation-provider.ts index 19527d31e70..57fb2767e4c 100644 --- a/extensions/google/video-generation-provider.ts +++ b/extensions/google/video-generation-provider.ts @@ -300,6 +300,25 @@ async function requestGoogleVideoJson(params: { stage: "create" | "poll"; body?: unknown; }): Promise { + function createHttpError(response: Response, detail: unknown): Error { + const parts = [`HTTP ${response.status}`]; + const statusText = response.statusText.trim(); + if (statusText) { + parts.push(statusText); + } + if (typeof detail === "string") { + const trimmed = detail.trim(); + if (trimmed) { + parts.push(trimmed); + } + } else if (detail && typeof detail === "object") { + parts.push(JSON.stringify(detail)); + } + const error = new Error(parts.join(": ")); + Object.assign(error, { status: response.status, statusCode: response.status }); + return error; + } + return await executeProviderOperationWithRetry({ provider: "google", stage: params.stage, @@ -328,12 +347,18 @@ async function requestGoogleVideoJson(params: { }); try { const text = await response.text(); - const payload = text ? (JSON.parse(text) as unknown) : {}; if (!response.ok) { - throw new Error( - typeof payload === "string" ? payload : JSON.stringify(payload ?? null), - ); + let detail: unknown = text; + if (text) { + try { + detail = JSON.parse(text) as unknown; + } catch { + detail = text; + } + } + throw createHttpError(response, detail); } + const payload = text ? (JSON.parse(text) as unknown) : {}; return payload; } finally { await release();