outlet-orm 7.0.0 → 9.0.1

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 (46) hide show
  1. package/README.md +130 -2
  2. package/docs/skills/outlet-orm/AI.md +452 -102
  3. package/docs/skills/outlet-orm/API.md +108 -0
  4. package/docs/skills/outlet-orm/QUERIES.md +64 -0
  5. package/docs/skills/outlet-orm/SEEDS.md +47 -0
  6. package/docs/skills/outlet-orm/SKILL.md +15 -7
  7. package/package.json +1 -1
  8. package/src/AI/AIPromptEnhancer.js +170 -0
  9. package/src/AI/AIQueryBuilder.js +234 -0
  10. package/src/AI/AIQueryOptimizer.js +185 -0
  11. package/src/AI/AISeeder.js +181 -0
  12. package/src/AI/AiBridgeManager.js +287 -0
  13. package/src/AI/Builders/TextBuilder.js +170 -0
  14. package/src/AI/Contracts/AudioProviderContract.js +29 -0
  15. package/src/AI/Contracts/ChatProviderContract.js +38 -0
  16. package/src/AI/Contracts/EmbeddingsProviderContract.js +19 -0
  17. package/src/AI/Contracts/ImageProviderContract.js +19 -0
  18. package/src/AI/Contracts/ModelsProviderContract.js +26 -0
  19. package/src/AI/Contracts/ToolContract.js +25 -0
  20. package/src/AI/Facades/AiBridge.js +79 -0
  21. package/src/AI/MCPServer.js +113 -0
  22. package/src/AI/Providers/ClaudeProvider.js +64 -0
  23. package/src/AI/Providers/CustomOpenAIProvider.js +238 -0
  24. package/src/AI/Providers/GeminiProvider.js +68 -0
  25. package/src/AI/Providers/GrokProvider.js +46 -0
  26. package/src/AI/Providers/MistralProvider.js +21 -0
  27. package/src/AI/Providers/OllamaProvider.js +249 -0
  28. package/src/AI/Providers/OllamaTurboProvider.js +32 -0
  29. package/src/AI/Providers/OnnProvider.js +46 -0
  30. package/src/AI/Providers/OpenAIProvider.js +471 -0
  31. package/src/AI/Support/AudioNormalizer.js +37 -0
  32. package/src/AI/Support/ChatNormalizer.js +42 -0
  33. package/src/AI/Support/Document.js +77 -0
  34. package/src/AI/Support/DocumentAttachmentMapper.js +101 -0
  35. package/src/AI/Support/EmbeddingsNormalizer.js +30 -0
  36. package/src/AI/Support/Exceptions/ProviderError.js +22 -0
  37. package/src/AI/Support/FileSecurity.js +56 -0
  38. package/src/AI/Support/ImageNormalizer.js +62 -0
  39. package/src/AI/Support/JsonSchemaValidator.js +73 -0
  40. package/src/AI/Support/Message.js +40 -0
  41. package/src/AI/Support/StreamChunk.js +45 -0
  42. package/src/AI/Support/ToolChatRunner.js +160 -0
  43. package/src/AI/Support/ToolRegistry.js +62 -0
  44. package/src/AI/Tools/SystemInfoTool.js +25 -0
  45. package/src/index.js +67 -1
  46. package/types/index.d.ts +326 -0
