corent-sdk 0.2.0 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Corent SDK
2
2
 
3
- One API for AI **image, video, and voice** generation. You pick a quality tier; Corent's router picks the best live model, reroutes failures, verifies the output, and returns the exact charge on every response. Failed generations are never billed.
3
+ One API for AI **image, video, voice, and text** generation. You pick a quality tier; Corent's router picks the best live model, reroutes failures, verifies the output, and returns the exact charge on every response. Failed generations are never billed.
4
4
 
5
5
  ```bash
6
6
  npm install corent-sdk
@@ -26,6 +26,9 @@ console.log(video.url, video.resolution, video.costCents);
26
26
 
27
27
  const speech = await client.speech.generate("Welcome to Corent.");
28
28
  console.log(speech.url, speech.costCents);
29
+
30
+ const answer = await client.text.generate("Name three uses for a paperclip.", { tier: "premium" });
31
+ console.log(answer.text, answer.costCents);
29
32
  ```
30
33
 
31
34
  ## What the SDK handles for you
@@ -41,15 +44,115 @@ Pass `model` instead of `tier` to pin an exact model. It is never substituted:
41
44
  if that model can't deliver, the call fails and you are not charged.
42
45
 
43
46
  ```ts
44
- const image = await client.images.generate("a lighthouse at dusk", { model: "flux-schnell" });
45
- console.log(image.model); // "flux-schnell" — the name you asked for
47
+ const image = await client.images.generate("a lighthouse at dusk", { model: "corent-flux-schnell" });
48
+ console.log(image.model); // "corent-flux-schnell" — the name you asked for
49
+
50
+ await client.models(); // the menu: every model with its kind, quality and live status
51
+ ```
52
+
53
+ Each model has one name, published as `corent-` + the model: `corent-flux-schnell`,
54
+ `corent-seedance-2.0`, `corent-claude-opus-5`. That is the spelling `client.models()`
55
+ lists and the one every receipt echoes back. Older spellings you may already have
56
+ hard-coded (`flux-schnell`, `seedream-5.0-direct`) keep working — the API accepts
57
+ them and answers with the published name.
58
+
59
+ When we can reach a model by more than one route, Corent serves whichever is
60
+ cheapest at that moment and charges you that price — you never have to shop
61
+ between near-identical entries.
46
62
 
47
- await client.models(); // the menu: every model with its kind, quality and exact price
63
+ ## Text (language models)
64
+
65
+ Every frontier lab on one key and one bill, priced per token.
66
+
67
+ ```ts
68
+ const answer = await client.text.generate("Explain reserve-then-settle billing.", {
69
+ system: "Answer in two sentences.",
70
+ tier: "premium",
71
+ });
72
+ console.log(answer.text, answer.promptTokens, answer.completionTokens, answer.costCents);
73
+
74
+ // a real conversation, tool calls included
75
+ const reply = await client.text.chat(
76
+ [
77
+ { role: "system", content: "Be terse." },
78
+ { role: "user", content: "What's the weather?" },
79
+ ],
80
+ { model: "corent-claude-opus-5" },
81
+ );
48
82
  ```
49
83
 
