backend-manager 3.2.86 → 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.86",
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",
@@ -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;