@saplingai/sapling-js 1.0.45 → 1.0.53

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/sapling/client.js CHANGED
@@ -3,6 +3,76 @@ import { v4 as uuidv4 } from 'uuid';
3
3
 
4
4
  // Value types an extract() field may declare; 'string' is the default.
5
5
  const EXTRACT_FIELD_TYPES = ['string', 'number', 'integer', 'boolean', 'date', 'list'];
6
+ // Registers translate() accepts; the API rejects anything else with a 400.
7
+ const TRANSLATE_FORMALITIES = ['formal', 'informal'];
8
+ const SUMMARIZE_LENGTHS = ['short', 'medium', 'long'];
9
+ // Reading levels simplify() accepts; the API rejects anything else with a 400.
10
+ const SIMPLIFY_READING_LEVELS = ['plain', 'elementary', 'middle_school', 'high_school'];
11
+ // rephrase() enums, mirroring the API's validators (utils/request_validator.py),
12
+ // which reject anything else with a 400.
13
+ const REPHRASE_MAPPINGS = [
14
+ 'paraphrase', 'passive_to_active', 'active_to_passive', 'informal_to_formal',
15
+ 'formal_to_informal', 'sentence_split', 'reword', 'generate_title', 'generate_acronym',
16
+ 'generate_meta_description', 'summarize', 'expand', 'shorten', 'switch_tone', 'switch_tense',
17
+ ];
18
+ const REPHRASE_TONE_MAPPINGS = [
19
+ 'straightforward', 'confident', 'friendly', 'empathetic', 'professional', 'casual',
20
+ 'diplomatic', 'persuasive',
21
+ ];
22
+ const REPHRASE_TENSE_MAPPINGS = [
23
+ 'simple past', 'past perfect', 'past continuous', 'past perfect continuous',
24
+ 'simple present', 'present perfect', 'present continuous', 'present perfect continuous',
25
+ 'simple future', 'future perfect', 'future continuous', 'future perfect continuous',
26
+ ];
27
+ // Languages statistics() scores readability for (plus 'auto').
28
+ const STATISTICS_LANGS = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'ru', 'ar'];
29
+
30
+ // Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
31
+ // size limits (rule/description length) are server configuration and enforced
32
+ // there with a clear 400 (see classify()).
33
+ const validateStyleguideRules = (rules, method) => {
34
+ const isValidRule = (rule) => {
35
+ if (typeof rule === 'string') {
36
+ return rule.trim().length > 0;
37
+ }
38
+ if (rule == null || typeof rule !== 'object' || Array.isArray(rule)) {
39
+ return false;
40
+ }
41
+ if (typeof rule.name !== 'string' || rule.name.trim().length === 0) {
42
+ return false;
43
+ }
44
+ if (rule.description !== undefined && typeof rule.description !== 'string') {
45
+ return false;
46
+ }
47
+ // Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
48
+ // here rather than being forwarded to the API as a malformed rule (the
49
+ // classify()/extract() convention; the MCP object schema is `.strict()`).
50
+ const allowed = new Set(['name', 'description']);
51
+ if (Object.keys(rule).some((key) => !allowed.has(key))) {
52
+ return false;
53
+ }
54
+ return true;
55
+ };
56
+
57
+ if (
58
+ !Array.isArray(rules)
59
+ || rules.length < 1
60
+ || rules.length > 20
61
+ // Array.from() materializes holes in sparse arrays as `undefined` (which
62
+ // isValidRule rejects); a bare `.every()` would skip them and let a hole
63
+ // through to be serialized as `null`.
64
+ || !Array.from(rules).every(isValidRule)
65
+ ) {
66
+ throw new TypeError(`${method}: rules must be an array of 1 to 20 non-empty strings or `
67
+ + '{name, description?} objects (string name, optional string description).');
68
+ }
69
+
70
+ const names = rules.map(
71
+ (rule) => (typeof rule === 'string' ? rule : rule.name).trim().toLowerCase());
72
+ if (names.length !== new Set(names).size) {
73
+ throw new TypeError(`${method}: rule names must be unique (case-insensitive).`);
74
+ }
75
+ };
6
76
 
