backend-manager 5.0.202 → 5.1.0

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