Skip to content

Commit ecdad94

Browse files
committed
refactor(provider): centralize transient retry stages
1 parent 9741bbe commit ecdad94

19 files changed

Lines changed: 913 additions & 462 deletions

extensions/byteplus/video-generation-provider.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runt
44
import {
55
assertOkOrThrowHttpError,
66
createProviderOperationDeadline,
7-
fetchWithTimeout,
7+
fetchProviderDownloadResponse,
8+
fetchProviderOperationResponse,
89
postJsonRequest,
910
resolveProviderOperationTimeoutMs,
1011
resolveProviderHttpRequestConfig,
@@ -82,16 +83,21 @@ async function pollBytePlusTask(params: {
8283
label: `BytePlus video generation task ${params.taskId}`,
8384
});
8485
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt += 1) {
85-
const response = await fetchWithTimeout(
86-
`${params.baseUrl}/contents/generations/tasks/${params.taskId}`,
87-
{
86+
const response = await fetchProviderOperationResponse({
87+
stage: "poll",
88+
url: `${params.baseUrl}/contents/generations/tasks/${params.taskId}`,
89+
init: {
8890
method: "GET",
8991
headers: params.headers,
9092
},
91-
resolveProviderOperationTimeoutMs({ deadline, defaultTimeoutMs: DEFAULT_TIMEOUT_MS }),
92-
params.fetchFn,
93-
);
94-
await assertOkOrThrowHttpError(response, "BytePlus video status request failed");
93+
timeoutMs: resolveProviderOperationTimeoutMs({
94+
deadline,
95+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
96+
}),
97+
fetchFn: params.fetchFn,
98+
provider: "byteplus",
99+
requestFailedMessage: "BytePlus video status request failed",
100+
});
95101
const payload = (await response.json()) as BytePlusTaskResponse;
96102
switch (normalizeOptionalString(payload.status)) {
97103
case "succeeded":
@@ -116,13 +122,14 @@ async function downloadBytePlusVideo(params: {
116122
timeoutMs?: number;
117123
fetchFn: typeof fetch;
118124
}): Promise<GeneratedVideoAsset> {
119-
const response = await fetchWithTimeout(
120-
params.url,
121-
{ method: "GET" },
122-
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
123-
params.fetchFn,
124-
);
125-
await assertOkOrThrowHttpError(response, "BytePlus generated video download failed");
125+
const response = await fetchProviderDownloadResponse({
126+
url: params.url,
127+
init: { method: "GET" },
128+
timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
129+
fetchFn: params.fetchFn,
130+
provider: "byteplus",
131+
requestFailedMessage: "BytePlus generated video download failed",
132+
});
126133
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "video/mp4";
127134
const arrayBuffer = await response.arrayBuffer();
128135
return {

extensions/google/embedding-provider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
requireApiKey,
1515
resolveApiKeyForProvider,
1616
} from "openclaw/plugin-sdk/provider-auth-runtime";
17-
import { createProviderHttpError } from "openclaw/plugin-sdk/provider-http";
17+
import {
18+
createProviderHttpError,
19+
providerOperationRetryConfig,
20+
} from "openclaw/plugin-sdk/provider-http";
1821
import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
1922
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2023

@@ -199,7 +202,7 @@ async function fetchGeminiEmbeddingPayload(params: {
199202
return await executeWithApiKeyRotation({
200203
provider: "google",
201204
apiKeys: params.client.apiKeys,
202-
transientRetry: true,
205+
transientRetry: providerOperationRetryConfig("read"),
203206
execute: async (apiKey) => {
204207
const authHeaders = parseGeminiAuth(apiKey);
205208
const headers = {

extensions/google/video-generation-provider.ts

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
44
import {
55
createProviderOperationDeadline,
6+
executeProviderOperationWithRetry,
67
resolveProviderOperationTimeoutMs,
78
waitProviderOperationPollInterval,
89
} from "openclaw/plugin-sdk/provider-http";
@@ -161,9 +162,15 @@ async function downloadGeneratedVideo(params: {
161162
rootDir: tempDir,
162163
path: fileName,
163164
write: async (downloadPath) => {
164-
await params.client.files.download({
165-
file: params.file as never,
166-
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+
},
167174
});
168175
},
169176
});
@@ -230,27 +237,33 @@ async function downloadGeneratedVideoFromUri(params: {
230237
if (!downloadUrl) {
231238
return undefined;
232239
}
233-
const { response, release } = await fetchWithSsrFGuard({
234-
url: downloadUrl,
240+
return await executeProviderOperationWithRetry({
241+
provider: "google",
242+
stage: "download",
243+
operation: async () => {
244+
const { response, release } = await fetchWithSsrFGuard({
245+
url: downloadUrl,
246+
});
247+
try {
248+
if (!response.ok) {
249+
throw new Error(
250+
`Failed to download Google generated video: ${response.status} ${response.statusText}`,
251+
);
252+
}
253+
const buffer = Buffer.from(await response.arrayBuffer());
254+
return {
255+
buffer,
256+
mimeType:
257+
normalizeOptionalString(response.headers.get("content-type")) ||
258+
normalizeOptionalString(params.mimeType) ||
259+
"video/mp4",
260+
fileName: `video-${params.index + 1}.mp4`,
261+
};
262+
} finally {
263+
await release();
264+
}
265+
},
235266
});
236-
try {
237-
if (!response.ok) {
238-
throw new Error(
239-
`Failed to download Google generated video: ${response.status} ${response.statusText}`,
240-
);
241-
}
242-
const buffer = Buffer.from(await response.arrayBuffer());
243-
return {
244-
buffer,
245-
mimeType:
246-
normalizeOptionalString(response.headers.get("content-type")) ||
247-
normalizeOptionalString(params.mimeType) ||
248-
"video/mp4",
249-
fileName: `video-${params.index + 1}.mp4`,
250-
};
251-
} finally {
252-
await release();
253-
}
254267
}
255268

256269
function extractGoogleApiErrorCode(error: unknown): number | undefined {
@@ -284,39 +297,52 @@ async function requestGoogleVideoJson(params: {
284297
method: "GET" | "POST";
285298
headers: Record<string, string>;
286299
deadline: ReturnType<typeof createProviderOperationDeadline>;
300+
stage: "create" | "poll";
287301
body?: unknown;
288302
}): Promise<unknown> {
289-
const controller = new AbortController();
290-
const timeout = setTimeout(
291-
() => controller.abort(),
292-
resolveProviderOperationTimeoutMs({
293-
deadline: params.deadline,
294-
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
295-
}),
296-
);
297-
try {
298-
const { response, release } = await fetchWithSsrFGuard({
299-
url: params.url,
300-
init: {
301-
method: params.method,
302-
headers: params.headers,
303-
...(params.body === undefined ? {} : { body: JSON.stringify(params.body) }),
304-
},
305-
signal: controller.signal,
306-
});
307-
try {
308-
const text = await response.text();
309-
const payload = text ? (JSON.parse(text) as unknown) : {};
310-
if (!response.ok) {
311-
throw new Error(typeof payload === "string" ? payload : JSON.stringify(payload ?? null));
303+
return await executeProviderOperationWithRetry({
304+
provider: "google",
305+
stage: params.stage,
306+
operation: async () => {
307+
const controller = new AbortController();
308+
const timeout = setTimeout(
309+
() => {
310+
const error = new Error("request timed out");
311+
error.name = "TimeoutError";
312+
controller.abort(error);
313+
},
314+
resolveProviderOperationTimeoutMs({
315+
deadline: params.deadline,
316+
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
317+
}),
318+
);
319+
try {
320+
const { response, release } = await fetchWithSsrFGuard({
321+
url: params.url,
322+
init: {
323+
method: params.method,
324+
headers: params.headers,
325+
...(params.body === undefined ? {} : { body: JSON.stringify(params.body) }),
326+
},
327+
signal: controller.signal,
328+
});
329+
try {
330+
const text = await response.text();
331+
const payload = text ? (JSON.parse(text) as unknown) : {};
332+
if (!response.ok) {
333+
throw new Error(
334+
typeof payload === "string" ? payload : JSON.stringify(payload ?? null),
335+
);
336+
}
337+
return payload;
338+
} finally {
339+
await release();
340+
}
341+
} finally {
342+
clearTimeout(timeout);
312343
}
313-
return payload;
314-
} finally {
315-
await release();
316-
}
317-
} finally {
318-
clearTimeout(timeout);
319-
}
344+
},
345+
});
320346
}
321347

322348
async function generateGoogleVideoViaRest(params: {
@@ -334,6 +360,7 @@ async function generateGoogleVideoViaRest(params: {
334360
method: "POST",
335361
headers: params.headers,
336362
deadline: params.deadline,
363+
stage: "create",
337364
body: {
338365
instances: [{ prompt: params.prompt }],
339366
parameters: {
@@ -363,6 +390,7 @@ async function generateGoogleVideoViaRest(params: {
363390
method: "GET",
364391
headers: params.headers,
365392
deadline: params.deadline,
393+
stage: "poll",
366394
});
367395
}
368396
const error = (operation as { error?: unknown }).error;
@@ -462,7 +490,11 @@ export function buildGoogleVideoGenerationProvider(): VideoGenerationProvider {
462490
}
463491
await waitProviderOperationPollInterval({ deadline, pollIntervalMs: POLL_INTERVAL_MS });
464492
resolveProviderOperationTimeoutMs({ deadline, defaultTimeoutMs: DEFAULT_TIMEOUT_MS });
465-
sdkOperation = await client.operations.getVideosOperation({ operation: sdkOperation });
493+
sdkOperation = await executeProviderOperationWithRetry({
494+
provider: "google",
495+
stage: "poll",
496+
operation: () => client.operations.getVideosOperation({ operation: sdkOperation }),
497+
});
466498
}
467499
operation = sdkOperation;
468500
}

extensions/minimax/music-generation-provider.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
88
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
99
import {
1010
assertOkOrThrowHttpError,
11-
fetchWithTimeout,
11+
fetchProviderDownloadResponse,
1212
postJsonRequest,
1313
resolveProviderHttpRequestConfig,
1414
} from "openclaw/plugin-sdk/provider-http";
@@ -89,13 +89,14 @@ async function downloadTrackFromUrl(params: {
8989
timeoutMs?: number;
9090
fetchFn: typeof fetch;
9191
}): Promise<GeneratedMusicAsset> {
92-
const response = await fetchWithTimeout(
93-
params.url,
94-
{ method: "GET" },
95-
params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
96-
params.fetchFn,
97-
);
98-
await assertOkOrThrowHttpError(response, "MiniMax generated music download failed");
92+
const response = await fetchProviderDownloadResponse({
93+
url: params.url,
94+
init: { method: "GET" },
95+
timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
96+
fetchFn: params.fetchFn,
97+
provider: "minimax",
98+
requestFailedMessage: "MiniMax generated music download failed",
99+
});
99100
const mimeType = normalizeOptionalString(response.headers.get("content-type")) ?? "audio/mpeg";
100101
const ext = extensionForMime(mimeType)?.replace(/^\./u, "") || "mp3";
101102
return {

0 commit comments

Comments
 (0)