docling-serve-client 0.4.0 → 0.5.0

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/src/client.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright 2026 Martin Winkler
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import { readCapabilities } from './capabilities.js';
4
5
  import { errorMessage, fetchJson } from './core/http.js';
5
6
  import { detectInputFormat, type InputFormat } from './core/input.js';
6
7
  import { findTruncatedXref } from './core/pdf.js';
@@ -23,12 +24,15 @@ import type {
23
24
  ConvertInput,
24
25
  DoclingClient,
25
26
  DoclingConfig,
27
+ DoclingSettings,
26
28
  DocumentJob,
27
- Job,
29
+ JobRef,
28
30
  JobState,
29
31
  MarkdownJob,
30
32
  Result,
33
+ ServerCapabilities,
31
34
  ServerVersions,
35
+ WithSettings,
32
36
  } from './types.js';
33
37
 
34
38
  // -------------------------------------------------------------------
@@ -55,15 +59,30 @@ type FormatFields = {
55
59
  pageImages: boolean;
56
60
  };
57
61
 
62
+ /** Parsed JSON answer of a conversion POST, with the settings its form was built from. */
63
+ type Posted = {
64
+ body: unknown;
65
+ settings: DoclingSettings;
66
+ };
67
+
58
68
  // -------------------------------------------------------------------
59
69
  // --- 2. Constants
60
70
  // -------------------------------------------------------------------
61
71
 
62
- /** Fixed conversion form fields sent with every request. */
63
- const FIXED_FORM_FIELDS: [name: string, value: string][] = [
64
- ['do_table_structure', 'true'],
65
- ['table_mode', 'accurate'],
66
- ];
72
+ /** Table structure model; pinned so the server's fallback preset cannot change table output. */
73
+ const TABLE_STRUCTURE_PRESET = 'tableformer_v1_accurate';
74
+
75
+ /** Model of `pipeline: 'vlm'`. */
76
+ const VLM_PIPELINE_PRESET = 'granite_docling';
77
+
78
+ /** Model of `chartExtraction`. */
79
+ const CHART_EXTRACTION_PRESET = 'granite_vision_v4';
80
+
81
+ /** Model of `formulaEnrichment` and `codeEnrichment`. */
82
+ const CODE_FORMULA_PRESET = 'codeformulav2';
83
+
84
+ /** Matches each upper-case letter of a camelCase settings key, for its snake_case form field name. */
85
+ const MATCH_UPPER_CASE = /[A-Z]/g;
67
86
 
68
87
  /** Error message for missing, empty or unrecognized input bytes. */
69
88
  const INVALID_INPUT = 'Expected a non-empty PDF, PNG, JPEG, TIFF or WebP as Uint8Array';
@@ -106,18 +125,29 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
106
125
  submitMarkdown,
107
126
  submitDocument,
108
127
  check: check as DoclingClient['check'],
128
+ capabilities,
109
129
  };
110
130
 
111
131
  /** Converts PDF or image bytes to md in one request. */