50
- Each model has one name. When we can reach it by more than one route, Corent
51
- serves whichever is cheapest at that moment and charges you that price — you
52
- never have to shop between near-identical entries.
84
+ Streaming isn't wrapped here: point any OpenAI-compatible client at
85
+ `https://api.corent.tech/v1` with your Corent key and it works as-is.
86
+
87
+ ## Keep a character or product consistent
88
+
89
+ Pass 1–4 reference images and the prompt is applied as an *edit* of them, so
90
+ the same face, character, or product carries into a new scene.
91
+
92
+ ```ts
93
+ const shot = await client.images.generate("the same woman, now on a beach", {
94
+ tier: "pro", // edit-capable models sit at premium and up
95
+ referenceImageUrls: ["https://cdn.example/her.png"],
96
+ });
97
+ ```
98
+
99
+ `air` and `lite` cannot do this and say so with a 400.
100
+ `client.tiers()` reports `capabilities.supports_reference_images` per tier.
101
+
102
+ ## Batches and webhooks
103
+
104
+ ```ts
105
+ // Up to 50 renders in one call. Each item bills at the normal rate.
106
+ const batch = await client.batches.images(
107
+ [{ prompt: "a fox", tier: "air" }, { prompt: "a heron", tier: "air" }],
108
+ { idempotencyKey: "campaign-9" }, // a retry replays instead of re-billing
109
+ );
110
+ const progress = await client.batches.progress(batch.batchId);
111
+
112
+ // Or have the server deliver each result and skip polling entirely.
113
+ await client.videos.generate("a drone shot", {
114
+ tier: "premium",
115
+ webhookUrl: "https://your-server.com/webhooks/corent",
116
+ webhookSecret: "your_shared_secret", // signs every delivery
117
+ });
118
+ ```
119
+
120
+ ## What else you can ask for
121
+
122
+ ```ts
123
+ // Repeat an image exactly, then change one thing.
124
+ const first = await client.images.generate("a fox in a library", { tier: "pro" });
125
+ const again = await client.images.generate("a fox in a library, wearing glasses", {
126
+ tier: "pro", seed: 12345, negativePrompt: "text, watermark",
127
+ });
128
+
129
+ // Four versions of one prompt in one call (four real renders, four charges).
130
+ const { images, totalCostCents } = await client.images.generateMany("a fox", 4, { tier: "air" });
131
+
132
+ // A transparent logo, at a size you choose.
133
+ await client.images.generate("a minimal fox mark", { transparent: true, width: 1024, height: 1024 });
134
+
135
+ // Video with sound, going from one picture to another.
136
+ await client.videos.generate("the camera pulls back", {
137
+ tier: "premium", audio: true,
138
+ imageUrl: "https://.../start.png", endImageUrl: "https://.../end.png",
139
+ camera: "zoom_out",
140
+ });
141
+
142
+ // Pick a voice, and shape how it reads.
143
+ const voices = await client.voices();
144
+ await client.speech.generate("Welcome aboard.", {
145
+ voiceId: voices[0].voice_id, stability: 0.3, speed: 1.1,
146
+ });
147
+
148
+ // Started something by mistake? Stop it. Costs nothing.
149
+ await client.jobs.cancel(job.id);
150
+ ```
151
+
152
+ `audio: true` routes only to models that actually render sound, so a silent
153
+ model can never quietly serve the request. Not every model takes every
154
+ setting: anything the chosen one could not honour comes back in
155
+ `meta.unsupported_options` rather than being silently ignored.
53
156
 
54
157
  ## Fine-grained control
55
158
 
@@ -57,9 +160,14 @@ never have to shop between near-identical entries.
57
160
  const job = await client.images.generate("...", { tier: "max_pro", wait: false });
58
161
  const done = await client.jobs.wait(job.id); // resume any time
59
162
 
