utilitas 2001.1.136 → 2001.1.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/rag.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { BASE64, convert } from './storage.mjs';
1
+ import { BASE64, convert, formatDataURL } from './storage.mjs';
2
2
  import { countTokens, trimText } from './alan.mjs';
3
3
  import { ensureArray, ensureString, need } from './utilitas.mjs';
4
4
 
@@ -9,20 +9,16 @@ const rerankerClients = {};
9
9
  const [
10
10
  OPENAI, GOOGLE, OPENROUTER, JINA,
11
11
  GOOGLE_DEFAULT_LOCATION, GOOGLE_RERANK_CONFIG_ID,
12
- OPENAI_MODEL_EMBED_SMALL,
13
- OPENAI_MODEL_EMBED_LARGE,
14
- GOOGLE_MODEL_GEMINI_EMBED,
15
- JINA_MODEL_V_4,
12
+ OPENAI_EMBED,
13
+ GEMINI_EMBED,
16
14
  GOOGLE_MODEL_SEMANTIC_RANKER,
17
15
  JINA_MODEL_RERANKER_M0,
18
16
  ] = [
19
17
  'OPENAI', 'GOOGLE', 'OPENROUTER', 'JINA',
20
18
  'global', 'default_ranking_config',
21
19
  'text-embedding-3-small', // dim: 1536
22
- 'text-embedding-3-large', // dim: 3072
23
20
  // https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-embedding-2/
24
- 'gemini-embedding-001', // dim: 768(default), 1536, or 3072(google default)
25
- 'jina-embeddings-v4', // dim: 256‑2048
21
+ 'gemini-embedding-2-preview', // dim: from 128 to 3072, default 768.
26
22
  'semantic-ranker-default@latest',
27
23
  'jina-reranker-m0',
28
24
  ];
@@ -33,9 +29,8 @@ const PROVIDER_BASE_URL = {
33
29
  };
34
30
 
35
31
  const DEFAULT_EMBEDDING_MODELS = {
36
- [OPENAI]: OPENAI_MODEL_EMBED_SMALL,
37
- [OPENROUTER]: GOOGLE_MODEL_GEMINI_EMBED,
38
- [JINA]: JINA_MODEL_V_4,
32
+ [OPENAI]: OPENAI_EMBED,
33
+ [OPENROUTER]: GEMINI_EMBED,
39
34
  };
40
35
 
