wavespeed 1.0.7__tar.gz → 1.0.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wavespeed-1.0.7 → wavespeed-1.0.9}/PKG-INFO +5 -3
- {wavespeed-1.0.7 → wavespeed-1.0.9}/README.md +4 -2
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/_version.py +1 -1
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/api/__init__.py +3 -1
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/api/client.py +87 -8
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/config.py +0 -1
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/__init__.py +15 -4
- wavespeed-1.0.9/src/wavespeed/serverless/modules/health.py +61 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/PKG-INFO +5 -3
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/SOURCES.txt +1 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_api.py +37 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_config.py +1 -3
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.github/workflows/claude-code-review.yml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.github/workflows/claude.yml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.github/workflows/pre-commit.yml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.github/workflows/python-packages.yml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.github/workflows/python-publish.yml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.gitignore +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/.pre-commit-config.yaml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/CLAUDE.md +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/LICENSE +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/MANIFEST.in +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/VERSIONING.md +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/build_and_push.sh +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/test_worker/Dockerfile +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/test_worker/build_and_test.sh +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/test_worker/handler.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/test_worker/requirements.txt +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/images/test_worker/test_input.json +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/pyproject.toml +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/pytest.ini +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/setup.cfg +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/_config_module.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/fastapi.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/handler.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/heartbeat.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/http.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/job.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/local.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/logger.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/progress.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/scaler.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/modules/state.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/boto3_utils.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/utils/validator.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed/serverless/worker.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/dependency_links.txt +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/not-zip-safe +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/requires.txt +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/src/wavespeed.egg-info/top_level.txt +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/conftest.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_fastapi.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_handler.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_http.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_job.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_local.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_logger.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_progress.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_scaler.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_modules/test_state.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_utils/__init__.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_utils/test_validator.py +0 -0
- {wavespeed-1.0.7 → wavespeed-1.0.9}/tests/test_serverless/test_worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespeed
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: WaveSpeedAI Python Client — Official Python SDK for WaveSpeedAI inference platform
|
|
5
5
|
Author-email: WaveSpeedAI <support@wavespeed.ai>
|
|
6
6
|
License: MIT
|
|
@@ -93,13 +93,15 @@ output = wavespeed.run(
|
|
|
93
93
|
{"prompt": "Cat"},
|
|
94
94
|
timeout=36000.0, # Max wait time in seconds (default: 36000.0)
|
|
95
95
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
96
|
-
enable_sync_mode=False, #
|
|
96
|
+
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
97
97
|
)
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
### Sync Mode
|
|
101
101
|
|
|
102
|
-
Use `enable_sync_mode=True`
|
|
102
|
+
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
103
|
+
request. If the server-side sync wait times out, the SDK raises an error with
|
|
104
|
+
the task ID/result URL; the task continues processing and can be queried later.
|
|
103
105
|
|
|
104
106
|
> **Note:** Not all models support sync mode. Check the model documentation for availability.
|
|
105
107
|
|
|
@@ -64,13 +64,15 @@ output = wavespeed.run(
|
|
|
64
64
|
{"prompt": "Cat"},
|
|
65
65
|
timeout=36000.0, # Max wait time in seconds (default: 36000.0)
|
|
66
66
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
67
|
-
enable_sync_mode=False, #
|
|
67
|
+
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
68
68
|
)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
### Sync Mode
|
|
72
72
|
|
|
73
|
-
Use `enable_sync_mode=True`
|
|
73
|
+
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
74
|
+
request. If the server-side sync wait times out, the SDK raises an error with
|
|
75
|
+
the task ID/result URL; the task continues processing and can be queried later.
|
|
74
76
|
|
|
75
77
|
> **Note:** Not all models support sync mode. Check the model documentation for availability.
|
|
76
78
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by setuptools_scm
|
|
2
|
-
__version__ = "1.0.
|
|
2
|
+
__version__ = "1.0.9"
|
|
@@ -51,7 +51,9 @@ def run(
|
|
|
51
51
|
input: Input parameters for the model.
|
|
52
52
|
timeout: Maximum time to wait for completion (None = no timeout).
|
|
53
53
|
poll_interval: Interval between status checks in seconds.
|
|
54
|
-
enable_sync_mode: If True, use synchronous mode (single
|
|
54
|
+
enable_sync_mode: If True, use synchronous mode (best-effort single
|
|
55
|
+
request). If the server-side sync wait times out, an error is
|
|
56
|
+
raised with the task ID so the result can be queried later.
|
|
55
57
|
max_retries: Maximum retries for this request (overrides default setting).
|
|
56
58
|
|
|
57
59
|
Returns:
|
|
@@ -9,6 +9,9 @@ import requests
|
|
|
9
9
|
|
|
10
10
|
from wavespeed.config import api as api_config
|
|
11
11
|
|
|
12
|
+
# HTTP status codes that are safe to retry
|
|
13
|
+
_RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class Client:
|
|
14
17
|
"""WaveSpeed API client.
|
|
@@ -25,7 +28,7 @@ class Client:
|
|
|
25
28
|
client = Client(api_key="your-api-key")
|
|
26
29
|
output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"})
|
|
27
30
|
|
|
28
|
-
# With sync mode (single request, waits for result)
|
|
31
|
+
# With sync mode (best-effort single request, waits for result)
|
|
29
32
|
output = client.run("wavespeed-ai/z-image/turbo", {"prompt": "Cat"}, enable_sync_mode=True)
|
|
30
33
|
|
|
31
34
|
# With retry
|
|
@@ -57,6 +60,18 @@ class Client:
|
|
|
57
60
|
retry_interval if retry_interval is not None else api_config.retry_interval
|
|
58
61
|
)
|
|
59
62
|
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _is_retryable_status(status_code: int) -> bool:
|
|
65
|
+
"""Check if an HTTP status code is retryable.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
status_code: HTTP response status code.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if the status code indicates a transient error worth retrying.
|
|
72
|
+
"""
|
|
73
|
+
return status_code in _RETRYABLE_STATUS_CODES
|
|
74
|
+
|
|
60
75
|
def _get_headers(self) -> dict[str, str]:
|
|
61
76
|
"""Get request headers with authentication."""
|
|
62
77
|
if not self.api_key:
|
|
@@ -106,6 +121,8 @@ class Client:
|
|
|
106
121
|
)
|
|
107
122
|
timeouts = (connect_timeout, request_timeout)
|
|
108
123
|
|
|
124
|
+
last_error: Exception | None = None
|
|
125
|
+
|
|
109
126
|
for retry in range(self.max_connection_retries + 1):
|
|
110
127
|
try:
|
|
111
128
|
response = requests.post(
|
|
@@ -113,6 +130,24 @@ class Client:
|
|
|
113
130
|
)
|
|
114
131
|
|
|
115
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
|
|
116
151
|
raise RuntimeError(
|
|
117
152
|
f"Failed to submit prediction: HTTP {response.status_code}: "
|
|
118
153
|
f"{response.text}"
|
|
@@ -133,6 +168,7 @@ class Client:
|
|
|
133
168
|
requests.exceptions.ConnectionError,
|
|
134
169
|
requests.exceptions.Timeout,
|
|
135
170
|
) as e:
|
|
171
|
+
last_error = e
|
|
136
172
|
print(
|
|
137
173
|
f"Connection error on attempt {retry + 1}/{self.max_connection_retries + 1}:"
|
|
138
174
|
)
|
|
@@ -171,6 +207,8 @@ class Client:
|
|
|
171
207
|
)
|
|
172
208
|
timeouts = (connect_timeout, request_timeout)
|
|
173
209
|
|
|
210
|
+
last_error: Exception | None = None
|
|
211
|
+
|
|
174
212
|
for retry in range(self.max_connection_retries + 1):
|
|
175
213
|
try:
|
|
176
214
|
response = requests.get(
|
|
@@ -178,6 +216,23 @@ class Client:
|
|
|
178
216
|
)
|
|
179
217
|
|
|
180
218
|
if response.status_code != 200:
|
|
219
|
+
# Retry on transient server errors (5xx) and rate limiting (429)
|
|
220
|
+
if self._is_retryable_status(response.status_code):
|
|
221
|
+
last_error = RuntimeError(
|
|
222
|
+
f"Failed to get result for task {request_id}: "
|
|
223
|
+
f"HTTP {response.status_code}: {response.text}"
|
|
224
|
+
)
|
|
225
|
+
if retry < self.max_connection_retries:
|
|
226
|
+
delay = self.retry_interval * (retry + 1)
|
|
227
|
+
print(
|
|
228
|
+
f"Server error (HTTP {response.status_code}) getting result "
|
|
229
|
+
f"on attempt {retry + 1}/{self.max_connection_retries + 1}, "
|
|
230
|
+
f"retrying in {delay} seconds..."
|
|
231
|
+
)
|
|
232
|
+
time.sleep(delay)
|
|
233
|
+
continue
|
|
234
|
+
raise last_error
|
|
235
|
+
|
|
181
236
|
raise RuntimeError(
|
|
182
237
|
f"Failed to get result for task {request_id}: "
|
|
183
238
|
f"HTTP {response.status_code}: {response.text}"
|
|
@@ -189,6 +244,7 @@ class Client:
|
|
|
189
244
|
requests.exceptions.ConnectionError,
|
|
190
245
|
requests.exceptions.Timeout,
|
|
191
246
|
) as e:
|
|
247
|
+
last_error = e
|
|
192
248
|
print(
|
|
193
249
|
f"Connection error getting result on attempt {retry + 1}/{self.max_connection_retries + 1}:"
|
|
194
250
|
)
|
|
@@ -261,7 +317,12 @@ class Client:
|
|
|
261
317
|
"""
|
|
262
318
|
# Always retry timeout and connection errors
|
|
263
319
|
if isinstance(
|
|
264
|
-
error,
|
|
320
|
+
error,
|
|
321
|
+
(
|
|
322
|
+
requests.exceptions.Timeout,
|
|
323
|
+
requests.exceptions.ConnectionError,
|
|
324
|
+
TimeoutError,
|
|
325
|
+
),
|
|
265
326
|
):
|
|
266
327
|
return True
|
|
267
328
|
|
|
@@ -273,6 +334,25 @@ class Client:
|
|
|
273
334
|
|
|
274
335
|
return False
|
|
275
336
|
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _format_sync_mode_error(data: dict[str, Any]) -> str:
|
|
339
|
+
"""Build an actionable error for a non-completed sync-mode response."""
|
|
340
|
+
request_id = data.get("id") or "unknown"
|
|
341
|
+
error = data.get("error") or "Unknown error"
|
|
342
|
+
urls = data.get("urls") or {}
|
|
343
|
+
result_url = urls.get("get") if isinstance(urls, dict) else None
|
|
344
|
+
|
|
345
|
+
is_sync_timeout = data.get("code") == 5004 or (
|
|
346
|
+
data.get("status") == "processing" and "Sync mode timed out" in error
|
|
347
|
+
)
|
|
348
|
+
if is_sync_timeout:
|
|
349
|
+
message = f"Sync mode timed out (task_id: {request_id}): {error}"
|
|
350
|
+
if result_url and result_url not in message:
|
|
351
|
+
message += f" Query the result later at: {result_url}"
|
|
352
|
+
return message
|
|
353
|
+
|
|
354
|
+
return f"Prediction failed (task_id: {request_id}): {error}"
|
|
355
|
+
|
|
276
356
|
def run(
|
|
277
357
|
self,
|
|
278
358
|
model: str,
|
|
@@ -290,7 +370,9 @@ class Client:
|
|
|
290
370
|
input: Input parameters for the model.
|
|
291
371
|
timeout: Maximum time to wait for completion (None = no timeout).
|
|
292
372
|
poll_interval: Interval between status checks in seconds.
|
|
293
|
-
enable_sync_mode: If True, use synchronous mode (single
|
|
373
|
+
enable_sync_mode: If True, use synchronous mode (best-effort single
|
|
374
|
+
request). If the server-side sync wait times out, the SDK raises
|
|
375
|
+
an error with the task ID so the result can be queried later.
|
|
294
376
|
max_retries: Maximum task-level retries (overrides client setting).
|
|
295
377
|
|
|
296
378
|
Returns:
|
|
@@ -314,12 +396,8 @@ class Client:
|
|
|
314
396
|
# In sync mode, extract outputs from the result
|
|
315
397
|
status = sync_result.get("data", {}).get("status")
|
|
316
398
|
if status != "completed":
|
|
317
|
-
error = (
|
|
318
|
-
sync_result.get("data", {}).get("error") or "Unknown error"
|
|
319
|
-
)
|
|
320
|
-
request_id = sync_result.get("data", {}).get("id", "unknown")
|
|
321
399
|
raise RuntimeError(
|
|
322
|
-
|
|
400
|
+
self._format_sync_mode_error(sync_result.get("data", {}))
|
|
323
401
|
)
|
|
324
402
|
data = sync_result.get("data", {})
|
|
325
403
|
return {"outputs": data.get("outputs", [])}
|
|
@@ -328,6 +406,7 @@ class Client:
|
|
|
328
406
|
|
|
329
407
|
except Exception as e:
|
|
330
408
|
last_error = e
|
|
409
|
+
|
|
331
410
|
is_retryable = self._is_retryable_error(e)
|
|
332
411
|
|
|
333
412
|
if not is_retryable or attempt >= task_retries:
|
|
@@ -84,7 +84,6 @@ def _detect_serverless_env() -> Optional[str]:
|
|
|
84
84
|
The serverless environment type ("runpod", "waverless") or None
|
|
85
85
|
if not running in a known serverless environment.
|
|
86
86
|
"""
|
|
87
|
-
|
|
88
87
|
# Check for native Waverless environment
|
|
89
88
|
if os.environ.get("WAVERLESS_ENDPOINT_ID"):
|
|
90
89
|
return "waverless"
|
|
@@ -56,8 +56,8 @@ def _parse_args() -> argparse.Namespace:
|
|
|
56
56
|
"--waverless_api_host",
|
|
57
57
|
"--rp_api_host",
|
|
58
58
|
type=str,
|
|
59
|
-
default="
|
|
60
|
-
help="API server host (default:
|
|
59
|
+
default="0.0.0.0",
|
|
60
|
+
help="API server host (default: 0.0.0.0)",
|
|
61
61
|
)
|
|
62
62
|
parser.add_argument(
|
|
63
63
|
"--waverless_api_port",
|
|
@@ -160,6 +160,17 @@ def start(config: Dict[str, Any]) -> None:
|
|
|
160
160
|
port=args.waverless_api_port,
|
|
161
161
|
)
|
|
162
162
|
else:
|
|
163
|
-
# Standard worker mode
|
|
163
|
+
# Standard worker mode with health check server
|
|
164
164
|
log.info("Starting serverless worker...")
|
|
165
|
-
|
|
165
|
+
from .modules.health import HealthServer
|
|
166
|
+
|
|
167
|
+
health_server = HealthServer(
|
|
168
|
+
host=args.waverless_api_host,
|
|
169
|
+
port=args.waverless_api_port,
|
|
170
|
+
)
|
|
171
|
+
health_server.start()
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
run_worker(config)
|
|
175
|
+
finally:
|
|
176
|
+
health_server.stop()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Lightweight health check HTTP server for worker mode."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .logger import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HealthHandler(BaseHTTPRequestHandler):
|
|
11
|
+
"""Simple HTTP handler for health checks."""
|
|
12
|
+
|
|
13
|
+
def do_GET(self) -> None:
|
|
14
|
+
"""Handle GET requests."""
|
|
15
|
+
if self.path == "/health":
|
|
16
|
+
self.send_response(200)
|
|
17
|
+
self.send_header("Content-Type", "application/json")
|
|
18
|
+
self.end_headers()
|
|
19
|
+
self.wfile.write(b'{"status": "ok"}')
|
|
20
|
+
else:
|
|
21
|
+
self.send_response(404)
|
|
22
|
+
self.end_headers()
|
|
23
|
+
|
|
24
|
+
def log_message(self, format: str, *args) -> None:
|
|
25
|
+
"""Suppress default logging."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HealthServer:
|
|
30
|
+
"""Background health check server."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
|
33
|
+
"""Initialize the health server.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
host: Host to bind to.
|
|
37
|
+
port: Port to bind to.
|
|
38
|
+
"""
|
|
39
|
+
self.host = host
|
|
40
|
+
self.port = port
|
|
41
|
+
self._server: Optional[HTTPServer] = None
|
|
42
|
+
self._thread: Optional[threading.Thread] = None
|
|
43
|
+
|
|
44
|
+
def start(self) -> None:
|
|
45
|
+
"""Start the health server in a background thread."""
|
|
46
|
+
try:
|
|
47
|
+
self._server = HTTPServer((self.host, self.port), HealthHandler)
|
|
48
|
+
self._thread = threading.Thread(
|
|
49
|
+
target=self._server.serve_forever, daemon=True
|
|
50
|
+
)
|
|
51
|
+
self._thread.start()
|
|
52
|
+
log.info(f"Health server started at http://{self.host}:{self.port}/health")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
log.error(f"Failed to start health server: {e}")
|
|
55
|
+
|
|
56
|
+
def stop(self) -> None:
|
|
57
|
+
"""Stop the health server."""
|
|
58
|
+
if self._server:
|
|
59
|
+
self._server.shutdown()
|
|
60
|
+
self._server = None
|
|
61
|
+
log.debug("Health server stopped")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wavespeed
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: WaveSpeedAI Python Client — Official Python SDK for WaveSpeedAI inference platform
|
|
5
5
|
Author-email: WaveSpeedAI <support@wavespeed.ai>
|
|
6
6
|
License: MIT
|
|
@@ -93,13 +93,15 @@ output = wavespeed.run(
|
|
|
93
93
|
{"prompt": "Cat"},
|
|
94
94
|
timeout=36000.0, # Max wait time in seconds (default: 36000.0)
|
|
95
95
|
poll_interval=1.0, # Status check interval (default: 1.0)
|
|
96
|
-
enable_sync_mode=False, #
|
|
96
|
+
enable_sync_mode=False, # Best-effort sync result attempt (default: False)
|
|
97
97
|
)
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
### Sync Mode
|
|
101
101
|
|
|
102
|
-
Use `enable_sync_mode=True`
|
|
102
|
+
Use `enable_sync_mode=True` to ask the API to wait for the result in the initial
|
|
103
|
+
request. If the server-side sync wait times out, the SDK raises an error with
|
|
104
|
+
the task ID/result URL; the task continues processing and can be queried later.
|
|
103
105
|
|
|
104
106
|
> **Note:** Not all models support sync mode. Check the model documentation for availability.
|
|
105
107
|
|
|
@@ -36,6 +36,7 @@ src/wavespeed/serverless/worker.py
|
|
|
36
36
|
src/wavespeed/serverless/modules/__init__.py
|
|
37
37
|
src/wavespeed/serverless/modules/fastapi.py
|
|
38
38
|
src/wavespeed/serverless/modules/handler.py
|
|
39
|
+
src/wavespeed/serverless/modules/health.py
|
|
39
40
|
src/wavespeed/serverless/modules/heartbeat.py
|
|
40
41
|
src/wavespeed/serverless/modules/http.py
|
|
41
42
|
src/wavespeed/serverless/modules/job.py
|
|
@@ -171,6 +171,43 @@ class TestClient(unittest.TestCase):
|
|
|
171
171
|
client.run("wavespeed-ai/z-image/turbo", {"prompt": "test"}, timeout=10)
|
|
172
172
|
self.assertIn("timed out", str(ctx.exception))
|
|
173
173
|
|
|
174
|
+
@patch("wavespeed.api.client.requests.get")
|
|
175
|
+
@patch("wavespeed.api.client.requests.post")
|
|
176
|
+
def test_run_sync_mode_timeout_raises_without_fallback(self, mock_post, mock_get):
|
|
177
|
+
"""Test sync-mode timeout keeps the task queryable without async fallback."""
|
|
178
|
+
result_url = "https://api.wavespeed.ai/api/v3/predictions/req-timeout/result"
|
|
179
|
+
mock_post_response = MagicMock()
|
|
180
|
+
mock_post_response.status_code = 200
|
|
181
|
+
mock_post_response.json.return_value = {
|
|
182
|
+
"data": {
|
|
183
|
+
"id": "req-timeout",
|
|
184
|
+
"status": "processing",
|
|
185
|
+
"code": 5004,
|
|
186
|
+
"error": (
|
|
187
|
+
"Sync mode timed out after 90 seconds. The prediction is "
|
|
188
|
+
"still processing asynchronously."
|
|
189
|
+
),
|
|
190
|
+
"urls": {"get": result_url},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
mock_post.return_value = mock_post_response
|
|
194
|
+
|
|
195
|
+
client = Client(api_key="test-key")
|
|
196
|
+
with self.assertRaises(RuntimeError) as ctx:
|
|
197
|
+
client.run(
|
|
198
|
+
"wavespeed-ai/z-image/turbo",
|
|
199
|
+
{"prompt": "test"},
|
|
200
|
+
enable_sync_mode=True,
|
|
201
|
+
max_retries=1,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
error = str(ctx.exception)
|
|
205
|
+
self.assertIn("Sync mode timed out", error)
|
|
206
|
+
self.assertIn("req-timeout", error)
|
|
207
|
+
self.assertIn(result_url, error)
|
|
208
|
+
mock_post.assert_called_once()
|
|
209
|
+
mock_get.assert_not_called()
|
|
210
|
+
|
|
174
211
|
|
|
175
212
|
class TestModuleLevelRun(unittest.TestCase):
|
|
176
213
|
"""Tests for the module-level run() function."""
|
|
@@ -43,9 +43,7 @@ class TestResolveWaverlessUrl(unittest.TestCase):
|
|
|
43
43
|
"""Test that $WAVERLESS_POD_ID is replaced with pod_id."""
|
|
44
44
|
template = "https://api.wavespeed.ai/v2/test/job-take/$WAVERLESS_POD_ID"
|
|
45
45
|
result = _resolve_waverless_url(template, "my-pod-123")
|
|
46
|
-
self.assertEqual(
|
|
47
|
-
result, "https://api.wavespeed.ai/v2/test/job-take/my-pod-123"
|
|
48
|
-
)
|
|
46
|
+
self.assertEqual(result, "https://api.wavespeed.ai/v2/test/job-take/my-pod-123")
|
|
49
47
|
|
|
50
48
|
def test_preserves_id_placeholder(self):
|
|
51
49
|
"""Test that $ID is NOT replaced - it's for job/worker ID at runtime."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|