@@ -0,0 +1,471 @@
1
+ 'use strict';
2
+
3
+ const ChatProviderContract = require('../Contracts/ChatProviderContract');
4
+ const EmbeddingsProviderContract = require('../Contracts/EmbeddingsProviderContract');
5
+ const ImageProviderContract = require('../Contracts/ImageProviderContract');
6
+ const AudioProviderContract = require('../Contracts/AudioProviderContract');
7
+ const ModelsProviderContract = require('../Contracts/ModelsProviderContract');
8
+ const JsonSchemaValidator = require('../Support/JsonSchemaValidator');
9
+ const DocumentAttachmentMapper = require('../Support/DocumentAttachmentMapper');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * OpenAIProvider
15
+ * Full-featured provider for OpenAI Chat Completions & Responses APIs.
16
+ * Supports chat, streaming (SSE), embeddings, images (DALL-E), audio TTS/STT, models,
17
+ * function calling, JSON schema validation.
18
+ */
19
+ class OpenAIProvider {
20
+ /**
21
+ * @param {string} apiKey
22
+ * @param {string} [chatEndpoint='https://api.openai.com/v1/chat/completions']
23
+ */
24
+ constructor(apiKey, chatEndpoint = 'https://api.openai.com/v1/chat/completions') {
25
+ this.apiKey = apiKey;
26
+ this.chatEndpoint = chatEndpoint;
27
+ this.responsesEndpoint = 'https://api.openai.com/v1/responses';
28
+ this.modelsEndpoint = 'https://api.openai.com/v1/models';
29
+ this.embeddingsEndpoint = 'https://api.openai.com/v1/embeddings';
30
+ this.imageEndpoint = 'https://api.openai.com/v1/images/generations';
31
+ this.imageEditsEndpoint = 'https://api.openai.com/v1/images/edits';
32
+ this.imageVariationsEndpoint = 'https://api.openai.com/v1/images/variations';
33
+ this.speechToTextEndpoint = 'https://api.openai.com/v1/audio/transcriptions';
34
+ this.speechTranslationsEndpoint = 'https://api.openai.com/v1/audio/translations';
35
+ this.textToSpeechEndpoint = 'https://api.openai.com/v1/audio/speech';
36
+ this.filesEndpoint = 'https://api.openai.com/v1/files';
37
+ this.vectorStoresEndpoint = 'https://api.openai.com/v1/vector_stores';
38
+ }
39
+
40
+ /** @private */
41
+ _headers(options = {}) {
42
+ const h = {
43
+ 'Authorization': `Bearer ${this.apiKey}`,
44
+ 'Content-Type': 'application/json',
45
+ 'Accept': 'application/json',
46
+ };
47
+ if (options.organization) h['OpenAI-Organization'] = options.organization;
48
+ if (options.project) h['OpenAI-Project'] = options.project;
49
+ return h;
50
+ }
51
+
52
+ /** @private */
53
+ async _post(url, body, options = {}, streamFlag = false) {
54
+ const headers = this._headers(options);
55
+ const fetchOpts = { method: 'POST', headers, body: JSON.stringify(body) };
56
+ if (streamFlag) {
57
+ // Return raw response for streaming
58
+ return fetch(url, fetchOpts);
59
+ }
60
+ const res = await fetch(url, fetchOpts);
61
+ return res.json();
62
+ }
63
+
64
+ /** @private */
65
+ async _get(url, options = {}) {
66
+ const headers = this._headers(options);
67
+ const res = await fetch(url, { method: 'GET', headers });
68
+ return res.json();
69
+ }
70
+
71
+ // ─── Chat ───
72
+ async chat(messages, options = {}) {
73
+ // Default: Responses API (unless api === 'chat')
74
+ if ((options.api || null) !== 'chat') {
75
+ const payload = this._buildResponsesPayload(messages, options);
76
+ this._maybeAttachFileSearch(payload, options);
77
+ const data = await this._post(this.responsesEndpoint, payload, options);
78
+ if (options.response_format === 'json') {
79
+ const schema = (options.json_schema || {}).schema || { type: 'object' };
80
+ const rawContent = data.output_text || null;
81
+ if (typeof rawContent === 'string') {
82
+ try {
83
+ const decoded = JSON.parse(rawContent);
84
+ if (typeof decoded === 'object' && decoded !== null) {
85
+ const errors = [];
86
+ if (!JsonSchemaValidator.validate(decoded, schema, errors)) {
87
+ data.schema_validation = { valid: false, errors };
88
+ } else {
89
+ data.schema_validation = { valid: true };
90
+ }
91
+ }
92
+ } catch { /* not JSON */ }
93
+ }
94
+ }
95
+ return data;
96
+ }
97
+
98
+ // Chat Completions API path
99
+ const payload = this._buildBasePayload(messages, options);
100
+ const schema = this._applySchemaIfAny(payload, options);
101
+ this._applyNativeToolsIfAny(payload, options);
102
+ const data = await this._post(this.chatEndpoint, payload, options);
103
+
104
+ // Normalize tool_calls
105
+ if (data?.choices?.[0]?.message?.tool_calls) {
106
+ data.tool_calls = data.choices[0].message.tool_calls.map(tc => ({
107
+ id: tc.id || null,
108
+ name: (tc.function || {}).name || null,
109
+ arguments: (() => { try { return JSON.parse((tc.function || {}).arguments || '{}'); } catch { return {}; } })(),
110
+ }));
111
+ }
112
+
113
+ // Schema validation
114
+ if (schema && data?.choices?.[0]?.message?.content) {
115
+ try {
116
+ const decoded = JSON.parse(data.choices[0].message.content);
117
+ if (typeof decoded === 'object' && decoded !== null) {
118
+ const errors = [];
119
+ if (!JsonSchemaValidator.validate(decoded, schema, errors)) {
120
+ data.schema_validation = { valid: false, errors };
121
+ } else {
122
+ data.schema_validation = { valid: true };
123
+ }
124
+ }
125
+ } catch { /* not JSON */ }
126
+ }
127
+ return data;
128
+ }
129
+
130
+ /** @private */
131
+ _buildBasePayload(messages, options) {
132
+ const payload = { model: options.model || 'gpt-4o-mini', messages };
133
+ const passThrough = ['temperature', 'top_p', 'max_tokens', 'frequency_penalty', 'presence_penalty', 'stop', 'seed', 'user', 'logprobs', 'top_logprobs'];
134
+ for (const k of passThrough) {
135
+ if (options[k] !== undefined) payload[k] = options[k];
136
+ }
137
+ return payload;
138
+ }
139
+
140
+ /** @private */
141
+ _buildResponsesPayload(messages, options) {
142
+ // Process attachments
143
+ for (let i = 0; i < messages.length; i++) {
144
+ const atts = messages[i].attachments || [];
145
+ if (atts.length > 0) {
146
+ const inline = DocumentAttachmentMapper.extractInlineTexts(atts);
147
+ if (inline.length > 0) {
148
+ messages[i].content = ((messages[i].content || '') + '\n\n' + inline.join('\n\n')).trim();
149
+ }
150
+ delete messages[i].attachments;
151
+ }
152
+ }
153
+
154
+ const model = options.model || 'gpt-4o-mini';
155
+ const instructions = [];
156
+ const parts = [];
157
+ for (const m of messages) {
158
+ const role = m.role || 'user';
159
+ const content = Array.isArray(m.content) ? JSON.stringify(m.content) : (m.content || '');
160
+ if (role === 'system') { instructions.push(content); continue; }
161
+ parts.push(`${role}: ${content}`);
162
+ }
163
+
164
+ const payload = { model, input: parts.join('\n') };
165
+ if (instructions.length > 0) payload.instructions = instructions.join('\n\n');
166
+
167
+ const passThroughKeys = ['temperature', 'top_p', 'max_tokens', 'seed', 'stop', 'user', 'service_tier', 'prompt_cache_key', 'safety_identifier', 'logprobs', 'top_logprobs'];
168
+ for (const k of passThroughKeys) {
169
+ if (options[k] !== undefined) payload[k] = options[k];
170
+ }
171
+
172
+ // tools
173
+ if (options.tools && Array.isArray(options.tools)) {
174
+ payload.tools = options.tools.map(tool => {
175
+ if (tool.type && tool.type !== 'function') return tool;
176
+ return {
177
+ type: 'function',
178
+ function: {
179
+ name: tool.name,
180
+ description: tool.description || '',
181
+ parameters: tool.parameters || tool.schema || { type: 'object', properties: {} },
182
+ },
183
+ };
184
+ });
185
+ if (options.tool_choice) payload.tool_choice = options.tool_choice;
186
+ }
187
+
188
+ // JSON schema response format
189
+ if (options.response_format === 'json') {
190
+ const schema = (options.json_schema || {}).schema || { type: 'object' };
191
+ payload.response_format = {
192
+ type: 'json_schema',
193
+ json_schema: options.json_schema || { name: 'auto_schema', schema },
194
+ };
195
+ }
196
+
197
+ return payload;
198
+ }
199
+
200
+ /** @private */
201
+ _applySchemaIfAny(payload, options) {
202
+ if (!options.response_format || options.response_format !== 'json') return null;
203
+ const schema = (options.json_schema || {}).schema || { type: 'object' };
204
+ payload.response_format = {
205
+ type: 'json_schema',
206
+ json_schema: options.json_schema || { name: 'auto_schema', schema },
207
+ };
208
+ return schema;
209
+ }
210
+
211
+ /** @private */
212
+ _applyNativeToolsIfAny(payload, options) {
213
+ if (!options.tools || !Array.isArray(options.tools)) return;
214
+ payload.tools = options.tools.map(tool => ({
215
+ type: 'function',
216
+ function: {
217
+ name: tool.name,
218
+ description: tool.description || '',
219
+ parameters: tool.parameters || tool.schema || { type: 'object', properties: {} },
220
+ },
221
+ }));
222
+ if (options.tool_choice) payload.tool_choice = options.tool_choice;
223
+ }
224
+
225
+ /** @private */
226
+ _maybeAttachFileSearch(payload, options) {
227
+ const tools = options.tools || [];
228
+ const wantsFileSearch = tools.some(t => t.type === 'file_search');
229
+ if (!wantsFileSearch) return payload;
230
+ if (options.vector_store_id) {
231
+ payload.resources = payload.resources || {};
232
+ payload.resources.file_search = { vector_store_ids: [options.vector_store_id] };
233
+ return payload;
234
+ }
235
+ if (options.file_ids && Array.isArray(options.file_ids)) {
236
+ payload.resources = payload.resources || {};
237
+ payload.resources.file_search = { file_ids: options.file_ids };
238
+ return payload;
239
+ }
240
+ // File upload not supported in Node without multipart — skip for now
241
+ return payload;
242
+ }
243
+
244
+ // ─── Streaming ───
245
+ async *stream(messages, options = {}) {
246
+ if ((options.api || null) !== 'chat') {
247
+ const payload = this._buildResponsesPayload(messages, options);
248
+ payload.stream = true;
249
+ this._maybeAttachFileSearch(payload, options);
250
+ const res = await this._post(this.responsesEndpoint, payload, options, true);
251
+ yield* this._readResponsesSse(res.body);
252
+ return;
253
+ }
254
+ const payload = this._buildBasePayload(messages, options);
255
+ payload.stream = true;
256
+ const res = await this._post(this.chatEndpoint, payload, options, true);
257
+ yield* this._readSseStream(res.body);
258
+ }
259
+
260
+ async *streamEvents(messages, options = {}) {
261
+ if ((options.api || null) !== 'chat') {
262
+ const payload = this._buildResponsesPayload(messages, options);
263
+ payload.stream = true;
264
+ const res = await this._post(this.responsesEndpoint, payload, options, true);
265
+ for await (const chunk of this._readLinesSse(res.body)) {
266
+ if (chunk === '[DONE]') { yield { type: 'end', data: null }; return; }
267
+ try {
268
+ const decoded = JSON.parse(chunk);
269
+ const evtType = decoded.type || null;
270
+ if (evtType === 'response.completed') { yield { type: 'end', data: null }; return; }
271
+ let text = decoded.delta || decoded.output_text || null;
272
+ if (typeof text === 'object') text = null;
273
+ if (text !== null && text !== '') yield { type: 'delta', data: text };
274
+ } catch { /* skip */ }
275
+ }
276
+ return;
277
+ }
278
+
279
+ const payload = this._buildBasePayload(messages, options);
280
+ payload.stream = true;
281
+ const res = await this._post(this.chatEndpoint, payload, options, true);
282
+ for await (const chunk of this._readLinesSse(res.body)) {
283
+ if (chunk === '[DONE]') { yield { type: 'end', data: null }; return; }
284
+ try {
285
+ const decoded = JSON.parse(chunk);
286
+ const delta = decoded?.choices?.[0]?.delta?.content || null;
287
+ if (delta !== null) yield { type: 'delta', data: delta };
288
+ } catch { /* skip */ }
289
+ }
290
+ }
291
+
292
+ /** @private — yields raw text deltas from Chat Completions SSE */
293
+ async *_readSseStream(body) {
294
+ for await (const jsonStr of this._readLinesSse(body)) {
295
+ if (jsonStr === '[DONE]') return;
296
+ try {
297
+ const decoded = JSON.parse(jsonStr);
298
+ const delta = decoded?.choices?.[0]?.delta?.content || null;
299
+ if (delta !== null) yield delta;
300
+ } catch { /* skip */ }
301
+ }
302
+ }
303
+
304
+ /** @private — yields raw text deltas from Responses API SSE */
305
+ async *_readResponsesSse(body) {
306
+ for await (const jsonStr of this._readLinesSse(body)) {
307
+ if (jsonStr === '[DONE]') return;
308
+ try {
309
+ const decoded = JSON.parse(jsonStr);
310
+ const evtType = decoded.type || null;
311
+ if (evtType === 'response.completed') return;
312
+ let text = decoded.delta || decoded.output_text || null;
313
+ if (typeof text === 'object') text = null;
314
+ if (text !== null && text !== '') yield text;
315
+ } catch { /* skip */ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * @private
321
+ * Low-level SSE line reader.
322
+ * Yields the string after "data: " for each SSE line.
323
+ */
324
+ async *_readLinesSse(body) {
325
+ const reader = body.getReader();
326
+ const decoder = new TextDecoder();
327
+ let buffer = '';
328
+ try {
329
+ while (true) {
330
+ const { done, value } = await reader.read();
331
+ if (done) break;
332
+ buffer += decoder.decode(value, { stream: true });
333
+ const lines = buffer.split(/\r?\n/);
334
+ buffer = lines.pop() || '';
335
+ for (const line of lines) {
336
+ const trimmed = line.trim();
337
+ if (trimmed === '' || trimmed.startsWith(':')) continue;
338
+ if (!trimmed.startsWith('data:')) continue;
339
+ const json = trimmed.slice(5).trim();
340
+ yield json;
341
+ }
342
+ }
343
+ // Flush remaining buffer
344
+ if (buffer.trim()) {
345
+ const trimmed = buffer.trim();
346
+ if (trimmed.startsWith('data:')) {
347
+ yield trimmed.slice(5).trim();
348
+ }
349
+ }
350
+ } finally {
351
+ reader.releaseLock();
352
+ }
353
+ }
354
+
355
+ supportsStreaming() { return true; }
356
+
357
+ // ─── Embeddings ───
358
+ async embeddings(inputs, options = {}) {
359
+ const payload = {
360
+ model: options.model || 'text-embedding-3-small',
361
+ input: inputs,
362
+ };
363
+ if (options.dimensions !== undefined) payload.dimensions = options.dimensions;
364
+ if (options.encoding_format !== undefined) payload.encoding_format = options.encoding_format;
365
+ const res = await this._post(this.embeddingsEndpoint, payload, options);
366
+ return {
367
+ embeddings: (res.data || []).map(d => d.embedding || []),
368
+ usage: res.usage || {},
369
+ raw: res,
370
+ };
371
+ }
372
+
373
+ // ─── Images ───
374
+ async generateImage(prompt, options = {}) {
375
+ const mode = options.mode || 'generation';
376
+ if (mode === 'edit' || mode === 'variation') {
377
+ // Edit/variation require multipart — basic JSON fallback
378
+ const payload = {
379
+ prompt,
380
+ model: options.model || 'dall-e-2',
381
+ size: options.size || '1024x1024',
382
+ n: options.n || 1,
383
+ };
384
+ const endpoint = mode === 'edit' ? this.imageEditsEndpoint : this.imageVariationsEndpoint;
385
+ const res = await this._post(endpoint, payload, options);
386
+ return { images: res.data || [], raw: res };
387
+ }
388
+
389
+ // Generation
390
+ const payload = {
391
+ prompt,
392
+ model: options.model || 'dall-e-3',
393
+ size: options.size || '1024x1024',
394
+ n: options.n || 1,
395
+ };
396
+ if (payload.model === 'gpt-image-1') {
397
+ if (options.image_format !== undefined) payload.image_format = options.image_format;
398
+ if (options.quality !== undefined) payload.quality = options.quality;
399
+ if (options.moderation !== undefined) payload.moderation = options.moderation;
400
+ } else {
401
+ payload.response_format = options.response_format || 'url';
402
+ }
403
+ const res = await this._post(this.imageEndpoint, payload, options);
404
+ return { images: res.data || [], raw: res };
405
+ }
406
+
407
+ // ─── Audio TTS ───
408
+ async textToSpeech(text, options = {}) {
409
+ const format = options.format || 'mp3';
410
+ const payload = {
411
+ model: options.model || 'tts-1',
412
+ input: text,
413
+ voice: options.voice || 'alloy',
414
+ format,
415
+ };
416
+ if (options.speed !== undefined) payload.speed = options.speed;
417
+ if (options.voice_instructions) payload.voice_instructions = options.voice_instructions;
418
+
419
+ // For SSE streaming TTS — not typical, fallback to normal
420
+ const res = await fetch(this.textToSpeechEndpoint, {
421
+ method: 'POST',
422
+ headers: this._headers(options),
423
+ body: JSON.stringify(payload),
424
+ });
425
+
426
+ const arrayBuf = await res.arrayBuffer();
427
+ const b64 = Buffer.from(arrayBuf).toString('base64');
428
+ const mimeMap = { mp3: 'audio/mpeg', wav: 'audio/wav', flac: 'audio/flac', aac: 'audio/aac', opus: 'audio/opus', pcm: 'audio/pcm' };
429
+ return { audio: b64, mime: mimeMap[format] || 'application/octet-stream' };
430
+ }
431
+
432
+ // ─── Audio STT ───
433
+ async speechToText(filePath, options = {}) {
434
+ // Multipart upload via FormData (Node 18+)
435
+ const { FormData, Blob } = await import('buffer').catch(() => ({}));
436
+ const formData = new (globalThis.FormData || FormData)();
437
+ const fileBuffer = fs.readFileSync(filePath);
438
+ const blob = new (globalThis.Blob || Blob)([fileBuffer], { type: 'application/octet-stream' });
439
+ formData.append('file', blob, path.basename(filePath));
440
+ formData.append('model', options.model || 'whisper-1');
441
+ formData.append('response_format', options.response_format || 'json');
442
+ if (options.language) formData.append('language', options.language);
443
+ if (options.prompt) formData.append('prompt', options.prompt);
444
+ if (options.temperature !== undefined) formData.append('temperature', String(options.temperature));
445
+
446
+ const endpoint = (options.translate || options.mode === 'translation')
447
+ ? this.speechTranslationsEndpoint
448
+ : this.speechToTextEndpoint;
449
+
450
+ const headers = { 'Authorization': `Bearer ${this.apiKey}` };
451
+ if (options.organization) headers['OpenAI-Organization'] = options.organization;
452
+ if (options.project) headers['OpenAI-Project'] = options.project;
453
+
454
+ const res = await fetch(endpoint, { method: 'POST', headers, body: formData });
455
+ const data = await res.json();
456
+ return { text: data.text || '', raw: data };
457
+ }
458
+
459
+ // ─── Models ───
460
+ async listModels() {
461
+ const res = await this._get(this.modelsEndpoint);
462
+ return res.data || [];
463
+ }
464
+
465
+ async getModel(id) {
466
+ const res = await this._get(`${this.modelsEndpoint}/${encodeURIComponent(id)}`);
467
+ return res || {};
468
+ }
469
+ }
470
+
471
+ module.exports = OpenAIProvider;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_MIME = 'audio/mpeg';
4
+
5
+ /**
6
+ * AudioNormalizer
7
+ * Normalizes TTS and STT responses into unified shapes.
8
+ */
9
+ class AudioNormalizer {
10
+ /**
11
+ * Normalize Text-to-Speech response.
12
+ * @param {Object} raw
13
+ * @returns {{b64: string, mime: string}}
14
+ */
15
+ static normalizeTTS(raw) {
16
+ if (raw?.audio != null) {
17
+ return { b64: raw.audio, mime: raw.mime || DEFAULT_MIME };
18
+ }
19
+ if (raw?.data != null) {
20
+ return { b64: raw.data, mime: raw.mime || DEFAULT_MIME };
21
+ }
22
+ return { b64: '', mime: DEFAULT_MIME };
23
+ }
24
+
25
+ /**
26
+ * Normalize Speech-to-Text response.
27
+ * @param {Object} raw
28
+ * @returns {{text: string}}
29
+ */
30
+ static normalizeSTT(raw) {
31
+ if (raw?.text != null) return { text: String(raw.text) };
32
+ if (raw?.transcript != null) return { text: String(raw.transcript) };
33
+ return { text: '' };
34
+ }
35
+ }
36
+
37
+ module.exports = AudioNormalizer;
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ChatNormalizer
5
+ * Normalizes heterogeneous provider responses into a common shape.
6
+ */
7
+ class ChatNormalizer {
8
+ /**
9
+ * Normalize raw provider response.
10
+ * @param {Object} raw
11
+ * @returns {{text: string, tool_calls: Array, raw: Object}}
12
+ */
13
+ static normalize(raw) {
14
+ let text = '';
15
+ if (raw?.choices?.[0]?.message?.content != null) {
16
+ text = String(raw.choices[0].message.content);
17
+ } else if (raw?.message?.content != null) {
18
+ text = String(raw.message.content);
19
+ } else if (raw?.response != null) {
20
+ text = String(raw.response);
21
+ } else if (raw?.output_text != null) {
22
+ text = String(raw.output_text);
23
+ } else if (raw?.content?.[0]?.text != null) {
24
+ // Claude format
25
+ text = String(raw.content[0].text);
26
+ } else if (raw?.candidates?.[0]?.content?.parts?.[0]?.text != null) {
27
+ // Gemini format
28
+ text = String(raw.candidates[0].content.parts[0].text);
29
+ }
30
+
31
+ let toolCalls = [];
32
+ if (Array.isArray(raw?.choices?.[0]?.message?.tool_calls)) {
33
+ toolCalls = raw.choices[0].message.tool_calls;
34
+ } else if (Array.isArray(raw?.tool_calls)) {
35
+ toolCalls = raw.tool_calls;
36
+ }
37
+
38
+ return { text, tool_calls: toolCalls, raw };
39
+ }
40
+ }
41
+
42
+ module.exports = ChatNormalizer;
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Document
5
+ * Value object representing a document attachment (local file, base64, text, URL, chunks, file_id).
6
+ */
7
+ class Document {
8
+ constructor(kind) {
9
+ /** @type {'local'|'base64'|'raw'|'text'|'url'|'chunks'|'file_id'} */
10
+ this.kind = kind;
11
+ this.path = null;
12
+ this.mime = null;
13
+ this.title = null;
14
+ this.base64 = null;
15
+ this.raw = null;
16
+ this.text = null;
17
+ this.url = null;
18
+ /** @type {string[]} */
19
+ this.chunks = [];
20
+ this.fileId = null;
21
+ }
22
+
23
+ static fromLocalPath(path, title = null, mime = null) {
24
+ const d = new Document('local');
25
+ d.path = path;
26
+ d.title = title;
27
+ d.mime = mime;
28
+ return d;
29
+ }
30
+
31
+ static fromBase64(base64, mime, title = null) {
32
+ const d = new Document('base64');
33
+ d.base64 = base64;
34
+ d.mime = mime;
35
+ d.title = title;
36
+ return d;
37
+ }
38
+
39
+ static fromRawContent(raw, mime, title = null) {
40
+ const d = new Document('raw');
41
+ d.raw = raw;
42
+ d.mime = mime;
43
+ d.title = title;
44
+ return d;
45
+ }
46
+
47
+ static fromText(text, title = null) {
48
+ const d = new Document('text');
49
+ d.text = text;
50
+ d.mime = 'text/plain';
51
+ d.title = title;
52
+ return d;
53
+ }
54
+
55
+ static fromUrl(url, title = null) {
56
+ const d = new Document('url');
57
+ d.url = url;
58
+ d.title = title;
59
+ return d;
60
+ }
61
+
62
+ static fromChunks(chunks, title = null) {
63
+ const d = new Document('chunks');
64
+ d.chunks = chunks.map(String);
65
+ d.title = title;
66
+ return d;
67
+ }
68
+
69
+ static fromFileId(fileId, title = null) {
70
+ const d = new Document('file_id');
71
+ d.fileId = fileId;
72
+ d.title = title;
73
+ return d;
74
+ }
75
+ }
76
+
77
+ module.exports = Document;