@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 +1 -1
- package/server/parser.js +1 -1
- package/server/rest.js +1 -1
- package/tests/claude3VertexPlugin.test.js +54 -0
- package/tests/openai_api.test.js +125 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.3.
|
|
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(
|
|
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
|
+
});
|
package/tests/openai_api.test.js
CHANGED
|
@@ -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
|
|