millas 0.2.12-beta-2 → 0.2.13

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.
Files changed (57) hide show
  1. package/package.json +3 -2
  2. package/src/admin/Admin.js +122 -38
  3. package/src/admin/ViewContext.js +12 -3
  4. package/src/admin/resources/AdminResource.js +10 -0
  5. package/src/admin/static/admin.css +95 -14
  6. package/src/admin/views/layouts/base.njk +23 -34
  7. package/src/admin/views/pages/detail.njk +16 -5
  8. package/src/admin/views/pages/error.njk +65 -0
  9. package/src/admin/views/pages/list.njk +127 -2
  10. package/src/admin/views/partials/form-scripts.njk +7 -3
  11. package/src/admin/views/partials/form-widget.njk +2 -1
  12. package/src/admin/views/partials/icons.njk +64 -0
  13. package/src/ai/AIManager.js +954 -0
  14. package/src/ai/AITokenBudget.js +250 -0
  15. package/src/ai/PromptGuard.js +216 -0
  16. package/src/ai/agents.js +218 -0
  17. package/src/ai/conversation.js +213 -0
  18. package/src/ai/drivers.js +734 -0
  19. package/src/ai/files.js +249 -0
  20. package/src/ai/media.js +303 -0
  21. package/src/ai/pricing.js +152 -0
  22. package/src/ai/provider_tools.js +114 -0
  23. package/src/ai/types.js +356 -0
  24. package/src/commands/createsuperuser.js +17 -4
  25. package/src/commands/serve.js +2 -4
  26. package/src/container/AppInitializer.js +39 -15
  27. package/src/container/Application.js +31 -1
  28. package/src/core/foundation.js +1 -1
  29. package/src/errors/HttpError.js +32 -16
  30. package/src/facades/AI.js +411 -0
  31. package/src/facades/Hash.js +67 -0
  32. package/src/facades/Process.js +144 -0
  33. package/src/hashing/Hash.js +262 -0
  34. package/src/http/HtmlEscape.js +162 -0
  35. package/src/http/MillasRequest.js +63 -7
  36. package/src/http/MillasResponse.js +70 -4
  37. package/src/http/ResponseDispatcher.js +21 -27
  38. package/src/http/SafeFilePath.js +195 -0
  39. package/src/http/SafeRedirect.js +62 -0
  40. package/src/http/SecurityBootstrap.js +70 -0
  41. package/src/http/helpers.js +40 -125
  42. package/src/http/index.js +10 -1
  43. package/src/http/middleware/CsrfMiddleware.js +258 -0
  44. package/src/http/middleware/RateLimiter.js +314 -0
  45. package/src/http/middleware/SecurityHeaders.js +281 -0
  46. package/src/i18n/Translator.js +10 -2
  47. package/src/logger/LogRedactor.js +247 -0
  48. package/src/logger/Logger.js +1 -1
  49. package/src/logger/formatters/JsonFormatter.js +11 -4
  50. package/src/logger/formatters/PrettyFormatter.js +3 -1
  51. package/src/logger/formatters/SimpleFormatter.js +14 -3
  52. package/src/middleware/ThrottleMiddleware.js +27 -4
  53. package/src/process/Process.js +333 -0
  54. package/src/router/MiddlewareRegistry.js +27 -2
  55. package/src/scaffold/templates.js +3 -0
  56. package/src/validation/Validator.js +348 -607
  57. package/src/admin.zip +0 -0
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ const { AIError, AIProviderError } = require('./types');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // AIFile — a file stored with an AI provider
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ class AIFile {
10
+ constructor({ id, filename, mimeType, size, provider, createdAt }) {
11
+ this.id = id;
12
+ this.filename = filename;
13
+ this.mimeType = mimeType;
14
+ this.size = size;
15
+ this.provider = provider;
16
+ this.createdAt = createdAt;
17
+ }
18
+
19
+ mimeType() { return this.mimeType; }
20
+ }
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // PendingFile — fluent builder for uploading a file to a provider
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ class PendingFile {
27
+ constructor(manager, source) {
28
+ this._manager = manager;
29
+ this._source = source; // { type: 'path'|'storage'|'url'|'buffer'|'id', value, mimeType, filename }
30
+ this._provider = null;
31
+ this._purpose = 'assistants';
32
+ }
33
+
34
+ using(provider) { this._provider = provider; return this; }
35
+ purpose(p) { this._purpose = p; return this; }
36
+
37
+ /**
38
+ * Upload the file to the provider.
39
+ * Returns AIFile with the stored file's ID.
40
+ *
41
+ * const f = await AI.files.fromPath('/report.pdf').put();
42
+ * console.log(f.id); // 'file-abc123'
43
+ */
44
+ async put(provider = null) {
45
+ const prov = provider || this._provider || this._manager._default;
46
+ const driver = this._manager._resolveDriver(prov);
47
+ if (typeof driver.uploadFile !== 'function') {
48
+ throw new AIProviderError(prov, `Provider "${prov}" does not support file storage.`);
49
+ }
50
+
51
+ const fs = require('fs');
52
+ const path = require('path');
53
+ const { resolveStoragePath } = require('../http/SafeFilePath');
54
+ let buf, filename, mimeType;
55
+
56
+ if (this._source.type === 'path') {
57
+ // Guard: resolve within storage root to prevent path traversal.
58
+ // If the path is already absolute and within an allowed directory,
59
+ // resolveStoragePath returns it unchanged. If it escapes, it throws.
60
+ const safePath = resolveStoragePath(
61
+ this._source.value,
62
+ require('../http/SafeFilePath').SafeFilePath.getStorageRoot()
63
+ );
64
+ buf = fs.readFileSync(safePath);
65
+ filename = this._source.filename || path.basename(safePath);
66
+ mimeType = this._source.mimeType || _mimeFromExt(path.extname(filename));
67
+ } else if (this._source.type === 'storage') {
68
+ const storage = this._manager._storage;
69
+ if (!storage) throw new Error('Storage service not available.');
70
+ buf = await storage.get(this._source.value);
71
+ filename = this._source.filename || path.basename(this._source.value);
72
+ mimeType = this._source.mimeType || _mimeFromExt(path.extname(filename));
73
+ } else if (this._source.type === 'url') {
74
+ const res = await fetch(this._source.value);
75
+ buf = Buffer.from(await res.arrayBuffer());
76
+ filename = this._source.filename || path.basename(new URL(this._source.value).pathname) || 'file';
77
+ mimeType = res.headers.get('content-type') || 'application/octet-stream';
78
+ } else if (this._source.type === 'buffer') {
79
+ buf = this._source.value;
80
+ filename = this._source.filename || 'file';
81
+ mimeType = this._source.mimeType || 'application/octet-stream';
82
+ } else if (this._source.type === 'id') {
83
+ // Already stored — return a stub
84
+ return new AIFile({ id: this._source.value, filename: null, mimeType: null, size: null, provider: prov });
85
+ }
86
+
87
+ const result = await driver.uploadFile({ buf, filename, mimeType, purpose: this._purpose });
88
+ return new AIFile({ ...result, provider: prov });
89
+ }
90
+
91
+ /**
92
+ * Retrieve metadata for a previously stored file.
93
+ *
94
+ * const f = await AI.files.fromId('file-abc').get();
95
+ */
96
+ async get(provider = null) {
97
+ if (this._source.type !== 'id') throw new Error('get() is only available on fromId().');
98
+ const prov = provider || this._provider || this._manager._default;
99
+ const driver = this._manager._resolveDriver(prov);
100
+ if (typeof driver.getFile !== 'function') throw new AIProviderError(prov, `Provider "${prov}" does not support file retrieval.`);
101
+ const result = await driver.getFile(this._source.value);
102
+ return new AIFile({ ...result, provider: prov });
103
+ }
104
+
105
+ /**
106
+ * Delete a previously stored file.
107
+ *
108
+ * await AI.files.fromId('file-abc').delete();
109
+ */
110
+ async delete(provider = null) {
111
+ if (this._source.type !== 'id') throw new Error('delete() is only available on fromId().');
112
+ const prov = provider || this._provider || this._manager._default;
113
+ const driver = this._manager._resolveDriver(prov);
114
+ if (typeof driver.deleteFile !== 'function') throw new AIProviderError(prov, `Provider "${prov}" does not support file deletion.`);
115
+ return driver.deleteFile(this._source.value);
116
+ }
117
+
118
+ /** Use this file as an attachment in an agent prompt. */
119
+ toAttachment() { return { type: 'file', source: this._source }; }
120
+ }
121
+
122
+ // ─────────────────────────────────────────────────────────────────────────────
123
+ // AIFilesAPI — the AI.files namespace
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+
126
+ class AIFilesAPI {
127
+ constructor(manager) { this._manager = manager; }
128
+
129
+ fromPath(path, opts = {}) { return new PendingFile(this._manager, { type: 'path', value: path, ...opts }); }
130
+ fromStorage(path, opts = {}) { return new PendingFile(this._manager, { type: 'storage', value: path, ...opts }); }
131
+ fromUrl(url, opts = {}) { return new PendingFile(this._manager, { type: 'url', value: url, ...opts }); }
132
+ fromBuffer(buf, filename, mimeType) { return new PendingFile(this._manager, { type: 'buffer', value: buf, filename, mimeType }); }
133
+ fromId(id) { return new PendingFile(this._manager, { type: 'id', value: id }); }
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // AIVectorStore — a vector store (collection of indexed files for RAG)
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+
140
+ class AIVectorStore {
141
+ constructor({ id, name, provider, fileCounts = {}, ready = false, meta = {} }) {
142
+ this.id = id;
143
+ this.name = name;
144
+ this.provider = provider;
145
+ this.fileCounts = fileCounts;
146
+ this.ready = ready;
147
+ this.meta = meta;
148
+ }
149
+
150
+ /**
151
+ * Add a file to this vector store.
152
+ * Accepts a file ID string, a PendingFile, or any uploadable source.
153
+ *
154
+ * await store.add('file-abc123');
155
+ * await store.add(AI.files.fromPath('/doc.pdf'));
156
+ * await store.add(AI.files.fromStorage('manual.pdf'));
157
+ */
158
+ async add(fileOrId, metadata = {}) {
159
+ const driver = this._driver;
160
+ let fileId;
161
+
162
+ if (typeof fileOrId === 'string') {
163
+ fileId = fileOrId;
164
+ } else if (fileOrId instanceof PendingFile) {
165
+ // Upload first then add
166
+ const uploaded = await fileOrId.using(this.provider).put();
167
+ fileId = uploaded.id;
168
+ } else {
169
+ throw new Error('store.add() expects a file ID string or PendingFile.');
170
+ }
171
+
172
+ return driver.addFileToStore(this.id, fileId, metadata);
173
+ }
174
+
175
+ /**
176
+ * Remove a file from this vector store.
177
+ *
178
+ * await store.remove('file-abc123');
179
+ * await store.remove('file-abc123', { deleteFile: true }); // also deletes from provider storage
180
+ */
181
+ async remove(fileId, { deleteFile = false } = {}) {
182
+ const driver = this._driver;
183
+ await driver.removeFileFromStore(this.id, fileId);
184
+ if (deleteFile) await driver.deleteFile(fileId);
185
+ }
186
+
187
+ /** Delete this entire vector store. */
188
+ async delete() {
189
+ return this._driver.deleteStore(this.id);
190
+ }
191
+ }
192
+
193
+ // ─────────────────────────────────────────────────────────────────────────────
194
+ // AIStoresAPI — the AI.stores namespace
195
+ // ─────────────────────────────────────────────────────────────────────────────
196
+
197
+ class AIStoresAPI {
198
+ constructor(manager) { this._manager = manager; }
199
+
200
+ /**
201
+ * Create a new vector store.
202
+ *
203
+ * const store = await AI.stores.create('Knowledge Base');
204
+ * const store = await AI.stores.create('Docs', { description: '...', expiresIn: 86400 * 30 });
205
+ */
206
+ async create(name, opts = {}, provider = null) {
207
+ const prov = provider || this._manager._default;
208
+ const driver = this._manager._resolveDriver(prov);
209
+ if (typeof driver.createStore !== 'function') throw new AIProviderError(prov, `Provider "${prov}" does not support vector stores.`);
210
+ const result = await driver.createStore({ name, ...opts });
211
+ const store = new AIVectorStore({ ...result, provider: prov });
212
+ store._driver = driver;
213
+ return store;
214
+ }
215
+
216
+ /**
217
+ * Retrieve an existing vector store by ID.
218
+ *
219
+ * const store = await AI.stores.get('vs_abc123');
220
+ */
221
+ async get(id, provider = null) {
222
+ const prov = provider || this._manager._default;
223
+ const driver = this._manager._resolveDriver(prov);
224
+ if (typeof driver.getStore !== 'function') throw new AIProviderError(prov, `Provider "${prov}" does not support vector stores.`);
225
+ const result = await driver.getStore(id);
226
+ const store = new AIVectorStore({ ...result, provider: prov });
227
+ store._driver = driver;
228
+ return store;
229
+ }
230
+
231
+ /**
232
+ * Delete a vector store by ID.
233
+ *
234
+ * await AI.stores.delete('vs_abc123');
235
+ */
236
+ async delete(id, provider = null) {
237
+ const prov = provider || this._manager._default;
238
+ const driver = this._manager._resolveDriver(prov);
239
+ if (typeof driver.deleteStore !== 'function') throw new AIProviderError(prov, `Provider "${prov}" does not support vector stores.`);
240
+ return driver.deleteStore(id);
241
+ }
242
+ }
243
+
244
+ function _mimeFromExt(ext) {
245
+ const map = { '.pdf': 'application/pdf', '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.csv': 'text/csv', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.mp3': 'audio/mpeg', '.mp4': 'audio/mp4', '.wav': 'audio/wav' };
246
+ return map[(ext || '').toLowerCase()] || 'application/octet-stream';
247
+ }
248
+
249
+ module.exports = { AIFile, PendingFile, AIFilesAPI, AIVectorStore, AIStoresAPI };
@@ -0,0 +1,303 @@
1
+ 'use strict';
2
+
3
+ const { AIProviderError } = require('./types');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // AIImageResponse
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ class AIImageResponse {
10
+ constructor({ images, urls = [], provider, model, prompt }) {
11
+ this._images = images || []; // array of Buffers (when b64 returned)
12
+ this._urls = urls || []; // array of URLs (when url returned)
13
+ this.provider = provider;
14
+ this.model = model;
15
+ this.prompt = prompt;
16
+ }
17
+
18
+ /** First image buffer (null if provider returned URLs instead). */
19
+ get buffer() { return this._images[0] || null; }
20
+
21
+ /** All image buffers. */
22
+ get buffers() { return this._images; }
23
+
24
+ /** First image URL (null if provider returned buffers instead). */
25
+ get url() { return this._urls[0] || null; }
26
+
27
+ /** All image URLs. */
28
+ get urls() { return this._urls; }
29
+
30
+ /** Store first image to a path using Millas Storage or write to local filesystem. */
31
+ async store(filePath, disk = null) {
32
+ if (!this.buffer) throw new Error('No image buffer available — provider returned a URL. Use img.url instead.');
33
+ const storage = AIImageResponse._storage;
34
+ if (storage) {
35
+ const d = disk ? storage.disk(disk) : storage;
36
+ await d.put(filePath, this.buffer);
37
+ return filePath;
38
+ }
39
+ // Fallback — write directly to filesystem
40
+ const fs = require('fs');
41
+ const path = require('path');
42
+ const dir = path.dirname(filePath);
43
+ if (dir && dir !== '.') fs.mkdirSync(dir, { recursive: true });
44
+ fs.writeFileSync(filePath, this.buffer);
45
+ return filePath;
46
+ }
47
+
48
+ /** Store with auto-generated extension. */
49
+ async storeAs(name, disk = null) {
50
+ const filePath = name.includes('.') ? name : `${name}.png`;
51
+ return this.store(filePath, disk);
52
+ }
53
+
54
+ toString() {
55
+ if (this._urls[0]) return this._urls[0];
56
+ if (this._images[0]) return `[AIImageResponse: ${this._images.length} image(s) from ${this.provider}]`;
57
+ return '[AIImageResponse: empty]';
58
+ }
59
+ }
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // PendingImage
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+
65
+ class PendingImage {
66
+ constructor(manager, prompt) {
67
+ this._manager = manager;
68
+ this._prompt = prompt;
69
+ this._provider = null;
70
+ this._model = null;
71
+ this._size = null;
72
+ this._quality = 'standard';
73
+ this._n = 1;
74
+ this._aspect = '1:1';
75
+ this._attachments = [];
76
+ this._queued = false;
77
+ this._thenFns = [];
78
+ }
79
+
80
+ /** Select a specific provider. */
81
+ using(provider) { this._provider = provider; return this; }
82
+ model(model) { this._model = model; return this; }
83
+ quality(q) { this._quality = q; return this; }
84
+ count(n) { this._n = n; return this; }
85
+
86
+ /** 1:1 square aspect ratio (default). */
87
+ square() { this._aspect = '1:1'; this._size = '1024x1024'; return this; }
88
+ /** 16:9 landscape. */
89
+ landscape() { this._aspect = '16:9'; this._size = '1792x1024'; return this; }
90
+ /** 9:16 portrait. */
91
+ portrait() { this._aspect = '9:16'; this._size = '1024x1792'; return this; }
92
+
93
+ /** Attach reference images. */
94
+ attachments(files) { this._attachments = files; return this; }
95
+
96
+ /** Queue the generation. Returns a QueuedResponse. */
97
+ queue() { this._queued = true; return this; }
98
+
99
+ then(fn) { this._thenFns.push(fn); return this; }
100
+
101
+ async generate() {
102
+ const provider = this._provider || this._manager._default;
103
+ const driver = this._manager._resolveDriver(provider);
104
+ const result = await driver.image({
105
+ prompt: this._prompt,
106
+ model: this._model,
107
+ size: this._size,
108
+ quality: this._quality,
109
+ n: this._n,
110
+ aspectRatio: this._aspect,
111
+ attachments: this._attachments,
112
+ });
113
+ const response = new AIImageResponse({ ...result, prompt: this._prompt });
114
+ for (const fn of this._thenFns) await fn(response);
115
+ return response;
116
+ }
117
+ }
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // AIAudioResponse
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ class AIAudioResponse {
124
+ constructor({ audio, format, provider }) {
125
+ this._audio = audio; // Buffer
126
+ this.format = format;
127
+ this.provider = provider;
128
+ }
129
+
130
+ get buffer() { return this._audio; }
131
+
132
+ async store(filePath, disk = null) {
133
+ const storage = AIAudioResponse._storage;
134
+ if (!storage) throw new Error('Storage service not available.');
135
+ const d = disk ? storage.disk(disk) : storage;
136
+ await d.put(filePath, this._audio);
137
+ return filePath;
138
+ }
139
+
140
+ async storeAs(name, disk = null) {
141
+ const path = name.includes('.') ? name : `${name}.${this.format || 'mp3'}`;
142
+ return this.store(path, disk);
143
+ }
144
+
145
+ toString() { return `[AIAudioResponse: ${this._audio.length} bytes, ${this.format}]`; }
146
+ }
147
+
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ // PendingAudio (TTS)
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+
152
+ class PendingAudio {
153
+ constructor(manager, text) {
154
+ this._manager = manager;
155
+ this._text = text;
156
+ this._provider = null;
157
+ this._model = null;
158
+ this._voice = null;
159
+ this._instructions = null;
160
+ }
161
+
162
+ using(provider) { this._provider = provider; return this; }
163
+ model(model) { this._model = model; return this; }
164
+ voice(v) { this._voice = v; return this; }
165
+ male() { this._voice = 'onyx'; return this; }
166
+ female() { this._voice = 'nova'; return this; }
167
+ instructions(text) { this._instructions = text; return this; }
168
+
169
+ async generate() {
170
+ const provider = this._provider || this._manager._audioProvider || this._manager._default;
171
+ const driver = this._manager._resolveDriver(provider);
172
+ const result = await driver.tts({ text: this._text, model: this._model, voice: this._voice, instructions: this._instructions });
173
+ return new AIAudioResponse(result);
174
+ }
175
+ }
176
+
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ // AITranscriptionResponse
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+
181
+ class AITranscriptionResponse {
182
+ constructor({ text, words = [], speakers = [], provider }) {
183
+ this.text = text;
184
+ this.words = words; // [{ word, start, end, speaker }]
185
+ this.speakers = speakers; // [{ speaker, segments }]
186
+ this.provider = provider;
187
+ }
188
+
189
+ toString() { return this.text; }
190
+ }
191
+
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+ // PendingTranscription (STT)
194
+ // ─────────────────────────────────────────────────────────────────────────────
195
+
196
+ class PendingTranscription {
197
+ constructor(manager, source) {
198
+ this._manager = manager;
199
+ this._source = source; // { type: 'path'|'storage'|'buffer', value }
200
+ this._provider = null;
201
+ this._model = null;
202
+ this._language = null;
203
+ this._prompt = null;
204
+ this._diarize = false;
205
+ }
206
+
207
+ using(provider) { this._provider = provider; return this; }
208
+ model(model) { this._model = model; return this; }
209
+ language(lang) { this._language = lang; return this; }
210
+ prompt(p) { this._prompt = p; return this; }
211
+ /** Include speaker diarization (who said what). */
212
+ diarize() { this._diarize = true; return this; }
213
+
214
+ async generate() {
215
+ const fs = require('fs');
216
+ const path = require('path');
217
+ const { resolveStoragePath, SafeFilePath } = require('../http/SafeFilePath');
218
+ let audio, filename, mimeType;
219
+
220
+ if (this._source.type === 'path') {
221
+ const safePath = resolveStoragePath(
222
+ this._source.value,
223
+ SafeFilePath.getStorageRoot()
224
+ );
225
+ audio = fs.readFileSync(safePath);
226
+ filename = path.basename(safePath);
227
+ mimeType = _mimeFromExt(path.extname(filename));
228
+ } else if (this._source.type === 'storage') {
229
+ const storage = PendingTranscription._storage;
230
+ if (!storage) throw new Error('Storage not available.');
231
+ audio = await storage.get(this._source.value);
232
+ filename = path.basename(this._source.value);
233
+ mimeType = _mimeFromExt(path.extname(filename));
234
+ } else if (this._source.type === 'buffer') {
235
+ audio = this._source.value;
236
+ filename = this._source.filename || 'audio.mp3';
237
+ mimeType = this._source.mimeType || 'audio/mpeg';
238
+ }
239
+
240
+ const provider = this._provider || this._manager._audioProvider || 'openai';
241
+ const driver = this._manager._resolveDriver(provider);
242
+ const result = await driver.transcribe({ audio, filename, mimeType, model: this._model, language: this._language, prompt: this._prompt, diarize: this._diarize });
243
+ return new AITranscriptionResponse(result);
244
+ }
245
+ }
246
+
247
+ function _mimeFromExt(ext) {
248
+ const map = { '.mp3': 'audio/mpeg', '.mp4': 'audio/mp4', '.m4a': 'audio/mp4', '.wav': 'audio/wav', '.webm': 'audio/webm', '.ogg': 'audio/ogg', '.flac': 'audio/flac' };
249
+ return map[ext.toLowerCase()] || 'audio/mpeg';
250
+ }
251
+
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ // AIRerankResponse
254
+ // ─────────────────────────────────────────────────────────────────────────────
255
+
256
+ class AIRerankResponse {
257
+ constructor(results) {
258
+ this._results = results; // [{ index, score, document }] sorted by score desc
259
+ }
260
+
261
+ /** Top result. */
262
+ get first() { return this._results[0]; }
263
+
264
+ /** All results. */
265
+ get all() { return this._results; }
266
+
267
+ /** Get result at position n (0-indexed). */
268
+ at(n) { return this._results[n]; }
269
+
270
+ [Symbol.iterator]() { return this._results[Symbol.iterator](); }
271
+ }
272
+
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+ // PendingReranking
275
+ // ─────────────────────────────────────────────────────────────────────────────
276
+
277
+ class PendingReranking {
278
+ constructor(manager, documents) {
279
+ this._manager = manager;
280
+ this._documents = documents;
281
+ this._provider = null;
282
+ this._model = null;
283
+ this._limit = null;
284
+ }
285
+
286
+ using(provider) { this._provider = provider; return this; }
287
+ model(model) { this._model = model; return this; }
288
+ limit(n) { this._limit = n; return this; }
289
+
290
+ async rerank(query) {
291
+ const provider = this._provider || this._manager._rerankProvider || 'cohere';
292
+ const driver = this._manager._resolveDriver(provider);
293
+ const results = await driver.rerank({ query, documents: this._documents, model: this._model, limit: this._limit });
294
+ return new AIRerankResponse(results.sort((a, b) => b.score - a.score));
295
+ }
296
+ }
297
+
298
+ module.exports = {
299
+ PendingImage, AIImageResponse,
300
+ PendingAudio, AIAudioResponse,
301
+ PendingTranscription, AITranscriptionResponse,
302
+ PendingReranking, AIRerankResponse,
303
+ };