@saplingai/sapling-js 1.0.54 → 1.0.55

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
@@ -144,12 +144,12 @@ numbered lines); show those results hedged rather than as a bare score. `sentenc
144
144
  same model and 0.5 threshold, and their `start`/`end` (like a segment's) index `text` in Unicode code points (`end` exclusive) — slice with
145
145
  `Array.from(text).slice(start, end).join('')` if the text may contain emoji. A response may also
146
146
  carry an advisory `language` (`{ code, name, confidence }`) when the text is confidently
147
- identified. English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese are
148
- supported: unpinned text detected as `es`, `pt`, `fr`, `de`, `it`, `nl` or `ja` is scored by a
149
- multilingual model and answers `version: 'ml20261009'` (that model has no Italian or Dutch training
150
- data yet, so it catches somewhat less AI-generated text in those two), while other languages are
151
- scored by the English model and are less reliable. Options: `sent_scores` (default true), `score_string` (HTML
152
- heatmap), `lang` (`'en'`, `'es'`, `'pt'`, `'fr'`, `'de'`, `'it'`, `'nl'` or `'ja'`, case-insensitive, region
147
+ identified. English, Arabic, Chinese, Dutch, French, German, Italian, Japanese, Korean, Polish,
148
+ Portuguese, Romanian, Russian, Spanish, Swedish and Turkish are supported: unpinned text detected as `ar`,
149
+ `de`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `ro`, `ru`, `sv`, `tr` or `zh` is scored by a
150
+ multilingual model (trained on all fifteen) and answers `version: 'ml20261104'`, while other
151
+ languages are scored by the English model and are less reliable. Options: `sent_scores` (default true), `score_string` (HTML
152
+ heatmap), `lang` (`'en'`, `'ar'`, `'de'`, `'es'`, `'fr'`, `'it'`, `'ja'`, `'ko'`, `'nl'`, `'pl'`, `'pt'`, `'ro'`, `'ru'`, `'sv'`, `'tr'` or `'zh'`, case-insensitive, region
153
153
  subtags such as `'pt-BR'` accepted: use that language's model instead of detecting it, which helps
154
154
  with short texts; omit it or pass `'auto'` to detect; other languages get a 400), `version` (pin
155
155
  a model; a pin always overrides the language routing and `lang`). `language` still reports the
@@ -171,7 +171,7 @@ still scored and billed. `fileAidetect()` and `urlAidetect()` take it too, namin
171
171
  the file or the final URL.
172
172
 
173
173
  ```javascript
174
- const { data } = await client.aidetect(reviewText, { lang: 'pt-BR' }); // version: 'ml20261009'
174
+ const { data } = await client.aidetect(reviewText, { lang: 'pt-BR' }); // version: 'ml20261104'
175
175
  ```
176
176
 
177
177
  To score a document without extracting its text yourself, upload it with `fileAidetect()`. It
@@ -208,6 +208,27 @@ const { data } = await client.urlAidetect('https://example.com/blog/post', { sen
208
208
  // characters: 5120, truncated: false } }
209
209
  ```
210
210
 
211
+ ### Resume and job description overlap
212
+
213
+ `jdMatch()` measures how closely a resume overlaps with the job description it was submitted for:
214
+
215
+ ```javascript
216
+ const { data } = await client.jdMatch(resumeText, jobDescriptionText);
217
+ // { score: 0.786, level: 'high', keyword_coverage: 0.571,
218
+ // matched_keywords: [{ term: 'python', job_description_count: 1, resume_count: 1 }, ...],
219
+ // missing_keywords: [{ term: 'terraform', job_description_count: 1 }, ...],
220
+ // copied_fraction: 0.312, has_copied_span: true, copied_spans_total: 1,
221
+ // copied_spans: [{ start: 136, end: 207, text: '...', words: 10 }],
222
+ // resume_words: 32, job_description_words: 43, version: '...' }
223
+ ```
224
+
225
+ `score` (0–1) is a heuristic over keyword coverage and passages of 6+ words shared verbatim, not a
226
+ probability. It says nothing about who wrote the resume, and high overlap can simply mean a good fit,
227
+ so pair it with `aidetect()` and don't use it alone to decide an application. It's unbilled and
228
+ tuned for English. The API caps the resume at 50,000 characters and the posting at 20,000, and a
229
+ posting with fewer than 5 distinct keywords gets a 400. Span offsets index the resume in Unicode code
230
+ points.
231
+
211
232
  ### Rephrase, tone, sentiment and readability statistics
