@saplingai/sapling-js 1.0.52 → 1.0.54
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 +346 -4
- package/package.json +2 -2
- package/sapling/client.d.ts +717 -44
- package/sapling/client.js +1070 -53
- package/sapling/sapling-sdk.min.js +1 -1
package/sapling/client.js
CHANGED
|
@@ -8,6 +8,42 @@ const TRANSLATE_FORMALITIES = ['formal', 'informal'];
|
|
|
8
8
|
const SUMMARIZE_LENGTHS = ['short', 'medium', 'long'];
|
|
9
9
|
// Reading levels simplify() accepts; the API rejects anything else with a 400.
|
|
10
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
|
+
// Buckets apiEndpointUsage() accepts (request_validator.INTERVAL_VALS).
|
|
30
|
+
const REPORTING_INTERVALS = ['Daily', 'Weekly', 'Monthly'];
|
|
31
|
+
|
|
32
|
+
// Shared by the reporting methods: the request body with the window offsets,
|
|
33
|
+
// which the API documents as non-negative integers (days before today).
|
|
34
|
+
// Omitted offsets are left out so each endpoint applies its own default.
|
|
35
|
+
const reportingWindowBody = (apiKey, options, method) => {
|
|
36
|
+
const data = { key: apiKey };
|
|
37
|
+
for (const field of ['start_days_back', 'end_days_back']) {
|
|
38
|
+
if (options?.[field] !== undefined) {
|
|
39
|
+
if (!Number.isInteger(options[field]) || options[field] < 0) {
|
|
40
|
+
throw new TypeError(`${method}: ${field} must be a non-negative integer.`);
|
|
41
|
+
}
|
|
42
|
+
data[field] = options[field];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
};
|
|
11
47
|
|
|
12
48
|
// Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
|
|
13
49
|
// size limits (rule/description length) are server configuration and enforced
|
|
@@ -56,6 +92,92 @@ const validateStyleguideRules = (rules, method) => {
|
|
|
56
92
|
}
|
|
57
93
|
};
|
|
58
94
|
|
|
95
|
+
// Shared by extract() and fileExtract(). Shape/type checks only — size limits
|
|
96
|
+
// (field name/description length) are server configuration and enforced there
|
|
97
|
+
// with a clear 400.
|
|
98
|
+
const validateExtractFields = (fields, method) => {
|
|
99
|
+
const isValidField = (field) => {
|
|
100
|
+
if (typeof field === 'string') {
|
|
101
|
+
return field.trim().length > 0;
|
|
102
|
+
}
|
|
103
|
+
if (field == null || typeof field !== 'object' || Array.isArray(field)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (typeof field.name !== 'string' || field.name.trim().length === 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (field.type !== undefined && !EXTRACT_FIELD_TYPES.includes(field.type)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (field.description !== undefined && typeof field.description !== 'string') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (field.required !== undefined && typeof field.required !== 'boolean') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// Reject unknown keys so a typo like `{ name, typ: 'number' }` fails here
|
|
119
|
+
// rather than being forwarded to the API as a malformed field (the MCP
|
|
120
|
+
// schema is `.strict()` for the same reason).
|
|
121
|
+
const allowed = new Set(['name', 'type', 'description', 'required']);
|
|
122
|
+
if (Object.keys(field).some((key) => !allowed.has(key))) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
!Array.isArray(fields)
|
|
130
|
+
|| fields.length < 1
|
|
131
|
+
|| fields.length > 20
|
|
132
|
+
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
133
|
+
// isValidField rejects); a bare `.every()` would skip them and let a hole
|
|
134
|
+
// through to be serialized as `null`.
|
|
135
|
+
|| !Array.from(fields).every(isValidField)
|
|
136
|
+
) {
|
|
137
|
+
throw new TypeError(`${method}: fields must be an array of 1 to 20 non-empty strings or `
|
|
138
|
+
+ '{name, type?, description?, required?} objects (string name, type one of '
|
|
139
|
+
+ `${EXTRACT_FIELD_TYPES.join(', ')}, optional string description, optional boolean required).`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const names = fields.map((field) => (typeof field === 'string' ? field : field.name).toLowerCase());
|
|
143
|
+
if (names.length !== new Set(names).size) {
|
|
144
|
+
throw new TypeError(`${method}: field names must be unique (case-insensitive).`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Multipart body for the document-upload endpoints (/api/v1/file-aidetect,
|
|
149
|
+
// /api/v1/file-extract, /api/v1/file-summarize): the document in a `file` part and the options as a
|
|
150
|
+
// `jsonParams` form field. `file` is a Blob/File (browser, or Node 18+), or a
|
|
151
|
+
// Buffer/Uint8Array/ArrayBuffer read from disk. The API picks the format by
|
|
152
|
+
// file extension, so a name is required: a File's own name, or
|
|
153
|
+
// `options.filename`. Which extensions are accepted is server configuration
|
|
154
|
+
// (a clear 400 lists them), so it is not mirrored here.
|
|
155
|
+
const buildUploadForm = (file, filename, jsonParams, method) => {
|
|
156
|
+
// Global FormData/Blob arrived in Node 18; fail with a clear message rather
|
|
157
|
+
// than a ReferenceError on older runtimes without a polyfill.
|
|
158
|
+
if (typeof FormData === 'undefined' || typeof Blob === 'undefined') {
|
|
159
|
+
throw new Error(`${method}: document upload needs global FormData and Blob `
|
|
160
|
+
+ '(Node 18+ or a browser).');
|
|
161
|
+
}
|
|
162
|
+
let blob;
|
|
163
|
+
if (file instanceof Blob) {
|
|
164
|
+
blob = file;
|
|
165
|
+
} else if (file instanceof ArrayBuffer || ArrayBuffer.isView(file)) {
|
|
166
|
+
blob = new Blob([file]);
|
|
167
|
+
} else {
|
|
168
|
+
throw new TypeError(`${method}: file must be a Blob, File, Buffer, Uint8Array or ArrayBuffer.`);
|
|
169
|
+
}
|
|
170
|
+
const name = filename !== undefined ? filename : file.name;
|
|
171
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
172
|
+
throw new TypeError(`${method}: a filename is required (pass options.filename, e.g. `
|
|
173
|
+
+ "'report.pdf'); the API chooses the document format by its extension.");
|
|
174
|
+
}
|
|
175
|
+
const form = new FormData();
|
|
176
|
+
form.append('file', blob, name);
|
|
177
|
+
form.append('jsonParams', JSON.stringify(jsonParams));
|
|
178
|
+
return form;
|
|
179
|
+
};
|
|
180
|
+
|
|
59
181
|
export class Client {
|
|
60
182
|
constructor(apiKey, options = {}) {
|
|
61
183
|
this.apiKey = apiKey;
|
|
@@ -64,10 +186,32 @@ export class Client {
|
|
|
64
186
|
this.pathname = options.pathname || '/api/v1/';
|
|
65
187
|
this.urlEndpoint = `${this.hostname}${this.pathname}`;
|
|
66
188
|
this.defaultSessionId = uuidv4();
|
|
189
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
|
|
190
|
+
// bad type rather than silently treated as "omitted", matching the
|
|
191
|
+
// per-method option handling.
|
|
192
|
+
if (options.returnUsage !== undefined) {
|
|
193
|
+
if (typeof options.returnUsage !== 'boolean') {
|
|
194
|
+
throw new TypeError('Client: returnUsage must be a boolean.');
|
|
195
|
+
}
|
|
196
|
+
this.returnUsage = options.returnUsage;
|
|
197
|
+
} else {
|
|
198
|
+
this.returnUsage = false;
|
|
199
|
+
}
|
|
67
200
|
axios.defaults.timeout = this.timeout;
|
|
68
201
|
}
|
|
69
202
|
|
|
70
203
|
axiosPromise(url, method, data) {
|
|
204
|
+
// Client-wide opt-in usage reporting: `return_usage` is accepted by every
|
|
205
|
+
// API-key endpoint (attached centrally server-side), so it is injected
|
|
206
|
+
// centrally here rather than wired into each method. Only `true` is sent —
|
|
207
|
+
// false is the server default and is omitted, and a `return_usage` a
|
|
208
|
+
// future method sets itself wins over the client-level flag. The
|
|
209
|
+
// `[object Object]` guard restricts the shallow copy to plain objects so a
|
|
210
|
+
// future non-plain body (Buffer, FormData, URLSearchParams, ...) is left
|
|
211
|
+
// untouched rather than spread into a stripped-down copy.
|
|
212
|
+
if (this.returnUsage && Object.prototype.toString.call(data) === '[object Object]' && !('return_usage' in data)) {
|
|
213
|
+
data = { ...data, return_usage: true };
|
|
214
|
+
}
|
|
71
215
|
return new Promise((resolve, reject) => {
|
|
72
216
|
axios({
|
|
73
217
|
url,
|
|
@@ -186,8 +330,10 @@ export class Client {
|
|
|
186
330
|
data.variety = options.variety;
|
|
187
331
|
}
|
|
188
332
|
|
|
333
|
+
// The API reads this option as camelCase `userData` (and echoes it back
|
|
334
|
+
// as `user_data`); a snake_case key is silently ignored.
|
|
189
335
|
if (options.userData != null) {
|
|
190
|
-
data.
|
|
336
|
+
data.userData = options.userData;
|
|
191
337
|
}
|
|
192
338
|
|
|
193
339
|
if (userDetails) {
|
|
@@ -200,6 +346,18 @@ export class Client {
|
|
|
200
346
|
}
|
|
201
347
|
|
|
202
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Autocomplete: predict how a partially written sentence continues.
|
|
351
|
+
*
|
|
352
|
+
* Returns `output` (the continuation), `query` (the part of the sent query
|
|
353
|
+
* it continues: only the text after the last run of three spaces) and
|
|
354
|
+
* `hash`. A 200 with `[]` means no completion is available. The query is
|
|
355
|
+
* capped at 200 characters, and the API key needs autocomplete enabled.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} query - The end of what the user has typed.
|
|
358
|
+
* @param {string} [sessionId] - Defaults to the client's session id.
|
|
359
|
+
* @returns {Promise<AxiosResponse>}
|
|
360
|
+
*/
|
|
203
361
|
complete(query, sessionId) {
|
|
204
362
|
const url = `${this.urlEndpoint}complete`;
|
|
205
363
|
sessionId = sessionId || this.defaultSessionId;
|
|
@@ -212,6 +370,18 @@ export class Client {
|
|
|
212
370
|
}
|
|
213
371
|
|
|
214
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Record that the user accepted a completion from complete().
|
|
375
|
+
*
|
|
376
|
+
* Pass the `hash`, `query` and `output` that complete() returned, unchanged:
|
|
377
|
+
* the API recomputes the hash and answers 400 when they don't match.
|
|
378
|
+
*
|
|
379
|
+
* @param {string} completeUUID - The `hash` from complete().
|
|
380
|
+
* @param {string} query - The `query` from complete().
|
|
381
|
+
* @param {string} completion - The `output` from complete().
|
|
382
|
+
* @param {string} [sessionId] - Defaults to the client's session id.
|
|
383
|
+
* @returns {Promise<AxiosResponse>}
|
|
384
|
+
*/
|
|
215
385
|
acceptComplete(completeUUID, query, completion, sessionId) {
|
|
216
386
|
const url = `${this.urlEndpoint}complete/${completeUUID}/accept`;
|
|
217
387
|
sessionId = sessionId || this.defaultSessionId;
|
|
@@ -227,6 +397,17 @@ export class Client {
|
|
|
227
397
|
return this.axiosPromise(url, 'post', data);
|
|
228
398
|
}
|
|
229
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Tone classification: overall and per-sentence emotional tone.
|
|
402
|
+
*
|
|
403
|
+
* Returns `overall` (up to 3 [probability, tone name, emoji] triples, most
|
|
404
|
+
* likely first, probabilities summing to 1), `results` (one list of triples
|
|
405
|
+
* per sentence) and `sents` (the sentences). A text the API cannot split
|
|
406
|
+
* into sentences gets a 200 with `[]`.
|
|
407
|
+
*
|
|
408
|
+
* @param {string} text - The text to analyze, at most 8000 characters.
|
|
409
|
+
* @returns {Promise<AxiosResponse>}
|
|
410
|
+
*/
|
|
230
411
|
tone(text) {
|
|
231
412
|
const url = `${this.urlEndpoint}tone`;
|
|
232
413
|
|
|
@@ -237,6 +418,498 @@ export class Client {
|
|
|
237
418
|
return this.axiosPromise(url, 'post', data);
|
|
238
419
|
}
|
|
239
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Rephrase / paraphrase: generate alternative phrasings of a text.
|
|
423
|
+
*
|
|
424
|
+
* Returns `results`: one `{hash, original, replacement, rephrase_type,
|
|
425
|
+
* model_version}` entry per alternative. HTML is stripped server-side.
|
|
426
|
+
*
|
|
427
|
+
* @param {string} text - The text to rephrase (the API caps its length).
|
|
428
|
+
* @param {Object} [options]
|
|
429
|
+
* @param {string} [options.mapping] - Transformation to apply (see
|
|
430
|
+
* REPHRASE_MAPPINGS). Defaults to 'paraphrase'.
|
|
431
|
+
* @param {string} [options.tone_mapping] - Target tone; only used with
|
|
432
|
+
* mapping 'switch_tone'. Defaults to 'straightforward'.
|
|
433
|
+
* @param {string} [options.tense_mapping] - Target tense; only used with
|
|
434
|
+
* mapping 'switch_tense'. Defaults to 'simple past'.
|
|
435
|
+
* @param {number} [options.num_results] - Number of alternatives, 1–8
|
|
436
|
+
* (default 5). Honored by the paraphrase, informal_to_formal,
|
|
437
|
+
* formal_to_informal, generate_title and generate_acronym mappings; other
|
|
438
|
+
* mappings return their default number of results.
|
|
439
|
+
* @param {string} [options.session_id] - Optional session identifier used to
|
|
440
|
+
* group feedback events.
|
|
441
|
+
* @returns {Promise<AxiosResponse>}
|
|
442
|
+
*/
|
|
443
|
+
rephrase(text, options = {}) {
|
|
444
|
+
const url = `${this.urlEndpoint}rephrase`;
|
|
445
|
+
|
|
446
|
+
// Shape/type/enum checks only; the length cap is server configuration and
|
|
447
|
+
// enforced there with a clear 400 (see seo()). The enums and the 1–8
|
|
448
|
+
// num_results bound mirror the API's validators, which reject anything else.
|
|
449
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
450
|
+
throw new TypeError('rephrase: text must be a non-empty string.');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let data = {
|
|
454
|
+
text,
|
|
455
|
+
key: this.apiKey,
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
|
|
459
|
+
// type rather than silently treated as "omitted", matching classify()/seo().
|
|
460
|
+
if (options?.mapping !== undefined) {
|
|
461
|
+
if (!REPHRASE_MAPPINGS.includes(options.mapping)) {
|
|
462
|
+
throw new TypeError(`rephrase: mapping must be one of ${REPHRASE_MAPPINGS.join(', ')}.`);
|
|
463
|
+
}
|
|
464
|
+
data.mapping = options.mapping;
|
|
465
|
+
}
|
|
466
|
+
if (options?.tone_mapping !== undefined) {
|
|
467
|
+
if (!REPHRASE_TONE_MAPPINGS.includes(options.tone_mapping)) {
|
|
468
|
+
throw new TypeError(
|
|
469
|
+
`rephrase: tone_mapping must be one of ${REPHRASE_TONE_MAPPINGS.join(', ')}.`);
|
|
470
|
+
}
|
|
471
|
+
data.tone_mapping = options.tone_mapping;
|
|
472
|
+
}
|
|
473
|
+
if (options?.tense_mapping !== undefined) {
|
|
474
|
+
if (!REPHRASE_TENSE_MAPPINGS.includes(options.tense_mapping)) {
|
|
475
|
+
throw new TypeError(
|
|
476
|
+
`rephrase: tense_mapping must be one of ${REPHRASE_TENSE_MAPPINGS.join(', ')}.`);
|
|
477
|
+
}
|
|
478
|
+
data.tense_mapping = options.tense_mapping;
|
|
479
|
+
}
|
|
480
|
+
if (options?.num_results !== undefined) {
|
|
481
|
+
if (!Number.isInteger(options.num_results)
|
|
482
|
+
|| options.num_results < 1 || options.num_results > 8) {
|
|
483
|
+
throw new TypeError('rephrase: num_results must be an integer from 1 to 8.');
|
|
484
|
+
}
|
|
485
|
+
data.num_results = options.num_results;
|
|
486
|
+
}
|
|
487
|
+
if (options?.session_id !== undefined) {
|
|
488
|
+
if (typeof options.session_id !== 'string') {
|
|
489
|
+
throw new TypeError('rephrase: session_id must be a string.');
|
|
490
|
+
}
|
|
491
|
+
data.session_id = options.session_id;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return this.axiosPromise(url, 'post', data);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Sentiment analysis: positive / neutral / negative, overall and per sentence.
|
|
499
|
+
*
|
|
500
|
+
* Returns `sents` (the sentences found), `overall` (a list of
|
|
501
|
+
* `[probability, label]` pairs, most likely first) and `results` (one such
|
|
502
|
+
* list per sentence, aligned with `sents`). Labels are 'POSITIVE',
|
|
503
|
+
* 'NEUTRAL' or 'NEGATIVE'. A text with no sentences gets a 204 with no body
|
|
504
|
+
* (axios gives `data === ''`), and a text the API cannot split into
|
|
505
|
+
* sentences gets a 200 with `[]`.
|
|
506
|
+
*
|
|
507
|
+
* @param {string} text - The text to analyze, plain text or HTML (tags are
|
|
508
|
+
* stripped server-side; the API caps its length).
|
|
509
|
+
* @returns {Promise<AxiosResponse>}
|
|
510
|
+
*/
|
|
511
|
+
sentiment(text) {
|
|
512
|
+
const url = `${this.urlEndpoint}sentiment`;
|
|
513
|
+
|
|
514
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
515
|
+
throw new TypeError('sentiment: text must be a non-empty string.');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let data = {
|
|
519
|
+
text,
|
|
520
|
+
key: this.apiKey,
|
|
521
|
+
}
|
|
522
|
+
return this.axiosPromise(url, 'post', data);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Readability statistics: character / word / sentence counts, reading time
|
|
527
|
+
* and readability grades.
|
|
528
|
+
*
|
|
529
|
+
* Returns `chars`, `words`, `sentences`, `reading_time_sec`,
|
|
530
|
+
* `reading_time_min`, `readability` (a Flesch-Kincaid grade level, -3.4 when
|
|
531
|
+
* it cannot be scored) and `readability_scores` (a map of formula name to
|
|
532
|
+
* score; which formulas appear depends on `lang`, and it is empty for
|
|
533
|
+
* 'auto'). Unbilled.
|
|
534
|
+
*
|
|
535
|
+
* @param {string} text - The text to measure, plain text or HTML (tags are
|
|
536
|
+
* stripped server-side; the API caps it at 100,000 characters).
|
|
537
|
+
* @param {Object} [options]
|
|
538
|
+
* @param {string} [options.lang] - Language of the text: one of
|
|
539
|
+
* STATISTICS_LANGS ('en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'ru', 'ar')
|
|
540
|
+
* or 'auto'. Defaults to 'en'.
|
|
541
|
+
* @returns {Promise<AxiosResponse>}
|
|
542
|
+
*/
|
|
543
|
+
statistics(text, options = {}) {
|
|
544
|
+
const url = `${this.urlEndpoint}statistics`;
|
|
545
|
+
|
|
546
|
+
// An empty text is a valid request (all-zero counts), so only the type is
|
|
547
|
+
// checked here.
|
|
548
|
+
if (typeof text !== 'string') {
|
|
549
|
+
throw new TypeError('statistics: text must be a string.');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let data = {
|
|
553
|
+
text,
|
|
554
|
+
key: this.apiKey,
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (options?.lang !== undefined) {
|
|
558
|
+
if (options.lang !== 'auto' && !STATISTICS_LANGS.includes(options.lang)) {
|
|
559
|
+
throw new TypeError(
|
|
560
|
+
`statistics: lang must be 'auto' or one of ${STATISTICS_LANGS.join(', ')}.`);
|
|
561
|
+
}
|
|
562
|
+
data.lang = options.lang;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return this.axiosPromise(url, 'post', data);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Similar terms: thesaurus synonyms plus semantically similar terms for a
|
|
570
|
+
* word or short phrase.
|
|
571
|
+
*
|
|
572
|
+
* Returns `neighbors` (up to 5 `[similarity, term]` pairs from embedding
|
|
573
|
+
* similarity, similarity 0–1, most similar first; only alphabetic terms)
|
|
574
|
+
* and `synonyms` (WordNet lemma names — multi-word ones use underscores,
|
|
575
|
+
* e.g. 'domestic_dog'). Unbilled.
|
|
576
|
+
*
|
|
577
|
+
* @param {string} query - The word or phrase to look up (the API uses only
|
|
578
|
+
* its first 100 characters).
|
|
579
|
+
* @returns {Promise<AxiosResponse>}
|
|
580
|
+
*/
|
|
581
|
+
similarTerms(query) {
|
|
582
|
+
const url = `${this.urlEndpoint}thesaurus`;
|
|
583
|
+
|
|
584
|
+
// The API answers a blank query with a 400.
|
|
585
|
+
if (typeof query !== 'string' || query.trim().length === 0) {
|
|
586
|
+
throw new TypeError('similarTerms: query must be a non-empty string.');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let data = {
|
|
590
|
+
query,
|
|
591
|
+
key: this.apiKey,
|
|
592
|
+
}
|
|
593
|
+
return this.axiosPromise(url, 'post', data);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Profanity filter: label each whitespace-separated token of a text.
|
|
598
|
+
*
|
|
599
|
+
* Returns `toks` (the text split on whitespace; punctuation stays attached,
|
|
600
|
+
* e.g. 'stupid.') and `labels` (aligned with `toks`: 1 for profanity, 0
|
|
601
|
+
* otherwise). The text is not HTML-stripped. Unbilled.
|
|
602
|
+
*
|
|
603
|
+
* @param {string} text - The text to check.
|
|
604
|
+
* @returns {Promise<AxiosResponse>}
|
|
605
|
+
*/
|
|
606
|
+
profanity(text) {
|
|
607
|
+
const url = `${this.urlEndpoint}profanity`;
|
|
608
|
+
|
|
609
|
+
if (typeof text !== 'string' || text.trim().length === 0) {
|
|
610
|
+
throw new TypeError('profanity: text must be a non-empty string.');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let data = {
|
|
614
|
+
text,
|
|
615
|
+
key: this.apiKey,
|
|
616
|
+
}
|
|
617
|
+
return this.axiosPromise(url, 'post', data);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* List the custom dictionary of this API key's account: words the grammar
|
|
622
|
+
* and spelling checks accept as correct.
|
|
623
|
+
*
|
|
624
|
+
* Returns a bare array of `{id, entry, case_sensitive, created_at,
|
|
625
|
+
* updated_at}` entries, sorted by entry (timestamps are RFC 1123 strings).
|
|
626
|
+
*
|
|
627
|
+
* @returns {Promise<AxiosResponse>}
|
|
628
|
+
*/
|
|
629
|
+
dictionaryEntries() {
|
|
630
|
+
const url = `${this.urlEndpoint}dictionary`;
|
|
631
|
+
// Not axiosPromise: this is a GET and the key rides the query string,
|
|
632
|
+
// like styleguideRulesets(). The client-level return_usage opt-in is
|
|
633
|
+
// deliberately not forwarded: the response is a bare JSON array, which
|
|
634
|
+
// the API cannot attach `usage` to.
|
|
635
|
+
return axios({
|
|
636
|
+
url,
|
|
637
|
+
method: 'get',
|
|
638
|
+
params: { key: this.apiKey },
|
|
639
|
+
timeout: this.timeout,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Add a word to this API key's account dictionary so the grammar and
|
|
645
|
+
* spelling checks stop flagging it (product names, jargon...).
|
|
646
|
+
*
|
|
647
|
+
* Returns the created `{id, entry, case_sensitive, created_at, updated_at}`
|
|
648
|
+
* entry. The API answers 400 for a duplicate entry ("Entry already
|
|
649
|
+
* exists.") and for an entry with more than two digits.
|
|
650
|
+
*
|
|
651
|
+
* @param {string} entry - The word to add (surrounding whitespace is
|
|
652
|
+
* stripped server-side).
|
|
653
|
+
* @param {Object} [options]
|
|
654
|
+
* @param {boolean} [options.case_sensitive] - Whether matching is case
|
|
655
|
+
* sensitive. Defaults to true.
|
|
656
|
+
* @returns {Promise<AxiosResponse>}
|
|
657
|
+
*/
|
|
658
|
+
addDictionaryEntry(entry, options = {}) {
|
|
659
|
+
const url = `${this.urlEndpoint}dictionary`;
|
|
660
|
+
|
|
661
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
662
|
+
throw new TypeError('addDictionaryEntry: entry must be a non-empty string.');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let data = {
|
|
666
|
+
entry,
|
|
667
|
+
key: this.apiKey,
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (options?.case_sensitive !== undefined) {
|
|
671
|
+
if (typeof options.case_sensitive !== 'boolean') {
|
|
672
|
+
throw new TypeError('addDictionaryEntry: case_sensitive must be a boolean.');
|
|
673
|
+
}
|
|
674
|
+
data.case_sensitive = options.case_sensitive;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return this.axiosPromise(url, 'post', data);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Delete a custom dictionary entry by the `id` dictionaryEntries() or
|
|
682
|
+
* addDictionaryEntry() returned. Answers 204 with no body; an id that is
|
|
683
|
+
* malformed or not this account's gets a 400 (not a 404).
|
|
684
|
+
*
|
|
685
|
+
* @param {string} entryId - The entry's id (a UUID).
|
|
686
|
+
* @returns {Promise<AxiosResponse>}
|
|
687
|
+
*/
|
|
688
|
+
deleteDictionaryEntry(entryId) {
|
|
689
|
+
if (typeof entryId !== 'string' || entryId.trim().length === 0) {
|
|
690
|
+
throw new TypeError('deleteDictionaryEntry: entryId must be a non-empty string.');
|
|
691
|
+
}
|
|
692
|
+
const url = `${this.urlEndpoint}dictionary/${encodeURIComponent(entryId)}`;
|
|
693
|
+
// The key rides the query string, like deleteStyleguideRuleset(): some
|
|
694
|
+
// HTTP stacks drop DELETE bodies. return_usage is deliberately not
|
|
695
|
+
// forwarded: the 204 has no body to attach `usage` to.
|
|
696
|
+
return axios({
|
|
697
|
+
url,
|
|
698
|
+
method: 'delete',
|
|
699
|
+
params: { key: this.apiKey },
|
|
700
|
+
timeout: this.timeout,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* List the custom mappings of this API key's account: rules that make the
|
|
706
|
+
* grammar check suggest a fixed replacement wherever `entry` matches
|
|
707
|
+
* (brand names, preferred terms, banned phrasings).
|
|
708
|
+
*
|
|
709
|
+
* Returns `custom_mappings`: `{id, entry, mapping, description,
|
|
710
|
+
* case_sensitive, created_at, updated_at}` entries sorted by entry
|
|
711
|
+
* (timestamps are RFC 1123 strings).
|
|
712
|
+
*
|
|
713
|
+
* @returns {Promise<AxiosResponse>}
|
|
714
|
+
*/
|
|
715
|
+
customMappings() {
|
|
716
|
+
const url = `${this.urlEndpoint}custom_mapping`;
|
|
717
|
+
// Not axiosPromise (that injects into the POST body): this is a GET, so
|
|
718
|
+
// the key and the client-level return_usage opt-in ride the query
|
|
719
|
+
// string, like styleguideRulesets(). The response is a JSON object, so
|
|
720
|
+
// the API can attach `usage` to it.
|
|
721
|
+
const params = { key: this.apiKey };
|
|
722
|
+
if (this.returnUsage) {
|
|
723
|
+
params.return_usage = true;
|
|
724
|
+
}
|
|
725
|
+
return axios({
|
|
726
|
+
url,
|
|
727
|
+
method: 'get',
|
|
728
|
+
params,
|
|
729
|
+
timeout: this.timeout,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Add a custom mapping to this API key's account: wherever `entry`
|
|
735
|
+
* matches, the grammar check suggests `mapping` instead.
|
|
736
|
+
*
|
|
737
|
+
* `entry` supports two pattern forms: `^` / `$` anchor a match to the
|
|
738
|
+
* start / end of a sentence (e.g. `^the` to enforce capitalization), and
|
|
739
|
+
* `#` matches a number (e.g. `Gate #`); a `#` in `mapping` substitutes the
|
|
740
|
+
* matched number back in.
|
|
741
|
+
*
|
|
742
|
+
* Returns the created `{id, entry, mapping, description, case_sensitive,
|
|
743
|
+
* created_at, updated_at}` entry. The API answers 400 for a duplicate
|
|
744
|
+
* entry ("Entry already exists."), an entry equal to its mapping, an entry
|
|
745
|
+
* with more than two digits, and a case-insensitive rule whose entry and
|
|
746
|
+
* mapping differ only in case (e.g. iphone -> iPhone; mark it case
|
|
747
|
+
* sensitive).
|
|
748
|
+
*
|
|
749
|
+
* @param {string} entry - The text to match (surrounding whitespace is
|
|
750
|
+
* stripped server-side).
|
|
751
|
+
* @param {string} mapping - The replacement to suggest.
|
|
752
|
+
* @param {Object} [options]
|
|
753
|
+
* @param {string} [options.description] - Shown to the end user with the
|
|
754
|
+
* suggestion. A mapping with a description cannot be ignored.
|
|
755
|
+
* @param {boolean} [options.case_sensitive] - Whether matching is case
|
|
756
|
+
* sensitive. Defaults to true; when false, lower and title case both
|
|
757
|
+
* match.
|
|
758
|
+
* @returns {Promise<AxiosResponse>}
|
|
759
|
+
*/
|
|
760
|
+
addCustomMapping(entry, mapping, options = {}) {
|
|
761
|
+
const url = `${this.urlEndpoint}custom_mapping`;
|
|
762
|
+
|
|
763
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
764
|
+
throw new TypeError('addCustomMapping: entry must be a non-empty string.');
|
|
765
|
+
}
|
|
766
|
+
if (typeof mapping !== 'string') {
|
|
767
|
+
throw new TypeError('addCustomMapping: mapping must be a string.');
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
let data = {
|
|
771
|
+
entry,
|
|
772
|
+
mapping,
|
|
773
|
+
key: this.apiKey,
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (options?.description !== undefined) {
|
|
777
|
+
if (typeof options.description !== 'string') {
|
|
778
|
+
throw new TypeError('addCustomMapping: description must be a string.');
|
|
779
|
+
}
|
|
780
|
+
data.description = options.description;
|
|
781
|
+
}
|
|
782
|
+
if (options?.case_sensitive !== undefined) {
|
|
783
|
+
if (typeof options.case_sensitive !== 'boolean') {
|
|
784
|
+
throw new TypeError('addCustomMapping: case_sensitive must be a boolean.');
|
|
785
|
+
}
|
|
786
|
+
data.case_sensitive = options.case_sensitive;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return this.axiosPromise(url, 'post', data);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Delete a custom mapping by the `id` customMappings() or
|
|
794
|
+
* addCustomMapping() returned. Answers 204 with no body; an id that is not
|
|
795
|
+
* this account's gets a 400 (not a 404).
|
|
796
|
+
*
|
|
797
|
+
* @param {string} mappingId - The mapping's id (a UUID).
|
|
798
|
+
* @returns {Promise<AxiosResponse>}
|
|
799
|
+
*/
|
|
800
|
+
deleteCustomMapping(mappingId) {
|
|
801
|
+
if (typeof mappingId !== 'string' || mappingId.trim().length === 0) {
|
|
802
|
+
throw new TypeError('deleteCustomMapping: mappingId must be a non-empty string.');
|
|
803
|
+
}
|
|
804
|
+
const url = `${this.urlEndpoint}custom_mapping/${encodeURIComponent(mappingId)}`;
|
|
805
|
+
// Key in the query string, like deleteDictionaryEntry(): some HTTP
|
|
806
|
+
// stacks drop DELETE bodies, and the 204 has no body to attach `usage`
|
|
807
|
+
// to, so return_usage is not forwarded.
|
|
808
|
+
return axios({
|
|
809
|
+
url,
|
|
810
|
+
method: 'delete',
|
|
811
|
+
params: { key: this.apiKey },
|
|
812
|
+
timeout: this.timeout,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* List the custom filters of this API key's account: rules that suppress
|
|
818
|
+
* grammar-check suggestions rewriting `input` to `output`.
|
|
819
|
+
*
|
|
820
|
+
* Returns `custom_filters`: `{id, input, output, case_sensitive,
|
|
821
|
+
* created_at, updated_at}` entries sorted by input. An empty `input` or
|
|
822
|
+
* `output` is a wildcard.
|
|
823
|
+
*
|
|
824
|
+
* @returns {Promise<AxiosResponse>}
|
|
825
|
+
*/
|
|
826
|
+
customFilters() {
|
|
827
|
+
const url = `${this.urlEndpoint}custom_filter`;
|
|
828
|
+
// GET: key and return_usage ride the query string, like customMappings().
|
|
829
|
+
const params = { key: this.apiKey };
|
|
830
|
+
if (this.returnUsage) {
|
|
831
|
+
params.return_usage = true;
|
|
832
|
+
}
|
|
833
|
+
return axios({
|
|
834
|
+
url,
|
|
835
|
+
method: 'get',
|
|
836
|
+
params,
|
|
837
|
+
timeout: this.timeout,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Add a custom filter to this API key's account, so the grammar check
|
|
843
|
+
* stops suggesting edits that rewrite `input` to `output`. Pass an empty
|
|
844
|
+
* string for either side as a wildcard: `('Patient', '')` suppresses every
|
|
845
|
+
* edit of "Patient"; `('', 'The patient')` every edit that produces "The
|
|
846
|
+
* patient".
|
|
847
|
+
*
|
|
848
|
+
* Returns the created `{id, input, output, case_sensitive, created_at,
|
|
849
|
+
* updated_at}` filter. The API answers 400 when both sides are blank
|
|
850
|
+
* after trimming, when they are equal, for a duplicate filter ("Filter
|
|
851
|
+
* already exists."), an input with more than two digits, and a
|
|
852
|
+
* case-insensitive filter whose sides differ only in case.
|
|
853
|
+
*
|
|
854
|
+
* @param {string} input - The original text of the edits to suppress, or
|
|
855
|
+
* '' for any (surrounding whitespace is stripped server-side).
|
|
856
|
+
* @param {string} output - The replacement text of the edits to suppress,
|
|
857
|
+
* or '' for any.
|
|
858
|
+
* @param {Object} [options]
|
|
859
|
+
* @param {boolean} [options.case_sensitive] - Whether matching is case
|
|
860
|
+
* sensitive. Defaults to true.
|
|
861
|
+
* @returns {Promise<AxiosResponse>}
|
|
862
|
+
*/
|
|
863
|
+
addCustomFilter(input, output, options = {}) {
|
|
864
|
+
const url = `${this.urlEndpoint}custom_filter`;
|
|
865
|
+
|
|
866
|
+
if (typeof input !== 'string' || typeof output !== 'string') {
|
|
867
|
+
throw new TypeError('addCustomFilter: input and output must be strings.');
|
|
868
|
+
}
|
|
869
|
+
if (input.trim().length === 0 && output.trim().length === 0) {
|
|
870
|
+
throw new TypeError('addCustomFilter: input and output cannot both be blank.');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// The API's field names are `inp` / `out`; the response says
|
|
874
|
+
// `input` / `output`.
|
|
875
|
+
let data = {
|
|
876
|
+
inp: input,
|
|
877
|
+
out: output,
|
|
878
|
+
key: this.apiKey,
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (options?.case_sensitive !== undefined) {
|
|
882
|
+
if (typeof options.case_sensitive !== 'boolean') {
|
|
883
|
+
throw new TypeError('addCustomFilter: case_sensitive must be a boolean.');
|
|
884
|
+
}
|
|
885
|
+
data.case_sensitive = options.case_sensitive;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return this.axiosPromise(url, 'post', data);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Delete a custom filter by the `id` customFilters() or addCustomFilter()
|
|
893
|
+
* returned. Answers 204 with no body; an id that is malformed or not this
|
|
894
|
+
* account's gets a 400 (not a 404).
|
|
895
|
+
*
|
|
896
|
+
* @param {string} filterId - The filter's id (a UUID).
|
|
897
|
+
* @returns {Promise<AxiosResponse>}
|
|
898
|
+
*/
|
|
899
|
+
deleteCustomFilter(filterId) {
|
|
900
|
+
if (typeof filterId !== 'string' || filterId.trim().length === 0) {
|
|
901
|
+
throw new TypeError('deleteCustomFilter: filterId must be a non-empty string.');
|
|
902
|
+
}
|
|
903
|
+
const url = `${this.urlEndpoint}custom_filter/${encodeURIComponent(filterId)}`;
|
|
904
|
+
// Key in the query string and no return_usage, like deleteCustomMapping().
|
|
905
|
+
return axios({
|
|
906
|
+
url,
|
|
907
|
+
method: 'delete',
|
|
908
|
+
params: { key: this.apiKey },
|
|
909
|
+
timeout: this.timeout,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
240
913
|
/**
|
|
241
914
|
* Structured data extraction: pull caller-named fields out of unstructured text.
|
|
242
915
|
*
|
|
@@ -292,53 +965,7 @@ export class Client {
|
|
|
292
965
|
throw new TypeError('extract: text must be a non-empty string.');
|
|
293
966
|
}
|
|
294
967
|
|
|
295
|
-
|
|
296
|
-
if (typeof field === 'string') {
|
|
297
|
-
return field.trim().length > 0;
|
|
298
|
-
}
|
|
299
|
-
if (field == null || typeof field !== 'object' || Array.isArray(field)) {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
if (typeof field.name !== 'string' || field.name.trim().length === 0) {
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
if (field.type !== undefined && !EXTRACT_FIELD_TYPES.includes(field.type)) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
if (field.description !== undefined && typeof field.description !== 'string') {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
if (field.required !== undefined && typeof field.required !== 'boolean') {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
// Reject unknown keys so a typo like `{ name, typ: 'number' }` fails here
|
|
315
|
-
// rather than being forwarded to the API as a malformed field (the MCP
|
|
316
|
-
// schema is `.strict()` for the same reason).
|
|
317
|
-
const allowed = new Set(['name', 'type', 'description', 'required']);
|
|
318
|
-
if (Object.keys(field).some((key) => !allowed.has(key))) {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
!Array.isArray(fields)
|
|
326
|
-
|| fields.length < 1
|
|
327
|
-
|| fields.length > 20
|
|
328
|
-
// Array.from() materializes holes in sparse arrays as `undefined` (which
|
|
329
|
-
// isValidField rejects); a bare `.every()` would skip them and let a hole
|
|
330
|
-
// through to be serialized as `null`.
|
|
331
|
-
|| !Array.from(fields).every(isValidField)
|
|
332
|
-
) {
|
|
333
|
-
throw new TypeError('extract: fields must be an array of 1 to 20 non-empty strings or '
|
|
334
|
-
+ '{name, type?, description?, required?} objects (string name, type one of '
|
|
335
|
-
+ `${EXTRACT_FIELD_TYPES.join(', ')}, optional string description, optional boolean required).`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const names = fields.map((field) => (typeof field === 'string' ? field : field.name).toLowerCase());
|
|
339
|
-
if (names.length !== new Set(names).size) {
|
|
340
|
-
throw new TypeError('extract: field names must be unique (case-insensitive).');
|
|
341
|
-
}
|
|
968
|
+
validateExtractFields(fields, 'extract');
|
|
342
969
|
|
|
343
970
|
let data = {
|
|
344
971
|
...(isBatch ? { texts: text } : { text }),
|
|
@@ -359,6 +986,48 @@ export class Client {
|
|
|
359
986
|
return this.axiosPromise(url, 'post', data);
|
|
360
987
|
}
|
|
361
988
|
|
|
989
|
+
/**
|
|
990
|
+
* Extract caller-named fields from an uploaded document
|
|
991
|
+
* (POST /api/v1/file-extract): the same extraction as extract(), with the text
|
|
992
|
+
* read server-side from a .pdf, .docx, .pptx, .odt, .txt, .md, .html or .htm
|
|
993
|
+
* file of up to 30MB.
|
|
994
|
+
*
|
|
995
|
+
* Only the first 10,000 characters of the document's text are read; the
|
|
996
|
+
* response's `document` block (`file_type`, `characters`, `characters_used`,
|
|
997
|
+
* `truncated`) says how much was used, and `characters_used` is what is billed.
|
|
998
|
+
*
|
|
999
|
+
* @param {Blob|File|Buffer|Uint8Array|ArrayBuffer} file - The document.
|
|
1000
|
+
* @param {Array<string|object>} fields - Same as extract().
|
|
1001
|
+
* @param {object} [options]
|
|
1002
|
+
* @param {string} [options.filename] - Required unless `file` is a File: the
|
|
1003
|
+
* format is chosen by its extension (e.g. 'invoice.pdf').
|
|
1004
|
+
* @param {string} [options.context] - Same as extract().
|
|
1005
|
+
*/
|
|
1006
|
+
fileExtract(file, fields, options = {}) {
|
|
1007
|
+
const url = `${this.urlEndpoint}file-extract`;
|
|
1008
|
+
|
|
1009
|
+
validateExtractFields(fields, 'fileExtract');
|
|
1010
|
+
|
|
1011
|
+
let params = {
|
|
1012
|
+
key: this.apiKey,
|
|
1013
|
+
fields,
|
|
1014
|
+
};
|
|
1015
|
+
if (options?.context !== undefined) {
|
|
1016
|
+
if (typeof options.context !== 'string') {
|
|
1017
|
+
throw new TypeError('fileExtract: context must be a string.');
|
|
1018
|
+
}
|
|
1019
|
+
params.context = options.context;
|
|
1020
|
+
}
|
|
1021
|
+
// axiosPromise only injects return_usage into plain-object bodies, so the
|
|
1022
|
+
// client-level flag rides the jsonParams part here.
|
|
1023
|
+
if (this.returnUsage) {
|
|
1024
|
+
params.return_usage = true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const form = buildUploadForm(file, options?.filename, params, 'fileExtract');
|
|
1028
|
+
return this.axiosPromise(url, 'post', form);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
362
1031
|
/**
|
|
363
1032
|
* Translate text into a target language with an LLM.
|
|
364
1033
|
*
|
|
@@ -508,6 +1177,44 @@ export class Client {
|
|
|
508
1177
|
return this.axiosPromise(url, 'post', data);
|
|
509
1178
|
}
|
|
510
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* Summarize an uploaded document (POST /api/v1/file-summarize): the same
|
|
1182
|
+
* summary as summarize(), with the text read server-side from a .pdf, .docx,
|
|
1183
|
+
* .pptx, .odt, .txt, .md, .html or .htm file of up to 30MB.
|
|
1184
|
+
*
|
|
1185
|
+
* Only the first 20,000 characters of the document's text are read; the
|
|
1186
|
+
* response's `document` block (`file_type`, `characters`, `characters_used`,
|
|
1187
|
+
* `truncated`) says how much was used, and `characters_used` is what is billed.
|
|
1188
|
+
*
|
|
1189
|
+
* @param {Blob|File|Buffer|Uint8Array|ArrayBuffer} file - The document.
|
|
1190
|
+
* @param {object} [options]
|
|
1191
|
+
* @param {string} [options.filename] - Required unless `file` is a File: the
|
|
1192
|
+
* format is chosen by its extension (e.g. 'report.pdf').
|
|
1193
|
+
* @param {string} [options.length] - Same as summarize().
|
|
1194
|
+
*/
|
|
1195
|
+
fileSummarize(file, options = {}) {
|
|
1196
|
+
const url = `${this.urlEndpoint}file-summarize`;
|
|
1197
|
+
|
|
1198
|
+
let params = {
|
|
1199
|
+
key: this.apiKey,
|
|
1200
|
+
};
|
|
1201
|
+
if (options?.length !== undefined) {
|
|
1202
|
+
if (!SUMMARIZE_LENGTHS.includes(options.length)) {
|
|
1203
|
+
throw new TypeError('fileSummarize: length must be one of '
|
|
1204
|
+
+ `${SUMMARIZE_LENGTHS.join(', ')}.`);
|
|
1205
|
+
}
|
|
1206
|
+
params.length = options.length;
|
|
1207
|
+
}
|
|
1208
|
+
// axiosPromise only injects return_usage into plain-object bodies, so the
|
|
1209
|
+
// client-level flag rides the jsonParams part here.
|
|
1210
|
+
if (this.returnUsage) {
|
|
1211
|
+
params.return_usage = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const form = buildUploadForm(file, options?.filename, params, 'fileSummarize');
|
|
1215
|
+
return this.axiosPromise(url, 'post', form);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
511
1218
|
langdetect(text, options = {}) {
|
|
512
1219
|
const url = `${this.urlEndpoint}langdetect`;
|
|
513
1220
|
|
|
@@ -866,9 +1573,134 @@ export class Client {
|
|
|
866
1573
|
if (options.sent_scores != null) {
|
|
867
1574
|
data.sent_scores = options.sent_scores;
|
|
868
1575
|
}
|
|
1576
|
+
if (options.score_string !== undefined) {
|
|
1577
|
+
if (typeof options.score_string !== 'boolean') {
|
|
1578
|
+
throw new TypeError('aidetect: score_string must be a boolean.');
|
|
1579
|
+
}
|
|
1580
|
+
data.score_string = options.score_string;
|
|
1581
|
+
}
|
|
1582
|
+
if (options.version !== undefined) {
|
|
1583
|
+
if (typeof options.version !== 'string') {
|
|
1584
|
+
throw new TypeError('aidetect: version must be a string.');
|
|
1585
|
+
}
|
|
1586
|
+
data.version = options.version;
|
|
1587
|
+
}
|
|
1588
|
+
// The supported codes are server config (the language routes), so only
|
|
1589
|
+
// the type is checked here; an unsupported code gets the API's 400.
|
|
1590
|
+
if (options.lang !== undefined) {
|
|
1591
|
+
if (typeof options.lang !== 'string') {
|
|
1592
|
+
throw new TypeError('aidetect: lang must be a string.');
|
|
1593
|
+
}
|
|
1594
|
+
data.lang = options.lang;
|
|
1595
|
+
}
|
|
1596
|
+
if (options.zero_retention !== undefined) {
|
|
1597
|
+
if (typeof options.zero_retention !== 'boolean') {
|
|
1598
|
+
throw new TypeError('aidetect: zero_retention must be a boolean.');
|
|
1599
|
+
}
|
|
1600
|
+
data.zero_retention = options.zero_retention;
|
|
1601
|
+
}
|
|
1602
|
+
// Single text only, and not with zero_retention or a public key: the
|
|
1603
|
+
// API 400s those before scoring, so only the type is checked here.
|
|
1604
|
+
if (options.create_report !== undefined) {
|
|
1605
|
+
if (typeof options.create_report !== 'boolean') {
|
|
1606
|
+
throw new TypeError('aidetect: create_report must be a boolean.');
|
|
1607
|
+
}
|
|
1608
|
+
data.create_report = options.create_report;
|
|
1609
|
+
}
|
|
869
1610
|
|
|
870
1611
|
return this.axiosPromise(url, 'post', data);
|
|
871
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Score an uploaded document for AI-generated text (POST /api/v1/file-aidetect).
|
|
1615
|
+
*
|
|
1616
|
+
* The API reads the text of a .pdf, .docx, .pptx, .odt, .txt, .md, .html or
|
|
1617
|
+
* .htm file of up to 30MB and scores its first 200,000 characters like
|
|
1618
|
+
* aidetect() with sentence scores and score_string enabled. The language is
|
|
1619
|
+
* detected and routed automatically (no lang/version option). Billed like
|
|
1620
|
+
* aidetect().
|
|
1621
|
+
*
|
|
1622
|
+
* @param {Blob|File|Buffer|Uint8Array|ArrayBuffer} file - The document.
|
|
1623
|
+
* @param {object} [options]
|
|
1624
|
+
* @param {string} [options.filename] - Required unless `file` is a File: the
|
|
1625
|
+
* format is chosen by its extension (e.g. 'essay.docx').
|
|
1626
|
+
* @param {boolean} [options.zero_retention] - As for aidetect(): keep no
|
|
1627
|
+
* copy of the extracted text.
|
|
1628
|
+
* @param {boolean} [options.create_report] - As for aidetect(): save the
|
|
1629
|
+
* result as a shareable report named after the file and return `report`.
|
|
1630
|
+
*/
|
|
1631
|
+
fileAidetect(file, options = {}) {
|
|
1632
|
+
const url = `${this.urlEndpoint}file-aidetect`;
|
|
1633
|
+
|
|
1634
|
+
let params = { key: this.apiKey };
|
|
1635
|
+
if (this.returnUsage) {
|
|
1636
|
+
params.return_usage = true;
|
|
1637
|
+
}
|
|
1638
|
+
if (options?.zero_retention !== undefined) {
|
|
1639
|
+
if (typeof options.zero_retention !== 'boolean') {
|
|
1640
|
+
throw new TypeError('fileAidetect: zero_retention must be a boolean.');
|
|
1641
|
+
}
|
|
1642
|
+
params.zero_retention = options.zero_retention;
|
|
1643
|
+
}
|
|
1644
|
+
if (options?.create_report !== undefined) {
|
|
1645
|
+
if (typeof options.create_report !== 'boolean') {
|
|
1646
|
+
throw new TypeError('fileAidetect: create_report must be a boolean.');
|
|
1647
|
+
}
|
|
1648
|
+
params.create_report = options.create_report;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const form = buildUploadForm(file, options?.filename, params, 'fileAidetect');
|
|
1652
|
+
return this.axiosPromise(url, 'post', form);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Score a public web page for AI-generated text (POST /api/v1/url-aidetect).
|
|
1657
|
+
*
|
|
1658
|
+
* The API fetches the URL and scores the page's main readable text
|
|
1659
|
+
* (navigation, headers, footers, forms and hidden elements dropped; the
|
|
1660
|
+
* largest <article>, else <main>, preferred) like aidetect(). A linked
|
|
1661
|
+
* .pdf, .docx, .pptx, .odt, .txt or .md document is read like
|
|
1662
|
+
* fileAidetect(). The response adds `source` ({url, title, content_type,
|
|
1663
|
+
* characters, truncated}). Billed like aidetect() on the characters scored.
|
|
1664
|
+
*
|
|
1665
|
+
* @param {string} url - http(s) URL; a bare host/path gets https://.
|
|
1666
|
+
* @param {object} [options] - sent_scores, score_string, version, lang,
|
|
1667
|
+
* zero_retention, create_report: as for aidetect() (a report is named
|
|
1668
|
+
* after the final URL).
|
|
1669
|
+
*/
|
|
1670
|
+
urlAidetect(url, options = {}) {
|
|
1671
|
+
const endpoint = `${this.urlEndpoint}url-aidetect`;
|
|
1672
|
+
|
|
1673
|
+
// Scheme, length and address checks are the server's (it 400s with a
|
|
1674
|
+
// clear message); only the type is checked here.
|
|
1675
|
+
if (typeof url !== 'string' || url.trim().length === 0) {
|
|
1676
|
+
throw new TypeError('urlAidetect: url must be a non-empty string.');
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
let data = {
|
|
1680
|
+
url,
|
|
1681
|
+
key: this.apiKey,
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
for (const name of ['sent_scores', 'score_string', 'zero_retention', 'create_report']) {
|
|
1685
|
+
if (options?.[name] !== undefined) {
|
|
1686
|
+
if (typeof options[name] !== 'boolean') {
|
|
1687
|
+
throw new TypeError(`urlAidetect: ${name} must be a boolean.`);
|
|
1688
|
+
}
|
|
1689
|
+
data[name] = options[name];
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const name of ['version', 'lang']) {
|
|
1693
|
+
if (options?.[name] !== undefined) {
|
|
1694
|
+
if (typeof options[name] !== 'string') {
|
|
1695
|
+
throw new TypeError(`urlAidetect: ${name} must be a string.`);
|
|
1696
|
+
}
|
|
1697
|
+
data[name] = options[name];
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
return this.axiosPromise(endpoint, 'post', data);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
872
1704
|
|
|
873
1705
|
/**
|
|
874
1706
|
* Check a text for compliance with style rules you supply (house style,
|
|
@@ -957,11 +1789,17 @@ export class Client {
|
|
|
957
1789
|
*/
|
|
958
1790
|
styleguideRulesets() {
|
|
959
1791
|
const url = `${this.urlEndpoint}styleguide/rulesets`;
|
|
960
|
-
// Not axiosPromise
|
|
1792
|
+
// Not axiosPromise (that injects into the POST body): this is a GET, so
|
|
1793
|
+
// the key — and the client-level return_usage opt-in — ride the query
|
|
1794
|
+
// string, keeping usage reporting consistent with the sibling POST/DELETE.
|
|
1795
|
+
const params = { key: this.apiKey };
|
|
1796
|
+
if (this.returnUsage) {
|
|
1797
|
+
params.return_usage = true;
|
|
1798
|
+
}
|
|
961
1799
|
return axios({
|
|
962
1800
|
url,
|
|
963
1801
|
method: 'get',
|
|
964
|
-
params
|
|
1802
|
+
params,
|
|
965
1803
|
timeout: this.timeout,
|
|
966
1804
|
});
|
|
967
1805
|
}
|
|
@@ -1011,11 +1849,17 @@ export class Client {
|
|
|
1011
1849
|
throw new TypeError('deleteStyleguideRuleset: name must be a non-empty string.');
|
|
1012
1850
|
}
|
|
1013
1851
|
// The name rides the query string: some HTTP stacks drop DELETE bodies,
|
|
1014
|
-
// and the API accepts the query form for exactly that reason.
|
|
1852
|
+
// and the API accepts the query form for exactly that reason. The
|
|
1853
|
+
// client-level return_usage opt-in rides it too (axiosPromise only
|
|
1854
|
+
// injects into POST bodies), keeping usage reporting consistent.
|
|
1855
|
+
const params = { key: this.apiKey, name };
|
|
1856
|
+
if (this.returnUsage) {
|
|
1857
|
+
params.return_usage = true;
|
|
1858
|
+
}
|
|
1015
1859
|
return axios({
|
|
1016
1860
|
url,
|
|
1017
1861
|
method: 'delete',
|
|
1018
|
-
params
|
|
1862
|
+
params,
|
|
1019
1863
|
timeout: this.timeout,
|
|
1020
1864
|
});
|
|
1021
1865
|
}
|
|
@@ -1255,7 +2099,10 @@ export class Client {
|
|
|
1255
2099
|
accountStatus() {
|
|
1256
2100
|
const url = `${this.urlEndpoint}account/status`;
|
|
1257
2101
|
// Not axiosPromise: this is a GET and the key rides the query string,
|
|
1258
|
-
// like styleguideRulesets().
|
|
2102
|
+
// like styleguideRulesets(). The client-level return_usage opt-in is
|
|
2103
|
+
// deliberately not forwarded: the status check is unbilled and the
|
|
2104
|
+
// response already has its own month-to-date `usage` object, which the
|
|
2105
|
+
// API leaves unchanged.
|
|
1259
2106
|
return axios({
|
|
1260
2107
|
url,
|
|
1261
2108
|
method: 'get',
|
|
@@ -1263,4 +2110,174 @@ export class Client {
|
|
|
1263
2110
|
timeout: this.timeout,
|
|
1264
2111
|
});
|
|
1265
2112
|
}
|
|
2113
|
+
|
|
2114
|
+
/**
|
|
2115
|
+
* Usage and cost report for the account that owns this API key (or, for a
|
|
2116
|
+
* team-scoped key, the team): characters and list-price cost over a window,
|
|
2117
|
+
* broken down by key, endpoint, day and month. Keys are identified by
|
|
2118
|
+
* masked ids (`****<last4>-<hash8>`), never raw. Unbilled.
|
|
2119
|
+
*
|
|
2120
|
+
* Returns `start_date`, `end_date`, `total_characters`, `total_cost_usd`,
|
|
2121
|
+
* `currency`, `by_api_key`, `by_endpoint`, `daily_usage`, `monthly_usage`,
|
|
2122
|
+
* `last_updated` and, for keys that are not team-scoped,
|
|
2123
|
+
* `credit_balance_data` (the same object as accountStatus()'s `credits`).
|
|
2124
|
+
* The API answers 400 when start_days_back < end_days_back and 403 for a
|
|
2125
|
+
* window over a year; the window is clamped to the account's age.
|
|
2126
|
+
*
|
|
2127
|
+
* @param {Object} [options]
|
|
2128
|
+
* @param {number} [options.start_days_back] - Days before today the window
|
|
2129
|
+
* starts. Defaults to the first of the current month.
|
|
2130
|
+
* @param {number} [options.end_days_back] - Days before today the window
|
|
2131
|
+
* ends (inclusive). Defaults to 0, today.
|
|
2132
|
+
* @returns {Promise<AxiosResponse>}
|
|
2133
|
+
*/
|
|
2134
|
+
apiUsage(options = {}) {
|
|
2135
|
+
const url = `${this.urlEndpoint}reporting/api_usage`;
|
|
2136
|
+
const data = reportingWindowBody(this.apiKey, options, 'apiUsage');
|
|
2137
|
+
return this.axiosPromise(url, 'post', data);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Characters per endpoint over a window, bucketed by day, week or month.
|
|
2142
|
+
* `data` maps an endpoint name (e.g. `EDIT`) to `{<days back>: characters}`,
|
|
2143
|
+
* where each key is the bucket's start as days before today (a string, as
|
|
2144
|
+
* in all JSON object keys). `last_updated` is an ISO timestamp or null.
|
|
2145
|
+
* Unbilled.
|
|
2146
|
+
*
|
|
2147
|
+
* @param {Object} [options]
|
|
2148
|
+
* @param {number} [options.start_days_back] - Days before today the window
|
|
2149
|
+
* starts. Defaults to 6.
|
|
2150
|
+
* @param {number} [options.end_days_back] - Days before today the window
|
|
2151
|
+
* ends. Defaults to 0.
|
|
2152
|
+
* @param {'Daily'|'Weekly'|'Monthly'} [options.interval] - Bucket size.
|
|
2153
|
+
* Defaults to 'Daily'.
|
|
2154
|
+
* @returns {Promise<AxiosResponse>}
|
|
2155
|
+
*/
|
|
2156
|
+
apiEndpointUsage(options = {}) {
|
|
2157
|
+
const url = `${this.urlEndpoint}reporting/api_endpoint_usage`;
|
|
2158
|
+
const data = reportingWindowBody(this.apiKey, options, 'apiEndpointUsage');
|
|
2159
|
+
if (options?.interval !== undefined) {
|
|
2160
|
+
if (!REPORTING_INTERVALS.includes(options.interval)) {
|
|
2161
|
+
throw new TypeError(
|
|
2162
|
+
`apiEndpointUsage: interval must be one of ${REPORTING_INTERVALS.join(', ')}.`);
|
|
2163
|
+
}
|
|
2164
|
+
data.interval = options.interval;
|
|
2165
|
+
}
|
|
2166
|
+
return this.axiosPromise(url, 'post', data);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/**
|
|
2170
|
+
* Monthly quota and characters used over a window. Returns `quota` (the
|
|
2171
|
+
* monthly character limit, -1 for none), `usage` (characters in the
|
|
2172
|
+
* window), `accepts`/`ignores` (suggestions accepted or ignored through
|
|
2173
|
+
* acceptEdit()/rejectEdit() since the first of the current month, whatever
|
|
2174
|
+
* the window) and `last_updated` (Unix epoch seconds). Unbilled.
|
|
2175
|
+
*
|
|
2176
|
+
* The API reads a start_days_back of 0 as 1 (its default), so the shortest
|
|
2177
|
+
* window it reports is yesterday and today.
|
|
2178
|
+
*
|
|
2179
|
+
* @param {Object} [options]
|
|
2180
|
+
* @param {number} [options.start_days_back] - Days before today the window
|
|
2181
|
+
* starts. Defaults to 1.
|
|
2182
|
+
* @param {number} [options.end_days_back] - Days before today the window
|
|
2183
|
+
* ends. Defaults to 0.
|
|
2184
|
+
* @returns {Promise<AxiosResponse>}
|
|
2185
|
+
*/
|
|
2186
|
+
apiQuotaUsage(options = {}) {
|
|
2187
|
+
const url = `${this.urlEndpoint}reporting/api_quota_usage`;
|
|
2188
|
+
const data = reportingWindowBody(this.apiKey, options, 'apiQuotaUsage');
|
|
2189
|
+
// Not axiosPromise: the client-level return_usage opt-in is deliberately
|
|
2190
|
+
// not forwarded, like accountStatus(). The response's own `usage` is an
|
|
2191
|
+
// integer, which the API leaves unchanged, so the flag would do nothing.
|
|
2192
|
+
return axios({
|
|
2193
|
+
url,
|
|
2194
|
+
method: 'post',
|
|
2195
|
+
data,
|
|
2196
|
+
timeout: this.timeout,
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Suggestion activity and active users over a window: `edits_shown`,
|
|
2202
|
+
* `edits_accepted`, `edits_ignored` and `active_users` ({logged_in,
|
|
2203
|
+
* anonymous}). Accepts and ignores are only counted when your integration
|
|
2204
|
+
* reports them with acceptEdit()/rejectEdit(). Unbilled.
|
|
2205
|
+
*
|
|
2206
|
+
* @param {Object} [options]
|
|
2207
|
+
* @param {number} [options.start_days_back] - Days before today the window
|
|
2208
|
+
* starts. Defaults to 6.
|
|
2209
|
+
* @param {number} [options.end_days_back] - Days before today the window
|
|
2210
|
+
* ends. Defaults to 0.
|
|
2211
|
+
* @returns {Promise<AxiosResponse>}
|
|
2212
|
+
*/
|
|
2213
|
+
apiUserActivity(options = {}) {
|
|
2214
|
+
const url = `${this.urlEndpoint}reporting/api_user_activity`;
|
|
2215
|
+
const data = reportingWindowBody(this.apiKey, options, 'apiUserActivity');
|
|
2216
|
+
return this.axiosPromise(url, 'post', data);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
* Inclusive-language check: finds non-inclusive or biased wording
|
|
2221
|
+
* (gendered defaults, ableist idioms, ageist framing, outdated terms for
|
|
2222
|
+
* groups, classist or appearance-based language). Each issue carries the
|
|
2223
|
+
* verbatim passage, start/end offsets into the exact text submitted (null
|
|
2224
|
+
* when the passage can't be located), a category, a severity
|
|
2225
|
+
* (low / medium / high), a short note and a neutral replacement; the
|
|
2226
|
+
* response's `inclusive` is true when nothing was flagged. Wording is
|
|
2227
|
+
* flagged, not topics: text that discusses or quotes a term to critique
|
|
2228
|
+
* it is not an issue.
|
|
2229
|
+
*
|
|
2230
|
+
* @param {string|string[]} text - The text to check, or an array of 1–10
|
|
2231
|
+
* texts for the batch form (sent as the API's `texts` parameter; combined
|
|
2232
|
+
* length up to 10,000 characters, same categories applied to every item).
|
|
2233
|
+
* A batch response is `{results: [...]}` with one single-response-shaped
|
|
2234
|
+
* entry per input, in order.
|
|
2235
|
+
* @param {Object} [options]
|
|
2236
|
+
* @param {string[]} [options.categories] - Categories to check (gender,
|
|
2237
|
+
* race_ethnicity, disability, age, lgbtq, religion, socioeconomic,
|
|
2238
|
+
* appearance). Default: all of them.
|
|
2239
|
+
*/
|
|
2240
|
+
inclusive(text, options = {}) {
|
|
2241
|
+
const url = `${this.urlEndpoint}inclusive`;
|
|
2242
|
+
|
|
2243
|
+
// Shape/type checks only; the set of valid categories is server
|
|
2244
|
+
// configuration and enforced there with a clear 400 (see ner()).
|
|
2245
|
+
const isBatch = Array.isArray(text);
|
|
2246
|
+
if (isBatch) {
|
|
2247
|
+
// for...of (not text.every) so holes in a sparse array are visited as
|
|
2248
|
+
// undefined and rejected, rather than skipped.
|
|
2249
|
+
let ok = text.length >= 1 && text.length <= 10;
|
|
2250
|
+
if (ok) {
|
|
2251
|
+
for (const item of text) {
|
|
2252
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
2253
|
+
ok = false;
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (!ok) {
|
|
2259
|
+
throw new TypeError('inclusive: texts must be an array of 1 to 10 non-empty strings.');
|
|
2260
|
+
}
|
|
2261
|
+
} else if (typeof text !== 'string' || text.trim().length === 0) {
|
|
2262
|
+
throw new TypeError('inclusive: text must be a non-empty string.');
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
let data = {
|
|
2266
|
+
...(isBatch ? { texts: text } : { text }),
|
|
2267
|
+
key: this.apiKey,
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// `!== undefined` so an explicit `null` is rejected rather than treated
|
|
2271
|
+
// as omitted; an empty list is rejected too (the API and MCP schema both
|
|
2272
|
+
// require at least one category), matching ner()'s types.
|
|
2273
|
+
if (options?.categories !== undefined) {
|
|
2274
|
+
if (!Array.isArray(options.categories) || options.categories.length === 0
|
|
2275
|
+
|| !Array.from(options.categories).every((category) => typeof category === 'string' && category.trim().length > 0)) {
|
|
2276
|
+
throw new TypeError('inclusive: categories must be a non-empty array of non-empty strings.');
|
|
2277
|
+
}
|
|
2278
|
+
data.categories = options.categories;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
return this.axiosPromise(url, 'post', data);
|
|
2282
|
+
}
|
|
1266
2283
|
}
|