@modelprofile.com/flexharness-models 10.6.0 → 10.7.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,5 +3,6 @@ export * from './classes.modelregistry.js';
3
3
  export * from './smartai.cache.js';
4
4
  export * from './smartai.limits.js';
5
5
  export * from './smartai.failures.js';
6
+ export * from './smartai.textfiles.js';
6
7
  export { generateText, streamText, tool, jsonSchema } from './plugins.js';
7
8
  export type { ModelMessage, ToolSet, StreamTextResult } from './plugins.js';
@@ -3,5 +3,6 @@ export * from './classes.modelregistry.js';
3
3
  export * from './smartai.cache.js';
4
4
  export * from './smartai.limits.js';
5
5
  export * from './smartai.failures.js';
6
+ export * from './smartai.textfiles.js';
6
7
  export { generateText, streamText, tool, jsonSchema } from './plugins.js';
7
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19tb2RlbHMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLE9BQU8sRUFBRSxZQUFZLEVBQUUsVUFBVSxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsTUFBTSxjQUFjLENBQUMifQ==
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19tb2RlbHMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxpQkFBaUIsQ0FBQztBQUNoQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsT0FBTyxFQUFFLFlBQVksRUFBRSxVQUFVLEVBQUUsSUFBSSxFQUFFLFVBQVUsRUFBRSxNQUFNLGNBQWMsQ0FBQyJ9
@@ -0,0 +1,22 @@
1
+ import type * as plugins from './plugins.js';
2
+ /**
3
+ * Whether a media type names text: any `text/*` type, a JSON, XML, YAML, CSV, script or similar
4
+ * `application/*` type, a structured-syntax `+json`, `+xml` or `+yaml` type, or any type that
5
+ * states its `charset`. `text` alone, the top-level form a prompt may use, counts as text.
6
+ */
7
+ export declare function isTextMediaType(mediaType: string): boolean;
8
+ /** The text a model reads for one text file: its name and media type, then its content, fenced. */
9
+ export declare function formatTextFileForModel(file: {
10
+ filename?: string;
11
+ mediaType: string;
12
+ text: string;
13
+ }): string;
14
+ /**
15
+ * Sends the text files of user messages as text parts, for providers that read only images and
16
+ * PDFs as files. A file part whose data is inline text, or whose media type names text (see
17
+ * `isTextMediaType`), becomes one text part that carries the file name, the media type and the
18
+ * content decoded in the stated charset, UTF-8 by default. Images, PDFs, other binary files, URLs
19
+ * and provider file references are passed on unchanged. A file that names text but does not decode
20
+ * fails the call before the request is sent.
21
+ */
22
+ export declare function createTextFilePartMiddleware(): plugins.LanguageModelV4Middleware;
@@ -0,0 +1,133 @@
1
+ const textualApplicationTypes = new Set([
2
+ 'application/csv',
3
+ 'application/ecmascript',
4
+ 'application/graphql',
5
+ 'application/javascript',
6
+ 'application/json',
7
+ 'application/jsonl',
8
+ 'application/ndjson',
9
+ 'application/sql',
10
+ 'application/toml',
11
+ 'application/typescript',
12
+ 'application/x-javascript',
13
+ 'application/x-ndjson',
14
+ 'application/x-sh',
15
+ 'application/x-toml',
16
+ 'application/x-typescript',
17
+ 'application/x-yaml',
18
+ 'application/xml',
19
+ 'application/yaml',
20
+ ]);
21
+ const textualSuffixes = ['+json', '+xml', '+yaml'];
22
+ function parseMediaType(mediaType) {
23
+ const [essence = '', ...parameters] = mediaType.split(';');
24
+ let charset;
25
+ for (const parameter of parameters) {
26
+ const separator = parameter.indexOf('=');
27
+ if (separator === -1)
28
+ continue;
29
+ if (parameter.slice(0, separator).trim().toLowerCase() !== 'charset')
30
+ continue;
31
+ charset = parameter.slice(separator + 1).trim().replace(/^"(.*)"$/u, '$1');
32
+ }
33
+ return { essence: essence.trim().toLowerCase(), ...(charset ? { charset } : {}) };
34
+ }
35
+ /**
36
+ * Whether a media type names text: any `text/*` type, a JSON, XML, YAML, CSV, script or similar
37
+ * `application/*` type, a structured-syntax `+json`, `+xml` or `+yaml` type, or any type that
38
+ * states its `charset`. `text` alone, the top-level form a prompt may use, counts as text.
39
+ */
40
+ export function isTextMediaType(mediaType) {
41
+ const { essence, charset } = parseMediaType(mediaType);
42
+ if (charset !== undefined)
43
+ return true;
44
+ if (essence === 'text' || essence.startsWith('text/'))
45
+ return true;
46
+ if (textualApplicationTypes.has(essence))
47
+ return true;
48
+ return essence.startsWith('application/') && textualSuffixes.some((suffix) => essence.endsWith(suffix));
49
+ }
50
+ function decodeBase64(data) {
51
+ const binary = atob(data);
52
+ const bytes = new Uint8Array(binary.length);
53
+ for (let index = 0; index < binary.length; index++)
54
+ bytes[index] = binary.charCodeAt(index);
55
+ return bytes;
56
+ }
57
+ function describeFile(part) {
58
+ return part.filename === undefined ? `file part of media type ${part.mediaType}` : `file "${part.filename}"`;
59
+ }
60
+ function decodeText(part, data) {
61
+ const charset = parseMediaType(part.mediaType).charset ?? 'utf-8';
62
+ let decoder;
63
+ try {
64
+ decoder = new TextDecoder(charset, { fatal: true });
65
+ }
66
+ catch (error) {
67
+ throw new Error(`The ${describeFile(part)} names charset "${charset}", which cannot be decoded.`, { cause: error });
68
+ }
69
+ try {
70
+ return decoder.decode(typeof data === 'string' ? decodeBase64(data) : data);
71
+ }
72
+ catch (error) {
73
+ throw new Error(`The ${describeFile(part)} is not valid ${charset} text.`, { cause: error });
74
+ }
75
+ }
76
+ /** The text a model reads for one text file: its name and media type, then its content, fenced. */
77
+ export function formatTextFileForModel(file) {
78
+ const longestBacktickRun = Math.max(0, ...Array.from(file.text.matchAll(/`+/gu), (match) => match[0].length));
79
+ const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1));
80
+ const heading = file.filename === undefined
81
+ ? `Attached file (${file.mediaType}):`
82
+ : `Attached file ${file.filename} (${file.mediaType}):`;
83
+ const body = file.text.endsWith('\n') ? file.text : `${file.text}\n`;
84
+ return `${heading}\n${fence}\n${body}${fence}`;
85
+ }
86
+ function inlineTextFile(part) {
87
+ if (part.type !== 'file')
88
+ return part;
89
+ let text;
90
+ if (part.data.type === 'text') {
91
+ text = part.data.text;
92
+ }
93
+ else if (part.data.type === 'data' && isTextMediaType(part.mediaType)) {
94
+ text = decodeText(part, part.data.data);
95
+ }
96
+ else {
97
+ return part;
98
+ }
99
+ return {
100
+ type: 'text',
101
+ text: formatTextFileForModel({
102
+ ...(part.filename === undefined ? {} : { filename: part.filename }),
103
+ mediaType: part.mediaType,
104
+ text,
105
+ }),
106
+ ...(part.providerOptions === undefined ? {} : { providerOptions: part.providerOptions }),
107
+ };
108
+ }
109
+ /**
110
+ * Sends the text files of user messages as text parts, for providers that read only images and
111
+ * PDFs as files. A file part whose data is inline text, or whose media type names text (see
112
+ * `isTextMediaType`), becomes one text part that carries the file name, the media type and the
113
+ * content decoded in the stated charset, UTF-8 by default. Images, PDFs, other binary files, URLs
114
+ * and provider file references are passed on unchanged. A file that names text but does not decode
115
+ * fails the call before the request is sent.
116
+ */
117
+ export function createTextFilePartMiddleware() {
118
+ return {
119
+ specificationVersion: 'v4',
120
+ transformParams: async ({ params }) => {
121
+ const hasTextFile = params.prompt.some((message) => message.role === 'user' && message.content.some((part) => part.type === 'file' && (part.data.type === 'text' || (part.data.type === 'data' && isTextMediaType(part.mediaType)))));
122
+ if (!hasTextFile)
123
+ return params;
124
+ return {
125
+ ...params,
126
+ prompt: params.prompt.map((message) => message.role === 'user'
127
+ ? { ...message, content: message.content.map(inlineTextFile) }
128
+ : message),
129
+ };
130
+ },
131
+ };
132
+ }
133
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhaS50ZXh0ZmlsZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90c19tb2RlbHMvc21hcnRhaS50ZXh0ZmlsZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBV0EsTUFBTSx1QkFBdUIsR0FBRyxJQUFJLEdBQUcsQ0FBQztJQUN0QyxpQkFBaUI7SUFDakIsd0JBQXdCO0lBQ3hCLHFCQUFxQjtJQUNyQix3QkFBd0I7SUFDeEIsa0JBQWtCO0lBQ2xCLG1CQUFtQjtJQUNuQixvQkFBb0I7SUFDcEIsaUJBQWlCO0lBQ2pCLGtCQUFrQjtJQUNsQix3QkFBd0I7SUFDeEIsMEJBQTBCO0lBQzFCLHNCQUFzQjtJQUN0QixrQkFBa0I7SUFDbEIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQixvQkFBb0I7SUFDcEIsaUJBQWlCO0lBQ2pCLGtCQUFrQjtDQUNuQixDQUFDLENBQUM7QUFFSCxNQUFNLGVBQWUsR0FBRyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7QUFFbkQsU0FBUyxjQUFjLENBQUMsU0FBaUI7SUFDdkMsTUFBTSxDQUFDLE9BQU8sR0FBRyxFQUFFLEVBQUUsR0FBRyxVQUFVLENBQUMsR0FBRyxTQUFTLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQzNELElBQUksT0FBMkIsQ0FBQztJQUNoQyxLQUFLLE1BQU0sU0FBUyxJQUFJLFVBQVUsRUFBRSxDQUFDO1FBQ25DLE1BQU0sU0FBUyxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDekMsSUFBSSxTQUFTLEtBQUssQ0FBQyxDQUFDO1lBQUUsU0FBUztRQUMvQixJQUFJLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFNBQVMsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRSxLQUFLLFNBQVM7WUFBRSxTQUFTO1FBQy9FLE9BQU8sR0FBRyxTQUFTLENBQUMsS0FBSyxDQUFDLFNBQVMsR0FB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
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-models",
5
- "version": "10.6.0",
5
+ "version": "10.7.0",
6
6
  "type": "module",
7
7
  "description": "Provider-independent model contracts, prompt caching and explicit model registration.",
8
8
  "main": "./dist_ts_models/index.js",
package/readme.md CHANGED
@@ -40,6 +40,14 @@ most 64 characters, and never when it looks like a credential: a key or token pr
40
40
  `sk_`, `pk_`, `ghp_` or `eyJ`, or a long run of letters and digits. The description follows `cause`
41
41
  and the last attempt of a retried call.
42
42
 
43
+ `createTextFilePartMiddleware()` sends the text files of user messages as text parts, for
44
+ providers that read only images and PDFs as files. A file part with inline text, or whose media
45
+ type names text (`isTextMediaType`: `text/*`, JSON, XML, YAML, CSV, script types, `+json`, `+xml`
46
+ and `+yaml` types, and any type with a `charset`), becomes one text part with the file name, the
47
+ media type and the content decoded in the stated charset (UTF-8 by default), in a fence longer
48
+ than any backtick run in the content. Images, PDFs, other binary files, URLs and file references
49
+ pass unchanged; a file that names text but does not decode fails before the request is sent.
50
+
43
51
  ## License and Legal Information
44
52
 
45
53
  This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
@@ -3,5 +3,6 @@ export * from './classes.modelregistry.js';
3
3
  export * from './smartai.cache.js';
4
4
  export * from './smartai.limits.js';
5
5
  export * from './smartai.failures.js';
6
+ export * from './smartai.textfiles.js';
6
7
  export { generateText, streamText, tool, jsonSchema } from './plugins.js';
7
8
  export type { ModelMessage, ToolSet, StreamTextResult } from './plugins.js';
@@ -40,6 +40,14 @@ most 64 characters, and never when it looks like a credential: a key or token pr
40
40
  `sk_`, `pk_`, `ghp_` or `eyJ`, or a long run of letters and digits. The description follows `cause`
41
41
  and the last attempt of a retried call.
42
42
 
43
+ `createTextFilePartMiddleware()` sends the text files of user messages as text parts, for
44
+ providers that read only images and PDFs as files. A file part with inline text, or whose media
45
+ type names text (`isTextMediaType`: `text/*`, JSON, XML, YAML, CSV, script types, `+json`, `+xml`
46
+ and `+yaml` types, and any type with a `charset`), becomes one text part with the file name, the
47
+ media type and the content decoded in the stated charset (UTF-8 by default), in a fence longer
48
+ than any backtick run in the content. Images, PDFs, other binary files, URLs and file references
49
+ pass unchanged; a file that names text but does not decode fails before the request is sent.
50
+
43
51
  ## License and Legal Information
44
52
 
45
53
  This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
@@ -0,0 +1,141 @@
1
+ import type * as plugins from './plugins.js';
2
+
3
+ type TPromptMessage = plugins.LanguageModelV4Prompt[number];
4
+ type TUserContentPart = Extract<TPromptMessage, { role: 'user' }>['content'][number];
5
+ type TFilePart = Extract<TUserContentPart, { type: 'file' }>;
6
+
7
+ interface IParsedMediaType {
8
+ essence: string;
9
+ charset?: string;
10
+ }
11
+
12
+ const textualApplicationTypes = new Set([
13
+ 'application/csv',
14
+ 'application/ecmascript',
15
+ 'application/graphql',
16
+ 'application/javascript',
17
+ 'application/json',
18
+ 'application/jsonl',
19
+ 'application/ndjson',
20
+ 'application/sql',
21
+ 'application/toml',
22
+ 'application/typescript',
23
+ 'application/x-javascript',
24
+ 'application/x-ndjson',
25
+ 'application/x-sh',
26
+ 'application/x-toml',
27
+ 'application/x-typescript',
28
+ 'application/x-yaml',
29
+ 'application/xml',
30
+ 'application/yaml',
31
+ ]);
32
+
33
+ const textualSuffixes = ['+json', '+xml', '+yaml'];
34
+
35
+ function parseMediaType(mediaType: string): IParsedMediaType {
36
+ const [essence = '', ...parameters] = mediaType.split(';');
37
+ let charset: string | undefined;
38
+ for (const parameter of parameters) {
39
+ const separator = parameter.indexOf('=');
40
+ if (separator === -1) continue;
41
+ if (parameter.slice(0, separator).trim().toLowerCase() !== 'charset') continue;
42
+ charset = parameter.slice(separator + 1).trim().replace(/^"(.*)"$/u, '$1');
43
+ }
44
+ return { essence: essence.trim().toLowerCase(), ...(charset ? { charset } : {}) };
45
+ }
46
+
47
+ /**
48
+ * Whether a media type names text: any `text/*` type, a JSON, XML, YAML, CSV, script or similar
49
+ * `application/*` type, a structured-syntax `+json`, `+xml` or `+yaml` type, or any type that
50
+ * states its `charset`. `text` alone, the top-level form a prompt may use, counts as text.
51
+ */
52
+ export function isTextMediaType(mediaType: string): boolean {
53
+ const { essence, charset } = parseMediaType(mediaType);
54
+ if (charset !== undefined) return true;
55
+ if (essence === 'text' || essence.startsWith('text/')) return true;
56
+ if (textualApplicationTypes.has(essence)) return true;
57
+ return essence.startsWith('application/') && textualSuffixes.some((suffix) => essence.endsWith(suffix));
58
+ }
59
+
60
+ function decodeBase64(data: string): Uint8Array {
61
+ const binary = atob(data);
62
+ const bytes = new Uint8Array(binary.length);
63
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
64
+ return bytes;
65
+ }
66
+
67
+ function describeFile(part: TFilePart): string {
68
+ return part.filename === undefined ? `file part of media type ${part.mediaType}` : `file "${part.filename}"`;
69
+ }
70
+
71
+ function decodeText(part: TFilePart, data: Uint8Array | string): string {
72
+ const charset = parseMediaType(part.mediaType).charset ?? 'utf-8';
73
+ let decoder: TextDecoder;
74
+ try {
75
+ decoder = new TextDecoder(charset, { fatal: true });
76
+ } catch (error) {
77
+ throw new Error(`The ${describeFile(part)} names charset "${charset}", which cannot be decoded.`, { cause: error });
78
+ }
79
+ try {
80
+ return decoder.decode(typeof data === 'string' ? decodeBase64(data) : data);
81
+ } catch (error) {
82
+ throw new Error(`The ${describeFile(part)} is not valid ${charset} text.`, { cause: error });
83
+ }
84
+ }
85
+
86
+ /** The text a model reads for one text file: its name and media type, then its content, fenced. */
87
+ export function formatTextFileForModel(file: { filename?: string; mediaType: string; text: string }): string {
88
+ const longestBacktickRun = Math.max(0, ...Array.from(file.text.matchAll(/`+/gu), (match) => match[0].length));
89
+ const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1));
90
+ const heading = file.filename === undefined
91
+ ? `Attached file (${file.mediaType}):`
92
+ : `Attached file ${file.filename} (${file.mediaType}):`;
93
+ const body = file.text.endsWith('\n') ? file.text : `${file.text}\n`;
94
+ return `${heading}\n${fence}\n${body}${fence}`;
95
+ }
96
+
97
+ function inlineTextFile(part: TUserContentPart): TUserContentPart {
98
+ if (part.type !== 'file') return part;
99
+ let text: string;
100
+ if (part.data.type === 'text') {
101
+ text = part.data.text;
102
+ } else if (part.data.type === 'data' && isTextMediaType(part.mediaType)) {
103
+ text = decodeText(part, part.data.data);
104
+ } else {
105
+ return part;
106
+ }
107
+ return {
108
+ type: 'text',
109
+ text: formatTextFileForModel({
110
+ ...(part.filename === undefined ? {} : { filename: part.filename }),
111
+ mediaType: part.mediaType,
112
+ text,
113
+ }),
114
+ ...(part.providerOptions === undefined ? {} : { providerOptions: part.providerOptions }),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Sends the text files of user messages as text parts, for providers that read only images and
120
+ * PDFs as files. A file part whose data is inline text, or whose media type names text (see
121
+ * `isTextMediaType`), becomes one text part that carries the file name, the media type and the
122
+ * content decoded in the stated charset, UTF-8 by default. Images, PDFs, other binary files, URLs
123
+ * and provider file references are passed on unchanged. A file that names text but does not decode
124
+ * fails the call before the request is sent.
125
+ */
126
+ export function createTextFilePartMiddleware(): plugins.LanguageModelV4Middleware {
127
+ return {
128
+ specificationVersion: 'v4',
129
+ transformParams: async ({ params }) => {
130
+ const hasTextFile = params.prompt.some((message) => message.role === 'user' && message.content.some((part) =>
131
+ part.type === 'file' && (part.data.type === 'text' || (part.data.type === 'data' && isTextMediaType(part.mediaType)))));
132
+ if (!hasTextFile) return params;
133
+ return {
134
+ ...params,
135
+ prompt: params.prompt.map((message) => message.role === 'user'
136
+ ? { ...message, content: message.content.map(inlineTextFile) }
137
+ : message),
138
+ };
139
+ },
140
+ };
141
+ }