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.
Files changed (80) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/CLAUDE.md +100 -1529
  3. package/TODO-CHARGEBLAST.md +32 -0
  4. package/TODO-email-auth.md +14 -0
  5. package/docs/admin-post-route.md +24 -0
  6. package/docs/ai-library.md +23 -0
  7. package/docs/architecture.md +31 -0
  8. package/docs/auth-hooks.md +74 -0
  9. package/docs/cli-firestore-auth.md +59 -0
  10. package/docs/cli-logs.md +67 -0
  11. package/docs/code-patterns.md +67 -0
  12. package/docs/common-mistakes.md +11 -0
  13. package/docs/common-operations.md +64 -0
  14. package/docs/directory-structure.md +119 -0
  15. package/docs/environment-detection.md +7 -0
  16. package/docs/file-naming.md +11 -0
  17. package/docs/key-files.md +36 -0
  18. package/docs/marketing-campaigns.md +244 -0
  19. package/docs/marketing-fields.md +25 -0
  20. package/docs/mcp.md +95 -0
  21. package/docs/payment-system.md +325 -0
  22. package/docs/response-headers.md +7 -0
  23. package/docs/routes.md +126 -0
  24. package/docs/sanitization.md +61 -0
  25. package/docs/schemas.md +39 -0
  26. package/docs/stripe-webhook-forwarding.md +18 -0
  27. package/docs/testing.md +129 -0
  28. package/docs/usage-rate-limiting.md +67 -0
  29. package/package.json +8 -4
  30. package/scripts/update-disposable-domains.js +1 -1
  31. package/src/defaults/CHANGELOG.md +15 -0
  32. package/src/defaults/CLAUDE.md +8 -4
  33. package/src/defaults/docs/README.md +17 -0
  34. package/src/defaults/test/README.md +33 -0
  35. package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
  36. package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
  37. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +5 -0
  38. package/src/manager/helpers/utilities.js +21 -0
  39. package/src/manager/index.js +1 -1
  40. package/src/manager/libraries/ai/index.js +162 -0
  41. package/src/manager/libraries/ai/providers/anthropic.js +193 -0
  42. package/src/manager/libraries/ai/providers/claude-code.js +206 -0
  43. package/src/manager/libraries/ai/providers/openai.js +934 -0
  44. package/src/manager/libraries/email/data/blocked-local-parts.json +55 -0
  45. package/src/manager/libraries/email/data/blocked-local-patterns.js +11 -0
  46. package/src/manager/libraries/email/data/corporate-domains.json +23 -0
  47. package/src/manager/libraries/{disposable-domains.json → email/data/disposable-domains.json} +3 -0
  48. package/src/manager/libraries/email/generators/lib/filter.js +179 -0
  49. package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
  50. package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
  51. package/src/manager/libraries/email/generators/lib/structure.js +278 -0
  52. package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
  53. package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
  54. package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
  55. package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
  56. package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
  57. package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
  58. package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
  59. package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
  60. package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
  61. package/src/manager/libraries/email/generators/newsletter.js +377 -95
  62. package/src/manager/libraries/email/marketing/index.js +16 -2
  63. package/src/manager/libraries/email/providers/beehiiv.js +7 -3
  64. package/src/manager/libraries/email/validation.js +53 -38
  65. package/src/manager/libraries/openai.js +13 -932
  66. package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
  67. package/src/manager/routes/admin/post/post.js +10 -17
  68. package/src/manager/routes/marketing/contact/post.js +5 -1
  69. package/templates/_.env +4 -0
  70. package/templates/_.gitignore +1 -0
  71. package/templates/backend-manager-config.json +48 -4
  72. package/test/helpers/email-validation.js +141 -3
  73. package/test/helpers/slugify.js +394 -0
  74. package/test/marketing/fixtures/clean.json +31 -0
  75. package/test/marketing/fixtures/editorial.json +31 -0
  76. package/test/marketing/fixtures/field-report.json +54 -0
  77. package/test/marketing/newsletter-generate.js +731 -0
  78. package/test/marketing/newsletter-templates.js +512 -0
  79. package/test/routes/admin/deduplicate-image-alts.js +190 -0
  80. /package/src/manager/libraries/{custom-disposable-domains.json → email/data/custom-disposable-domains.json} +0 -0
@@ -1,932 +1,13 @@
1
- const fetch = require('wonderful-fetch');
2
- const jetpack = require('fs-jetpack');
3
- const powertools = require('node-powertools');
4
- const _ = require('lodash');
5
- const JSON5 = require('json5');
6
- const path = require('path');
7
- const mimeTypes = require('mime-types');
8
-
9
- // Constants
10
- const DEFAULT_MODEL = 'gpt-5-mini';
11
- const MODERATION_MODEL = 'omni-moderation-latest';
12
-
13
- // OpenAI model pricing table (per 1M tokens)
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');