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.
- package/README.md +130 -2
- package/docs/skills/outlet-orm/AI.md +452 -102
- package/docs/skills/outlet-orm/API.md +108 -0
- package/docs/skills/outlet-orm/QUERIES.md +64 -0
- package/docs/skills/outlet-orm/SEEDS.md +47 -0
- package/docs/skills/outlet-orm/SKILL.md +15 -7
- package/package.json +1 -1
- package/src/AI/AIPromptEnhancer.js +170 -0
- package/src/AI/AIQueryBuilder.js +234 -0
- package/src/AI/AIQueryOptimizer.js +185 -0
- package/src/AI/AISeeder.js +181 -0
- package/src/AI/AiBridgeManager.js +287 -0
- package/src/AI/Builders/TextBuilder.js +170 -0
- package/src/AI/Contracts/AudioProviderContract.js +29 -0
- package/src/AI/Contracts/ChatProviderContract.js +38 -0
- package/src/AI/Contracts/EmbeddingsProviderContract.js +19 -0
- package/src/AI/Contracts/ImageProviderContract.js +19 -0
- package/src/AI/Contracts/ModelsProviderContract.js +26 -0
- package/src/AI/Contracts/ToolContract.js +25 -0
- package/src/AI/Facades/AiBridge.js +79 -0
- package/src/AI/MCPServer.js +113 -0
- package/src/AI/Providers/ClaudeProvider.js +64 -0
- package/src/AI/Providers/CustomOpenAIProvider.js +238 -0
- package/src/AI/Providers/GeminiProvider.js +68 -0
- package/src/AI/Providers/GrokProvider.js +46 -0
- package/src/AI/Providers/MistralProvider.js +21 -0
- package/src/AI/Providers/OllamaProvider.js +249 -0
- package/src/AI/Providers/OllamaTurboProvider.js +32 -0
- package/src/AI/Providers/OnnProvider.js +46 -0
- package/src/AI/Providers/OpenAIProvider.js +471 -0
- package/src/AI/Support/AudioNormalizer.js +37 -0
- package/src/AI/Support/ChatNormalizer.js +42 -0
- package/src/AI/Support/Document.js +77 -0
- package/src/AI/Support/DocumentAttachmentMapper.js +101 -0
- package/src/AI/Support/EmbeddingsNormalizer.js +30 -0
- package/src/AI/Support/Exceptions/ProviderError.js +22 -0
- package/src/AI/Support/FileSecurity.js +56 -0
- package/src/AI/Support/ImageNormalizer.js +62 -0
- package/src/AI/Support/JsonSchemaValidator.js +73 -0
- package/src/AI/Support/Message.js +40 -0
- package/src/AI/Support/StreamChunk.js +45 -0
- package/src/AI/Support/ToolChatRunner.js +160 -0
- package/src/AI/Support/ToolRegistry.js +62 -0
- package/src/AI/Tools/SystemInfoTool.js +25 -0
- package/src/index.js +67 -1
- 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;
|