@modelprofile.com/flexharness 10.18.0 → 10.20.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: '10.18.0',
6
+ version: '10.20.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwrQkFBK0I7SUFDckMsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLDBFQUEwRTtDQUN4RixDQUFBIn0=
@@ -5,6 +5,12 @@ export interface ISmartAiOcrImageInput {
5
5
  dataBase64: string;
6
6
  mimeType: TSmartAiOcrImageMimeType;
7
7
  }
8
+ /** Mistral OCR reads PDF, PPTX, DOCX and other document formats. */
9
+ export type TSmartAiOcrDocumentMimeType = 'application/pdf' | 'application/vnd.openxmlformats-officedocument.presentationml.presentation' | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | string;
10
+ export interface ISmartAiOcrDocumentInput {
11
+ dataBase64: string;
12
+ mimeType: TSmartAiOcrDocumentMimeType;
13
+ }
8
14
  export interface ISmartAiOcrPageResult {
9
15
  index: number;
10
16
  text: string;
@@ -19,6 +25,10 @@ export interface ISmartAiOcrResult<TRaw = unknown> {
19
25
  export interface ISmartAiOcrEngine {
20
26
  recognizeImage: (input: ISmartAiOcrImageInput, options?: ISmartAiMistralOcrRecognizeOptions) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
21
27
  }
28
+ /** The Mistral OCR engine also reads whole documents, such as a PDF with all its pages. */
29
+ export interface ISmartAiMistralOcrEngine extends ISmartAiOcrEngine {
30
+ recognizeDocument: (input: ISmartAiOcrDocumentInput, options?: ISmartAiMistralOcrRecognizeOptions) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
31
+ }
22
32
  export interface IMistralOcrPageConfidenceScores {
23
33
  average_page_confidence_score?: number;
24
34
  averagePageConfidenceScore?: number;
@@ -51,8 +61,17 @@ export interface IMistralOcrRequest {
51
61
  extract_footer?: boolean;
52
62
  confidence_scores_granularity?: TSmartAiMistralOcrConfidenceScoresGranularity;
53
63
  }
64
+ /** A document (PDF, PPTX, DOCX and other formats) as a URL or a base64 data URL; the other fields are those of an image request. */
65
+ export interface IMistralOcrDocumentRequest extends Omit<IMistralOcrRequest, 'document'> {
66
+ document: {
67
+ type: 'document_url';
68
+ document_url: string;
69
+ };
70
+ }
54
71
  export interface ISmartAiMistralOcrTransport {
55
72
  process: (request: IMistralOcrRequest) => Promise<IMistralOcrResponse>;
73
+ /** Reads documents. A transport without it reads images only, and `recognizeDocument` refuses documents. */
74
+ processDocument?: (request: IMistralOcrDocumentRequest) => Promise<IMistralOcrResponse>;
56
75
  }
57
76
  export interface ISmartAiMistralOcrOptions {
58
77
  apiKey?: string;
@@ -72,4 +91,4 @@ export interface ISmartAiMistralOcrRecognizeOptions {
72
91
  extractFooter?: boolean;
73
92
  confidenceScoresGranularity?: TSmartAiMistralOcrConfidenceScoresGranularity;
74
93
  }
75
- export declare const createMistralOcrEngine: (options?: ISmartAiMistralOcrOptions) => ISmartAiOcrEngine;
94
+ export declare const createMistralOcrEngine: (options?: ISmartAiMistralOcrOptions) => ISmartAiMistralOcrEngine;
@@ -1,32 +1,50 @@
1
1
  const defaultMistralOcrModel = 'mistral-ocr-latest';
2
2
  const defaultMistralOcrEndpointUrl = 'https://api.mistral.ai/v1/ocr';
3
3
  const createMistralOcrHttpTransport = (options) => {
4
- return {
5
- process: async (request) => {
6
- if (!options.apiKey) {
7
- throw new Error('Mistral OCR requires an apiKey when no custom transport is provided.');
8
- }
9
- const response = await fetch(options.endpointUrl ?? defaultMistralOcrEndpointUrl, {
10
- method: 'POST',
11
- headers: {
12
- Authorization: `Bearer ${options.apiKey}`,
13
- 'Content-Type': 'application/json',
14
- },
15
- body: JSON.stringify(request),
16
- });
17
- if (!response.ok) {
18
- const errorBody = await response.text();
19
- throw new Error(`Mistral OCR request failed with status ${response.status}: ${errorBody}`);
20
- }
21
- return (await response.json());
22
- },
4
+ const post = async (request) => {
5
+ if (!options.apiKey) {
6
+ throw new Error('Mistral OCR requires an apiKey when no custom transport is provided.');
7
+ }
8
+ const response = await fetch(options.endpointUrl ?? defaultMistralOcrEndpointUrl, {
9
+ method: 'POST',
10
+ headers: {
11
+ Authorization: `Bearer ${options.apiKey}`,
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify(request),
15
+ });
16
+ if (!response.ok) {
17
+ const errorBody = await response.text();
18
+ throw new Error(`Mistral OCR request failed with status ${response.status}: ${errorBody}`);
19
+ }
20
+ return (await response.json());
23
21
  };
22
+ return { process: post, processDocument: post };
24
23
  };
25
24
  const getPageConfidence = (page) => {
26
25
  const confidenceScores = page.confidence_scores ?? page.confidenceScores;
27
26
  return (confidenceScores?.average_page_confidence_score ??
28
27
  confidenceScores?.averagePageConfidenceScore);
29
28
  };
29
+ const toOcrResult = (response) => {
30
+ const pages = response.pages.map((page) => ({
31
+ index: page.index,
32
+ text: page.markdown,
33
+ confidence: getPageConfidence(page),
34
+ }));
35
+ const pageConfidences = pages
36
+ .map((page) => page.confidence)
37
+ .filter((confidence) => typeof confidence === 'number');
38
+ const confidence = pageConfidences.length
39
+ ? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
40
+ : undefined;
41
+ return {
42
+ text: pages.map((page) => page.text).join('\n\n').trim(),
43
+ confidence,
44
+ pages,
45
+ raw: response,
46
+ };
47
+ };
30
48
  export const createMistralOcrEngine = (options = {}) => {
31
49
  const transport = options.transport ??
32
50
  createMistralOcrHttpTransport({
@@ -34,6 +52,13 @@ export const createMistralOcrEngine = (options = {}) => {
34
52
  endpointUrl: options.endpointUrl,
35
53
  });
36
54
  const model = options.model ?? defaultMistralOcrModel;
55
+ const settingsOf = (recognizeOptions) => ({
56
+ include_image_base64: recognizeOptions.includeImageBase64 ?? options.includeImageBase64 ?? false,
57
+ table_format: recognizeOptions.tableFormat ?? options.tableFormat,
58
+ extract_header: recognizeOptions.extractHeader ?? options.extractHeader,
59
+ extract_footer: recognizeOptions.extractFooter ?? options.extractFooter,
60
+ confidence_scores_granularity: recognizeOptions.confidenceScoresGranularity ?? options.confidenceScoresGranularity,
61
+ });
37
62
  return {
38
63
  recognizeImage: async (input, recognizeOptions = {}) => {
39
64
  if (!input.dataBase64) {
@@ -42,36 +67,34 @@ export const createMistralOcrEngine = (options = {}) => {
42
67
  if (!input.mimeType) {
43
68
  throw new Error('Mistral OCR image input requires mimeType.');
44
69
  }
45
- const response = await transport.process({
70
+ return toOcrResult(await transport.process({
46
71
  model,
47
72
  document: {
48
73
  type: 'image_url',
49
74
  image_url: `data:${input.mimeType};base64,${input.dataBase64}`,
50
75
  },
51
- include_image_base64: recognizeOptions.includeImageBase64 ?? options.includeImageBase64 ?? false,
52
- table_format: recognizeOptions.tableFormat ?? options.tableFormat,
53
- extract_header: recognizeOptions.extractHeader ?? options.extractHeader,
54
- extract_footer: recognizeOptions.extractFooter ?? options.extractFooter,
55
- confidence_scores_granularity: recognizeOptions.confidenceScoresGranularity ?? options.confidenceScoresGranularity,
56
- });
57
- const pages = response.pages.map((page) => ({
58
- index: page.index,
59
- text: page.markdown,
60
- confidence: getPageConfidence(page),
76
+ ...settingsOf(recognizeOptions),
77
+ }));
78
+ },
79
+ recognizeDocument: async (input, recognizeOptions = {}) => {
80
+ if (!transport.processDocument) {
81
+ throw new Error('This Mistral OCR transport reads images only: it has no processDocument, so it cannot read documents.');
82
+ }
83
+ if (!input.dataBase64) {
84
+ throw new Error('Mistral OCR document input requires dataBase64.');
85
+ }
86
+ if (!input.mimeType) {
87
+ throw new Error('Mistral OCR document input requires mimeType.');
88
+ }
89
+ return toOcrResult(await transport.processDocument({
90
+ model,
91
+ document: {
92
+ type: 'document_url',
93
+ document_url: `data:${input.mimeType};base64,${input.dataBase64}`,
94
+ },
95
+ ...settingsOf(recognizeOptions),
61
96
  }));
62
- const pageConfidences = pages
63
- .map((page) => page.confidence)
64
- .filter((confidence) => typeof confidence === 'number');
65
- const confidence = pageConfidences.length
66
- ? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
67
- : undefined;
68
- return {
69
- text: pages.map((page) => page.text).join('\n\n').trim(),
70
- confidence,
71
- pages,
72
- raw: response,
73
- };
74
97
  },
75
98
  };
76
99
  };
77
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2NyLm1pc3RyYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19vY3Ivb2NyLm1pc3RyYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBZ0dBLE1BQU0sc0JBQXNCLEdBQUcsb0JBQW9CLENBQUM7QUFDcEQsTUFBTSw0QkFBNEIsR0FBRywrQkFBK0IsQ0FBQztBQUVyRSxNQUFNLDZCQUE2QixHQUFHLENBQUMsT0FHdEMsRUFBK0IsRUFBRTtJQUNoQyxPQUFPO1FBQ0wsT0FBTyxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtZQUN6QixJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDO2dCQUNwQixNQUFNLElBQUksS0FBSyxDQUFDLHNFQUFzRSxDQUFDLENBQUM7WUFDMUYsQ0FBQztZQUVELE1BQU0sUUFBUSxHQUFHLE1BQU0sS0FBSyxDQUFDLE9BQU8sQ0FBQyxXQUFXLElBQUksNEJBQTRCLEVBQUU7Z0JBQ2hGLE1BQU0sRUFBRSxNQUFNO2dCQUNkLE9BQU8sRUFBRTtvQkFDUCxhQUFhLEVBQUUsVUFBVSxPQUFPLENBQUMsTUFBTSxFQUFFO29CQUN6QyxjQUFjLEVBQUUsa0JBQWtCO2lCQUNuQztnQkFDRCxJQUFJLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUM7YUFDOUIsQ0FBQyxDQUFDO1lBRUgsSUFBSSxDQUFDLFFBQVEsQ0FBQyxFQUFFLEVBQUUsQ0FBQztnQkFDakIsTUFBTSxTQUFTLEdBQUcsTUFBTSxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUM7Z0JBQ3hDLE1BQU0sSUFBSSxLQUFLLENBQUMsMENBQTBDLFFBQVEsQ0FBQyxNQUFNLEtBQUssU0FBUyxFQUFFLENBQUMsQ0FBQztZQUM3RixDQUFDO1lBRUQsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDLElBQUksRUFBRSxDQUF3QixDQUFDO1FBQ3hELENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQyxDQUFDO0FBRUYsTUFBTSxpQkFBaUIsR0FB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
100
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2NyLm1pc3RyYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19vY3Ivb2NyLm1pc3RyYWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBOEhBLE1BQU0sc0JBQXNCLEdBQUcsb0JBQW9CLENBQUM7QUFDcEQsTUFBTSw0QkFBNEIsR0FBRywrQkFBK0IsQ0FBQztBQUVyRSxNQUFNLDZCQUE2QixHQUFHLENBQUMsT0FHdEMsRUFBK0IsRUFBRTtJQUNoQyxNQUFNLElBQUksR0FBRyxLQUFLLEVBQUUsT0FBd0QsRUFBZ0MsRUFBRTtRQUM1RyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDO1lBQ3BCLE1BQU0sSUFBSSxLQUFLLENBQUMsc0VBQXNFLENBQUMsQ0FBQztRQUMxRixDQUFDO1FBRUQsTUFBTSxRQUFRLEdBQUcsTUFBTSxLQUFLLENBQUMsT0FBTyxDQUFDLFdBQVcsSUFBSSw0QkFBNEIsRUFBRTtZQUNoRixNQUFNLEVBQUUsTUFBTTtZQUNkLE9BQU8sRUFBRTtnQkFDUCxhQUFhLEVBQUUsVUFBVSxPQUFPLENBQUMsTUFBTSxFQUFFO2dCQUN6QyxjQUFjLEVBQUUsa0JBQWtCO2FBQ25DO1lBQ0QsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDO1NBQzlCLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxFQUFFLENBQUM7WUFDakIsTUFBTSxTQUFTLEdBQUcsTUFBTSxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDeEMsTUFBTSxJQUFJLEtBQUssQ0FBQywwQ0FBMEMsUUFBUSxDQUFDLE1BQU0sS0FBSyxTQUFTLEVBQUUsQ0FBQyxDQUFDO1FBQzdGLENBQUM7UUFFRCxPQUFPLENBQUMsTUFBTSxRQUFRLENBQUMsSUFBSSxFQUFFLENBQXdCLENBQUM7SUFDeEQsQ0FBQyxDQUFDO0lBQ0YsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsZUFBZSxFQUFFLElBQUks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
@@ -23,6 +23,8 @@ export declare class FlexTypedRequestError extends Error {
23
23
  * The tool catalog of a typedrequest surface: every included method becomes a catalog method of
24
24
  * its path, declared as its contract writes it and validated against its schema, and runs through
25
25
  * `router.routeAndAddResponse()` in process, its request the model's parameters with the omitted
26
- * properties `request` fills. A refusal rejects with `FlexTypedRequestError`.
26
+ * properties `request` fills and its result what `response` makes of the answer. A refusal rejects
27
+ * with `FlexTypedRequestError`. A method `readOnly` marks is declared read-only and not gated by
28
+ * `onCall`.
27
29
  */
28
30
  export declare function catalogFromTypedRequests<TScope = unknown>(surface: IFlexTypedRequestSurface, options: IFlexTypedRequestCatalogOptions<TScope>): IFlexToolCatalog<TScope>;
@@ -25,7 +25,9 @@ export class FlexTypedRequestError extends Error {
25
25
  * The tool catalog of a typedrequest surface: every included method becomes a catalog method of
26
26
  * its path, declared as its contract writes it and validated against its schema, and runs through
27
27
  * `router.routeAndAddResponse()` in process, its request the model's parameters with the omitted
28
- * properties `request` fills. A refusal rejects with `FlexTypedRequestError`.
28
+ * properties `request` fills and its result what `response` makes of the answer. A refusal rejects
29
+ * with `FlexTypedRequestError`. A method `readOnly` marks is declared read-only and not gated by
30
+ * `onCall`.
29
31
  */
30
32
  export function catalogFromTypedRequests(surface, options) {
31
33
  if (typeof options?.router?.routeAndAddResponse !== 'function') {
@@ -41,6 +43,7 @@ export function catalogFromTypedRequests(surface, options) {
41
43
  throw new plugins.FlexHarnessValidationError(`The surface leaves ${surface.omit.join(', ')} out of every request; the options' request() must fill them.`);
42
44
  }
43
45
  const include = options.include ?? ((method) => !method.deprecated);
46
+ const readOnly = options.readOnly ?? (() => false);
44
47
  const { router } = options;
45
48
  const call = async (method, params, context) => {
46
49
  const filled = options.request ? await options.request(method, context) : {};
@@ -55,7 +58,9 @@ export function catalogFromTypedRequests(surface, options) {
55
58
  throw new FlexTypedRequestError(method.method, 'The router answered nothing.');
56
59
  if (answer.error)
57
60
  throw new FlexTypedRequestError(method.method, answer.error.text, answer.error.data);
58
- return answer.response;
61
+ return options.response
62
+ ? await options.response(method, answer.response, context)
63
+ : answer.response;
59
64
  };
60
65
  return {
61
66
  methods: surface.methods.filter((method) => include(method)).map((method) => ({
@@ -67,13 +72,15 @@ export function catalogFromTypedRequests(surface, options) {
67
72
  outputSchema: method.outputSchema,
68
73
  declaration: method.declaration,
69
74
  run: (params, context) => call(method, params, context),
70
- ...(options.onCall
71
- ? { onCall: (params, context) => options.onCall(method, params, context) }
72
- : {}),
75
+ ...(readOnly(method)
76
+ ? { readOnly: true }
77
+ : options.onCall
78
+ ? { onCall: (params, context) => options.onCall(method, params, context) }
79
+ : {}),
73
80
  })),
74
81
  types: surface.types,
75
82
  ...(options.vocabulary ? { vocabulary: options.vocabulary } : {}),
76
83
  ...(options.onProposals ? { onProposals: options.onProposals } : {}),
77
84
  };
78
85
  }
79
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2F0YWxvZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3R5cGVkcmVxdWVzdC9jYXRhbG9nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBUXhDOzs7R0FHRztBQUNILE1BQU0sT0FBTyxxQkFBc0IsU0FBUSxLQUFLO0lBRzVCO0lBRUE7SUFFQTtJQU5sQjtJQUNFLHNDQUFzQztJQUN0QixNQUFjO0lBQzlCLGtFQUFrRTtJQUNsRCxJQUFZO0lBQzVCLHdCQUF3QjtJQUNSLElBQWM7UUFFOUIsS0FBSyxDQUFDLEdBQUcsTUFBTSxpQkFBaUIsSUFBSSxFQUFFLENBQUMsQ0FBQztRQU54QixXQUFNLEdBQU4sTUFBTSxDQUFRO1FBRWQsU0FBSSxHQUFKLElBQUksQ0FBUTtRQUVaLFNBQUksR0FBSixJQUFJLENBQVU7UUFHOUIsSUFBSSxDQUFDLElBQUksR0FBRyx1QkFBdUIsQ0FBQztJQUN0QyxDQUFDO0NBQ0Y7QUFFRDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSx3QkFBd0IsQ0FDdEMsT0FBaUMsRUFDakMsT0FBZ0Q7SUFFaEQsSUFBSSxPQUFPLE9BQU8sRUFBRSxNQUFNLEVBQUUsbUJBQW1CLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDL0QsTUFBTSxJQUFJLE9BQU8sQ0FBQywwQkFBMEIsQ0FBQyxxRUFBcUUsQ0FBQyxDQUFDO0lBQ3RILENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsT0FBTyxJQUFJLEVBQUUsQ0FBQztJQUN0QyxNQUFNLEtBQUssR0FBRyxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7SUFDdEUsTUFBTSxPQUFPLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FB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
86
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2F0YWxvZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3R5cGVkcmVxdWVzdC9jYXRhbG9nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBUXhDOzs7R0FHRztBQUNILE1BQU0sT0FBTyxxQkFBc0IsU0FBUSxLQUFLO0lBRzVCO0lBRUE7SUFFQTtJQU5sQjtJQUNFLHNDQUFzQztJQUN0QixNQUFjO0lBQzlCLGtFQUFrRTtJQUNsRCxJQUFZO0lBQzVCLHdCQUF3QjtJQUNSLElBQWM7UUFFOUIsS0FBSyxDQUFDLEdBQUcsTUFBTSxpQkFBaUIsSUFBSSxFQUFFLENBQUMsQ0FBQztRQU54QixXQUFNLEdBQU4sTUFBTSxDQUFRO1FBRWQsU0FBSSxHQUFKLElBQUksQ0FBUTtRQUVaLFNBQUksR0FBSixJQUFJLENBQVU7UUFHOUIsSUFBSSxDQUFDLElBQUksR0FBRyx1QkFBdUIsQ0FBQztJQUN0QyxDQUFDO0NBQ0Y7QUFFRDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLHdCQUF3QixDQUN0QyxPQUFpQyxFQUNqQyxPQUFnRDtJQUVoRCxJQUFJLE9BQU8sT0FBTyxFQUFFLE1BQU0sRUFBRSxtQkFBbUIsS0FBSyxVQUFVLEVBQUUsQ0FBQztRQUMvRCxNQUFNLElBQUksT0FBTyxDQUFDLDBCQUEwQixDQUFDLHFFQUFxRSxDQUFDLENBQUM7SUFDdEgsQ0FBQztJQUNELE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLElBQUksRUFBRSxDQUFDO0lBQ3RDLE1BQU0sS0FBSyxHQUFHLElBQUksR0FBRyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztJQUN0RSxNQUFNLE9BQU8sR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxD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QUFNLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBOEIsRUFBRSxDQUFDLENBQUM7WUFDeEcsTUFBTSxFQUFFLE1BQU0sQ0FBQyxJQUFJO1lBQ25CLE9BQU8sRUFBRSxNQUFNLENBQUMsT0FBTztZQUN2QixHQUFHLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUMsRUFBRSxXQUFXLEVBQUUsTUFBTSxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDbEUsR0FBRyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1lBQ3RFLFdBQVcsRUFBRSxNQUFNLENBQUMsV0FBVztZQUMvQixZQUFZLEVBQUUsTUFBTSxDQUFDLFlBQVk7WUFDakMsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXO1lBQy9CLEdBQUcsRUFBRSxDQUFDLE1BQStCLEVBQUUsT0FBTyxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUM7WUFDaEYsR0FBRyxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUM7Z0JBQ2xCLENBQUMsQ0FBQyxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUU7Z0JBQ3BCLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTTtvQkFDZCxDQUFDLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQyxNQUErQixFQUFFLE9BQXVDLEVBQUUsRUFBRSxDQUFDLE9BQU8sQ0FBQyxNQUFPLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxPQUFPLENBQUMsRUFBRTtvQkFDcEksQ0FBQyxDQUFDLEVBQUUsQ0FBQztTQUNWLENBQUMsQ0FBQztRQUNILEtBQUssRUFBRSxPQUFPLENBQUMsS0FBSztRQUNwQixHQUFHLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQUFDLENBQUMsRUFBRSxVQUFVLEVBQUUsT0FBTyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDakUsR0FBRyxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLEVBQUUsV0FBVyxFQUFFLE9BQU8sQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0tBQ3JFLENBQUM7QUFDSixDQUFDIn0=
@@ -96,11 +96,27 @@ export interface IFlexTypedRequestCatalogOptions<TScope = unknown> {
96
96
  localData?(method: IFlexTypedRequestMethod, context: IFlexCatalogRunContext<TScope>): Record<string, unknown> | PromiseLike<Record<string, unknown>>;
97
97
  /** The methods the model may use. Default: every method that is not deprecated. */
98
98
  include?(method: IFlexTypedRequestMethod): boolean;
99
+ /**
100
+ * The methods that only read: a call has no effect but its result. Such a method is declared
101
+ * `readOnly` and `onCall` does not decide its calls, so they run concurrently with other reads, may
102
+ * start while the model still streams its response, and a turn answers identical ones once; see
103
+ * `IFlexCatalogMethod.readOnly`. Default: none.
104
+ */
105
+ readOnly?(method: IFlexTypedRequestMethod): boolean;
106
+ /**
107
+ * What the model sees of a method's answered response, such as the response without a
108
+ * credential's secret. Default: the response. A refusal does not reach it, and the method's
109
+ * declaration still shows the contract's response.
110
+ */
111
+ response?(method: IFlexTypedRequestMethod, response: Record<string, unknown>, context: IFlexCatalogRunContext<TScope>): unknown;
99
112
  /** Other names of methods, by typed method: `{ getVoucher: ['Beleg abrufen'] }`. */
100
113
  aliases?: Readonly<Record<string, readonly string[]>>;
101
114
  /** The words the API's users say for its own words; see `IFlexToolCatalog.vocabulary`. */
102
115
  vocabulary?: IFlexToolCatalog['vocabulary'];
103
- /** Decides each validated call before it runs; see `IFlexCatalogMethod.onCall`. */
116
+ /**
117
+ * Decides each validated call of a method `readOnly` does not mark before it runs; see
118
+ * `IFlexCatalogMethod.onCall`.
119
+ */
104
120
  onCall?(method: IFlexTypedRequestMethod, params: Record<string, unknown>, context: IFlexCatalogRunContext<TScope>): TFlexCatalogCallDecision | PromiseLike<TFlexCatalogCallDecision>;
105
121
  /** Receives the calls `onCall` proposed; see `IFlexToolCatalog.onProposals`. */
106
122
  onProposals?: IFlexToolCatalog<TScope>['onProposals'];
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": "10.18.0",
5
+ "version": "10.20.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",
@@ -87,8 +87,8 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@cfworker/json-schema": "^4.1.1",
90
- "@modelprofile.com/flexharness-agent": "10.18.0",
91
- "@modelprofile.com/flexharness-models": "10.18.0",
90
+ "@modelprofile.com/flexharness-agent": "10.20.0",
91
+ "@modelprofile.com/flexharness-models": "10.20.0",
92
92
  "@push.rocks/smartrequest": "^5.5.1",
93
93
  "ai": "^7.0.111",
94
94
  "zod": "^4.6.5"
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '10.18.0',
6
+ version: '10.20.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
@@ -14,6 +14,18 @@ export interface ISmartAiOcrImageInput {
14
14
  mimeType: TSmartAiOcrImageMimeType;
15
15
  }
16
16
 
17
+ /** Mistral OCR reads PDF, PPTX, DOCX and other document formats. */
18
+ export type TSmartAiOcrDocumentMimeType =
19
+ | 'application/pdf'
20
+ | 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
21
+ | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
22
+ | string;
23
+
24
+ export interface ISmartAiOcrDocumentInput {
25
+ dataBase64: string;
26
+ mimeType: TSmartAiOcrDocumentMimeType;
27
+ }
28
+
17
29
  export interface ISmartAiOcrPageResult {
18
30
  index: number;
19
31
  text: string;
@@ -34,6 +46,14 @@ export interface ISmartAiOcrEngine {
34
46
  ) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
35
47
  }
36
48
 
49
+ /** The Mistral OCR engine also reads whole documents, such as a PDF with all its pages. */
50
+ export interface ISmartAiMistralOcrEngine extends ISmartAiOcrEngine {
51
+ recognizeDocument: (
52
+ input: ISmartAiOcrDocumentInput,
53
+ options?: ISmartAiMistralOcrRecognizeOptions
54
+ ) => Promise<ISmartAiOcrResult<IMistralOcrResponse>>;
55
+ }
56
+
37
57
  export interface IMistralOcrPageConfidenceScores {
38
58
  average_page_confidence_score?: number;
39
59
  averagePageConfidenceScore?: number;
@@ -70,8 +90,18 @@ export interface IMistralOcrRequest {
70
90
  confidence_scores_granularity?: TSmartAiMistralOcrConfidenceScoresGranularity;
71
91
  }
72
92
 
93
+ /** A document (PDF, PPTX, DOCX and other formats) as a URL or a base64 data URL; the other fields are those of an image request. */
94
+ export interface IMistralOcrDocumentRequest extends Omit<IMistralOcrRequest, 'document'> {
95
+ document: {
96
+ type: 'document_url';
97
+ document_url: string;
98
+ };
99
+ }
100
+
73
101
  export interface ISmartAiMistralOcrTransport {
74
102
  process: (request: IMistralOcrRequest) => Promise<IMistralOcrResponse>;
103
+ /** Reads documents. A transport without it reads images only, and `recognizeDocument` refuses documents. */
104
+ processDocument?: (request: IMistralOcrDocumentRequest) => Promise<IMistralOcrResponse>;
75
105
  }
76
106
 
77
107
  export interface ISmartAiMistralOcrOptions {
@@ -101,29 +131,28 @@ const createMistralOcrHttpTransport = (options: {
101
131
  apiKey?: string;
102
132
  endpointUrl?: string;
103
133
  }): ISmartAiMistralOcrTransport => {
104
- return {
105
- process: async (request) => {
106
- if (!options.apiKey) {
107
- throw new Error('Mistral OCR requires an apiKey when no custom transport is provided.');
108
- }
109
-
110
- const response = await fetch(options.endpointUrl ?? defaultMistralOcrEndpointUrl, {
111
- method: 'POST',
112
- headers: {
113
- Authorization: `Bearer ${options.apiKey}`,
114
- 'Content-Type': 'application/json',
115
- },
116
- body: JSON.stringify(request),
117
- });
134
+ const post = async (request: IMistralOcrRequest | IMistralOcrDocumentRequest): Promise<IMistralOcrResponse> => {
135
+ if (!options.apiKey) {
136
+ throw new Error('Mistral OCR requires an apiKey when no custom transport is provided.');
137
+ }
138
+
139
+ const response = await fetch(options.endpointUrl ?? defaultMistralOcrEndpointUrl, {
140
+ method: 'POST',
141
+ headers: {
142
+ Authorization: `Bearer ${options.apiKey}`,
143
+ 'Content-Type': 'application/json',
144
+ },
145
+ body: JSON.stringify(request),
146
+ });
118
147
 
119
- if (!response.ok) {
120
- const errorBody = await response.text();
121
- throw new Error(`Mistral OCR request failed with status ${response.status}: ${errorBody}`);
122
- }
148
+ if (!response.ok) {
149
+ const errorBody = await response.text();
150
+ throw new Error(`Mistral OCR request failed with status ${response.status}: ${errorBody}`);
151
+ }
123
152
 
124
- return (await response.json()) as IMistralOcrResponse;
125
- },
153
+ return (await response.json()) as IMistralOcrResponse;
126
154
  };
155
+ return { process: post, processDocument: post };
127
156
  };
128
157
 
129
158
  const getPageConfidence = (page: IMistralOcrPageResponse): number | undefined => {
@@ -134,9 +163,30 @@ const getPageConfidence = (page: IMistralOcrPageResponse): number | undefined =>
134
163
  );
135
164
  };
136
165
 
166
+ const toOcrResult = (response: IMistralOcrResponse): ISmartAiOcrResult<IMistralOcrResponse> => {
167
+ const pages = response.pages.map((page) => ({
168
+ index: page.index,
169
+ text: page.markdown,
170
+ confidence: getPageConfidence(page),
171
+ }));
172
+ const pageConfidences = pages
173
+ .map((page) => page.confidence)
174
+ .filter((confidence): confidence is number => typeof confidence === 'number');
175
+ const confidence = pageConfidences.length
176
+ ? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
177
+ : undefined;
178
+
179
+ return {
180
+ text: pages.map((page) => page.text).join('\n\n').trim(),
181
+ confidence,
182
+ pages,
183
+ raw: response,
184
+ };
185
+ };
186
+
137
187
  export const createMistralOcrEngine = (
138
188
  options: ISmartAiMistralOcrOptions = {}
139
- ): ISmartAiOcrEngine => {
189
+ ): ISmartAiMistralOcrEngine => {
140
190
  const transport =
141
191
  options.transport ??
142
192
  createMistralOcrHttpTransport({
@@ -145,6 +195,16 @@ export const createMistralOcrEngine = (
145
195
  });
146
196
  const model = options.model ?? defaultMistralOcrModel;
147
197
 
198
+ const settingsOf = (recognizeOptions: ISmartAiMistralOcrRecognizeOptions) => ({
199
+ include_image_base64:
200
+ recognizeOptions.includeImageBase64 ?? options.includeImageBase64 ?? false,
201
+ table_format: recognizeOptions.tableFormat ?? options.tableFormat,
202
+ extract_header: recognizeOptions.extractHeader ?? options.extractHeader,
203
+ extract_footer: recognizeOptions.extractFooter ?? options.extractFooter,
204
+ confidence_scores_granularity:
205
+ recognizeOptions.confidenceScoresGranularity ?? options.confidenceScoresGranularity,
206
+ });
207
+
148
208
  return {
149
209
  recognizeImage: async (input, recognizeOptions = {}) => {
150
210
  if (!input.dataBase64) {
@@ -153,40 +213,33 @@ export const createMistralOcrEngine = (
153
213
  if (!input.mimeType) {
154
214
  throw new Error('Mistral OCR image input requires mimeType.');
155
215
  }
156
-
157
- const response = await transport.process({
216
+ return toOcrResult(await transport.process({
158
217
  model,
159
218
  document: {
160
219
  type: 'image_url',
161
220
  image_url: `data:${input.mimeType};base64,${input.dataBase64}`,
162
221
  },
163
- include_image_base64:
164
- recognizeOptions.includeImageBase64 ?? options.includeImageBase64 ?? false,
165
- table_format: recognizeOptions.tableFormat ?? options.tableFormat,
166
- extract_header: recognizeOptions.extractHeader ?? options.extractHeader,
167
- extract_footer: recognizeOptions.extractFooter ?? options.extractFooter,
168
- confidence_scores_granularity:
169
- recognizeOptions.confidenceScoresGranularity ?? options.confidenceScoresGranularity,
170
- });
171
-
172
- const pages = response.pages.map((page) => ({
173
- index: page.index,
174
- text: page.markdown,
175
- confidence: getPageConfidence(page),
222
+ ...settingsOf(recognizeOptions),
223
+ }));
224
+ },
225
+ recognizeDocument: async (input, recognizeOptions = {}) => {
226
+ if (!transport.processDocument) {
227
+ throw new Error('This Mistral OCR transport reads images only: it has no processDocument, so it cannot read documents.');
228
+ }
229
+ if (!input.dataBase64) {
230
+ throw new Error('Mistral OCR document input requires dataBase64.');
231
+ }
232
+ if (!input.mimeType) {
233
+ throw new Error('Mistral OCR document input requires mimeType.');
234
+ }
235
+ return toOcrResult(await transport.processDocument({
236
+ model,
237
+ document: {
238
+ type: 'document_url',
239
+ document_url: `data:${input.mimeType};base64,${input.dataBase64}`,
240
+ },
241
+ ...settingsOf(recognizeOptions),
176
242
  }));
177
- const pageConfidences = pages
178
- .map((page) => page.confidence)
179
- .filter((confidence): confidence is number => typeof confidence === 'number');
180
- const confidence = pageConfidences.length
181
- ? pageConfidences.reduce((sum, value) => sum + value, 0) / pageConfidences.length
182
- : undefined;
183
-
184
- return {
185
- text: pages.map((page) => page.text).join('\n\n').trim(),
186
- confidence,
187
- pages,
188
- raw: response,
189
- };
190
243
  },
191
244
  };
192
245
  };
package/ts_ocr/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- Image OCR with Mistral or an injected recognition engine. This subpath imports no runtime dependencies and works with Node buffers and browser byte arrays.
1
+ Image OCR with Mistral or an injected recognition engine, and document OCR with Mistral. This subpath imports no runtime dependencies and works with Node buffers and browser byte arrays.
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;
@@ -20,6 +20,17 @@ console.log(result.text);
20
20
 
21
21
  Constructor and call options accept `engine` or `mistralOptions`. A per-call engine takes precedence over the constructor engine. Otherwise Mistral settings merge, with per-call settings taking precedence. `recognizeOptions` controls each request. `recognizeImage` accepts an existing `{ dataBase64, mimeType }` image; `createMistralOcrEngine` remains available for lower-level integrations.
22
22
 
23
+ `createMistralOcrEngine()` also reads whole documents: `recognizeDocument({ dataBase64, mimeType: 'application/pdf' })` sends the document as a `document_url` data URL, as Mistral's OCR API takes PDF, PPTX, DOCX and other document formats, and returns every page. Mistral accepts documents of up to 50 MB and 1,000 pages.
24
+
25
+ A custom `transport` receives image requests (`IMistralOcrRequest`) through `process`, as before. It reads documents only when it also implements the optional `processDocument`, which receives an `IMistralOcrDocumentRequest`, whose `document` is a `document_url`; for a transport without it, `recognizeDocument` fails with "This Mistral OCR transport reads images only". The built-in transport implements both.
26
+
27
+ ```typescript
28
+ import { createMistralOcrEngine } from '@modelprofile.com/flexharness/ocr';
29
+
30
+ const engine = createMistralOcrEngine({ apiKey: process.env.MISTRAL_API_KEY });
31
+ const { text, pages } = await engine.recognizeDocument({ dataBase64: pdfBase64, mimeType: 'application/pdf' });
32
+ ```
33
+
23
34
  Model-based PDF text extraction lives in `@modelprofile.com/flexharness/documents`. Native `ocrmypdf` processing remains in `@push.rocks/smartocr`.
24
35
 
25
36
  ## Issue Reporting and Security
@@ -28,7 +28,9 @@ export class FlexTypedRequestError extends Error {
28
28
  * The tool catalog of a typedrequest surface: every included method becomes a catalog method of
29
29
  * its path, declared as its contract writes it and validated against its schema, and runs through
30
30
  * `router.routeAndAddResponse()` in process, its request the model's parameters with the omitted
31
- * properties `request` fills. A refusal rejects with `FlexTypedRequestError`.
31
+ * properties `request` fills and its result what `response` makes of the answer. A refusal rejects
32
+ * with `FlexTypedRequestError`. A method `readOnly` marks is declared read-only and not gated by
33
+ * `onCall`.
32
34
  */
33
35
  export function catalogFromTypedRequests<TScope = unknown>(
34
36
  surface: IFlexTypedRequestSurface,
@@ -49,6 +51,7 @@ export function catalogFromTypedRequests<TScope = unknown>(
49
51
  );
50
52
  }
51
53
  const include = options.include ?? ((method: IFlexTypedRequestMethod) => !method.deprecated);
54
+ const readOnly = options.readOnly ?? (() => false);
52
55
  const { router } = options;
53
56
  const call = async (
54
57
  method: IFlexTypedRequestMethod,
@@ -68,7 +71,9 @@ export function catalogFromTypedRequests<TScope = unknown>(
68
71
  );
69
72
  if (!answer) throw new FlexTypedRequestError(method.method, 'The router answered nothing.');
70
73
  if (answer.error) throw new FlexTypedRequestError(method.method, answer.error.text, answer.error.data);
71
- return answer.response;
74
+ return options.response
75
+ ? await options.response(method, answer.response as Record<string, unknown>, context)
76
+ : answer.response;
72
77
  };
73
78
  return {
74
79
  methods: surface.methods.filter((method) => include(method)).map((method): IFlexCatalogMethod<TScope> => ({
@@ -80,9 +85,11 @@ export function catalogFromTypedRequests<TScope = unknown>(
80
85
  outputSchema: method.outputSchema,
81
86
  declaration: method.declaration,
82
87
  run: (params: Record<string, unknown>, context) => call(method, params, context),
83
- ...(options.onCall
84
- ? { onCall: (params: Record<string, unknown>, context: IFlexCatalogRunContext<TScope>) => options.onCall!(method, params, context) }
85
- : {}),
88
+ ...(readOnly(method)
89
+ ? { readOnly: true }
90
+ : options.onCall
91
+ ? { onCall: (params: Record<string, unknown>, context: IFlexCatalogRunContext<TScope>) => options.onCall!(method, params, context) }
92
+ : {}),
86
93
  })),
87
94
  types: surface.types,
88
95
  ...(options.vocabulary ? { vocabulary: options.vocabulary } : {}),
@@ -109,11 +109,31 @@ export interface IFlexTypedRequestCatalogOptions<TScope = unknown> {
109
109
  ): Record<string, unknown> | PromiseLike<Record<string, unknown>>;
110
110
  /** The methods the model may use. Default: every method that is not deprecated. */
111
111
  include?(method: IFlexTypedRequestMethod): boolean;
112
+ /**
113
+ * The methods that only read: a call has no effect but its result. Such a method is declared
114
+ * `readOnly` and `onCall` does not decide its calls, so they run concurrently with other reads, may
115
+ * start while the model still streams its response, and a turn answers identical ones once; see
116
+ * `IFlexCatalogMethod.readOnly`. Default: none.
117
+ */
118
+ readOnly?(method: IFlexTypedRequestMethod): boolean;
119
+ /**
120
+ * What the model sees of a method's answered response, such as the response without a
121
+ * credential's secret. Default: the response. A refusal does not reach it, and the method's
122
+ * declaration still shows the contract's response.
123
+ */
124
+ response?(
125
+ method: IFlexTypedRequestMethod,
126
+ response: Record<string, unknown>,
127
+ context: IFlexCatalogRunContext<TScope>,
128
+ ): unknown;
112
129
  /** Other names of methods, by typed method: `{ getVoucher: ['Beleg abrufen'] }`. */
113
130
  aliases?: Readonly<Record<string, readonly string[]>>;
114
131
  /** The words the API's users say for its own words; see `IFlexToolCatalog.vocabulary`. */
115
132
  vocabulary?: IFlexToolCatalog['vocabulary'];
116
- /** Decides each validated call before it runs; see `IFlexCatalogMethod.onCall`. */
133
+ /**
134
+ * Decides each validated call of a method `readOnly` does not mark before it runs; see
135
+ * `IFlexCatalogMethod.onCall`.
136
+ */
117
137
  onCall?(
118
138
  method: IFlexTypedRequestMethod,
119
139
  params: Record<string, unknown>,
@@ -19,7 +19,8 @@ const surface = await describeTypedRequests({
19
19
  const catalog = catalogFromTypedRequests<IAppScope>(surface, {
20
20
  router: server.typedrouter,
21
21
  request: (method, context) => ({ identity: identityOf(context.provider.scope) }),
22
- onCall: (method) => (method.method.startsWith('get') || method.method.startsWith('list') ? 'run' : 'propose'),
22
+ readOnly: (method) => method.method.startsWith('get') || method.method.startsWith('list'),
23
+ onCall: () => 'propose',
23
24
  });
24
25
  ```
25
26
 
@@ -38,6 +39,8 @@ The surface is plain JSON, so a build step can write it next to the server and t
38
39
  - `router` answers the calls: anything with `routeAndAddResponse()`, such as a `TypedRouter` of `@api.global/typedrequest`. A call is routed as a typed request with the model's parameters and what `request` returns for the omitted properties, `localData` as the trusted route data a transport would hand the handlers, and the run's abort signal.
39
40
  - A refusal rejects with `FlexTypedRequestError`: the typed method, the error's `text` and `data`, as a client of the API would receive them. A handler's own failure arrives as the router's internal error. A host's `externalErrorProjector` decides what of it the model sees.
40
41
  - `include` picks the methods the model may use, by default every method that is not deprecated; `aliases` gives methods other names by typed method; `vocabulary`, `onCall` and `onProposals` are the catalog's own (`onCall` receives the typed method first).
42
+ - `readOnly` marks the methods that only read. A marked method is declared `readOnly` and `onCall` does not decide its calls, so they run concurrently with other reads, may start while the model still streams, and a turn answers identical ones once; every other method's calls, gated by `onCall` or not, run in the order the model made them. Mark nothing that asks for permission.
43
+ - `response` decides what the model sees of an answered response, such as the response without a credential's secret; by default the response itself. A refusal does not reach it, and the method's declaration still shows the contract's response.
41
44
  - A surface that omits properties needs `request`; an alias for a method the surface lacks is refused.
42
45
 
43
46
  ## Issue Reporting and Security