corent-sdk 0.1.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
@@ -35,14 +38,136 @@ console.log(speech.url, speech.costCents);
35
38
  - **Backoff** — 429/5xx retried with `Retry-After` respected.
36
39
  - **Honest receipts** — `width`/`height` are the *measured* pixels of the delivered file; `costCents` is the exact charge.
37
40
 
41
+ ## Pick a model yourself (direct access)
42
+
43
+ Pass `model` instead of `tier` to pin an exact model. It is never substituted:
44
+ if that model can't deliver, the call fails and you are not charged.
45
+
46
+ ```ts
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.
62
+
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
+ );
82
+ ```
83
+
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.
156
+
38
157
  ## Fine-grained control
39
158
 
40
159
  ```ts
41
160
  const job = await client.images.generate("...", { tier: "max_pro", wait: false });
42
161
  const done = await client.jobs.wait(job.id); // resume any time
43
162
 
44
- await client.tiers(); // live catalog with honest min–max price ranges
45
- 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
46
167
  ```
47
168
 
48
- Tiers: `air` | `lite` | `premium` | `pro` | `max_pro` see [corent.tech/pricing](https://corent.tech/pricing). Docs: [corent.tech/docs](https://corent.tech/docs). MCP server for agents: [`corent-mcp`](https://www.npmjs.com/package/corent-mcp).
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
+
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,26 +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
  }
113
+ /** The direct-access menu: every model you can pin by name, with its kind,
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. */
118
+ async models() {
119
+ const r = await this.request("GET", "/v1/models");
120
+ return r.models;
121
+ }
81
122
  /** @internal */
82
- async request(method, path, body, idempotent = false) {
123
+ async request(method, path, body, idempotent = false, retryTransport = true, idempotencyKey) {
83
124
  const headers = {
84
125
  Authorization: `Bearer ${this.apiKey}`,
85
- "User-Agent": "corent-js/0.1.0"
126
+ "User-Agent": "corent-js/0.3.0"
86
127
  };
87
128
  if (body) headers["Content-Type"] = "application/json";
88
- if (idempotent) headers["Idempotency-Key"] = crypto.randomUUID();
129
+ if (idempotent) headers["Idempotency-Key"] = idempotencyKey ?? crypto.randomUUID();
89
130
  let lastError;
90
131
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
91
132
  let resp;
@@ -97,6 +138,7 @@ var Corent = class {
97
138
  });
98
139
  } catch (err) {
99
140
  lastError = err;
141
+ if (!retryTransport) throw new CorentError(`request failed: ${err}`);
100
142
  await sleep(Math.min(2 ** attempt * 1e3, 8e3));
101
143
  continue;
102
144
  }
@@ -139,22 +181,108 @@ var Images = class {
139
181
  }
140
182
  c;
141
183
  /** Render an image. Submits as a background job and polls, so a client
142
- * timeout can never lose a finished render. Pass wait:false for the Job. */
184
+ * timeout can never lose a finished render. Pass wait:false for the Job.
185
+ *
186
+ * `model` pins an exact model from client.models() instead of letting the
187
+ * router choose — direct access: never substituted, flat cost-plus price.
188
+ * e.g. model: "corent-flux-schnell". Pass tier OR model, not both. */
143
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
+ }
144
193
  const body = {
145
194
  prompt,
146
195
  aspect_ratio: options.aspectRatio ?? "1:1",
147
196
  async: true
148
197
  };
149
198
  if (options.tier) body.tier = options.tier;
199
+ if (options.model) body.model = options.model;
150
200
  if (options.style) body.style = options.style;
151
- 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
+ );
152
224
  if (submitted.status === "completed") return imageFromJob(submitted);
153
- if (options.wait === false) {
225
+ if (options.webhookUrl || options.wait === false) {
154
226
  return { id: submitted.id, status: submitted.status, raw: submitted };
155
227
  }
156
228
  return imageFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
157
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
+ }
158
286
  };
159
287
  var Videos = class {
160
288
  constructor(c) {
@@ -163,18 +291,41 @@ var Videos = class {
163
291
  c;
164
292
  /** Render a video (1–5 minutes typical). resolution: 720p | 1080p | 4k —
165
293
  * tier-capped, clamped down rather than rejected. imageUrl animates an
166
- * existing image (image-to-video). */
294
+ * existing image (image-to-video).
295
+ *
296
+ * `model` pins an exact model from client.models() — direct access, never
297
+ * substituted, and duration/resolution snap to THAT model's own menu rather
298
+ * than a tier cap. e.g. model: "corent-seedance-2.0". Pass tier OR model,
299
+ * not both. */
167
300
  async generate(prompt, options = {}) {
168
- const body = {
169
- prompt,
170
- aspect_ratio: options.aspectRatio ?? "16:9"
171
- };
301
+ const body = { prompt };
302
+ if (options.aspectRatio) body.aspect_ratio = options.aspectRatio;
172
303
  if (options.tier) body.tier = options.tier;
304
+ if (options.model) body.model = options.model;
305
+ if (options.style) body.style = options.style;
173
306
  if (options.durationS !== void 0) body.duration_s = options.durationS;
174
307
  if (options.resolution) body.resolution = options.resolution;
175
308
  if (options.imageUrl) body.image_url = options.imageUrl;
176
- const submitted = await this.c.request("POST", "/v1/videos/generate", body, true);
177
- 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) {
178
329
  return { id: submitted.id, status: submitted.status, raw: submitted };
179
330
  }
180
331
  return videoFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
@@ -185,15 +336,111 @@ var Speech = class {
185
336
  this.c = c;
186
337
  }
187
338
  c;
188
- /** Text to speech; synchronous, returns the finished audio and exact charge. */
339
+ /** Text to speech; synchronous, returns the finished audio and exact charge.
340
+ * `model` pins an exact speech model from client.models(),
341
+ * e.g. model: "corent-eleven-multilingual-v2". */
189
342
  async generate(text, options = {}) {
190
343
  const body = { text };
191
344
  if (options.voiceId) body.voice_id = options.voiceId;
192
- const r = await this.c.request("POST", "/v1/audio/speech", body, true);
345
+ if (options.model) body.model = options.model;
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);
193
353
  const meta = r.meta ?? {};
194
354
  return { id: r.id, url: r.audio_url, model: meta.model, costCents: meta.cost_cents };
195
355
  }
196
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
+ };
197
444
  var Jobs = class {
198
445
  constructor(c) {
199
446
  this.c = c;
@@ -203,6 +450,15 @@ var Jobs = class {
203
450
  const raw = await this.c.request("GET", `/v1/jobs/${jobId}`);
204
451
  return { id: raw.id, status: raw.status, raw };
205
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
+ }
206
462
  async wait(jobId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
207
463
  const raw = await this.c.waitForJob(jobId, timeoutMs);
208
464
  return { id: raw.id, status: raw.status, raw };
@@ -246,6 +502,22 @@ function videoFromJob(job) {
246
502
  costCents: meta.cost_cents
247
503
  };
248
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
+ }
249
521
  function sleep(ms) {
250
522
  return new Promise((resolve) => setTimeout(resolve, ms));
251
523
  }