@aj-archipelago/cortex 1.1.19 → 1.1.21

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.
@@ -1,172 +1,306 @@
1
- import OpenAIVisionPlugin from './openAiVisionPlugin.js';
2
- import logger from '../../lib/logger.js';
1
+ import OpenAIVisionPlugin from "./openAiVisionPlugin.js";
2
+ import logger from "../../lib/logger.js";
3
+ import mime from 'mime-types';
3
4
 
4
- class Claude3VertexPlugin extends OpenAIVisionPlugin {
5
+ async function convertContentItem(item) {
5
6
 
6
- parseResponse(data)
7
- {
8
- if (!data) {
9
- return data;
10
- }
7
+ let imageUrl = "";
8
+ let isDataURL = false;
9
+ let urlData = "";
10
+ let mimeTypeMatch = "";
11
+ let mimeType = "";
12
+ let base64Image = "";
13
+ const allowedMIMETypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
14
+
15
+ try {
16
+ switch (typeof item) {
17
+ case "string":
18
+ return item ? { type: "text", text: item } : null;
19
+
20
+ case "object":
21
+ switch (item.type) {
22
+ case "text":
23
+ return item.text ? { type: "text", text: item.text } : null;
24
+
25
+ case "image_url":
26
+ imageUrl = item.image_url.url || item.image_url;
27
+ if (!imageUrl) {
28
+ logger.warn("Could not parse image URL from content - skipping image content.");
29
+ return null;
30
+ }
11
31
 
12
- const { content } = data;
32
+ if (!allowedMIMETypes.includes(mime.lookup(imageUrl) || "")) {
33
+ logger.warn("Unsupported image type - skipping image content.");
34
+ return null;
35
+ }
13
36
 
14
- // if the response is an array, return the text property of the first item
15
- // if the type property is 'text'
16
- if (content && Array.isArray(content) && content[0].type === 'text') {
17
- return content[0].text;
18
- } else {
19
- return data;
37
+ isDataURL = imageUrl.startsWith("data:");
38
+ urlData = isDataURL ? item.image_url.url : await fetchImageAsDataURL(imageUrl);
39
+ mimeTypeMatch = urlData.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/);
40
+ mimeType = mimeTypeMatch && mimeTypeMatch[1] ? mimeTypeMatch[1] : "image/jpeg";
41
+ base64Image = urlData.split(",")[1];
42
+
43
+ return {
44
+ type: "image",
45
+ source: {
46
+ type: "base64",
47
+ media_type: mimeType,
48
+ data: base64Image,
49
+ },
50
+ };
51
+
52
+ default:
53
+ return null;
20
54
  }
55
+
56
+ default:
57
+ return null;
21
58
  }
59
+ }
60
+ catch (e) {
61
+ logger.warn(`Error converting content item: ${e}`);
62
+ return null;
63
+ }
64
+ }
22
65
 
23
- // This code converts messages to the format required by the Claude Vertex API
24
- convertMessagesToClaudeVertex(messages) {
25
- let modifiedMessages = [];
26
- let system = '';
27
- let lastAuthor = '';
28
-
29
- // Claude needs system messages in a separate field
30
- const systemMessages = messages.filter(message => message.role === 'system');
31
- if (systemMessages.length > 0) {
32
- system = systemMessages.map(message => message.content).join('\n');
33
- modifiedMessages = messages.filter(message => message.role !== 'system');
34
- } else {
35
- modifiedMessages = messages;
36
- }
66
+ // Fetch image and convert to base 64 data URL
67
+ async function fetchImageAsDataURL(imageUrl) {
68
+ try {
69
+ const response = await fetch(imageUrl);
37
70
 
38
- // remove any empty messages
39
- modifiedMessages = modifiedMessages.filter(message => message.content);
71
+ if (!response.ok) {
72
+ throw new Error(`HTTP error! status: ${response.status}`);
73
+ }
40
74
 
41
- // combine any consecutive messages from the same author
42
- var combinedMessages = [];
75
+ const buffer = await response.arrayBuffer();
76
+ const base64Image = Buffer.from(buffer).toString("base64");
77
+ const mimeType = mime.lookup(imageUrl) || "image/jpeg";
78
+ return `data:${mimeType};base64,${base64Image}`;
79
+ }
80
+ catch (e) {
81
+ logger.error(`Failed to fetch image: ${imageUrl}. ${e}`);
82
+ throw e;
83
+ }
84
+ }
43
85
 
44
- modifiedMessages.forEach((message) => {
45
- if (message.role === lastAuthor) {
46
- combinedMessages[combinedMessages.length - 1].content += '\n' + message.content;
47
- } else {
48
- combinedMessages.push(message);
49
- lastAuthor = message.role;
50
- }
51
- });
86
+ class Claude3VertexPlugin extends OpenAIVisionPlugin {
87
+
88
+ parseResponse(data) {
89
+ if (!data) {
90
+ return data;
91
+ }
52
92
 
53
- modifiedMessages = combinedMessages;
93
+ const { content } = data;
54
94
 
55
- // Claude vertex requires an even number of messages
56
- if (modifiedMessages.length % 2 === 0) {
57
- modifiedMessages = modifiedMessages.slice(1);
58
- }
95
+ // if the response is an array, return the text property of the first item
96
+ // if the type property is 'text'
97
+ if (content && Array.isArray(content) && content[0].type === "text") {
98
+ return content[0].text;
99
+ } else {
100
+ return data;
101
+ }
102
+ }
103
+
104
+ // This code converts messages to the format required by the Claude Vertex API
105
+ async convertMessagesToClaudeVertex(messages) {
106
+ let modifiedMessages = [];
107
+ let system = "";
108
+ let lastAuthor = "";
59
109
 
110
+ // Claude needs system messages in a separate field
111
+ const systemMessages = messages.filter(
112
+ (message) => message.role === "system"
113
+ );
114
+ if (systemMessages.length > 0) {
115
+ system = systemMessages.map((message) => message.content).join("\n");
116
+ modifiedMessages = messages.filter(
117
+ (message) => message.role !== "system"
118
+ );
119
+ } else {
120
+ modifiedMessages = messages;
121
+ }
122
+
123
+ // remove any empty messages
124
+ modifiedMessages = modifiedMessages.filter((message) => message.content);
125
+
126
+ // combine any consecutive messages from the same author
127
+ var combinedMessages = [];
128
+
129
+ modifiedMessages.forEach((message) => {
130
+ if (message.role === lastAuthor) {
131
+ combinedMessages[combinedMessages.length - 1].content +=
132
+ "\n" + message.content;
133
+ } else {
134
+ combinedMessages.push(message);
135
+ lastAuthor = message.role;
136
+ }
137
+ });
138
+
139
+ modifiedMessages = combinedMessages;
140
+
141
+ // Claude vertex requires an odd number of messages
142
+ // for proper conversation turn-taking
143
+ if (modifiedMessages.length % 2 === 0) {
144
+ modifiedMessages = modifiedMessages.slice(1);
145
+ }
146
+
147
+ const claude3Messages = await Promise.all(
148
+ modifiedMessages.map(async (message) => {
149
+ const contentArray = Array.isArray(message.content) ? message.content : [message.content];
150
+ const claude3Content = await Promise.all(contentArray.map(convertContentItem));
60
151
  return {
61
- system,
62
- modifiedMessages,
152
+ role: message.role,
153
+ content: claude3Content.filter(Boolean),
63
154
  };
64
- }
155
+ })
156
+ );
65
157
 
66
- getRequestParameters(text, parameters, prompt, cortexRequest) {
67
- const requestParameters = super.getRequestParameters(text, parameters, prompt, cortexRequest);
68
- const { system, modifiedMessages } = this.convertMessagesToClaudeVertex(requestParameters.messages);
69
- requestParameters.system = system;
70
- requestParameters.messages = modifiedMessages;
71
- requestParameters.max_tokens = this.getModelMaxReturnTokens();
72
- requestParameters.anthropic_version = 'vertex-2023-10-16';
73
- return requestParameters;
158
+ return {
159
+ system,
160
+ modifiedMessages: claude3Messages,
161
+ };
162
+ }
163
+
164
+ async getRequestParameters(text, parameters, prompt, cortexRequest) {
165
+ const requestParameters = await super.getRequestParameters(
166
+ text,
167
+ parameters,
168
+ prompt,
169
+ cortexRequest
170
+ );
171
+ const { system, modifiedMessages } =
172
+ await this.convertMessagesToClaudeVertex(requestParameters.messages);
173
+ requestParameters.system = system;
174
+ requestParameters.messages = modifiedMessages;
175
+ requestParameters.max_tokens = this.getModelMaxReturnTokens();
176
+ requestParameters.anthropic_version = "vertex-2023-10-16";
177
+ return requestParameters;
178
+ }
179
+
180
+ // Override the logging function to display the messages and responses
181
+ logRequestData(data, responseData, prompt) {
182
+ const { stream, messages, system } = data;
183
+ if (system) {
184
+ const { length, units } = this.getLength(system);
185
+ logger.info(`[system messages sent containing ${length} ${units}]`);
186
+ logger.debug(`${system}`);
74
187
  }
75
188
 
76
- // Override the logging function to display the messages and responses
77
- logRequestData(data, responseData, prompt) {
78
- const { stream, messages, system } = data;
79
- if (system) {
80
- const { length, units } = this.getLength(system);
81
- logger.info(`[system messages sent containing ${length} ${units}]`);
82
- logger.debug(`${system}`);
83
- }
84
-
85
- if (messages && messages.length > 1) {
86
- logger.info(`[chat request sent containing ${messages.length} messages]`);
87
- let totalLength = 0;
88
- let totalUnits;
89
- messages.forEach((message, index) => {
90
- //message.content string or array
91
- const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content;
92
- const words = content.split(" ");
93
- const { length, units } = this.getLength(content);
94
- const preview = words.length < 41 ? content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
95
-
96
- logger.debug(`message ${index + 1}: role: ${message.role}, ${units}: ${length}, content: "${preview}"`);
97
- totalLength += length;
98
- totalUnits = units;
99
- });
100
- logger.info(`[chat request contained ${totalLength} ${totalUnits}]`);
101
- } else {
102
- const message = messages[0];
103
- const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content;
104
- const { length, units } = this.getLength(content);
105
- logger.info(`[request sent containing ${length} ${units}]`);
106
- logger.debug(`${content}`);
107
- }
108
-
109
- if (stream) {
110
- logger.info(`[response received as an SSE stream]`);
111
- } else {
112
- const responseText = this.parseResponse(responseData);
113
- const { length, units } = this.getLength(responseText);
114
- logger.info(`[response received containing ${length} ${units}]`);
115
- logger.debug(`${responseText}`);
116
- }
189
+ if (messages && messages.length > 1) {
190
+ logger.info(`[chat request sent containing ${messages.length} messages]`);
191
+ let totalLength = 0;
192
+ let totalUnits;
193
+ messages.forEach((message, index) => {
194
+ //message.content string or array
195
+ const content = Array.isArray(message.content)
196
+ ? message.content.map((item) => {
197
+ if (item.source && item.source.type === 'base64') {
198
+ item.source.data = '* base64 data truncated for log *';
199
+ }
200
+ return JSON.stringify(item);
201
+ }).join(", ")
202
+ : message.content;
203
+ const words = content.split(" ");
204
+ const { length, units } = this.getLength(content);
205
+ const preview =
206
+ words.length < 41
207
+ ? content
208
+ : words.slice(0, 20).join(" ") +
209
+ " ... " +
210
+ words.slice(-20).join(" ");
117
211
 
118
- prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
212
+ logger.debug(
213
+ `message ${index + 1}: role: ${
214
+ message.role
215
+ }, ${units}: ${length}, content: "${preview}"`
216
+ );
217
+ totalLength += length;
218
+ totalUnits = units;
219
+ });
220
+ logger.info(`[chat request contained ${totalLength} ${totalUnits}]`);
221
+ } else {
222
+ const message = messages[0];
223
+ const content = Array.isArray(message.content)
224
+ ? message.content.map((item) => JSON.stringify(item)).join(", ")
225
+ : message.content;
226
+ const { length, units } = this.getLength(content);
227
+ logger.info(`[request sent containing ${length} ${units}]`);
228
+ logger.debug(`${content}`);
119
229
  }
120
230
 
121
- async execute(text, parameters, prompt, cortexRequest) {
122
- const requestParameters = this.getRequestParameters(text, parameters, prompt, cortexRequest);
123
- const { stream } = parameters;
231
+ if (stream) {
232
+ logger.info(`[response received as an SSE stream]`);
233
+ } else {
234
+ const responseText = this.parseResponse(responseData);
235
+ const { length, units } = this.getLength(responseText);
236
+ logger.info(`[response received containing ${length} ${units}]`);
237
+ logger.debug(`${responseText}`);
238
+ }
124
239
 
125
- cortexRequest.data = { ...(cortexRequest.data || {}), ...requestParameters };
126
- cortexRequest.params = {}; // query params
127
- cortexRequest.stream = stream;
128
- cortexRequest.urlSuffix = cortexRequest.stream ? ':streamRawPredict' : ':rawPredict';
240
+ prompt &&
241
+ prompt.debugInfo &&
242
+ (prompt.debugInfo += `\n${JSON.stringify(data)}`);
243
+ }
129
244
 
130
- const gcpAuthTokenHelper = this.config.get('gcpAuthTokenHelper');
131
- const authToken = await gcpAuthTokenHelper.getAccessToken();
132
- cortexRequest.headers.Authorization = `Bearer ${authToken}`;
245
+ async execute(text, parameters, prompt, cortexRequest) {
246
+ const requestParameters = await this.getRequestParameters(
247
+ text,
248
+ parameters,
249
+ prompt,
250
+ cortexRequest
251
+ );
252
+ const { stream } = parameters;
133
253
 
134
- return this.executeRequest(cortexRequest);
135
- }
254
+ cortexRequest.data = {
255
+ ...(cortexRequest.data || {}),
256
+ ...requestParameters,
257
+ };
258
+ cortexRequest.params = {}; // query params
259
+ cortexRequest.stream = stream;
260
+ cortexRequest.urlSuffix = cortexRequest.stream
261
+ ? ":streamRawPredict"
262
+ : ":rawPredict";
136
263
 
137
- processStreamEvent(event, requestProgress) {
138
- const eventData = JSON.parse(event.data);
139
- switch (eventData.type) {
140
- case 'message_start':
141
- requestProgress.data = JSON.stringify(eventData.message);
142
- break;
143
- case 'content_block_start':
144
- break;
145
- case 'ping':
146
- break;
147
- case 'content_block_delta':
148
- if (eventData.delta.type === 'text_delta') {
149
- requestProgress.data = JSON.stringify(eventData.delta.text);
150
- }
151
- break;
152
- case 'content_block_stop':
153
- break;
154
- case 'message_delta':
155
- break;
156
- case 'message_stop':
157
- requestProgress.data = '[DONE]';
158
- requestProgress.progress = 1;
159
- break;
160
- case 'error':
161
- requestProgress.data = `\n\n*** ${eventData.error.message || eventData.error} ***`;
162
- requestProgress.progress = 1;
163
- break;
164
- }
264
+ const gcpAuthTokenHelper = this.config.get("gcpAuthTokenHelper");
265
+ const authToken = await gcpAuthTokenHelper.getAccessToken();
266
+ cortexRequest.headers.Authorization = `Bearer ${authToken}`;
165
267
 
166
- return requestProgress;
268
+ return this.executeRequest(cortexRequest);
269
+ }
167
270
 
271
+ processStreamEvent(event, requestProgress) {
272
+ const eventData = JSON.parse(event.data);
273
+ switch (eventData.type) {
274
+ case "message_start":
275
+ requestProgress.data = JSON.stringify(eventData.message);
276
+ break;
277
+ case "content_block_start":
278
+ break;
279
+ case "ping":
280
+ break;
281
+ case "content_block_delta":
282
+ if (eventData.delta.type === "text_delta") {
283
+ requestProgress.data = JSON.stringify(eventData.delta.text);
284
+ }
285
+ break;
286
+ case "content_block_stop":
287
+ break;
288
+ case "message_delta":
289
+ break;
290
+ case "message_stop":
291
+ requestProgress.data = "[DONE]";
292
+ requestProgress.progress = 1;
293
+ break;
294
+ case "error":
295
+ requestProgress.data = `\n\n*** ${
296
+ eventData.error.message || eventData.error
297
+ } ***`;
298
+ requestProgress.progress = 1;
299
+ break;
168
300
  }
169
301
 
302
+ return requestProgress;
303
+ }
170
304
  }
171
305
 
172
306
  export default Claude3VertexPlugin;
@@ -76,7 +76,7 @@ class Gemini15ChatPlugin extends ModelPlugin {
76
76
  });
77
77
  }
78
78
 
79
- // Gemini requires an even number of messages
79
+ // Gemini requires an odd number of messages
80
80
  if (modifiedMessages.length % 2 === 0) {
81
81
  modifiedMessages = modifiedMessages.slice(1);
82
82
  }
@@ -135,7 +135,13 @@ class Gemini15ChatPlugin extends ModelPlugin {
135
135
  if (data && data.contents && Array.isArray(data.contents)) {
136
136
  dataToMerge = data.contents;
137
137
  } else if (data && data.candidates && Array.isArray(data.candidates)) {
138
- return data.candidates[0].content.parts[0].text;
138
+ const { content, finishReason, safetyRatings } = data.candidates[0];
139
+ if (finishReason === 'STOP') {
140
+ return content?.parts?.[0]?.text ?? '';
141
+ } else {
142
+ const returnString = `Response was not completed. Finish reason: ${finishReason}, Safety ratings: ${JSON.stringify(safetyRatings, null, 2)}`;
143
+ throw new Error(returnString);
144
+ }
139
145
  } else if (Array.isArray(data)) {
140
146
  dataToMerge = data;
141
147
  } else {
@@ -154,7 +160,6 @@ class Gemini15ChatPlugin extends ModelPlugin {
154
160
  cortexRequest.data = { ...(cortexRequest.data || {}), ...requestParameters };
155
161
  cortexRequest.params = {}; // query params
156
162
  cortexRequest.stream = stream;
157
- cortexRequest.stream = stream;
158
163
  cortexRequest.urlSuffix = cortexRequest.stream ? ':streamGenerateContent?alt=sse' : ':generateContent';
159
164
 
160
165
  const gcpAuthTokenHelper = this.config.get('gcpAuthTokenHelper');
@@ -1,6 +1,5 @@
1
1
  import Gemini15ChatPlugin from './gemini15ChatPlugin.js';
2
2
  import mime from 'mime-types';
3
- import logger from '../../lib/logger.js';
4
3
 
5
4
  class Gemini15VisionPlugin extends Gemini15ChatPlugin {
6
5
 
@@ -25,9 +24,10 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
25
24
  }
26
25
 
27
26
  // Convert content to Gemini format, trying to maintain compatibility
28
- const convertPartToGemini = (partString) => {
27
+ const convertPartToGemini = (inputPart) => {
29
28
  try {
30
- const part = JSON.parse(partString);
29
+ const part = typeof inputPart === 'string' ? JSON.parse(inputPart) : inputPart;
30
+
31
31
  if (typeof part === 'string') {
32
32
  return { text: part };
33
33
  } else if (part.type === 'text') {
@@ -50,9 +50,10 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
50
50
  }
51
51
  }
52
52
  } catch (e) {
53
- logger.warn(`Unable to parse part - including as string: ${partString}`);
53
+ // this space intentionally left blank
54
54
  }
55
- return { text: partString };
55
+
56
+ return { text: inputPart };
56
57
  };
57
58
 
58
59
  const addPartToMessages = (geminiPart) => {
@@ -82,7 +83,7 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
82
83
  });
83
84
  }
84
85
 
85
- // Gemini requires an even number of messages
86
+ // Gemini requires an odd number of messages
86
87
  if (modifiedMessages.length % 2 === 0) {
87
88
  modifiedMessages = modifiedMessages.slice(1);
88
89
  }
@@ -95,6 +96,22 @@ class Gemini15VisionPlugin extends Gemini15ChatPlugin {
95
96
  };
96
97
  }
97
98
 
99
+ async execute(text, parameters, prompt, cortexRequest) {
100
+ let result = null;
101
+ try {
102
+ result = await super.execute(text, parameters, prompt, cortexRequest);
103
+ } catch (e) {
104
+ const { data } = e;
105
+ if (data && data.error) {
106
+ if (data.error.code === 400 && data.error.message === 'Precondition check failed.') {
107
+ throw new Error('One or more of the included files is too large to process. Please try again with a smaller file.');
108
+ }
109
+ throw e;
110
+ }
111
+ }
112
+ return result;
113
+ }
114
+
98
115
  }
99
116
 
100
117
  export default Gemini15VisionPlugin;
@@ -81,7 +81,7 @@ class GeminiChatPlugin extends ModelPlugin {
81
81
  });
82
82
  }
