@saplingai/sapling-js 1.0.45 → 1.0.52

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
@@ -76,6 +76,14 @@ You should see something like the following output:
76
76
  }
77
77
  ```
78
78
 
79
+ If you only need to apply the corrections, pass `includeErrorCategories: false`. The API then
80
+ skips error classification (faster and lighter), and each edit comes back without
81
+ `error_type`, `general_error_type` or classifier-generated descriptions:
82
+
83
+ ```javascript
84
+ client.edits('I have a apple!', undefined, { includeErrorCategories: false, autoApply: true })
85
+ ```
86
+
79
87
  ### Text classification
80
88
 
81
89
  Zero-shot classification with your own labels (plain names or `{ name, description }` objects, 2–20):
@@ -94,6 +102,30 @@ client
94
102
  })
95
103
  ```
96
104
 
105
+ ### Style-guide rulesets
106
+
107
+ Check text against style rules inline, or save a named ruleset once (owned by
108
+ your API key's account — its team, for team-scoped keys) and check against it
109
+ by name:
110
+
111
+ ```javascript
112
+ // Define the house style once…
113
+ await client.saveStyleguideRuleset('House Style', [
114
+ 'no exclamation marks',
115
+ { name: 'no jargon', description: 'No corporate buzzwords or filler.' },
116
+ ]);
117
+
118
+ // …then check against it by name (names match case-insensitively).
119
+ const response = await client.styleguide('Our synergy-driven solution!!', 'House Style');
120
+ console.log(response.data);
121
+ // { violations: [{ rule: 'no jargon', text: 'synergy-driven solution', ... }],
122
+ // rules: ['no exclamation marks', 'no jargon'], compliant: false }
123
+
124
+ // List and delete:
125
+ await client.styleguideRulesets(); // { rulesets: [{ name, rules, ... }] }
126
+ await client.deleteStyleguideRuleset('House Style');
127
+ ```
128
+
97
129
  ## Frontend Quickstart
98
130
 
99
131
  The SDK provides a way to automatically apply edit suggestions to any HTML contenteditable and textarea elements.
@@ -159,6 +191,28 @@ as `YYYY-MM-DD`, or `'list'`), and every value must be grounded in a quote from
159
191
  extraction, not inference, so a value that isn't stated is reported in `missing` rather than
160
192
  guessed.
161
193
 
194
+ ## Translation
195
+
196
+ Translate text into a target language, named by ISO 639 code (`'fr'`, `'zh-TW'`) or English
197
+ name (`'French'`). Line breaks and markup are preserved, and the detected source language is
198
+ reported alongside the translation:
199
+
200
+ ```javascript
201
+ client
202
+ .translate('Hello world.\nSee you at 5pm.', 'fr', { formality: 'formal' })
203
+ .then(function (response) {
204
+ console.log(response.data);
205
+ // { translation: 'Bonjour le monde.\nÀ 17h.',
206
+ // source_lang: 'en', source_lang_name: 'English',
207
+ // target_lang: 'fr', target_lang_name: 'French' }
208
+ })
209
+ ```
210
+
211
+ Region variants `zh-CN`/`zh-TW`/`zh-HK`, `pt-BR`/`pt-PT`, `en-GB`/`en-US`, `fr-CA` and
212
+ `es-419` are supported as targets; the optional `source_lang` hint never overrides what the
213
+ text actually is, and `formality` (`'formal'`/`'informal'`) picks the register where the
214
+ target language distinguishes them.
215
+
162
216
  ## Further Documentation
163
217
 
