@modelprofile.com/flexharness 9.8.0 → 9.9.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '9.8.0',
6
+ version: '9.9.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLDBFQUEwRTtDQUN4RixDQUFBIn0=
@@ -18,6 +18,11 @@ export interface IDocumentRequestOptions {
18
18
  * refused.
19
19
  */
20
20
  maxPages?: number;
21
+ /**
22
+ * The longest edge, in pixels, of an image document as it is sent to the model; a larger image is
23
+ * scaled down to it. Defaults to `DEFAULT_MAX_IMAGE_EDGE`.
24
+ */
25
+ maxImageEdge?: number;
21
26
  /**
22
27
  * Called once for every model call of the request, as soon as its usage is known, whatever the
23
28
  * request's outcome: `reported` with the provider's usage, or `unreported` with the reason when
@@ -31,7 +36,19 @@ export interface IDocumentOptions extends IDocumentRequestOptions {
31
36
  model: plugins.TSmartAiLanguageModel;
32
37
  systemMessage?: string;
33
38
  userMessage: string;
34
- pdfDocuments: Uint8Array[];
39
+ /** PDFs, rendered page by page. The model sees their pages first, in the order given. */
40
+ pdfDocuments?: Uint8Array[];
41
+ /**
42
+ * Images, after the PDFs' pages, in the order given: JPEG, PNG, WebP, GIF, TIFF (every page) and
43
+ * AVIF. A JPEG, PNG or WebP within `maxImageEdge` and `MAX_IMAGE_BYTES` that needs no turning is
44
+ * sent as it is. Any other is turned upright by its EXIF orientation, scaled down to fit
45
+ * `maxImageEdge` and sent as JPEG (from a JPEG or AVIF, transparency on white) or PNG, for this
46
+ * request only. Each image and each TIFF page is a page of the request. Refused with
47
+ * `DocumentFormatError` before the model is called: an image that cannot be read, a format outside
48
+ * these, HEIC (not decodable here), a GIF or WebP of more than one frame, an AVIF of more than one
49
+ * image, and an image or TIFF page of more than `MAX_IMAGE_PIXELS`. Needs the optional peer dependency `sharp`.
50
+ */
51
+ imageDocuments?: Uint8Array[];
35
52
  messageHistory?: plugins.ModelMessage[];
36
53
  }
