@sume-com/client 0.0.0-stage → 0.3.0
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.
- package/LICENSE +21 -0
- package/README.md +541 -2
- package/THIRD-PARTY-NOTICES.txt +714 -0
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +11 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +69 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +98 -0
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.js +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +12 -0
- package/dist/utils/retry.d.ts +11 -0
- package/dist/utils/retry.js +84 -0
- package/dist/v2/client.d.ts +24 -0
- package/dist/v2/client.js +379 -0
- package/dist/v2/index.d.ts +12 -0
- package/dist/v2/index.js +33 -0
- package/dist/v2/types.d.ts +58 -0
- package/dist/v2/types.js +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +79 -4
package/LICENSE
ADDED
|
@@ -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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,542 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sume SDK for Node.js and TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official SDK for Sume's image and video generation API, the [Sume Open API](https://open.sume.com/docs) at `https://api.open.sume.com`.
|
|
4
|
+
|
|
5
|
+
sume-js follows the shape of Higgsfield's JavaScript client, [higgsfield-js](https://github.com/higgsfield-ai/higgsfield-js): the same `config()`, `subscribe()`, `withPolling` and error classes. Only the calls and the host are Sume's. [Coming from Higgsfield](#coming-from-higgsfield) lists every difference.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @sume-com/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> Not on npm yet. Until the first release, install it from GitHub: `npm install github:sumelabs/sume-js`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Client
|
|
18
|
+
|
|
19
|
+
The client is exported from `@sume-com/client/v2`, the same path as Higgsfield's v2 client, and from the package root. It features:
|
|
20
|
+
- Single `credentials` field: one Sume API key (`sume_live_...`)
|
|
21
|
+
- `Authorization: Bearer <key>` authentication header
|
|
22
|
+
- `User-Agent: sume-server-js/<version>` header
|
|
23
|
+
- Server-side only (browser usage blocked for security)
|
|
24
|
+
- Simplified API with `subscribe()` method: name a model, or a router path
|
|
25
|
+
- Automatic polling via `/v1/jobs/{id}/status`, then the result from `/v1/jobs/{id}/result`
|
|
26
|
+
- Safe retries: every submit carries an `Idempotency-Key`, so a retried submit never makes a second, billed job
|
|
27
|
+
|
|
28
|
+
### Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { sume, config } from '@sume-com/client/v2';
|
|
32
|
+
|
|
33
|
+
// Configure credentials
|
|
34
|
+
config({
|
|
35
|
+
credentials: 'YOUR_SUME_API_KEY'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Generate content
|
|
39
|
+
const response = await sume.subscribe('gpt-image-2', {
|
|
40
|
+
input: {
|
|
41
|
+
prompt: 'A ceramic coffee cup on a wooden table, soft morning light',
|
|
42
|
+
quality: 'low',
|
|
43
|
+
aspect_ratio: '1:1'
|
|
44
|
+
},
|
|
45
|
+
withPolling: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (response.status === 'completed') {
|
|
49
|
+
console.log('Image URL:', response.images?.[0].url);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`gpt-image-2` at `quality: 'low'` costs $0.006 an image. Every model and its dollar price is in the [open model catalog](https://open.sume.com/docs/reference/catalog/list).
|
|
54
|
+
|
|
55
|
+
### Authentication
|
|
56
|
+
|
|
57
|
+
Make a key at [open.sume.com → API keys](https://open.sume.com/api-keys). A key spends your open.sume.com balance, in US dollars. Keep it on your server.
|
|
58
|
+
|
|
59
|
+
**Option 1: Single credentials field (recommended)**
|
|
60
|
+
```typescript
|
|
61
|
+
import { config } from '@sume-com/client/v2';
|
|
62
|
+
|
|
63
|
+
config({
|
|
64
|
+
credentials: 'YOUR_SUME_API_KEY'
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Option 2: `apiKey` field**
|
|
69
|
+
```typescript
|
|
70
|
+
config({
|
|
71
|
+
apiKey: 'YOUR_SUME_API_KEY'
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Option 3: Environment variable**
|
|
76
|
+
```bash
|
|
77
|
+
export SUME_API_KEY="sume_live_..."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// No config needed - automatically reads SUME_API_KEY from the environment
|
|
82
|
+
import { sume } from '@sume-com/client/v2';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Creating a Client Instance
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createSumeClient } from '@sume-com/client/v2';
|
|
89
|
+
|
|
90
|
+
const client = createSumeClient({
|
|
91
|
+
credentials: 'YOUR_SUME_API_KEY'
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Explicit client instances keep their credentials separate. `client.configure()` updates only that instance; module-level `config()` updates the shared `sume` client.
|
|
96
|
+
|
|
97
|
+
### API Methods
|
|
98
|
+
|
|
99
|
+
#### `subscribe(endpoint, options)`
|
|
100
|
+
|
|
101
|
+
Submit a job and wait for its result:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const response = await sume.subscribe('seedance-2.5', {
|
|
105
|
+
input: {
|
|
106
|
+
prompt: 'Steam rises from the cup as the camera slowly pushes in',
|
|
107
|
+
duration: 5,
|
|
108
|
+
resolution: '480p',
|
|
109
|
+
aspect_ratio: '16:9'
|
|
110
|
+
},
|
|
111
|
+
withPolling: true, // Automatically poll for job completion (default: true)
|
|
112
|
+
webhook: { // Optional webhook
|
|
113
|
+
url: 'https://your-webhook-url.com/callback'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Parameters:**
|
|
119
|
+
- `endpoint` (string): A model id from the [open model catalog](https://open.sume.com/docs/reference/catalog/list) (e.g., `'gpt-image-2'`), or an API path with `model` in the input: `'/v1/image-router/generate'` for images, `'/v1/video-router/generate'` for video, lip-sync and captions
|
|
120
|
+
- `options.input` (object): Input parameters for the model (`prompt`, `aspect_ratio`, `duration`, `image_url` and so on)
|
|
121
|
+
- `options.withPolling` (boolean, default: `true`): Automatically poll for job completion
|
|
122
|
+
- `options.webhook` (object, optional): Webhook configuration
|
|
123
|
+
- `url` (string): Public HTTPS URL that Sume calls when the job ends
|
|
124
|
+
- `options.idempotencyKey` (string, optional): Your own `Idempotency-Key` (1 to 255 printable ASCII characters). Without it, the SDK makes a UUID for each call
|
|
125
|
+
|
|
126
|
+
**Note:** When a webhook is provided, the SDK sends `mode: "webhook"` and `webhook_url` in the request body.
|
|
127
|
+
|
|
128
|
+
Sume runs every model behind two routers. For a model id, the SDK reads the model's `kind` once from the public catalog (`GET /v1/open/catalog/{id}`), then submits to the image router (`image`) or the video router (`video`, `lipsync`, `captions`) with `model` in the body. An unknown id fails there, before any job. Each model's fields and accepted values are in its catalog row: [image models](https://open.sume.com/docs/reference/images/list-models), [video models](https://open.sume.com/docs/reference/video/list-models).
|
|
129
|
+
|
|
130
|
+
#### `upload(data, contentType)`
|
|
131
|
+
|
|
132
|
+
Upload a file that is not online yet, and get an HTTPS URL to pass to a model:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { readFile } from 'node:fs/promises';
|
|
136
|
+
|
|
137
|
+
const audioUrl = await sume.upload(await readFile('speech.wav'), 'audio/wav');
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The SDK asks `POST /v1/files/generate-upload-url` for a place to put the file, `PUT`s the bytes there without your API key, and returns the file's `public_url`. Uploading is free.
|
|
141
|
+
|
|
142
|
+
#### `uploadImage(imageBuffer, format?)`
|
|
143
|
+
|
|
144
|
+
`upload()` for an image: `format` is `'jpeg'` (default), `'png'` or `'webp'`.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const imageUrl = await sume.uploadImage(await readFile('photo.png'), 'png');
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Examples
|
|
151
|
+
|
|
152
|
+
#### Text-to-Image Generation
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { sume, config } from '@sume-com/client/v2';
|
|
156
|
+
|
|
157
|
+
config({
|
|
158
|
+
credentials: 'YOUR_SUME_API_KEY'
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const response = await sume.subscribe('nano-banana-pro', {
|
|
162
|
+
input: {
|
|
163
|
+
prompt: 'A majestic mountain landscape at sunset',
|
|
164
|
+
aspect_ratio: '16:9'
|
|
165
|
+
},
|
|
166
|
+
withPolling: true
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (response.status === 'completed') {
|
|
170
|
+
console.log('Image URL:', response.images?.[0].url);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
#### Image Editing
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const response = await sume.subscribe('gpt-image-2', {
|
|
178
|
+
input: {
|
|
179
|
+
prompt: 'Make the cup cobalt blue, keep everything else the same',
|
|
180
|
+
quality: 'low',
|
|
181
|
+
image_urls: ['https://example.com/cup.png']
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Image-to-Video Generation
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { readFile } from 'node:fs/promises';
|
|
190
|
+
|
|
191
|
+
const imageUrl = await sume.uploadImage(await readFile('cup.png'), 'png');
|
|
192
|
+
|
|
193
|
+
const response = await sume.subscribe('/v1/video-router/generate', {
|
|
194
|
+
input: {
|
|
195
|
+
model: 'seedance-2.5',
|
|
196
|
+
prompt: 'Cinematic camera movement around the cup',
|
|
197
|
+
image_url: imageUrl,
|
|
198
|
+
duration: 5,
|
|
199
|
+
resolution: '480p'
|
|
200
|
+
},
|
|
201
|
+
withPolling: true
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (response.status === 'completed') {
|
|
205
|
+
console.log('Video URL:', response.video?.url);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Lip-sync
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const response = await sume.subscribe('veed-fabric-1.0', {
|
|
213
|
+
input: {
|
|
214
|
+
image_url: await sume.uploadImage(await readFile('face.jpg')),
|
|
215
|
+
audio_url: await sume.upload(await readFile('speech.wav'), 'audio/wav'),
|
|
216
|
+
duration: 8,
|
|
217
|
+
resolution: '480p'
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Polling & Status Lifecycle
|
|
223
|
+
|
|
224
|
+
The client automatically polls `/v1/jobs/{id}/status` when `withPolling` is `true`. It waits as long as the API asks (`next_poll_after_seconds`, 2 seconds while a job runs) and never less than `pollInterval`. It keeps polling through `5xx`, `429` and dropped connections. When the job completes, it reads `/v1/jobs/{id}/result`.
|
|
225
|
+
|
|
226
|
+
**Status Values:**
|
|
227
|
+
- `queued` – Request accepted and waiting for a worker
|
|
228
|
+
- `in_progress` – Generation currently running
|
|
229
|
+
- `completed` – Generation finished and media URLs are returned
|
|
230
|
+
- `nsfw` – Content refused by the model's or Sume's content policy (`error.public_reason` is `content_policy_rejected`); the price is refunded
|
|
231
|
+
- `failed` – Generation errored; the price is refunded and `error` says why
|
|
232
|
+
- `canceled` – Canceled before generation started; the price is refunded
|
|
233
|
+
|
|
234
|
+
**API Response Format:**
|
|
235
|
+
|
|
236
|
+
The status call answers with Sume's job status:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"data": {
|
|
241
|
+
"request_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
|
|
242
|
+
"job_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
|
|
243
|
+
"status": "COMPLETED",
|
|
244
|
+
"sume_status": "completed",
|
|
245
|
+
"status_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/status",
|
|
246
|
+
"result_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/result",
|
|
247
|
+
"cancel_url": null,
|
|
248
|
+
"next_action": "fetch_result",
|
|
249
|
+
"terminal": true
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The SDK turns it, and the result's artifacts, into one `V2Response`:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"status": "completed",
|
|
259
|
+
"request_id": "job_7513b52d-3669-4ffe-b317-8b02000715eb",
|
|
260
|
+
"status_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/status",
|
|
261
|
+
"cancel_url": null,
|
|
262
|
+
"result_url": "https://api.open.sume.com/v1/jobs/job_7513b52d-3669-4ffe-b317-8b02000715eb/result",
|
|
263
|
+
"images": [{ "url": "https://media.sume.com/artifacts/artf_.../images.png" }],
|
|
264
|
+
"artifacts": [
|
|
265
|
+
{
|
|
266
|
+
"id": "artf_...",
|
|
267
|
+
"type": "image",
|
|
268
|
+
"url": "https://media.sume.com/artifacts/artf_.../images.png",
|
|
269
|
+
"content_type": "image/png"
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`images` holds every image artifact and `video` the first video artifact, as in the Higgsfield client. `artifacts` keeps every artifact as Sume returns it, audio included. A failed job resolves with `status: 'failed'` (or `'nsfw'`) and `error` (`code`, `message`, `public_reason`), not an exception. Artifacts live on `media.sume.com` and are readable by anyone with the link; copy them to your own storage if you need to control access.
|
|
276
|
+
|
|
277
|
+
### Working with the Response
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const response = await sume.subscribe('gpt-image-2', {
|
|
281
|
+
input: { prompt: 'Test', quality: 'low', num_images: 2 },
|
|
282
|
+
withPolling: true
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Check status
|
|
286
|
+
console.log('Request ID:', response.request_id);
|
|
287
|
+
console.log('Status:', response.status);
|
|
288
|
+
|
|
289
|
+
// Access results
|
|
290
|
+
for (const image of response.images ?? []) {
|
|
291
|
+
console.log('Result URL:', image.url);
|
|
292
|
+
}
|
|
293
|
+
if (response.status === 'failed' || response.status === 'nsfw') {
|
|
294
|
+
console.error(response.error?.code, response.error?.message);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Long Jobs and Resuming
|
|
299
|
+
|
|
300
|
+
`maxPollTime` (5 minutes by default) bounds the wait. When it runs out, `subscribe()` throws `TimeoutError`, and **the job keeps running**. Do not submit a new job: call `subscribe()` again with the same input and the error's `idempotencyKey`. Sume returns the same job, charges it once, and the SDK keeps waiting.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { TimeoutError } from '@sume-com/client/v2';
|
|
304
|
+
|
|
305
|
+
const input = { prompt: 'A paper boat drifting down a rain gutter', duration: 10 };
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await sume.subscribe('seedance-2.5', { input });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof TimeoutError) {
|
|
311
|
+
const response = await sume.subscribe('seedance-2.5', {
|
|
312
|
+
input,
|
|
313
|
+
idempotencyKey: error.idempotencyKey
|
|
314
|
+
});
|
|
315
|
+
console.log(response.video?.url);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The same works across processes: pass your own `idempotencyKey`, store it, and submit with `withPolling: false`. Later, the same call with `withPolling: true` waits for that job.
|
|
321
|
+
|
|
322
|
+
### Webhooks
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
await sume.subscribe('kling-3', {
|
|
326
|
+
input: { prompt: 'A slow dolly shot through a neon market at night', duration: 5 },
|
|
327
|
+
webhook: { url: 'https://example.com/hooks/sume' },
|
|
328
|
+
withPolling: false
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Sume POSTs `job.completed`, `job.failed` or `job.canceled` to the URL once the job ends, and retries for about three hours until it gets a `2xx`. Every delivery is signed with your workspace's signing secret (dashboard → **Webhooks**). Check it with `verifyWebhook` on the raw body before you trust the event:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { verifyWebhook, WebhookEvent } from '@sume-com/client';
|
|
336
|
+
|
|
337
|
+
export async function POST(request: Request) {
|
|
338
|
+
const body = await request.text();
|
|
339
|
+
const valid = verifyWebhook(
|
|
340
|
+
body,
|
|
341
|
+
request.headers.get('x-sume-webhook-timestamp') ?? undefined,
|
|
342
|
+
request.headers.get('x-sume-webhook-signature') ?? undefined,
|
|
343
|
+
process.env.SUME_WEBHOOK_SECRET!
|
|
344
|
+
);
|
|
345
|
+
if (!valid) {
|
|
346
|
+
return new Response('invalid signature', { status: 401 });
|
|
347
|
+
}
|
|
348
|
+
const event = JSON.parse(body) as WebhookEvent;
|
|
349
|
+
console.log(event.event, event.job_id, event.payload?.artifacts[0]?.url);
|
|
350
|
+
return new Response('ok');
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`verifyWebhook` accepts any `sume-v1=` signature in the header (there is one per live secret during a rotation) and refuses deliveries more than five minutes old. A delivery can arrive more than once: dedupe on `job_id`.
|
|
355
|
+
|
|
356
|
+
### Configuration
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { createSumeClient } from '@sume-com/client/v2';
|
|
360
|
+
|
|
361
|
+
const client = createSumeClient({
|
|
362
|
+
credentials: 'YOUR_SUME_API_KEY',
|
|
363
|
+
|
|
364
|
+
// Optional configuration
|
|
365
|
+
baseURL: 'https://api.open.sume.com', // Default
|
|
366
|
+
timeout: 120000, // 2 minutes default
|
|
367
|
+
maxRetries: 3,
|
|
368
|
+
retryBackoff: 1000,
|
|
369
|
+
retryMaxBackoff: 60000,
|
|
370
|
+
pollInterval: 2000, // Check every 2 seconds at most
|
|
371
|
+
maxPollTime: 300000, // Timeout after 5 minutes
|
|
372
|
+
headers: {
|
|
373
|
+
'X-Custom-Header': 'value'
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Retries cover network errors, `429` and `5xx`, with exponential backoff and jitter, and follow `retry-after`. A `4xx` is never retried. Raise `maxPollTime` for long video, which can take several minutes.
|
|
379
|
+
|
|
380
|
+
### TypeScript Support
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import {
|
|
384
|
+
createSumeClient,
|
|
385
|
+
SumeClient,
|
|
386
|
+
V2Response
|
|
387
|
+
} from '@sume-com/client/v2';
|
|
388
|
+
|
|
389
|
+
const client: SumeClient = createSumeClient({
|
|
390
|
+
credentials: 'YOUR_SUME_API_KEY'
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const response: V2Response = await client.subscribe('/v1/image-router/generate', {
|
|
394
|
+
input: {
|
|
395
|
+
model: 'gpt-image-2',
|
|
396
|
+
prompt: 'Test',
|
|
397
|
+
aspect_ratio: '1:1'
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The two router paths type their input (`ImageGenerateInput`, `VideoGenerateInput`): `model` is required, the documented fields are checked, and model-specific fields pass through. A model id takes any input object.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Error Handling
|
|
407
|
+
|
|
408
|
+
The SDK provides comprehensive error handling with specific error types:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import {
|
|
412
|
+
AuthenticationError,
|
|
413
|
+
BadInputError,
|
|
414
|
+
ValidationError,
|
|
415
|
+
NotEnoughCreditsError,
|
|
416
|
+
APIError,
|
|
417
|
+
TimeoutError,
|
|
418
|
+
BrowserNotSupportedError
|
|
419
|
+
} from '@sume-com/client';
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const response = await sume.subscribe('gpt-image-2', {
|
|
423
|
+
input: { prompt: 'Test' }
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (error instanceof AuthenticationError) {
|
|
427
|
+
console.error('❌ Authentication failed - check your API key');
|
|
428
|
+
} else if (error instanceof NotEnoughCreditsError) {
|
|
429
|
+
console.error('💳 Insufficient funds - add funds at open.sume.com/billing');
|
|
430
|
+
} else if (error instanceof BadInputError) {
|
|
431
|
+
console.error('📋 Invalid input parameters:', error.message, error.details);
|
|
432
|
+
} else if (error instanceof ValidationError) {
|
|
433
|
+
console.error('⚠️ Validation error:', error.message);
|
|
434
|
+
} else if (error instanceof TimeoutError) {
|
|
435
|
+
console.error('⏱️ Still running, resume with idempotencyKey', error.idempotencyKey);
|
|
436
|
+
} else if (error instanceof APIError) {
|
|
437
|
+
console.error('🌐 API Error:', error.statusCode, error.code, error.requestId, error.message);
|
|
438
|
+
} else if (error instanceof BrowserNotSupportedError) {
|
|
439
|
+
console.error('🚫 Browser usage not supported - use Node.js environment');
|
|
440
|
+
} else {
|
|
441
|
+
console.error('💥 Unexpected error:', error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
| Class | When | Sume `code` |
|
|
447
|
+
|---|---|---|
|
|
448
|
+
| `AuthenticationError` | `401` | `unauthorized` |
|
|
449
|
+
| `NotEnoughCreditsError` | `402` | `insufficient_funds`; `responseData.error.details` has `required_usd` and `available_usd` |
|
|
450
|
+
| `BadInputError` | `400` | `invalid_request` (`details` lists each field and why), `concurrency_limit_reached`, `bad_request` |
|
|
451
|
+
| `ValidationError` | `422` | |
|
|
452
|
+
| `APIError` | any other status | `open_key_route_not_allowed` (`403`), `not_found` and `model_not_found` (`404`), `idempotency_conflict` (`409`), `rate_limited` (`429`), `database_busy` (`503`), `internal_error` (`500`) |
|
|
453
|
+
| `TimeoutError` | `maxPollTime` ran out; the job keeps running | |
|
|
454
|
+
| `CredentialsMissedError` | no key in config or `SUME_API_KEY` | |
|
|
455
|
+
| `BrowserNotSupportedError` | the SDK was loaded in a browser | |
|
|
456
|
+
|
|
457
|
+
Every API error carries `statusCode`, Sume's stable `code`, the `requestId` to quote to support, and the full body in `responseData`. A network error is an `APIError` with no `statusCode` and the system `code` (`ECONNRESET`, `ENOTFOUND`, ...). Errors never carry your API key or a signed upload URL. All of them extend `SumeError`.
|
|
458
|
+
|
|
459
|
+
A workspace may have only so many jobs queued or running at once (2 to 40, by the funds added in the past 28 days). A submit past the cap is refused with `BadInputError` and `code: 'concurrency_limit_reached'`, and is not charged. Wait for a job to end, then submit the next.
|
|
460
|
+
|
|
461
|
+
### Job Status Handling
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
switch (response.status) {
|
|
465
|
+
case 'completed':
|
|
466
|
+
console.log('Success:', response.images?.[0]?.url ?? response.video?.url);
|
|
467
|
+
break;
|
|
468
|
+
case 'failed':
|
|
469
|
+
console.error('Generation failed:', response.error?.message);
|
|
470
|
+
break;
|
|
471
|
+
case 'nsfw':
|
|
472
|
+
console.warn('Content flagged as NSFW');
|
|
473
|
+
break;
|
|
474
|
+
case 'canceled':
|
|
475
|
+
console.warn('Job was canceled');
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Best Practices
|
|
483
|
+
|
|
484
|
+
1. **Name the model yourself**: prices differ by more than 100× between models. The [catalog](https://open.sume.com/docs/reference/catalog/list) lists every model's dollar price, and [Estimate a price](https://open.sume.com/docs/reference/catalog/estimate) prices a request for free.
|
|
485
|
+
|
|
486
|
+
2. **Upload large files**: a local image, clip or audio file goes up once with `upload()` / `uploadImage()`, and its URL works in any input field:
|
|
487
|
+
```typescript
|
|
488
|
+
const imageUrl = await sume.uploadImage(localImageBuffer, 'jpeg');
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
3. **Handle rate limits**: configure exponential backoff for retries:
|
|
492
|
+
```typescript
|
|
493
|
+
const client = createSumeClient({
|
|
494
|
+
credentials: 'YOUR_SUME_API_KEY',
|
|
495
|
+
maxRetries: 5,
|
|
496
|
+
retryBackoff: 2000,
|
|
497
|
+
retryMaxBackoff: 30000
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
4. **Use webhooks for long operations**: for video, a webhook saves the polling. Keep a slow poll as a fallback in case a delivery is lost.
|
|
502
|
+
|
|
503
|
+
5. **Never resubmit a running job**: after a `TimeoutError` or a crash, call `subscribe()` again with the same `idempotencyKey`. A new key is a new, billed job.
|
|
504
|
+
|
|
505
|
+
6. **Environment variables**: store your key in the environment:
|
|
506
|
+
```bash
|
|
507
|
+
export SUME_API_KEY="sume_live_..."
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Coming from Higgsfield
|
|
513
|
+
|
|
514
|
+
| higgsfield-js | sume-js |
|
|
515
|
+
|---|---|
|
|
516
|
+
| `npm install @higgsfield/client` | `npm install @sume-com/client` |
|
|
517
|
+
| `import { higgsfield, config } from '@higgsfield/client/v2'` | `import { sume, config } from '@sume-com/client/v2'` |
|
|
518
|
+
| `createHiggsfieldClient()`, `HiggsfieldClient`, `HiggsfieldError` | `createSumeClient()`, `SumeClient`, `SumeError` |
|
|
519
|
+
| `config({ credentials: 'KEY_ID:KEY_SECRET' })` | `config({ credentials: 'sume_live_...' })` |
|
|
520
|
+
| `HF_CREDENTIALS`, `HF_KEY`, `HF_API_KEY` + `HF_API_SECRET` | `SUME_API_KEY` |
|
|
521
|
+
| `Authorization: Key KEY_ID:KEY_SECRET` | `Authorization: Bearer sume_live_...` |
|
|
522
|
+
| `https://api.higgsfield.ai` | `https://api.open.sume.com` |
|
|
523
|
+
| `subscribe('flux-pro/kontext/max/text-to-image', { input })` | `subscribe('flux-kontext-max', { input })`, routed by the catalog's `kind`; or `subscribe('/v1/image-router/generate', { input: { model: 'flux-kontext-max', ... } })` |
|
|
524
|
+
| polls `/requests/{request_id}/status` | polls `/v1/jobs/{id}/status`, then reads `/v1/jobs/{id}/result` |
|
|
525
|
+
| `webhook: { url, secret }` becomes `?hf_webhook=<url>` | `webhook: { url }` becomes `mode: "webhook"` and `webhook_url`; check deliveries with `verifyWebhook()` |
|
|
526
|
+
| statuses `queued`, `in_progress`, `completed`, `failed`, `nsfw` | the same, plus `canceled`; `nsfw` is a refusal with `public_reason: content_policy_rejected` |
|
|
527
|
+
| `NotEnoughCreditsError` on `403` | `NotEnoughCreditsError` on `402` |
|
|
528
|
+
| retried submits have no idempotency key | every submit sends `Idempotency-Key`; retries and `TimeoutError` resume the same job |
|
|
529
|
+
| `uploadImage()` on the deprecated v1 client | `upload()` and `uploadImage()` on the client |
|
|
530
|
+
| `client.agents` (Agent API) | not in sume-js: the Open API has no agent sessions |
|
|
531
|
+
| v1 `HiggsfieldClient` with `generate()` | not ported: Sume has one API, the one above |
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Support
|
|
536
|
+
|
|
537
|
+
- Documentation: https://open.sume.com/docs
|
|
538
|
+
- API Status: https://status.sume.com
|
|
539
|
+
|
|
540
|
+
## License
|
|
541
|
+
|
|
542
|
+
MIT
|