@aj-archipelago/cortex 1.3.27 → 1.3.28

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": "@aj-archipelago/cortex",
3
- "version": "1.3.27",
3
+ "version": "1.3.28",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
package/server/parser.js CHANGED
@@ -16,7 +16,7 @@ const parseNumberedList = (str) => {
16
16
  async function parseNumberedObjectList(text, format) {
17
17
  const parsedList = await callPathway('sys_parse_numbered_object_list', { text, format });
18
18
  try {
19
- return JSON.parse(parsedList);
19
+ return JSON.parse(parsedList) || [];
20
20
  } catch (error) {
21
21
  logger.warn(`Failed to parse numbered object list: ${error.message}`);
22
22
  return [];
package/server/rest.js CHANGED
@@ -48,7 +48,7 @@ const processRestRequest = async (server, req, pathway, name, parameterMap = {})
48
48
  return value.map(msg => ({
49
49
  ...msg,
50
50
  content: Array.isArray(msg.content) ?
51
- JSON.stringify(msg.content) :
51
+ msg.content.map(item => JSON.stringify(item)) :
52
52
  msg.content
53
53
  }));
54
54
  } else {
@@ -212,3 +212,57 @@ test('convertMessagesToClaudeVertex user message with no content', async (t) =>
212
212
  t.deepEqual(output, { system: '', modifiedMessages: [] });
213
213
  });
214
214
 
215
+ test('convertMessagesToClaudeVertex with multi-part content array', async (t) => {
216
+ const plugin = new Claude3VertexPlugin(pathway, model);
217
+
218
+ // Test with multi-part content array
219
+ const multiPartContent = [
220
+ {
221
+ type: 'text',
222
+ text: 'Hello world'
223
+ },
224
+ {
225
+ type: 'text',
226
+ text: 'Hello2 world2'
227
+ },
228
+ {
229
+ type: 'image_url',
230
+ image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
231
+ }
232
+ ];
233
+
234
+ const messages = [
235
+ { role: 'system', content: 'System message' },
236
+ { role: 'user', content: multiPartContent }
237
+ ];
238
+
239
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
240
+
241
+ // Verify system message is preserved
242
+ t.is(output.system, 'System message');
243
+
244
+ // Verify the user message role is preserved
245
+ t.is(output.modifiedMessages[0].role, 'user');
246
+
247
+ // Verify the content array has the correct number of items
248
+ // We expect 3 items: 2 text items and 1 image item
249
+ t.is(output.modifiedMessages[0].content.length, 3);
250
+
251
+ // Verify the text content items
252
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
253
+ t.is(output.modifiedMessages[0].content[0].text, 'Hello world');
254
+
255
+ t.is(output.modifiedMessages[0].content[1].type, 'text');
256
+ t.is(output.modifiedMessages[0].content[1].text, 'Hello2 world2');
257
+
258
+ // Verify the image content item
259
+ t.is(output.modifiedMessages[0].content[2].type, 'image');
260
+ t.is(output.modifiedMessages[0].content[2].source.type, 'base64');
261
+ t.is(output.modifiedMessages[0].content[2].source.media_type, 'image/jpeg');
262
+
263
+ // Check if the base64 data looks reasonable
264
+ const base64Data = output.modifiedMessages[0].content[2].source.data;
265
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
266
+ t.true(base64Data.length > 100); // Check if the data is sufficiently long
267
+ t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
268
+ });
@@ -496,4 +496,129 @@ test('POST /chat/completions should return complete responses from gpt-4o', asyn
496
496
  const content = response.body.choices[0].message.content;
497
497
  t.regex(content, /END_MARKER_XYZ$/);
498
498
  });
499
+
500
+ test('POST /chat/completions should handle array content properly', async (t) => {
501
+ // This test verifies the functionality in server/rest.js where array content is JSON stringified
502
+ // Specifically testing: content: Array.isArray(msg.content) ? msg.content.map(item => JSON.stringify(item)) : msg.content
503
+
504
+ // Create a request with MultiMessage array content
505
+ const testContent = [
506
+ {
507
+ type: 'text',
508
+ text: 'Hello world'
509
+ },
510
+ {
511
+ type: 'text',
512
+ text: 'Hello2 world2'
513
+ },
514
+ {
515
+ type: 'image',
516
+ url: 'https://example.com/test.jpg'
517
+ }
518
+ ];
519
+
520
+ try {
521
+ // First, check if the API server is running and get available models
522
+ let modelToUse = '*'; // Default fallback model
523
+ try {
524
+ const modelsResponse = await got(`${API_BASE}/models`, { responseType: 'json' });
525
+ if (modelsResponse.body && modelsResponse.body.data && modelsResponse.body.data.length > 0) {
526
+ const models = modelsResponse.body.data.map(model => model.id);
527
+
528
+ // Priority 1: Find sonnet with highest version (e.g., claude-3.7-sonnet)
529
+ const sonnetVersions = models
530
+ .filter(id => id.includes('-sonnet') && id.startsWith('claude-'))
531
+ .sort((a, b) => {
532
+ // Extract version numbers and compare
533
+ const versionA = a.match(/claude-(\d+\.\d+)-sonnet/);
534
+ const versionB = b.match(/claude-(\d+\.\d+)-sonnet/);
535
+ if (versionA && versionB) {
536
+ return parseFloat(versionB[1]) - parseFloat(versionA[1]); // Descending order
537
+ }
538
+ return 0;
539
+ });
540
+
541
+ if (sonnetVersions.length > 0) {
542
+ modelToUse = sonnetVersions[0]; // Use highest version sonnet
543
+ } else {
544
+ // Priority 2: Any model ending with -sonnet
545
+ const anySonnet = models.find(id => id.endsWith('-sonnet'));
546
+ if (anySonnet) {
547
+ modelToUse = anySonnet;
548
+ } else {
549
+ // Priority 3: Any model starting with claude-
550
+ const anyClaude = models.find(id => id.startsWith('claude-'));
551
+ if (anyClaude) {
552
+ modelToUse = anyClaude;
553
+ } else {
554
+ // Fallback: Just use the first available model
555
+ modelToUse = models[0];
556
+ }
557
+ }
558
+ }
559
+
560
+ t.log(`Using model: ${modelToUse}`);
561
+ }
562
+ } catch (modelError) {
563
+ t.log('Could not get available models, using default model');
564
+ }
565
+
566
+ // Make a direct HTTP request to the REST API
567
+ const response = await axios.post(`${API_BASE}/chat/completions`, {
568
+ model: modelToUse,
569
+ messages: [
570
+ {
571
+ role: 'user',
572
+ content: testContent
573
+ }
574
+ ]
575
+ });
576
+
577
+ t.log('Response:', response.data.choices[0].message);
578
+
579
+ const message = response.data.choices[0].message;
580
+
581
+ //message should not have anything similar to:
582
+ //Execution failed for sys_claude_37_sonnet: HTTP error: 400 Bad Request
583
+ //HTTP error:
584
+ t.falsy(message.content.startsWith('HTTP error:'));
585
+ //400 Bad Request
586
+ t.falsy(message.content.startsWith('400 Bad Request'));
587
+ //Execution failed
588
+ t.falsy(message.content.startsWith('Execution failed'));
589
+ //Invalid JSON
590
+ t.falsy(message.content.startsWith('Invalid JSON'));
591
+
592
+
593
+ // If the request succeeds, it means the array content was properly processed
594
+ // If the JSON.stringify was not applied correctly, the request would fail
595
+ t.truthy(response.data);
596
+ t.pass('REST API successfully processed array content');
597
+ } catch (error) {
598
+ // If there's a connection error (e.g., API not running), we'll skip this test
599
+ if (error.code === 'ECONNREFUSED') {
600
+ t.pass('Skipping test - REST API not available');
601
+ } else {
602
+ // Check if the error response contains useful information
603
+ if (error.response) {
604
+ // We got a response from the server, but with an error status
605
+ t.log('Server responded with:', error.response.data);
606
+
607
+ // Skip the test if the server is running but no pathway is configured to handle the request
608
+ if (error.response.status === 404 &&
609
+ error.response.data.error &&
610
+ error.response.data.error.includes('not found')) {
611
+ t.pass('Skipping test - No suitable pathway configured for this API endpoint');
612
+ } else {
613
+ t.fail(`API request failed with status ${error.response.status}: ${error.response.statusText}`);
614
+ }
615
+ } else {
616
+ // No response received
617
+ t.fail(`API request failed: ${error.message}`);
618
+ }
619
+ }
620
+ }
621
+ });
622
+
623
+
499
624