112
- async function convertMarkdown(input: ConvertInput, options?: unknown): Promise<Result<ConvertedMarkdown>> {
132
+ async function convertMarkdown(
133
+ input: ConvertInput,
134
+ options?: unknown,
135
+ ): Promise<Result<WithSettings<ConvertedMarkdown>>> {
113
136
  const call = resolveMarkdownCall(defaults, options);
114
137
  const fields: FormatFields = { format: 'md', embedImages: call.output.images === 'extract', pageImages: false };
115
138
  const res = await post('/v1/convert/file', input, call.options, fields, call.signal);
116
- return res.ok ? withVersions(readMarkdown(res.data, call.output)) : res;
139
+ if (!res.ok) {
140
+ return res;
141
+ }
142
+ const converted = await withVersions<ConvertedMarkdown>(readMarkdown(res.data.body, call.output));
143
+ return converted.ok ? ok({ ...converted.data, settings: res.data.settings }) : converted;
117
144
  }
118
145
 
119
146
  /** Converts PDF or image bytes to a DoclingDocument in one request. */
120
- async function convertDocument(input: ConvertInput, options?: unknown): Promise<Result<ConvertedDocument>> {
147
+ async function convertDocument(
148
+ input: ConvertInput,
149
+ options?: unknown,
150
+ ): Promise<Result<WithSettings<ConvertedDocument>>> {
121
151
  const call = resolveDocumentCall(defaults, options);
122
152
  const fields: FormatFields = {
123
153
  format: 'json',
@@ -125,15 +155,21 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
125
155
  pageImages: call.pageImages,
126
156
  };
127
157
  const res = await post('/v1/convert/file', input, call.options, fields, call.signal);
128
- return res.ok ? withVersions(readDocument(res.data)) : res;
158
+ if (!res.ok) {
159
+ return res;
160
+ }
161
+ const converted = await withVersions<ConvertedDocument>(readDocument(res.data.body));
162
+ return converted.ok ? ok({ ...converted.data, settings: res.data.settings }) : converted;
129
163
  }
130
164
 
131
- /** Starts an async md conversion; the job carries the image handling for `check`. */
165
+ /** Starts an async md conversion; the job carries the image handling for `check` and the sent settings. */
132
166
  async function submitMarkdown(input: ConvertInput, options?: unknown): Promise<Result<MarkdownJob>> {
133
167
  const call = resolveMarkdownCall(defaults, options);
134
168
  const fields: FormatFields = { format: 'md', embedImages: call.output.images === 'extract', pageImages: false };
135
- const id = await submit(input, call.options, fields, call.signal);
136
- return id.ok ? ok({ kind: 'markdown', id: id.data, output: call.output }) : id;
169
+ const task = await submit(input, call.options, fields, call.signal);
170
+ return task.ok
171
+ ? ok({ kind: 'markdown', id: task.data.id, output: call.output, settings: task.data.settings })
172
+ : task;
137
173
  }
138
174
 
139
175
  /** Starts an async document conversion; page images need no job field, `check` moves any it finds. */
@@ -144,8 +180,8 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
144
180
  embedImages: call.images === 'embedded',
145
181
  pageImages: call.pageImages,
146
182
  };
147
- const id = await submit(input, call.options, fields, call.signal);
148
- return id.ok ? ok({ kind: 'document', id: id.data }) : id;
183
+ const task = await submit(input, call.options, fields, call.signal);
184
+ return task.ok ? ok({ kind: 'document', id: task.data.id, settings: task.data.settings }) : task;
149
185
  }
150
186
 
151
187
  /**
@@ -156,7 +192,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
156
192
  * `Result already fetched or expired`
157
193
  */
158
194
  async function check(
159
- job: Job,
195
+ job: JobRef,
160
196
  options?: unknown,
161
197
  ): Promise<Result<JobState<ConvertedMarkdown | ConvertedDocument>>> {
162
198
  const { signal } = resolveCheckOptions(options);
@@ -209,18 +245,33 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
209
245
  }
210
246
  }
211
247
 
212
- /** Posts the input to the async endpoint; resolves to the task id. */
248
+ /**
249
+ * **Queries `GET /v1/capabilities` on demand**.
250
+ *
251
+ * - older servers without the endpoint answer `http` (404)
252
+ */
253
+ async function capabilities(options?: unknown): Promise<Result<ServerCapabilities>> {
254
+ const { signal } = resolveCheckOptions(options);
255
+ const res = await get('/v1/capabilities', signal);
256
+ if (!res.ok) {
257
+ return failWithVersions(res.error);
258
+ }
259
+ const read = readCapabilities(res.data);
260
+ return read.ok ? read : failWithVersions(read.error);
261
+ }
262
+
263
+ /** Posts the input to the async endpoint; resolves to the task id and the sent settings. */
213
264
  async function submit(
214
265
  input: ConvertInput,
215
266
  options: ResolvedOptions,
216
267
  fields: FormatFields,
217
268
  signal: AbortSignal | undefined,
218
- ): Promise<Result<string>> {
269
+ ): Promise<Result<{ id: string; settings: DoclingSettings }>> {
219
270
  const res = await post('/v1/convert/file/async', input, options, fields, signal);
220
271
  if (!res.ok) {
221
272
  return res;
222
273
  }
223
- const id: unknown = (res.data as DoclingTaskStatus | null)?.task_id;
274
+ const id: unknown = (res.data.body as DoclingTaskStatus | null)?.task_id;
224
275
  if (readText(id) === undefined) {
225
276
  return failWithVersions(ERR.INVALID_RESPONSE('Response has no task_id'));
226
277
  }
@@ -228,7 +279,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
228
279
  const shown = JSON.stringify(id).slice(0, MAX_SHOWN_ID_LENGTH);
229
280
  return failWithVersions(ERR.INVALID_RESPONSE(`Response has an unusable task_id: ${shown}`));
230
281
  }
