|
5 | 5 | import { Errors } from '@z-image/shared' |
6 | 6 |
|
7 | 7 | const PROVIDER_NAME = 'HuggingFace' |
| 8 | +const MAX_GRADIO_RETRIES = 3 |
| 9 | + |
| 10 | +function sleep(ms: number): Promise<void> { |
| 11 | + return new Promise((resolve) => setTimeout(resolve, ms)) |
| 12 | +} |
8 | 13 |
|
9 | 14 | /** |
10 | 15 | * Parse HuggingFace error message into appropriate ApiError |
@@ -98,31 +103,64 @@ export async function callGradioApi( |
98 | 103 | const headers: Record<string, string> = { 'Content-Type': 'application/json' } |
99 | 104 | if (hfToken) headers.Authorization = `Bearer ${hfToken}` |
100 | 105 |
|
101 | | - const queue = await fetch(`${baseUrl}/gradio_api/call/${endpoint}`, { |
102 | | - method: 'POST', |
103 | | - headers, |
104 | | - body: JSON.stringify({ data }), |
105 | | - }) |
| 106 | + // HuggingFace Spaces can be "cold" (starting/loading) and sometimes return transient 404/503. |
| 107 | + // Retry a few times to reduce false-negative failures in serverless runtimes (e.g. Cloudflare). |
| 108 | + let queueData: { event_id?: string } | null = null |
| 109 | + for (let attempt = 0; attempt < MAX_GRADIO_RETRIES; attempt++) { |
| 110 | + const queue = await fetch(`${baseUrl}/gradio_api/call/${endpoint}`, { |
| 111 | + method: 'POST', |
| 112 | + headers, |
| 113 | + body: JSON.stringify({ data }), |
| 114 | + }) |
| 115 | + |
| 116 | + if (queue.ok) { |
| 117 | + queueData = (await queue.json()) as { event_id?: string } |
| 118 | + break |
| 119 | + } |
106 | 120 |
|
107 | | - if (!queue.ok) { |
108 | 121 | const errText = await queue.text().catch(() => '') |
109 | | - throw parseHuggingFaceError(errText || `Queue request failed: ${queue.status}`, queue.status) |
| 122 | + const status = queue.status |
| 123 | + const shouldRetry = attempt < MAX_GRADIO_RETRIES - 1 && (status === 404 || status === 503) |
| 124 | + if (shouldRetry) { |
| 125 | + await sleep(600 * (attempt + 1)) |
| 126 | + continue |
| 127 | + } |
| 128 | + throw parseHuggingFaceError(errText || `Queue request failed: ${status}`, status) |
| 129 | + } |
| 130 | + |
| 131 | + if (!queueData) { |
| 132 | + throw Errors.providerError(PROVIDER_NAME, 'Queue request failed after retries') |
110 | 133 | } |
111 | 134 |
|
112 | | - const queueData = (await queue.json()) as { event_id?: string } |
113 | 135 | if (!queueData.event_id) { |
114 | 136 | throw Errors.providerError(PROVIDER_NAME, 'No event_id returned from queue') |
115 | 137 | } |
116 | 138 |
|
117 | | - const result = await fetch(`${baseUrl}/gradio_api/call/${endpoint}/${queueData.event_id}`, { |
118 | | - headers, |
119 | | - }) |
120 | | - if (!result.ok) { |
| 139 | + let text = '' |
| 140 | + for (let attempt = 0; attempt < MAX_GRADIO_RETRIES; attempt++) { |
| 141 | + const result = await fetch(`${baseUrl}/gradio_api/call/${endpoint}/${queueData.event_id}`, { |
| 142 | + headers, |
| 143 | + }) |
| 144 | + |
| 145 | + if (result.ok) { |
| 146 | + text = await result.text() |
| 147 | + break |
| 148 | + } |
| 149 | + |
121 | 150 | const errText = await result.text().catch(() => '') |
122 | | - throw parseHuggingFaceError(errText || `Result request failed: ${result.status}`, result.status) |
| 151 | + const status = result.status |
| 152 | + const shouldRetry = attempt < MAX_GRADIO_RETRIES - 1 && (status === 404 || status === 503) |
| 153 | + if (shouldRetry) { |
| 154 | + await sleep(600 * (attempt + 1)) |
| 155 | + continue |
| 156 | + } |
| 157 | + throw parseHuggingFaceError(errText || `Result request failed: ${status}`, status) |
| 158 | + } |
| 159 | + |
| 160 | + if (!text) { |
| 161 | + throw Errors.providerError(PROVIDER_NAME, 'Empty result after retries') |
123 | 162 | } |
124 | 163 |
|
125 | | - const text = await result.text() |
126 | 164 | const complete = extractCompleteEventData(text) |
127 | 165 |
|
128 | 166 | // Normalize the "complete" payload to the common `unknown[]` that providers expect. |
|
0 commit comments