37
54
  export interface IPdfTextExtractionOptions extends IDocumentRequestOptions {
@@ -51,7 +68,10 @@ export interface IDocumentUsage {
51
68
  export interface IDocumentAnalysisResult {
52
69
  text: string;
53
70
  usage: IDocumentUsage;
54
- /** Rendered pages sent to the model. A page that renders to an empty image is not sent. */
71
+ /**
72
+ * Pages sent to the model: the PDFs' rendered pages and the images' pages. A PDF page that renders
73
+ * to an empty image is not sent.
74
+ */
55
75
  pageCount: number;
56
76
  }
57
77
  export interface IDocumentPageLimitErrorDetails {
@@ -78,6 +98,51 @@ export declare class DocumentPageLimitError extends Error {
78
98
  readonly documentIndex: number;
79
99
  constructor(details: IDocumentPageLimitErrorDetails, options?: ErrorOptions);
80
100
  }
101
+ /**
102
+ * The longest edge, in pixels, of an image sent to the model when a request names none: Anthropic's
103
+ * 2000 pixels per side for a request with more than 20 images, the tightest of the limits above.
104
+ */
105
+ export declare const DEFAULT_MAX_IMAGE_EDGE = 2000;
106
+ /**
107
+ * The most bytes of one image sent to the model: 5 000 000 characters once base64-encoded, within
108
+ * Anthropic's 5 MB per image on Amazon Bedrock and Google Cloud, the smallest of the limits above.
109
+ */
110
+ export declare const MAX_IMAGE_BYTES = 3750000;
111
+ /**
112
+ * The most pixels (width times height) of one image or TIFF page that is decoded: an A3 page scanned
113
+ * at 600 dpi has about 70 million. The images come from mail and uploads, so a larger one is refused
114
+ * from its header, before it is decoded, instead of being given sharp's own limit of about 268 million.
115
+ */
116
+ export declare const MAX_IMAGE_PIXELS = 100000000;
117
+ /** Why an image document is refused. */
118
+ export type TDocumentFormatRefusal =
119
+ /** it could not be read at all */
120
+ 'unreadable'
121
+ /** it is a format this component does not read */
122
+ | 'unsupported'
123
+ /** it is HEIC, which the prebuilt sharp cannot decode */
124
+ | 'heic'
125
+ /** it is a GIF or WebP of more than one frame: frames are no pages, and none is dropped */
126
+ | 'animated'
127
+ /** it is an AVIF container of more than one image: which of them is the document is not known, and none is dropped */
128
+ | 'multiple_images'
129
+ /** it has more pixels than `MAX_IMAGE_PIXELS` */
130
+ | 'too_many_pixels'
131
+ /** it stays larger than `MAX_IMAGE_BYTES` after scaling */
132
+ | 'too_large';
133
+ /** An image document this component does not read; the model was not called. */
134
+ export declare class DocumentFormatError extends Error {
135
+ /** Zero-based position, in the request's documents (the PDFs first, then the images), of the image. */
136
+ readonly documentIndex: number;
137
+ /** The format the image was read as, or null when it could not be read at all. */
138
+ readonly format: string | null;
139
+ readonly reason: TDocumentFormatRefusal;
140
+ constructor(message: string, details: {
141
+ documentIndex: number;
142
+ format: string | null;
143
+ reason: TDocumentFormatRefusal;
144
+ }, options?: ErrorOptions);
145
+ }
81
146
  /** The model-based PDF OCR recipe formerly provided by SmartOCR. */
82
147
  export declare function extractTextFromPdf(pdfDocument: Uint8Array, options: IPdfTextExtractionOptions): Promise<string>;
83
148
  /** `extractTextFromPdf` that also returns the request's token usage and the pages sent. */
@@ -39,6 +39,165 @@ export class DocumentPageLimitError extends Error {
39
39
  this.documentIndex = details.documentIndex;
40
40
  }
41
41
  }
42
+ /*
43
+ * The image limits below stay within what the providers publish (read 2026-09-28):
44
+ * - Anthropic, https://platform.claude.com/docs/en/build-with-claude/vision: at most 8000x8000 px per
45
+ * image; in a request with more than 20 images at most 2000 px per side; at most 10 MB per image
46
+ * (base64) on the Claude API and 5 MB (base64) on Amazon Bedrock and Google Cloud; JPEG, PNG, GIF
47
+ * and WebP, of an animation only the first frame.
48
+ * - OpenAI, https://developers.openai.com/api/docs/guides/images-vision: PNG, JPEG, WebP and
49
+ * non-animated GIF; up to 512 MB payload and 1500 images per request; the page names no size per
50
+ * image. Images are scaled down on the provider's side.
51
+ */
52
+ /**
53
+ * The longest edge, in pixels, of an image sent to the model when a request names none: Anthropic's
54
+ * 2000 pixels per side for a request with more than 20 images, the tightest of the limits above.
55
+ */
56
+ export const DEFAULT_MAX_IMAGE_EDGE = 2000;
57
+ /**
58
+ * The most bytes of one image sent to the model: 5 000 000 characters once base64-encoded, within
59
+ * Anthropic's 5 MB per image on Amazon Bedrock and Google Cloud, the smallest of the limits above.
60
+ */
61
+ export const MAX_IMAGE_BYTES = 3_750_000;
62
+ /**
63
+ * The most pixels (width times height) of one image or TIFF page that is decoded: an A3 page scanned
64
+ * at 600 dpi has about 70 million. The images come from mail and uploads, so a larger one is refused
65
+ * from its header, before it is decoded, instead of being given sharp's own limit of about 268 million.
66
+ */
67
+ export const MAX_IMAGE_PIXELS = 100_000_000;
68
+ /** An image document this component does not read; the model was not called. */
69
+ export class DocumentFormatError extends Error {
70
+ /** Zero-based position, in the request's documents (the PDFs first, then the images), of the image. */
71
+ documentIndex;
72
+ /** The format the image was read as, or null when it could not be read at all. */
73
+ format;
74
+ reason;
75
+ constructor(message, details, options) {
76
+ super(message, options);
77
+ this.name = 'DocumentFormatError';
78
+ this.documentIndex = details.documentIndex;
79
+ this.format = details.format;
80
+ this.reason = details.reason;
81
+ }
82
+ }
83
+ /** The formats sent as they are when they need no scaling and no turning. */
84
+ const AS_IS_MEDIA_TYPES = new Map([
85
+ ['jpeg', 'image/jpeg'],
86
+ ['png', 'image/png'],
87
+ ['webp', 'image/webp'],
88
+ ]);
89
+ /** The formats read at all: sharp's names, HEIF only with AV1 compression (AVIF). */
90
+ const READABLE_IMAGE_FORMATS = new Set(['jpeg', 'png', 'webp', 'gif', 'tiff', 'heif']);
91
+ /** The formats whose further pages are frames of an animation, not pages of a document. */
92
+ const ANIMATED_FORMATS = new Set(['gif', 'webp']);
93
+ /** The one load of the optional peer dependency sharp; a failed load is forgotten. */
94
+ let sharpLoad = null;
95
+ function loadSharp() {
96
+ if (!sharpLoad) {
97
+ const load = plugins.importSharp().catch((error) => {
98
+ if (sharpLoad === load) {
99
+ sharpLoad = null;
100
+ }
101
+ throw new Error('Image documents need the optional peer dependency sharp.', { cause: error });
102
+ });
103
+ sharpLoad = load;
104
+ }
105
+ return sharpLoad;
106
+ }
107
+ /** Refuses a page with more pixels than `MAX_IMAGE_PIXELS`, from what its header says. */
108
+ function checkPixels(metadata, documentIndex, format) {
109
+ const pixels = (metadata.width ?? 0) * (metadata.height ?? 0);
110
+ if (pixels > MAX_IMAGE_PIXELS) {
111
+ throw new DocumentFormatError(`The image has ${metadata.width}x${metadata.height} pixels, more than the ${MAX_IMAGE_PIXELS} read here.`, { documentIndex, format, reason: 'too_many_pixels' });
112
+ }
113
+ }
114
+ /** What an image document is, read from its header without decoding it. */
115
+ async function readImage(sharp, image, documentIndex) {
116
+ let metadata;
117
+ try {
118
+ metadata = await sharp(image).metadata();
119
+ }
120
+ catch (error) {
121
+ throw new DocumentFormatError('The image could not be read.', { documentIndex, format: null, reason: 'unreadable' }, { cause: error });
122
+ }
123
+ const format = metadata.format ?? null;
124
+ if (format === 'heif' && metadata.compression !== 'av1') {
125
+ throw new DocumentFormatError('The image is HEIC, which cannot be decoded here; send it as JPEG.', { documentIndex, format, reason: 'heic' });
126
+ }
127
+ if (!format || !READABLE_IMAGE_FORMATS.has(format)) {
128
+ throw new DocumentFormatError(`The image is ${format ?? 'of an unknown format'}; images are read as JPEG, PNG, WebP, GIF, TIFF or AVIF.`, { documentIndex, format, reason: 'unsupported' });
129
+ }
130
+ const pages = Math.max(metadata.pages ?? 1, 1);
131
+ if (ANIMATED_FORMATS.has(format) && pages > 1) {
132
+ throw new DocumentFormatError(`The image is an animated ${format === 'gif' ? 'GIF' : 'WebP'} of ${pages} frames; its frames are no pages, so it is not read.`, { documentIndex, format, reason: 'animated' });
133
+ }
134
+ // sharp reports the top-level images of a HEIF container as its pages (libvips heifload, n-pages);
135
+ // only a TIFF's pages are pages of a document
136
+ if (format !== 'tiff' && pages > 1) {
137
+ throw new DocumentFormatError(`The image is an AVIF holding ${pages} images; which of them is the document is not known, so it is not read.`, { documentIndex, format, reason: 'multiple_images' });
138
+ }
139
+ checkPixels(metadata, documentIndex, format);
140
+ return { metadata, pageCount: format === 'tiff' ? pages : 1 };
141
+ }
142
+ /** One page of an image document, upright, scaled down and encoded; a page that cannot be decoded is refused. */
143
+ async function imagePage(sharp, image, page, format, photo, maxEdge, documentIndex) {
144
+ const pipeline = () => sharp(image, { page, limitInputPixels: MAX_IMAGE_PIXELS })
145
+ .autoOrient()
146
+ .resize({ width: maxEdge, height: maxEdge, fit: 'inside', withoutEnlargement: true });
147
+ // JPEG has no transparency: a transparent background becomes white, not black
148
+ const jpeg = async (quality) => ({
149
+ data: await pipeline().flatten({ background: '#ffffff' }).jpeg({ quality }).toBuffer(),
150
+ mediaType: 'image/jpeg',
151
+ });
152
+ let encoded;
153
+ try {
154
+ if (page > 0) {
155
+ // each TIFF page has its own size; the first was checked with the document
156
+ checkPixels(await sharp(image, { page }).metadata(), documentIndex, format);
157
+ }
158
+ encoded = photo ? await jpeg(90) : { data: await pipeline().png().toBuffer(), mediaType: 'image/png' };
159
+ // too large: a photo tries a lower quality, any other a JPEG at the higher one first
160
+ for (const quality of photo ? [75] : [90, 75]) {
161
+ if (encoded.data.byteLength <= MAX_IMAGE_BYTES) {
162
+ break;
163
+ }
164
+ encoded = await jpeg(quality);
165
+ }
166
+ }
167
+ catch (error) {
168
+ if (error instanceof DocumentFormatError) {
169
+ throw error;
170
+ }
171
+ throw new DocumentFormatError(pageLabel(page, 'could not be decoded.'), { documentIndex, format, reason: 'unreadable' }, { cause: error });
172
+ }
173
+ if (encoded.data.byteLength > MAX_IMAGE_BYTES) {
174
+ throw new DocumentFormatError(pageLabel(page, `stays larger than ${MAX_IMAGE_BYTES} bytes after scaling to ${maxEdge} pixels.`), { documentIndex, format, reason: 'too_large' });
175
+ }
176
+ // a Buffer is a Uint8Array view of a pooled ArrayBuffer; send exactly its bytes
177
+ return { data: new Uint8Array(encoded.data), mediaType: encoded.mediaType };
178
+ }
179
+ /** "The image …" for a one-page image, "Page n of the image …" for a TIFF page after the first. */
180
+ const pageLabel = (page, predicate) => page === 0 ? `The image ${predicate}` : `Page ${page + 1} of the image ${predicate}`;
181
+ /** An image document's pages as they are sent: as they are where that fits, else upright and scaled down. */
182
+ async function imagePages(sharp, image, metadata, pageCount, maxEdge, documentIndex, abortSignal) {
183
+ const format = metadata.format;
184
+ const asIs = AS_IS_MEDIA_TYPES.get(format);
185
+ if (asIs
186
+ && pageCount === 1
187
+ && (metadata.orientation ?? 1) === 1
188
+ && (metadata.width ?? Infinity) <= maxEdge
189
+ && (metadata.height ?? Infinity) <= maxEdge
190
+ && image.byteLength <= MAX_IMAGE_BYTES) {
191
+ return [{ data: image, mediaType: asIs }];
192
+ }
193
+ const photo = format === 'jpeg' || format === 'heif';
194
+ const pages = [];
195
+ for (let page = 0; page < pageCount; page++) {
196
+ abortSignal?.throwIfAborted();
197
+ pages.push(await imagePage(sharp, image, page, format, photo, maxEdge, documentIndex));
198
+ }
199
+ return pages;
200
+ }
42
201
  const DEFAULT_OCR_SYSTEM_MESSAGE = 'You are a precise OCR engine. Extract text faithfully and do not summarize.';
43
202
  const DEFAULT_OCR_USER_MESSAGE = 'Extract all readable text from this PDF. Preserve page order, line breaks, table structure, and obvious headings. Return only the extracted text.';
44
203
  /** The model-based PDF OCR recipe formerly provided by SmartOCR. */
@@ -68,41 +227,64 @@ export async function analyzeDocumentsWithUsage(options) {
68
227
  if (maxPages !== undefined && (!Number.isSafeInteger(maxPages) || maxPages < 1)) {
69
228
  throw new RangeError('maxPages must be a positive whole number.');
70
229
  }
230
+ const maxImageEdge = options.maxImageEdge ?? DEFAULT_MAX_IMAGE_EDGE;
231
+ if (!Number.isSafeInteger(maxImageEdge) || maxImageEdge < 1) {
232
+ throw new RangeError('maxImageEdge must be a positive whole number.');
233
+ }
71
234
  const abortSignal = options.abortSignal;
72
235
  abortSignal?.throwIfAborted();
73
- const imagePages = [];
74
- for (const [index, doc] of options.pdfDocuments.entries()) {
236
+ const pdfDocuments = options.pdfDocuments ?? [];
237
+ const imageDocuments = options.imageDocuments ?? [];
238
+ const documentCount = pdfDocuments.length + imageDocuments.length;
239
+ const limitDetails = (index, renderedPages, limit) => ({
240
+ renderedPages,
241
+ maxPages: limit,
242
+ documentCount,
243
+ documentIndex: index,
244
+ allDocumentsRendered: index === documentCount - 1,
245
+ });
246
+ const modelPages = [];
247
+ // every rendered page counts against maxPages, also a PDF page that renders empty and is not sent
248
+ let countedPages = 0;
249
+ for (const [index, doc] of pdfDocuments.entries()) {
75
250
  const pdf = await ensureSmartpdf();
76
251
  abortSignal?.throwIfAborted();
77
- const limitDetails = (renderedPages, limit) => ({
78
- renderedPages,
79
- maxPages: limit,
80
- documentCount: options.pdfDocuments.length,
81
- documentIndex: index,
82
- allDocumentsRendered: index === options.pdfDocuments.length - 1,
83
- });
84
252
  // SmartPDF limits one document, so each render gets the pages the request has left. It takes
85
253
  // no limit of zero: when the earlier documents used up the request's pages, the smallest limit
86
254
  // lets the document's page count be known, and a document of one page renders and is refused below.
87
- const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - imagePages.length, 1);
255
+ const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - countedPages, 1);
88
256
  let pages;
89
257
  try {
90
258
  pages = await pdf.convertPDFToPngBytes(doc, { signal: abortSignal, maxPages: renderMaxPages });
91
259
  }
92
260
  catch (error) {
93
261
  if (maxPages !== undefined && error instanceof plugins.smartpdf.SmartPdfPageLimitError) {
94
- throw new DocumentPageLimitError(limitDetails(imagePages.length + error.pageCount, maxPages), { cause: error });
262
+ throw new DocumentPageLimitError(limitDetails(index, countedPages + error.pageCount, maxPages), { cause: error });
95
263
  }
96
264
  throw error;
97
265
  }
98
266
  abortSignal?.throwIfAborted();
99
- imagePages.push(...pages);
100
- if (maxPages !== undefined && imagePages.length > maxPages) {
101
- throw new DocumentPageLimitError(limitDetails(imagePages.length, maxPages));
267
+ countedPages += pages.length;
268
+ modelPages.push(...pages.filter((page) => page && page.length > 0).map((page) => ({ data: page, mediaType: 'image/png' })));
269
+ if (maxPages !== undefined && countedPages > maxPages) {
270
+ throw new DocumentPageLimitError(limitDetails(index, countedPages, maxPages));
102
271
  }
103
272
  }
104
- // Filter out empty buffers
105
- const validPages = imagePages.filter(page => page && page.length > 0);
273
+ if (imageDocuments.length) {
274
+ const sharp = await loadSharp();
275
+ for (const [imageIndex, image] of imageDocuments.entries()) {
276
+ abortSignal?.throwIfAborted();
277
+ const index = pdfDocuments.length + imageIndex;
278
+ const { metadata, pageCount } = await readImage(sharp, image, index);
279
+ // an image's pages are known from its header, so an image over the limit is not decoded
280
+ if (maxPages !== undefined && countedPages + pageCount > maxPages) {
281
+ throw new DocumentPageLimitError(limitDetails(index, countedPages + pageCount, maxPages));
282
+ }
283
+ countedPages += pageCount;
284
+ modelPages.push(...await imagePages(sharp, image, metadata, pageCount, maxImageEdge, index, abortSignal));
285
+ }
286
+ abortSignal?.throwIfAborted();
287
+ }
106
288
  const onUsage = options.onUsage;
107
289
  const modelCalls = onUsage ? new plugins.AgentModelCallUsageRecorder(options.model, onUsage) : null;
108
290
  // Each attempt, retries included, is one model call: it starts in the middleware, which also
@@ -130,10 +312,10 @@ export async function analyzeDocumentsWithUsage(options) {
130
312
  role: 'user',
131
313
  content: [
132
314
  { type: 'text', text: options.userMessage },
133
- ...validPages.map(page => ({
315
+ ...modelPages.map(page => ({
134
316
  type: 'file',
135
- data: page,
136
- mediaType: 'image/png',
317
+ data: page.data,
318
+ mediaType: page.mediaType,
137
319
  })),
138
320
  ],
139
321
  },
@@ -172,7 +354,7 @@ export async function analyzeDocumentsWithUsage(options) {
172
354
  cacheReadTokens: usage.inputTokenDetails.cacheReadTokens ?? 0,
173
355
  cacheWriteTokens: usage.inputTokenDetails.cacheWriteTokens ?? 0,
174
356
  },
175
- pageCount: validPages.length,
357
+ pageCount: modelPages.length,
176
358
  };
177
359
  }
178
360
  export async function stopSmartpdf() {
@@ -193,4 +375,4 @@ export async function stopSmartpdf() {
193
375
  }
194
376
  await instance.stop();
195
377
  }
196
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19kb2N1bWVudC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QywrR0FBK0c7QUFDL0csSUFBSSxhQUFhLEdBQThCLElBQUksQ0FBQztBQUVwRCxTQUFTLGNBQWM7SUFDckIsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQ25CLE1BQU0sUUFBUSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNqRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3BELGFBQWEsR0FBRyxLQUFLLENBQUM7UUFDdEIsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDZixJQUFJLGFBQWEsS0FBSyxLQUFLLEVBQUUsQ0FBQztnQkFDNUIsYUFBYSxHQUFHLElBQUksQ0FBQztZQUN2QixDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTyxhQUFhLENBQUM7QUFDdkIsQ0FBQztBQXlFRCwrRkFBK0Y7QUFDL0YsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDL0M7Ozs7O09BS0c7SUFDYSxhQUFhLENBQVM7SUFDdEIsUUFBUSxDQUFTO0lBQ2pDLGdHQUFnRztJQUNoRixhQUFhLENBQVM7SUFFdEMsWUFBWSxPQUF1QyxFQUFFLE9BQXNCO1FBQ3pFLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLEtBQUssQ0FBQztZQUN6QyxDQUFDLENBQUMsb0JBQW9CLE9BQU8sQ0FBQyxhQUFhLFFBQVE7WUFDbkQsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0I7Z0JBQzVCLENBQUMsQ0FBQyxzQkFBc0IsT0FBTyxDQUFDLGFBQWEsUUFBUTtnQkFDckQs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
378
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19kb2N1bWVudC9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQUl4QywrR0FBK0c7QUFDL0csSUFBSSxhQUFhLEdBQThCLElBQUksQ0FBQztBQUVwRCxTQUFTLGNBQWM7SUFDckIsSUFBSSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQ25CLE1BQU0sUUFBUSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUNqRCxNQUFNLEtBQUssR0FBRyxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3BELGFBQWEsR0FBRyxLQUFLLENBQUM7UUFDdEIsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUU7WUFDZixJQUFJLGFBQWEsS0FBSyxLQUFLLEVBQUUsQ0FBQztnQkFDNUIsYUFBYSxHQUFHLElBQUksQ0FBQztZQUN2QixDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUM7SUFDTCxDQUFDO0lBQ0QsT0FBTyxhQUFhLENBQUM7QUFDdkIsQ0FBQztBQTZGRCwrRkFBK0Y7QUFDL0YsTUFBTSxPQUFPLHNCQUF1QixTQUFRLEtBQUs7SUFDL0M7Ozs7O09BS0c7SUFDYSxhQUFhLENBQVM7SUFDdEIsUUFBUSxDQUFTO0lBQ2pDLGdHQUFnRztJQUNoRixhQUFhLENBQVM7SUFFdEMsWUFBWSxPQUF1QyxFQUFFLE9BQXNCO1FBQ3pFLE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxhQUFhLEtBQUssQ0FBQztZQUN6QyxDQUFDLENBQUMsb0JBQW9CLE9BQU8sQ0FBQyxhQUFhLFFBQVE7WUFDbkQsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0I7Z0JBQzVCLENBQUMsQ0FBQyxzQkFBc0IsT0FBTyxDQUFDLGFBQWEsUUFBUTtnQkFDckQs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
@@ -5,3 +5,38 @@ export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder
5
5
  export type { ModelMessage } from 'ai';
6
6
  export type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
7
7
  export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
8
+ /** What sharp reads from an image's header (`sharp.Metadata`, the fields used here). */
9
+ export interface IImageMetadata {
10
+ format?: string;
11
+ compression?: string;
12
+ pages?: number;
13
+ orientation?: number;
14
+ width?: number;
15
+ height?: number;
16
+ }
17
+ /** A sharp pipeline, the operations used here. */
18
+ export interface IImagePipeline {
19
+ metadata(): Promise<IImageMetadata>;
20
+ autoOrient(): IImagePipeline;
21
+ resize(options: {
22
+ width: number;
23
+ height: number;
24
+ fit: 'inside';
25
+ withoutEnlargement: boolean;
26
+ }): IImagePipeline;
27
+ flatten(options: {
28
+ background: string;
29
+ }): IImagePipeline;
30
+ jpeg(options: {
31
+ quality: number;
32
+ }): IImagePipeline;
33
+ png(): IImagePipeline;
34
+ toBuffer(): Promise<Uint8Array>;
35
+ }
36
+ /** Opens an image (a page of it) as a pipeline, as `sharp(input, options)` does. */
37
+ export type TImageReader = (input: Uint8Array, options?: {
38
+ page?: number;
39
+ limitInputPixels?: number;
40
+ }) => IImagePipeline;
41
+ /** Loads sharp and hands out its reader. */
42
+ export declare const importSharp: () => Promise<TImageReader>;
@@ -2,4 +2,9 @@ import { generateText, wrapLanguageModel } from 'ai';
2
2
  import * as smartpdf from '@push.rocks/smartpdf';
3
3
  import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
4
4
  export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
5
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2RvY3VtZW50L3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFlBQVksRUFBRSxpQkFBaUIsRUFBRSxNQUFNLElBQUksQ0FBQztBQUNyRCxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsaUJBQWlCLEVBQUUsUUFBUSxFQUFFLDJCQUEyQixFQUFFLENBQUMifQ==
5
+ /** Loads sharp and hands out its reader. */
6
+ export const importSharp = async () => {
7
+ const sharp = (await import('sharp')).default;
8
+ return (input, options) => sharp(input, options);
9
+ };
10
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2RvY3VtZW50L3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFlBQVksRUFBRSxpQkFBaUIsRUFBRSxNQUFNLElBQUksQ0FBQztBQUNyRCxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHFDQUFxQyxDQUFDO0FBQ2xGLE9BQU8sRUFBRSxZQUFZLEVBQUUsaUJBQWlCLEVBQUUsUUFBUSxFQUFFLDJCQUEyQixFQUFFLENBQUM7QUFrQ2xGLDRDQUE0QztBQUM1QyxNQUFNLENBQUMsTUFBTSxXQUFXLEdBQUcsS0FBSyxJQUEyQixFQUFFO0lBQzNELE1BQU0sS0FBSyxHQUFHLENBQUMsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUM7SUFDOUMsT0FBTyxDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRSxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFDbkQsQ0FBQyxDQUFDIn0=
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Task Venture Capital GmbH",
3
3
  "license": "MIT",
4
4
  "name": "@modelprofile.com/flexharness",
5
- "version": "9.8.0",
5
+ "version": "9.9.0",
6
6
  "type": "module",
7
7
  "description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
8
8
  "main": "./dist_ts/index.js",
@@ -62,8 +62,8 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "@modelprofile.com/flexharness-agent": "9.8.0",
66
- "@modelprofile.com/flexharness-models": "9.8.0",
65
+ "@modelprofile.com/flexharness-agent": "9.9.0",
66
+ "@modelprofile.com/flexharness-models": "9.9.0",
67
67
  "@push.rocks/smartrequest": "^5.0.1",
68
68
  "ai": "^7.0.107",
69
69
  "zod": "^4.4.1"
@@ -71,6 +71,7 @@
71
71
  "peerDependencies": {
72
72
  "@modelcontextprotocol/sdk": "^1.29.0",
73
73
  "@push.rocks/smartpdf": "^6.0.0",
74
+ "sharp": "^0.35.3",
74
75
  "puppeteer": "^24.42.0",
75
76
  "ink": "^6.8.0",
76
77
  "ink-text-input": "^6.0.0",
@@ -85,6 +86,9 @@
85
86
  "@push.rocks/smartpdf": {
86
87
  "optional": true
87
88
  },
89
+ "sharp": {
90
+ "optional": true
91
+ },
88
92
  "puppeteer": {
89
93
  "optional": true
90
94
  },
package/readme.md CHANGED
@@ -62,7 +62,7 @@ or provider SDKs.
62
62
  | `/chat/cli` | Ink/React terminal chat | `ink ink-text-input react`; TypeScript: `@types/react` |
63
63
  | `/chat/web` | Lit `flexchat-window`, `flexchat-message`, `flexchat-input` | `lit` |
64
64
  | `/kvm` | Browser control, terminal framing and OCR observation | `puppeteer` |
65
- | `/documents` | PDF processing and `extractTextFromPdf` | `@push.rocks/smartpdf` |
65
+ | `/documents` | PDF and image documents, `extractTextFromPdf` | `@push.rocks/smartpdf`; for image documents `sharp` |
66
66
  | `/mcp` | MCP clients and AI SDK tool conversion | `@modelcontextprotocol/sdk` |
67
67
  | `/migration` | Existing versioned harness migrations | None |
68
68
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '9.8.0',
6
+ version: '9.9.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
package/ts/readme.md CHANGED
@@ -62,7 +62,7 @@ or provider SDKs.
62
62
  | `/chat/cli` | Ink/React terminal chat | `ink ink-text-input react`; TypeScript: `@types/react` |
63
63
  | `/chat/web` | Lit `flexchat-window`, `flexchat-message`, `flexchat-input` | `lit` |
64
64
  | `/kvm` | Browser control, terminal framing and OCR observation | `puppeteer` |
65
- | `/documents` | PDF processing and `extractTextFromPdf` | `@push.rocks/smartpdf` |
65
+ | `/documents` | PDF and image documents, `extractTextFromPdf` | `@push.rocks/smartpdf`; for image documents `sharp` |
66
66
  | `/mcp` | MCP clients and AI SDK tool conversion | `@modelcontextprotocol/sdk` |
67
67
  | `/migration` | Existing versioned harness migrations | None |
68
68
 
@@ -38,6 +38,11 @@ export interface IDocumentRequestOptions {
38
38
  * refused.
39
39
  */
40
40
  maxPages?: number;
41
+ /**
42
+ * The longest edge, in pixels, of an image document as it is sent to the model; a larger image is
43
+ * scaled down to it. Defaults to `DEFAULT_MAX_IMAGE_EDGE`.
44
+ */
45
+ maxImageEdge?: number;
41
46
  /**
42
47
  * Called once for every model call of the request, as soon as its usage is known, whatever the
43
48
  * request's outcome: `reported` with the provider's usage, or `unreported` with the reason when
@@ -52,7 +57,19 @@ export interface IDocumentOptions extends IDocumentRequestOptions {
52
57
  model: plugins.TSmartAiLanguageModel;
53
58
  systemMessage?: string;
54
59
  userMessage: string;
55
- pdfDocuments: Uint8Array[];
60
+ /** PDFs, rendered page by page. The model sees their pages first, in the order given. */
61
+ pdfDocuments?: Uint8Array[];
62
+ /**
63
+ * Images, after the PDFs' pages, in the order given: JPEG, PNG, WebP, GIF, TIFF (every page) and
64
+ * AVIF. A JPEG, PNG or WebP within `maxImageEdge` and `MAX_IMAGE_BYTES` that needs no turning is
65
+ * sent as it is. Any other is turned upright by its EXIF orientation, scaled down to fit
66
+ * `maxImageEdge` and sent as JPEG (from a JPEG or AVIF, transparency on white) or PNG, for this
67
+ * request only. Each image and each TIFF page is a page of the request. Refused with
68
+ * `DocumentFormatError` before the model is called: an image that cannot be read, a format outside
69
+ * these, HEIC (not decodable here), a GIF or WebP of more than one frame, an AVIF of more than one
70
+ * image, and an image or TIFF page of more than `MAX_IMAGE_PIXELS`. Needs the optional peer dependency `sharp`.
71
+ */
72
+ imageDocuments?: Uint8Array[];
56
73
  messageHistory?: plugins.ModelMessage[];
57
74
  }
58
75
 
@@ -75,7 +92,10 @@ export interface IDocumentUsage {
75
92
  export interface IDocumentAnalysisResult {
76
93
  text: string;
77
94
  usage: IDocumentUsage;
78
- /** Rendered pages sent to the model. A page that renders to an empty image is not sent. */
95
+ /**
96
+ * Pages sent to the model: the PDFs' rendered pages and the images' pages. A PDF page that renders
97
+ * to an empty image is not sent.
98
+ */
79
99
  pageCount: number;
80
100
  }
81
101
 
@@ -117,6 +137,251 @@ export class DocumentPageLimitError extends Error {
117
137
  }
118
138
  }
119
139
 
140
+ /*
141
+ * The image limits below stay within what the providers publish (read 2026-09-28):
142
+ * - Anthropic, https://platform.claude.com/docs/en/build-with-claude/vision: at most 8000x8000 px per
143
+ * image; in a request with more than 20 images at most 2000 px per side; at most 10 MB per image
144
+ * (base64) on the Claude API and 5 MB (base64) on Amazon Bedrock and Google Cloud; JPEG, PNG, GIF
145
+ * and WebP, of an animation only the first frame.
146
+ * - OpenAI, https://developers.openai.com/api/docs/guides/images-vision: PNG, JPEG, WebP and
147
+ * non-animated GIF; up to 512 MB payload and 1500 images per request; the page names no size per
148
+ * image. Images are scaled down on the provider's side.
149
+ */
150
+
151
+ /**
152
+ * The longest edge, in pixels, of an image sent to the model when a request names none: Anthropic's
153
+ * 2000 pixels per side for a request with more than 20 images, the tightest of the limits above.
154
+ */
155
+ export const DEFAULT_MAX_IMAGE_EDGE = 2000;
156
+
157
+ /**
158
+ * The most bytes of one image sent to the model: 5 000 000 characters once base64-encoded, within
159
+ * Anthropic's 5 MB per image on Amazon Bedrock and Google Cloud, the smallest of the limits above.
160
+ */
161
+ export const MAX_IMAGE_BYTES = 3_750_000;
162
+
163
+ /**
164
+ * The most pixels (width times height) of one image or TIFF page that is decoded: an A3 page scanned
165
+ * at 600 dpi has about 70 million. The images come from mail and uploads, so a larger one is refused
166
+ * from its header, before it is decoded, instead of being given sharp's own limit of about 268 million.
167
+ */
168
+ export const MAX_IMAGE_PIXELS = 100_000_000;
169
+
170
+ /** Why an image document is refused. */
171
+ export type TDocumentFormatRefusal =
172
+ /** it could not be read at all */
173
+ | 'unreadable'
174
+ /** it is a format this component does not read */
175
+ | 'unsupported'
176
+ /** it is HEIC, which the prebuilt sharp cannot decode */
177
+ | 'heic'
178
+ /** it is a GIF or WebP of more than one frame: frames are no pages, and none is dropped */
179
+ | 'animated'
180
+ /** it is an AVIF container of more than one image: which of them is the document is not known, and none is dropped */
181
+ | 'multiple_images'
182
+ /** it has more pixels than `MAX_IMAGE_PIXELS` */
183
+ | 'too_many_pixels'
184
+ /** it stays larger than `MAX_IMAGE_BYTES` after scaling */
185
+ | 'too_large';
186
+
187
+ /** An image document this component does not read; the model was not called. */
188
+ export class DocumentFormatError extends Error {
189
+ /** Zero-based position, in the request's documents (the PDFs first, then the images), of the image. */
190
+ public readonly documentIndex: number;
191
+ /** The format the image was read as, or null when it could not be read at all. */
192
+ public readonly format: string | null;
193
+ public readonly reason: TDocumentFormatRefusal;
194
+
195
+ constructor(
196
+ message: string,
197
+ details: { documentIndex: number; format: string | null; reason: TDocumentFormatRefusal },
198
+ options?: ErrorOptions,
199
+ ) {
200
+ super(message, options);
201
+ this.name = 'DocumentFormatError';
202
+ this.documentIndex = details.documentIndex;
203
+ this.format = details.format;
204
+ this.reason = details.reason;
205
+ }
206
+ }
207
+
208
+ /** A page as it is sent to the model. */
209
+ interface IModelPage {
210
+ data: Uint8Array;
211
+ mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
212
+ }
213
+
214
+ /** The formats sent as they are when they need no scaling and no turning. */
215
+ const AS_IS_MEDIA_TYPES: ReadonlyMap<string, IModelPage['mediaType']> = new Map([
216
+ ['jpeg', 'image/jpeg'],
217
+ ['png', 'image/png'],
218
+ ['webp', 'image/webp'],
219
+ ]);
220
+
221
+ /** The formats read at all: sharp's names, HEIF only with AV1 compression (AVIF). */
222
+ const READABLE_IMAGE_FORMATS: ReadonlySet<string> = new Set(['jpeg', 'png', 'webp', 'gif', 'tiff', 'heif']);
223
+
224
+ /** The formats whose further pages are frames of an animation, not pages of a document. */
225
+ const ANIMATED_FORMATS: ReadonlySet<string> = new Set(['gif', 'webp']);
226
+
227
+ /** The one load of the optional peer dependency sharp; a failed load is forgotten. */
228
+ let sharpLoad: Promise<plugins.TImageReader> | null = null;
229
+
230
+ function loadSharp(): Promise<plugins.TImageReader> {
231
+ if (!sharpLoad) {
232
+ const load = plugins.importSharp().catch((error: unknown) => {
233
+ if (sharpLoad === load) {
234
+ sharpLoad = null;
235
+ }
236
+ throw new Error('Image documents need the optional peer dependency sharp.', { cause: error });
237
+ });
238
+ sharpLoad = load;
239
+ }
240
+ return sharpLoad;
241
+ }
242
+
243
+ /** Refuses a page with more pixels than `MAX_IMAGE_PIXELS`, from what its header says. */
244
+ function checkPixels(metadata: plugins.IImageMetadata, documentIndex: number, format: string): void {
245
+ const pixels = (metadata.width ?? 0) * (metadata.height ?? 0);
246
+ if (pixels > MAX_IMAGE_PIXELS) {
247
+ throw new DocumentFormatError(
248
+ `The image has ${metadata.width}x${metadata.height} pixels, more than the ${MAX_IMAGE_PIXELS} read here.`,
249
+ { documentIndex, format, reason: 'too_many_pixels' },
250
+ );
251
+ }
252
+ }
253
+
254
+ /** What an image document is, read from its header without decoding it. */
255
+ async function readImage(
256
+ sharp: plugins.TImageReader,
257
+ image: Uint8Array,
258
+ documentIndex: number,
259
+ ): Promise<{ metadata: plugins.IImageMetadata; pageCount: number }> {
260
+ let metadata: plugins.IImageMetadata;
261
+ try {
262
+ metadata = await sharp(image).metadata();
263
+ } catch (error) {
264
+ throw new DocumentFormatError('The image could not be read.', { documentIndex, format: null, reason: 'unreadable' }, { cause: error });
265
+ }
266
+ const format = metadata.format ?? null;
267
+ if (format === 'heif' && metadata.compression !== 'av1') {
268
+ throw new DocumentFormatError(
269
+ 'The image is HEIC, which cannot be decoded here; send it as JPEG.',
270
+ { documentIndex, format, reason: 'heic' },
271
+ );
272
+ }
273
+ if (!format || !READABLE_IMAGE_FORMATS.has(format)) {
274
+ throw new DocumentFormatError(
275
+ `The image is ${format ?? 'of an unknown format'}; images are read as JPEG, PNG, WebP, GIF, TIFF or AVIF.`,
276
+ { documentIndex, format, reason: 'unsupported' },
277
+ );
278
+ }
279
+ const pages = Math.max(metadata.pages ?? 1, 1);
280
+ if (ANIMATED_FORMATS.has(format) && pages > 1) {
281
+ throw new DocumentFormatError(
282
+ `The image is an animated ${format === 'gif' ? 'GIF' : 'WebP'} of ${pages} frames; its frames are no pages, so it is not read.`,
283
+ { documentIndex, format, reason: 'animated' },
284
+ );
285
+ }
286
+ // sharp reports the top-level images of a HEIF container as its pages (libvips heifload, n-pages);
287
+ // only a TIFF's pages are pages of a document
288
+ if (format !== 'tiff' && pages > 1) {
289
+ throw new DocumentFormatError(
290
+ `The image is an AVIF holding ${pages} images; which of them is the document is not known, so it is not read.`,
291
+ { documentIndex, format, reason: 'multiple_images' },
292
+ );
293
+ }
294
+ checkPixels(metadata, documentIndex, format);
295
+ return { metadata, pageCount: format === 'tiff' ? pages : 1 };
296
+ }
297
+
298
+ /** One page of an image document, upright, scaled down and encoded; a page that cannot be decoded is refused. */
299
+ async function imagePage(
300
+ sharp: plugins.TImageReader,
301
+ image: Uint8Array,
302
+ page: number,
303
+ format: string,
304
+ photo: boolean,
305
+ maxEdge: number,
306
+ documentIndex: number,
307
+ ): Promise<IModelPage> {
308
+ const pipeline = () => sharp(image, { page, limitInputPixels: MAX_IMAGE_PIXELS })
309
+ .autoOrient()
310
+ .resize({ width: maxEdge, height: maxEdge, fit: 'inside', withoutEnlargement: true });
311
+ // JPEG has no transparency: a transparent background becomes white, not black
312
+ const jpeg = async (quality: number): Promise<IModelPage> => ({
313
+ data: await pipeline().flatten({ background: '#ffffff' }).jpeg({ quality }).toBuffer(),
314
+ mediaType: 'image/jpeg',
315
+ });
316
+ let encoded: IModelPage;
317
+ try {
318
+ if (page > 0) {
319
+ // each TIFF page has its own size; the first was checked with the document
320
+ checkPixels(await sharp(image, { page }).metadata(), documentIndex, format);
321
+ }
322
+ encoded = photo ? await jpeg(90) : { data: await pipeline().png().toBuffer(), mediaType: 'image/png' };
323
+ // too large: a photo tries a lower quality, any other a JPEG at the higher one first
324
+ for (const quality of photo ? [75] : [90, 75]) {
325
+ if (encoded.data.byteLength <= MAX_IMAGE_BYTES) {
326
+ break;
327
+ }
328
+ encoded = await jpeg(quality);
329
+ }
330
+ } catch (error) {
331
+ if (error instanceof DocumentFormatError) {
332
+ throw error;
333
+ }
334
+ throw new DocumentFormatError(
335
+ pageLabel(page, 'could not be decoded.'),
336
+ { documentIndex, format, reason: 'unreadable' },
337
+ { cause: error },
338
+ );
339
+ }
340
+ if (encoded.data.byteLength > MAX_IMAGE_BYTES) {
341
+ throw new DocumentFormatError(
342
+ pageLabel(page, `stays larger than ${MAX_IMAGE_BYTES} bytes after scaling to ${maxEdge} pixels.`),
343
+ { documentIndex, format, reason: 'too_large' },
344
+ );
345
+ }
346
+ // a Buffer is a Uint8Array view of a pooled ArrayBuffer; send exactly its bytes
347
+ return { data: new Uint8Array(encoded.data), mediaType: encoded.mediaType };
348
+ }
349
+
350
+ /** "The image …" for a one-page image, "Page n of the image …" for a TIFF page after the first. */
351
+ const pageLabel = (page: number, predicate: string): string =>
352
+ page === 0 ? `The image ${predicate}` : `Page ${page + 1} of the image ${predicate}`;
353
+
354
+ /** An image document's pages as they are sent: as they are where that fits, else upright and scaled down. */
355
+ async function imagePages(
356
+ sharp: plugins.TImageReader,
357
+ image: Uint8Array,
358
+ metadata: plugins.IImageMetadata,
359
+ pageCount: number,
360
+ maxEdge: number,
361
+ documentIndex: number,
362
+ abortSignal: AbortSignal | undefined,
363
+ ): Promise<IModelPage[]> {
364
+ const format = metadata.format!;
365
+ const asIs = AS_IS_MEDIA_TYPES.get(format);
366
+ if (
367
+ asIs
368
+ && pageCount === 1
369
+ && (metadata.orientation ?? 1) === 1
370
+ && (metadata.width ?? Infinity) <= maxEdge
371
+ && (metadata.height ?? Infinity) <= maxEdge
372
+ && image.byteLength <= MAX_IMAGE_BYTES
373
+ ) {
374
+ return [{ data: image, mediaType: asIs }];
375
+ }
376
+ const photo = format === 'jpeg' || format === 'heif';
377
+ const pages: IModelPage[] = [];
378
+ for (let page = 0; page < pageCount; page++) {
379
+ abortSignal?.throwIfAborted();
380
+ pages.push(await imagePage(sharp, image, page, format, photo, maxEdge, documentIndex));
381
+ }
382
+ return pages;
383
+ }
384
+
120
385
  const DEFAULT_OCR_SYSTEM_MESSAGE = 'You are a precise OCR engine. Extract text faithfully and do not summarize.';
121
386
  const DEFAULT_OCR_USER_MESSAGE = 'Extract all readable text from this PDF. Preserve page order, line breaks, table structure, and obvious headings. Return only the extracted text.';
122
387
 
@@ -156,42 +421,65 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
156
421
  if (maxPages !== undefined && (!Number.isSafeInteger(maxPages) || maxPages < 1)) {
157
422
  throw new RangeError('maxPages must be a positive whole number.');
158
423
  }
424
+ const maxImageEdge = options.maxImageEdge ?? DEFAULT_MAX_IMAGE_EDGE;
425
+ if (!Number.isSafeInteger(maxImageEdge) || maxImageEdge < 1) {
426
+ throw new RangeError('maxImageEdge must be a positive whole number.');
427
+ }
159
428
  const abortSignal = options.abortSignal;
160
429
  abortSignal?.throwIfAborted();
161
430
 
162
- const imagePages: Uint8Array[] = [];
163
- for (const [index, doc] of options.pdfDocuments.entries()) {
431
+ const pdfDocuments = options.pdfDocuments ?? [];
432
+ const imageDocuments = options.imageDocuments ?? [];
433
+ const documentCount = pdfDocuments.length + imageDocuments.length;
434
+ const limitDetails = (index: number, renderedPages: number, limit: number): IDocumentPageLimitErrorDetails => ({
435
+ renderedPages,
436
+ maxPages: limit,
437
+ documentCount,
438
+ documentIndex: index,
439
+ allDocumentsRendered: index === documentCount - 1,
440
+ });
441
+ const modelPages: IModelPage[] = [];
442
+ // every rendered page counts against maxPages, also a PDF page that renders empty and is not sent
443
+ let countedPages = 0;
444
+ for (const [index, doc] of pdfDocuments.entries()) {
164
445
  const pdf = await ensureSmartpdf();
165
446
  abortSignal?.throwIfAborted();
166
- const limitDetails = (renderedPages: number, limit: number): IDocumentPageLimitErrorDetails => ({
167
- renderedPages,
168
- maxPages: limit,
169
- documentCount: options.pdfDocuments.length,
170
- documentIndex: index,
171
- allDocumentsRendered: index === options.pdfDocuments.length - 1,
172
- });
173
447
  // SmartPDF limits one document, so each render gets the pages the request has left. It takes
174
448
  // no limit of zero: when the earlier documents used up the request's pages, the smallest limit
175
449
  // lets the document's page count be known, and a document of one page renders and is refused below.
176
- const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - imagePages.length, 1);
450
+ const renderMaxPages = maxPages === undefined ? undefined : Math.max(maxPages - countedPages, 1);
177
451
  let pages: Uint8Array[];
178
452
  try {
179
453
  pages = await pdf.convertPDFToPngBytes(doc, { signal: abortSignal, maxPages: renderMaxPages });
180
454
  } catch (error) {
181
455
  if (maxPages !== undefined && error instanceof plugins.smartpdf.SmartPdfPageLimitError) {
182
- throw new DocumentPageLimitError(limitDetails(imagePages.length + error.pageCount, maxPages), { cause: error });
456
+ throw new DocumentPageLimitError(limitDetails(index, countedPages + error.pageCount, maxPages), { cause: error });
183
457
  }
184
458
  throw error;
185
459
  }
186
460
  abortSignal?.throwIfAborted();
187
- imagePages.push(...pages);
188
- if (maxPages !== undefined && imagePages.length > maxPages) {
189
- throw new DocumentPageLimitError(limitDetails(imagePages.length, maxPages));
461
+ countedPages += pages.length;
462
+ modelPages.push(...pages.filter((page) => page && page.length > 0).map((page) => ({ data: page, mediaType: 'image/png' as const })));
463
+ if (maxPages !== undefined && countedPages > maxPages) {
464
+ throw new DocumentPageLimitError(limitDetails(index, countedPages, maxPages));
190
465
  }
191
466
  }
192
467
 
193
- // Filter out empty buffers
194
- const validPages = imagePages.filter(page => page && page.length > 0);
468
+ if (imageDocuments.length) {
469
+ const sharp = await loadSharp();
470
+ for (const [imageIndex, image] of imageDocuments.entries()) {
471
+ abortSignal?.throwIfAborted();
472
+ const index = pdfDocuments.length + imageIndex;
473
+ const { metadata, pageCount } = await readImage(sharp, image, index);
474
+ // an image's pages are known from its header, so an image over the limit is not decoded
475
+ if (maxPages !== undefined && countedPages + pageCount > maxPages) {
476
+ throw new DocumentPageLimitError(limitDetails(index, countedPages + pageCount, maxPages));
477
+ }
478
+ countedPages += pageCount;
479
+ modelPages.push(...await imagePages(sharp, image, metadata, pageCount, maxImageEdge, index, abortSignal));
480
+ }
481
+ abortSignal?.throwIfAborted();
482
+ }
195
483
 
196
484
  const onUsage = options.onUsage;
197
485
  const modelCalls = onUsage ? new plugins.AgentModelCallUsageRecorder(options.model, onUsage) : null;
@@ -219,10 +507,10 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
219
507
  role: 'user',
220
508
  content: [
221
509
  { type: 'text', text: options.userMessage },
222
- ...validPages.map(page => ({
510
+ ...modelPages.map(page => ({
223
511
  type: 'file' as const,
224
- data: page,
225
- mediaType: 'image/png' as const,
512
+ data: page.data,
513
+ mediaType: page.mediaType,
226
514
  })),
227
515
  ],
228
516
  },
@@ -260,7 +548,7 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
260
548
  cacheReadTokens: usage.inputTokenDetails.cacheReadTokens ?? 0,
261
549
  cacheWriteTokens: usage.inputTokenDetails.cacheWriteTokens ?? 0,
262
550
  },
263
- pageCount: validPages.length,
551
+ pageCount: modelPages.length,
264
552
  };
265
553
  }
266
554
 
@@ -5,3 +5,38 @@ export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder
5
5
  export type { ModelMessage } from 'ai';
6
6
  export type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
7
7
  export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
8
+
9
+ // sharp is an optional peer dependency: it is loaded when a request carries its first image
10
+ // document, so a project that sends only PDFs does not install it. What of it this component uses is
11
+ // typed here, structurally, so that no declaration of /documents names sharp: a project without it
12
+ // type-checks /documents all the same.
13
+
14
+ /** What sharp reads from an image's header (`sharp.Metadata`, the fields used here). */
15
+ export interface IImageMetadata {
16
+ format?: string;
17
+ compression?: string;
18
+ pages?: number;
19
+ orientation?: number;
20
+ width?: number;
21
+ height?: number;
22
+ }
23
+
24
+ /** A sharp pipeline, the operations used here. */
25
+ export interface IImagePipeline {
26
+ metadata(): Promise<IImageMetadata>;
27
+ autoOrient(): IImagePipeline;
28
+ resize(options: { width: number; height: number; fit: 'inside'; withoutEnlargement: boolean }): IImagePipeline;
29
+ flatten(options: { background: string }): IImagePipeline;
30
+ jpeg(options: { quality: number }): IImagePipeline;
31
+ png(): IImagePipeline;
32
+ toBuffer(): Promise<Uint8Array>;
33
+ }
34
+
35
+ /** Opens an image (a page of it) as a pipeline, as `sharp(input, options)` does. */
36
+ export type TImageReader = (input: Uint8Array, options?: { page?: number; limitInputPixels?: number }) => IImagePipeline;
37
+
38
+ /** Loads sharp and hands out its reader. */
39
+ export const importSharp = async (): Promise<TImageReader> => {
40
+ const sharp = (await import('sharp')).default;
41
+ return (input, options) => sharp(input, options);
42
+ };
@@ -1,4 +1,4 @@
1
- PDF processing and language-model document recipes.
1
+ PDF and image processing and language-model document recipes.
2
2
 
3
3
  Part of the [FlexHarness toolbox](https://code.foss.global/modelprofile.com/flexharness).
4
4
  Install the components your project uses. Provider adapters are registered explicitly;
@@ -6,6 +6,8 @@ the agent and harness do not install or register vendor SDKs.
6
6
 
7
7
  ```sh
8
8
  pnpm add @modelprofile.com/flexharness @push.rocks/smartpdf
9
+ # for image documents as well
10
+ pnpm add sharp
9
11
  ```
10
12
 
11
13
  ```typescript
@@ -23,11 +25,46 @@ Supply a model from your selected FlexHarness provider. `extractTextFromPdf` rep
23
25
 
24
26
  `analyzeDocuments({ model, userMessage, pdfDocuments, systemMessage?, messageHistory? })` supports general analysis across multiple PDFs. This component owns a shared SmartPDF instance; call `stopSmartpdf()` when all document work has finished. Image-only OCR is available independently in `flexharness/ocr`.
25
27
 
26
- Both functions accept four request options:
28
+ `analyzeDocuments` also takes `imageDocuments`, sent after the PDFs' pages in the order given. They need the optional peer dependency `sharp`, loaded with the first image:
29
+
30
+ - JPEG, PNG and WebP that fit `maxImageEdge` and `MAX_IMAGE_BYTES` and need no turning go to the model as they are.
31
+ - Any other image is turned upright by its EXIF orientation, scaled down to fit `maxImageEdge` and sent as JPEG (from a JPEG or AVIF) or PNG (from PNG, WebP, GIF or TIFF), for this request only; nothing is stored. A PNG that stays larger than `MAX_IMAGE_BYTES` is sent as JPEG. A transparent background becomes white in a JPEG.
32
+ - Each image and each TIFF page is a page of the request. Their count is read from the header, so an image over `maxPages` is refused with `DocumentPageLimitError` before it is decoded.
33
+ - A TIFF that stores a pyramid, its lower resolutions as further pages, sends every level as a page: sharp's metadata does not tell such levels from the pages of a document, and none is dropped.
34
+ - Refused with `DocumentFormatError` before the model is called, its `reason` saying why:
35
+ - `unreadable`: the image cannot be read, or a page of it cannot be decoded;
36
+ - `unsupported`: none of JPEG, PNG, WebP, GIF, TIFF and AVIF;
37
+ - `heic`: the prebuilt sharp decodes AVIF but has no HEVC decoder;
38
+ - `animated`: a GIF or WebP of more than one frame, since frames are no pages and none is dropped;
39
+ - `multiple_images`: an AVIF container of more than one top-level image, since which of them is the document is not known and none is dropped;
40
+ - `too_many_pixels`: an image or TIFF page of more than `MAX_IMAGE_PIXELS` (100 000 000, an A3 page at 600 dpi has about 70 million), refused from its header before anything is decoded;
41
+ - `too_large`: an image that stays larger than `MAX_IMAGE_BYTES` after scaling.
42
+
43
+ The limits stay within what the providers publish (read 2026-09-28):
44
+
45
+ - Anthropic, <https://platform.claude.com/docs/en/build-with-claude/vision>: at most 8000x8000 px per image, at most 2000 px per side in a request with more than 20 images, at most 10 MB per image (base64) on the Claude API and 5 MB (base64) on Amazon Bedrock and Google Cloud.
46
+ - OpenAI, <https://developers.openai.com/api/docs/guides/images-vision>: PNG, JPEG, WebP and non-animated GIF, up to 512 MB payload and 1500 images per request; the page names no limit per image.
47
+ - `DEFAULT_MAX_IMAGE_EDGE` (2000 pixels) is Anthropic's limit for a request with more than 20 images, the tightest; `MAX_IMAGE_BYTES` (3 750 000 bytes, 5 000 000 characters once base64-encoded) is within Anthropic's 5 MB on Amazon Bedrock and Google Cloud.
48
+
49
+ ```typescript
50
+ import { analyzeDocumentsWithUsage, DocumentFormatError } from '@modelprofile.com/flexharness/documents';
51
+
52
+ const { text, pageCount } = await analyzeDocumentsWithUsage({
53
+ model,
54
+ userMessage: 'Read the invoice: supplier, date, number, net, VAT and gross amounts.',
55
+ imageDocuments: [photoBytes],
56
+ maxPages: 20,
57
+ });
58
+ ```
59
+
60
+ The request options below apply to image documents as they do to PDFs: an abort is checked before each document and each TIFF page, and before the model call.
61
+
62
+ Both functions accept these request options:
27
63
 
28
64
  - `providerOptions` is passed to the model call unchanged, for example `{ openai: { store: false } }` so the provider does not keep the request as application state.
29
65
  - `abortSignal` is checked before each document is rendered and before the model call, and is passed to both the render and the model call, so an abort also stops a document that is rendering. The request rejects with the signal's reason.
30
66
  - `maxPages` is the most rendered pages one request may send to the model, counted over all its documents. A request with more pages rejects with `DocumentPageLimitError` before the model is called. Each document is rendered only when its page count fits the pages the request has left, so the document that exceeds the limit and every document after it are not rendered; SmartPDF counts that document's pages without rendering them. The exception is a one-page document that comes after the request's pages are used up: SmartPDF takes no limit of zero pages, so that document renders and is then refused. The error carries `renderedPages` (the pages of the documents up to and including the one that exceeded the limit), `maxPages` and `documentIndex` (the zero-based position of that document), and SmartPDF's `SmartPdfPageLimitError` as its `cause` when SmartPDF refused the document.
67
+ - `maxImageEdge` is the longest edge, in pixels, of an image document as it is sent; see above.
31
68
  - `onUsage` receives the usage of each model call of the request, also of a request that fails or is aborted; see [Usage of a request that fails or is aborted](#usage-of-a-request-that-fails-or-is-aborted).
32
69
 
33
70
  `extractTextFromPdfWithUsage` and `analyzeDocumentsWithUsage` take the same arguments and return `{ text, usage, pageCount }`. `usage` has the shape of an agent run's usage (`inputTokens`, `outputTokens`, `totalTokens`, `cacheReadTokens`, `cacheWriteTokens`), summed over the request's model steps, with usage the provider leaves out counted as zero; `pageCount` is the number of rendered pages sent to the model.