@saplingai/sapling-js 1.0.53 → 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 +350 -12
- package/package.json +2 -2
- package/sapling/client.d.ts +647 -58
- package/sapling/client.js +944 -57
- package/sapling/sapling-sdk.min.js +1 -1
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Node.js client for the [Sapling.ai API](https://sapling.ai/api).
|
|
4
4
|
|
|
5
|
-
Try out a [grammar check demo](https://sapling.ai/grammar-check).
|
|
5
|
+
Try out a [grammar check demo](https://sapling.ai/grammar-check) or an [AI detection demo](https://sapling.ai/ai-content-detector). The [SDK demo](https://sapling.ai/sdk-textarea-demo) shows both running live in a textarea through the browser SDK bundled with this package.
|
|
6
6
|
Compare against grammar checking tools and APIs like Grammarly (Grammerly), LanguageTool, ProWritingAid and Ginger.
|
|
7
7
|
|
|
8
8
|
## Functionality
|
|
9
9
|
|
|
10
|
-
Sapling is an AI
|
|
11
|
-
The API
|
|
10
|
+
Sapling is an AI writing assistant.
|
|
11
|
+
The API offers spelling and grammar checking, AI content detection, rephrasing, tone detection and more; the bundled browser SDK adds in-page grammar suggestions, autocomplete and AI detection to textareas and contenteditables.
|
|
12
12
|
|
|
13
13
|
Benefits include:
|
|
14
14
|
- **60% more grammar corrections**: Compared to [other systems](https://sapling.ai/comparison/api) at similar accuracy using state-of-the-art generative AI models.
|
|
@@ -84,6 +84,24 @@ skips error classification (faster and lighter), and each edit comes back withou
|
|
|
84
84
|
client.edits('I have a apple!', undefined, { includeErrorCategories: false, autoApply: true })
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
`edits()` and `spellcheck()` responses are typed (`EditsResponse`, `SpellcheckResponse`).
|
|
88
|
+
If a model stage can't process some sentences in time, the response still returns
|
|
89
|
+
everything that did complete and adds a `degraded` object. Those sentences may be
|
|
90
|
+
missing some or all of their edits, and the object gives counts only (not which
|
|
91
|
+
sentences), so retry the whole request after a short backoff if you need a
|
|
92
|
+
complete result:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
const { data } = await client.edits(text);
|
|
96
|
+
if (data.degraded) {
|
|
97
|
+
// { sentences_degraded: 1, sentences_total: 12, reason: 'model_timeout' }
|
|
98
|
+
// reason: model_timeout | model_unavailable | model_error
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If no sentence could be checked, the request rejects with a `503` and a
|
|
103
|
+
`Retry-After` header instead (see [Error handling](#error-handling)).
|
|
104
|
+
|
|
87
105
|
### Text classification
|
|
88
106
|
|
|
89
107
|
Zero-shot classification with your own labels (plain names or `{ name, description }` objects, 2–20):
|
|
@@ -106,22 +124,112 @@ client
|
|
|
106
124
|
|
|
107
125
|
```javascript
|
|
108
126
|
const { data } = await client.aidetect(documentText);
|
|
109
|
-
// { score: 0.05, ai_fraction: 0.51,
|
|
127
|
+
// { score: 0.05, ai_fraction: 0.51, verdict: 'mixed',
|
|
128
|
+
// segments: [{ start: 0, end: 240, score: 0.96 }],
|
|
129
|
+
// low_confidence: false, low_confidence_reasons: [], text: '...', version: '...',
|
|
110
130
|
// sentence_scores: [{ sentence: '...', score: 0.97, start: 0, end: 112 }, ...],
|
|
111
131
|
// tokens: [...], token_probs: [...] }
|
|
112
132
|
```
|
|
113
133
|
|
|
114
134
|
`score` (0–1) rates the whole text, so a document that is only partly AI-written can score low;
|
|
115
135
|
`ai_fraction` is the share of the text in sentences scoring ≥ 0.5 (null when nothing could be
|
|
116
|
-
scored), which flags those documents. `
|
|
117
|
-
`
|
|
136
|
+
scored), which flags those documents. `verdict` sums both up as `'human'`, `'mixed'` or `'ai'`
|
|
137
|
+
(`'ai'` needs `score` ≥ 0.5 and `ai_fraction` ≥ 0.8; `'mixed'` is anything else with `score` ≥ 0.5
|
|
138
|
+
or `ai_fraction` ≥ 0.2; with a null `ai_fraction` it is `'ai'` or `'human'` by `score` alone), and
|
|
139
|
+
is omitted when the text couldn't be scored. `segments` lists each run
|
|
140
|
+
of consecutive sentences scoring ≥ 0.5 as `{ start, end, score }`, even with `sent_scores: false`.
|
|
141
|
+
`low_confidence` is true for inputs with a higher false-positive rate, named in
|
|
142
|
+
`low_confidence_reasons`: `'short_text'` (under 500 characters) and `'list_format'` (mostly bullet or
|
|
143
|
+
numbered lines); show those results hedged rather than as a bare score. `sentence_scores` use the
|
|
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
|
|
118
145
|
`Array.from(text).slice(start, end).join('')` if the text may contain emoji. A response may also
|
|
119
146
|
carry an advisory `language` (`{ code, name, confidence }`) when the text is confidently
|
|
120
|
-
identified
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
subtags such as `'pt-BR'` accepted: use that language's model instead of detecting it, which helps
|
|
154
|
+
with short texts; omit it or pass `'auto'` to detect; other languages get a 400), `version` (pin
|
|
155
|
+
a model; a pin always overrides the language routing and `lang`). `language` still reports the
|
|
156
|
+
detected language when `lang` is set, so a mismatch is visible. Pass an array of 1–10 texts for the
|
|
157
|
+
batch form, which answers `{ results: [...] }` in input order; `lang` applies to every item.
|
|
158
|
+
|
|
159
|
+
Pass `zero_retention: true` to keep no copy of the text. The request is still scored and billed,
|
|
160
|
+
but its result is not written to the shared result cache and usage is logged without the text (a
|
|
161
|
+
hash of it is kept so a repeat isn't billed twice). Teams with data retention disabled get this on
|
|
162
|
+
every request. `fileAidetect()` and `urlAidetect()` take the same option.
|
|
163
|
+
|
|
164
|
+
Pass `create_report: true` (single text only, not a batch) to also save the result as a shareable
|
|
165
|
+
report: the response adds `report` (`{ id, url, shared_at, expires_at }`), a public link to the
|
|
166
|
+
text and its scores that you can revoke from your AI detector history and that expires 90 days
|
|
167
|
+
after creation. It needs your private API key, and can't be combined with `zero_retention`
|
|
168
|
+
(both get a 400). When no report can be made (for example, history or text retention is turned
|
|
169
|
+
off, or the history is full), `report` is `null` and `report_skip_reason` says why; the request is
|
|
170
|
+
still scored and billed. `fileAidetect()` and `urlAidetect()` take it too, naming the report after
|
|
171
|
+
the file or the final URL.
|
|
123
172
|
|
|
124
|
-
|
|
173
|
+
```javascript
|
|
174
|
+
const { data } = await client.aidetect(reviewText, { lang: 'pt-BR' }); // version: 'ml20261104'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
To score a document without extracting its text yourself, upload it with `fileAidetect()`. It
|
|
178
|
+
accepts `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt`, `.md`, `.html` or `.htm` files up to 30MB, and the
|
|
179
|
+
format is chosen by file extension. The first 200,000 characters of the document's text are scored
|
|
180
|
+
like `aidetect()`, with `sentence_scores` and `score_string` always included. The language is
|
|
181
|
+
detected and routed automatically; there is no `lang` or `version` option, but `zero_retention`
|
|
182
|
+
works as on `aidetect()`. Billing is the same as `aidetect()`.
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
import { readFileSync } from 'node:fs';
|
|
186
|
+
|
|
187
|
+
const { data } = await client.fileAidetect(readFileSync('essay.pdf'), { filename: 'essay.pdf' });
|
|
188
|
+
// In the browser, pass the File from an <input type="file"> (its own name is used):
|
|
189
|
+
// await client.fileAidetect(input.files[0]);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
To score a web page, pass its URL to `urlAidetect()`. The API fetches the page and scores its main
|
|
193
|
+
readable text: navigation, headers, footers, forms and hidden elements are dropped, and the largest
|
|
194
|
+
`<article>` (else `<main>`) is preferred, so a scraped page's boilerplate isn't scored or billed. A
|
|
195
|
+
URL that serves a `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt` or `.md` document is read like
|
|
196
|
+
`fileAidetect()`. Options and billing are the same as `aidetect()`, and the response adds `source`:
|
|
197
|
+
the final URL after redirects, the page title, its content type, the `characters` scored, and
|
|
198
|
+
`truncated` (HTML past 2MB, documents past 15MB and text past 200,000 characters are not scored).
|
|
199
|
+
Only public http(s) URLs on ports 80/443/8080/8443 are fetched, at most 30 per minute per account. A
|
|
200
|
+
page that is fetched but can't be scored (a non-2xx status, unsupported type, no readable text)
|
|
201
|
+
answers 422, and one that can't be fetched (timeout, connection failure, or a site that blocks
|
|
202
|
+
automated requests) answers 502.
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
const { data } = await client.urlAidetect('https://example.com/blog/post', { sent_scores: false });
|
|
206
|
+
// { score: 0.93, ai_fraction: 0.88, text: '...', version: '...', ...,
|
|
207
|
+
// source: { url: 'https://example.com/blog/post', title: '...', content_type: 'text/html',
|
|
208
|
+
// characters: 5120, truncated: false } }
|
|
209
|
+
```
|
|
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
|
+
|
|
232
|
+
### Rephrase, tone, sentiment and readability statistics
|
|
125
233
|
|
|
126
234
|
```javascript
|
|
127
235
|
const { data: rephrased } = await client.rephrase('hey, can u send it over', {
|
|
@@ -131,6 +239,11 @@ const { data: rephrased } = await client.rephrase('hey, can u send it over', {
|
|
|
131
239
|
// { results: [{ hash: '...', original: '...', replacement: 'Could you please send it over?',
|
|
132
240
|
// rephrase_type: 'informal_to_formal', model_version: '...' }, ...] }
|
|
133
241
|
|
|
242
|
+
const { data: tone } = await client.tone('Really stoked about this! Will it be ready by next week?');
|
|
243
|
+
// { overall: [[0.92, 'curious', '🤓'], [0.05, 'excited', '😀'], [0.01, 'confused', '😕']],
|
|
244
|
+
// results: [[[0.99, 'excited', '😀'], ...], [[0.55, 'confused', '😕'], ...]],
|
|
245
|
+
// sents: ['Really stoked about this!', 'Will it be ready by next week?'] }
|
|
246
|
+
|
|
134
247
|
const { data: sentiment } = await client.sentiment('Love the new release. The install was painful.');
|
|
135
248
|
// { overall: [[0.71, 'POSITIVE'], [0.29, 'NEGATIVE']],
|
|
136
249
|
// results: [[[0.99, 'POSITIVE']], [[0.95, 'NEGATIVE'], ...]],
|
|
@@ -144,13 +257,54 @@ const { data: stats } = await client.statistics(documentText, { lang: 'en' });
|
|
|
144
257
|
`rephrase()` mappings: `paraphrase`, `reword`, `expand`, `shorten`, `summarize`,
|
|
145
258
|
`passive_to_active`, `active_to_passive`, `informal_to_formal`, `formal_to_informal`,
|
|
146
259
|
`sentence_split`, `generate_title`, `generate_acronym`, `generate_meta_description`,
|
|
147
|
-
`switch_tone` (with `tone_mapping`) and `switch_tense` (with `tense_mapping`).
|
|
260
|
+
`switch_tone` (with `tone_mapping`) and `switch_tense` (with `tense_mapping`). Tones come as
|
|
261
|
+
`[probability, tone name, emoji]` triples: `overall` holds up to three for the whole text, most
|
|
262
|
+
likely first, with probabilities summing to 1, and `results` holds one list per sentence. A text
|
|
263
|
+
the API cannot split into sentences gets a 200 with `[]`, and the text is capped at 8000
|
|
264
|
+
characters. Sentiment labels
|
|
148
265
|
are `POSITIVE`, `NEUTRAL` and `NEGATIVE`, as `[probability, label]` pairs, most likely first;
|
|
149
266
|
a text with no sentences (markup or punctuation only) gets a 204 with an empty body (`data === ''`),
|
|
150
267
|
and one the API cannot split into sentences gets a 200 with `[]`.
|
|
151
268
|
`statistics()` is unbilled; `lang` is one of `en`, `de`, `es`, `fr`, `it`, `nl`, `pl`, `ru`, `ar`
|
|
152
269
|
or `auto` and picks the readability formulas (`readability_scores` is empty for `auto`).
|
|
153
270
|
|
|
271
|
+
### Autocomplete
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const { data: completion } = await client.complete('Thanks for getting back to me so');
|
|
275
|
+
if (!Array.isArray(completion)) {
|
|
276
|
+
// { output: ' quickly.', query: 'Thanks for getting back to me so', hash: '...' }
|
|
277
|
+
// When the user takes the suggestion:
|
|
278
|
+
await client.acceptComplete(completion.hash, completion.query, completion.output);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`complete()` needs an API key with autocomplete enabled and a query of at most 200 characters.
|
|
283
|
+
It answers `[]` when it has no completion. `query` in the response is the part of the sent
|
|
284
|
+
query the prediction continues: the API keeps only the text after the last run of three
|
|
285
|
+
spaces. Pass `hash`, `query` and `output` to `acceptComplete()` unchanged, because the API
|
|
286
|
+
checks them against the hash and answers 400 when they don't match.
|
|
287
|
+
|
|
288
|
+
### Language detection
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
const { data: detected } = await client.langdetect('Bonjour tout le monde, comment allez-vous ?');
|
|
292
|
+
// { lang: 'fr', conf: 0.99, name: 'French', reliable: true, script: 'Latn',
|
|
293
|
+
// candidates: [{ lang: 'fr', conf: 0.99, name: 'French' }, ...] }
|
|
294
|
+
|
|
295
|
+
const { data: short } = await client.langdetect('Hola');
|
|
296
|
+
// { lang: 'es', conf: 0.95, name: 'Spanish', reliable: false, script: 'Latn', ... }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Act on `lang` only when `reliable` is true. It is true only when the confidence is high and the
|
|
300
|
+
text has enough letters, so short inputs like `"OK"` or `"Hola"` are never reliable however high
|
|
301
|
+
`conf` is. `script` is the text's dominant writing system as an ISO 15924 code: `Latn`, `Cyrl`,
|
|
302
|
+
`Arab`, `Deva`, `Hang`, `Hani` for Chinese, and `Jpan` once the text contains any kana. It is `Zzzz`
|
|
303
|
+
for a script outside the map, and `null` when the text has no letters. Options are `top_k` (1–10
|
|
304
|
+
candidates, default 3) and `segments: true`, which adds a per-sentence `segments` breakdown (`start`,
|
|
305
|
+
`end`, `text` plus the same fields) for mixed-language text. Pass an array of 1–10 texts for the
|
|
306
|
+
batch form, `{ results: [...] }`. Language detection is unbilled.
|
|
307
|
+
|
|
154
308
|
### Similar terms, profanity and the custom dictionary
|
|
155
309
|
|
|
156
310
|
```javascript
|
|
@@ -175,6 +329,31 @@ or 0. Both are unbilled. `addDictionaryEntry()` answers 400 for a duplicate entr
|
|
|
175
329
|
than two digits; `deleteDictionaryEntry()` answers 400 (not 404) for an id that is not this
|
|
176
330
|
account's.
|
|
177
331
|
|
|
332
|
+
### Custom mappings and custom filters
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// Always suggest `mapping` wherever `entry` matches (brand names, house style).
|
|
336
|
+
const { data: mapping } = await client.addCustomMapping('sapling', 'Sapling', {
|
|
337
|
+
description: 'Our product name is capitalized.', // shown with the suggestion; it then can't be ignored
|
|
338
|
+
});
|
|
339
|
+
// { id: '...', entry: 'sapling', mapping: 'Sapling', description: '...', case_sensitive: true, ... }
|
|
340
|
+
const { data: { custom_mappings } } = await client.customMappings(); // sorted by entry
|
|
341
|
+
await client.deleteCustomMapping(mapping.id); // 204, empty body
|
|
342
|
+
|
|
343
|
+
// Stop suggesting edits that rewrite `input` to `output`; '' on either side is a wildcard.
|
|
344
|
+
const { data: filter } = await client.addCustomFilter('Patient', 'The patient');
|
|
345
|
+
// { id: '...', input: 'Patient', output: 'The patient', case_sensitive: true, ... }
|
|
346
|
+
const { data: { custom_filters } } = await client.customFilters(); // sorted by input
|
|
347
|
+
await client.deleteCustomFilter(filter.id); // 204, empty body
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
In a mapping's `entry`, `^` and `$` anchor the match to the start and end of a sentence, and `#`
|
|
351
|
+
matches a number (a `#` in `mapping` puts it back). `case_sensitive` defaults to true. The API
|
|
352
|
+
answers 400 for a duplicate, an entry equal to its mapping (or a filter whose two sides are equal),
|
|
353
|
+
more than two digits in the entry or input, and a case-insensitive rule whose two sides differ only
|
|
354
|
+
in case (`iphone` → `iPhone` must be case sensitive). A filter needs at least one non-blank side.
|
|
355
|
+
Deleting an id that is not this account's answers 400, not 404. All six calls are unbilled.
|
|
356
|
+
|
|
178
357
|
### Style-guide rulesets
|
|
179
358
|
|
|
180
359
|
Check text against style rules inline, or save a named ruleset once (owned by
|
|
@@ -264,6 +443,78 @@ as `YYYY-MM-DD`, or `'list'`), and every value must be grounded in a quote from
|
|
|
264
443
|
extraction, not inference, so a value that isn't stated is reported in `missing` rather than
|
|
265
444
|
guessed.
|
|
266
445
|
|
|
446
|
+
### Extracting from documents
|
|
447
|
+
|
|
448
|
+
`fileExtract()` runs the same extraction over an uploaded document. It takes the same file types
|
|
449
|
+
and 30MB limit as `fileAidetect()`, and the same `fields` and `context` as `extract()`. Only the
|
|
450
|
+
first 10,000 characters of the document's text are read. The response adds a `document` block that
|
|
451
|
+
says how much text was used; `characters_used` is what is billed.
|
|
452
|
+
|
|
453
|
+
```javascript
|
|
454
|
+
const { data } = await client.fileExtract(
|
|
455
|
+
readFileSync('invoice.pdf'),
|
|
456
|
+
['invoice_number', { name: 'total', type: 'number' }, { name: 'due_date', type: 'date' }],
|
|
457
|
+
{ filename: 'invoice.pdf', context: 'A vendor invoice' },
|
|
458
|
+
);
|
|
459
|
+
// { data: { invoice_number: 'INV-1042', total: 1299.0, due_date: '2026-03-05' }, fields: [...],
|
|
460
|
+
// missing: [],
|
|
461
|
+
// document: { file_type: 'pdf', characters: 2310, characters_used: 2310, truncated: false } }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
When `truncated` is true, fields that appear only after the first 10,000 characters are reported
|
|
465
|
+
in `missing`. To read a later section, extract its text yourself and pass it to `extract()`.
|
|
466
|
+
|
|
467
|
+
## Document Summarization
|
|
468
|
+
|
|
469
|
+
`fileSummarize()` summarizes an uploaded document: the same `summary`, `key_points` and `length`
|
|
470
|
+
as the API's `/summarize` endpoint, for the same file types and 30MB limit as `fileAidetect()`.
|
|
471
|
+
`length` is `'short'`, `'medium'` (the default) or `'long'`. Only the first 20,000 characters of the
|
|
472
|
+
document's text are read, and the `document` block says how much was used; `characters_used` is
|
|
473
|
+
what is billed.
|
|
474
|
+
|
|
475
|
+
```javascript
|
|
476
|
+
const { data } = await client.fileSummarize(readFileSync('report.pdf'),
|
|
477
|
+
{ filename: 'report.pdf', length: 'short' });
|
|
478
|
+
// { summary: 'Revenue rose 12% ...', key_points: ['...', '...'], length: 'short', result: '...',
|
|
479
|
+
// document: { file_type: 'pdf', characters: 48210, characters_used: 19996, truncated: true } }
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
All three upload methods take a `File`/`Blob` (in the browser, or Node 18+) or the file's bytes
|
|
483
|
+
(`Buffer`, `Uint8Array`, `ArrayBuffer`). Bytes need `options.filename`, because the extension picks
|
|
484
|
+
the format. File problems come back as 400 `{ error: '...' }` and an oversized upload as 413.
|
|
485
|
+
Uploads are limited to 60 per minute per account across all three methods.
|
|
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
|
+
|
|
267
518
|
## Translation
|
|
268
519
|
|
|
269
520
|
Translate text into a target language, named by ISO 639 code (`'fr'`, `'zh-TW'`) or English
|
|
@@ -286,12 +537,58 @@ Region variants `zh-CN`/`zh-TW`/`zh-HK`, `pt-BR`/`pt-PT`, `en-GB`/`en-US`, `fr-C
|
|
|
286
537
|
text actually is, and `formality` (`'formal'`/`'informal'`) picks the register where the
|
|
287
538
|
target language distinguishes them.
|
|
288
539
|
|
|
540
|
+
## Inclusive Language
|
|
541
|
+
|
|
542
|
+
Flag non-inclusive or biased wording (gendered defaults, ableist idioms, ageist framing,
|
|
543
|
+
outdated terms for groups) with offsets, a category, a severity and a neutral replacement:
|
|
544
|
+
|
|
545
|
+
```javascript
|
|
546
|
+
client
|
|
547
|
+
.inclusive('The chairman said the plan was crazy.', { categories: ['gender', 'disability'] })
|
|
548
|
+
.then(function (response) {
|
|
549
|
+
console.log(response.data);
|
|
550
|
+
// { issues: [
|
|
551
|
+
// { category: 'gender', severity: 'low', text: 'The chairman', start: 0, end: 12,
|
|
552
|
+
// note: 'Gendered job title...', suggestion: 'The chair' },
|
|
553
|
+
// { category: 'disability', severity: 'low', text: 'crazy', start: 31, end: 36,
|
|
554
|
+
// note: 'Ableist idiom...', suggestion: 'unworkable' } ],
|
|
555
|
+
// categories: ['gender', 'disability'], inclusive: false }
|
|
556
|
+
})
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Categories: `gender`, `race_ethnicity`, `disability`, `age`, `lgbtq`, `religion`,
|
|
560
|
+
`socioeconomic`, `appearance` (default: all). Pass an array of up to 10 texts for the batch
|
|
561
|
+
form (`{ results: [...] }`).
|
|
562
|
+
|
|
289
563
|
## Further Documentation
|
|
290
564
|
|
|
291
565
|
- Full Sapling HTTP API and SDK [documentation](https://sapling.ai/docs).
|
|
292
566
|
- Create a key by following [these instructions](https://sapling.ai/docs/api/api-access).
|
|
293
567
|
- JavaScript, HTML, and Python [starter code](https://github.com/saplingai/sapling-samples).
|
|
294
568
|
|
|
569
|
+
## Usage reporting
|
|
570
|
+
|
|
571
|
+
Construct the client with `returnUsage: true` and every successful response additionally
|
|
572
|
+
reports the usage the request submitted for metering, as a top-level `usage` field:
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
const client = new Client(apiKey, { returnUsage: true });
|
|
576
|
+
const response = await client.classify('I was charged twice.', ['billing', 'other']);
|
|
577
|
+
console.log(response.data.usage);
|
|
578
|
+
// { characters: 20 }
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`characters` is the count submitted for billing (one count per **distinct** item of a batch
|
|
582
|
+
request; `0` for unbilled requests). It is captured before the billing pipeline's dedup
|
|
583
|
+
window applies, so it is an upper bound on the final charge. Batch responses carry `usage`
|
|
584
|
+
on the `{ results: [...] }` wrapper, not on the per-item results.
|
|
585
|
+
`accountStatus()` and `apiQuotaUsage()` are unaffected: both are unbilled and their responses
|
|
586
|
+
already have their own `usage` field, which the option leaves untouched.
|
|
587
|
+
Responses that are not a JSON object cannot carry the field, so `dictionaryEntries()` (a bare
|
|
588
|
+
array), `deleteDictionaryEntry()`, `deleteCustomMapping()` and `deleteCustomFilter()` (`204`,
|
|
589
|
+
empty body), `sentiment()`'s `204` / `[]` edge cases and the `[]` answers of `tone()` and
|
|
590
|
+
`complete()` never report it.
|
|
591
|
+
|
|
295
592
|
## Error handling
|
|
296
593
|
|
|
297
594
|
Every client method returns the axios promise, so API errors (a `400` for
|
|
@@ -311,7 +608,9 @@ API error bodies are JSON objects with a human-readable `msg`, for example
|
|
|
311
608
|
`{ msg: 'Invalid API key.' }`. The one exception is the 4MB `413`, which uses
|
|
312
609
|
`message`/`result` keys. When a `402` or `429` body has a numeric `retry_after`,
|
|
313
610
|
the request can succeed after waiting that many seconds. Without it, retrying
|
|
314
|
-
unchanged won't help.
|
|
611
|
+
unchanged won't help. A `503` means the service is temporarily saturated: wait
|
|
612
|
+
for its `Retry-After` header (`error.response.headers['retry-after']`, in
|
|
613
|
+
seconds), then retry. See
|
|
315
614
|
[Rate Limits & Errors](https://sapling.ai/docs/rate_limits/).
|
|
316
615
|
|
|
317
616
|
To find out why a key is blocked, and for how long, call `accountStatus()`. It
|
|
@@ -337,3 +636,42 @@ account's `subscription`, prepaid `credits`, `usage.chars_this_month` and, on a
|
|
|
337
636
|
free trial, `trial`. See [Account Status](https://sapling.ai/docs/api/account-status/).
|
|
338
637
|
Each call counts as 2,000 characters against the endpoint's own rate-limit
|
|
339
638
|
bucket, so call it when something fails rather than before every request.
|
|
639
|
+
|
|
640
|
+
## Usage and quota reports
|
|
641
|
+
|
|
642
|
+
Four unbilled methods report this key's usage over a window of days, set with
|
|
643
|
+
`start_days_back` / `end_days_back` (non-negative integers, days before today):
|
|
644
|
+
|
|
645
|
+
```javascript
|
|
646
|
+
const { data } = await client.apiUsage({ start_days_back: 30 });
|
|
647
|
+
console.log(data.total_characters, data.total_cost_usd, data.by_endpoint);
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
- `apiUsage()`: characters and list-price cost by masked key id, endpoint, day and month.
|
|
651
|
+
The window defaults to the current month. A team-scoped key covers the whole team; other
|
|
652
|
+
keys also get `credit_balance_data`.
|
|
653
|
+
- `apiEndpointUsage({ interval })`: characters per endpoint, bucketed `'Daily'`
|
|
654
|
+
(default), `'Weekly'` or `'Monthly'`.
|
|
655
|
+
- `apiQuotaUsage()`: the monthly `quota` (`-1` for none) and the window's `usage`.
|
|
656
|
+
- `apiUserActivity()`: edits shown, accepted and ignored, plus active users. Accepts and
|
|
657
|
+
ignores are only counted if you report them with `acceptEdit()` / `rejectEdit()`.
|
|
658
|
+
|
|
659
|
+
A window over a year gets a `403`. Unlike `accountStatus()`, these need a key that isn't
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saplingai/sapling-js",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "JavaScript
|
|
3
|
+
"version": "1.0.55",
|
|
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",
|
|
7
7
|
"types": "./sapling/client.d.ts",
|