60
- await client.tiers(); // live catalog with honest min–max price ranges
61
- await client.models(); // direct-access menu with per-model prices
62
- await client.balanceCents(); // prepaid balance
163
+ await client.tiers(); // live catalog with honest min–max price ranges
164
+ await client.models(); // direct-access menu with per-model prices
165
+ await client.balance(); // { balanceCents, heldCents, availableCents }
166
+ await client.usage(); // what this account has spent
63
167
  ```
64
168
 
169
+ Every generate call sends an `Idempotency-Key`, so the SDK's own retries can
170
+ never double-charge. Pass your own `idempotencyKey` to make that survive a
171
+ process restart too.
172
+
65
173
  Tiers: `air` | `lite` | `premium` | `pro` | `max_pro` — see [corent.tech/pricing](https://corent.tech/pricing). Models: [corent.tech/models](https://corent.tech/models). Docs: [corent.tech/docs](https://corent.tech/docs). MCP server for agents: [`corent-mcp`](https://www.npmjs.com/package/corent-mcp).
package/dist/index.cjs CHANGED
@@ -31,6 +31,8 @@ var DEFAULT_BASE_URL = "https://api.corent.tech";
31
31
  var MAX_RETRIES = 3;
32
32
  var POLL_INTERVAL_MS = 3e3;
33
33
  var DEFAULT_WAIT_TIMEOUT_MS = 9e5;
34
+ var MAX_BATCH_ITEMS = 50;
35
+ var MAX_IMAGES_PER_REQUEST = 10;
34
36
  var CorentError = class extends Error {
35
37
  constructor(message, statusCode) {
36
38
  super(message);
@@ -58,7 +60,9 @@ var Corent = class {
58
60
  images;
59
61
  videos;
60
62
  speech;
63
+ text;
61
64
  jobs;
65
+ batches;
62
66
  constructor(apiKey, options = {}) {
63
67
  this.apiKey = apiKey;
64
68
  this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
@@ -66,32 +70,63 @@ var Corent = class {
66
70
  this.images = new Images(this);
67
71
  this.videos = new Videos(this);
68
72
  this.speech = new Speech(this);
73
+ this.text = new Text(this);
69
74
  this.jobs = new Jobs(this);
75
+ this.batches = new Batches(this);
70
76
  }
71
- /** Prepaid balance in cents. */
77
+ /** Prepaid balance in cents. See `balance()` for what is actually spendable. */
72
78
  async balanceCents() {
73
79
  const r = await this.request("GET", "/v1/account/balance");
74
80
  return r.balance_cents;
75
81
  }
82
+ /** The full picture: the total, what running generations have reserved, and
83
+ * what a new request can actually spend. availableCents is the number a 402
84
+ * is decided against — check that one before an expensive batch. */
85
+ async balance() {
86
+ const r = await this.request("GET", "/v1/account/balance");
87
+ return {
88
+ balanceCents: r.balance_cents,
89
+ heldCents: r.held_cents ?? 0,
90
+ availableCents: r.available_cents ?? r.balance_cents
91
+ };
92
+ }
93
+ /** The voices `speech.generate` will accept as voiceId. This exists
94
+ * because the API requires a voice id and, until 2026-08-30, published no
95
+ * list of legal values. */
96
+ async voices() {
97
+ const r = await this.request("GET", "/v1/voices");
98
+ return r.voices ?? [];
99
+ }
100
+ /** What this account has spent, by lane and over time. */
101
+ async usage() {
102
+ return this.request("GET", "/v1/account/usage");
103
+ }
104
+ /** Live operational status of the generation tiers. Public. */
105
+ async status() {
106
+ return this.request("GET", "/v1/status");
107
+ }
76
108
  /** The live tier catalog with honest min–max price ranges. Public. */
77
109
  async tiers() {
78
110
  const r = await this.request("GET", "/v1/tiers");
79
111
  return r.tiers;
80
112
  }
81
113
  /** The direct-access menu: every model you can pin by name, with its kind,
82
- * quality score and flat cost-plus price. Public. */
114
+ * quality score and live status. Names come back in their published
115
+ * spelling ("corent-flux-schnell") and can be passed straight back as
116
+ * `model`. Carries no price -- billing is flat cost-plus and the exact
117
+ * charge comes back on each generation. Public. */
83
118
  async models() {
84
119
  const r = await this.request("GET", "/v1/models");
85
120
  return r.models;
86
121
  }
87
122
  /** @internal */
88
- async request(method, path, body, idempotent = false) {
123
+ async request(method, path, body, idempotent = false, retryTransport = true, idempotencyKey) {
89
124
  const headers = {
90
125
  Authorization: `Bearer ${this.apiKey}`,
91
- "User-Agent": "corent-js/0.1.0"
126
+ "User-Agent": "corent-js/0.3.0"
92
127
  };
93
128
  if (body) headers["Content-Type"] = "application/json";
94
- if (idempotent) headers["Idempotency-Key"] = crypto.randomUUID();
129
+ if (idempotent) headers["Idempotency-Key"] = idempotencyKey ?? crypto.randomUUID();
95
130
  let lastError;
96
131
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
97
132
  let resp;
@@ -103,6 +138,7 @@ var Corent = class {
103
138
  });
104
139
  } catch (err) {
105
140
  lastError = err;
141
+ if (!retryTransport) throw new CorentError(`request failed: ${err}`);
106
142
  await sleep(Math.min(2 ** attempt * 1e3, 8e3));
107
143
  continue;
108
144
  }
@@ -149,8 +185,11 @@ var Images = class {
149
185
  *
150
186
  * `model` pins an exact model from client.models() instead of letting the
151
187
  * router choose — direct access: never substituted, flat cost-plus price.
152
- * Pass tier OR model, not both. */
188
+ * e.g. model: "corent-flux-schnell". Pass tier OR model, not both. */
153
189
  async generate(prompt, options = {}) {
190
+ if (options.referenceImageUrls && (options.referenceImageUrls.length < 1 || options.referenceImageUrls.length > 4)) {
191
+ throw new InvalidRequestError("referenceImageUrls must contain 1 to 4 image URLs", 400);
192
+ }
154
193
  const body = {
155
194
  prompt,
156
195
  aspect_ratio: options.aspectRatio ?? "1:1",
@@ -159,13 +198,91 @@ var Images = class {
159
198
  if (options.tier) body.tier = options.tier;
160
199
  if (options.model) body.model = options.model;
161
200
  if (options.style) body.style = options.style;
162
- const submitted = await this.c.request("POST", "/v1/images/generate", body, true);
201
+ if (options.referenceImageUrls) body.reference_image_urls = options.referenceImageUrls;
202
+ if (options.seed !== void 0) body.seed = options.seed;
203
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
204
+ if (options.sourceImageUrl) body.source_image_url = options.sourceImageUrl;
205
+ if (options.strength !== void 0) body.strength = options.strength;
206
+ if (options.outputFormat) body.output_format = options.outputFormat;
207
+ if (options.transparent) body.transparent = true;
208
+ if (options.width !== void 0) body.width = options.width;
209
+ if (options.height !== void 0) body.height = options.height;
210
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
211
+ if (options.webhookUrl) {
212
+ body.webhook_url = options.webhookUrl;
213
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
214
+ delete body.async;
215
+ }
216
+ const submitted = await this.c.request(
217
+ "POST",
218
+ "/v1/images/generate",
219
+ body,
220
+ true,
221
+ true,
222
+ options.idempotencyKey
223
+ );
163
224
  if (submitted.status === "completed") return imageFromJob(submitted);
164
- if (options.wait === false) {
225
+ if (options.webhookUrl || options.wait === false) {
165
226
  return { id: submitted.id, status: submitted.status, raw: submitted };
166
227
  }
167
228
  return imageFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
168
229
  }
230
+ /** Render several versions of one prompt in a single call, 2–10.
231
+ *
232
+ * Each one is a real render at the normal price, so asking for four costs
233
+ * four images. They come back together, and `totalCostCents` is the whole
234
+ * charge. Partial success is a real outcome: if three of four land you get
235
+ * three and are billed for three.
236
+ *
237
+ * Different from client.batches.images(), which runs DIFFERENT prompts and
238
+ * returns job ids to poll. This one runs the SAME prompt and waits. */
239
+ async generateMany(prompt, count, options = {}) {
240
+ if (count < 2 || count > MAX_IMAGES_PER_REQUEST) {
241
+ throw new InvalidRequestError(
242
+ `generateMany takes 2 to ${MAX_IMAGES_PER_REQUEST}; use generate() for one`,
243
+ 400
244
+ );
245
+ }
246
+ const body = {
247
+ prompt,
248
+ aspect_ratio: options.aspectRatio ?? "1:1",
249
+ n: count
250
+ };
251
+ if (options.tier) body.tier = options.tier;
252
+ if (options.model) body.model = options.model;
253
+ if (options.style) body.style = options.style;
254
+ if (options.referenceImageUrls) body.reference_image_urls = options.referenceImageUrls;
255
+ if (options.seed !== void 0) body.seed = options.seed;
256
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
257
+ if (options.sourceImageUrl) body.source_image_url = options.sourceImageUrl;
258
+ if (options.strength !== void 0) body.strength = options.strength;
259
+ if (options.outputFormat) body.output_format = options.outputFormat;
260
+ if (options.transparent) body.transparent = true;
261
+ if (options.width !== void 0) body.width = options.width;
262
+ if (options.height !== void 0) body.height = options.height;
263
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
264
+ const r = await this.c.request(
265
+ "POST",
266
+ "/v1/images/generate",
267
+ body,
268
+ true,
269
+ true,
270
+ options.idempotencyKey
271
+ );
272
+ const meta = r.meta ?? {};
273
+ return {
274
+ images: (r.images ?? []).map((image) => ({
275
+ id: r.id,
276
+ url: image.url,
277
+ width: image.width,
278
+ height: image.height,
279
+ model: meta.model,
280
+ costCents: void 0
281
+ })),
282
+ // The total for every image that actually landed.
283
+ totalCostCents: meta.cost_cents
284
+ };
285
+ }
169
286
  };
170
287
  var Videos = class {
171
288
  constructor(c) {
@@ -178,19 +295,37 @@ var Videos = class {
178
295
  *
179
296
  * `model` pins an exact model from client.models() — direct access, never
180
297
  * substituted, and duration/resolution snap to THAT model's own menu rather
181
- * than a tier cap. Pass tier OR model, not both. */
298
+ * than a tier cap. e.g. model: "corent-seedance-2.0". Pass tier OR model,
299
+ * not both. */
182
300
  async generate(prompt, options = {}) {
183
- const body = {
184
- prompt,
185
- aspect_ratio: options.aspectRatio ?? "16:9"
186
- };
301
+ const body = { prompt };
302
+ if (options.aspectRatio) body.aspect_ratio = options.aspectRatio;
187
303
  if (options.tier) body.tier = options.tier;
188
304
  if (options.model) body.model = options.model;
305
+ if (options.style) body.style = options.style;
189
306
  if (options.durationS !== void 0) body.duration_s = options.durationS;
190
307
  if (options.resolution) body.resolution = options.resolution;
191
308
  if (options.imageUrl) body.image_url = options.imageUrl;
192
- const submitted = await this.c.request("POST", "/v1/videos/generate", body, true);
193
- if (options.wait === false) {
309
+ if (options.audio !== void 0) body.audio = options.audio;
310
+ if (options.endImageUrl) body.end_image_url = options.endImageUrl;
311
+ if (options.camera) body.camera = options.camera;
312
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
313
+ if (options.seed !== void 0) body.seed = options.seed;
314
+ if (options.fps !== void 0) body.fps = options.fps;
315
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
316
+ if (options.webhookUrl) {
317
+ body.webhook_url = options.webhookUrl;
318
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
319
+ }
320
+ const submitted = await this.c.request(
321
+ "POST",
322
+ "/v1/videos/generate",
323
+ body,
324
+ true,
325
+ true,
326
+ options.idempotencyKey
327
+ );
328
+ if (options.webhookUrl || options.wait === false) {
194
329
  return { id: submitted.id, status: submitted.status, raw: submitted };
195
330
  }
196
331
  return videoFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
@@ -202,16 +337,110 @@ var Speech = class {
202
337
  }
203
338
  c;
204
339
  /** Text to speech; synchronous, returns the finished audio and exact charge.
205
- * `model` pins an exact speech model from client.models(). */
340
+ * `model` pins an exact speech model from client.models(),
341
+ * e.g. model: "corent-eleven-multilingual-v2". */
206
342
  async generate(text, options = {}) {
207
343
  const body = { text };
208
344
  if (options.voiceId) body.voice_id = options.voiceId;
209
345
  if (options.model) body.model = options.model;
210
- const r = await this.c.request("POST", "/v1/audio/speech", body, true);
346
+ if (options.stability !== void 0) body.stability = options.stability;
347
+ if (options.similarity !== void 0) body.similarity = options.similarity;
348
+ if (options.style !== void 0) body.style = options.style;
349
+ if (options.speed !== void 0) body.speed = options.speed;
350
+ if (options.language) body.language = options.language;
351
+ if (options.outputFormat) body.output_format = options.outputFormat;
352
+ const r = await this.c.request("POST", "/v1/audio/speech", body, true, true, options.idempotencyKey);
211
353
  const meta = r.meta ?? {};
212
354
  return { id: r.id, url: r.audio_url, model: meta.model, costCents: meta.cost_cents };
213
355
  }
214
356
  };
357
+ var Text = class {
358
+ constructor(c) {
359
+ this.c = c;
360
+ }
361
+ c;
362
+ /** One prompt in, the finished text out. `system` frames the request.
363
+ *
364
+ * `model` pins an exact text model from client.models() (kind "text"),
365
+ * e.g. model: "corent-claude-opus-5"; otherwise `tier` picks the routed
366
+ * lane. Pass tier OR model, not both. */
367
+ async generate(prompt, options = {}) {
368
+ const messages = options.system ? [{ role: "system", content: options.system }, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
369
+ return this.chat(messages, options);
370
+ }
371
+ /** A full OpenAI-shaped conversation, including tool results. Streaming is
372
+ * not wrapped here — point any OpenAI client at https://api.corent.tech/v1
373
+ * with your Corent key for that. */
374
+ async chat(messages, options = {}) {
375
+ const body = {
376
+ // The API's `model` field carries both modes: a pinned catalog name
377
+ // ("corent-claude-opus-5"), or "corent/text-<tier>" for the routed lane.
378
+ // Note the two are different namespaces -- the routed lane keeps its
379
+ // slash form. Whatever the caller passes goes through verbatim; the API
380
+ // still accepts the older bare spellings. lite matches the router's own
381
+ // default for an unrecognised tier.
382
+ model: options.model ?? `corent/text-${options.tier ?? "lite"}`,
383
+ messages
384
+ };
385
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
386
+ if (options.maxCompletionTokens !== void 0) body.max_completion_tokens = options.maxCompletionTokens;
387
+ if (options.temperature !== void 0) body.temperature = options.temperature;
388
+ if (options.tools) body.tools = options.tools;
389
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
390
+ if (options.responseFormat) body.response_format = options.responseFormat;
391
+ const r = await this.c.request("POST", "/v1/chat/completions", body, false, false);
392
+ const choice = (r.choices ?? [{}])[0];
393
+ const message = choice.message ?? {};
394
+ const usage = r.usage ?? {};
395
+ return {
396
+ id: r.id,
397
+ text: message.content ?? "",
398
+ model: r.model,
399
+ finishReason: choice.finish_reason,
400
+ toolCalls: message.tool_calls,
401
+ promptTokens: usage.prompt_tokens,
402
+ completionTokens: usage.completion_tokens,
403
+ costCents: (r.corent ?? {}).cost_cents
404
+ };
405
+ }
406
+ };
407
+ var Batches = class {
408
+ constructor(c) {
409
+ this.c = c;
410
+ }
411
+ c;
412
+ async images(items, options = {}) {
413
+ return this.submit("/v1/images/generate/batch", items.map(imageBatchItemBody), options);
414
+ }
415
+ async videos(items, options = {}) {
416
+ return this.submit("/v1/videos/generate/batch", items.map(videoBatchItemBody), options);
417
+ }
418
+ /** How far along a submitted batch is, and each item's job id. Fetch a
419
+ * finished item's media with client.jobs.get(). */
420
+ async progress(batchId) {
421
+ const r = await this.c.request("GET", `/v1/batches/${batchId}`);
422
+ return {
423
+ batchId: r.batch_id,
424
+ total: r.total,
425
+ completed: r.completed,
426
+ failed: r.failed,
427
+ pending: r.pending,
428
+ jobs: r.jobs ?? []
429
+ };
430
+ }
431
+ async submit(path, items, options) {
432
+ if (items.length < 1 || items.length > MAX_BATCH_ITEMS) {
433
+ throw new InvalidRequestError(`a batch takes 1 to ${MAX_BATCH_ITEMS} items, got ${items.length}`, 400);
434
+ }
435
+ const body = { items };
436
+ if (options.webhookUrl) {
437
+ body.webhook_url = options.webhookUrl;
438
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
439
+ }
440
+ const r = await this.c.request("POST", path, body, true, true, options.idempotencyKey);
441
+ return { batchId: r.batch_id, jobIds: r.job_ids ?? [] };
442
+ }
443
+ };
215
444
  var Jobs = class {
216
445
  constructor(c) {
217
446
  this.c = c;
@@ -221,6 +450,15 @@ var Jobs = class {
221
450
  const raw = await this.c.request("GET", `/v1/jobs/${jobId}`);
222
451
  return { id: raw.id, status: raw.status, raw };
223
452
  }
453
+ /** Stop a job that has not finished and release its money hold.
454
+ *
455
+ * Charges nothing. A job that completed just before the cancel landed stays
456
+ * completed and stays billed for what was really delivered — `cancelled`
457
+ * says which happened. */
458
+ async cancel(jobId) {
459
+ const r = await this.c.request("POST", `/v1/jobs/${jobId}/cancel`);
460
+ return { id: r.id, status: r.status, cancelled: Boolean(r.cancelled) };
461
+ }
224
462
  async wait(jobId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
225
463
  const raw = await this.c.waitForJob(jobId, timeoutMs);
226
464
  return { id: raw.id, status: raw.status, raw };
@@ -264,6 +502,22 @@ function videoFromJob(job) {
264
502
  costCents: meta.cost_cents
265
503
  };
266
504
  }
505
+ function imageBatchItemBody(item) {
506
+ const body = { prompt: item.prompt };
507
+ if (item.tier) body.tier = item.tier;
508
+ if (item.style) body.style = item.style;
509
+ if (item.aspectRatio) body.aspect_ratio = item.aspectRatio;
510
+ return body;
511
+ }
512
+ function videoBatchItemBody(item) {
513
+ const body = { prompt: item.prompt };
514
+ if (item.tier) body.tier = item.tier;
515
+ if (item.style) body.style = item.style;
516
+ if (item.aspectRatio) body.aspect_ratio = item.aspectRatio;
517
+ if (item.durationS !== void 0) body.duration_s = item.durationS;
518
+ if (item.resolution) body.resolution = item.resolution;
519
+ return body;
520
+ }
267
521
  function sleep(ms) {
268
522
  return new Promise((resolve) => setTimeout(resolve, ms));
269
523
  }