diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs index 5c2bd1aae..da0310e08 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -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) { diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index 5b23bc70e..e37683cd8 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -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) {