fix(provider): retry google rest status failures

This commit is contained in:
Peter Steinberger
2026-05-13 08:42:38 +01:00
parent af021aac8d
commit 8b0b4ea82f
2 changed files with 99 additions and 4 deletions

View File

@@ -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",

View File

@@ -300,6 +300,25 @@ async function requestGoogleVideoJson(params: {
stage: "create" | "poll";
body?: unknown;
}): Promise<unknown> {
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();