41
36
  const DEFAULT_RERANKER_MODELS = {
@@ -44,27 +39,12 @@ const DEFAULT_RERANKER_MODELS = {
44
39
  };
45
40
 
46
41
  const MODEL_CONFIG = {
47
- [OPENAI_MODEL_EMBED_SMALL]: {
42
+ [OPENAI_EMBED]: {
48
43
  source: 'openai', image: false, maxTokens: 8192,
49
44
  },
50
- [OPENAI_MODEL_EMBED_LARGE]: {
51
- source: 'openai', image: false, maxTokens: 8192,
52
- },
53
- [GOOGLE_MODEL_GEMINI_EMBED]: {
54
- source: 'google', image: false, maxTokens: 2048,
55
- options: { dimensions: 768 },
56
- },
57
- // Token calculation may be incorrect because its limitation applies to the
58
- // entire request rather than individual entries.
59
- // https://jina.ai/embeddings
60
- [JINA_MODEL_V_4]: {
61
- source: 'jina', image: true, maxTokens: 8192, recordsLimit: 512,
62
- options: {
63
- task: 'text-matching', // 'retrieval.query', 'retrieval.passage'
64
- dimensions: 768, // normalized: true, by default DONT submit
65
- truncate: true, // late_chunking: true, by default DONT submit
66
- embedding_type: 'float',
67
- },
45
+ [GEMINI_EMBED]: {
46
+ source: 'google', image: true, maxTokens: 8192,
47
+ options: { dimensions: 768, encoding_format: 'float' },
68
48
  },
69
49
  [GOOGLE_MODEL_SEMANTIC_RANKER]: {
70
50
  source: 'google', image: false, maxTokens: 1024, recordsLimit: 200,
@@ -136,6 +116,70 @@ const initEmbedding = async (options = {}) => {
136
116
  return getEmbeddingClient(provider);
137
117
  };
138
118
 
119
+ const parseDataUrl = (value) => {
120
+ const match = ensureString(value).match(/^data:([^;]+);base64,(.*)$/i);
121
+ return match ? { mime: match[1], data: match[2], url: value } : null;
122
+ };
123
+
124
+ const normalizeEmbedImage = async (value, options) => {
125
+ const dataUrl = parseDataUrl(value);
126
+ if (dataUrl) { return dataUrl; }
127
+ if (!options?.input && /^https?:\/\//i.test(ensureString(value))) {
128
+ return { url: value, mime: options?.mime };
129
+ }
130
+ if (!options?.input && String.isString(value)) {
131
+ assert(options?.mime, 'Image MIME type is required.', 400);
132
+ return { data: value, mime: options.mime };
133
+ }
134
+ const { content, mime } = await convert(value, {
135
+ ...options, expected: BASE64, meta: true,
136
+ });
137
+ assert(
138
+ (options?.mime || mime)?.startsWith('image/'),
139
+ `Invalid image MIME type: ${options?.mime || mime}.`, 400
140
+ );
141
+ return { data: content, mime: options?.mime || mime };
142
+ };
143
+
144
+ const formatOpenRouterInput = (items) => items.map((x) => {
145
+ if (x.text !== undefined) {
146
+ return { content: [{ type: 'text', text: x.text }] };
147
+ }
148
+ if (x.image) {
149
+ return {
150
+ content: [{
151
+ type: 'image_url',
152
+ image_url: {
153
+ url: x.image.url || formatDataURL(
154
+ x.image.mime, x.image.data
155
+ ),
156
+ },
157
+ }],
158
+ };
159
+ }
160
+ assert(false, 'Unsupported embedding input type.', 400);
161
+ });
162
+
163
+ /**
164
+ * Generate embeddings for text or image input.
165
+ *
166
+ * Usage:
167
+ * await initEmbedding({ provider: 'OPENROUTER', apiKey });
168
+ * await embed('hello world', { provider: 'OPENROUTER' });
169
+ * await embed({ image: '/path/to/image.jpg' }, {
170
+ * provider: 'OPENROUTER',
171
+ * input: 'FILE',
172
+ * });
173
+ * await embed([
174
+ * 'hello world',
175
+ * { image: 'https://example.com/image.jpg' },
176
+ * ], { provider: 'OPENROUTER' });
177
+ *
178
+ * Input accepts a string, { text }, { image }, or an array of those values.
179
+ * Local image files should pass { input: 'FILE' }. Raw base64 image strings
180
+ * need { mime: 'image/...' }. OpenRouter image embeddings are sent through
181
+ * its multimodal `image_url` embeddings format.
182
+ */
139
183
  const embed = async (input, options = {}) => {
140
184
  let [{ client, model: selectedModel, provider, source }, resp]
141
185
  = [getEmbeddingClient(options?.provider), null];
@@ -147,35 +191,33 @@ const embed = async (input, options = {}) => {
147
191
  Object.keys(x).length == 1,
148
192
  'Only one type of input is allowed at a time.', 400
149
193
  );
150
- if (x.text) {
151
- x.text = await trimText(x.text, MODEL_CONFIG[model]?.maxTokens);
152
- } else if (x.image) {
194
+ if (x.text !== undefined) {
195
+ x.text = trimText(x.text, MODEL_CONFIG[model]?.maxTokens);
196
+ } else if (x.image !== undefined) {
153
197
  assert(
154
198
  MODEL_CONFIG[model]?.image,
155
199
  `Model ${model} does not support image embeddings.`, 400
156
200
  );
157
- if (options?.input) {
158
- x.image = await convert(
159
- x.image, { ...options, expected: BASE64 }
160
- );
161
- }
201
+ x.image = await normalizeEmbedImage(x.image, options);
202
+ } else {
203
+ assert(false, 'Unsupported embedding input type.', 400);
162
204
  }
163
205
  return x;
164
206
  }));
165
- MODEL_CONFIG[model]?.image || (input = input.map(x => x.text));
166
207
  assert(input.length, 'Input is required.', 400);
167
208
  const body = {
168
- model, input, ...MODEL_CONFIG[model]?.options || {},
209
+ model,
210
+ input: input.map(x => x.text),
211
+ ...MODEL_CONFIG[model]?.options || {},
169
212
  ...options?.requestOptions || {},
170
213
  };
171
214
  switch (provider) {
172
- case JINA:
173
- resp = await client.post('/embeddings', { body });
174
- break;
175
215
  case OPENROUTER:
176
216
  source = options?.source || source
177
217
  || MODEL_CONFIG[body.model]?.source;
178
218
  body.model = `${source ? `${source}/` : ''}${body.model}`;
219
+ input.some(x => x.image)
220
+ && (body.input = formatOpenRouterInput(input));
179
221
  case OPENAI:
180
222
  resp = await client.embeddings.create(body);
181
223
  break;
package/lib/storage.mjs CHANGED
@@ -31,7 +31,7 @@ const [
31
31
  MIME_JPEG, MIME_MOV, MIME_MPEG, MIME_MP4, MIME_MPG, MIME_AVI, MIME_WMV,
32
32
  MIME_MPEGPS, MIME_FLV, MIME_GIF, MIME_WEBP, MIME_PDF, MIME_AAC, MIME_FLAC,
33
33
  MIME_MP3, MIME_MPEGA, MIME_M4A, MIME_MPGA, MIME_OPUS, MIME_PCM, MIME_WAV,
34
- MIME_WEBM, MIME_TGPP, MIME_PCM16, MIME_OGG,
34
+ MIME_WAVE, MIME_WEBM, MIME_TGPP, MIME_PCM16, MIME_OGG,
35
35
  ] = [
36
36
  'NULL', 'BASE64', 'BUFFER', 'FILE', 'STREAM', 'TEXT', 'JSON', 'utf8',
37
37
  'binary', 'BLOB', 'DATAURL', '0644', '0755', 'text/plain',
@@ -40,7 +40,8 @@ const [
40
40
  'video/avi', 'video/wmv', 'video/mpegps', 'video/x-flv', 'image/gif',
41
41
  'image/webp', 'application/pdf', 'audio/aac', 'audio/flac', 'audio/mp3',
42
42
  'audio/mpeg', 'audio/m4a', 'audio/mpga', 'audio/opus', 'audio/pcm',
43
- 'audio/wav', 'audio/webm', 'video/3gpp', 'audio/x-wav', 'audio/ogg',
43
+ 'audio/wav', 'audio/wave', 'audio/webm', 'video/3gpp', 'audio/x-wav',
44
+ 'audio/ogg',
44
45
  ];
45
46
 
46
47
  const [encodeBase64, encodeBinary, encodeNull]
@@ -551,6 +552,7 @@ export {
551
552
  MIME_TEXT,
552
553
  MIME_TGPP,
553
554
  MIME_WAV,
555
+ MIME_WAVE,
554
556
  MIME_WEBM,
555
557
  MIME_WEBP,
556
558
  MIME_WMV,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "2001.1.136",
4
+ "version": "2001.1.139",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
8
8
  "type": "module",
9
9
  "engines": {
10
- "node": ">=20.x"
10
+ "node": ">=22.5.0"
11
11
  },
12
12
  "scripts": {
13
13
  "start": "node index.mjs",
@@ -30,19 +30,19 @@
30
30
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz"
31
31
  },
32
32
  "dependencies": {
33
- "file-type": "^21.3.3",
34
- "mathjs": "^15.1.1",
35
- "uuid": "^13.0.0"
33
+ "file-type": "^22.0.1",
34
+ "mathjs": "^15.2.0",
35
+ "uuid": "^14.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
39
39
  "@ffprobe-installer/ffprobe": "^2.1.2",
40
- "@google-cloud/discoveryengine": "^2.5.3",
40
+ "@google-cloud/discoveryengine": "^2.6.0",
41
41
  "@google-cloud/storage": "^7.19.0",
42
- "@google/genai": "^1.46.0",
42
+ "@google/genai": "^1.51.0",
43
43
  "@mozilla/readability": "github:mozilla/readability",
44
- "@sentry/node": "^10.44.0",
45
- "@sentry/profiling-node": "^10.44.0",
44
+ "@sentry/node": "^10.51.0",
45
+ "@sentry/profiling-node": "^10.51.0",
46
46
  "acme-client": "^5.4.0",
47
47
  "browserify-fs": "^1.0.0",
48
48
  "buffer": "^6.0.3",
@@ -51,34 +51,34 @@
51
51
  "fluent-ffmpeg": "^2.1.3",
52
52
  "form-data": "^4.0.5",
53
53
  "google-gax": "^5.0.6",
54
- "ioredis": "^5.10.0",
55
- "jsdom": "^29.0.0",
54
+ "ioredis": "^5.10.1",
55
+ "jsdom": "^29.1.0",
56
56
  "lorem-ipsum": "^2.0.8",
57
- "mailgun.js": "^12.7.1",
58
- "mailparser": "^3.9.4",
57
+ "mailgun.js": "^13.0.0",
58
+ "mailparser": "^3.9.8",
59
59
  "mime": "^4.1.0",
60
- "mysql2": "^3.20.0",
60
+ "mysql2": "^3.22.3",
61
61
  "node-mailjet": "^6.0.11",
62
62
  "node-polyfill-webpack-plugin": "^4.1.0",
63
63
  "office-text-extractor": "^4.0.0",
64
- "openai": "^6.32.0",
64
+ "openai": "^6.35.0",
65
65
  "pdf-lib": "^1.17.1",
66
- "pdfjs-dist": "^5.5.207",
66
+ "pdfjs-dist": "^5.7.284",
67
67
  "pg": "^8.20.0",
68
68
  "pgvector": "^0.2.1",
69
69
  "ping": "^1.0.0",
70
70
  "process": "^0.11.10",
71
- "puppeteer": "^24.39.1",
71
+ "puppeteer": "^24.42.0",
72
72
  "say": "^0.16.0",
73
73
  "telegraf": "^4.16.3",
74
74
  "telesignsdk": "^5.0.0",
75
75
  "tesseract.js": "^7.0.0",
76
- "twilio": "^5.13.0",
76
+ "twilio": "^6.0.0",
77
77
  "url": "github:Leask/node-url",
78
78
  "webpack-cli": "^7.0.2",
79
79
  "whisper-node": "^1.1.1",
80
- "wrangler": "^4.75.0",
80
+ "wrangler": "^4.86.0",
81
81
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz",
82
- "youtube-transcript": "^1.3.0"
82
+ "youtube-transcript": "^1.3.1"
83
83
  }
84
84
  }