212
233
 
213
234
  ```javascript
@@ -463,6 +484,37 @@ All three upload methods take a `File`/`Blob` (in the browser, or Node 18+) or t
463
484
  the format. File problems come back as 400 `{ error: '...' }` and an oversized upload as 413.
464
485
  Uploads are limited to 60 per minute per account across all three methods.
465
486
 
487
+ ## Chunking
488
+
489
+ `chunkText()` splits long text into chunks of at most `maxLength` characters, e.g. to keep each
490
+ piece under another endpoint's length limit. Each split prefers a page break, then a paragraph
491
+ break, a line break, a tab, ending punctuation and any whitespace, before falling back to a hard
492
+ cut. Chunks don't overlap, so joining them gives back the original text. `chunkHtml()` does the
493
+ same for HTML after stripping the tags. Both are unbilled.
494
+
495
+ ```javascript
496
+ const { data } = await client.chunkText('Here is one paragraph.\n\nHere is another.', 25);
497
+ // { chunks: ['Here is one paragraph.\n\n', 'Here is another.'] }
498
+ ```
499
+
500
+ `maxLength` must be a positive integer. The optional `step_size` sets how far back from each cut
501
+ to look for a split point; it defaults to 10% of the text length. See
502
+ [Chunking](https://sapling.ai/docs/api/chunk/).
503
+
504
+ `pdfToText()` and `docxToText()` return the plain text of an uploaded PDF or Word document, for
505
+ example to chunk it before sending it to another endpoint. Pass a `File`/`Blob`, or the file's
506
+ bytes with `filename`, which must end in `.pdf` or `.docx`. Both are unbilled.
507
+
508
+ ```javascript
509
+ import { readFile } from 'node:fs/promises';
510
+
511
+ const { data } = await client.pdfToText(await readFile('report.pdf'), { filename: 'report.pdf' });
512
+ const { data: chunks } = await client.chunkText(data.text, 4000);
513
+ ```
514
+
515
+ On a file the API can't read (password-protected, empty or corrupt), the 400 body is `{ error }`
516
+ rather than `{ msg }`. See [File Uploads](https://sapling.ai/docs/api/files/).
517
+
466
518
  ## Translation
467
519
 
468
520
  Translate text into a target language, named by ISO 639 code (`'fr'`, `'zh-TW'`) or English
@@ -606,3 +658,20 @@ console.log(data.total_characters, data.total_cost_usd, data.by_endpoint);
606
658
 
607
659
  A window over a year gets a `403`. Unlike `accountStatus()`, these need a key that isn't
608
660
  blocked. See [API Reporting](https://sapling.ai/docs/api/api-reporting/).
661
+
662
+ ## Health checks
663
+
664
+ `version()` and `liveness()` are unbilled and need no API key. The client doesn't send its key
665
+ on these calls.
666
+
667
+ ```javascript
668
+ const { data } = await client.version();
669
+ console.log(data.version, data.build); // 2026.6 v4626
670
+
671
+ await client.liveness(); // { msg: 'alive' }
672
+ ```
673
+
674
+ `version()` reports which release a hostname is serving, which is useful on-prem. `build` is
675
+ `'unknown'` when the image carries no build number. `liveness()` only says the API process is up,
676
+ so it can succeed while a service behind the API is down. To check that a key works, use
677
+ `accountStatus()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saplingai/sapling-js",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "description": "JavaScript client for the Sapling.ai API (grammar checking, AI content detection, rephrasing and more) plus the browser SDK.",
5
5
  "type": "module",
6
6
  "main": "./sapling/client.js",
