为图片素材脚本补回同模型瞬时错误重试

- 两个脚本新增 fetchJsonWithRetry:对 408/429/5xx 按 1s/2s 退避重试同一 concrete model,最多 3 次,不引入跨模型回退
- requestImagePayload 改用带 requestId 的重试入口,单次 408/429/5xx 不再中断整批生成
- 删除只写不读的 providerBody / providerResponseParse 错误字段,解析失败与缺图改为把响应片段写进错误信息
This commit is contained in:
2026-09-22 12:13:25 +08:00
parent 0b1caf7280
commit 827164ed70
2 changed files with 74 additions and 18 deletions
@@ -223,6 +223,15 @@ function inferExtensionFromBytes(bytes) {
return null;
}
// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由
// server-rs 负责,脚本不做任何模型替换。
const transientRetryMaxAttempts = 3;
const transientRetryBaseDelayMs = 1000;
function isTransientProviderStatus(status) {
return status === 408 || status === 429 || status >= 500;
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -237,15 +246,14 @@ async function fetchJson(url, options, timeoutMs) {
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
);
error.providerStatus = response.status;
error.providerBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.providerResponseParse = true;
error.providerBody = text;
throw error;
throw new Error(
`Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`,
);
}
} catch (error) {
if (error?.name === 'AbortError') {
@@ -257,6 +265,26 @@ async function fetchJson(url, options, timeoutMs) {
}
}
async function fetchJsonWithRetry(url, options, timeoutMs, requestId) {
for (let attempt = 1; ; attempt += 1) {
try {
return await fetchJson(url, options, timeoutMs);
} catch (error) {
if (
attempt >= transientRetryMaxAttempts ||
!isTransientProviderStatus(error?.providerStatus)
) {
throw error;
}
const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1);
console.warn(
`Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
async function requestImagePayload(env, entry) {
const model = imageModel;
const requestBody = {
@@ -265,7 +293,7 @@ async function requestImagePayload(env, entry) {
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
const payload = await fetchJsonWithRetry(
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
@@ -277,15 +305,15 @@ async function requestImagePayload(env, entry) {
body: JSON.stringify(requestBody),
},
env.timeoutMs,
entry.id,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`Tiantoken returned no image for ${entry.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
throw new Error(
`Tiantoken returned no image for ${entry.id}:${JSON.stringify(payload).slice(0, 600)}`,
);
}
async function downloadUrl(url, timeoutMs) {
@@ -193,6 +193,15 @@ function inferExtensionFromBytes(bytes) {
return null;
}
// Tiantoken 偶发 408/429/5xx 时重试同一个 concrete model;跨模型回退仍由
// server-rs 负责,脚本不做任何模型替换。
const transientRetryMaxAttempts = 3;
const transientRetryBaseDelayMs = 1000;
function isTransientProviderStatus(status) {
return status === 408 || status === 429 || status >= 500;
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -207,15 +216,14 @@ async function fetchJson(url, options, timeoutMs) {
`Tiantoken ${response.status}: ${text.slice(0, 600)}`,
);
error.providerStatus = response.status;
error.providerBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.providerResponseParse = true;
error.providerBody = text;
throw error;
throw new Error(
`Tiantoken 返回无法解析的响应体:${error.message};响应片段:${text.slice(0, 600)}`,
);
}
} catch (error) {
if (error?.name === 'AbortError') {
@@ -227,6 +235,26 @@ async function fetchJson(url, options, timeoutMs) {
}
}
async function fetchJsonWithRetry(url, options, timeoutMs, requestId) {
for (let attempt = 1; ; attempt += 1) {
try {
return await fetchJson(url, options, timeoutMs);
} catch (error) {
if (
attempt >= transientRetryMaxAttempts ||
!isTransientProviderStatus(error?.providerStatus)
) {
throw error;
}
const delayMs = transientRetryBaseDelayMs * 2 ** (attempt - 1);
console.warn(
`Tiantoken ${error.providerStatus} on ${requestId}, retrying the same model in ${delayMs}ms (attempt ${attempt + 1}/${transientRetryMaxAttempts})`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
async function requestImagePayload(env, template) {
const model = imageModel;
const requestBody = {
@@ -235,7 +263,7 @@ async function requestImagePayload(env, template) {
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
const payload = await fetchJsonWithRetry(
buildTiantokenImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
@@ -247,15 +275,15 @@ async function requestImagePayload(env, template) {
body: JSON.stringify(requestBody),
},
env.timeoutMs,
template.id,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (extractImageUrls(payload)[0] || base64Image) {
return payload;
}
const error = new Error(`Tiantoken returned no image for ${template.id}`);
error.providerResponseParse = true;
error.providerBody = JSON.stringify(payload).slice(0, 600);
throw error;
throw new Error(
`Tiantoken returned no image for ${template.id}:${JSON.stringify(payload).slice(0, 600)}`,
);
}
async function downloadUrl(url, timeoutMs) {