backend-manager 5.0.203 → 5.1.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/CHANGELOG.md +72 -0
- package/CLAUDE.md +100 -1529
- package/TODO-CHARGEBLAST.md +32 -0
- package/TODO-email-auth.md +14 -0
- package/docs/admin-post-route.md +24 -0
- package/docs/ai-library.md +23 -0
- package/docs/architecture.md +31 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/cli-firestore-auth.md +59 -0
- package/docs/cli-logs.md +67 -0
- package/docs/code-patterns.md +67 -0
- package/docs/common-mistakes.md +11 -0
- package/docs/common-operations.md +64 -0
- package/docs/directory-structure.md +119 -0
- package/docs/environment-detection.md +7 -0
- package/docs/file-naming.md +11 -0
- package/docs/key-files.md +36 -0
- package/docs/marketing-campaigns.md +244 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +95 -0
- package/docs/payment-system.md +325 -0
- package/docs/response-headers.md +7 -0
- package/docs/routes.md +126 -0
- package/docs/sanitization.md +61 -0
- package/docs/schemas.md +39 -0
- package/docs/stripe-webhook-forwarding.md +18 -0
- package/docs/testing.md +129 -0
- package/docs/usage-rate-limiting.md +67 -0
- package/package.json +8 -4
- package/scripts/update-disposable-domains.js +1 -1
- package/src/defaults/CHANGELOG.md +15 -0
- package/src/defaults/CLAUDE.md +8 -4
- package/src/defaults/docs/README.md +17 -0
- package/src/defaults/test/README.md +33 -0
- package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
- package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +5 -0
- package/src/manager/helpers/utilities.js +21 -0
- package/src/manager/index.js +1 -1
- package/src/manager/libraries/ai/index.js +162 -0
- package/src/manager/libraries/ai/providers/anthropic.js +193 -0
- package/src/manager/libraries/ai/providers/claude-code.js +206 -0
- package/src/manager/libraries/ai/providers/openai.js +934 -0
- package/src/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/src/manager/libraries/email/data/blocked-local-patterns.js +11 -0
- package/src/manager/libraries/email/data/corporate-domains.json +23 -0
- package/src/manager/libraries/{disposable-domains.json → email/data/disposable-domains.json} +3 -0
- package/src/manager/libraries/email/generators/lib/filter.js +179 -0
- package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
- package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
- package/src/manager/libraries/email/generators/lib/structure.js +278 -0
- package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
- package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
- package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
- package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
- package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
- package/src/manager/libraries/email/generators/newsletter.js +377 -95
- package/src/manager/libraries/email/marketing/index.js +16 -2
- package/src/manager/libraries/email/providers/beehiiv.js +7 -3
- package/src/manager/libraries/email/validation.js +53 -38
- package/src/manager/libraries/openai.js +13 -932
- package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/src/manager/routes/admin/post/post.js +10 -17
- package/src/manager/routes/marketing/contact/post.js +5 -1
- package/templates/_.env +4 -0
- package/templates/_.gitignore +1 -0
- package/templates/backend-manager-config.json +48 -4
- package/test/helpers/email-validation.js +141 -3
- package/test/helpers/slugify.js +394 -0
- package/test/marketing/fixtures/clean.json +31 -0
- package/test/marketing/fixtures/editorial.json +31 -0
- package/test/marketing/fixtures/field-report.json +54 -0
- package/test/marketing/newsletter-generate.js +731 -0
- package/test/marketing/newsletter-templates.js +512 -0
- package/test/routes/admin/deduplicate-image-alts.js +190 -0
- /package/src/manager/libraries/{custom-disposable-domains.json → email/data/custom-disposable-domains.json} +0 -0
|
@@ -1,932 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// https://platform.openai.com/docs/pricing
|
|
15
|
-
const MODEL_TABLE = {
|
|
16
|
-
// Mar 9, 2026
|
|
17
|
-
// GPT-5 family
|
|
18
|
-
'gpt-5.4': {
|
|
19
|
-
input: 2.50,
|
|
20
|
-
output: 15.00,
|
|
21
|
-
provider: 'openai',
|
|
22
|
-
features: {
|
|
23
|
-
json: true,
|
|
24
|
-
temperature: false,
|
|
25
|
-
reasoning: true,
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
'gpt-5.2': {
|
|
29
|
-
input: 1.75,
|
|
30
|
-
output: 14.00,
|
|
31
|
-
provider: 'openai',
|
|
32
|
-
features: {
|
|
33
|
-
json: true,
|
|
34
|
-
temperature: false,
|
|
35
|
-
reasoning: true,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
'gpt-5.1': {
|
|
39
|
-
input: 1.25,
|
|
40
|
-
output: 10.00,
|
|
41
|
-
provider: 'openai',
|
|
42
|
-
features: {
|
|
43
|
-
json: true,
|
|
44
|
-
temperature: false,
|
|
45
|
-
reasoning: true,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
'gpt-5': {
|
|
49
|
-
input: 1.25,
|
|
50
|
-
output: 10.00,
|
|
51
|
-
provider: 'openai',
|
|
52
|
-
features: {
|
|
53
|
-
json: true,
|
|
54
|
-
temperature: false,
|
|
55
|
-
reasoning: true,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
'gpt-5-mini': {
|
|
59
|
-
input: 0.25,
|
|
60
|
-
output: 2.00,
|
|
61
|
-
provider: 'openai',
|
|
62
|
-
features: {
|
|
63
|
-
json: true,
|
|
64
|
-
temperature: false,
|
|
65
|
-
reasoning: true,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
'gpt-5-nano': {
|
|
69
|
-
input: 0.05,
|
|
70
|
-
output: 0.40,
|
|
71
|
-
provider: 'openai',
|
|
72
|
-
features: {
|
|
73
|
-
json: true,
|
|
74
|
-
temperature: false,
|
|
75
|
-
reasoning: true,
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
// Mar 20, 2026
|
|
79
|
-
// GPT-5.4 mini/nano family
|
|
80
|
-
'gpt-5.4-mini': {
|
|
81
|
-
input: 0.75,
|
|
82
|
-
output: 4.50,
|
|
83
|
-
provider: 'openai',
|
|
84
|
-
features: {
|
|
85
|
-
json: true,
|
|
86
|
-
temperature: false,
|
|
87
|
-
reasoning: true,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
'gpt-5.4-nano': {
|
|
91
|
-
input: 0.20,
|
|
92
|
-
output: 1.25,
|
|
93
|
-
provider: 'openai',
|
|
94
|
-
features: {
|
|
95
|
-
json: true,
|
|
96
|
-
temperature: false,
|
|
97
|
-
reasoning: true,
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
// GPT-4.5
|
|
101
|
-
'gpt-4.5-preview': {
|
|
102
|
-
input: 75.00,
|
|
103
|
-
output: 150.00,
|
|
104
|
-
provider: 'openai',
|
|
105
|
-
features: {
|
|
106
|
-
json: true,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
// GPT-4.1 family
|
|
110
|
-
'gpt-4.1': {
|
|
111
|
-
input: 2.00,
|
|
112
|
-
output: 8.00,
|
|
113
|
-
provider: 'openai',
|
|
114
|
-
features: {
|
|
115
|
-
json: true,
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
'gpt-4.1-mini': {
|
|
119
|
-
input: 0.40,
|
|
120
|
-
output: 1.60,
|
|
121
|
-
provider: 'openai',
|
|
122
|
-
features: {
|
|
123
|
-
json: true,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
'gpt-4.1-nano': {
|
|
127
|
-
input: 0.10,
|
|
128
|
-
output: 0.40,
|
|
129
|
-
provider: 'openai',
|
|
130
|
-
features: {
|
|
131
|
-
json: true,
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
// GPT-4o family
|
|
135
|
-
'gpt-4o': {
|
|
136
|
-
input: 2.50,
|
|
137
|
-
output: 10.00,
|
|
138
|
-
provider: 'openai',
|
|
139
|
-
features: {
|
|
140
|
-
json: true,
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
'gpt-4o-mini': {
|
|
144
|
-
input: 0.15,
|
|
145
|
-
output: 0.60,
|
|
146
|
-
provider: 'openai',
|
|
147
|
-
features: {
|
|
148
|
-
json: true,
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
// Reasoning models
|
|
152
|
-
'o4-mini': {
|
|
153
|
-
input: 1.10,
|
|
154
|
-
output: 4.40,
|
|
155
|
-
provider: 'openai',
|
|
156
|
-
features: {
|
|
157
|
-
json: true,
|
|
158
|
-
reasoning: true,
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
'o3-pro': {
|
|
162
|
-
input: 20.00,
|
|
163
|
-
output: 80.00,
|
|
164
|
-
provider: 'openai',
|
|
165
|
-
features: {
|
|
166
|
-
json: true,
|
|
167
|
-
reasoning: true,
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
'o3': {
|
|
171
|
-
input: 2.00,
|
|
172
|
-
output: 8.00,
|
|
173
|
-
provider: 'openai',
|
|
174
|
-
features: {
|
|
175
|
-
json: true,
|
|
176
|
-
reasoning: true,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
'o3-mini': {
|
|
180
|
-
input: 1.10,
|
|
181
|
-
output: 4.40,
|
|
182
|
-
provider: 'openai',
|
|
183
|
-
features: {
|
|
184
|
-
json: true,
|
|
185
|
-
reasoning: true,
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
'o1-pro': {
|
|
189
|
-
input: 150.00,
|
|
190
|
-
output: 600.00,
|
|
191
|
-
provider: 'openai',
|
|
192
|
-
features: {
|
|
193
|
-
json: true,
|
|
194
|
-
reasoning: true,
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
'o1': {
|
|
198
|
-
input: 15.00,
|
|
199
|
-
output: 60.00,
|
|
200
|
-
provider: 'openai',
|
|
201
|
-
features: {
|
|
202
|
-
json: true,
|
|
203
|
-
reasoning: true,
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
'o1-preview': {
|
|
207
|
-
input: 15.00,
|
|
208
|
-
output: 60.00,
|
|
209
|
-
provider: 'openai',
|
|
210
|
-
features: {
|
|
211
|
-
json: true,
|
|
212
|
-
reasoning: true,
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
'o1-mini': {
|
|
216
|
-
input: 1.10,
|
|
217
|
-
output: 4.40,
|
|
218
|
-
provider: 'openai',
|
|
219
|
-
features: {
|
|
220
|
-
json: true,
|
|
221
|
-
reasoning: true,
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
'gpt-4-turbo': {
|
|
225
|
-
input: 10.00,
|
|
226
|
-
output: 30.00,
|
|
227
|
-
provider: 'openai',
|
|
228
|
-
features: {
|
|
229
|
-
json: true,
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
'gpt-4': {
|
|
233
|
-
input: 30.00,
|
|
234
|
-
output: 60.00,
|
|
235
|
-
provider: 'openai',
|
|
236
|
-
features: {
|
|
237
|
-
json: true,
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
'gpt-4-vision': {
|
|
241
|
-
input: 30.00,
|
|
242
|
-
output: 60.00,
|
|
243
|
-
provider: 'openai',
|
|
244
|
-
features: {
|
|
245
|
-
json: false,
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
'gpt-3.5-turbo': {
|
|
249
|
-
input: 0.50,
|
|
250
|
-
output: 1.50,
|
|
251
|
-
provider: 'openai',
|
|
252
|
-
features: {
|
|
253
|
-
json: false,
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function OpenAI(assistant, key) {
|
|
259
|
-
const self = this;
|
|
260
|
-
|
|
261
|
-
self.assistant = assistant;
|
|
262
|
-
self.Manager = assistant.Manager;
|
|
263
|
-
self.user = assistant.user;
|
|
264
|
-
self.key = key
|
|
265
|
-
|| self.Manager.config?.openai?.key
|
|
266
|
-
|| self.Manager.config?.openai?.global
|
|
267
|
-
|| self.Manager.config?.openai?.main
|
|
268
|
-
|
|
269
|
-
self.tokens = {
|
|
270
|
-
total: {
|
|
271
|
-
count: 0,
|
|
272
|
-
price: 0,
|
|
273
|
-
},
|
|
274
|
-
input: {
|
|
275
|
-
count: 0,
|
|
276
|
-
price: 0,
|
|
277
|
-
},
|
|
278
|
-
output: {
|
|
279
|
-
count: 0,
|
|
280
|
-
price: 0,
|
|
281
|
-
},
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return self;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
OpenAI.prototype.request = function (options) {
|
|
288
|
-
const self = this;
|
|
289
|
-
const Manager = self.Manager;
|
|
290
|
-
const assistant = self.assistant;
|
|
291
|
-
|
|
292
|
-
return new Promise(async function(resolve, reject) {
|
|
293
|
-
// Deep merge options
|
|
294
|
-
options = _.merge({}, options);
|
|
295
|
-
|
|
296
|
-
// Set defaults
|
|
297
|
-
options.model = typeof options.model === 'undefined' ? DEFAULT_MODEL : options.model;
|
|
298
|
-
options.response = typeof options.response === 'undefined' ? undefined : options.response;
|
|
299
|
-
options.timeout = typeof options.timeout === 'undefined' ? 120000 : options.timeout;
|
|
300
|
-
options.moderate = typeof options.moderate === 'undefined' ? true : options.moderate;
|
|
301
|
-
options.log = typeof options.log === 'undefined' ? false : options.log;
|
|
302
|
-
options.user = options.user || assistant.getUser();
|
|
303
|
-
|
|
304
|
-
// Format retries
|
|
305
|
-
options.retries = typeof options.retries === 'undefined' ? 0 : options.retries;
|
|
306
|
-
options.retryTriggers = typeof options.retryTriggers === 'undefined' ? ['network', 'parse'] : options.retryTriggers;
|
|
307
|
-
|
|
308
|
-
// Format other options
|
|
309
|
-
options.temperature = typeof options.temperature === 'undefined' ? 0.7 : options.temperature;
|
|
310
|
-
options.maxTokens = typeof options.maxTokens === 'undefined' ? 1024 : options.maxTokens;
|
|
311
|
-
|
|
312
|
-
// Custom options
|
|
313
|
-
options.dedupeConsecutiveRoles = typeof options.dedupeConsecutiveRoles === 'undefined' ? true : options.dedupeConsecutiveRoles;
|
|
314
|
-
|
|
315
|
-
// Format schema
|
|
316
|
-
options.schema = options.schema || undefined;
|
|
317
|
-
|
|
318
|
-
// Reasons
|
|
319
|
-
options.reasoning = options.reasoning || undefined;
|
|
320
|
-
|
|
321
|
-
// Format prompt
|
|
322
|
-
options.prompt = options.prompt || {};
|
|
323
|
-
options.prompt.path = options.prompt.path || '';
|
|
324
|
-
options.prompt.content = options.prompt.content || options.prompt.content || '';
|
|
325
|
-
options.prompt.settings = options.prompt.settings || {};
|
|
326
|
-
|
|
327
|
-
// Format message
|
|
328
|
-
options.message = options.message || {};
|
|
329
|
-
options.message.path = options.message.path || '';
|
|
330
|
-
options.message.content = options.message.content || options.message.content || '';
|
|
331
|
-
options.message.settings = options.message.settings || {};
|
|
332
|
-
options.message.attachments = options.message.attachments || [];
|
|
333
|
-
|
|
334
|
-
// Format history
|
|
335
|
-
options.history = options.history || {};
|
|
336
|
-
options.history.messages = options.history.messages || [];
|
|
337
|
-
options.history.limit = typeof options.history.limit === 'undefined' ? 5 : options.history.limit;
|
|
338
|
-
|
|
339
|
-
let attempt = { count: 0 };
|
|
340
|
-
|
|
341
|
-
function _log() {
|
|
342
|
-
if (!options.log) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
assistant.log('callOpenAI():', ...arguments);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// Log
|
|
351
|
-
_log('Starting', options);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Load prompt
|
|
355
|
-
const prompt = loadContent(options.prompt, _log);
|
|
356
|
-
const message = loadContent(options.message, _log);
|
|
357
|
-
const user = options.user?.auth?.uid || assistant.request.geolocation.ip || 'unknown';
|
|
358
|
-
|
|
359
|
-
// Log
|
|
360
|
-
_log('Prompt', prompt);
|
|
361
|
-
_log('Message', message);
|
|
362
|
-
_log('User', user);
|
|
363
|
-
|
|
364
|
-
// Check for errors
|
|
365
|
-
if (prompt instanceof Error) {
|
|
366
|
-
return reject(assistant.errorify(`Error loading prompt: ${prompt}`, {code: 400}));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (message instanceof Error) {
|
|
370
|
-
return reject(assistant.errorify(`Error loading message: ${message}`, {code: 400}));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Moderate if needed
|
|
374
|
-
let moderation = null;
|
|
375
|
-
if (options.moderate) {
|
|
376
|
-
moderation = await makeRequest('moderations', options, self, prompt, message, user, _log)
|
|
377
|
-
.then(async (r) => {
|
|
378
|
-
// {
|
|
379
|
-
// id: 'modr-8205',
|
|
380
|
-
// model: 'omni-moderation-latest',
|
|
381
|
-
// results: [
|
|
382
|
-
// {
|
|
383
|
-
// flagged: false,
|
|
384
|
-
// categories: [Object],
|
|
385
|
-
// category_scores: [Object],
|
|
386
|
-
// category_applied_input_types: [Object]
|
|
387
|
-
// }
|
|
388
|
-
// ]
|
|
389
|
-
// }
|
|
390
|
-
|
|
391
|
-
// Log
|
|
392
|
-
_log('Moderated', r);
|
|
393
|
-
|
|
394
|
-
// Return results
|
|
395
|
-
return r.results[0];
|
|
396
|
-
})
|
|
397
|
-
.catch((e) => e);
|
|
398
|
-
|
|
399
|
-
// Check for moderation flag
|
|
400
|
-
if (moderation?.flagged) {
|
|
401
|
-
return reject(assistant.errorify(`This request is inappropriate`, {code: 451}));
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Make attempt
|
|
407
|
-
attemptRequest(options, self, prompt, message, user, moderation, attempt, assistant, resolve, reject, _log);
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function tryParse(content) {
|
|
412
|
-
try {
|
|
413
|
-
return JSON5.parse(content);
|
|
414
|
-
} catch (e) {
|
|
415
|
-
return content;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function loadContent(input, _log) {
|
|
420
|
-
// console.log('*** input!!!', input.content.slice(0, 50), input.path);
|
|
421
|
-
// console.log('*** input.content', input.content.slice(0, 50));
|
|
422
|
-
// console.log('*** input.path', input.path);
|
|
423
|
-
|
|
424
|
-
let content = '';
|
|
425
|
-
|
|
426
|
-
// Load content
|
|
427
|
-
if (input.path) {
|
|
428
|
-
// Convert to array if not already
|
|
429
|
-
const pathArray = Array.isArray(input.path) ? input.path : [input.path];
|
|
430
|
-
|
|
431
|
-
// Load and concatenate all files
|
|
432
|
-
for (const path of pathArray) {
|
|
433
|
-
const exists = jetpack.exists(path);
|
|
434
|
-
|
|
435
|
-
_log('Reading prompt from path:', path);
|
|
436
|
-
|
|
437
|
-
if (!exists) {
|
|
438
|
-
return new Error(`Path ${path} not found`);
|
|
439
|
-
} else if (exists === 'dir') {
|
|
440
|
-
return new Error(`Path ${path} is a directory`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
try {
|
|
444
|
-
const fileContent = jetpack.read(path);
|
|
445
|
-
content += (content ? '\n' : '') + fileContent;
|
|
446
|
-
} catch (e) {
|
|
447
|
-
return new Error(`Error reading file ${path}: ${e}`);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
} else {
|
|
451
|
-
content = input.content;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return powertools.template(content, input.settings).trim();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function loadAttachment(type, content, _log) {
|
|
458
|
-
if (!content) {
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
_log('Loading attachment:', type, content.substring(0, 100));
|
|
463
|
-
|
|
464
|
-
// Handle remote URLs (https://, http://)
|
|
465
|
-
if (content.startsWith('http://') || content.startsWith('https://')) {
|
|
466
|
-
_log('Remote URL detected:', content);
|
|
467
|
-
return {
|
|
468
|
-
contentType: 'url',
|
|
469
|
-
data: content
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Handle base64 data URLs (data:image/png;base64,...)
|
|
474
|
-
if (content.startsWith('data:')) {
|
|
475
|
-
_log('Base64 data URL detected');
|
|
476
|
-
return {
|
|
477
|
-
contentType: 'base64',
|
|
478
|
-
data: content
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Handle local file paths - need to read and convert to base64
|
|
483
|
-
try {
|
|
484
|
-
const exists = jetpack.exists(content);
|
|
485
|
-
if (!exists) {
|
|
486
|
-
throw new Error(`File not found: ${content}`);
|
|
487
|
-
}
|
|
488
|
-
if (exists === 'dir') {
|
|
489
|
-
throw new Error(`Path is a directory: ${content}`);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
_log('Local file detected, reading:', content);
|
|
493
|
-
|
|
494
|
-
// Read file as buffer
|
|
495
|
-
const fileBuffer = jetpack.read(content, 'buffer');
|
|
496
|
-
if (!fileBuffer) {
|
|
497
|
-
throw new Error(`Failed to read file: ${content}`);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Get MIME type from file extension
|
|
501
|
-
const mimeType = mimeTypes.lookup(content) || 'application/octet-stream';
|
|
502
|
-
_log('Detected MIME type:', mimeType);
|
|
503
|
-
|
|
504
|
-
// Convert to base64 data URL
|
|
505
|
-
const base64Data = fileBuffer.toString('base64');
|
|
506
|
-
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
|
507
|
-
|
|
508
|
-
_log('Converted to base64 data URL, length:', dataUrl.length);
|
|
509
|
-
return {
|
|
510
|
-
contentType: 'base64',
|
|
511
|
-
data: dataUrl
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
} catch (error) {
|
|
515
|
-
_log('Error loading attachment:', error.message);
|
|
516
|
-
throw new Error(`Failed to load attachment: ${error.message}`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function formatMessageContent(content, attachments, _log, mode = 'responses', role = 'user') {
|
|
521
|
-
const formattedContent = [];
|
|
522
|
-
|
|
523
|
-
// Format text content
|
|
524
|
-
if (content) {
|
|
525
|
-
let contentType = 'text';
|
|
526
|
-
|
|
527
|
-
if (mode === 'moderations') {
|
|
528
|
-
contentType = 'text';
|
|
529
|
-
} else if (role === 'assistant') {
|
|
530
|
-
contentType = 'output_text';
|
|
531
|
-
} else {
|
|
532
|
-
contentType = 'input_text';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
formattedContent.push({
|
|
536
|
-
type: contentType,
|
|
537
|
-
text: content,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Format attachments
|
|
542
|
-
if (attachments) {
|
|
543
|
-
attachments.forEach((attachment) => {
|
|
544
|
-
try {
|
|
545
|
-
// Use content field (supports URLs, base64, local paths) or fallback to url field
|
|
546
|
-
const attachmentContent = attachment.content || attachment.url;
|
|
547
|
-
|
|
548
|
-
if (!attachmentContent) {
|
|
549
|
-
_log('Skipping attachment with no content or url:', attachment);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const loadedAttachment = loadAttachment(attachment.type, attachmentContent, _log);
|
|
554
|
-
|
|
555
|
-
// Handle image attachments
|
|
556
|
-
if (attachment.type === 'image' && loadedAttachment) {
|
|
557
|
-
if (mode === 'moderations') {
|
|
558
|
-
formattedContent.push({
|
|
559
|
-
type: 'image_url',
|
|
560
|
-
image_url: {
|
|
561
|
-
url: loadedAttachment.data
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
} else {
|
|
565
|
-
formattedContent.push({
|
|
566
|
-
type: 'input_image',
|
|
567
|
-
image_url: loadedAttachment.data,
|
|
568
|
-
detail: attachment.detail || 'low',
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
// Handle file attachments (only for responses, not moderation)
|
|
573
|
-
else if (attachment.type === 'file' && loadedAttachment && mode !== 'moderations') {
|
|
574
|
-
const fileContent = {
|
|
575
|
-
type: 'input_file',
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
// Use correct field name based on content type
|
|
579
|
-
if (loadedAttachment.contentType === 'url') {
|
|
580
|
-
fileContent.file_url = loadedAttachment.data;
|
|
581
|
-
} else if (loadedAttachment.contentType === 'base64') {
|
|
582
|
-
fileContent.file_data = loadedAttachment.data;
|
|
583
|
-
// Only include filename for base64 data, not for URLs
|
|
584
|
-
fileContent.filename = attachment.filename || path.basename(attachmentContent);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
formattedContent.push(fileContent);
|
|
588
|
-
}
|
|
589
|
-
} catch (error) {
|
|
590
|
-
_log('Error processing attachment:', error.message);
|
|
591
|
-
// Continue processing other attachments
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return formattedContent;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
function formatHistory(options, prompt, message, _log) {
|
|
601
|
-
// Get history with respect to the message limit
|
|
602
|
-
const history = options.history.messages.slice(-options.history.limit);
|
|
603
|
-
|
|
604
|
-
// Add prompt to beginning of history
|
|
605
|
-
history.unshift({
|
|
606
|
-
role: 'developer',
|
|
607
|
-
content: prompt,
|
|
608
|
-
attachments: [],
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Get last history item
|
|
612
|
-
const lastHistory = history[history.length - 1];
|
|
613
|
-
|
|
614
|
-
// Remove last message from history
|
|
615
|
-
if (
|
|
616
|
-
options.dedupeConsecutiveRoles
|
|
617
|
-
&& lastHistory?.role === 'user'
|
|
618
|
-
) {
|
|
619
|
-
history.pop();
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Add message to history
|
|
623
|
-
history.push({
|
|
624
|
-
role: 'user',
|
|
625
|
-
content: message,
|
|
626
|
-
attachments: options.message.attachments,
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
// Format history into new objects (avoid mutating originals which may be persisted by the caller)
|
|
630
|
-
const formatted = history.map((m) => {
|
|
631
|
-
const role = m.role || 'developer';
|
|
632
|
-
const content = typeof m.content === 'string' ? m.content.trim() : String(m.content || '');
|
|
633
|
-
|
|
634
|
-
const result = {
|
|
635
|
-
role: role,
|
|
636
|
-
content: formatMessageContent(content, m.attachments, _log, 'responses', role),
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
// Log
|
|
640
|
-
_log('Message', result.role, result.content);
|
|
641
|
-
|
|
642
|
-
return result;
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
return formatted;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function attemptRequest(options, self, prompt, message, user, moderation, attempt, assistant, resolve, reject, _log) {
|
|
649
|
-
const retries = options.retries;
|
|
650
|
-
const triggers = options.retryTriggers;
|
|
651
|
-
|
|
652
|
-
// Increment attempt
|
|
653
|
-
attempt.count++;
|
|
654
|
-
|
|
655
|
-
// Log
|
|
656
|
-
_log(`Request ${attempt.count}/${retries}`);
|
|
657
|
-
|
|
658
|
-
// Request
|
|
659
|
-
makeRequest('responses', options, self, prompt, message, user, _log)
|
|
660
|
-
.then((r) => {
|
|
661
|
-
// Example
|
|
662
|
-
// {
|
|
663
|
-
// id: 'resp_68734dd2e6148199956fb6ef63a72b13095b79119b6129af',
|
|
664
|
-
// object: 'response',
|
|
665
|
-
// created_at: 1752387027,
|
|
666
|
-
// status: 'completed',
|
|
667
|
-
// background: false,
|
|
668
|
-
// error: null,
|
|
669
|
-
// incomplete_details: null,
|
|
670
|
-
// instructions: null,
|
|
671
|
-
// max_output_tokens: 1024,
|
|
672
|
-
// max_tool_calls: null,
|
|
673
|
-
// model: 'gpt-4o-2024-08-06',
|
|
674
|
-
// output: [
|
|
675
|
-
// {
|
|
676
|
-
// id: 'msg_6872127d078081989822de29fea13a1b07e3a2c4abdba0ba',
|
|
677
|
-
// type: 'message',
|
|
678
|
-
// status: 'completed',
|
|
679
|
-
// content: [
|
|
680
|
-
// {
|
|
681
|
-
// type: 'output_text,
|
|
682
|
-
// annotations: [],
|
|
683
|
-
// logprobs: [],
|
|
684
|
-
// text: 'Hi!'
|
|
685
|
-
// }
|
|
686
|
-
// ],
|
|
687
|
-
// role: 'assistant'
|
|
688
|
-
// }
|
|
689
|
-
// ],
|
|
690
|
-
// parallel_tool_calls: true,
|
|
691
|
-
// previous_response_id: null,
|
|
692
|
-
// reasoning: { effort: null, summary: null },
|
|
693
|
-
// service_tier: 'default',
|
|
694
|
-
// store: true,
|
|
695
|
-
// temperature: 0.7,
|
|
696
|
-
// text: { format: { type: 'text' } },
|
|
697
|
-
// tool_choice: 'auto',
|
|
698
|
-
// tools: [],
|
|
699
|
-
// top_logprobs: 0,
|
|
700
|
-
// top_p: 1,
|
|
701
|
-
// truncation: 'disabled',
|
|
702
|
-
// usage: {
|
|
703
|
-
// input_tokens: 32,
|
|
704
|
-
// input_tokens_details: { cached_tokens: 0 },
|
|
705
|
-
// output_tokens: 3,
|
|
706
|
-
// output_tokens_details: { reasoning_tokens: 0 },
|
|
707
|
-
// total_tokens: 35
|
|
708
|
-
// },
|
|
709
|
-
// user: '127.0.0.1',
|
|
710
|
-
// metadata: {}
|
|
711
|
-
// }
|
|
712
|
-
|
|
713
|
-
// Get output
|
|
714
|
-
const output = r.output;
|
|
715
|
-
|
|
716
|
-
// Ensure content is set
|
|
717
|
-
const content = output.find((o) => o.type === 'message')?.content || [];
|
|
718
|
-
|
|
719
|
-
// Trim and combine all output text
|
|
720
|
-
const outputText = content
|
|
721
|
-
.filter((c) => c.type === 'output_text')
|
|
722
|
-
.map((c) => c.text.trim())
|
|
723
|
-
.join('\n')
|
|
724
|
-
.trim();
|
|
725
|
-
|
|
726
|
-
// Get model configuration
|
|
727
|
-
const modelConfig = getModelConfig(options.model);
|
|
728
|
-
|
|
729
|
-
// Set token counts
|
|
730
|
-
self.tokens.input.count += (r.usage.input_tokens || 0)
|
|
731
|
-
- (r.usage.input_tokens_details.cached_tokens || 0);
|
|
732
|
-
self.tokens.output.count += r.usage.output_tokens || 0;
|
|
733
|
-
self.tokens.total.count = self.tokens.input.count + self.tokens.output.count;
|
|
734
|
-
|
|
735
|
-
// Set token prices
|
|
736
|
-
self.tokens.input.price = (self.tokens.input.count * modelConfig.input) / 1000000;
|
|
737
|
-
self.tokens.output.price = (self.tokens.output.count * modelConfig.output) / 1000000;
|
|
738
|
-
self.tokens.total.price = self.tokens.input.price + self.tokens.output.price;
|
|
739
|
-
|
|
740
|
-
// Log
|
|
741
|
-
_log('Response', outputText.length, typeof outputText, outputText);
|
|
742
|
-
_log('Tokens', self.tokens);
|
|
743
|
-
|
|
744
|
-
// Try to parse JSON response if needed
|
|
745
|
-
try {
|
|
746
|
-
const parsed = options.response === 'json' ? JSON5.parse(outputText) : outputText;
|
|
747
|
-
|
|
748
|
-
// Return
|
|
749
|
-
return resolve({
|
|
750
|
-
output: content,
|
|
751
|
-
content: parsed,
|
|
752
|
-
tokens: self.tokens,
|
|
753
|
-
moderation: moderation,
|
|
754
|
-
})
|
|
755
|
-
} catch (e) {
|
|
756
|
-
assistant.error('Error parsing response', r, e);
|
|
757
|
-
|
|
758
|
-
// Retry
|
|
759
|
-
if (attempt.count < retries && triggers.includes('parse')) {
|
|
760
|
-
return attemptRequest(options, self, prompt, message, user, moderation, attempt, assistant, resolve, reject, _log);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Return
|
|
764
|
-
return reject(e);
|
|
765
|
-
}
|
|
766
|
-
})
|
|
767
|
-
.catch((e) => {
|
|
768
|
-
const parsed = tryParse(e.message)?.error || {};
|
|
769
|
-
const type = parsed?.type || '';
|
|
770
|
-
const message = parsed?.message || e.message;
|
|
771
|
-
|
|
772
|
-
// Log
|
|
773
|
-
assistant.error(`Error requesting (type=${type}, message=${message})`, e);
|
|
774
|
-
|
|
775
|
-
// Check for invalid request error
|
|
776
|
-
if (type === 'invalid_request_error') {
|
|
777
|
-
return reject(assistant.errorify(message, {code: 400}));
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Retry
|
|
781
|
-
if (attempt.count < retries && triggers.includes('network')) {
|
|
782
|
-
return attemptRequest(options, self, prompt, message, user, moderation, attempt, assistant, resolve, reject, _log);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Return
|
|
786
|
-
return reject(e);
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function makeRequest(mode, options, self, prompt, message, user, _log) {
|
|
791
|
-
return new Promise(async function(resolve, reject) {
|
|
792
|
-
const request = {
|
|
793
|
-
url: '',
|
|
794
|
-
method: 'post',
|
|
795
|
-
response: 'json',
|
|
796
|
-
// response: 'raw',
|
|
797
|
-
// log: true,
|
|
798
|
-
attachResponseHeaders: true,
|
|
799
|
-
tries: 1,
|
|
800
|
-
timeout: options.timeout,
|
|
801
|
-
headers: {
|
|
802
|
-
'Authorization': `Bearer ${self.key}`,
|
|
803
|
-
},
|
|
804
|
-
body: {},
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Format depending on mode
|
|
808
|
-
if (mode === 'moderations') {
|
|
809
|
-
// Format moderation input using shared helper
|
|
810
|
-
const input = formatMessageContent(message, options.message.attachments, _log, 'moderations');
|
|
811
|
-
|
|
812
|
-
// Set request
|
|
813
|
-
request.url = 'https://api.openai.com/v1/moderations';
|
|
814
|
-
request.body = {
|
|
815
|
-
model: MODERATION_MODEL,
|
|
816
|
-
input: input,
|
|
817
|
-
user: user,
|
|
818
|
-
}
|
|
819
|
-
} else if (mode === 'responses') {
|
|
820
|
-
// Format history for responses API
|
|
821
|
-
const history = formatHistory(options, prompt, message, _log);
|
|
822
|
-
|
|
823
|
-
// Set request
|
|
824
|
-
request.url = 'https://api.openai.com/v1/responses';
|
|
825
|
-
request.body = {
|
|
826
|
-
model: options.model,
|
|
827
|
-
input: history,
|
|
828
|
-
user: user,
|
|
829
|
-
max_output_tokens: options.maxTokens,
|
|
830
|
-
text: resolveFormatting(options),
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Only include temperature if the model supports it
|
|
834
|
-
const temperature = resolveTemperature(options);
|
|
835
|
-
if (temperature !== undefined) {
|
|
836
|
-
request.body.temperature = temperature;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Only include reasoning if the model supports it
|
|
840
|
-
const reasoning = resolveReasoning(options);
|
|
841
|
-
if (reasoning) {
|
|
842
|
-
request.body.reasoning = reasoning;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Request
|
|
847
|
-
await fetch(request.url, request)
|
|
848
|
-
.then(async (r) => {
|
|
849
|
-
// Log raw response
|
|
850
|
-
_log('Response RAW', JSON.stringify(r, null, 2));
|
|
851
|
-
|
|
852
|
-
// Return
|
|
853
|
-
return resolve(r);
|
|
854
|
-
})
|
|
855
|
-
.catch((e) => {
|
|
856
|
-
return reject(e);
|
|
857
|
-
})
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Helper function to get model configuration with fallback to default model
|
|
862
|
-
function getModelConfig(model) {
|
|
863
|
-
const config = MODEL_TABLE[model];
|
|
864
|
-
|
|
865
|
-
// Return config if found
|
|
866
|
-
if (config) {
|
|
867
|
-
return config;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Fallback to default model if not found
|
|
871
|
-
console.warn(`Model configuration not found for: ${model}, falling back to ${DEFAULT_MODEL}`);
|
|
872
|
-
return MODEL_TABLE[DEFAULT_MODEL];
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function resolveFormatting(options) {
|
|
876
|
-
const modelConfig = getModelConfig(options.model);
|
|
877
|
-
|
|
878
|
-
// Format for JSON
|
|
879
|
-
if (options.response === 'json' && modelConfig.features?.json) {
|
|
880
|
-
|
|
881
|
-
// If schema is set, return JSON schema format
|
|
882
|
-
if (options.schema) {
|
|
883
|
-
return {
|
|
884
|
-
format: {
|
|
885
|
-
type: 'json_schema',
|
|
886
|
-
name: 'response_schema',
|
|
887
|
-
schema: options.schema || {},
|
|
888
|
-
},
|
|
889
|
-
};
|
|
890
|
-
} else {
|
|
891
|
-
return {
|
|
892
|
-
format: {
|
|
893
|
-
type: 'json_object',
|
|
894
|
-
},
|
|
895
|
-
};
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Other, return undefined
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function resolveTemperature(options) {
|
|
904
|
-
// Check if the model supports temperature
|
|
905
|
-
const modelConfig = getModelConfig(options.model);
|
|
906
|
-
if (modelConfig.features?.temperature === false) {
|
|
907
|
-
return undefined;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
return options.temperature;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function resolveReasoning(options) {
|
|
914
|
-
// If reasoning is not requested, return undefined
|
|
915
|
-
if (!options.reasoning) {
|
|
916
|
-
return undefined;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Check if the model supports reasoning
|
|
920
|
-
const modelConfig = getModelConfig(options.model);
|
|
921
|
-
if (!modelConfig.features?.reasoning) {
|
|
922
|
-
console.warn(`Reasoning not supported for model: ${options.model}, ignoring reasoning option`);
|
|
923
|
-
return undefined;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
return {
|
|
927
|
-
effort: options.reasoning.effort || 'medium',
|
|
928
|
-
// summary: options.reasoning.summary || 'concise',
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
module.exports = OpenAI;
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility shim — `libraries/openai.js` was the original AI library.
|
|
3
|
+
* It has been moved to `libraries/ai/providers/openai.js` and the new unified
|
|
4
|
+
* surface is `libraries/ai/index.js`.
|
|
5
|
+
*
|
|
6
|
+
* Existing callers (`require('./libraries/openai.js')`) continue to receive the
|
|
7
|
+
* OpenAI provider class with the same constructor + prototype as before. New
|
|
8
|
+
* code should use the unified surface:
|
|
9
|
+
*
|
|
10
|
+
* const ai = Manager.AI(assistant);
|
|
11
|
+
* await ai.request({ provider: 'openai' | 'anthropic', ... });
|
|
12
|
+
*/
|
|
13
|
+
module.exports = require('./ai/providers/openai.js');
|