231
- return ok(id);
282
+ return ok({ id, settings: res.data.settings });
232
283
  }
233
284
 
234
285
  /** Detects and checks the input format, then sends a multipart POST with timeout; errors already carry versions. */
@@ -238,14 +289,15 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
238
289
  options: ResolvedOptions,
239
290
  fields: FormatFields,
240
291
  signal: AbortSignal | undefined,
241
- ): Promise<Result<unknown>> {
292
+ ): Promise<Result<Posted>> {
242
293
  const format = readInputFormat(input);
243
294
  if (!format.ok) {
244
295
  return format;
245
296
  }
297
+ const settings = buildSettings(format.data, options, fields);
246
298
  let body: FormData;
247
299
  try {
248
- body = buildFormData(input, format.data, options, fields);
300
+ body = buildFormData(input, format.data, settings);
249
301
  } catch (error) {
250
302
  return fail(ERR.INVALID_INPUT(`Input could not be prepared for upload: ${errorMessage(error)}`));
251
303
  }
@@ -256,7 +308,7 @@ export function createDoclingClient(config: DoclingConfig): DoclingClient {
256
308
  body,
257
309
  ...(limited === undefined ? {} : { signal: limited }),
258
310
  });
259
- return res.ok ? res : failWithVersions(res.error);
311
+ return res.ok ? ok({ body: res.data, settings }) : failWithVersions(res.error);
260
312
  }
261
313
 
262
314
  /** GET request; `limit: false` skips `timeoutMs` (used by `/version`). */
@@ -354,60 +406,75 @@ function readInputFormat(input: unknown): Result<InputFormat> {
354
406
  }
355
407
 
356
408
  /**
357
- * **Builds the multipart body**: input file, fixed fields, output format and every docling option explicitly.
409
+ * **Every form field of a conversion request, as typed settings**.
358
410
  *
359
- * - input: `from_formats` (`pdf` or `image`) with a matching file name and MIME type
360
- * - md: `md_compact_tables=true`, no page break placeholder
411
+ * - md: `md_compact_tables=true`, empty page break placeholder; json sends neither
361
412
  * - images: `image_export_mode=embedded` when picture data or page images are requested, else `placeholder`;
362
413
  * `include_images` and `include_page_images` pick which data docling sends
363
414
  * - `images_scale` fixed at `PAGE_IMAGE_SCALE`; it sets picture and page image resolution only, OCR has its own scale
364
415
  * - image input: OCR always on and never full-page, whatever `ocr` and `forceOcr` say
416
+ * - model presets pinned; a preset of a disabled stage is not sent
417
+ * - fields docling-serve would default silently are sent with that default
365
418
  */
