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/dist/index.js CHANGED
@@ -3,6 +3,8 @@ var DEFAULT_BASE_URL = "https://api.corent.tech";
3
3
  var MAX_RETRIES = 3;
4
4
  var POLL_INTERVAL_MS = 3e3;
5
5
  var DEFAULT_WAIT_TIMEOUT_MS = 9e5;
6
+ var MAX_BATCH_ITEMS = 50;
7
+ var MAX_IMAGES_PER_REQUEST = 10;
6
8
  var CorentError = class extends Error {
7
9
  constructor(message, statusCode) {
8
10
  super(message);
@@ -30,7 +32,9 @@ var Corent = class {
30
32
  images;
31
33
  videos;
32
34
  speech;
35
+ text;
33
36
  jobs;
37
+ batches;
34
38
  constructor(apiKey, options = {}) {
35
39
  this.apiKey = apiKey;
36
40
  this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
@@ -38,32 +42,63 @@ var Corent = class {
38
42
  this.images = new Images(this);
39
43
  this.videos = new Videos(this);
40
44
  this.speech = new Speech(this);
45
+ this.text = new Text(this);
41
46
  this.jobs = new Jobs(this);
47
+ this.batches = new Batches(this);
42
48
  }
43
- /** Prepaid balance in cents. */
49
+ /** Prepaid balance in cents. See `balance()` for what is actually spendable. */
44
50
  async balanceCents() {
45
51
  const r = await this.request("GET", "/v1/account/balance");
46
52
  return r.balance_cents;
47
53
  }
54
+ /** The full picture: the total, what running generations have reserved, and
55
+ * what a new request can actually spend. availableCents is the number a 402
56
+ * is decided against — check that one before an expensive batch. */
57
+ async balance() {
58
+ const r = await this.request("GET", "/v1/account/balance");
59
+ return {
60
+ balanceCents: r.balance_cents,
61
+ heldCents: r.held_cents ?? 0,
62
+ availableCents: r.available_cents ?? r.balance_cents
63
+ };
64
+ }
65
+ /** The voices `speech.generate` will accept as voiceId. This exists
66
+ * because the API requires a voice id and, until 2026-08-30, published no
67
+ * list of legal values. */
68
+ async voices() {
69
+ const r = await this.request("GET", "/v1/voices");
70
+ return r.voices ?? [];
71
+ }
72
+ /** What this account has spent, by lane and over time. */
73
+ async usage() {
74
+ return this.request("GET", "/v1/account/usage");
75
+ }
76
+ /** Live operational status of the generation tiers. Public. */
77
+ async status() {
78
+ return this.request("GET", "/v1/status");
79
+ }
48
80
  /** The live tier catalog with honest min–max price ranges. Public. */
49
81
  async tiers() {
50
82
  const r = await this.request("GET", "/v1/tiers");
51
83
  return r.tiers;
52
84
  }
53
85
  /** The direct-access menu: every model you can pin by name, with its kind,
54
- * quality score and flat cost-plus price. Public. */
86
+ * quality score and live status. Names come back in their published
87
+ * spelling ("corent-flux-schnell") and can be passed straight back as
88
+ * `model`. Carries no price -- billing is flat cost-plus and the exact
89
+ * charge comes back on each generation. Public. */
55
90
  async models() {
56
91
  const r = await this.request("GET", "/v1/models");
57
92
  return r.models;
58
93
  }
59
94
  /** @internal */
60
- async request(method, path, body, idempotent = false) {
95
+ async request(method, path, body, idempotent = false, retryTransport = true, idempotencyKey) {
61
96
  const headers = {
62
97
  Authorization: `Bearer ${this.apiKey}`,
63
- "User-Agent": "corent-js/0.1.0"
98
+ "User-Agent": "corent-js/0.3.0"
64
99
  };
65
100
  if (body) headers["Content-Type"] = "application/json";
66
- if (idempotent) headers["Idempotency-Key"] = crypto.randomUUID();
101
+ if (idempotent) headers["Idempotency-Key"] = idempotencyKey ?? crypto.randomUUID();
67
102
  let lastError;
68
103
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
69
104
  let resp;
@@ -75,6 +110,7 @@ var Corent = class {
75
110
  });
76
111
  } catch (err) {
77
112
  lastError = err;
113
+ if (!retryTransport) throw new CorentError(`request failed: ${err}`);
78
114
  await sleep(Math.min(2 ** attempt * 1e3, 8e3));
79
115
  continue;
80
116
  }
@@ -121,8 +157,11 @@ var Images = class {
121
157
  *
122
158
  * `model` pins an exact model from client.models() instead of letting the
123
159
  * router choose — direct access: never substituted, flat cost-plus price.
124
- * Pass tier OR model, not both. */
160
+ * e.g. model: "corent-flux-schnell". Pass tier OR model, not both. */
125
161
  async generate(prompt, options = {}) {
162
+ if (options.referenceImageUrls && (options.referenceImageUrls.length < 1 || options.referenceImageUrls.length > 4)) {
163
+ throw new InvalidRequestError("referenceImageUrls must contain 1 to 4 image URLs", 400);
164
+ }
126
165
  const body = {
127
166
  prompt,
128
167
  aspect_ratio: options.aspectRatio ?? "1:1",
@@ -131,13 +170,91 @@ var Images = class {
131
170
  if (options.tier) body.tier = options.tier;
132
171
  if (options.model) body.model = options.model;
133
172
  if (options.style) body.style = options.style;
134
- const submitted = await this.c.request("POST", "/v1/images/generate", body, true);
173
+ if (options.referenceImageUrls) body.reference_image_urls = options.referenceImageUrls;
174
+ if (options.seed !== void 0) body.seed = options.seed;
175
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
176
+ if (options.sourceImageUrl) body.source_image_url = options.sourceImageUrl;
177
+ if (options.strength !== void 0) body.strength = options.strength;
178
+ if (options.outputFormat) body.output_format = options.outputFormat;
179
+ if (options.transparent) body.transparent = true;
180
+ if (options.width !== void 0) body.width = options.width;
181
+ if (options.height !== void 0) body.height = options.height;
182
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
183
+ if (options.webhookUrl) {
184
+ body.webhook_url = options.webhookUrl;
185
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
186
+ delete body.async;
187
+ }
188
+ const submitted = await this.c.request(
189
+ "POST",
190
+ "/v1/images/generate",
191
+ body,
192
+ true,
193
+ true,
194
+ options.idempotencyKey
195
+ );
135
196
  if (submitted.status === "completed") return imageFromJob(submitted);
136
- if (options.wait === false) {
197
+ if (options.webhookUrl || options.wait === false) {
137
198
  return { id: submitted.id, status: submitted.status, raw: submitted };
138
199
  }
139
200
  return imageFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
140
201
  }
202
+ /** Render several versions of one prompt in a single call, 2–10.
203
+ *
204
+ * Each one is a real render at the normal price, so asking for four costs
205
+ * four images. They come back together, and `totalCostCents` is the whole
206
+ * charge. Partial success is a real outcome: if three of four land you get
207
+ * three and are billed for three.
208
+ *
209
+ * Different from client.batches.images(), which runs DIFFERENT prompts and
210
+ * returns job ids to poll. This one runs the SAME prompt and waits. */
211
+ async generateMany(prompt, count, options = {}) {
212
+ if (count < 2 || count > MAX_IMAGES_PER_REQUEST) {
213
+ throw new InvalidRequestError(
214
+ `generateMany takes 2 to ${MAX_IMAGES_PER_REQUEST}; use generate() for one`,
215
+ 400
216
+ );
217
+ }
218
+ const body = {
219
+ prompt,
220
+ aspect_ratio: options.aspectRatio ?? "1:1",
221
+ n: count
222
+ };
223
+ if (options.tier) body.tier = options.tier;
224
+ if (options.model) body.model = options.model;
225
+ if (options.style) body.style = options.style;
226
+ if (options.referenceImageUrls) body.reference_image_urls = options.referenceImageUrls;
227
+ if (options.seed !== void 0) body.seed = options.seed;
228
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
229
+ if (options.sourceImageUrl) body.source_image_url = options.sourceImageUrl;
230
+ if (options.strength !== void 0) body.strength = options.strength;
231
+ if (options.outputFormat) body.output_format = options.outputFormat;
232
+ if (options.transparent) body.transparent = true;
233
+ if (options.width !== void 0) body.width = options.width;
234
+ if (options.height !== void 0) body.height = options.height;
235
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
236
+ const r = await this.c.request(
237
+ "POST",
238
+ "/v1/images/generate",
239
+ body,
240
+ true,
241
+ true,
242
+ options.idempotencyKey
243
+ );
244
+ const meta = r.meta ?? {};
245
+ return {
246
+ images: (r.images ?? []).map((image) => ({
247
+ id: r.id,
248
+ url: image.url,
249
+ width: image.width,
250
+ height: image.height,
251
+ model: meta.model,
252
+ costCents: void 0
253
+ })),
254
+ // The total for every image that actually landed.
255
+ totalCostCents: meta.cost_cents
256
+ };
257
+ }
141
258
  };
142
259
  var Videos = class {
143
260
  constructor(c) {
@@ -150,19 +267,37 @@ var Videos = class {
150
267
  *
151
268
  * `model` pins an exact model from client.models() — direct access, never
152
269
  * substituted, and duration/resolution snap to THAT model's own menu rather
153
- * than a tier cap. Pass tier OR model, not both. */
270
+ * than a tier cap. e.g. model: "corent-seedance-2.0". Pass tier OR model,
271
+ * not both. */
154
272
  async generate(prompt, options = {}) {
155
- const body = {
156
- prompt,
157
- aspect_ratio: options.aspectRatio ?? "16:9"
158
- };
273
+ const body = { prompt };
274
+ if (options.aspectRatio) body.aspect_ratio = options.aspectRatio;
159
275
  if (options.tier) body.tier = options.tier;
160
276
  if (options.model) body.model = options.model;
277
+ if (options.style) body.style = options.style;
161
278
  if (options.durationS !== void 0) body.duration_s = options.durationS;
162
279
  if (options.resolution) body.resolution = options.resolution;
163
280
  if (options.imageUrl) body.image_url = options.imageUrl;
164
- const submitted = await this.c.request("POST", "/v1/videos/generate", body, true);
165
- if (options.wait === false) {
281
+ if (options.audio !== void 0) body.audio = options.audio;
282
+ if (options.endImageUrl) body.end_image_url = options.endImageUrl;
283
+ if (options.camera) body.camera = options.camera;
284
+ if (options.negativePrompt) body.negative_prompt = options.negativePrompt;
285
+ if (options.seed !== void 0) body.seed = options.seed;
286
+ if (options.fps !== void 0) body.fps = options.fps;
287
+ if (options.enhancePrompt === false) body.enhance_prompt = false;
288
+ if (options.webhookUrl) {
289
+ body.webhook_url = options.webhookUrl;
290
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
291
+ }
292
+ const submitted = await this.c.request(
293
+ "POST",
294
+ "/v1/videos/generate",
295
+ body,
296
+ true,
297
+ true,
298
+ options.idempotencyKey
299
+ );
300
+ if (options.webhookUrl || options.wait === false) {
166
301
  return { id: submitted.id, status: submitted.status, raw: submitted };
167
302
  }
168
303
  return videoFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
@@ -174,16 +309,110 @@ var Speech = class {
174
309
  }
175
310
  c;
176
311
  /** Text to speech; synchronous, returns the finished audio and exact charge.
177
- * `model` pins an exact speech model from client.models(). */
312
+ * `model` pins an exact speech model from client.models(),
313
+ * e.g. model: "corent-eleven-multilingual-v2". */
178
314
  async generate(text, options = {}) {
179
315
  const body = { text };
180
316
  if (options.voiceId) body.voice_id = options.voiceId;
181
317
  if (options.model) body.model = options.model;
182
- const r = await this.c.request("POST", "/v1/audio/speech", body, true);
318
+ if (options.stability !== void 0) body.stability = options.stability;
319
+ if (options.similarity !== void 0) body.similarity = options.similarity;
320
+ if (options.style !== void 0) body.style = options.style;
321
+ if (options.speed !== void 0) body.speed = options.speed;
322
+ if (options.language) body.language = options.language;
323
+ if (options.outputFormat) body.output_format = options.outputFormat;
324
+ const r = await this.c.request("POST", "/v1/audio/speech", body, true, true, options.idempotencyKey);
183
325
  const meta = r.meta ?? {};
184
326
  return { id: r.id, url: r.audio_url, model: meta.model, costCents: meta.cost_cents };
185
327
  }
186
328
  };
329
+ var Text = class {
330
+ constructor(c) {
331
+ this.c = c;
332
+ }
333
+ c;
334
+ /** One prompt in, the finished text out. `system` frames the request.
335
+ *
336
+ * `model` pins an exact text model from client.models() (kind "text"),
337
+ * e.g. model: "corent-claude-opus-5"; otherwise `tier` picks the routed
338
+ * lane. Pass tier OR model, not both. */
339
+ async generate(prompt, options = {}) {
340
+ const messages = options.system ? [{ role: "system", content: options.system }, { role: "user", content: prompt }] : [{ role: "user", content: prompt }];
341
+ return this.chat(messages, options);
342
+ }
343
+ /** A full OpenAI-shaped conversation, including tool results. Streaming is
344
+ * not wrapped here — point any OpenAI client at https://api.corent.tech/v1
345
+ * with your Corent key for that. */
346
+ async chat(messages, options = {}) {
347
+ const body = {
348
+ // The API's `model` field carries both modes: a pinned catalog name
349
+ // ("corent-claude-opus-5"), or "corent/text-<tier>" for the routed lane.
350
+ // Note the two are different namespaces -- the routed lane keeps its
351
+ // slash form. Whatever the caller passes goes through verbatim; the API
352
+ // still accepts the older bare spellings. lite matches the router's own
353
+ // default for an unrecognised tier.
354
+ model: options.model ?? `corent/text-${options.tier ?? "lite"}`,
355
+ messages
356
+ };
357
+ if (options.maxTokens !== void 0) body.max_tokens = options.maxTokens;
358
+ if (options.maxCompletionTokens !== void 0) body.max_completion_tokens = options.maxCompletionTokens;
359
+ if (options.temperature !== void 0) body.temperature = options.temperature;
360
+ if (options.tools) body.tools = options.tools;
361
+ if (options.toolChoice !== void 0) body.tool_choice = options.toolChoice;
362
+ if (options.responseFormat) body.response_format = options.responseFormat;
363
+ const r = await this.c.request("POST", "/v1/chat/completions", body, false, false);
364
+ const choice = (r.choices ?? [{}])[0];
365
+ const message = choice.message ?? {};
366
+ const usage = r.usage ?? {};
367
+ return {
368
+ id: r.id,
369
+ text: message.content ?? "",
370
+ model: r.model,
371
+ finishReason: choice.finish_reason,
372
+ toolCalls: message.tool_calls,
373
+ promptTokens: usage.prompt_tokens,
374
+ completionTokens: usage.completion_tokens,
375
+ costCents: (r.corent ?? {}).cost_cents
376
+ };
377
+ }
378
+ };
379
+ var Batches = class {
380
+ constructor(c) {
381
+ this.c = c;
382
+ }
383
+ c;
384
+ async images(items, options = {}) {
385
+ return this.submit("/v1/images/generate/batch", items.map(imageBatchItemBody), options);
386
+ }
387
+ async videos(items, options = {}) {
388
+ return this.submit("/v1/videos/generate/batch", items.map(videoBatchItemBody), options);
389
+ }
390
+ /** How far along a submitted batch is, and each item's job id. Fetch a
391
+ * finished item's media with client.jobs.get(). */
392
+ async progress(batchId) {
393
+ const r = await this.c.request("GET", `/v1/batches/${batchId}`);
394
+ return {
395
+ batchId: r.batch_id,
396
+ total: r.total,
397
+ completed: r.completed,
398
+ failed: r.failed,
399
+ pending: r.pending,
400
+ jobs: r.jobs ?? []
401
+ };
402
+ }
403
+ async submit(path, items, options) {
404
+ if (items.length < 1 || items.length > MAX_BATCH_ITEMS) {
405
+ throw new InvalidRequestError(`a batch takes 1 to ${MAX_BATCH_ITEMS} items, got ${items.length}`, 400);
406
+ }
407
+ const body = { items };
408
+ if (options.webhookUrl) {
409
+ body.webhook_url = options.webhookUrl;
410
+ if (options.webhookSecret) body.webhook_secret = options.webhookSecret;
411
+ }
412
+ const r = await this.c.request("POST", path, body, true, true, options.idempotencyKey);
413
+ return { batchId: r.batch_id, jobIds: r.job_ids ?? [] };
414
+ }
415
+ };
187
416
  var Jobs = class {
188
417
  constructor(c) {
189
418
  this.c = c;
@@ -193,6 +422,15 @@ var Jobs = class {
193
422
  const raw = await this.c.request("GET", `/v1/jobs/${jobId}`);
194
423
  return { id: raw.id, status: raw.status, raw };
195
424
  }
425
+ /** Stop a job that has not finished and release its money hold.
426
+ *
427
+ * Charges nothing. A job that completed just before the cancel landed stays
428
+ * completed and stays billed for what was really delivered — `cancelled`
429
+ * says which happened. */
430
+ async cancel(jobId) {
431
+ const r = await this.c.request("POST", `/v1/jobs/${jobId}/cancel`);
432
+ return { id: r.id, status: r.status, cancelled: Boolean(r.cancelled) };
433
+ }
196
434
  async wait(jobId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
197
435
  const raw = await this.c.waitForJob(jobId, timeoutMs);
198
436
  return { id: raw.id, status: raw.status, raw };
@@ -236,6 +474,22 @@ function videoFromJob(job) {
236
474
  costCents: meta.cost_cents
237
475
  };
238
476
  }
477
+ function imageBatchItemBody(item) {
478
+ const body = { prompt: item.prompt };
479
+ if (item.tier) body.tier = item.tier;
480
+ if (item.style) body.style = item.style;
481
+ if (item.aspectRatio) body.aspect_ratio = item.aspectRatio;
482
+ return body;
483
+ }
484
+ function videoBatchItemBody(item) {
485
+ const body = { prompt: item.prompt };
486
+ if (item.tier) body.tier = item.tier;
487
+ if (item.style) body.style = item.style;
488
+ if (item.aspectRatio) body.aspect_ratio = item.aspectRatio;
489
+ if (item.durationS !== void 0) body.duration_s = item.durationS;
490
+ if (item.resolution) body.resolution = item.resolution;
491
+ return body;
492
+ }
239
493
  function sleep(ms) {
240
494
  return new Promise((resolve) => setTimeout(resolve, ms));
241
495
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "corent-sdk",
3
- "version": "0.2.0",
4
- "description": "Official TypeScript/JavaScript SDK for Corent — one API for AI image, video, and voice generation with built-in routing, failover, and exact receipts.",
3
+ "version": "0.3.0",
4
+ "description": "Official TypeScript/JavaScript SDK for Corent — one API for AI image, video, voice, and text generation with built-in routing, failover, and exact receipts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.cjs",
@@ -20,7 +20,7 @@
20
20
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
21
21
  "test": "npm run build && node --test test/*.test.js"
22
22
  },
23
- "keywords": ["ai", "image-generation", "video-generation", "text-to-speech", "api", "corent"],
23
+ "keywords": ["ai", "image-generation", "video-generation", "text-to-speech", "llm", "api", "corent"],
24
24
  "homepage": "https://corent.tech/docs",
25
25
  "devDependencies": {
26
26
  "tsup": "^8.0.0",