164
218
  - Full Sapling HTTP API and SDK [documentation](https://sapling.ai/docs).
@@ -179,3 +233,34 @@ try {
179
233
  console.error(error.response?.status, error.response?.data ?? error.message);
180
234
  }
181
235
  ```
236
+
237
+ API error bodies are JSON objects with a human-readable `msg`, for example
238
+ `{ msg: 'Invalid API key.' }`. The one exception is the 4MB `413`, which uses
239
+ `message`/`result` keys. When a `402` or `429` body has a numeric `retry_after`,
240
+ the request can succeed after waiting that many seconds. Without it, retrying
241
+ unchanged won't help. See
242
+ [Rate Limits & Errors](https://sapling.ai/docs/rate_limits/).
243
+
244
+ To find out why a key is blocked, and for how long, call `accountStatus()`. It
245
+ is unbilled and works while the key is blocked. It reports account-wide blocks:
246
+ an expired key or subscription, depleted credits, a billing limit, the trial
247
+ daily allowance or the monthly quota. It does not report a single endpoint's
248
+ short-term rate limit, because each endpoint has its own allowance. For a
249
+ `Capacity used.` 429, use that response's `retry_after` instead:
250
+
251
+ ```javascript
252
+ const { data } = await client.accountStatus();
253
+ if (!data.api_access) {
254
+ // blocked_reason: key_expired | subscription_expired | credits_depleted |
255
+ // monthly_billing_limit | trial_daily_limit | monthly_quota_exhausted
256
+ const retry = data.retry_after ? `, retry in ${data.retry_after}s` : '';
257
+ console.log(`${data.blocked_reason}: ${data.message}${retry}`);
258
+ }
259
+ ```
260
+
261
+ Every response includes `api_access`, `blocked_reason`, `message` and
262
+ `usage.monthly_quota_chars`. Keys that aren't team-scoped also get the
263
+ account's `subscription`, prepaid `credits`, `usage.chars_this_month` and, on a
264
+ free trial, `trial`. See [Account Status](https://sapling.ai/docs/api/account-status/).
265
+ Each call counts as 2,000 characters against the endpoint's own rate-limit
266
+ bucket, so call it when something fails rather than before every request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saplingai/sapling-js",
3
- "version": "1.0.45",
3
+ "version": "1.0.52",
4
4
  "description": "JavaScript library for using the Sapling.ai API.",
5
5
  "type": "module",
6
6
  "main": "./sapling/client.js",
@@ -32,12 +32,137 @@ export type EditOptions = {
32
32
  lang?: string,
33
33
  medical?: boolean,
34
34
  advancedEdits?: AdvancedEditOptions,
35
+ /** Defaults to true. false skips error classification: edits omit error_type,
36
+ * general_error_type and classifier-generated descriptions. */
37
+ includeErrorCategories?: boolean,
35
38
  }
36
39
 
37
40
  export type AIDetectOptions = {
38
41
  sent_scores?: boolean,
39
42
  }
40
43
 
44
+ export type StyleguideRule = string | { name: string; description?: string };
45
+
46
+ export type StyleguideViolation = {
47
+ rule: string;
48
+ text: string;
49
+ start: number | null;
50
+ end: number | null;
51
+ note: string;
52
+ suggestion: string;
53
+ }
54
+
55
+ export type StyleguideResponse = {
56
+ violations: StyleguideViolation[];
57
+ rules: string[];
58
+ compliant: boolean;
59
+ }
60
+
61
+ export type StyleguideBatchResponse = {
62
+ results: StyleguideResponse[];
63
+ }
64
+
65
+ export type StyleguideRuleset = {
66
+ name: string;
67
+ rules: { name: string; description: string }[];
68
+ created_at: string | null;
69
+ updated_at: string | null;
70
+ }
71
+
72
+ export type StyleguideRulesetsResponse = {
73
+ rulesets: StyleguideRuleset[];
74
+ }
75
+
76
+ export type SaveStyleguideRulesetResponse = {
77
+ ruleset: StyleguideRuleset;
78
+ created: boolean;
79
+ }
80
+
81
+ export type DeleteStyleguideRulesetResponse = {
82
+ deleted: string;
83
+ }
84
+
85
+ export type AccountBlockedReason =
86
+ | 'key_expired'
87
+ | 'subscription_expired'
88
+ | 'credits_depleted'
89
+ | 'monthly_billing_limit'
90
+ | 'trial_daily_limit'
91
+ | 'monthly_quota_exhausted';
92
+
93
+ export type AccountCreditSummary = {
94
+ enabled: boolean;
95
+ currency: string;
96
+ alert_threshold_percent: number;
97
+ // Present only when `enabled` is true.
98
+ balance_credits?: number;
99
+ purchased_credits?: number;
100
+ consumed_credits?: number;
101
+ consumed_percent?: number;
102
+ low_balance?: boolean;
103
+ depleted?: boolean;
104
+ billing_mode?: string;
105
+ billing_mode_effective_at?: string | null;
106
+ metered_fallback_available?: boolean;
107
+ stops_when_depleted?: boolean;
108
+ billing_started_at?: string;
109
+ auto_topup?: { enabled: boolean; [field: string]: unknown };
110
+ [field: string]: unknown;
111
+ }
112
+
113
+ export type AccountStatusResponse = {
114
+ api_access: boolean;
115
+ blocked_reason: AccountBlockedReason | null;
116
+ message: string | null;
117
+ // Seconds until the block may clear; present only when blocked.
118
+ retry_after?: number;
119
+ usage: {
120
+ monthly_quota_chars: number | null;
121
+ // Omitted for team-scoped keys, like `subscription`, `credits` and `trial`.
122
+ month_start?: string;
123
+ chars_this_month?: number;
124
+ };
125
+ subscription?: {
126
+ active: boolean;
127
+ trial_only: boolean;
128
+ key_expired: boolean;
129
+ };
130
+ credits?: AccountCreditSummary;
131
+ // Present only for free-trial accounts.
132
+ trial?: {
133
+ daily_limit_chars: number | null;
134
+ chars_used_today: number;
135
+ resets_at: 'utc_midnight';
136
+ };
137
+ }
138
+
139
+ export type SimplifyReadingLevel = 'plain' | 'elementary' | 'middle_school' | 'high_school';
140
+
141
+ export type SimplifyOptions = {
142
+ reading_level?: SimplifyReadingLevel,
143
+ preserve_terms?: string[],
144
+ lang?: string,
145
+ }
146
+
147
+ export type SimplifyReadabilityScores = {
148
+ grade: number;
149
+ ease: number;
150
+ }
151
+
152
+ export type SimplifyResponse = {
153
+ simplified: string;
154
+ reading_level: SimplifyReadingLevel;
155
+ lang: string;
156
+ readability?: {
157
+ before: SimplifyReadabilityScores;
158
+ after: SimplifyReadabilityScores;
159
+ };
160
+ }
161
+
162
+ export type SimplifyBatchResponse = {
163
+ results: SimplifyResponse[];
164
+ }
165
+
41
166
  export type LangdetectOptions = {
42
167
  top_k?: number,
43
168
  segments?: boolean,
@@ -59,6 +184,10 @@ export type ClassifyResponse = {
59
184
  multi_label: boolean;
60
185
  }
61
186
 
187
+ export type ClassifyBatchResponse = {
188
+ results: ClassifyResponse[];
189
+ }
190
+
62
191
  export type SeoOptions = {
63
192
  keywords?: string[],
64
193
  suggestions?: boolean,
@@ -137,9 +266,14 @@ export type QualityRubric = {
137
266
  export type QualityResponse = {
138
267
  score: number;
139
268
  sentences?: QualitySentenceScore[];
269
+ truncated?: boolean;
140
270
  rubric?: QualityRubric;
141
271
  }
142
272
 
273
+ export type QualityBatchResponse = {
274
+ results: QualityResponse[];
275
+ }
276
+
143
277
  export type PIIType = 'email' | 'phone' | 'ssn' | 'credit_card' | 'ip_address' | 'iban' | 'us_bank_routing';
144
278
 
145
279
  export type PIIOptions = {
@@ -162,19 +296,55 @@ export type PIIResponse = {
162
296
  redacted_text?: string;
163
297
  }
164
298
 
299
+ export type NERType = 'person' | 'organization' | 'location' | 'date' | 'time' |
300
+ 'money' | 'percent' | 'quantity' | 'product' | 'event';
301
+
302
+ export type NEROptions = {
303
+ types?: NERType[],
304
+ }
305
+
306
+ export type NEREntity = {
307
+ text: string;
308
+ type: NERType;
309
+ start: number;
310
+ end: number;
311
+ }
312
+
313
+ export type NERResponse = {
314
+ entities: NEREntity[];
315
+ types: NERType[];
316
+ }
317
+
318
+ export type NERBatchResponse = {
319
+ results: NERResponse[];
320
+ }
321
+
322
+ export type SafetyCategory = 'toxicity' | 'profanity' | 'harassment' | 'hate_speech' |
323
+ 'self_harm' | 'sexual' | 'violence';
324
+
325
+ export type SafetySpan = {
326
+ text: string;
327
+ start: number | null;
328
+ end: number | null;
329
+ scores: Partial<Record<SafetyCategory, number>>;
330
+ flagged_categories: SafetyCategory[];
331
+ }
332
+
165
333
  export type SafetyResponse = {
166
- toxicity: number;
167
- profanity: number;
168
- harassment: number;
169
- hate_speech: number;
170
- self_harm: number;
171
- sexual: number;
172
- violence: number;
173
- flagged: string[];
334
+ scores: Record<SafetyCategory, number>;
335
+ flagged: boolean;
336
+ flagged_categories: SafetyCategory[];
337
+ threshold: number;
338
+ spans?: SafetySpan[];
339
+ }
340
+
341
+ export type SafetyBatchResponse = {
342
+ results: SafetyResponse[];
174
343
  }
175
344
 
176
345
  export type SafetyOptions = {
177
346
  threshold?: number,
347
+ spans?: boolean,
178
348
  }
179
349
 
180
350
  export type SpellcheckOptions = {
@@ -215,6 +385,43 @@ export type ExtractResponse = {
215
385
  missing: string[],
216
386
  }
217
387
 
388
+ export type ExtractBatchResponse = {
389
+ results: ExtractResponse[];
390
+ }
391
+
392
+ export type TranslateOptions = {
393
+ source_lang?: string,
394
+ formality?: 'formal' | 'informal',
395
+ }
396
+
397
+ export type TranslateResponse = {
398
+ translation: string,
399
+ source_lang: string,
400
+ source_lang_name: string | null,
401
+ target_lang: string,
402
+ target_lang_name: string,
403
+ }
404
+
405
+ export type TranslateBatchResponse = {
406
+ results: TranslateResponse[];
407
+ }
408
+
409
+ export type SummarizeOptions = {
410
+ length?: 'short' | 'medium' | 'long',
411
+ }
412
+
413
+ export type SummarizeResponse = {
414
+ /** Mirrors `summary` — the endpoint's original response key. */
415
+ result: string,
416
+ summary: string,
417
+ key_points: string[],
418
+ length: 'short' | 'medium' | 'long',
419
+ }
420
+
421
+ export type SummarizeBatchResponse = {
422
+ results: SummarizeResponse[];
423
+ }
424
+
218
425
  export type UserDetails = {
219
426
  userId?: string,
220
427
  anonUser?: boolean,
@@ -233,12 +440,39 @@ export declare class Client {
233
440
  acceptComplete(completeUUID: string, query: string, completion: string, sessionId?: string): Promise<AxiosResponse>;
234
441
  tone(text: string): Promise<AxiosResponse>;
235
442
  extract(text: string, fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractResponse>>;
236
-
237
- aidetect(text: string, options?: AIDetectOptions): Promise<AxiosResponse>;
238
- langdetect(text: string, options?: LangdetectOptions): Promise<AxiosResponse>;
443
+ extract(texts: string[], fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractBatchResponse>>;
444
+ extract(text: string | string[], fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractResponse | ExtractBatchResponse>>;
445
+ translate(text: string, targetLang: string, options?: TranslateOptions): Promise<AxiosResponse<TranslateResponse>>;
446
+ translate(texts: string[], targetLang: string, options?: TranslateOptions): Promise<AxiosResponse<TranslateBatchResponse>>;
447
+ translate(text: string | string[], targetLang: string, options?: TranslateOptions): Promise<AxiosResponse<TranslateResponse | TranslateBatchResponse>>;
448
+ summarize(text: string, options?: SummarizeOptions): Promise<AxiosResponse<SummarizeResponse>>;
449
+ summarize(texts: string[], options?: SummarizeOptions): Promise<AxiosResponse<SummarizeBatchResponse>>;
450
+ summarize(text: string | string[], options?: SummarizeOptions): Promise<AxiosResponse<SummarizeResponse | SummarizeBatchResponse>>;
451
+
452
+ aidetect(text: string | string[], options?: AIDetectOptions): Promise<AxiosResponse>;
453
+ styleguide(text: string, rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideResponse>>;
454
+ styleguide(texts: string[], rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideBatchResponse>>;
455
+ styleguide(text: string | string[], rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideResponse | StyleguideBatchResponse>>;
456
+ styleguideRulesets(): Promise<AxiosResponse<StyleguideRulesetsResponse>>;
457
+ saveStyleguideRuleset(name: string, rules: StyleguideRule[]): Promise<AxiosResponse<SaveStyleguideRulesetResponse>>;
458
+ deleteStyleguideRuleset(name: string): Promise<AxiosResponse<DeleteStyleguideRulesetResponse>>;
459
+ simplify(text: string, options?: SimplifyOptions): Promise<AxiosResponse<SimplifyResponse>>;
460
+ simplify(texts: string[], options?: SimplifyOptions): Promise<AxiosResponse<SimplifyBatchResponse>>;
461
+ simplify(text: string | string[], options?: SimplifyOptions): Promise<AxiosResponse<SimplifyResponse | SimplifyBatchResponse>>;
462
+ langdetect(text: string | string[], options?: LangdetectOptions): Promise<AxiosResponse>;
239
463
  quality(text: string, options?: QualityOptions): Promise<AxiosResponse<QualityResponse>>;
464
+ quality(texts: string[], options?: QualityOptions): Promise<AxiosResponse<QualityBatchResponse>>;
465
+ quality(text: string | string[], options?: QualityOptions): Promise<AxiosResponse<QualityResponse | QualityBatchResponse>>;
240
466
  pii(text: string, options?: PIIOptions): Promise<AxiosResponse<PIIResponse>>;
467
+ ner(text: string, options?: NEROptions): Promise<AxiosResponse<NERResponse>>;
468
+ ner(texts: string[], options?: NEROptions): Promise<AxiosResponse<NERBatchResponse>>;
469
+ ner(text: string | string[], options?: NEROptions): Promise<AxiosResponse<NERResponse | NERBatchResponse>>;
241
470
  safety(text: string, options?: SafetyOptions): Promise<AxiosResponse<SafetyResponse>>;
471
+ safety(texts: string[], options?: SafetyOptions): Promise<AxiosResponse<SafetyBatchResponse>>;
472
+ safety(text: string | string[], options?: SafetyOptions): Promise<AxiosResponse<SafetyResponse | SafetyBatchResponse>>;
242
473
  classify(text: string, labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<ClassifyResponse>>;
474
+ classify(texts: string[], labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<ClassifyBatchResponse>>;
475
+ classify(text: string | string[], labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<ClassifyResponse | ClassifyBatchResponse>>;
243
476
  seo(text: string, options?: SeoOptions): Promise<AxiosResponse<SeoResponse>>;
477
+ accountStatus(): Promise<AxiosResponse<AccountStatusResponse>>;
244
478
  }
package/sapling/client.js CHANGED
@@ -3,6 +3,58 @@ 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
+
12
+ // Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
13
+ // size limits (rule/description length) are server configuration and enforced
14
+ // there with a clear 400 (see classify()).
15
+ const validateStyleguideRules = (rules, method) => {
16
+ const isValidRule = (rule) => {
17
+ if (typeof rule === 'string') {
18
+ return rule.trim().length > 0;
19
+ }
20
+ if (rule == null || typeof rule !== 'object' || Array.isArray(rule)) {
21
+ return false;
22
+ }
23
+ if (typeof rule.name !== 'string' || rule.name.trim().length === 0) {
24
+ return false;
25
+ }
26
+ if (rule.description !== undefined && typeof rule.description !== 'string') {
27
+ return false;
28
+ }
29
+ // Reject unknown keys so a typo like `{ name, descrption: '...' }` fails
30
+ // here rather than being forwarded to the API as a malformed rule (the
31
+ // classify()/extract() convention; the MCP object schema is `.strict()`).
32
+ const allowed = new Set(['name', 'description']);
33
+ if (Object.keys(rule).some((key) => !allowed.has(key))) {
34
+ return false;
35
+ }
36
+ return true;
37
+ };
38
+
39
+ if (
40
+ !Array.isArray(rules)
41
+ || rules.length < 1
42
+ || rules.length > 20
43
+ // Array.from() materializes holes in sparse arrays as `undefined` (which
44
+ // isValidRule rejects); a bare `.every()` would skip them and let a hole
45
+ // through to be serialized as `null`.
46
+ || !Array.from(rules).every(isValidRule)
47
+ ) {
48
+ throw new TypeError(`${method}: rules must be an array of 1 to 20 non-empty strings or `
49
+ + '{name, description?} objects (string name, optional string description).');
50
+ }
51
+
52
+ const names = rules.map(
53
+ (rule) => (typeof rule === 'string' ? rule : rule.name).trim().toLowerCase());
54
+ if (names.length !== new Set(names).size) {
55
+ throw new TypeError(`${method}: rule names must be unique (case-insensitive).`);
56
+ }
57
+ };
6
58
 
7
59
  export class Client {
8
60
  constructor(apiKey, options = {}) {
@@ -60,6 +112,11 @@ export class Client {
60
112
  data.advanced_edits = options.advancedEdits;
61
113
  }
62
114
 
115
+ // Omitted unless set; an explicit null is forwarded so the API rejects it.
116
+ if (options.includeErrorCategories !== undefined) {
117
+ data.include_error_categories = options.includeErrorCategories;
118
+ }
119
+
63
120
  if (userDetails) {
64
121
  const { userId, anonUser } = userDetails;
65
122
  data.user_id = userId;
@@ -195,8 +252,11 @@ export class Client {
195
252
  * from the text, so this is extraction, not inference: a value that isn't stated is
196
253
  * reported missing rather than guessed. Finding nothing is a normal 200.
197
254
  *
198
- * @param {string} text - The document to extract from, plain text or HTML (the API caps
199
- * it at 10,000 characters).
255
+ * @param {string|string[]} text - The document to extract from, plain text or HTML (the
256
+ * API caps it at 10,000 characters). Pass an array of 1–10 strings to extract from a
257
+ * batch in one request (sent as the API's `texts` parameter; combined length capped at
258
+ * 10,000 chars) — the response is then `{results: [...]}` with one
259
+ * single-response-shaped entry per input, in order.
200
260
  * @param {Array<string|{name: string, type?: string, description?: string, required?: boolean}>} fields -
201
261
  * The fields to pull out (1–20): field names, or objects. `type` is one of 'string'
202
262
  * (default), 'number', 'integer', 'boolean', 'date' (returned as YYYY-MM-DD) or 'list'.
@@ -215,7 +275,20 @@ export class Client {
215
275
  // context length) are server configuration and enforced there with a clear
216
276
  // 400 — mirroring them here would drift the day they change (and JS
217
277
  // `length` counts UTF-16 units, not the characters the API counts).
218
- if (typeof text !== 'string' || text.trim().length === 0) {
278
+ // An array of texts is the batch form (sent as `texts`, one result per
279
+ // item); the 1–10 item bound is structural like the field count below.
280
+ const isBatch = Array.isArray(text);
281
+ if (isBatch) {
282
+ // for...of (not text.every) so holes in a sparse array are visited as
283
+ // undefined and rejected, rather than skipped.
284
+ let ok = text.length >= 1 && text.length <= 10;
285
+ for (const item of text) {
286
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
287
+ }
288
+ if (!ok) {
289
+ throw new TypeError('extract: texts must be an array of 1 to 10 non-empty strings.');
290
+ }
291
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
219
292
  throw new TypeError('extract: text must be a non-empty string.');
220
293
  }
221
294
 
@@ -268,7 +341,7 @@ export class Client {
268
341
  }
269
342
 
270
343
  let data = {
271
- text,
344
+ ...(isBatch ? { texts: text } : { text }),
272
345
  fields,
273
346
  key: this.apiKey,
274
347
  }
@@ -286,11 +359,182 @@ export class Client {
286
359
  return this.axiosPromise(url, 'post', data);
287
360
  }
288
361
 
362
+ /**
363
+ * Translate text into a target language with an LLM.
364
+ *
365
+ * Line breaks, markup and placeholders are preserved in place, and the response
366
+ * reports the language the text was actually written in (detected — never assumed
367
+ * from the hint): `translation`, `source_lang`/`source_lang_name` (`'und'`/`null`
368
+ * when undetermined) and the normalized `target_lang`/`target_lang_name`.
369
+ *
370
+ * @param {string|string[]} text - The text to translate (the API caps it at
371
+ * 5,000 characters). Markup and whitespace are preserved, not stripped.
372
+ * Pass an array of 1-10 strings to translate a batch in one request (sent
373
+ * as the API's `texts` parameter; combined length up to 5,000 characters,
374
+ * same target/source/formality applied to every item). A batch response is
375
+ * `{results: [...]}` with one single-response-shaped entry per input, in
376
+ * order.
377
+ * @param {string} targetLang - Language to translate into: an ISO 639 code
378
+ * ('fr', 'zh-TW') or an English language name ('French'). Region variants
379
+ * zh-CN/zh-TW/zh-HK, pt-BR/pt-PT, en-GB/en-US, fr-CA and es-419 are supported
380
+ * as targets.
381
+ * @param {Object} [options]
382
+ * @param {string} [options.source_lang] - Optional hint for the source language,
383
+ * same forms as targetLang. The text itself wins if they disagree.
384
+ * @param {string} [options.formality] - Optional register: 'formal'
385
+ * (vous/Sie/usted) or 'informal' (tu/du/tú). Omit for the model default.
386
+ * @returns {Promise<AxiosResponse>}
387
+ */
388
+ translate(text, targetLang, options = {}) {
389
+ const url = `${this.urlEndpoint}translate`;
390
+
391
+ // Shape/type checks only; the language vocabulary and length caps are
392
+ // server configuration enforced there with a clear 400 (mirroring the
393
+ // 176-language table here would drift the day it changes).
394
+ // An array of texts is the batch form (SAP-398, sent as `texts`, one
395
+ // result per item); the 1–10 item bound is structural like safety's.
396
+ const isBatch = Array.isArray(text);
397
+ if (isBatch) {
398
+ // for...of (not text.every) so holes in a sparse array are visited as
399
+ // undefined and rejected, rather than skipped.
400
+ let ok = text.length >= 1 && text.length <= 10;
401
+ if (ok) {
402
+ for (const item of text) {
403
+ if (typeof item !== 'string' || item.trim().length === 0) {
404
+ ok = false;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ if (!ok) {
410
+ throw new TypeError('translate: texts must be an array of 1 to 10 non-empty strings.');
411
+ }
412
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
413
+ throw new TypeError('translate: text must be a non-empty string.');
414
+ }
415
+ if (typeof targetLang !== 'string' || targetLang.trim().length === 0) {
416
+ throw new TypeError('translate: targetLang must be a non-empty string (an ISO 639 '
417
+ + 'code like "fr" or an English language name like "French").');
418
+ }
419
+
420
+ let data = {
421
+ ...(isBatch ? { texts: text } : { text }),
422
+ target_lang: targetLang,
423
+ key: this.apiKey,
424
+ }
425
+
426
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
427
+ // bad type rather than silently treated as "omitted" (same as extract's
428
+ // context handling).
429
+ if (options?.source_lang !== undefined) {
430
+ if (typeof options.source_lang !== 'string' || options.source_lang.trim().length === 0) {
431
+ throw new TypeError('translate: source_lang must be a non-empty string.');
432
+ }
433
+ data.source_lang = options.source_lang;
434
+ }
435
+ if (options?.formality !== undefined) {
436
+ if (!TRANSLATE_FORMALITIES.includes(options.formality)) {
437
+ throw new TypeError('translate: formality must be one of '
438
+ + `${TRANSLATE_FORMALITIES.join(', ')}.`);
439
+ }
440
+ data.formality = options.formality;
441
+ }
442
+
443
+ return this.axiosPromise(url, 'post', data);
444
+ }
445
+
446
+ /**
447
+ * Summarize a document, webpage or email thread into a prose summary plus
448
+ * key points.
449
+ *
450
+ * Returns `summary` (the prose summary), `key_points` (an array of strings),
451
+ * `length` (the option the summary was generated with) and `result`
452
+ * (mirrors `summary` — the endpoint's original response key, kept for
453
+ * backward compatibility).
454
+ *
455
+ * @param {string|string[]} text - The document to summarize, plain text or
456
+ * HTML (tags are stripped; the API caps it at 20,000 characters). Pass an
457
+ * array of 1-10 strings to summarize a batch in one request (sent as the
458
+ * API's `texts` parameter; combined length up to 20,000 characters, same
459
+ * length option applied to every item). A batch response is
460
+ * `{results: [...]}` with one single-response-shaped entry per input, in
461
+ * order.
462
+ * @param {Object} [options]
463
+ * @param {string} [options.length] - Optional summary length: 'short',
464
+ * 'medium' (the default) or 'long'.
465
+ * @returns {Promise<AxiosResponse>}
466
+ */
467
+ summarize(text, options = {}) {
468
+ const url = `${this.urlEndpoint}summarize`;
469
+
470
+ // An array of texts is the batch form (SAP-399, sent as `texts`, one
471
+ // result per item); the 1–10 item bound is structural like translate's.
472
+ const isBatch = Array.isArray(text);
473
+ if (isBatch) {
474
+ // for...of (not text.every) so holes in a sparse array are visited as
475
+ // undefined and rejected, rather than skipped.
476
+ let ok = text.length >= 1 && text.length <= 10;
477
+ if (ok) {
478
+ for (const item of text) {
479
+ if (typeof item !== 'string' || item.trim().length === 0) {
480
+ ok = false;
481
+ break;
482
+ }
483
+ }
484
+ }
485
+ if (!ok) {
486
+ throw new TypeError('summarize: texts must be an array of 1 to 10 non-empty strings.');
487
+ }
488
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
489
+ throw new TypeError('summarize: text must be a non-empty string.');
490
+ }
491
+
492
+ let data = {
493
+ ...(isBatch ? { texts: text } : { text }),
494
+ key: this.apiKey,
495
+ }
496
+
497
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
498
+ // bad type rather than silently treated as "omitted" (same as translate's
499
+ // formality handling).
500
+ if (options?.length !== undefined) {
501
+ if (!SUMMARIZE_LENGTHS.includes(options.length)) {
502
+ throw new TypeError('summarize: length must be one of '
503
+ + `${SUMMARIZE_LENGTHS.join(', ')}.`);
504
+ }
505
+ data.length = options.length;
506
+ }
507
+
508
+ return this.axiosPromise(url, 'post', data);
509
+ }
510
+
289
511
  langdetect(text, options = {}) {
290
512
  const url = `${this.urlEndpoint}langdetect`;
291
513
 
514
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
515
+ // result per item); the 1–10 item bound is structural like classify's.
516
+ // The single (string) form is passed through unvalidated, as it always
517
+ // has been — the server answers a clear 400.
518
+ const isBatch = Array.isArray(text);
519
+ if (isBatch) {
520
+ // for...of (not text.every) so holes in a sparse array are visited as
521
+ // undefined and rejected, rather than skipped.
522
+ let ok = text.length >= 1 && text.length <= 10;
523
+ if (ok) {
524
+ for (const item of text) {
525
+ if (typeof item !== 'string' || item.trim().length === 0) {
526
+ ok = false;
527
+ break;
528
+ }
529
+ }
530
+ }
531
+ if (!ok) {
532
+ throw new TypeError('langdetect: texts must be an array of 1 to 10 non-empty strings.');
533
+ }
534
+ }
535
+
292
536
  let data = {
293
- text,
537
+ ...(isBatch ? { texts: text } : { text }),
294
538
  key: this.apiKey,
295
539
  }
296
540
 
@@ -314,7 +558,11 @@ export class Client {
314
558
  * empty), `scores` (one `{label, score}` per input label, descending), a one-sentence
315
559
  * `rationale` and the echoed `multi_label` mode.
316
560
  *
317
- * @param {string} text - Text to classify, plain text or HTML (the API caps it at 10,000 chars).
561
+ * @param {string|string[]} text - Text to classify, plain text or HTML (the API caps it
562
+ * at 10,000 chars). Pass an array of 1–10 strings to classify a batch in one request
563
+ * (sent as the API's `texts` parameter; combined length capped at 10,000 chars) — the
564
+ * response is then `{results: [...]}` with one single-response-shaped entry per input,
565
+ * in order.
318
566
  * @param {Array<string|{name: string, description?: string}>} labels - Candidate labels
319
567
  * (2–20). Names unique case-insensitively; the API caps names at 50 and descriptions at 200 chars.
320
568
  * @param {Object} [options]
@@ -333,7 +581,20 @@ export class Client {
333
581
  // context length) are server configuration and enforced there with a
334
582
  // clear 400 — mirroring them here would drift the day they change (and
335
583
  // JS `length` counts UTF-16 units, not the characters the API counts).
336
- if (typeof text !== 'string' || text.trim().length === 0) {
584
+ // An array of texts is the batch form (sent as `texts`, one result per
585
+ // item); the 1–10 item bound is structural like the label count below.
586
+ const isBatch = Array.isArray(text);
587
+ if (isBatch) {
588
+ // for...of (not text.every) so holes in a sparse array are visited as
589
+ // undefined and rejected, rather than skipped.
590
+ let ok = text.length >= 1 && text.length <= 10;
591
+ for (const item of text) {
592
+ ok = ok && typeof item === 'string' && item.trim().length > 0;
593
+ }
594
+ if (!ok) {
595
+ throw new TypeError('classify: texts must be an array of 1 to 10 non-empty strings.');
596
+ }
597
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
337
598
  throw new TypeError('classify: text must be a non-empty string.');
338
599
  }
339
600
 
@@ -379,7 +640,7 @@ export class Client {
379
640
  }
380
641
 
381
642
  let data = {
382
- text,
643
+ ...(isBatch ? { texts: text } : { text }),
383
644
  labels,
384
645
  key: this.apiKey,
385
646
  }
@@ -477,12 +738,30 @@ export class Client {
477
738
  const url = `${this.urlEndpoint}quality`;
478
739
 
479
740
  // Shape/type checks only; size limits are server configuration (see seo()).
480
- if (typeof text !== 'string' || text.trim().length === 0) {
741
+ // An array of texts is the batch form (SAP-400, sent as `texts`, one
742
+ // result per item); the 1–10 item bound is structural like classify's.
743
+ const isBatch = Array.isArray(text);
744
+ if (isBatch) {
745
+ // for...of (not text.every) so holes in a sparse array are visited as
746
+ // undefined and rejected, rather than skipped.
747
+ let ok = text.length >= 1 && text.length <= 10;
748
+ if (ok) {
749
+ for (const item of text) {
750
+ if (typeof item !== 'string' || item.trim().length === 0) {
751
+ ok = false;
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ if (!ok) {
757
+ throw new TypeError('quality: texts must be an array of 1 to 10 non-empty strings.');
758
+ }
759
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
481
760
  throw new TypeError('quality: text must be a non-empty string.');
482
761
  }
483
762
 
484
763
  let data = {
485
- text,
764
+ ...(isBatch ? { texts: text } : { text }),
486
765
  key: this.apiKey,
487
766
  }
488
767
 
@@ -508,12 +787,30 @@ export class Client {
508
787
  const url = `${this.urlEndpoint}safety`;
509
788
 
510
789
  // Shape/type checks only; size limits are server configuration (see seo()).
511
- if (typeof text !== 'string' || text.trim().length === 0) {
790
+ // An array of texts is the batch form (SAP-397, sent as `texts`, one
791
+ // result per item); the 1–10 item bound is structural like classify's.
792
+ const isBatch = Array.isArray(text);
793
+ if (isBatch) {
794
+ // for...of (not text.every) so holes in a sparse array are visited as
795
+ // undefined and rejected, rather than skipped.
796
+ let ok = text.length >= 1 && text.length <= 10;
797
+ if (ok) {
798
+ for (const item of text) {
799
+ if (typeof item !== 'string' || item.trim().length === 0) {
800
+ ok = false;
801
+ break;
802
+ }
803
+ }
804
+ }
805
+ if (!ok) {
806
+ throw new TypeError('safety: texts must be an array of 1 to 10 non-empty strings.');
807
+ }
808
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
512
809
  throw new TypeError('safety: text must be a non-empty string.');
513
810
  }
514
811
 
515
812
  let data = {
516
- text,
813
+ ...(isBatch ? { texts: text } : { text }),
517
814
  key: this.apiKey,
518
815
  }
519
816
 
@@ -526,6 +823,12 @@ export class Client {
526
823
  }
527
824
  data.threshold = options.threshold;
528
825
  }
826
+ if (options?.spans !== undefined) {
827
+ if (typeof options.spans !== 'boolean') {
828
+ throw new TypeError('safety: spans must be a boolean.');
829
+ }
830
+ data.spans = options.spans;
831
+ }
529
832
 
530
833
  return this.axiosPromise(url, 'post', data);
531
834
  }
@@ -533,8 +836,30 @@ export class Client {
533
836
  aidetect(text, options = {}) {
534
837
  const url = `${this.urlEndpoint}aidetect`;
535
838
 
839
+ // An array of texts is the batch form (SAP-403, sent as `texts`, one
840
+ // result per item); the 1–10 item bound is structural like classify's.
841
+ // The single form keeps its historical no-validation behavior (the
842
+ // server 400s clearly on a bad `text`).
843
+ const isBatch = Array.isArray(text);
844
+ if (isBatch) {
845
+ // for...of (not text.every) so holes in a sparse array are visited as
846
+ // undefined and rejected, rather than skipped.
847
+ let ok = text.length >= 1 && text.length <= 10;
848
+ if (ok) {
849
+ for (const item of text) {
850
+ if (typeof item !== 'string' || item.trim().length === 0) {
851
+ ok = false;
852
+ break;
853
+ }
854
+ }
855
+ }
856
+ if (!ok) {
857
+ throw new TypeError('aidetect: texts must be an array of 1 to 10 non-empty strings.');
858
+ }
859
+ }
860
+
536
861
  let data = {
537
- text,
862
+ ...(isBatch ? { texts: text } : { text }),
538
863
  key: this.apiKey,
539
864
  }
540
865
 
@@ -545,6 +870,265 @@ export class Client {
545
870
  return this.axiosPromise(url, 'post', data);
546
871
  }
547
872
 
873
+ /**
874
+ * Check a text for compliance with style rules you supply (house style,
875
+ * brand voice, editorial guidelines). Only the listed rules are enforced.
876
+ *
877
+ * Returns `violations` (one `{rule, text, start, end, note, suggestion}`
878
+ * entry per violation, most important first — `start`/`end` index the
879
+ * submitted text and are null when the passage cannot be located), `rules`
880
+ * (the checked rule names) and `compliant` (true when no violations were
881
+ * found). The text is checked exactly as submitted — markup is NOT
882
+ * stripped, so offsets always index the string you sent.
883
+ *
884
+ * @param {string|string[]} text - The text to check (the API caps it at
885
+ * 10,000 characters), or an array of 1–10 texts for the batch form (sent
886
+ * as the API's `texts` parameter; combined length up to 10,000
887
+ * characters, same rules applied to every item). A batch response is
888
+ * `{results: [...]}` with one single-response-shaped entry per input, in
889
+ * order, each item's violation offsets indexing that item's own text.
890
+ * @param {Array<string|{name: string, description?: string}>|string} rules
891
+ * - 1 to 20 style rules: a rule name (max 80 chars) or a
892
+ * `{name, description}` object whose optional description (max 400
893
+ * chars) carries the nuance the model follows. Names must be unique
894
+ * case-insensitively. OR a string: the name of a saved ruleset (see
895
+ * saveStyleguideRuleset()) to check against, matched case-insensitively
896
+ * within the API key's account.
897
+ * @returns {Promise<AxiosResponse>}
898
+ */
899
+ styleguide(text, rules) {
900
+ const url = `${this.urlEndpoint}styleguide`;
901
+
902
+ // Shape/type checks only. Size limits (text/rule/description length) are
903
+ // server configuration and enforced there with a clear 400 (see
904
+ // classify()).
905
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
906
+ // result per item); the 1–10 item bound is structural like classify's.
907
+ const isBatch = Array.isArray(text);
908
+ if (isBatch) {
909
+ // for...of (not text.every) so holes in a sparse array are visited as
910
+ // undefined and rejected, rather than skipped.
911
+ let ok = text.length >= 1 && text.length <= 10;
912
+ if (ok) {
913
+ for (const item of text) {
914
+ if (typeof item !== 'string' || item.trim().length === 0) {
915
+ ok = false;
916
+ break;
917
+ }
918
+ }
919
+ }
920
+ if (!ok) {
921
+ throw new TypeError('styleguide: texts must be an array of 1 to 10 non-empty strings.');
922
+ }
923
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
924
+ throw new TypeError('styleguide: text must be a non-empty string.');
925
+ }
926
+
927
+ // A string names a saved ruleset (see saveStyleguideRuleset()) and is
928
+ // sent as the API's `ruleset` parameter in place of inline `rules`.
929
+ const isSavedRuleset = typeof rules === 'string';
930
+ if (isSavedRuleset) {
931
+ if (rules.trim().length === 0) {
932
+ throw new TypeError('styleguide: ruleset name must be a non-empty string.');
933
+ }
934
+ } else {
935
+ validateStyleguideRules(rules, 'styleguide');
936
+ }
937
+
938
+ let data = {
939
+ ...(isBatch ? { texts: text } : { text }),
940
+ ...(isSavedRuleset ? { ruleset: rules } : { rules }),
941
+ key: this.apiKey,
942
+ }
943
+
944
+ return this.axiosPromise(url, 'post', data);
945
+ }
946
+
947
+ /**
948
+ * List the saved style-guide rulesets of this API key's account (its team,
949
+ * for team-scoped keys — a team's rulesets are shared by every caller of
950
+ * its keys). Requires a per-account API key.
951
+ *
952
+ * Returns `rulesets`: `{name, rules, created_at, updated_at}` entries whose
953
+ * `rules` are the normalized `{name, description}` objects the check
954
+ * endpoint applies.
955
+ *
956
+ * @returns {Promise<AxiosResponse>}
957
+ */
958
+ styleguideRulesets() {
959
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
960
+ // Not axiosPromise: this is a GET and the key rides the query string.
961
+ return axios({
962
+ url,
963
+ method: 'get',
964
+ params: { key: this.apiKey },
965
+ timeout: this.timeout,
966
+ });
967
+ }
968
+
969
+ /**
970
+ * Create — or replace, when the name already exists (matched
971
+ * case-insensitively) — a saved style-guide ruleset, so later
972
+ * `styleguide(text, name)` calls check against it by name. Owned by the
973
+ * API key's account (its team, for team-scoped keys). Requires a
974
+ * per-account API key.
975
+ *
976
+ * Returns `ruleset` (the saved `{name, rules, created_at, updated_at}`)
977
+ * and `created` (false when an existing ruleset was replaced).
978
+ *
979
+ * @param {string} name - The ruleset name (the API caps it at 100
980
+ * characters; surrounding/duplicate whitespace is normalized).
981
+ * @param {Array<string|{name: string, description?: string}>} rules - The
982
+ * rules to save; same shape and bounds as styleguide()'s inline rules.
983
+ * @returns {Promise<AxiosResponse>}
984
+ */
985
+ saveStyleguideRuleset(name, rules) {
986
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
987
+ if (typeof name !== 'string' || name.trim().length === 0) {
988
+ throw new TypeError('saveStyleguideRuleset: name must be a non-empty string.');
989
+ }
990
+ validateStyleguideRules(rules, 'saveStyleguideRuleset');
991
+
992
+ let data = {
993
+ name,
994
+ rules,
995
+ key: this.apiKey,
996
+ }
997
+
998
+ return this.axiosPromise(url, 'post', data);
999
+ }
1000
+
1001
+ /**
1002
+ * Delete a saved style-guide ruleset by name (matched case-insensitively;
1003
+ * 404 when no such ruleset exists). Requires a per-account API key.
1004
+ *
1005
+ * @param {string} name - The ruleset name to delete.
1006
+ * @returns {Promise<AxiosResponse>}
1007
+ */
1008
+ deleteStyleguideRuleset(name) {
1009
+ const url = `${this.urlEndpoint}styleguide/rulesets`;
1010
+ if (typeof name !== 'string' || name.trim().length === 0) {
1011
+ throw new TypeError('deleteStyleguideRuleset: name must be a non-empty string.');
1012
+ }
1013
+ // The name rides the query string: some HTTP stacks drop DELETE bodies,
1014
+ // and the API accepts the query form for exactly that reason.
1015
+ return axios({
1016
+ url,
1017
+ method: 'delete',
1018
+ params: { key: this.apiKey, name },
1019
+ timeout: this.timeout,
1020
+ });
1021
+ }
1022
+
1023
+ /**
1024
+ * Rewrite a text in plain language at a target reading level, keeping all
1025
+ * of its content, structure and language — nothing is summarized or
1026
+ * dropped.
1027
+ *
1028
+ * Returns `simplified` (the rewrite, structure preserved), the applied
1029
+ * `reading_level`, the `lang` the readability scores were computed for
1030
+ * (detected when not supplied) and, when that language supports scoring, a
1031
+ * `readability` block: `{before: {grade, ease}, after: {grade, ease}}`
1032
+ * Flesch-Kincaid scores, so the improvement is verifiable.
1033
+ *
1034
+ * @param {string|string[]} text - The text to simplify (the API caps it at
1035
+ * 5,000 characters), or an array of 1–10 texts for the batch form (sent
1036
+ * as the API's `texts` parameter; combined length up to 5,000
1037
+ * characters, same options applied to every item). A batch response is
1038
+ * `{results: [...]}` with one single-response-shaped entry per input, in
1039
+ * order; with `lang` omitted, each item's language (and readability
1040
+ * scores) is detected on its own. Sent to the model as submitted —
1041
+ * markup and line structure are preserved, not stripped.
1042
+ * @param {Object} [options]
1043
+ * @param {string} [options.reading_level] - Target audience: 'plain'
1044
+ * (plain-language style for a general audience, the API default),
1045
+ * 'elementary' (~US grade 3-5), 'middle_school' (~grade 6-8) or
1046
+ * 'high_school' (~grade 9-10).
1047
+ * @param {string[]} [options.preserve_terms] - Up to 20 terms (each up to
1048
+ * 100 characters) the rewrite must keep exactly as written — product
1049
+ * names, defined legal terms. A rewrite that drops one is refused
1050
+ * server-side rather than returned.
1051
+ * @param {string} [options.lang] - ISO 639-1 code used to pick the
1052
+ * readability formula for the before/after scores. Omit for
1053
+ * auto-detection (falls back to 'en'). The rewrite itself always stays
1054
+ * in the text's own language.
1055
+ * @returns {Promise<AxiosResponse>}
1056
+ */
1057
+ simplify(text, options = {}) {
1058
+ const url = `${this.urlEndpoint}simplify`;
1059
+
1060
+ // Shape/type checks only. Size limits (text/term length) are server
1061
+ // configuration and enforced there with a clear 400 (see classify()).
1062
+ // An array of texts is the batch form (SAP-412, sent as `texts`, one
1063
+ // result per item); the 1–10 item bound is structural like classify's.
1064
+ const isBatch = Array.isArray(text);
1065
+ if (isBatch) {
1066
+ // for...of (not text.every) so holes in a sparse array are visited as
1067
+ // undefined and rejected, rather than skipped.
1068
+ let ok = text.length >= 1 && text.length <= 10;
1069
+ if (ok) {
1070
+ for (const item of text) {
1071
+ if (typeof item !== 'string' || item.trim().length === 0) {
1072
+ ok = false;
1073
+ break;
1074
+ }
1075
+ }
1076
+ }
1077
+ if (!ok) {
1078
+ throw new TypeError('simplify: texts must be an array of 1 to 10 non-empty strings.');
1079
+ }
1080
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1081
+ throw new TypeError('simplify: text must be a non-empty string.');
1082
+ }
1083
+
1084
+ let data = {
1085
+ ...(isBatch ? { texts: text } : { text }),
1086
+ key: this.apiKey,
1087
+ }
1088
+
1089
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
1090
+ // bad type rather than silently treated as "omitted" (same as translate's
1091
+ // option handling).
1092
+ if (options?.reading_level !== undefined) {
1093
+ if (!SIMPLIFY_READING_LEVELS.includes(options.reading_level)) {
1094
+ throw new TypeError('simplify: reading_level must be one of '
1095
+ + `${SIMPLIFY_READING_LEVELS.join(', ')}.`);
1096
+ }
1097
+ data.reading_level = options.reading_level;
1098
+ }
1099
+ if (options?.preserve_terms !== undefined) {
1100
+ const terms = options.preserve_terms;
1101
+ // An empty list is a valid no-op (callers may build it dynamically); it
1102
+ // is simply omitted from the payload below rather than rejected.
1103
+ // for...of (not terms.every) so holes in a sparse array are visited as
1104
+ // undefined and rejected, rather than skipped.
1105
+ let ok = Array.isArray(terms) && terms.length <= 20;
1106
+ if (ok) {
1107
+ for (const term of terms) {
1108
+ if (typeof term !== 'string' || term.trim().length === 0) {
1109
+ ok = false;
1110
+ break;
1111
+ }
1112
+ }
1113
+ }
1114
+ if (!ok) {
1115
+ throw new TypeError('simplify: preserve_terms must be an array of up to 20 '
1116
+ + 'non-empty strings.');
1117
+ }
1118
+ if (terms.length > 0) {
1119
+ data.preserve_terms = terms;
1120
+ }
1121
+ }
1122
+ if (options?.lang !== undefined) {
1123
+ if (typeof options.lang !== 'string' || options.lang.trim().length === 0) {
1124
+ throw new TypeError('simplify: lang must be a non-empty string.');
1125
+ }
1126
+ data.lang = options.lang;
1127
+ }
1128
+
1129
+ return this.axiosPromise(url, 'post', data);
1130
+ }
1131
+
548
1132
  pii(text, options = {}) {
549
1133
  const url = `${this.urlEndpoint}pii`;
550
1134
 
@@ -577,4 +1161,106 @@ export class Client {
577
1161
 
578
1162
  return this.axiosPromise(url, 'post', data);
579
1163
  }
1164
+
1165
+ /**
1166
+ * Named-entity recognition: people, organizations, locations, dates, times,
1167
+ * money amounts, percentages, quantities, products, and events, each with
1168
+ * character offsets into the exact text submitted (nothing is stripped or
1169
+ * normalized, so spans apply directly to your copy of the document).
1170
+ * Complements pii(): that finds checksum-validated identifiers, this finds
1171
+ * names and real-world references.
1172
+ *
1173
+ * @param {string|string[]} text - The text to analyze, or an array of 1–10
1174
+ * texts for the batch form (sent as the API's `texts` parameter; combined
1175
+ * length up to 10,000 characters, same types applied to every item). A
1176
+ * batch response is `{results: [...]}` with one single-response-shaped
1177
+ * entry per input, in order, each item's spans indexing that item's own
1178
+ * text.
1179
+ * @param {Object} [options]
1180
+ * @param {string[]} [options.types] - Entity types to recognize (subset of
1181
+ * the API's supported types). Default: all of them.
1182
+ */
1183
+ ner(text, options = {}) {
1184
+ const url = `${this.urlEndpoint}ner`;
1185
+
1186
+ // Shape/type checks only; the set of valid entity types is server
1187
+ // configuration and enforced there with a clear 400 (see pii()).
1188
+ // An array of texts is the batch form (SAP-395 series, sent as `texts`,
1189
+ // one result per item); the 1–10 item bound is structural like classify's.
1190
+ const isBatch = Array.isArray(text);
1191
+ if (isBatch) {
1192
+ // for...of (not text.every) so holes in a sparse array are visited as
1193
+ // undefined and rejected, rather than skipped.
1194
+ let ok = text.length >= 1 && text.length <= 10;
1195
+ if (ok) {
1196
+ for (const item of text) {
1197
+ if (typeof item !== 'string' || item.trim().length === 0) {
1198
+ ok = false;
1199
+ break;
1200
+ }
1201
+ }
1202
+ }
1203
+ if (!ok) {
1204
+ throw new TypeError('ner: texts must be an array of 1 to 10 non-empty strings.');
1205
+ }
1206
+ } else if (typeof text !== 'string' || text.trim().length === 0) {
1207
+ throw new TypeError('ner: text must be a non-empty string.');
1208
+ }
1209
+
1210
+ let data = {
1211
+ ...(isBatch ? { texts: text } : { text }),
1212
+ key: this.apiKey,
1213
+ }
1214
+
1215
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
1216
+ // type rather than silently treated as "omitted", matching pii().
1217
+ // Reject an empty list too: the MCP schema requires at least one type
1218
+ // (min(1)), so an empty array is typed-valid input that would otherwise
1219
+ // become an avoidable API error.
1220
+ if (options?.types !== undefined) {
1221
+ if (!Array.isArray(options.types) || options.types.length === 0
1222
+ || !Array.from(options.types).every((type) => typeof type === 'string' && type.trim().length > 0)) {
1223
+ throw new TypeError('ner: types must be a non-empty array of non-empty strings.');
1224
+ }
1225
+ data.types = options.types;
1226
+ }
1227
+
1228
+ return this.axiosPromise(url, 'post', data);
1229
+ }
1230
+
1231
+ /**
1232
+ * Check whether an account-wide block (expired key/subscription,
1233
+ * depleted credits, billing limit, trial daily allowance, monthly quota)
1234
+ * stops this API key from making billable calls, and if so why and for
1235
+ * how long. Works while the key is blocked, which is when it is most
1236
+ * useful: call it after an unexpected 402 or monthly-quota 429 instead of
1237
+ * parsing the error text. It does not reflect a single endpoint's
1238
+ * short-term rate limit (each endpoint has its own bucket), so a
1239
+ * "Capacity used." 429 elsewhere can coexist with api_access: true — use
1240
+ * that 429's retry_after / X-RateLimit-* headers instead. Unbilled, but each call draws a fixed 2,000
1241
+ * characters from its own rate-limit bucket, so don't poll it per request.
1242
+ * Requires a per-account API key; the public demo key and MCP OAuth
1243
+ * access tokens get a 403.
1244
+ *
1245
+ * Always returns `api_access`, `blocked_reason` (null, or one of
1246
+ * key_expired, subscription_expired, credits_depleted,
1247
+ * monthly_billing_limit, trial_daily_limit, monthly_quota_exhausted),
1248
+ * `message` and `usage` ({monthly_quota_chars}); `retry_after` (seconds)
1249
+ * only when blocked. Keys that are not team-scoped also get
1250
+ * `usage.month_start`/`usage.chars_this_month`, `subscription`, `credits`
1251
+ * and, for free-trial accounts, `trial`.
1252
+ *
1253
+ * @returns {Promise<AxiosResponse>}
1254
+ */
1255
+ accountStatus() {
1256
+ const url = `${this.urlEndpoint}account/status`;
1257
+ // Not axiosPromise: this is a GET and the key rides the query string,
1258
+ // like styleguideRulesets().
1259
+ return axios({
1260
+ url,
1261
+ method: 'get',
1262
+ params: { key: this.apiKey },
1263
+ timeout: this.timeout,
1264
+ });
1265
+ }
580
1266
  }