sume-client 0.1.0__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.
- sume_client-0.1.0/LICENSE +21 -0
- sume_client-0.1.0/NOTICE +17 -0
- sume_client-0.1.0/PKG-INFO +529 -0
- sume_client-0.1.0/README.md +503 -0
- sume_client-0.1.0/pyproject.toml +38 -0
- sume_client-0.1.0/src/sume_client/__init__.py +74 -0
- sume_client-0.1.0/src/sume_client/auth.py +15 -0
- sume_client-0.1.0/src/sume_client/exceptions.py +22 -0
- sume_client-0.1.0/src/sume_client/http/__init__.py +0 -0
- sume_client-0.1.0/src/sume_client/http/client.py +495 -0
- sume_client-0.1.0/src/sume_client/http/error.py +91 -0
- sume_client-0.1.0/src/sume_client/http/retry.py +85 -0
- sume_client-0.1.0/src/sume_client/http/transport.py +129 -0
- sume_client-0.1.0/src/sume_client/mixins/__init__.py +3 -0
- sume_client-0.1.0/src/sume_client/mixins/upload.py +25 -0
- sume_client-0.1.0/src/sume_client/types_.py +49 -0
- sume_client-0.1.0/src/sume_client/utils.py +77 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sume Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sume_client-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
sume-client
|
|
2
|
+
Copyright (c) 2026 Sume Labs
|
|
3
|
+
|
|
4
|
+
sume-client follows the public interface of higgsfield-client, the Python
|
|
5
|
+
client for the Higgsfield API: its module functions, client and controller
|
|
6
|
+
classes, status classes, retry strategies, module layout and README outline.
|
|
7
|
+
The code and the text of sume-client were written for the Sume Open API
|
|
8
|
+
(https://api.open.sume.com).
|
|
9
|
+
|
|
10
|
+
higgsfield-client source:
|
|
11
|
+
https://github.com/higgsfield-ai/higgsfield-client (commit aefd1ca)
|
|
12
|
+
|
|
13
|
+
higgsfield-client license:
|
|
14
|
+
Apache License 2.0, Copyright 2025 Higgsfield, Inc.
|
|
15
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
Its LICENSE file is reproduced in THIRD-PARTY-NOTICES.txt in the
|
|
17
|
+
sume-client repository.
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sume-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sume Open API Python SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Author: Sume Labs
|
|
9
|
+
Author-email: dev@sume.com
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Requires-Dist: httpx (>=0.21.0,<0.29.0)
|
|
21
|
+
Project-URL: Documentation, https://open.sume.com/docs
|
|
22
|
+
Project-URL: Homepage, https://open.sume.com
|
|
23
|
+
Project-URL: Repository, https://github.com/sumelabs/sume-client
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Sume Python Client
|
|
27
|
+
|
|
28
|
+
The official Python SDK for the [Sume Open API](https://open.sume.com/docs), with a synchronous and an asynchronous client.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
sume-client is not on PyPI yet. Install it from GitHub:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install git+https://github.com/sumelabs/sume-client.git
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Authentication
|
|
39
|
+
|
|
40
|
+
Before using the client, set your API key as an environment variable:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export SUME_API_KEY="sume_live_..."
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Get your key from [open.sume.com → API keys](https://open.sume.com/api-keys). The client sends it as `Authorization: Bearer <key>` to `https://api.open.sume.com`.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
**Synchronous:**
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import sume_client
|
|
54
|
+
|
|
55
|
+
result = sume_client.subscribe(
|
|
56
|
+
'gpt-image-2',
|
|
57
|
+
arguments={
|
|
58
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
59
|
+
'quality': 'low',
|
|
60
|
+
'aspect_ratio': '16:9',
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
print(result['result']['artifacts'][0]['url'])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Asynchronous:**
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
|
|
72
|
+
import sume_client
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
result = await sume_client.subscribe_async(
|
|
76
|
+
'gpt-image-2',
|
|
77
|
+
arguments={
|
|
78
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
79
|
+
'quality': 'low',
|
|
80
|
+
'aspect_ratio': '16:9',
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
print(result['result']['artifacts'][0]['url'])
|
|
85
|
+
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`gpt-image-2` at `quality: 'low'` costs under a cent. [List open models](https://open.sume.com/docs/reference/catalog/list) gives every model's price, and [Estimate a price](https://open.sume.com/docs/reference/catalog/estimate) prices a request before you send it.
|
|
90
|
+
|
|
91
|
+
## Usage Patterns
|
|
92
|
+
|
|
93
|
+
### Pattern 1: Simple Submit and Wait
|
|
94
|
+
|
|
95
|
+
`subscribe()` submits the job and returns when it has ended:
|
|
96
|
+
|
|
97
|
+
**Synchronous:**
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import sume_client
|
|
101
|
+
|
|
102
|
+
result = sume_client.subscribe(
|
|
103
|
+
'gpt-image-2',
|
|
104
|
+
arguments={
|
|
105
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
106
|
+
'quality': 'low',
|
|
107
|
+
'aspect_ratio': '16:9',
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print(result['result']['artifacts'][0]['url'])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Asynchronous:**
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import asyncio
|
|
118
|
+
|
|
119
|
+
import sume_client
|
|
120
|
+
|
|
121
|
+
async def main():
|
|
122
|
+
result = await sume_client.subscribe_async(
|
|
123
|
+
'gpt-image-2',
|
|
124
|
+
arguments={
|
|
125
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
126
|
+
'quality': 'low',
|
|
127
|
+
'aspect_ratio': '16:9',
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print(result['result']['artifacts'][0]['url'])
|
|
132
|
+
|
|
133
|
+
asyncio.run(main())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Pattern 2: Submit and Track Progress
|
|
137
|
+
|
|
138
|
+
`submit()` returns a request controller at once, and `poll_request_status()` yields each status until the job ends. `webhook_url` is optional: Sume POSTs the outcome to that public HTTPS URL when the job ends ([Webhooks](https://open.sume.com/docs/webhooks)).
|
|
139
|
+
|
|
140
|
+
**Synchronous:**
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import sume_client
|
|
144
|
+
|
|
145
|
+
request_controller = sume_client.submit(
|
|
146
|
+
'gpt-image-2',
|
|
147
|
+
arguments={
|
|
148
|
+
'prompt': 'Football ball',
|
|
149
|
+
'quality': 'low',
|
|
150
|
+
'aspect_ratio': '16:9',
|
|
151
|
+
},
|
|
152
|
+
webhook_url='https://example.com/webhook'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for status in request_controller.poll_request_status():
|
|
156
|
+
if isinstance(status, sume_client.Queued):
|
|
157
|
+
print('Queued')
|
|
158
|
+
elif isinstance(status, sume_client.InProgress):
|
|
159
|
+
print('In progress')
|
|
160
|
+
elif isinstance(status, sume_client.Completed):
|
|
161
|
+
print('Completed')
|
|
162
|
+
elif isinstance(status, (sume_client.Failed, sume_client.NSFW, sume_client.Cancelled)):
|
|
163
|
+
print('Oops!')
|
|
164
|
+
|
|
165
|
+
result = request_controller.get()
|
|
166
|
+
print(result['result']['artifacts'][0]['url'])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Asynchronous:**
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import asyncio
|
|
173
|
+
|
|
174
|
+
import sume_client
|
|
175
|
+
|
|
176
|
+
async def main():
|
|
177
|
+
request_controller = await sume_client.submit_async(
|
|
178
|
+
'gpt-image-2',
|
|
179
|
+
arguments={
|
|
180
|
+
'prompt': 'Football ball',
|
|
181
|
+
'quality': 'low',
|
|
182
|
+
'aspect_ratio': '16:9',
|
|
183
|
+
},
|
|
184
|
+
webhook_url='https://example.com/webhook'
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async for status in request_controller.poll_request_status():
|
|
188
|
+
if isinstance(status, sume_client.Queued):
|
|
189
|
+
print('Queued')
|
|
190
|
+
elif isinstance(status, sume_client.InProgress):
|
|
191
|
+
print('In progress')
|
|
192
|
+
elif isinstance(status, sume_client.Completed):
|
|
193
|
+
print('Completed')
|
|
194
|
+
elif isinstance(status, (sume_client.Failed, sume_client.NSFW, sume_client.Cancelled)):
|
|
195
|
+
print('Oops!')
|
|
196
|
+
|
|
197
|
+
result = await request_controller.get()
|
|
198
|
+
print(result['result']['artifacts'][0]['url'])
|
|
199
|
+
|
|
200
|
+
asyncio.run(main())
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pattern 3: Submit with Callbacks
|
|
204
|
+
|
|
205
|
+
`on_enqueue` gets the request ID once the job exists, and `on_queue_update` gets every status:
|
|
206
|
+
|
|
207
|
+
**Synchronous:**
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
import sume_client
|
|
211
|
+
|
|
212
|
+
def on_enqueue(request_id):
|
|
213
|
+
print(f'Request {request_id} was enqueued')
|
|
214
|
+
|
|
215
|
+
def on_status_update(status):
|
|
216
|
+
print(f'Status: {status}')
|
|
217
|
+
|
|
218
|
+
result = sume_client.subscribe(
|
|
219
|
+
'gpt-image-2',
|
|
220
|
+
arguments={
|
|
221
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
222
|
+
'quality': 'low',
|
|
223
|
+
'aspect_ratio': '16:9',
|
|
224
|
+
},
|
|
225
|
+
on_enqueue=on_enqueue,
|
|
226
|
+
on_queue_update=on_status_update
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Asynchronous:**
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import asyncio
|
|
234
|
+
|
|
235
|
+
import sume_client
|
|
236
|
+
|
|
237
|
+
def on_enqueue(request_id):
|
|
238
|
+
print(f'Request {request_id} was enqueued')
|
|
239
|
+
|
|
240
|
+
def on_status_update(status):
|
|
241
|
+
print(f'Status: {status}')
|
|
242
|
+
|
|
243
|
+
async def main():
|
|
244
|
+
await sume_client.subscribe_async(
|
|
245
|
+
'gpt-image-2',
|
|
246
|
+
arguments={
|
|
247
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
248
|
+
'quality': 'low',
|
|
249
|
+
'aspect_ratio': '16:9',
|
|
250
|
+
},
|
|
251
|
+
on_enqueue=on_enqueue,
|
|
252
|
+
on_queue_update=on_status_update
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
asyncio.run(main())
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Pattern 4: Manage Existing Requests
|
|
259
|
+
|
|
260
|
+
Keep the request controller, or only the request ID, and check, cancel or wait for the job later:
|
|
261
|
+
|
|
262
|
+
**Synchronous:**
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
import sume_client
|
|
266
|
+
|
|
267
|
+
request_controller = sume_client.submit(
|
|
268
|
+
'gpt-image-2',
|
|
269
|
+
arguments={
|
|
270
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
271
|
+
'quality': 'low',
|
|
272
|
+
'aspect_ratio': '16:9',
|
|
273
|
+
},
|
|
274
|
+
webhook_url='https://example.com/webhook'
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
status = request_controller.status()
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
request_controller.cancel()
|
|
281
|
+
except sume_client.SumeClientError as error:
|
|
282
|
+
print(error.code)
|
|
283
|
+
|
|
284
|
+
result = request_controller.get()
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`status()` checks the status once, `cancel()` asks Sume to cancel the job, and `get()` waits for the job to end and returns its result.
|
|
288
|
+
|
|
289
|
+
Or by request ID:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
import sume_client
|
|
293
|
+
|
|
294
|
+
request_id = 'job_7513b52d-3669-4ffe-b317-8b02000715eb'
|
|
295
|
+
|
|
296
|
+
status = sume_client.status(request_id=request_id)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
sume_client.cancel(request_id=request_id)
|
|
300
|
+
except sume_client.SumeClientError as error:
|
|
301
|
+
print(error.code)
|
|
302
|
+
|
|
303
|
+
result = sume_client.result(request_id=request_id)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Asynchronous:**
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
import asyncio
|
|
310
|
+
|
|
311
|
+
import sume_client
|
|
312
|
+
|
|
313
|
+
async def main():
|
|
314
|
+
request_controller = await sume_client.submit_async(
|
|
315
|
+
'gpt-image-2',
|
|
316
|
+
arguments={
|
|
317
|
+
'prompt': 'A serene lake at sunset with mountains',
|
|
318
|
+
'quality': 'low',
|
|
319
|
+
'aspect_ratio': '16:9',
|
|
320
|
+
},
|
|
321
|
+
webhook_url='https://example.com/webhook'
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
status = await request_controller.status()
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
await request_controller.cancel()
|
|
328
|
+
except sume_client.SumeClientError as error:
|
|
329
|
+
print(error.code)
|
|
330
|
+
|
|
331
|
+
result = await request_controller.get()
|
|
332
|
+
|
|
333
|
+
asyncio.run(main())
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Or by request ID:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
import asyncio
|
|
340
|
+
|
|
341
|
+
import sume_client
|
|
342
|
+
|
|
343
|
+
async def main():
|
|
344
|
+
request_id = 'job_7513b52d-3669-4ffe-b317-8b02000715eb'
|
|
345
|
+
|
|
346
|
+
status = await sume_client.status_async(request_id=request_id)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
await sume_client.cancel_async(request_id=request_id)
|
|
350
|
+
except sume_client.SumeClientError as error:
|
|
351
|
+
print(error.code)
|
|
352
|
+
|
|
353
|
+
result = await sume_client.result_async(request_id=request_id)
|
|
354
|
+
|
|
355
|
+
asyncio.run(main())
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Sume cancels a job only before generation starts, and the image and video routers hand the job to the model inside `submit()`. So `cancel()` usually raises `SumeClientError` with code `job_generation_already_started`, or `job_not_cancelable` once the job has ended. A canceled job is refunded.
|
|
359
|
+
|
|
360
|
+
## File Uploads
|
|
361
|
+
|
|
362
|
+
Put a local file in Sume storage and pass its URL to a model. Each upload returns a public HTTPS URL on `media.sume.com` that you pass in a model's input field, such as `image_urls`, `image_url`, `video_url` or `audio_url` ([Input media](https://open.sume.com/docs/input-media)). Uploading is free.
|
|
363
|
+
|
|
364
|
+
### Upload bytes
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
import sume_client
|
|
368
|
+
|
|
369
|
+
image_path = 'path/to/example.jpeg'
|
|
370
|
+
content_type = 'image/jpeg'
|
|
371
|
+
|
|
372
|
+
with open(image_path, 'rb') as f:
|
|
373
|
+
data = f.read()
|
|
374
|
+
|
|
375
|
+
url = sume_client.upload(data, content_type)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Upload by path
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
import sume_client
|
|
382
|
+
|
|
383
|
+
image_path = 'path/to/example.jpeg'
|
|
384
|
+
|
|
385
|
+
url = sume_client.upload_file(image_path)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Upload PIL Images
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from PIL import Image
|
|
392
|
+
import sume_client
|
|
393
|
+
|
|
394
|
+
image = Image.open('example.jpeg')
|
|
395
|
+
url = sume_client.upload_image(image, format='jpeg')
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Async Uploads
|
|
399
|
+
|
|
400
|
+
Each upload function has an `_async` twin:
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
import sume_client
|
|
404
|
+
|
|
405
|
+
url = await sume_client.upload_async(data, content_type='image/jpeg')
|
|
406
|
+
|
|
407
|
+
url = await sume_client.upload_file_async('path/to/example.jpeg')
|
|
408
|
+
|
|
409
|
+
url = await sume_client.upload_image_async(image, format='jpeg')
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Use an upload
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
import sume_client
|
|
416
|
+
|
|
417
|
+
url = sume_client.upload_file('path/to/example.jpeg')
|
|
418
|
+
|
|
419
|
+
result = sume_client.subscribe(
|
|
420
|
+
'seedance-2.5',
|
|
421
|
+
arguments={
|
|
422
|
+
'prompt': 'The camera slowly pushes in',
|
|
423
|
+
'image_url': url,
|
|
424
|
+
'duration': 5,
|
|
425
|
+
'resolution': '480p',
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
print(result['result']['artifacts'][0]['url'])
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Models and Arguments
|
|
433
|
+
|
|
434
|
+
The first argument is a model id from the open catalog, such as `gpt-image-2`, `nano-banana-pro`, `seedance-2.5` or `kling-3`. [List open models](https://open.sume.com/docs/reference/catalog/list) (`GET https://api.open.sume.com/v1/open/catalog`, no key needed) lists them all.
|
|
435
|
+
|
|
436
|
+
The client reads the model's `kind` once per client from `GET /v1/open/catalog/{model_id}`. Image models go to `POST /v1/image-router/generate`; video, lip-sync and caption models go to `POST /v1/video-router/generate`. `arguments` are the model's own fields (`prompt`, `quality`, `aspect_ratio`, `duration`, `resolution`, `image_url` and so on), described in [Generate an image](https://open.sume.com/docs/reference/images/generate) and [Generate a video](https://open.sume.com/docs/reference/video/generate).
|
|
437
|
+
|
|
438
|
+
## Results
|
|
439
|
+
|
|
440
|
+
`subscribe()`, `result()` and `get()` wait for the job to end and return the response's `data`:
|
|
441
|
+
|
|
442
|
+
- A completed job returns its result from `GET /v1/jobs/{id}/result`: `result['status']` is `COMPLETED`, and `result['result']['artifacts']` lists the outputs, each with a `url`, a `type` and a `content_type`.
|
|
443
|
+
- A failed or canceled job returns its final status from `GET /v1/jobs/{id}/status`: `result['status']` is `FAILED` or `CANCELED`, and `result['job']['error']` says why. Its price is refunded.
|
|
444
|
+
|
|
445
|
+
A request the model refuses at once raises `SumeClientError` from `submit()` or `subscribe()` instead, for example code `provider_submission_failed`.
|
|
446
|
+
|
|
447
|
+
## Statuses
|
|
448
|
+
|
|
449
|
+
`status()` and `poll_request_status()` return one of these classes. `DONE_STATUSES` holds the last four.
|
|
450
|
+
|
|
451
|
+
| Class | Job's `sume_status` |
|
|
452
|
+
|---|---|
|
|
453
|
+
| `Queued` | `queued` |
|
|
454
|
+
| `InProgress` | `processing` |
|
|
455
|
+
| `Completed` | `completed` |
|
|
456
|
+
| `Failed` | `failed` |
|
|
457
|
+
| `NSFW` | `failed`, with `error.public_reason` `content_policy_rejected` |
|
|
458
|
+
| `Cancelled` | `canceled` |
|
|
459
|
+
|
|
460
|
+
Polling asks for the status every 2 seconds, the API's `recommended_poll_interval_seconds`. Pass `poll_request_status(delay=...)` to change it.
|
|
461
|
+
|
|
462
|
+
## Errors
|
|
463
|
+
|
|
464
|
+
Every answer outside 2xx raises `sume_client.SumeClientError`. Its message is the API's message, with the failing fields of an `invalid_request`. `status_code`, `code`, `request_id` and `details` carry the rest of the [error](https://open.sume.com/docs/errors):
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
import sume_client
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
sume_client.subscribe('gpt-image-2', arguments={'prompt': 'A lake', 'quality': 'low'})
|
|
471
|
+
except sume_client.SumeClientError as error:
|
|
472
|
+
print(error.status_code, error.code, error.request_id, error)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
A missing key raises `sume_client.CredentialsMissedError` on the first call.
|
|
476
|
+
|
|
477
|
+
A request to the API is tried up to 3 times when the answer is 408, 429, 500, 502, 503 or 504, unless the error says `retryable: false`, and when the connection fails or times out. The client waits 0.2 s and then 0.4 s between tries, or longer when the API's `retry-after` asks for it (up to 60 s). After the last try, a connection failure raises the `httpx` error. Every generate request carries a new `Idempotency-Key`, and its retries reuse it, so the client's own retries never create a second, billed job.
|
|
478
|
+
|
|
479
|
+
The upload `PUT` goes to storage once, without your key. If storage refuses it, `SumeClientError` names the storage error code and leaves out the presigned URL.
|
|
480
|
+
|
|
481
|
+
## Clients
|
|
482
|
+
|
|
483
|
+
The module functions use two default clients, `sume_client.sync_client` and `sume_client.async_client`. Make your own to set the key, host or timeout in code:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from sume_client import AsyncClient, SyncClient
|
|
487
|
+
|
|
488
|
+
client = SyncClient(api_key='sume_live_...', base_url='https://api.open.sume.com', timeout=90.0)
|
|
489
|
+
result = client.subscribe('gpt-image-2', arguments={'prompt': 'A lake', 'quality': 'low'})
|
|
490
|
+
|
|
491
|
+
async_client = AsyncClient(api_key='sume_live_...')
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Every call goes to `base_url`. The client builds the job URLs from it and the request ID, and does not follow the hosts in API responses.
|
|
495
|
+
|
|
496
|
+
`AsyncClient` keeps one connection pool per event loop, so one client can serve several `asyncio.run()` calls and several threads.
|
|
497
|
+
|
|
498
|
+
## Coming from higgsfield-client
|
|
499
|
+
|
|
500
|
+
sume-client keeps [higgsfield-client](https://github.com/higgsfield-ai/higgsfield-client)'s module functions, clients, request controllers, status classes and retry strategies, with the same arguments. What changes:
|
|
501
|
+
|
|
502
|
+
| | higgsfield-client | sume-client |
|
|
503
|
+
|---|---|---|
|
|
504
|
+
| Package | `higgsfield_client` | `sume_client` |
|
|
505
|
+
| Host | `https://api.higgsfield.ai` | `https://api.open.sume.com` |
|
|
506
|
+
| Key | `HF_KEY`, or `HF_API_KEY` + `HF_API_SECRET` | `SUME_API_KEY` |
|
|
507
|
+
| Header | `Authorization: Key <key>` | `Authorization: Bearer <key>` |
|
|
508
|
+
| First argument | an application path | a model id |
|
|
509
|
+
| Webhook | `?hf_webhook=` | `mode: webhook` and `webhook_url` in the body |
|
|
510
|
+
| Result | the status response | the result's `data` (`result['result']['artifacts']`) |
|
|
511
|
+
| Error | `HiggsfieldClientError` | `SumeClientError`, with `status_code`, `code`, `request_id`, `details` |
|
|
512
|
+
| Upload | `content_type` | `content_type` and `size_bytes` |
|
|
513
|
+
| Polling | every 0.5 s | every 2 s |
|
|
514
|
+
| Cancel | a queued request | a job the model has not started; router jobs start inside `submit()` |
|
|
515
|
+
| Agent API | `client.agents` | not available: Open keys call the model API only |
|
|
516
|
+
|
|
517
|
+
## Development
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
pip install -e . pytest pytest-asyncio pytest-mock pillow
|
|
521
|
+
pytest
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
The tests run against a fake Sume API and spend nothing.
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
MIT. See [LICENSE](LICENSE). [NOTICE](NOTICE) credits higgsfield-client (Apache-2.0), whose public interface this client follows.
|
|
529
|
+
|