83
83
 
84
- // Gemini requires an even number of messages
84
+ // Gemini requires an odd number of messages
85
85
  if (modifiedMessages.length % 2 === 0) {
86
86
  modifiedMessages = modifiedMessages.slice(1);
87
87
  }
@@ -1,6 +1,5 @@
1
1
  import GeminiChatPlugin from './geminiChatPlugin.js';
2
2
  import mime from 'mime-types';
3
- import logger from '../../lib/logger.js';
4
3
 
5
4
  class GeminiVisionPlugin extends GeminiChatPlugin {
6
5
 
@@ -55,7 +54,7 @@ class GeminiVisionPlugin extends GeminiChatPlugin {
55
54
  }
56
55
  }
57
56
  } catch (e) {
58
- logger.warn(`Unable to parse part - including as string: ${partString}`);
57
+ // this space intentionally left blank
59
58
  }
60
59
  return { text: partString };
61
60
  };
@@ -87,7 +86,7 @@ class GeminiVisionPlugin extends GeminiChatPlugin {
87
86
  });
88
87
  }
89
88
 
90
- // Gemini requires an even number of messages
89
+ // Gemini requires an odd number of messages
91
90
  if (modifiedMessages.length % 2 === 0) {
92
91
  modifiedMessages = modifiedMessages.slice(1);
93
92
  }