7
77
  export class Client {
8
78
  constructor(apiKey, options = {}) {
@@ -60,6 +130,11 @@ export class Client {
60
130
  data.advanced_edits = options.advancedEdits;
61
131
  }
62
132
 
133
+ // Omitted unless set; an explicit null is forwarded so the API rejects it.
134
+ if (options.includeErrorCategories !== undefined) {
135
+ data.include_error_categories = options.includeErrorCategories;
136
+ }
137
+
63
138
  if (userDetails) {
64
139
  const { userId, anonUser } = userDetails;
65
140
  data.user_id = userId;
@@ -180,6 +255,286 @@ export class Client {
180
255
  return this.axiosPromise(url, 'post', data);
181
256
  }
182
257
 
258
+ /**
259
+ * Rephrase / paraphrase: generate alternative phrasings of a text.
260
+ *
261
+ * Returns `results`: one `{hash, original, replacement, rephrase_type,
262
+ * model_version}` entry per alternative. HTML is stripped server-side.
263
+ *
264
+ * @param {string} text - The text to rephrase (the API caps its length).
265
+ * @param {Object} [options]
266
+ * @param {string} [options.mapping] - Transformation to apply (see
267
+ * REPHRASE_MAPPINGS). Defaults to 'paraphrase'.
268
+ * @param {string} [options.tone_mapping] - Target tone; only used with
269
+ * mapping 'switch_tone'. Defaults to 'straightforward'.
270
+ * @param {string} [options.tense_mapping] - Target tense; only used with
271
+ * mapping 'switch_tense'. Defaults to 'simple past'.
272
+ * @param {number} [options.num_results] - Number of alternatives, 1–8
273
+ * (default 5). Honored by the paraphrase, informal_to_formal,
274
+ * formal_to_informal, generate_title and generate_acronym mappings; other
275
+ * mappings return their default number of results.
276
+ * @param {string} [options.session_id] - Optional session identifier used to
277
+ * group feedback events.
278
+ * @returns {Promise<AxiosResponse>}
279
+ */
280
+ rephrase(text, options = {}) {
281
+ const url = `${this.urlEndpoint}rephrase`;
282
+
283
+ // Shape/type/enum checks only; the length cap is server configuration and
284
+ // enforced there with a clear 400 (see seo()). The enums and the 1–8
285
+ // num_results bound mirror the API's validators, which reject anything else.
286
+ if (typeof text !== 'string' || text.trim().length === 0) {
287
+ throw new TypeError('rephrase: text must be a non-empty string.');
288
+ }
289
+
290
+ let data = {
291
+ text,
292
+ key: this.apiKey,
293
+ }
294
+
295
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
296
+ // type rather than silently treated as "omitted", matching classify()/seo().
297
+ if (options?.mapping !== undefined) {
298
+ if (!REPHRASE_MAPPINGS.includes(options.mapping)) {
299
+ throw new TypeError(`rephrase: mapping must be one of ${REPHRASE_MAPPINGS.join(', ')}.`);
300
+ }
301
+ data.mapping = options.mapping;
302
+ }
303
+ if (options?.tone_mapping !== undefined) {
304
+ if (!REPHRASE_TONE_MAPPINGS.includes(options.tone_mapping)) {
305
+ throw new TypeError(
306
+ `rephrase: tone_mapping must be one of ${REPHRASE_TONE_MAPPINGS.join(', ')}.`);
307
+ }
308
+ data.tone_mapping = options.tone_mapping;
309
+ }
310
+ if (options?.tense_mapping !== undefined) {
311
+ if (!REPHRASE_TENSE_MAPPINGS.includes(options.tense_mapping)) {
312
+ throw new TypeError(
313
+ `rephrase: tense_mapping must be one of ${REPHRASE_TENSE_MAPPINGS.join(', ')}.`);
314
+ }
315
+ data.tense_mapping = options.tense_mapping;
316
+ }
317
+ if (options?.num_results !== undefined) {
318
+ if (!Number.isInteger(options.num_results)
319
+ || options.num_results < 1 || options.num_results > 8) {
320
+ throw new TypeError('rephrase: num_results must be an integer from 1 to 8.');
321
+ }
322
+ data.num_results = options.num_results;
323
+ }
324
+ if (options?.session_id !== undefined) {
325
+ if (typeof options.session_id !== 'string') {
326
+ throw new TypeError('rephrase: session_id must be a string.');
327
+ }
328
+ data.session_id = options.session_id;
329
+ }
330
+
331
+ return this.axiosPromise(url, 'post', data);
332
+ }
333
+
334
+ /**
335
+ * Sentiment analysis: positive / neutral / negative, overall and per sentence.
336
+ *
337
+ * Returns `sents` (the sentences found), `overall` (a list of
338
+ * `[probability, label]` pairs, most likely first) and `results` (one such
339
+ * list per sentence, aligned with `sents`). Labels are 'POSITIVE',
340
+ * 'NEUTRAL' or 'NEGATIVE'. A text with no sentences gets a 204 with no body
341
+ * (axios gives `data === ''`), and a text the API cannot split into
342
+ * sentences gets a 200 with `[]`.
343
+ *
344
+ * @param {string} text - The text to analyze, plain text or HTML (tags are
345
+ * stripped server-side; the API caps its length).
346
+ * @returns {Promise<AxiosResponse>}
347
+ */
348
+ sentiment(text) {
349
+ const url = `${this.urlEndpoint}sentiment`;
350
+
351
+ if (typeof text !== 'string' || text.trim().length === 0) {
352
+ throw new TypeError('sentiment: text must be a non-empty string.');
353
+ }
354
+
355
+ let data = {
356
+ text,
357
+ key: this.apiKey,
358
+ }
359
+ return this.axiosPromise(url, 'post', data);
360
+ }
361
+
362
+ /**
363
+ * Readability statistics: character / word / sentence counts, reading time
364
+ * and readability grades.
365
+ *
366
+ * Returns `chars`, `words`, `sentences`, `reading_time_sec`,
367
+ * `reading_time_min`, `readability` (a Flesch-Kincaid grade level, -3.4 when
368
+ * it cannot be scored) and `readability_scores` (a map of formula name to
369
+ * score; which formulas appear depends on `lang`, and it is empty for
370
+ * 'auto'). Unbilled.
371
+ *
372
+ * @param {string} text - The text to measure, plain text or HTML (tags are
373
+ * stripped server-side; the API caps it at 100,000 characters).
374
+ * @param {Object} [options]
375
+ * @param {string} [options.lang] - Language of the text: one of
376
+ * STATISTICS_LANGS ('en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'ru', 'ar')
377
+ * or 'auto'. Defaults to 'en'.
378
+ * @returns {Promise<AxiosResponse>}
379
+ */
380
+ statistics(text, options = {}) {
381
+ const url = `${this.urlEndpoint}statistics`;
382
+
383
+ // An empty text is a valid request (all-zero counts), so only the type is
384
+ // checked here.
385
+ if (typeof text !== 'string') {
386
+ throw new TypeError('statistics: text must be a string.');
387
+ }
388
+
389
+ let data = {
390
+ text,
391
+ key: this.apiKey,
392
+ }
393
+
394
+ if (options?.lang !== undefined) {
395
+ if (options.lang !== 'auto' && !STATISTICS_LANGS.includes(options.lang)) {
396
+ throw new TypeError(
397
+ `statistics: lang must be 'auto' or one of ${STATISTICS_LANGS.join(', ')}.`);
398
+ }
399
+ data.lang = options.lang;
400
+ }
401
+
402
+ return this.axiosPromise(url, 'post', data);
403
+ }
404
+
405
+ /**
406
+ * Similar terms: thesaurus synonyms plus semantically similar terms for a
407
+ * word or short phrase.
408
+ *
409
+ * Returns `neighbors` (up to 5 `[similarity, term]` pairs from embedding
410
+ * similarity, similarity 0–1, most similar first; only alphabetic terms)
411
+ * and `synonyms` (WordNet lemma names — multi-word ones use underscores,
412
+ * e.g. 'domestic_dog'). Unbilled.
413
+ *
414
+ * @param {string} query - The word or phrase to look up (the API uses only
415
+ * its first 100 characters).
416
+ * @returns {Promise<AxiosResponse>}
417
+ */
418
+ similarTerms(query) {
419
+ const url = `${this.urlEndpoint}thesaurus`;
420
+
421
+ // The API answers a blank query with a 400.
422
+ if (typeof query !== 'string' || query.trim().length === 0) {
423
+ throw new TypeError('similarTerms: query must be a non-empty string.');
424
+ }
425
+
426
+ let data = {
427
+ query,
428
+ key: this.apiKey,
429
+ }
430
+ return this.axiosPromise(url, 'post', data);
431
+ }
432
+
433
+ /**
434
+ * Profanity filter: label each whitespace-separated token of a text.
435
+ *
436
+ * Returns `toks` (the text split on whitespace; punctuation stays attached,
437
+ * e.g. 'stupid.') and `labels` (aligned with `toks`: 1 for profanity, 0
438
+ * otherwise). The text is not HTML-stripped. Unbilled.
439
+ *
440
+ * @param {string} text - The text to check.
441
+ * @returns {Promise<AxiosResponse>}
442
+ */
443
+ profanity(text) {
444
+ const url = `${this.urlEndpoint}profanity`;
445
+
446
+ if (typeof text !== 'string' || text.trim().length === 0) {
447
+ throw new TypeError('profanity: text must be a non-empty string.');
448
+ }
449
+
450
+ let data = {
451
+ text,
452
+ key: this.apiKey,
453
+ }
454
+ return this.axiosPromise(url, 'post', data);
455
+ }
456
+
457
+ /**
458
+ * List the custom dictionary of this API key's account: words the grammar
459
+ * and spelling checks accept as correct.
460
+ *
461
+ * Returns a bare array of `{id, entry, case_sensitive, created_at,
462
+ * updated_at}` entries, sorted by entry (timestamps are RFC 1123 strings).
463
+ *
464
+ * @returns {Promise<AxiosResponse>}
465
+ */
466
+ dictionaryEntries() {
467
+ const url = `${this.urlEndpoint}dictionary`;
468
+ // Not axiosPromise: this is a GET and the key rides the query string,
469
+ // like styleguideRulesets().
470
+ return axios({
471
+ url,
472
+ method: 'get',
473
+ params: { key: this.apiKey },
474
+ timeout: this.timeout,
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Add a word to this API key's account dictionary so the grammar and
480
+ * spelling checks stop flagging it (product names, jargon...).
481
+ *
482
+ * Returns the created `{id, entry, case_sensitive, created_at, updated_at}`
483
+ * entry. The API answers 400 for a duplicate entry ("Entry already
484
+ * exists.") and for an entry with more than two digits.
485
+ *
486
+ * @param {string} entry - The word to add (surrounding whitespace is
487
+ * stripped server-side).
488
+ * @param {Object} [options]
489
+ * @param {boolean} [options.case_sensitive] - Whether matching is case
490
+ * sensitive. Defaults to true.
491
+ * @returns {Promise<AxiosResponse>}
492
+ */
493
+ addDictionaryEntry(entry, options = {}) {
494
+ const url = `${this.urlEndpoint}dictionary`;
495
+
496
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
497
+ throw new TypeError('addDictionaryEntry: entry must be a non-empty string.');
498
+ }
499
+
500
+ let data = {
501
+ entry,
502
+ key: this.apiKey,
503
+ }
504
+
505
+ if (options?.case_sensitive !== undefined) {
506
+ if (typeof options.case_sensitive !== 'boolean') {
507
+ throw new TypeError('addDictionaryEntry: case_sensitive must be a boolean.');
508
+ }
509
+ data.case_sensitive = options.case_sensitive;
510
+ }
511
+
512
+ return this.axiosPromise(url, 'post', data);
513
+ }
514
+
515
+ /**
516
+ * Delete a custom dictionary entry by the `id` dictionaryEntries() or
517
+ * addDictionaryEntry() returned. Answers 204 with no body; an id that is
518
+ * malformed or not this account's gets a 400 (not a 404).
519
+ *
520
+ * @param {string} entryId - The entry's id (a UUID).
521
+ * @returns {Promise<AxiosResponse>}
522
+ */
523
+ deleteDictionaryEntry(entryId) {
524
+ if (typeof entryId !== 'string' || entryId.trim().length === 0) {
525
+ throw new TypeError('deleteDictionaryEntry: entryId must be a non-empty string.');
526
+ }
527
+ const url = `${this.urlEndpoint}dictionary/${encodeURIComponent(entryId)}`;
528
+ // The key rides the query string, like deleteStyleguideRuleset(): some
529
+ // HTTP stacks drop DELETE bodies.
530
+ return axios({
531
+ url,
532
+ method: 'delete',
533
+ params: { key: this.apiKey },
534
+ timeout: this.timeout,
535
+ });
536
+ }
537
+
183
538
  /**
184
539
  * Structured data extraction: pull caller-named fields out of unstructured text.
185
540
  *
@@ -195,8 +550,11 @@ export class Client {
195
550
  * from the text, so this is extraction, not inference: a value that isn't stated is
196
551
  * reported missing rather than guessed. Finding nothing is a normal 200.
197
552
  *
198
- * @param {string} text - The document to extract from, plain text or HTML (the API caps
199
- * it at 10,000 characters).
553
+ * @param {string|string[]} text - The document to extract from, plain text or HTML (the
554
+ * API caps it at 10,000 characters). Pass an array of 1–10 strings to extract from a
555
+ * batch in one request (sent as the API's `texts` parameter; combined length capped at
556
+ * 10,000 chars) — the response is then `{results: [...]}` with one
557
+ * single-response-shaped entry per input, in order.
200
558
  * @param {Array<string|{name: string, type?: string, description?: string, required?: boolean}>} fields -
201
559
  * The fields to pull out (1–20): field names, or objects. `type` is one of 'string'
202
560
  * (default), 'number', 'integer', 'boolean', 'date' (returned as YYYY-MM-DD) or 'list'.
@@ -215,7 +573,20 @@ export class Client {
215
573
  // context length) are server configuration and enforced there with a clear
216
574
  // 400 — mirroring them here would drift the day they change (and JS
217
575
  // `length` counts UTF-16 units, not the characters the API counts).
218
- if (typeof text !== 'string' || text.trim().length === 0) {
576
+ // An array of texts is the batch form (sent as `texts`, one result per
577
+ // item); the 1–10 item bound is structural like the field count below.
578
+ const isBatch = Array.isArray(text);
579
+ if (isBatch) {
580
+ // for...of (not text.every) so holes in a sparse array are visited as
581
+ // undefined and rejected, rather than skipped.
582
+ let ok = text.length >= 1 && text.length <= 10;
583
+ for (const item of text) {
584
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
585
+ }
586
+ if (!ok) {
587
+ throw new TypeError('extract: texts must be an array of 1 to 10 non-empty strings.');
588
+ }
589
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
219
590
  throw new TypeError('extract: text must be a non-empty string.');
220
591
  }
221
592
 
@@ -268,7 +639,7 @@ export class Client {
268
639
  }
269
640
 
270
641
  let data = {
271
- text,
642
+ ...(isBatch ? { texts: text } : { text }),
272
643
  fields,
273
644
  key: this.apiKey,
274
645
  }
@@ -286,11 +657,182 @@ export class Client {
286
657
  return this.axiosPromise(url, 'post', data);
287
658
  }
288
659
 
660
+ /**
661
+ * Translate text into a target language with an LLM.
662
+ *
663
+ * Line breaks, markup and placeholders are preserved in place, and the response
664
+ * reports the language the text was actually written in (detected — never assumed
665
+ * from the hint): `translation`, `source_lang`/`source_lang_name` (`'und'`/`null`
666
+ * when undetermined) and the normalized `target_lang`/`target_lang_name`.
667
+ *
668
+ * @param {string|string[]} text - The text to translate (the API caps it at
669
+ * 5,000 characters). Markup and whitespace are preserved, not stripped.
670
+ * Pass an array of 1-10 strings to translate a batch in one request (sent
671
+ * as the API's `texts` parameter; combined length up to 5,000 characters,
672
+ * same target/source/formality applied to every item). A batch response is
673
+ * `{results: [...]}` with one single-response-shaped entry per input, in
674
+ * order.
675
+ * @param {string} targetLang - Language to translate into: an ISO 639 code
676
+ * ('fr', 'zh-TW') or an English language name ('French'). Region variants
677
+ * zh-CN/zh-TW/zh-HK, pt-BR/pt-PT, en-GB/en-US, fr-CA and es-419 are supported
678
+ * as targets.
679
+ * @param {Object} [options]
680
+ * @param {string} [options.source_lang] - Optional hint for the source language,
681
+ * same forms as targetLang. The text itself wins if they disagree.
682
+ * @param {string} [options.formality] - Optional register: 'formal'
683
+ * (vous/Sie/usted) or 'informal' (tu/du/tú). Omit for the model default.
684
+ * @returns {Promise<AxiosResponse>}
685
+ */
686
+ translate(text, targetLang, options = {}) {
687
+ const url = `${this.urlEndpoint}translate`;
688
+
689
+ // Shape/type checks only; the language vocabulary and length caps are
690
+ // server configuration enforced there with a clear 400 (mirroring the
691
+ // 176-language table here would drift the day it changes).
692
+ // An array of texts is the batch form (SAP-398, sent as `texts`, one
693
+ // result per item); the 1–10 item bound is structural like safety's.
694
+ const isBatch = Array.isArray(text);
695
+ if (isBatch) {
696
+ // for...of (not text.every) so holes in a sparse array are visited as
697
+ // undefined and rejected, rather than skipped.
698
+ let ok = text.length >= 1 && text.length <= 10;
699
+ if (ok) {
700
+ for (const item of text) {
701
+ if (typeof item !== 'string' || item.trim().length === 0) {
702
+ ok = false;
703
+ break;
704
+ }
705
+ }
706
+ }
707
+ if (!ok) {
708
+ throw new TypeError('translate: texts must be an array of 1 to 10 non-empty strings.');
709
+ }
710
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
711
+ throw new TypeError('translate: text must be a non-empty string.');
712
+ }
713
+ if (typeof targetLang !== 'string' || targetLang.trim().length === 0) {
714
+ throw new TypeError('translate: targetLang must be a non-empty string (an ISO 639 '
715
+ + 'code like "fr" or an English language name like "French").');
716
+ }
717
+
718
+ let data = {
719
+ ...(isBatch ? { texts: text } : { text }),
720
+ target_lang: targetLang,
721
+ key: this.apiKey,
722
+ }
723
+
724
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
725
+ // bad type rather than silently treated as "omitted" (same as extract's
726
+ // context handling).
727
+ if (options?.source_lang !== undefined) {
728
+ if (typeof options.source_lang !== 'string' || options.source_lang.trim().length === 0) {
729
+ throw new TypeError('translate: source_lang must be a non-empty string.');
730
+ }
731
+ data.source_lang = options.source_lang;
732
+ }
733
+ if (options?.formality !== undefined) {
734
+ if (!TRANSLATE_FORMALITIES.includes(options.formality)) {
735
+ throw new TypeError('translate: formality must be one of '
736
+ + `${TRANSLATE_FORMALITIES.join(', ')}.`);
737
+ }
738
+ data.formality = options.formality;
739
+ }
740
+
741
+ return this.axiosPromise(url, 'post', data);
742
+ }
743
+
744
+ /**
745
+ * Summarize a document, webpage or email thread into a prose summary plus
746
+ * key points.
747
+ *
748
+ * Returns `summary` (the prose summary), `key_points` (an array of strings),
749
+ * `length` (the option the summary was generated with) and `result`
750
+ * (mirrors `summary` — the endpoint's original response key, kept for
751
+ * backward compatibility).
752
+ *
753
+ * @param {string|string[]} text - The document to summarize, plain text or
754
+ * HTML (tags are stripped; the API caps it at 20,000 characters). Pass an
755
+ * array of 1-10 strings to summarize a batch in one request (sent as the
756
+ * API's `texts` parameter; combined length up to 20,000 characters, same
757
+ * length option applied to every item). A batch response is
758
+ * `{results: [...]}` with one single-response-shaped entry per input, in
759
+ * order.
760
+ * @param {Object} [options]
761
+ * @param {string} [options.length] - Optional summary length: 'short',
762
+ * 'medium' (the default) or 'long'.
763
+ * @returns {Promise<AxiosResponse>}
764
+ */
765
+ summarize(text, options = {}) {
766
+ const url = `${this.urlEndpoint}summarize`;
767
+
768
+ // An array of texts is the batch form (SAP-399, sent as `texts`, one
769
+ // result per item); the 1–10 item bound is structural like translate's.
770
+ const isBatch = Array.isArray(text);
771
+ if (isBatch) {
772
+ // for...of (not text.every) so holes in a sparse array are visited as
773
+ // undefined and rejected, rather than skipped.
774
+ let ok = text.length >= 1 && text.length <= 10;
775
+ if (ok) {
776
+ for (const item of text) {
777
+ if (typeof item !== 'string' || item.trim().length === 0) {
778
+ ok = false;
779
+ break;
780
+ }
781
+ }
782
+ }
783
+ if (!ok) {
784
+ throw new TypeError('summarize: texts must be an array of 1 to 10 non-empty strings.');
785
+ }
786
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
787
+ throw new TypeError('summarize: text must be a non-empty string.');
788
+ }
789
+
790
+ let data = {
791
+ ...(isBatch ? { texts: text } : { text }),
792
+ key: this.apiKey,
793
+ }
794
+
795
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
796
+ // bad type rather than silently treated as "omitted" (same as translate's
797
+ // formality handling).
798
+ if (options?.length !== undefined) {
799
+ if (!SUMMARIZE_LENGTHS.includes(options.length)) {
800
+ throw new TypeError('summarize: length must be one of '
801
+ + `${SUMMARIZE_LENGTHS.join(', ')}.`);
802
+ }
803
+ data.length = options.length;
804
+ }
805
+
806
+ return this.axiosPromise(url, 'post', data);
807
+ }
808
+
289
809
  langdetect(text, options = {}) {
290
810
  const url = `${this.urlEndpoint}langdetect`;
291
811
 
812
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
813
+ // result per item); the 1–10 item bound is structural like classify's.
814
+ // The single (string) form is passed through unvalidated, as it always
815
+ // has been — the server answers a clear 400.
816
+ const isBatch = Array.isArray(text);
817
+ if (isBatch) {
818
+ // for...of (not text.every) so holes in a sparse array are visited as
819
+ // undefined and rejected, rather than skipped.
820
+ let ok = text.length >= 1 && text.length <= 10;
821
+ if (ok) {
822
+ for (const item of text) {
823
+ if (typeof item !== 'string' || item.trim().length === 0) {
824
+ ok = false;
825
+ break;
826
+ }
827
+ }
828
+ }
829
+ if (!ok) {
830
+ throw new TypeError('langdetect: texts must be an array of 1 to 10 non-empty strings.');
831
+ }
832
+ }
833
+
292
834
  let data = {
293
- text,
835
+ ...(isBatch ? { texts: text } : { text }),
294
836
  key: this.apiKey,
295
837
  }
296
838
 
@@ -314,7 +856,11 @@ export class Client {
314
856
  * empty), `scores` (one `{label, score}` per input label, descending), a one-sentence
315
857
  * `rationale` and the echoed `multi_label` mode.
316
858
  *
317
- * @param {string} text - Text to classify, plain text or HTML (the API caps it at 10,000 chars).
859
+ * @param {string|string[]} text - Text to classify, plain text or HTML (the API caps it
860
+ * at 10,000 chars). Pass an array of 1–10 strings to classify a batch in one request
861
+ * (sent as the API's `texts` parameter; combined length capped at 10,000 chars) — the
862
+ * response is then `{results: [...]}` with one single-response-shaped entry per input,
863
+ * in order.
318
864
  * @param {Array<string|{name: string, description?: string}>} labels - Candidate labels
319
865
  * (2–20). Names unique case-insensitively; the API caps names at 50 and descriptions at 200 chars.
320
866
  * @param {Object} [options]
@@ -333,7 +879,20 @@ export class Client {
333
879
  // context length) are server configuration and enforced there with a
334
880
  // clear 400 — mirroring them here would drift the day they change (and
335
881
  // JS `length` counts UTF-16 units, not the characters the API counts).
336
- if (typeof text !== 'string' || text.trim().length === 0) {
882
+ // An array of texts is the batch form (sent as `texts`, one result per
883
+ // item); the 1–10 item bound is structural like the label count below.
884
+ const isBatch = Array.isArray(text);
885
+ if (isBatch) {
886
+ // for...of (not text.every) so holes in a sparse array are visited as
887
+ // undefined and rejected, rather than skipped.
888
+ let ok = text.length >= 1 && text.length <= 10;
889
+ for (const item of text) {
890
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
891
+ }
892
+ if (!ok) {
893
+ throw new TypeError('classify: texts must be an array of 1 to 10 non-empty strings.');
894
+ }
895
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
337
896
  throw new TypeError('classify: text must be a non-empty string.');
338
897
  }
339
898
 
@@ -379,7 +938,7 @@ export class Client {
379
938
  }
380
939
 
381
940
  let data = {
382
- text,
941
+ ...(isBatch ? { texts: text } : { text }),
383
942
  labels,
384
943
  key: this.apiKey,
385
944
  }
@@ -477,12 +1036,30 @@ export class Client {
477
1036
  const url = `${this.urlEndpoint}quality`;
478
1037
 
479
1038
  // Shape/type checks only; size limits are server configuration (see seo()).
480
- if (typeof text !== 'string' || text.trim().length === 0) {
1039
+ // An array of texts is the batch form (SAP-400, sent as `texts`, one
1040
+ // result per item); the 1–10 item bound is structural like classify's.
1041
+ const isBatch = Array.isArray(text);
1042
+ if (isBatch) {
1043
+ // for...of (not text.every) so holes in a sparse array are visited as
1044
+ // undefined and rejected, rather than skipped.
1045
+ let ok = text.length >= 1 && text.length <= 10;
1046
+ if (ok) {
1047
+ for (const item of text) {
1048
+ if (typeof item !== 'string' || item.trim().length === 0) {
1049
+ ok = false;
1050
+ break;
1051
+ }
1052
+ }
1053
+ }
1054
+ if (!ok) {
1055
+ throw new TypeError('quality: texts must be an array of 1 to 10 non-empty strings.');
1056
+ }
1057
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
481
1058
  throw new TypeError('quality: text must be a non-empty string.');
482
1059
  }
483
1060
 
484
1061
  let data = {
485
- text,
1062
+ ...(isBatch ? { texts: text } : { text }),
486
1063
  key: this.apiKey,
487
1064
  }
488
1065
 
@@ -508,12 +1085,30 @@ export class Client {
508
1085
  const url = `${this.urlEndpoint}safety`;
509
1086
 
510
1087
  // Shape/type checks only; size limits are server configuration (see seo()).
511
- if (typeof text !== 'string' || text.trim().length === 0) {
1088
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
1089
+ // result per item); the 1–10 item bound is structural like classify's.
1090
+ const isBatch = Array.isArray(text);
1091
+ if (isBatch) {
1092
+ // for...of (not text.every) so holes in a sparse array are visited as
1093
+ // undefined and rejected, rather than skipped.
1094
+ let ok = text.length >= 1 && text.length <= 10;
1095
+ if (ok) {
1096
+ for (const item of text) {
1097
+ if (typeof item !== 'string' || item.trim().length === 0) {
1098
+ ok = false;
1099
+ break;
1100
+ }
1101
+ }
1102
+ }
1103
+ if (!ok) {
1104
+ throw new TypeError('safety: texts must be an array of 1 to 10 non-empty strings.');
1105
+ }
1106
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
512
1107
  throw new TypeError('safety: text must be a non-empty string.');
513
1108
  }
514
1109
 
515
1110
  let data = {
516
- text,
1111
+ ...(isBatch ? { texts: text } : { text }),
517
1112
  key: this.apiKey,
518
1113
  }
519
1114
 
@@ -526,6 +1121,12 @@ export class Client {
526
1121
  }
527
1122
  data.threshold = options.threshold;
528
1123
  }
1124
+ if (options?.spans !== undefined) {
1125
+ if (typeof options.spans !== 'boolean') {
1126
+ throw new TypeError('safety: spans must be a boolean.');
1127
+ }
1128
+ data.spans = options.spans;
1129
+ }
529
1130
 
530
1131
  return this.axiosPromise(url, 'post', data);
531
1132
  }
@@ -533,14 +1134,307 @@ export class Client {
533
1134
  aidetect(text, options = {}) {
534
1135
  const url = `${this.urlEndpoint}aidetect`;
535
1136
 
1137
+ // An array of texts is the batch form (SAP-403, sent as `texts`, one
1138
+ // result per item); the 1–10 item bound is structural like classify's.
1139
+ // The single form keeps its historical no-validation behavior (the
1140
+ // server 400s clearly on a bad `text`).
1141
+ const isBatch = Array.isArray(text);
1142
+ if (isBatch) {
1143
+ // for...of (not text.every) so holes in a sparse array are visited as
1144
+ // undefined and rejected, rather than skipped.
1145
+ let ok = text.length >= 1 && text.length <= 10;
1146
+ if (ok) {
1147
+ for (const item of text) {
1148
+ if (typeof item !== 'string' || item.trim().length === 0) {
1149
+ ok = false;
1150
+ break;
1151
+ }
1152
+ }
1153
+ }
1154
+ if (!ok) {
1155
+ throw new TypeError('aidetect: texts must be an array of 1 to 10 non-empty strings.');
1156
+ }
1157
+ }
1158
+
536
1159
  let data = {
537
- text,
1160
+ ...(isBatch ? { texts: text } : { text }),
538
1161
  key: this.apiKey,
539
1162
  }
540
1163
 
541
1164
  if (options.sent_scores != null) {
542
1165
  data.sent_scores = options.sent_scores;
543
1166
  }
1167
+ if (options.score_string !== undefined) {
1168
+ if (typeof options.score_string !== 'boolean') {
1169
+ throw new TypeError('aidetect: score_string must be a boolean.');
1170
+ }
1171
+ data.score_string = options.score_string;
1172
+ }
1173
+ if (options.version !== undefined) {
1174
+ if (typeof options.version !== 'string') {
1175
+ throw new TypeError('aidetect: version must be a string.');
1176
+ }
1177
+ data.version = options.version;
1178
+ }
1179
+
1180
+ return this.axiosPromise(url, 'post', data);
1181
+ }
1182
+
1183
+ /**
1184
+ * Check a text for compliance with style rules you supply (house style,
1185
+ * brand voice, editorial guidelines). Only the listed rules are enforced.
1186
+ *
1187
+ * Returns `violations` (one `{rule, text, start, end, note, suggestion}`
1188
+ * entry per violation, most important first — `start`/`end` index the
1189
+ * submitted text and are null when the passage cannot be located), `rules`
1190
+ * (the checked rule names) and `compliant` (true when no violations were
1191
+ * found). The text is checked exactly as submitted — markup is NOT
1192
+ * stripped, so offsets always index the string you sent.
1193
+ *
1194
+ * @param {string|string[]} text - The text to check (the API caps it at
1195
+ * 10,000 characters), or an array of 1–10 texts for the batch form (sent
1196
+ * as the API's `texts` parameter; combined length up to 10,000
1197
+ * characters, same rules applied to every item). A batch response is
1198
+ * `{results: [...]}` with one single-response-shaped entry per input, in
1199
+ * order, each item's violation offsets indexing that item's own text.
1200
+ * @param {Array<string|{name: string, description?: string}>|string} rules
1201
+ * - 1 to 20 style rules: a rule name (max 80 chars) or a
1202
+ * `{name, description}` object whose optional description (max 400
1203
+ * chars) carries the nuance the model follows. Names must be unique
1204
+ * case-insensitively. OR a string: the name of a saved ruleset (see
1205
+ * saveStyleguideRuleset()) to check against, matched case-insensitively
1206
+ * within the API key's account.
1207
+ * @returns {Promise<AxiosResponse>}
1208
+ */
1209
+ styleguide(text, rules) {
1210
+ const url = `${this.urlEndpoint}styleguide`;
1211
+
1212
+ // Shape/type checks only. Size limits (text/rule/description length) are
1213
+ // server configuration and enforced there with a clear 400 (see
1214
+ // classify()).
1215
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
1216
+ // result per item); the 1–10 item bound is structural like classify's.
1217
+ const isBatch = Array.isArray(text);
1218
+ if (isBatch) {
1219
+ // for...of (not text.every) so holes in a sparse array are visited as
1220
+ // undefined and rejected, rather than skipped.
1221
+ let ok = text.length >= 1 && text.length <= 10;
1222
+ if (ok) {
1223
+ for (const item of text) {
1224
+ if (typeof item !== 'string' || item.trim().length === 0) {
1225
+ ok = false;
1226
+ break;
1227
+ }
1228
+ }
1229
+ }
1230
+ if (!ok) {
1231
+ throw new TypeError('styleguide: texts must be an array of 1 to 10 non-empty strings.');
1232
+ }
1233
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1234
+ throw new TypeError('styleguide: text must be a non-empty string.');
1235
+ }
1236
+
1237
+ // A string names a saved ruleset (see saveStyleguideRuleset()) and is
1238
+ // sent as the API's `ruleset` parameter in place of inline `rules`.
1239
+ const isSavedRuleset = typeof rules === 'string';
1240
+ if (isSavedRuleset) {
1241
+ if (rules.trim().length === 0) {
1242
+ throw new TypeError('styleguide: ruleset name must be a non-empty string.');
1243
+ }
1244
+ } else {
1245
+ validateStyleguideRules(rules, 'styleguide');
1246
+ }
1247
+
1248
+ let data = {
1249
+ ...(isBatch ? { texts: text } : { text }),
1250
+ ...(isSavedRuleset ? { ruleset: rules } : { rules }),
1251
+ key: this.apiKey,
1252
+ }
1253
+
1254
+ return this.axiosPromise(url, 'post', data);
1255
+ }
1256
+
1257
+ /**
1258
+ * List the saved style-guide rulesets of this API key's account (its team,
1259
+ * for team-scoped keys — a team's rulesets are shared by every caller of
1260
+ * its keys). Requires a per-account API key.
1261
+ *
1262
+ * Returns `rulesets`: `{name, rules, created_at, updated_at}` entries whose
1263
+ * `rules` are the normalized `{name, description}` objects the check
1264
+ * endpoint applies.
1265
+ *
1266
+ * @returns {Promise<AxiosResponse>}
1267
+ */
1268
+ styleguideRulesets() {
1269
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
1270
+ // Not axiosPromise: this is a GET and the key rides the query string.
1271
+ return axios({
1272
+ url,
1273
+ method: 'get',
1274
+ params: { key: this.apiKey },
1275
+ timeout: this.timeout,
1276
+ });
1277
+ }
1278
+
1279
+ /**
1280
+ * Create — or replace, when the name already exists (matched
1281
+ * case-insensitively) — a saved style-guide ruleset, so later
1282
+ * `styleguide(text, name)` calls check against it by name. Owned by the
1283
+ * API key's account (its team, for team-scoped keys). Requires a
1284
+ * per-account API key.
1285
+ *
1286
+ * Returns `ruleset` (the saved `{name, rules, created_at, updated_at}`)
1287
+ * and `created` (false when an existing ruleset was replaced).
1288
+ *
1289
+ * @param {string} name - The ruleset name (the API caps it at 100
1290
+ * characters; surrounding/duplicate whitespace is normalized).
1291
+ * @param {Array<string|{name: string, description?: string}>} rules - The
1292
+ * rules to save; same shape and bounds as styleguide()'s inline rules.
1293
+ * @returns {Promise<AxiosResponse>}
1294
+ */
1295
+ saveStyleguideRuleset(name, rules) {
1296
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
1297
+ if (typeof name !== 'string' || name.trim().length === 0) {
1298
+ throw new TypeError('saveStyleguideRuleset: name must be a non-empty string.');
1299
+ }
1300
+ validateStyleguideRules(rules, 'saveStyleguideRuleset');
1301
+
1302
+ let data = {
1303
+ name,
1304
+ rules,
1305
+ key: this.apiKey,
1306
+ }
1307
+
1308
+ return this.axiosPromise(url, 'post', data);
1309
+ }
1310
+
1311
+ /**
1312
+ * Delete a saved style-guide ruleset by name (matched case-insensitively;
1313
+ * 404 when no such ruleset exists). Requires a per-account API key.
1314
+ *
1315
+ * @param {string} name - The ruleset name to delete.
1316
+ * @returns {Promise<AxiosResponse>}
1317
+ */
1318
+ deleteStyleguideRuleset(name) {
1319
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
1320
+ if (typeof name !== 'string' || name.trim().length === 0) {
1321
+ throw new TypeError('deleteStyleguideRuleset: name must be a non-empty string.');
1322
+ }
1323
+ // The name rides the query string: some HTTP stacks drop DELETE bodies,
1324
+ // and the API accepts the query form for exactly that reason.
1325
+ return axios({
1326
+ url,
1327
+ method: 'delete',
1328
+ params: { key: this.apiKey, name },
1329
+ timeout: this.timeout,
1330
+ });
1331
+ }
1332
+
1333
+ /**
1334
+ * Rewrite a text in plain language at a target reading level, keeping all
1335
+ * of its content, structure and language — nothing is summarized or
1336
+ * dropped.
1337
+ *
1338
+ * Returns `simplified` (the rewrite, structure preserved), the applied
1339
+ * `reading_level`, the `lang` the readability scores were computed for
1340
+ * (detected when not supplied) and, when that language supports scoring, a
1341
+ * `readability` block: `{before: {grade, ease}, after: {grade, ease}}`
1342
+ * Flesch-Kincaid scores, so the improvement is verifiable.
1343
+ *
1344
+ * @param {string|string[]} text - The text to simplify (the API caps it at
1345
+ * 5,000 characters), or an array of 1–10 texts for the batch form (sent
1346
+ * as the API's `texts` parameter; combined length up to 5,000
1347
+ * characters, same options applied to every item). A batch response is
1348
+ * `{results: [...]}` with one single-response-shaped entry per input, in
1349
+ * order; with `lang` omitted, each item's language (and readability
1350
+ * scores) is detected on its own. Sent to the model as submitted —
1351
+ * markup and line structure are preserved, not stripped.
1352
+ * @param {Object} [options]
1353
+ * @param {string} [options.reading_level] - Target audience: 'plain'
1354
+ * (plain-language style for a general audience, the API default),
1355
+ * 'elementary' (~US grade 3-5), 'middle_school' (~grade 6-8) or
1356
+ * 'high_school' (~grade 9-10).
1357
+ * @param {string[]} [options.preserve_terms] - Up to 20 terms (each up to
1358
+ * 100 characters) the rewrite must keep exactly as written — product
1359
+ * names, defined legal terms. A rewrite that drops one is refused
1360
+ * server-side rather than returned.
1361
+ * @param {string} [options.lang] - ISO 639-1 code used to pick the
1362
+ * readability formula for the before/after scores. Omit for
1363
+ * auto-detection (falls back to 'en'). The rewrite itself always stays
1364
+ * in the text's own language.
1365
+ * @returns {Promise<AxiosResponse>}
1366
+ */
1367
+ simplify(text, options = {}) {
1368
+ const url = `${this.urlEndpoint}simplify`;
1369
+
1370
+ // Shape/type checks only. Size limits (text/term length) are server
1371
+ // configuration and enforced there with a clear 400 (see classify()).
1372
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
1373
+ // result per item); the 1–10 item bound is structural like classify's.
1374
+ const isBatch = Array.isArray(text);
1375
+ if (isBatch) {
1376
+ // for...of (not text.every) so holes in a sparse array are visited as
1377
+ // undefined and rejected, rather than skipped.
1378
+ let ok = text.length >= 1 && text.length <= 10;
1379
+ if (ok) {
1380
+ for (const item of text) {
1381
+ if (typeof item !== 'string' || item.trim().length === 0) {
1382
+ ok = false;
1383
+ break;
1384
+ }
1385
+ }
1386
+ }
1387
+ if (!ok) {
1388
+ throw new TypeError('simplify: texts must be an array of 1 to 10 non-empty strings.');
1389
+ }
1390
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1391
+ throw new TypeError('simplify: text must be a non-empty string.');
1392
+ }
1393
+
1394
+ let data = {
1395
+ ...(isBatch ? { texts: text } : { text }),
1396
+ key: this.apiKey,
1397
+ }
1398
+
1399
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
1400
+ // bad type rather than silently treated as "omitted" (same as translate's
1401
+ // option handling).
1402
+ if (options?.reading_level !== undefined) {
1403
+ if (!SIMPLIFY_READING_LEVELS.includes(options.reading_level)) {
1404
+ throw new TypeError('simplify: reading_level must be one of '
1405
+ + `${SIMPLIFY_READING_LEVELS.join(', ')}.`);
1406
+ }
1407
+ data.reading_level = options.reading_level;
1408
+ }
1409
+ if (options?.preserve_terms !== undefined) {
1410
+ const terms = options.preserve_terms;
1411
+ // An empty list is a valid no-op (callers may build it dynamically); it
1412
+ // is simply omitted from the payload below rather than rejected.
1413
+ // for...of (not terms.every) so holes in a sparse array are visited as
1414
+ // undefined and rejected, rather than skipped.
1415
+ let ok = Array.isArray(terms) && terms.length <= 20;
1416
+ if (ok) {
1417
+ for (const term of terms) {
1418
+ if (typeof term !== 'string' || term.trim().length === 0) {
1419
+ ok = false;
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1424
+ if (!ok) {
1425
+ throw new TypeError('simplify: preserve_terms must be an array of up to 20 '
1426
+ + 'non-empty strings.');
1427
+ }
1428
+ if (terms.length > 0) {
1429
+ data.preserve_terms = terms;
1430
+ }
1431
+ }
1432
+ if (options?.lang !== undefined) {
1433
+ if (typeof options.lang !== 'string' || options.lang.trim().length === 0) {
1434
+ throw new TypeError('simplify: lang must be a non-empty string.');
1435
+ }
1436
+ data.lang = options.lang;
1437
+ }
544
1438
 
545
1439
  return this.axiosPromise(url, 'post', data);
546
1440
  }
@@ -577,4 +1471,106 @@ export class Client {
577
1471
 
578
1472
  return this.axiosPromise(url, 'post', data);
579
1473
  }
1474
+
1475
+ /**
1476
+ * Named-entity recognition: people, organizations, locations, dates, times,
1477
+ * money amounts, percentages, quantities, products, and events, each with
1478
+ * character offsets into the exact text submitted (nothing is stripped or
1479
+ * normalized, so spans apply directly to your copy of the document).
1480
+ * Complements pii(): that finds checksum-validated identifiers, this finds
1481
+ * names and real-world references.
1482
+ *
1483
+ * @param {string|string[]} text - The text to analyze, or an array of 1–10
1484
+ * texts for the batch form (sent as the API's `texts` parameter; combined
1485
+ * length up to 10,000 characters, same types applied to every item). A
1486
+ * batch response is `{results: [...]}` with one single-response-shaped
1487
+ * entry per input, in order, each item's spans indexing that item's own
1488
+ * text.
1489
+ * @param {Object} [options]
1490
+ * @param {string[]} [options.types] - Entity types to recognize (subset of
1491
+ * the API's supported types). Default: all of them.
1492
+ */
1493
+ ner(text, options = {}) {
1494
+ const url = `${this.urlEndpoint}ner`;
1495
+
1496
+ // Shape/type checks only; the set of valid entity types is server
1497
+ // configuration and enforced there with a clear 400 (see pii()).
1498
+ // An array of texts is the batch form (SAP-395 series, sent as `texts`,
1499
+ // one result per item); the 1–10 item bound is structural like classify's.
1500
+ const isBatch = Array.isArray(text);
1501
+ if (isBatch) {
1502
+ // for...of (not text.every) so holes in a sparse array are visited as
1503
+ // undefined and rejected, rather than skipped.
1504
+ let ok = text.length >= 1 && text.length <= 10;
1505
+ if (ok) {
1506
+ for (const item of text) {
1507
+ if (typeof item !== 'string' || item.trim().length === 0) {
1508
+ ok = false;
1509
+ break;
1510
+ }
1511
+ }
1512
+ }
1513
+ if (!ok) {
1514
+ throw new TypeError('ner: texts must be an array of 1 to 10 non-empty strings.');
1515
+ }
1516
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1517
+ throw new TypeError('ner: text must be a non-empty string.');
1518
+ }
1519
+
1520
+ let data = {
1521
+ ...(isBatch ? { texts: text } : { text }),
1522
+ key: this.apiKey,
1523
+ }
1524
+
1525
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
1526
+ // type rather than silently treated as "omitted", matching pii().
1527
+ // Reject an empty list too: the MCP schema requires at least one type
1528
+ // (min(1)), so an empty array is typed-valid input that would otherwise
1529
+ // become an avoidable API error.
1530
+ if (options?.types !== undefined) {
1531
+ if (!Array.isArray(options.types) || options.types.length === 0
1532
+ || !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
1533
+ throw new TypeError('ner: types must be a non-empty array of non-empty strings.');
1534
+ }
1535
+ data.types = options.types;
1536
+ }
1537
+
1538
+ return this.axiosPromise(url, 'post', data);
1539
+ }
1540
+
1541
+ /**
1542
+ * Check whether an account-wide block (expired key/subscription,
1543
+ * depleted credits, billing limit, trial daily allowance, monthly quota)
1544
+ * stops this API key from making billable calls, and if so why and for
1545
+ * how long. Works while the key is blocked, which is when it is most
1546
+ * useful: call it after an unexpected 402 or monthly-quota 429 instead of
1547
+ * parsing the error text. It does not reflect a single endpoint's
1548
+ * short-term rate limit (each endpoint has its own bucket), so a
1549
+ * "Capacity used." 429 elsewhere can coexist with api_access: true — use
1550
+ * that 429's retry_after / X-RateLimit-* headers instead. Unbilled, but each call draws a fixed 2,000
1551
+ * characters from its own rate-limit bucket, so don't poll it per request.
1552
+ * Requires a per-account API key; the public demo key and MCP OAuth
1553
+ * access tokens get a 403.
1554
+ *
1555
+ * Always returns `api_access`, `blocked_reason` (null, or one of
1556
+ * key_expired, subscription_expired, credits_depleted,
1557
+ * monthly_billing_limit, trial_daily_limit, monthly_quota_exhausted),
1558
+ * `message` and `usage` ({monthly_quota_chars}); `retry_after` (seconds)
1559
+ * only when blocked. Keys that are not team-scoped also get
1560
+ * `usage.month_start`/`usage.chars_this_month`, `subscription`, `credits`
1561
+ * and, for free-trial accounts, `trial`.
1562
+ *
1563
+ * @returns {Promise<AxiosResponse>}
1564
+ */
1565
+ accountStatus() {
1566
+ const url = `${this.urlEndpoint}account/status`;
1567
+ // Not axiosPromise: this is a GET and the key rides the query string,
1568
+ // like styleguideRulesets().
1569
+ return axios({
1570
+ url,
1571
+ method: 'get',
1572
+ params: { key: this.apiKey },
1573
+ timeout: this.timeout,
1574
+ });
1575
+ }
580
1576
  }