1313_RETRYABLE_STATUS_CODES = {429 , 500 , 502 , 503 , 504 }
1414
1515
16+ class _SubmissionError (RuntimeError ):
17+ """A task submission failed and must not be retried automatically."""
18+
19+
1620class Client :
1721 """WaveSpeed API client.
1822
@@ -21,7 +25,7 @@ class Client:
2125 base_url: Base URL for the API. If not provided, uses wavespeed.config.api.base_url.
2226 connection_timeout: Timeout for HTTP requests in seconds.
2327 max_retries: Maximum number of retries for the entire operation.
24- max_connection_retries: Maximum retries for individual HTTP requests.
28+ max_connection_retries: Maximum retries for result-query GET requests.
2529 retry_interval: Base interval between retries in seconds.
2630
2731 Example:
@@ -31,8 +35,9 @@ class Client:
3135 # With sync mode (best-effort single request, waits for result)
3236 output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"}, enable_sync_mode=True)
3337
34- # With retry
35- output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"}, max_retries=3)
38+ # Task-level replacement attempts are opt-in; submission POSTs are
39+ # always sent at most once.
40+ output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"}, max_retries=1)
3641 """
3742
3843 def __init__ (
@@ -121,67 +126,34 @@ def _submit(
121126 )
122127 timeouts = (connect_timeout , request_timeout )
123128
124- last_error : Exception | None = None
125-
126- for retry in range (self .max_connection_retries + 1 ):
127- try :
128- response = requests .post (
129- url , json = body , headers = self ._get_headers (), timeout = timeouts
130- )
131-
132- if response .status_code != 200 :
133- # Retry on transient server errors (5xx) and rate limiting (429)
134- if self ._is_retryable_status (response .status_code ):
135- last_error = RuntimeError (
136- f"Failed to submit prediction: HTTP { response .status_code } : "
137- f"{ response .text } "
138- )
139- if retry < self .max_connection_retries :
140- delay = self .retry_interval * (retry + 1 )
141- print (
142- f"Server error (HTTP { response .status_code } ) on attempt "
143- f"{ retry + 1 } /{ self .max_connection_retries + 1 } , "
144- f"retrying in { delay } seconds..."
145- )
146- time .sleep (delay )
147- continue
148- raise last_error
149-
150- # Non-retryable HTTP errors (4xx etc.) fail immediately
151- raise RuntimeError (
152- f"Failed to submit prediction: HTTP { response .status_code } : "
153- f"{ response .text } "
154- )
155-
156- result = response .json ()
129+ try :
130+ response = requests .post (
131+ url , json = body , headers = self ._get_headers (), timeout = timeouts
132+ )
133+ except (
134+ requests .exceptions .ConnectionError ,
135+ requests .exceptions .Timeout ,
136+ ) as e :
137+ raise _SubmissionError (
138+ "Prediction submission did not return a response. The task may already "
139+ "have been created, so the SDK will not retry the POST automatically."
140+ ) from e
157141
158- if enable_sync_mode :
159- return None , result
142+ if response .status_code != 200 :
143+ raise _SubmissionError (
144+ f"Failed to submit prediction: HTTP { response .status_code } : { response .text } "
145+ )
160146
161- request_id = result .get ("data" , {}).get ("id" )
162- if not request_id :
163- raise RuntimeError (f"No request ID in response: { result } " )
147+ result = response .json ()
164148
165- return request_id , None
149+ if enable_sync_mode :
150+ return None , result
166151
167- except (
168- requests .exceptions .ConnectionError ,
169- requests .exceptions .Timeout ,
170- ) as e :
171- last_error = e
172- print (
173- f"Connection error on attempt { retry + 1 } /{ self .max_connection_retries + 1 } :"
174- )
175- traceback .print_exc ()
152+ request_id = result .get ("data" , {}).get ("id" )
153+ if not request_id :
154+ raise _SubmissionError (f"No request ID in response: { result } " )
176155
177- if retry < self .max_connection_retries :
178- delay = self .retry_interval * (retry + 1 )
179- print (f"Retrying in { delay } seconds..." )
180- time .sleep (delay )
181- else :
182- raise RuntimeError (
183- f"Failed to submit prediction after { self .max_connection_retries + 1 } attempts"
184- ) from e
156+ return request_id , None
185157
186158 def _get_result (
187159 self , request_id : str , timeout : float | None = None
@@ -315,7 +287,12 @@ def _is_retryable_error(self, error: Exception) -> bool:
315287 Returns:
316288 True if the error is retryable.
317289 """
318- # Always retry timeout and connection errors
290+ # Submission errors are ambiguous: the server may already have created
291+ # the task, so never turn them into another POST automatically.
292+ if isinstance (error , _SubmissionError ):
293+ return False
294+
295+ # Retry timeout and connection errors from result-query GETs.
319296 if isinstance (
320297 error ,
321298 (
@@ -366,7 +343,7 @@ def run(
366343 """Run a model and wait for the output.
367344
368345 Args:
369- model: Model identifier (e.g., "wavespeed-ai/flux-dev ").
346+ model: Model identifier (e.g., "wavespeed-ai/z-image/turbo ").
370347 input: Input parameters for the model.
371348 timeout: Maximum time to wait for completion (None = no timeout).
372349 poll_interval: Interval between status checks in seconds.
0 commit comments