backend-manager 3.2.85 → 3.2.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "3.2.85",
3
+ "version": "3.2.87",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "homepage": "https://itwcreativeworks.com",
36
36
  "dependencies": {
37
37
  "@firebase/rules-unit-testing": "^2.0.7",
38
- "@google-cloud/storage": "^7.8.0",
38
+ "@google-cloud/storage": "^7.9.0",
39
39
  "@sendgrid/mail": "^7.7.0",
40
40
  "@sentry/node": "^6.19.7",
41
41
  "busboy": "^1.6.0",
@@ -43,7 +43,7 @@
43
43
  "cors": "^2.8.5",
44
44
  "dotenv": "^16.4.5",
45
45
  "firebase-admin": "^11.11.1",
46
- "firebase-functions": "^4.8.0",
46
+ "firebase-functions": "^4.8.2",
47
47
  "fs-jetpack": "^5.1.0",
48
48
  "hcaptcha": "^0.1.1",
49
49
  "inquirer": "^8.2.5",
@@ -56,11 +56,11 @@
56
56
  "moment": "^2.30.1",
57
57
  "nanoid": "^3.3.7",
58
58
  "node-fetch": "^2.7.0",
59
- "node-powertools": "^1.4.0",
59
+ "node-powertools": "^1.4.1",
60
60
  "npm-api": "^1.0.1",
61
61
  "paypal-server-api": "^2.0.0",
62
62
  "pushid": "^1.0.0",
63
- "resolve-account": "^1.0.14",
63
+ "resolve-account": "^1.0.18",
64
64
  "semver": "^7.6.0",
65
65
  "shortid": "^2.2.16",
66
66
  "sizeitup": "^1.0.7",
@@ -71,4 +71,4 @@
71
71
  "wonderful-log": "^1.0.5",
72
72
  "yargs": "^17.7.2"
73
73
  }