@@ -23,6 +23,7 @@ export type AdvancedEditOptions = {
23
23
  export type ConstructorOptions = {
24
24
  timeout?: number,
25
25
  hostname?: string,
26
+ /** API path override. Leading and trailing slashes are optional. */
26
27
  pathname?: string,
27
28
  /**
28
29
  * When true, every request carries the API's `return_usage: true` flag and
@@ -148,11 +149,11 @@ export type AIDetectOptions = {
148
149
  sent_scores?: boolean,
149
150
  /** Include `score_string`, an HTML heatmap of the scored text. */
150
151
  score_string?: boolean,
151
- /** Pin a detector model version (e.g. `'20251027'`, or `'ml20261009'` for the multilingual
152
- * model); omitted = current default, with text detected as `es`/`pt`/`fr`/`de`/`it`/`nl`/`ja`
152
+ /** Pin a detector model version (e.g. `'20260820'`, or `'ml20261104'` for the current multilingual
153
+ * model; the previous multilingual models `'ml20261009'`, `'ml20261031'`, `'ml20261102'` and `'ml20261103'` stay pinnable); omitted = current default, with text detected as `ar`/`de`/`es`/`fr`/`it`/`ja`/`ko`/`nl`/`pl`/`pt`/`ro`/`ru`/`sv`/`tr`/`zh`
153
154
  * routed to the multilingual model. A pin always wins over that routing and over `lang`. */
154
155
  version?: string,
155
- /** Language of the text: `'en'`, `'es'`, `'pt'`, `'fr'`, `'de'`, `'it'`, `'nl'` or `'ja'`
156
+ /** Language of the text: `'en'`, `'ar'`, `'de'`, `'es'`, `'fr'`, `'it'`, `'ja'`, `'ko'`, `'nl'`, `'pl'`, `'pt'`, `'ro'`, `'ru'`, `'sv'`, `'tr'` or `'zh'`
156
157
  * (case-insensitive; region subtags such as `'pt-BR'` are accepted). Picks that language's
157
158
  * model instead of relying on detection, which helps with short texts; omit it (or pass
158
159
  * `'auto'`) to detect.
@@ -228,8 +229,8 @@ export type AIDetectResponse = {
228
229
  text: string;
229
230
  tokens: string[];
230
231
  token_probs: number[];
231
- /** The pinned `version` when one was requested; `'ml20261009'` when unpinned text was
232
- * detected as (or `lang` named) `es`/`pt`/`fr`/`de`/`it`/`nl`/`ja` and scored by the
232
+ /** The pinned `version` when one was requested; `'ml20261104'` when unpinned text was
233
+ * detected as (or `lang` named) `ar`/`de`/`es`/`fr`/`it`/`ja`/`ko`/`nl`/`pl`/`pt`/`ro`/`ru`/`sv`/`tr`/`zh` and scored by the
233
234
  * multilingual model; otherwise an opaque identifier of the default model — log it, don't
234
235
  * parse it. */
235
236
  version: string;
@@ -239,8 +240,8 @@ export type AIDetectResponse = {
239
240
  score_string?: string;
240
241
  /** Advisory detected language; omitted unless detection is high-confidence. Always the
241
242
  * detected language, even when `lang` was sent, so a mismatch is visible.
242
- * English plus `es`, `pt`, `fr`, `de`, `it`, `nl` and `ja` (routed to the multilingual
243
- * model) are supported; other languages are scored by the English model and are less
243
+ * English plus `ar`, `de`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `ro`, `ru`, `sv`,
244
+ * `tr` and `zh` (routed to the multilingual model) are supported; other languages are scored by the English model and are less
244
245
  * reliable. */
245
246
  language?: AIDetectLanguage;
246
247
  }
@@ -341,6 +342,61 @@ export type DeleteStyleguideRulesetResponse = {
341
342
  deleted: string;
342
343
  }
343
344
 
345
+ export type ChunkOptions = {
346
+ step_size?: number;
347
+ }
348
+
349
+ export type ChunkResponse = {
350
+ chunks: string[];
351
+ }
352
+
353
+ export type JdMatchLevel = 'low' | 'moderate' | 'high';
354
+
355
+ export type JdMatchMatchedKeyword = {
356
+ term: string;
357
+ job_description_count: number;
358
+ resume_count: number;
359
+ }
360
+
361
+ export type JdMatchMissingKeyword = {
362
+ term: string;
363
+ job_description_count: number;
364
+ }
365
+
366
+ export type JdMatchCopiedSpan = {
367
+ /** Offsets into the submitted resume (`end` exclusive), in Unicode code points —
368
+ * slice with `Array.from(text)` if it may contain emoji. */
369
+ start: number;
370
+ end: number;
371
+ text: string;
372
+ words: number;
373
+ }
374
+
375
+ /** jdMatch(): a text overlap report. It says nothing about who wrote the resume. */
376
+ export type JdMatchResponse = {
377
+ /** 0–1. A heuristic over keyword coverage and copying, not a probability. */
378
+ score: number;
379
+ level: JdMatchLevel;
380
+ keyword_coverage: number;
381
+ matched_keywords: JdMatchMatchedKeyword[];
382
+ missing_keywords: JdMatchMissingKeyword[];
383
+ /** Share of the resume's words inside a copied span. */
384
+ copied_fraction: number;
385
+ /** Whether the resume shares any run of 6+ words verbatim with the posting. */
386
+ has_copied_span: boolean;
387
+ /** At most 50, in document order; `copied_spans_total` has the full count. */
388
+ copied_spans: JdMatchCopiedSpan[];
389
+ copied_spans_total: number;
390
+ resume_words: number;
391
+ job_description_words: number;
392
+ version: string;
393
+ }
394
+
395
+ /** pdfToText()/docxToText(): the document's plain text. */
396
+ export type FileToTextResponse = {
397
+ text: string;
398
+ }
399
+
344
400
  export type AccountBlockedReason =
345
401
  | 'key_expired'
346
402
  | 'subscription_expired'
@@ -353,6 +409,9 @@ export type AccountCreditSummary = {
353
409
  enabled: boolean;
354
410
  currency: string;
355
411
  alert_threshold_percent: number;
412
+ // Prepaid-only account: API access needs a positive credit balance. Sent
413
+ // whether or not `enabled` is true (optional for older on-prem servers).
414
+ credits_only?: boolean;
356
415
  // Present only when `enabled` is true.
357
416
  balance_credits?: number;
358
417
  purchased_credits?: number;
@@ -1058,6 +1117,17 @@ export type UserDetails = {
1058
1117
  anonUser?: boolean,
1059
1118
  }
1060
1119
 
1120
+ export type VersionResponse = {
1121
+ /** Release version, for example `'2026.6'`. */
1122
+ version: string;
1123
+ /** Build identifier; `'unknown'` when the image carries no build number. */
1124
+ build: string;
1125
+ };
1126
+
1127
+ export type LivenessResponse = {
1128
+ msg: 'alive';
1129
+ };
1130
+
1061
1131
  export declare class Client {
1062
1132
  constructor(apiKey: string, options?: ConstructorOptions);
1063
1133
 
@@ -1142,6 +1212,17 @@ export declare class Client {
1142
1212
  classify(texts: string[], labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<WithUsage<ClassifyBatchResponse>>>;
1143
1213
  classify(text: string | string[], labels: ClassifyLabel[], options?: ClassifyOptions): Promise<AxiosResponse<WithUsage<ClassifyResponse | ClassifyBatchResponse>>>;
1144
1214
  seo(text: string, options?: SeoOptions): Promise<AxiosResponse<WithUsage<SeoResponse>>>;
1215
+ jdMatch(text: string, jobDescription: string): Promise<AxiosResponse<WithUsage<JdMatchResponse>>>;
1216
+ chunkText(text: string, maxLength: number, options?: ChunkOptions): Promise<AxiosResponse<WithUsage<ChunkResponse>>>;
1217
+ chunkHtml(html: string, maxLength: number, options?: ChunkOptions): Promise<AxiosResponse<WithUsage<ChunkResponse>>>;
1218
+ /** `file` must be named `*.pdf` (a File's own name, or options.filename). */
1219
+ pdfToText(file: UploadFile, options?: FileUploadOptions): Promise<AxiosResponse<WithUsage<FileToTextResponse>>>;
1220
+ /** `file` must be named `*.docx` (a File's own name, or options.filename). */
1221
+ docxToText(file: UploadFile, options?: FileUploadOptions): Promise<AxiosResponse<WithUsage<FileToTextResponse>>>;
1222
+ /** Not WithUsage<>: public and unbilled, and sends neither the API key nor return_usage. */
1223
+ version(): Promise<AxiosResponse<VersionResponse>>;
1224
+ /** Not WithUsage<>: public and unbilled, and sends neither the API key nor return_usage. */
1225
+ liveness(): Promise<AxiosResponse<LivenessResponse>>;
1145
1226
  accountStatus(): Promise<AxiosResponse<AccountStatusResponse>>;
1146
1227
  apiUsage(options?: ReportingWindowOptions): Promise<AxiosResponse<WithUsage<ApiUsageResponse>>>;
1147
1228
  apiEndpointUsage(options?: ApiEndpointUsageOptions): Promise<AxiosResponse<WithUsage<ApiEndpointUsageResponse>>>;
package/sapling/client.js CHANGED
@@ -45,6 +45,23 @@ const reportingWindowBody = (apiKey, options, method) => {
45
45
  return data;
46
46
  };
47
47
 
48
+ // Shared by chunkText() and chunkHtml(). The API reads both numbers with an
49
+ // integer type check (a numeric string like "25" is a 400), and max_length is
50
+ // required in practice: its default of 0 fails the positive-integer check.
51
+ const chunkBody = (apiKey, maxLength, options, method) => {
52
+ if (!Number.isInteger(maxLength) || maxLength < 1) {
53
+ throw new TypeError(`${method}: maxLength must be a positive integer.`);
54
+ }
55
+ const data = { key: apiKey, max_length: maxLength };
56
+ if (options?.step_size !== undefined) {
57
+ if (!Number.isInteger(options.step_size) || options.step_size < 0) {
58
+ throw new TypeError(`${method}: step_size must be a non-negative integer.`);
59
+ }
60
+ data.step_size = options.step_size;
61
+ }
62
+ return data;
63
+ };
64
+
48
65
  // Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
49
66
  // size limits (rule/description length) are server configuration and enforced
50
67
  // there with a clear 400 (see classify()).
@@ -178,12 +195,24 @@ const buildUploadForm = (file, filename, jsonParams, method) => {
178
195
  return form;
179
196
  };
180
197
 
198
+ // Shared by pdfToText() and docxToText(): the key (and the client-level
199
+ // return_usage flag, which axiosPromise only injects into plain-object bodies)
200
+ // ride the jsonParams part.
201
+ const fileToTextForm = (client, file, options, method) => {
202
+ const params = { key: client.apiKey };
203
+ if (client.returnUsage) {
204
+ params.return_usage = true;
205
+ }
206
+ return buildUploadForm(file, options?.filename, params, method);
207
+ };
208
+
181
209
  export class Client {
182
210
  constructor(apiKey, options = {}) {
183
211
  this.apiKey = apiKey;
184
212
  this.timeout = options.timeout || 120000;
185
- this.hostname = options.hostname || 'https://api.sapling.ai';
186
- this.pathname = options.pathname || '/api/v1/';
213
+ this.hostname = (options.hostname || 'https://api.sapling.ai').replace(/\/+$/, '');
214
+ const path = (options.pathname || '/api/v1/').replace(/^\/+|\/+$/g, '');
215
+ this.pathname = path ? `/${path}/` : '/';
187
216
  this.urlEndpoint = `${this.hostname}${this.pathname}`;
188
217
  this.defaultSessionId = uuidv4();
189
218
  // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a
@@ -2072,6 +2101,157 @@ export class Client {
2072
2101
  return this.axiosPromise(url, 'post', data);
2073
2102
  }
2074
2103
 
2104
+ /**
2105
+ * Measure how closely a resume overlaps with a job description
2106
+ * (POST /api/v1/jd-match): the share of the posting's keywords the resume
2107
+ * uses, the passages the two share word for word, and a low / moderate / high
2108
+ * level. Deterministic and unbilled; both texts count against the rate limit.
2109
+ *
2110
+ * `score` is a heuristic over those two signals, not a probability, and says
2111
+ * nothing about who wrote the resume. High overlap can simply mean a good fit,
2112
+ * so pair it with aidetect() and don't use it alone to decide an application.
2113
+ *
2114
+ * @param {string} text - The resume. The API caps it at 50,000 characters and
2115
+ * compares it as sent, so `copied_spans` offsets index this exact string, in
2116
+ * Unicode code points.
2117
+ * @param {string} jobDescription - The posting (sent as the API's
2118
+ * `job_description`). The API caps it at 20,000 characters and answers 400
2119
+ * when it has fewer than 5 distinct keywords.
2120
+ * @returns {Promise<AxiosResponse>}
2121
+ */
2122
+ jdMatch(text, jobDescription) {
2123
+ // Length, empty-text and keyword-count checks stay on the server, which
2124
+ // answers each with a clear 400.
2125
+ if (typeof text !== 'string') {
2126
+ throw new TypeError('jdMatch: text must be a string.');
2127
+ }
2128
+ if (typeof jobDescription !== 'string') {
2129
+ throw new TypeError('jdMatch: jobDescription must be a string.');
2130
+ }
2131
+ const data = { key: this.apiKey, text, job_description: jobDescription };
2132
+ return this.axiosPromise(`${this.urlEndpoint}jd-match`, 'post', data);
2133
+ }
2134
+
2135
+ /**
2136
+ * Split text into chunks of at most `maxLength` characters, e.g. to fit an
2137
+ * endpoint's length limit or an LLM context window. Each split prefers, in
2138
+ * order, a page break, a paragraph break, a line break, a tab, ending
2139
+ * punctuation (`.`, `?`, `!`) and then any whitespace, and falls back to a
2140
+ * hard cut at `maxLength`. Chunks don't overlap and keep their delimiters,
2141
+ * so joining them gives back the original text. Unbilled.
2142
+ *
2143
+ * Returns `chunks`, an array of strings in document order.
2144
+ *
2145
+ * @param {string} text - The text to chunk.
2146
+ * @param {number} maxLength - Maximum chunk length in characters (a
2147
+ * positive integer; sent as the API's `max_length`).
2148
+ * @param {Object} [options]
2149
+ * @param {number} [options.step_size] - Window, in characters, searched
2150
+ * backwards from each `maxLength` cut for a split point. Larger windows
2151
+ * find preferred split points more often but vary chunk sizes more.
2152
+ * Defaults to 10% of the text length.
2153
+ * @returns {Promise<AxiosResponse>}
2154
+ */
2155
+ chunkText(text, maxLength, options = {}) {
2156
+ if (typeof text !== 'string') {
2157
+ throw new TypeError('chunkText: text must be a string.');
2158
+ }
2159
+ const data = chunkBody(this.apiKey, maxLength, options, 'chunkText');
2160
+ data.text = text;
2161
+ return this.axiosPromise(`${this.urlEndpoint}ingest/chunk_text`, 'post', data);
2162
+ }
2163
+
2164
+ /**
2165
+ * Extract the text of an HTML document and split it like chunkText(). Tags
2166
+ * are discarded, so the chunks are plain text. Unbilled.
2167
+ *
2168
+ * @param {string} html - The HTML to extract text from and chunk.
2169
+ * @param {number} maxLength - Maximum chunk length in characters.
2170
+ * @param {Object} [options]
2171
+ * @param {number} [options.step_size] - See chunkText().
2172
+ * @returns {Promise<AxiosResponse>}
2173
+ */
2174
+ chunkHtml(html, maxLength, options = {}) {
2175
+ if (typeof html !== 'string') {
2176
+ throw new TypeError('chunkHtml: html must be a string.');
2177
+ }
2178
+ const data = chunkBody(this.apiKey, maxLength, options, 'chunkHtml');
2179
+ data.html = html;
2180
+ return this.axiosPromise(`${this.urlEndpoint}ingest/chunk_html`, 'post', data);
2181
+ }
2182
+
2183
+ /**
2184
+ * Extract the plain text of an uploaded PDF (POST /api/v1/ingest/pdf_to_text).
2185
+ * Resolves to `{text}`. The API requires a `.pdf` filename; a password-protected,
2186
+ * empty or malformed file gets a 400 whose body is `{error}` (not `{msg}`).
2187
+ * Unbilled.
2188
+ *
2189
+ * @param {Blob|File|Buffer|Uint8Array|ArrayBuffer} file - The PDF.
2190
+ * @param {object} [options]
2191
+ * @param {string} [options.filename] - Required unless `file` is a File,
2192
+ * e.g. 'report.pdf'.
2193
+ * @returns {Promise<AxiosResponse>}
2194
+ */
2195
+ pdfToText(file, options = {}) {
2196
+ const form = fileToTextForm(this, file, options, 'pdfToText');
2197
+ return this.axiosPromise(`${this.urlEndpoint}ingest/pdf_to_text`, 'post', form);
2198
+ }
2199
+
2200
+ /**
2201
+ * Extract the plain text of an uploaded Word document
2202
+ * (POST /api/v1/ingest/docx_to_text). Resolves to `{text}`, with blocks
2203
+ * (paragraphs, tables) separated by blank lines. The API requires a `.docx`
2204
+ * filename; a corrupt or macro-enabled file gets a 400 whose body is
2205
+ * `{error}` (not `{msg}`). Unbilled.
2206
+ *
2207
+ * @param {Blob|File|Buffer|Uint8Array|ArrayBuffer} file - The DOCX.
2208
+ * @param {object} [options]
2209
+ * @param {string} [options.filename] - Required unless `file` is a File,
2210
+ * e.g. 'notes.docx'.
2211
+ * @returns {Promise<AxiosResponse>}
2212
+ */
2213
+ docxToText(file, options = {}) {
2214
+ const form = fileToTextForm(this, file, options, 'docxToText');
2215
+ return this.axiosPromise(`${this.urlEndpoint}ingest/docx_to_text`, 'post', form);
2216
+ }
2217
+
2218
+ /**
2219
+ * Release and build identifiers of the deployment answering the request:
2220
+ * `version` (for example `'2026.6'`) and `build`, which is `'unknown'` when the
2221
+ * image carries no build number. Unbilled, and needs no API key and sends
2222
+ * none, so it can check which release a hostname is serving.
2223
+ *
2224
+ * @returns {Promise<AxiosResponse>}
2225
+ */
2226
+ version() {
2227
+ const url = `${this.urlEndpoint}version`;
2228
+ // Not axiosPromise, and no key: the route is public, so neither the API key
2229
+ // nor the client-level return_usage opt-in has any reason to leave the client.
2230
+ return axios({
2231
+ url,
2232
+ method: 'get',
2233
+ timeout: this.timeout,
2234
+ });
2235
+ }
2236
+
2237
+ /**
2238
+ * Whether the API process is up: answers 200 with `{ msg: 'alive' }`. Like
2239
+ * version(), it's unbilled, needs no API key and sends none. It doesn't
2240
+ * check the services behind the API, so it can succeed during an outage of
2241
+ * one of them.
2242
+ *
2243
+ * @returns {Promise<AxiosResponse>}
2244
+ */
2245
+ liveness() {
2246
+ const url = `${this.urlEndpoint}liveness`;
2247
+ // Not axiosPromise, and no key: see version().
2248
+ return axios({
2249
+ url,
2250
+ method: 'get',
2251
+ timeout: this.timeout,
2252
+ });
2253
+ }
2254
+
2075
2255
  /**
2076
2256
  * Check whether an account-wide block (expired key/subscription,
2077
2257
  * depleted credits, billing limit, trial daily allowance, monthly quota)