366
- function buildFormData(
367
- input: ConvertInput,
368
- { fromFormat, mimeType, fileName }: InputFormat,
419
+ function buildSettings(
420
+ { fromFormat }: InputFormat,
369
421
  options: ResolvedOptions,
370
422
  { format, embedImages, pageImages }: FormatFields,
371
- ): FormData {
423
+ ): DoclingSettings {
424
+ // Image has no text layer: without OCR it yields no text, and full-page OCR merges words.
425
+ const isImage = fromFormat === 'image';
426
+ const isMarkdown = format === 'md';
427
+ return {
428
+ fromFormats: fromFormat,
429
+ toFormats: format,
430
+ imageExportMode: embedImages || pageImages ? 'embedded' : 'placeholder',
431
+ includeImages: embedImages,
432
+ includePageImages: pageImages,
433
+ imagesScale: PAGE_IMAGE_SCALE,
434
+ mdCompactTables: isMarkdown ? true : null,
435
+ mdPageBreakPlaceholder: isMarkdown ? '' : null,
436
+ doOcr: isImage || options.ocr,
437
+ forceOcr: !isImage && options.forceOcr,
438
+ ocrPreset: options.ocrEngine,
439
+ ocrLang: [...(options.ocrLang ?? [])],
440
+ pdfBackend: options.pdfBackend,
441
+ pipeline: options.pipeline,
442
+ vlmPipelinePreset: options.pipeline === 'vlm' ? VLM_PIPELINE_PRESET : null,
443
+ doTableStructure: true,
444
+ tableMode: 'accurate',
445
+ tableStructurePreset: TABLE_STRUCTURE_PRESET,
446
+ tableCellMatching: true,
447
+ doPdfHeadingHierarchy: options.headingHierarchy,
448
+ doFormulaEnrichment: options.formulaEnrichment,
449
+ doCodeEnrichment: options.codeEnrichment,
450
+ codeFormulaPreset: options.formulaEnrichment || options.codeEnrichment ? CODE_FORMULA_PRESET : null,
451
+ doChartExtraction: options.chartExtraction,
452
+ chartExtractionPreset: options.chartExtraction ? CHART_EXTRACTION_PRESET : null,
453
+ doPictureClassification: false,
454
+ doPictureDescription: false,
455
+ pageRange: options.pageRange === undefined ? null : [options.pageRange[0], options.pageRange[1]],
456
+ documentTimeout: options.documentTimeout ?? null,
457
+ abortOnError: false,
458
+ };
459
+ }
460
+
461
+ /**
462
+ * **Builds the multipart body from the settings, so sent fields and returned settings cannot differ**.
463
+ *
464
+ * - field name: settings key in snake_case (`ocrPreset` → `ocr_preset`)
465
+ * - `null` not sent; lists as repeated fields (empty list: not sent); booleans and numbers as strings
466
+ * - input: file name and MIME type matching `from_formats`
467
+ */
468
+ function buildFormData(input: ConvertInput, { mimeType, fileName }: InputFormat, settings: DoclingSettings): FormData {
372
469
  const form = new FormData();
373
470
  // Blob does not accept SharedArrayBuffer-backed input: copy only that into a fresh ArrayBuffer.
374
471
  const bytes = input.buffer instanceof ArrayBuffer ? (input as Uint8Array<ArrayBuffer>) : new Uint8Array(input);
375
472
  form.append('files', new Blob([bytes], { type: mimeType }), fileName);
376
- form.append('from_formats', fromFormat);
377
- for (const [name, value] of FIXED_FORM_FIELDS) {
378
- form.append(name, value);
379
- }
380
- form.append('to_formats', format);
381
- if (format === 'md') {
382
- form.append('md_compact_tables', 'true');
383
- }
384
- form.append('include_images', String(embedImages));
385
- form.append('include_page_images', String(pageImages));
386
- form.append('image_export_mode', embedImages || pageImages ? 'embedded' : 'placeholder');
387
- form.append('images_scale', String(PAGE_IMAGE_SCALE));
388
- form.append('do_pdf_heading_hierarchy', String(options.headingHierarchy));
389
- form.append('do_formula_enrichment', String(options.formulaEnrichment));
390
- form.append('do_code_enrichment', String(options.codeEnrichment));
391
- if (options.formulaEnrichment || options.codeEnrichment) {
392
- form.append('code_formula_preset', 'codeformulav2');
393
- }
394
- form.append('do_chart_extraction', String(options.chartExtraction));
395
- form.append('pipeline', options.pipeline);
396
- form.append('pdf_backend', options.pdfBackend);
397
- // an image has no text layer: without OCR it yields no text, and full-page OCR merges its words
398
- const isImage = fromFormat === 'image';
399
- form.append('do_ocr', String(isImage || options.ocr));
400
- form.append('force_ocr', String(!isImage && options.forceOcr));
401
- // lists are sent as repeated fields
402
- for (const lang of options.ocrLang ?? []) {
403
- form.append('ocr_lang', lang);
404
- }
405
- if (options.pageRange !== undefined) {
406
- form.append('page_range', String(options.pageRange[0]));
407
- form.append('page_range', String(options.pageRange[1]));
408
- }
409
- if (options.documentTimeout !== undefined) {
410
- form.append('document_timeout', String(options.documentTimeout));
473
+ for (const [key, value] of Object.entries(settings)) {
474
+ const name = key.replace(MATCH_UPPER_CASE, (letter) => `_${letter.toLowerCase()}`);
475
+ for (const item of value === null ? [] : Array.isArray(value) ? value : [value]) {
476
+ form.append(name, String(item));
477
+ }
411
478
  }
412
479
  return form;
413
480
  }
package/src/core/input.ts CHANGED
@@ -33,10 +33,10 @@ type ImageSignature = {
33
33
  const IMAGE_SIGNATURES: readonly ImageSignature[] = [
34
34
  { mimeType: 'image/png', extension: 'png', parts: [[0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]]] },
35
35
  { mimeType: 'image/jpeg', extension: 'jpg', parts: [[0, [0xff, 0xd8, 0xff]]] },
36
- // little-endian `II*\0` and big-endian `MM\0*`
36
+ // Little-endian `II*\0` and big-endian `MM\0*`.
37
37
  { mimeType: 'image/tiff', extension: 'tiff', parts: [[0, [0x49, 0x49, 0x2a, 0x00]]] },
38
38
  { mimeType: 'image/tiff', extension: 'tiff', parts: [[0, [0x4d, 0x4d, 0x00, 0x2a]]] },
39
- // `RIFF` <size> `WEBP`
39
+ // `RIFF` <size> `WEBP`.
40
40
  {
41
41
  mimeType: 'image/webp',
42
42
  extension: 'webp',
@@ -59,7 +59,7 @@ const PDF_FORMAT: InputFormat = { fromFormat: 'pdf', mimeType: 'application/pdf'
59
59
  *
60
60
  * - image signatures must start at byte 0; PDF: `%PDF-` within the first 1024 bytes
61
61
  * - images are checked first, so an image whose data happens to contain `%PDF-` is still an image
62
- * @returns `undefined` for empty, non-`Uint8Array` or unrecognized input
62
+ * - returns `undefined` for empty, non-`Uint8Array` or unrecognized input
63
63
  */
64
64
  export function detectInputFormat(bytes: unknown): InputFormat | undefined {
65
65
  if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
package/src/core/pdf.ts CHANGED
@@ -40,11 +40,11 @@ export function isPdf(bytes: unknown): bytes is Uint8Array {
40
40
  }
41
41
 
42
42
  /**
43
- * **Returns cross-reference offset if PDF is truncated**: last 'startxref' points at or past EOF.
43
+ * **Returns cross-reference offset if PDF is truncated**: last `startxref` points at or past EOF.
44
44
  *
45
45
  * - typical for aborted downloads; docling then fails with a vague load error
46
46
  * - only the last 2 KB are read, as latin1
47
- * - returns 'undefined' when offset lies inside file or no 'startxref' found
47
+ * - returns `undefined` when offset lies inside file or no `startxref` found
48
48
  */
49
49
  export function findTruncatedXref(bytes: Uint8Array): number | undefined {
50
50
  const tail = new TextDecoder('latin1').decode(bytes.subarray(Math.max(0, bytes.length - TAIL_LENGTH)));
@@ -74,12 +74,12 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
74
74
  return prototype === Object.prototype || prototype === null;
75
75
  }
76
76
 
77
- /** Checks whether value is a boolean primitive. */
77
+ /** Returns true for boolean primitives (`true` or `false`). */
78
78
  export function isBoolean(value: unknown): value is boolean {
79
79
  return value === true || value === false;
80
80
  }
81
81
 
82
- /** Type guard checking whether string is included in allowed set. */
82
+ /** Returns true when string belongs to allowed set. */
83
83
  export function isOneOf<T extends string>(value: unknown, allowed: readonly T[]): value is T {
84
84
  return typeof value === 'string' && (allowed as readonly string[]).includes(value);
85
85
  }
package/src/images.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { isPlainObject } from './core/validate.js';
5
- import type { DoclingDocument, PageItem, ProvenanceItem } from './document.js';
5
+ import type { BoundingBox, DoclingDocument, PageItem } from './document.js';
6
6
  import type {
7
7
  ExtractedImage,
8
8
  ImageOptions,
@@ -154,7 +154,7 @@ export function extractImages(markdown: string, options: Required<ImageOptions>)
154
154
  }
155
155
  const size = readPngSize(bytes);
156
156
  if (options.omitSmall && size !== undefined && isSmallImage(size)) {
157
- // a marker, so removing it below also normalizes the blank lines around it
157
+ // Marker, so removing it below also normalizes surrounding blank lines.
158
158
  output += OMITTED_IMAGE;
159
159
  continue;
160
160
  }
@@ -218,11 +218,11 @@ export function extractPageImages(document: DoclingDocument): ExtractedPageImage
218
218
  * - `prov`: any `prov[]` entry; for a table cell pass `{ page_no: table.prov[0].page_no, bbox: cell.bbox }`
219
219
  * - flips `BOTTOMLEFT` boxes using the page height from `document.pages`, then scales points by `PAGE_IMAGE_SCALE`
220
220
  * - works without `pageImages: true`; the boxes fit the page images of any conversion by this client
221
- * - returns `undefined` when page_no or a coordinate is not a finite number, or page is missing from `document.pages`
221
+ * - returns `undefined` when `page_no` or coordinate is not a finite number, or page is missing from `document.pages`
222
222
  */
223
223
  export function toPageImageBox(
224
224
  document: DoclingDocument,
225
- prov: Pick<ProvenanceItem, 'page_no' | 'bbox'>,
225
+ prov: { page_no: number; bbox?: BoundingBox | null },
226
226
  ): PageImageBox | undefined {
227
227
  const pageNo = prov?.page_no;
228
228
  if (!isFiniteNumber(pageNo)) {
package/src/index.ts CHANGED
@@ -8,6 +8,9 @@
8
8
  export type * from './document.js';
9
9
  export type {
10
10
  CallOptions,
11
+ CapabilityLimits,
12
+ CapabilityPreset,
13
+ CapabilityStage,
11
14
  ClientOptions,
12
15
  ConversionFailure,
13
16
  ConvertError,
@@ -17,6 +20,7 @@ export type {
17
20
  DoclingClient,
18
21
  DoclingConfig,
19
22
  DoclingOptions,
23
+ DoclingSettings,
20
24
  DocumentImageMode,
21
25
  DocumentJob,
22
26
  DocumentOptions,
@@ -27,12 +31,14 @@ export type {
27
31
  ImageSize,
28
32
  ImageWarning,
29
33
  Job,
34
+ JobRef,
30
35
  JobState,
31
36
  MarkdownImageMode,
32
37
  MarkdownJob,
33
38
  MarkdownJobOutput,
34
39
  MarkdownOptions,
35
40
  MarkdownWarning,
41
+ OcrEngine,
36
42
  PageImage,
37
43
  PageImageBox,
38
44
  PageImageWarning,
@@ -40,11 +46,13 @@ export type {
40
46
  PdfInput,
41
47
  Pipeline,
42
48
  Result,
49
+ ServerCapabilities,
43
50
  ServerDetails,
44
51
  ServerError,
45
52
  ServerVersions,
46
53
  ServerWarning,
47
54
  TaskFailure,
55
+ WithSettings,
48
56
  } from './types.js';
49
57
 
50
58
  // -------------------------------------------------------------------
package/src/options.ts CHANGED
@@ -19,7 +19,7 @@ import type {
19
19
  DocumentImageMode,
20
20
  DocumentOptions,
21
21
  ImageOptions,
22
- Job,
22
+ JobRef,
23
23
  MarkdownImageMode,
24
24
  MarkdownJobOutput,
25
25
  MarkdownOptions,
@@ -89,6 +89,7 @@ export const DEFAULT_OPTIONS: Readonly<ResolvedOptions> = Object.freeze({
89
89
  pdfBackend: 'docling_parse',
90
90
  ocr: true,
91
91
  forceOcr: false,
92
+ ocrEngine: 'auto',
92
93
  });
93
94
 
94
95
  /** Rules for docling options, accepted by the factory and every conversion call. */
@@ -101,6 +102,8 @@ const CALL_OPTION_RULES: Record<keyof DoclingOptions, OptionRule> = {
101
102
  pdfBackend: (value) => isOneOf(value, PDF_BACKENDS),
102
103
  ocr: isBoolean,
103
104
  forceOcr: isBoolean,
105
+ // Open list: presets depend on server installation, unknown IDs are left to server.
106
+ ocrEngine: (value) => typeof value === 'string' && value !== '',
104
107
  };
105
108
 
106
109
  /** Rules for factory options: per-call options plus the factory-only ones. */
@@ -133,7 +136,7 @@ const IMAGE_OPTION_RULES: Record<keyof ImageOptions, OptionRule> = {
133
136
  omitSmall: isBoolean,
134
137
  };
135
138
 
136
- /** Rules for `check` options. */
139
+ /** Rules for `check` and `capabilities` options. */
137
140
  const CHECK_RULES: Record<keyof CallOptions, OptionRule> = {
138
141
  signal: isAbortSignal,
139
142
  };
@@ -196,7 +199,10 @@ export function resolveConfig(config: unknown): ResolvedConfig {
196
199
  };
197
200
  }
198
201
 
199
- /** Validates md call options over factory defaults; throws TypeError on invalid options. */
202
+ /**
203
+ * Validates md call options over factory defaults.
204
+ * @throws `TypeError` on invalid options.
205
+ */
200
206
  export function resolveMarkdownCall(defaults: Readonly<ResolvedOptions>, options: unknown): MarkdownCall {
201
207
  const {
202
208
  signal,
@@ -208,7 +214,10 @@ export function resolveMarkdownCall(defaults: Readonly<ResolvedOptions>, options
208
214
  return { options: { ...defaults, ...docling }, output, ...(signal === undefined ? {} : { signal }) };
209
215
  }
210
216
 
211
- /** Validates document call options over factory defaults; throws TypeError on invalid options. */
217
+ /**
218
+ * Validates document call options over factory defaults.
219
+ * @throws `TypeError` on invalid options.
220
+ */
212
221
  export function resolveDocumentCall(defaults: Readonly<ResolvedOptions>, options: unknown): DocumentCall {
213
222
  const {
214
223
  signal,
@@ -219,7 +228,10 @@ export function resolveDocumentCall(defaults: Readonly<ResolvedOptions>, options
219
228
  return { options: { ...defaults, ...docling }, images, pageImages, ...(signal === undefined ? {} : { signal }) };
220
229
  }
221
230
 
222
- /** Validates `check` call options (signal only); throws TypeError on invalid options. */
231
+ /**
232
+ * Validates signal-only call options (`check`, `capabilities`).
233
+ * @throws `TypeError` on invalid options.
234
+ */
223
235
  export function resolveCheckOptions(options: unknown): CallOptions {
224
236
  return validateKeys<CallOptions>(callObject(options), CHECK_RULES);
225
237
  }
@@ -228,8 +240,9 @@ export function resolveCheckOptions(options: unknown): CallOptions {
228
240
  * **Checks a job object against the shape `submitMarkdown()` and `submitDocument()` return**.
229
241
  *
230
242
  * - never throws: a malformed job is `invalid_input`
243
+ * - `settings` and other extra keys are ignored and not returned
231
244
  */
232
- export function validateJob(job: unknown): Result<Job> {
245
+ export function validateJob(job: unknown): Result<JobRef> {
233
246
  if (!isPlainObject(job) || !isJobId(job.id)) {
234
247
  return fail(ERR.INVALID_INPUT(INVALID_JOB));
235
248
  }
@@ -259,7 +272,10 @@ export function isJobId(value: unknown): value is string {
259
272
  // --- 4. Internal Functions
260
273
  // -------------------------------------------------------------------
261
274
 
262
- /** Returns call options as a plain object (`undefined` becomes `{}`); throws TypeError otherwise. */
275
+ /**
276
+ * Returns call options as a plain object (`undefined` becomes `{}`).
277
+ * @throws `TypeError` when options is not a plain object.
278
+ */
263
279
  function callObject(options: unknown): Record<string, unknown> {
264
280
  if (options === undefined) {
265
281
  return {};
package/src/response.ts CHANGED
@@ -212,7 +212,7 @@ function readServerError(error: DoclingErrorItem): ServerWarning[] {
212
212
  ];
213
213
  }
214
214
 
215
- /** docling `page_no`: integer >= 1; `0`, negatives and fractions are rejected. */
215
+ /** Docling `page_no`: integer >= 1; 0, negatives and fractions are rejected. */
216
216
  function isPageNumber(value: unknown): value is number {
217
217
  return typeof value === 'number' && Number.isInteger(value) && value >= 1;
218
218
  }