74
- }
74
+ }
@@ -349,7 +349,7 @@ BackendAssistant.prototype.errorify = function (e, options) {
349
349
  // Construct error
350
350
  const newError = e instanceof Error
351
351
  ? e
352
- : new Error(stringify(e));
352
+ : new Error(stringifyNonStrings(e));
353
353
 
354
354
  // Fix code
355
355
  // options.code = newError.code || options.code;
@@ -474,7 +474,7 @@ function isBetween(value, min, max) {
474
474
  return value >= min && value <= max;
475
475
  }
476
476
 
477
- function stringify(e) {
477
+ function stringifyNonStrings(e) {
478
478
  if (typeof e === 'string') {
479
479
  return e;
480
480
  } else {
@@ -942,30 +942,33 @@ BackendAssistant.prototype.parseMultipartFormData = function (options) {
942
942
  });
943
943
  }
944
944
 
945
- function stringify(obj, replacer, spaces, cycleReplacer) {
946
- return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
947
- }
948
-
949
- // https://github.com/moll/json-stringify-safe/blob/master/stringify.js
950
- function serializer(replacer, cycleReplacer) {
951
- var stack = [], keys = []
952
-
953
- if (cycleReplacer == null) cycleReplacer = function(key, value) {
954
- if (stack[0] === value) return '[Circular ~]'
955
- return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
956
- }
957
-
958
- return function(key, value) {
959
- if (stack.length > 0) {
960
- var thisPos = stack.indexOf(this)
961
- ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
962
- ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
963
- if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
964
- }
965
- else stack.push(value)
966
-
967
- return replacer == null ? value : replacer.call(this, key, value)
968
- }
969
- }
945
+ // Not sure what this is for? But it has a good serializer code
946
+ // Disabled 2024-03-21 because there was another stringify() function that i was intending to use but it was actually using this
947
+ // It was adding escaped quotes to strings
948
+ // function stringify(obj, replacer, spaces, cycleReplacer) {
949
+ // return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
950
+ // }
951
+
952
+ // // https://github.com/moll/json-stringify-safe/blob/master/stringify.js
953
+ // function serializer(replacer, cycleReplacer) {
954
+ // var stack = [], keys = []
955
+
956
+ // if (cycleReplacer == null) cycleReplacer = function(key, value) {
957
+ // if (stack[0] === value) return '[Circular ~]'
958
+ // return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
959
+ // }
960
+
961
+ // return function(key, value) {
962
+ // if (stack.length > 0) {
963
+ // var thisPos = stack.indexOf(this)
964
+ // ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
965
+ // ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
966
+ // if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
967
+ // }
968
+ // else stack.push(value)
969
+
970
+ // return replacer == null ? value : replacer.call(this, key, value)
971
+ // }
972
+ // }
970
973
 
971
974
  module.exports = BackendAssistant;
@@ -34,6 +34,7 @@ Middleware.prototype.run = function (libPath, options) {
34
34
  options.setupAnalytics = typeof options.setupAnalytics === 'boolean' ? options.setupAnalytics : true;
35
35
  options.setupUsage = typeof options.setupUsage === 'boolean' ? options.setupUsage : true;
36
36
  options.setupSettings = typeof options.setupSettings === 'undefined' ? true : options.setupSettings;
37
+ options.cleanSettings = typeof options.cleanSettings === 'undefined' ? true : options.cleanSettings;
37
38
  options.schema = typeof options.schema === 'undefined' ? undefined : options.schema;
38
39
 
39
40
  // Log
@@ -80,12 +81,19 @@ Middleware.prototype.run = function (libPath, options) {
80
81
  if (options.setupSettings) {
81
82
  // assistant.log(`Middleware.process(): Resolving settings with schema ${options.schema}...`);
82
83
 
84
+ // Resolve settings
83
85
  try {
84
86
  assistant.settings = Manager.Settings().resolve(assistant, options.schema, data);
85
87
  } catch (e) {
86
88
  return assistant.respond(new Error(`Unable to resolve schema ${options.schema}: ${e.message}`), {code: 500, sentry: true});
87
89
  }
88
90
 
91
+ // Clean settings by looping through and trimming all strings
92
+ if (options.cleanSettings) {
93
+ clean(assistant.settings);
94
+ }
95
+
96
+ // Log
89
97
  assistant.log(`Middleware.process(): Resolved settings with schema ${options.schema}`, JSON.stringify(assistant.settings));
90
98
  }
91
99
 
@@ -106,4 +114,15 @@ Middleware.prototype.run = function (libPath, options) {
106
114
  });
107
115
  };
108
116
 
117
+ function clean(obj) {
118
+ for (let key in obj) {
119
+ if (typeof obj[key] === 'object') {
120
+ clean(obj[key]);
121
+ } else if (typeof obj[key] === 'string') {
122
+ obj[key] = obj[key]
123
+ .trim();
124
+ }
125
+ }
126
+ }
127
+
109
128
  module.exports = Middleware;
@@ -42,22 +42,36 @@ Settings.prototype.resolve = function (assistant, schema, settings) {
42
42
  // Resolve settings
43
43
  self.settings = powertools.defaults(settings, schema);
44
44
 
45
- // Check for missing required keys
46
- powertools.getKeys(schema).forEach((key) => {
47
- const isRequired = key.endsWith('.required') ? _.get(schema, key, false) : false;
45
+ // Iterate each key and check for some things
46
+ processSchema(schema, (path, schemaNode) => {
47
+ const originalValue = _.get(settings, path);
48
+ const resolvedValue = _.get(self.settings, path);
49
+ let replaceValue = undefined;
50
+
51
+ // assistant.log('Found:', path, schemaNode);
52
+ // assistant.log('originalValue:', originalValue);
53
+ // assistant.log('resolvedValue:', resolvedValue);
54
+
55
+ // Check if this node is marked as required
56
+ if (schemaNode.required && typeof originalValue === 'undefined') {
57
+ throw assistant.errorify(`Required key {${path}} is missing in settings`, {code: 400});
58
+ }
48
59
 
49
- // Skip if not required
50
- if (!isRequired) {
51
- return;
60
+ // Clean
61
+ if (schemaNode.clean) {
62
+ if (schemaNode.clean instanceof RegExp) {
63
+ replaceValue = resolvedValue.replace(schemaNode.clean, '');
64
+ } else if (typeof schemaNode.clean === 'function') {
65
+ replaceValue = schemaNode.clean(resolvedValue);
66
+ }
52
67
  }
53
68
 
54
- // Use regex to replace '.required' only if it's at the end of the string
55
- const settingsKey = key.replace(/\.required$/, '');
69
+ // assistant.log('replaceValue:', replaceValue);
56
70
 
57
- // Check if the required key is missing
58
- if (typeof _.get(settings, settingsKey, undefined) === 'undefined') {
59
- // Handle the missing required key as needed
60
- throw assistant.errorify(`Required key '${settingsKey}' is missing in settings`, {code: 400});
71
+ // Replace
72
+ if (typeof replaceValue !== 'undefined' && replaceValue !== resolvedValue) {
73
+ assistant.warn(`Replacing ${path}: originalValue=${originalValue}, resolvedValue=${resolvedValue}, replaceValue=${replaceValue}`);
74
+ _.set(self.settings, path, replaceValue);
61
75
  }
62
76
  });
63
77
 
@@ -92,6 +106,23 @@ Settings.prototype.constant = function (name, options) {
92
106
  }
93
107
  };
94
108
 
109
+ function processSchema(schema, fn, path) {
110
+ path = path || '';
111
+
112
+ // Base case: Check if the current level has 'types' and 'default', indicating metadata
113
+ if (schema.hasOwnProperty('types') && schema.hasOwnProperty('default')) {
114
+ // Call the processing function with the current path and schema as arguments
115
+ fn(path, schema);
116
+ return;
117
+ }
118
+
119
+ // Recursive case: Iterate through nested keys if we're not at a metadata node
120
+ Object.keys(schema).forEach(key => {
121
+ const nextPath = path ? `${path}.${key}` : key;
122
+ processSchema(schema[key], fn, nextPath);
123
+ });
124
+ }
125
+
95
126
  function loadSchema(assistant, schema, settings) {
96
127
  const planId = assistant.request.user.plan.id;
97
128
 
@@ -0,0 +1,326 @@
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
+
7
+ const TOKEN_COST_TABLE = {
8
+ // Mar 6th, 2024
9
+ 'gpt-4-turbo-preview': {
10
+ input: 0.0100,
11
+ output: 0.0300,
12
+ },
13
+ 'gpt-4-1106-preview': {
14
+ input: 0.0100,
15
+ output: 0.0300,
16
+ },
17
+ 'gpt-4': {
18
+ input: 0.0300,
19
+ output: 0.0600,
20
+ },
21
+ 'gpt-3.5-turbo': {
22
+ input: 0.0005,
23
+ output: 0.0015,
24
+ },
25
+
26
+ // Nov 6th, 2023
27
+ // 'gpt-4-turbo-preview': {
28
+ // input: 0.0100,
29
+ // output: 0.0300,
30
+ // },
31
+ // 'gpt-4-1106-preview': {
32
+ // input: 0.0100,
33
+ // output: 0.0300,
34
+ // },
35
+ // 'gpt-4': {
36
+ // input: 0.0300,
37
+ // output: 0.0600,
38
+ // },
39
+ // 'gpt-3.5-turbo': {
40
+ // input: 0.0010,
41
+ // output: 0.0020,
42
+ // },
43
+ }
44
+
45
+ function OpenAI(assistant, key) {
46
+ const self = this;
47
+
48
+ self.assistant = assistant;
49
+ self.Manager = assistant.Manager;
50
+ self.user = assistant.user;
51
+ self.key = key;
52
+
53
+ self.tokens = {
54
+ total: {
55
+ count: 0,
56
+ price: 0,
57
+ },
58
+ input: {
59
+ count: 0,
60
+ price: 0,
61
+ },
62
+ output: {
63
+ count: 0,
64
+ price: 0,
65
+ },
66
+ }
67
+
68
+ return self;
69
+ }
70
+
71
+ OpenAI.prototype.request = function (options) {
72
+ const self = this;
73
+ const Manager = self.Manager;
74
+ const assistant = self.assistant;
75
+
76
+ return new Promise(async function(resolve, reject) {
77
+ options = _.merge({}, options);
78
+
79
+ options.model = typeof options.model === 'undefined' ? 'gpt-3.5-turbo' : options.model;
80
+ options.timeout = typeof options.timeout === 'undefined' ? 120000 : options.timeout;
81
+ options.moderate = typeof options.moderate === 'undefined' ? true : options.moderate;
82
+ options.log = typeof options.log === 'undefined' ? false : options.log;
83
+ options.user = options.user || assistant.getUser();
84
+
85
+ options.retries = typeof options.retries === 'undefined' ? 0 : options.retries;
86
+ options.retryTriggers = typeof options.retryTriggers === 'undefined' ? ['network', 'parse'] : options.retryTriggers;
87
+
88
+ options.prompt = options.prompt || {};
89
+ options.prompt.path = options.prompt.path || '';
90
+ options.prompt.content = options.prompt.content || '';
91
+ options.prompt.settings = options.prompt.settings || {};
92
+
93
+ options.message = options.message || {};
94
+ options.message.path = options.message.path || '';
95
+ options.message.content = options.message.content || '';
96
+ options.message.settings = options.message.settings || {};
97
+
98
+ options.history = options.history || {};
99
+ options.history.messages = options.history.messages || [];
100
+ options.history.limit = typeof options.history.limit === 'undefined' ? 5 : options.history.limit;
101
+
102
+ options.response = typeof options.response === 'undefined' ? undefined : options.response;
103
+ options.temperature = typeof options.temperature === 'undefined' ? 0.7 : options.temperature;
104
+ options.maxTokens = typeof options.maxTokens === 'undefined' ? 512 : options.maxTokens;
105
+
106
+ let attempt = 0;
107
+
108
+ function _log() {
109
+ if (!options.log) {
110
+ return;
111
+ }
112
+
113
+ assistant.log('callOpenAI():', ...arguments);
114
+ }
115
+
116
+ function _load(input) {
117
+ // console.log('*** input!!!', input.content.slice(0, 50), input.path);
118
+ // console.log('*** input.content', input.content.slice(0, 50));
119
+ // console.log('*** input.path', input.path);
120
+ // _log('Loading', input);
121
+
122
+ let content = '';
123
+
124
+ // Load content
125
+ if (input.path) {
126
+ const exists = jetpack.exists(input.path);
127
+ if (!exists) {
128
+ return new Error(`Path ${input.path} not found`);
129
+ } else if (exists === 'dir') {
130
+ return new Error(`Path ${input.path} is a directory`);
131
+ }
132
+
133
+ try {
134
+ content = jetpack.read(input.path);
135
+ } catch (e) {
136
+ return new Error(`Error reading file ${input.path}: ${e}`);
137
+ }
138
+ } else {
139
+ content = input.content;
140
+ }
141
+
142
+ return powertools.template(content, input.settings).trim();
143
+ }
144
+
145
+ // Log
146
+ _log('Starting', self.key, options);
147
+
148
+ // Load prompt
149
+ const prompt = _load(options.prompt);
150
+ const message = _load(options.message);
151
+ const user = options.user?.auth?.uid || assistant.request.geolocation.ip;
152
+ const responseFormat = options.response === 'json' && !options.model.includes('gpt-3.5')
153
+ ? { type: 'json_object' }
154
+ : undefined;
155
+
156
+ // Log
157
+ _log('Prompt', prompt);
158
+ _log('Message', message);
159
+ _log('User', user);
160
+
161
+ // Check for errors
162
+ if (prompt instanceof Error) {
163
+ return reject(assistant.errorify(`Error loading prompt: ${prompt}`, {code: 400}));
164
+ }
165
+
166
+ if (message instanceof Error) {
167
+ return reject(assistant.errorify(`Error loading message: ${message}`, {code: 400}));
168
+ }
169
+
170
+ // Format history
171
+ options.history.messages.forEach((m) => {
172
+ m.role = m.role || 'system';
173
+ m.content = (m.content || '').trim();
174
+ });
175
+
176
+ // Request
177
+ function _request(mode, options) {
178
+ return new Promise(async function(resolve, reject) {
179
+ let resultPath = '';
180
+ const request = {
181
+ url: '',
182
+ method: 'post',
183
+ response: 'json',
184
+ // log: true,
185
+ tries: 1,
186
+ timeout: options.timeout,
187
+ headers: {
188
+ 'Authorization': `Bearer ${Manager.config.openai.key}`,
189
+ },
190
+ body: {},
191
+ }
192
+
193
+ if (mode === 'chatgpt') {
194
+ request.url = 'https://api.openai.com/v1/chat/completions';
195
+ options.history.messages = options.history.messages.slice(-options.history.limit);
196
+ options.history.messages.unshift({
197
+ role: 'system',
198
+ content: prompt,
199
+ });
200
+
201
+ // Set last history item
202
+ const lastHistory = options.history.messages[options.history.messages.length - 1];
203
+
204
+ // If message is different than last message in history, add it
205
+ if (lastHistory?.content !== message) {
206
+ options.history.messages.push({
207
+ role: 'user',
208
+ content: message,
209
+ });
210
+ }
211
+
212
+ // Log message
213
+ _log('Messages', options.history.messages);
214
+
215
+ request.body = {
216
+ model: options.model,
217
+ response_format: responseFormat,
218
+ messages: options.history.messages,
219
+ temperature: options.temperature,
220
+ max_tokens: options.maxTokens,
221
+ user: user,
222
+ }
223
+ resultPath = 'choices[0].message.content';
224
+ } else if (mode === 'moderation') {
225
+ request.url = 'https://api.openai.com/v1/moderations';
226
+ request.body = {
227
+ input: message,
228
+ user: user,
229
+ }
230
+ resultPath = 'results[0]';
231
+ }
232
+
233
+ // Request
234
+ await fetch(request.url, request)
235
+ .then((r) => {
236
+ // Set token counts
237
+ self.tokens.total.count += r?.usage?.total_tokens || 0;
238
+ self.tokens.input.count += r?.usage?.prompt_tokens || 0;
239
+ self.tokens.output.count += r?.usage?.completion_tokens || 0;
240
+
241
+ // Set token prices
242
+ self.tokens.total.price = (self.tokens.total.count / 1000) * TOKEN_COST_TABLE[options.model].input;
243
+ self.tokens.input.price = (self.tokens.input.count / 1000) * TOKEN_COST_TABLE[options.model].input;
244
+ self.tokens.output.price = (self.tokens.output.count / 1000) * TOKEN_COST_TABLE[options.model].output;
245
+
246
+ return resolve(_.get(r, resultPath));
247
+ })
248
+ .catch((e) => {
249
+ return reject(e);
250
+ })
251
+ });
252
+ }
253
+
254
+ // Moderate if needed
255
+ let moderation = null;
256
+ if (options.moderate) {
257
+ moderation = await _request('moderation', options)
258
+ .then(async (r) => {
259
+ _log('Moderated', r);
260
+
261
+ return r;
262
+ })
263
+ .catch((e) => e);
264
+
265
+ // Check for moderation flag
266
+ if (moderation.flagged) {
267
+ return reject(assistant.errorify(`This request is inappropriate`, {code: 451}));
268
+ }
269
+ }
270
+
271
+ function _attempt() {
272
+ const retries = options.retries;
273
+ const triggers = options.retryTriggers;
274
+
275
+ // Increment attempt
276
+ attempt++;
277
+
278
+ // Log
279
+ _log(`Request ${attempt}/${retries}`);
280
+
281
+ // Request
282
+ _request('chatgpt', options)
283
+ .then((r) => {
284
+ _log('Response', r);
285
+
286
+ // Try to parse JSON response if needed
287
+ try {
288
+ const content = options.response === 'json' ? JSON5.parse(r) : r;
289
+
290
+ // Return
291
+ return resolve({
292
+ content: content,
293
+ tokens: self.tokens,
294
+ moderation: moderation,
295
+ })
296
+ } catch (e) {
297
+ assistant.error('Error parsing response', r, e);
298
+
299
+ // Retry
300
+ if (attempt < retries && triggers.includes('parse')) {
301
+ return _attempt();
302
+ }
303
+
304
+ // Return
305
+ return reject(e);
306
+ }
307
+ })
308
+ .catch((e) => {
309
+ assistant.error('Error requesting', e);
310
+
311
+ // Retry
312
+ if (attempt < retries && triggers.includes('network')) {
313
+ return _attempt();
314
+ }
315
+
316
+ // Return
317
+ return reject(e);
318
+ });
319
+ }
320
+
321
+ // Make attempt
322
+ _attempt();
323
+ });
324
+ }
325
+
326
+ module.exports = OpenAI;
@@ -10,6 +10,10 @@ const TOKEN_COST_TABLE = {
10
10
  input: 0.0100,
11
11
  output: 0.0300,
12
12
  },
13
+ 'gpt-4-vision-preview': {
14
+ input: 0.0100,
15
+ output: 0.0300,
16
+ },
13
17
  'gpt-4-1106-preview': {
14
18
  input: 0.0100,
15
19
  output: 0.0300,
@@ -42,6 +46,12 @@ const TOKEN_COST_TABLE = {
42
46
  // },
43
47
  }
44
48
 
49
+ const UNSUPPORTED_JSON = [
50
+ /gpt-3.5/,
51
+ /gpt-4-vision/,
52
+ ];
53
+
54
+
45
55
  function OpenAI(assistant, key) {
46
56
  const self = this;
47
57
 
@@ -87,13 +97,14 @@ OpenAI.prototype.request = function (options) {
87
97
 
88
98
  options.prompt = options.prompt || {};
89
99
  options.prompt.path = options.prompt.path || '';
90
- options.prompt.content = options.prompt.content || '';
100
+ options.prompt.text = options.prompt.text || options.prompt.content || '';
91
101
  options.prompt.settings = options.prompt.settings || {};
92
102
 
93
103
  options.message = options.message || {};
94
104
  options.message.path = options.message.path || '';
95
- options.message.content = options.message.content || '';
105
+ options.message.text = options.message.text || options.message.content || '';
96
106
  options.message.settings = options.message.settings || {};
107
+ options.message.images = options.message.images || [];
97
108
 
98
109
  options.history = options.history || {};
99
110
  options.history.messages = options.history.messages || [];
@@ -119,9 +130,9 @@ OpenAI.prototype.request = function (options) {
119
130
  // console.log('*** input.path', input.path);
120
131
  // _log('Loading', input);
121
132
 
122
- let content = '';
133
+ let text = '';
123
134
 
124
- // Load content
135
+ // Load text
125
136
  if (input.path) {
126
137
  const exists = jetpack.exists(input.path);
127
138
  if (!exists) {
@@ -131,27 +142,33 @@ OpenAI.prototype.request = function (options) {
131
142
  }
132
143
 
133
144
  try {
134
- content = jetpack.read(input.path);
145
+ text = jetpack.read(input.path);
135
146
  } catch (e) {
136
147
  return new Error(`Error reading file ${input.path}: ${e}`);
137
148
  }
138
149
  } else {
139
- content = input.content;
150
+ text = input.text;
140
151
  }
141
152
 
142
- return powertools.template(content, input.settings).trim();
153
+ return powertools.template(text, input.settings).trim();
143
154
  }
144
155
 
145
156
  // Log
146
157
  _log('Starting', self.key, options);
147
158
 
159
+ // Determine response format
160
+ let responseFormat = options.response === 'json' ? { type: 'json_object' } : undefined;
161
+ if (UNSUPPORTED_JSON.some((model) => options.model.match(model))) {
162
+ responseFormat = undefined;
163
+ assistant.warn(`Model ${options.model} does not support JSON response format`);
164
+ }
165
+
166
+ _log('responseFormat', responseFormat);
167
+
148
168
  // Load prompt
149
169
  const prompt = _load(options.prompt);
150
170
  const message = _load(options.message);
151
171
  const user = options.user?.auth?.uid || assistant.request.geolocation.ip;
152
- const responseFormat = options.response === 'json' && !options.model.includes('gpt-3.5')
153
- ? { type: 'json_object' }
154
- : undefined;
155
172
 
156
173
  // Log
157
174
  _log('Prompt', prompt);
@@ -167,12 +184,6 @@ OpenAI.prototype.request = function (options) {
167
184
  return reject(assistant.errorify(`Error loading message: ${message}`, {code: 400}));
168
185
  }
169
186
 
170
- // Format history
171
- options.history.messages.forEach((m) => {
172
- m.role = m.role || 'system';
173
- m.content = (m.content || '').trim();
174
- });
175
-
176
187
  // Request
177
188
  function _request(mode, options) {
178
189
  return new Promise(async function(resolve, reject) {
@@ -192,30 +203,57 @@ OpenAI.prototype.request = function (options) {
192
203
 
193
204
  if (mode === 'chatgpt') {
194
205
  request.url = 'https://api.openai.com/v1/chat/completions';
195
- options.history.messages = options.history.messages.slice(-options.history.limit);
196
- options.history.messages.unshift({
206
+ const history = options.history.messages.slice(-options.history.limit);
207
+ history.unshift({
197
208
  role: 'system',
198
- content: prompt,
209
+ text: prompt,
210
+ images: [],
199
211
  });
200
212
 
201
213
  // Set last history item
202
- const lastHistory = options.history.messages[options.history.messages.length - 1];
214
+ const lastHistory = history[history.length - 1];
203
215
 
204
216
  // If message is different than last message in history, add it
205
- if (lastHistory?.content !== message) {
206
- options.history.messages.push({
217
+ if (lastHistory?.text !== message) {
218
+ history.push({
207
219
  role: 'user',
208
- content: message,
220
+ text: message,
221
+ images: options.message.images,
209
222
  });
210
223
  }
211
224
 
225
+ // Format history
226
+ history.map((m) => {
227
+ m.role = m.role || 'system';
228
+ m.content = [
229
+ { type: 'text', text: m.text },
230
+ ]
231
+
232
+ // Loop through and add
233
+ m.images.forEach((i) => {
234
+ if (i.url) {
235
+ m.content.push({
236
+ type: 'image_url',
237
+ image_url: {
238
+ url: i.url,
239
+ detail: i.detail || 'low',
240
+ }
241
+ });
242
+ }
243
+ }),
244
+
245
+ // Delete text and images
246
+ delete m.text;
247
+ delete m.images;
248
+ })
249
+
212
250
  // Log message
213
- _log('Messages', options.history.messages);
251
+ _log('Messages', history);
214
252
 
215
253
  request.body = {
216
254
  model: options.model,
217
255
  response_format: responseFormat,
218
- messages: options.history.messages,
256
+ messages: history,
219
257
  temperature: options.temperature,
220
258
  max_tokens: options.maxTokens,
221
259
  user: user,
@@ -306,7 +344,17 @@ OpenAI.prototype.request = function (options) {
306
344
  }
307
345
  })
308
346
  .catch((e) => {
309
- assistant.error('Error requesting', e);
347
+ const parsed = tryParse(e.message)?.error || {};
348
+ const type = parsed?.type || '';
349
+ const message = parsed?.message || e.message;
350
+
351
+ // Log
352
+ assistant.error(`Error requesting (type=${type}, message=${message})`, e);
353
+
354
+ // Check for invalid request error
355
+ if (type === 'invalid_request_error') {
356
+ return reject(assistant.errorify(message, {code: 400}));
357
+ }
310
358
 
311
359
  // Retry
312
360
  if (attempt < retries && triggers.includes('network')) {
@@ -323,4 +371,12 @@ OpenAI.prototype.request = function (options) {
323
371
  });
324
372
  }
325
373
 
374
+ function tryParse(content) {
375
+ try {
376
+ return JSON5.parse(content);
377
+ } catch (e) {
378
+ return content;
379
+ }
380
+ }
381
+
326
382
  module.exports = OpenAI;