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/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,26 +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
  }
85
+ /** The direct-access menu: every model you can pin by name, with its kind,
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. */
90
+ async models() {
91
+ const r = await this.request("GET", "/v1/models");
92
+ return r.models;
93
+ }
53
94
  /** @internal */
54
- async request(method, path, body, idempotent = false) {
95
+ async request(method, path, body, idempotent = false, retryTransport = true, idempotencyKey) {
55
96
  const headers = {
56
97
  Authorization: `Bearer ${this.apiKey}`,
57
- "User-Agent": "corent-js/0.1.0"
98
+ "User-Agent": "corent-js/0.3.0"
58
99
  };
59
100
  if (body) headers["Content-Type"] = "application/json";
60
- if (idempotent) headers["Idempotency-Key"] = crypto.randomUUID();
101
+ if (idempotent) headers["Idempotency-Key"] = idempotencyKey ?? crypto.randomUUID();
61
102
  let lastError;
62
103
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
63
104
  let resp;
@@ -69,6 +110,7 @@ var Corent = class {
69
110
  });
70
111
  } catch (err) {
71
112
  lastError = err;
113
+ if (!retryTransport) throw new CorentError(`request failed: ${err}`);
72
114
  await sleep(Math.min(2 ** attempt * 1e3, 8e3));
73
115
  continue;
74
116
  }
@@ -111,22 +153,108 @@ var Images = class {
111
153
  }
112
154
  c;
113
155
  /** Render an image. Submits as a background job and polls, so a client
114
- * timeout can never lose a finished render. Pass wait:false for the Job. */
156
+ * timeout can never lose a finished render. Pass wait:false for the Job.
157
+ *
158
+ * `model` pins an exact model from client.models() instead of letting the
159
+ * router choose — direct access: never substituted, flat cost-plus price.
160
+ * e.g. model: "corent-flux-schnell". Pass tier OR model, not both. */
115
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
+ }
116
165
  const body = {
117
166
  prompt,
118
167
  aspect_ratio: options.aspectRatio ?? "1:1",
119
168
  async: true
120
169
  };
121
170
  if (options.tier) body.tier = options.tier;
171
+ if (options.model) body.model = options.model;
122
172
  if (options.style) body.style = options.style;
123
- 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
+ );
124
196
  if (submitted.status === "completed") return imageFromJob(submitted);
125
- if (options.wait === false) {
197
+ if (options.webhookUrl || options.wait === false) {
126
198
  return { id: submitted.id, status: submitted.status, raw: submitted };
127
199
  }
128
200
  return imageFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
129
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
+ }
130
258
  };
131
259
  var Videos = class {
132
260
  constructor(c) {
@@ -135,18 +263,41 @@ var Videos = class {
135
263
  c;
136
264
  /** Render a video (1–5 minutes typical). resolution: 720p | 1080p | 4k —
137
265
  * tier-capped, clamped down rather than rejected. imageUrl animates an
138
- * existing image (image-to-video). */
266
+ * existing image (image-to-video).
267
+ *
268
+ * `model` pins an exact model from client.models() — direct access, never
269
+ * substituted, and duration/resolution snap to THAT model's own menu rather
270
+ * than a tier cap. e.g. model: "corent-seedance-2.0". Pass tier OR model,
271
+ * not both. */
139
272
  async generate(prompt, options = {}) {
140
- const body = {
141
- prompt,
142
- aspect_ratio: options.aspectRatio ?? "16:9"
143
- };
273
+ const body = { prompt };
274
+ if (options.aspectRatio) body.aspect_ratio = options.aspectRatio;
144
275
  if (options.tier) body.tier = options.tier;
276
+ if (options.model) body.model = options.model;
277
+ if (options.style) body.style = options.style;
145
278
  if (options.durationS !== void 0) body.duration_s = options.durationS;
146
279
  if (options.resolution) body.resolution = options.resolution;
147
280
  if (options.imageUrl) body.image_url = options.imageUrl;
148
- const submitted = await this.c.request("POST", "/v1/videos/generate", body, true);
149
- 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) {
150
301
  return { id: submitted.id, status: submitted.status, raw: submitted };
151
302
  }
152
303
  return videoFromJob(await this.c.waitForJob(submitted.id, options.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
@@ -157,15 +308,111 @@ var Speech = class {
157
308
  this.c = c;
158
309
  }
159
310
  c;
160
- /** Text to speech; synchronous, returns the finished audio and exact charge. */
311
+ /** Text to speech; synchronous, returns the finished audio and exact charge.
312
+ * `model` pins an exact speech model from client.models(),
313
+ * e.g. model: "corent-eleven-multilingual-v2". */
161
314
  async generate(text, options = {}) {
162
315
  const body = { text };
163
316
  if (options.voiceId) body.voice_id = options.voiceId;
164
- const r = await this.c.request("POST", "/v1/audio/speech", body, true);
317
+ if (options.model) body.model = options.model;
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);
165
325
  const meta = r.meta ?? {};
166
326
  return { id: r.id, url: r.audio_url, model: meta.model, costCents: meta.cost_cents };
167
327
  }
168
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
+ };
169
416
  var Jobs = class {
170
417
  constructor(c) {
171
418
  this.c = c;
@@ -175,6 +422,15 @@ var Jobs = class {
175
422
  const raw = await this.c.request("GET", `/v1/jobs/${jobId}`);
176
423
  return { id: raw.id, status: raw.status, raw };
177
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
+ }
178
434
  async wait(jobId, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
179
435
  const raw = await this.c.waitForJob(jobId, timeoutMs);
180
436
  return { id: raw.id, status: raw.status, raw };
@@ -218,6 +474,22 @@ function videoFromJob(job) {
218
474
  costCents: meta.cost_cents
219
475
  };
220
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
+ }
221
493
  function sleep(ms) {
222
494
  return new Promise((resolve) => setTimeout(resolve, ms));
223
495
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "corent-sdk",
3
- "version": "0.1.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",