@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 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
- # Temporary Holding Version
1
+ # Sume SDK for Node.js and TypeScript
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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