@aj-archipelago/cortex 1.3.21 → 1.3.23
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/README.md +64 -0
- package/config.js +26 -1
- package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +2 -2
- package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +9 -4
- package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +1 -0
- package/lib/util.js +5 -25
- package/package.json +5 -2
- package/pathways/system/entity/memory/shared/sys_memory_helpers.js +228 -0
- package/pathways/system/entity/memory/sys_memory_format.js +30 -0
- package/pathways/system/entity/memory/sys_memory_manager.js +85 -27
- package/pathways/system/entity/memory/sys_memory_process.js +154 -0
- package/pathways/system/entity/memory/sys_memory_required.js +4 -2
- package/pathways/system/entity/memory/sys_memory_topic.js +22 -0
- package/pathways/system/entity/memory/sys_memory_update.js +50 -150
- package/pathways/system/entity/memory/sys_read_memory.js +67 -69
- package/pathways/system/entity/memory/sys_save_memory.js +1 -1
- package/pathways/system/entity/memory/sys_search_memory.js +1 -1
- package/pathways/system/entity/sys_entity_start.js +9 -6
- package/pathways/system/entity/sys_generator_image.js +5 -41
- package/pathways/system/entity/sys_generator_memory.js +3 -1
- package/pathways/system/entity/sys_generator_reasoning.js +1 -1
- package/pathways/system/entity/sys_router_tool.js +3 -4
- package/pathways/system/rest_streaming/sys_claude_35_sonnet.js +1 -1
- package/pathways/system/rest_streaming/sys_claude_3_haiku.js +1 -1
- package/pathways/system/rest_streaming/sys_google_gemini_chat.js +1 -1
- package/pathways/system/rest_streaming/sys_ollama_chat.js +21 -0
- package/pathways/system/rest_streaming/sys_ollama_completion.js +14 -0
- package/pathways/system/rest_streaming/sys_openai_chat_o1.js +1 -1
- package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +1 -1
- package/pathways/transcribe_gemini.js +525 -0
- package/server/modelExecutor.js +8 -0
- package/server/pathwayResolver.js +13 -8
- package/server/plugins/claude3VertexPlugin.js +150 -18
- package/server/plugins/gemini15ChatPlugin.js +90 -1
- package/server/plugins/gemini15VisionPlugin.js +16 -3
- package/server/plugins/modelPlugin.js +12 -9
- package/server/plugins/ollamaChatPlugin.js +158 -0
- package/server/plugins/ollamaCompletionPlugin.js +147 -0
- package/server/rest.js +70 -8
- package/tests/claude3VertexToolConversion.test.js +411 -0
- package/tests/memoryfunction.test.js +560 -46
- package/tests/multimodal_conversion.test.js +169 -0
- package/tests/openai_api.test.js +332 -0
- package/tests/transcribe_gemini.test.js +217 -0
|
@@ -271,12 +271,18 @@ test('Pathological cases', async (t) => {
|
|
|
271
271
|
|
|
272
272
|
t.is(geminiSystem15.parts[0].text, 'You are a helpful assistant.');
|
|
273
273
|
t.is(geminiSystem15.parts[1].text, 'You are also very knowledgeable.');
|
|
274
|
+
|
|
274
275
|
t.is(geminiMessages15.length, 3);
|
|
276
|
+
// First user message combines "Hello" and "Another greeting"
|
|
275
277
|
t.is(geminiMessages15[0].role, 'user');
|
|
276
278
|
t.is(geminiMessages15[0].parts[0].text, 'Hello');
|
|
277
279
|
t.is(geminiMessages15[0].parts[1].text, 'Another greeting');
|
|
280
|
+
|
|
281
|
+
// Assistant message "Hi there!"
|
|
278
282
|
t.is(geminiMessages15[1].role, 'assistant');
|
|
279
283
|
t.is(geminiMessages15[1].parts[0].text, 'Hi there!');
|
|
284
|
+
|
|
285
|
+
// Final user message combines "How are you?", image content, and "Another question"
|
|
280
286
|
t.is(geminiMessages15[2].role, 'user');
|
|
281
287
|
t.is(geminiMessages15[2].parts[0].text, 'How are you?');
|
|
282
288
|
t.is(geminiMessages15[2].parts[1].text, 'What\'s this?');
|
|
@@ -310,6 +316,79 @@ test('Empty message array', async (t) => {
|
|
|
310
316
|
t.is(geminiMessages15.length, 0);
|
|
311
317
|
});
|
|
312
318
|
|
|
319
|
+
// Test simple string array content
|
|
320
|
+
test('Simple string array content', async (t) => {
|
|
321
|
+
const { gemini15 } = createPlugins();
|
|
322
|
+
|
|
323
|
+
const messages = [
|
|
324
|
+
{ role: 'user', content: "Initial message" },
|
|
325
|
+
{ role: 'assistant', content: [
|
|
326
|
+
"\"Searchin' for my lost shaker of salt...\"\n",
|
|
327
|
+
]},
|
|
328
|
+
{ role: 'user', content: [
|
|
329
|
+
"Here's another simple string in an array",
|
|
330
|
+
]}
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const { modifiedMessages } = gemini15.convertMessagesToGemini(messages);
|
|
334
|
+
|
|
335
|
+
t.is(modifiedMessages.length, 3);
|
|
336
|
+
t.is(modifiedMessages[0].role, 'user');
|
|
337
|
+
t.is(modifiedMessages[0].parts.length, 1);
|
|
338
|
+
t.is(modifiedMessages[0].parts[0].text, "Initial message");
|
|
339
|
+
t.is(modifiedMessages[1].role, 'assistant');
|
|
340
|
+
t.is(modifiedMessages[1].parts.length, 1);
|
|
341
|
+
t.is(modifiedMessages[1].parts[0].text, "\"Searchin' for my lost shaker of salt...\"\n");
|
|
342
|
+
t.is(modifiedMessages[2].role, 'user');
|
|
343
|
+
t.is(modifiedMessages[2].parts.length, 1);
|
|
344
|
+
t.is(modifiedMessages[2].parts[0].text, "Here's another simple string in an array");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Test string-encoded multimodal content
|
|
348
|
+
test('String-encoded multimodal content', async (t) => {
|
|
349
|
+
const { gemini15 } = createPlugins();
|
|
350
|
+
|
|
351
|
+
const messages = [
|
|
352
|
+
{ role: 'user', content: [
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
type: 'text',
|
|
355
|
+
text: 'What is in this image?'
|
|
356
|
+
}),
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
type: 'image_url',
|
|
359
|
+
image_url: { url: 'gs://my-bucket/image.jpg' }
|
|
360
|
+
})
|
|
361
|
+
]},
|
|
362
|
+
{ role: 'assistant', content: [
|
|
363
|
+
JSON.stringify({
|
|
364
|
+
type: 'text',
|
|
365
|
+
text: 'I see a cat.'
|
|
366
|
+
})
|
|
367
|
+
]},
|
|
368
|
+
{ role: 'user', content: [
|
|
369
|
+
JSON.stringify({
|
|
370
|
+
type: 'text',
|
|
371
|
+
text: 'Is it a big cat?'
|
|
372
|
+
})
|
|
373
|
+
]}
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const { modifiedMessages } = gemini15.convertMessagesToGemini(messages);
|
|
377
|
+
|
|
378
|
+
t.is(modifiedMessages.length, 3);
|
|
379
|
+
t.is(modifiedMessages[0].role, 'user');
|
|
380
|
+
t.is(modifiedMessages[0].parts.length, 2);
|
|
381
|
+
t.is(modifiedMessages[0].parts[0].text, 'What is in this image?');
|
|
382
|
+
t.true('fileData' in modifiedMessages[0].parts[1]);
|
|
383
|
+
t.is(modifiedMessages[0].parts[1].fileData.fileUri, 'gs://my-bucket/image.jpg');
|
|
384
|
+
t.is(modifiedMessages[1].role, 'assistant');
|
|
385
|
+
t.is(modifiedMessages[1].parts.length, 1);
|
|
386
|
+
t.is(modifiedMessages[1].parts[0].text, 'I see a cat.');
|
|
387
|
+
t.is(modifiedMessages[2].role, 'user');
|
|
388
|
+
t.is(modifiedMessages[2].parts.length, 1);
|
|
389
|
+
t.is(modifiedMessages[2].parts[0].text, 'Is it a big cat?');
|
|
390
|
+
});
|
|
391
|
+
|
|
313
392
|
// Test messages with only system messages
|
|
314
393
|
test('Only system messages', async (t) => {
|
|
315
394
|
const { openai, claude, gemini, gemini15 } = createPlugins();
|
|
@@ -417,3 +496,93 @@ test('Gemini 1.5 image URL edge cases', t => {
|
|
|
417
496
|
// Verify we only have one part (the text)
|
|
418
497
|
t.is(modifiedMessages[0].parts.length, 1, 'Should only have the text part');
|
|
419
498
|
});
|
|
499
|
+
|
|
500
|
+
// Test multiple images in single message for Claude
|
|
501
|
+
test('Multiple images in single Claude message', async (t) => {
|
|
502
|
+
const { claude } = createPlugins();
|
|
503
|
+
|
|
504
|
+
const multiImageMessage = [
|
|
505
|
+
{ role: 'user', content: [
|
|
506
|
+
{ type: 'text', text: 'Compare these images:' },
|
|
507
|
+
{ type: 'image_url', image_url: { url: sampleBase64Image } },
|
|
508
|
+
{ type: 'text', text: 'with this one:' },
|
|
509
|
+
{ type: 'image_url', image_url: { url: sampleBase64Image } },
|
|
510
|
+
{ type: 'image_url', gcs: 'gs://cortex-bucket/image.jpg' }
|
|
511
|
+
]}
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
const { modifiedMessages } = await claude.convertMessagesToClaudeVertex(multiImageMessage);
|
|
515
|
+
|
|
516
|
+
t.is(modifiedMessages.length, 1);
|
|
517
|
+
t.is(modifiedMessages[0].role, 'user');
|
|
518
|
+
t.is(modifiedMessages[0].content.length, 4);
|
|
519
|
+
t.is(modifiedMessages[0].content[0].text, 'Compare these images:');
|
|
520
|
+
t.true(modifiedMessages[0].content[1].source.type === 'base64');
|
|
521
|
+
t.is(modifiedMessages[0].content[2].text, 'with this one:');
|
|
522
|
+
t.true(modifiedMessages[0].content[3].source.type === 'base64');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Test conversation history with mixed image types
|
|
526
|
+
test('Conversation history with mixed image types', async (t) => {
|
|
527
|
+
const { claude, gemini15 } = createPlugins();
|
|
528
|
+
|
|
529
|
+
const conversationHistory = [
|
|
530
|
+
{ role: 'system', content: 'You are a visual analysis assistant.' },
|
|
531
|
+
{ role: 'user', content: [
|
|
532
|
+
{ type: 'text', text: 'What\'s in this image?' },
|
|
533
|
+
{ type: 'image_url', image_url: { url: sampleBase64Image } }
|
|
534
|
+
]},
|
|
535
|
+
{ role: 'assistant', content: 'I see a landscape.' },
|
|
536
|
+
{ role: 'user', content: [
|
|
537
|
+
{ type: 'text', text: 'Compare it with this:' },
|
|
538
|
+
{ type: 'image_url', gcs: 'gs://cortex-bucket/image2.jpg' }
|
|
539
|
+
]},
|
|
540
|
+
{ role: 'assistant', content: 'The second image shows a different scene.' },
|
|
541
|
+
{ role: 'user', content: 'Which one do you prefer?' }
|
|
542
|
+
];
|
|
543
|
+
|
|
544
|
+
// Test Claude conversion
|
|
545
|
+
const { system: claudeSystem, modifiedMessages: claudeMessages } = await claude.convertMessagesToClaudeVertex(conversationHistory);
|
|
546
|
+
|
|
547
|
+
t.is(claudeSystem, 'You are a visual analysis assistant.');
|
|
548
|
+
t.is(claudeMessages.length, 5);
|
|
549
|
+
t.is(claudeMessages[1].content[0].text, 'I see a landscape.');
|
|
550
|
+
t.is(claudeMessages[3].content[0].text, 'The second image shows a different scene.');
|
|
551
|
+
t.is(claudeMessages[4].content[0].text, 'Which one do you prefer?');
|
|
552
|
+
|
|
553
|
+
// Test Gemini 1.5 conversion
|
|
554
|
+
const { system: geminiSystem15, modifiedMessages: geminiMessages15 } = gemini15.convertMessagesToGemini(conversationHistory);
|
|
555
|
+
|
|
556
|
+
t.is(geminiSystem15.parts[0].text, 'You are a visual analysis assistant.');
|
|
557
|
+
t.is(geminiMessages15.length, 5);
|
|
558
|
+
t.true('inlineData' in geminiMessages15[0].parts[1]);
|
|
559
|
+
t.is(geminiMessages15[1].parts[0].text, 'I see a landscape.');
|
|
560
|
+
t.true('fileData' in geminiMessages15[2].parts[1]);
|
|
561
|
+
t.is(geminiMessages15[2].parts[1].fileData.fileUri, 'gs://cortex-bucket/image2.jpg');
|
|
562
|
+
t.is(geminiMessages15[3].parts[0].text, 'The second image shows a different scene.');
|
|
563
|
+
t.is(geminiMessages15[4].parts[0].text, 'Which one do you prefer?');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Test handling of large images
|
|
567
|
+
test('Large image handling', async (t) => {
|
|
568
|
+
const { claude, gemini15 } = createPlugins();
|
|
569
|
+
|
|
570
|
+
// Create a large base64 string (>10MB)
|
|
571
|
+
const largeSampleImage = 'data:image/jpeg;base64,' + 'A'.repeat(10 * 1024 * 1024);
|
|
572
|
+
|
|
573
|
+
const largeImageMessage = [
|
|
574
|
+
{ role: 'user', content: [
|
|
575
|
+
{ type: 'text', text: 'Check this large image:' },
|
|
576
|
+
{ type: 'image_url', image_url: { url: largeSampleImage } }
|
|
577
|
+
]}
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
// Both Claude and Gemini should handle or reject oversized images gracefully
|
|
581
|
+
const { modifiedMessages: claudeMessages } = await claude.convertMessagesToClaudeVertex(largeImageMessage);
|
|
582
|
+
const { modifiedMessages: geminiMessages } = gemini15.convertMessagesToGemini(largeImageMessage);
|
|
583
|
+
|
|
584
|
+
// Verify both models handle the oversized image appropriately
|
|
585
|
+
// (The exact behavior - rejection vs. compression - should match the model's specifications)
|
|
586
|
+
t.is(claudeMessages[0].content[0].text, 'Check this large image:');
|
|
587
|
+
t.is(geminiMessages[0].parts[0].text, 'Check this large image:');
|
|
588
|
+
});
|
package/tests/openai_api.test.js
CHANGED
|
@@ -60,6 +60,36 @@ test('POST /chat/completions', async (t) => {
|
|
|
60
60
|
t.true(Array.isArray(response.body.choices));
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test('POST /chat/completions with multimodal content', async (t) => {
|
|
64
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
65
|
+
json: {
|
|
66
|
+
model: 'claude-3.5-sonnet',
|
|
67
|
+
messages: [{
|
|
68
|
+
role: 'user',
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: 'What do you see in this image?'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'image',
|
|
76
|
+
image_url: {
|
|
77
|
+
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDABQODxIPDRQSEBIXFRQdHx4eHRoaHSQrJyEwPENBMDQ4NDQ0QUJCSkNLS0tNSkpQUFFQR1BTYWNgY2FQYWFQYWj/2wBDARUXFyAeIBohHh4oIiE2LCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}],
|
|
82
|
+
stream: false,
|
|
83
|
+
},
|
|
84
|
+
responseType: 'json',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
t.is(response.statusCode, 200);
|
|
88
|
+
t.is(response.body.object, 'chat.completion');
|
|
89
|
+
t.true(Array.isArray(response.body.choices));
|
|
90
|
+
t.truthy(response.body.choices[0].message.content);
|
|
91
|
+
});
|
|
92
|
+
|
|
63
93
|
async function connectToSSEEndpoint(url, endpoint, payload, t, customAssertions) {
|
|
64
94
|
return new Promise(async (resolve, reject) => {
|
|
65
95
|
try {
|
|
@@ -143,5 +173,307 @@ test('POST SSE: /v1/chat/completions should send a series of events and a [DONE]
|
|
|
143
173
|
};
|
|
144
174
|
|
|
145
175
|
await connectToSSEEndpoint(url, '/chat/completions', payload, t, chatCompletionsAssertions);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('POST SSE: /v1/chat/completions with multimodal content should send a series of events and a [DONE] event', async (t) => {
|
|
179
|
+
const payload = {
|
|
180
|
+
model: 'claude-3.5-sonnet',
|
|
181
|
+
messages: [{
|
|
182
|
+
role: 'user',
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: 'What do you see in this image?'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: 'image',
|
|
190
|
+
image_url: {
|
|
191
|
+
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDABQODxIPDRQSEBIXFRQdHx4eHRoaHSQrJyEwPENBMDQ4NDQ0QUJCSkNLS0tNSkpQUFFQR1BTYWNgY2FQYWFQYWj/2wBDARUXFyAeIBohHh4oIiE2LCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k='
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}],
|
|
196
|
+
stream: true,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const url = `http://localhost:${process.env.CORTEX_PORT}/v1`;
|
|
200
|
+
|
|
201
|
+
const multimodalChatCompletionsAssertions = (t, messageJson) => {
|
|
202
|
+
t.truthy(messageJson.id);
|
|
203
|
+
t.is(messageJson.object, 'chat.completion.chunk');
|
|
204
|
+
t.truthy(messageJson.choices[0].delta);
|
|
205
|
+
if (messageJson.choices[0].finish_reason === 'stop') {
|
|
206
|
+
t.truthy(messageJson.choices[0].delta);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
await connectToSSEEndpoint(url, '/chat/completions', payload, t, multimodalChatCompletionsAssertions);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('POST /chat/completions should handle multimodal content for non-multimodal model', async (t) => {
|
|
214
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
215
|
+
json: {
|
|
216
|
+
model: 'gpt-3.5-turbo',
|
|
217
|
+
messages: [{
|
|
218
|
+
role: 'user',
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: 'What do you see in this image?'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'image',
|
|
226
|
+
image_url: {
|
|
227
|
+
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...'
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}],
|
|
232
|
+
stream: false,
|
|
233
|
+
},
|
|
234
|
+
responseType: 'json',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
t.is(response.statusCode, 200);
|
|
238
|
+
t.is(response.body.object, 'chat.completion');
|
|
239
|
+
t.true(Array.isArray(response.body.choices));
|
|
240
|
+
t.truthy(response.body.choices[0].message.content);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('POST SSE: /v1/chat/completions should handle streaming multimodal content for non-multimodal model', async (t) => {
|
|
244
|
+
const payload = {
|
|
245
|
+
model: 'gpt-3.5-turbo',
|
|
246
|
+
messages: [{
|
|
247
|
+
role: 'user',
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: 'What do you see in this image?'
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: 'image',
|
|
255
|
+
image_url: {
|
|
256
|
+
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
}],
|
|
261
|
+
stream: true,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const streamingAssertions = (t, messageJson) => {
|
|
265
|
+
t.truthy(messageJson.id);
|
|
266
|
+
t.is(messageJson.object, 'chat.completion.chunk');
|
|
267
|
+
t.truthy(messageJson.choices[0].delta);
|
|
268
|
+
if (messageJson.choices[0].finish_reason === 'stop') {
|
|
269
|
+
t.truthy(messageJson.choices[0].delta);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
await connectToSSEEndpoint(
|
|
274
|
+
`http://localhost:${process.env.CORTEX_PORT}/v1`,
|
|
275
|
+
'/chat/completions',
|
|
276
|
+
payload,
|
|
277
|
+
t,
|
|
278
|
+
streamingAssertions
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('POST /chat/completions should handle malformed multimodal content', async (t) => {
|
|
283
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
284
|
+
json: {
|
|
285
|
+
model: 'claude-3.5-sonnet',
|
|
286
|
+
messages: [{
|
|
287
|
+
role: 'user',
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: 'text',
|
|
291
|
+
// Missing text field
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'image',
|
|
295
|
+
// Missing image_url
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
}],
|
|
299
|
+
stream: false,
|
|
300
|
+
},
|
|
301
|
+
responseType: 'json',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
t.is(response.statusCode, 200);
|
|
305
|
+
t.is(response.body.object, 'chat.completion');
|
|
306
|
+
t.true(Array.isArray(response.body.choices));
|
|
307
|
+
t.truthy(response.body.choices[0].message.content);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('POST /chat/completions should handle invalid image data', async (t) => {
|
|
311
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
312
|
+
json: {
|
|
313
|
+
model: 'claude-3.5-sonnet',
|
|
314
|
+
messages: [{
|
|
315
|
+
role: 'user',
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: 'text',
|
|
319
|
+
text: 'What do you see in this image?'
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: 'image',
|
|
323
|
+
image_url: {
|
|
324
|
+
url: 'not-a-valid-base64-image'
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}],
|
|
329
|
+
stream: false,
|
|
330
|
+
},
|
|
331
|
+
responseType: 'json',
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
t.is(response.statusCode, 200);
|
|
335
|
+
t.is(response.body.object, 'chat.completion');
|
|
336
|
+
t.true(Array.isArray(response.body.choices));
|
|
337
|
+
t.truthy(response.body.choices[0].message.content);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('POST /completions should handle model parameters', async (t) => {
|
|
341
|
+
const response = await got.post(`${API_BASE}/completions`, {
|
|
342
|
+
json: {
|
|
343
|
+
model: 'gpt-3.5-turbo',
|
|
344
|
+
prompt: 'Say this is a test',
|
|
345
|
+
temperature: 0.7,
|
|
346
|
+
max_tokens: 100,
|
|
347
|
+
top_p: 1,
|
|
348
|
+
frequency_penalty: 0,
|
|
349
|
+
presence_penalty: 0,
|
|
350
|
+
stream: false,
|
|
351
|
+
},
|
|
352
|
+
responseType: 'json',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
t.is(response.statusCode, 200);
|
|
356
|
+
t.is(response.body.object, 'text_completion');
|
|
357
|
+
t.true(Array.isArray(response.body.choices));
|
|
358
|
+
t.truthy(response.body.choices[0].text);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('POST /chat/completions should handle function calling', async (t) => {
|
|
362
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
363
|
+
json: {
|
|
364
|
+
model: 'gpt-3.5-turbo',
|
|
365
|
+
messages: [{ role: 'user', content: 'What is the weather in Boston?' }],
|
|
366
|
+
functions: [{
|
|
367
|
+
name: 'get_weather',
|
|
368
|
+
description: 'Get the current weather in a given location',
|
|
369
|
+
parameters: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
location: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'The city and state, e.g. San Francisco, CA'
|
|
375
|
+
},
|
|
376
|
+
unit: {
|
|
377
|
+
type: 'string',
|
|
378
|
+
enum: ['celsius', 'fahrenheit']
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
required: ['location']
|
|
382
|
+
}
|
|
383
|
+
}],
|
|
384
|
+
stream: false,
|
|
385
|
+
},
|
|
386
|
+
responseType: 'json',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
t.is(response.statusCode, 200);
|
|
390
|
+
t.is(response.body.object, 'chat.completion');
|
|
391
|
+
t.true(Array.isArray(response.body.choices));
|
|
392
|
+
const choice = response.body.choices[0];
|
|
393
|
+
t.true(['function_call', 'stop'].includes(choice.finish_reason));
|
|
394
|
+
if (choice.finish_reason === 'function_call') {
|
|
395
|
+
t.truthy(choice.message.function_call);
|
|
396
|
+
t.truthy(choice.message.function_call.name);
|
|
397
|
+
t.truthy(choice.message.function_call.arguments);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('POST /chat/completions should validate response format', async (t) => {
|
|
402
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
403
|
+
json: {
|
|
404
|
+
model: 'gpt-3.5-turbo',
|
|
405
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
406
|
+
stream: false,
|
|
407
|
+
},
|
|
408
|
+
responseType: 'json',
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
t.is(response.statusCode, 200);
|
|
412
|
+
t.is(response.body.object, 'chat.completion');
|
|
413
|
+
t.true(Array.isArray(response.body.choices));
|
|
414
|
+
t.truthy(response.body.id);
|
|
415
|
+
t.truthy(response.body.created);
|
|
416
|
+
t.truthy(response.body.model);
|
|
417
|
+
|
|
418
|
+
const choice = response.body.choices[0];
|
|
419
|
+
t.is(typeof choice.index, 'number');
|
|
420
|
+
t.truthy(choice.message);
|
|
421
|
+
t.truthy(choice.message.role);
|
|
422
|
+
t.truthy(choice.message.content);
|
|
423
|
+
t.truthy(choice.finish_reason);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('POST /chat/completions should handle system messages', async (t) => {
|
|
427
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
428
|
+
json: {
|
|
429
|
+
model: 'gpt-3.5-turbo',
|
|
430
|
+
messages: [
|
|
431
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
432
|
+
{ role: 'user', content: 'Hello!' }
|
|
433
|
+
],
|
|
434
|
+
stream: false,
|
|
435
|
+
},
|
|
436
|
+
responseType: 'json',
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
t.is(response.statusCode, 200);
|
|
440
|
+
t.is(response.body.object, 'chat.completion');
|
|
441
|
+
t.true(Array.isArray(response.body.choices));
|
|
442
|
+
t.truthy(response.body.choices[0].message.content);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('POST /chat/completions should handle errors gracefully', async (t) => {
|
|
446
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
447
|
+
json: {
|
|
448
|
+
// Missing required model field
|
|
449
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
450
|
+
},
|
|
451
|
+
responseType: 'json',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
t.is(response.statusCode, 200);
|
|
455
|
+
t.is(response.body.object, 'chat.completion');
|
|
456
|
+
t.true(Array.isArray(response.body.choices));
|
|
457
|
+
t.truthy(response.body.choices[0].message.content);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('POST /chat/completions should handle token limits', async (t) => {
|
|
461
|
+
const response = await got.post(`${API_BASE}/chat/completions`, {
|
|
462
|
+
json: {
|
|
463
|
+
model: 'gpt-3.5-turbo',
|
|
464
|
+
messages: [{
|
|
465
|
+
role: 'user',
|
|
466
|
+
content: 'Hello!'.repeat(5000) // Very long message
|
|
467
|
+
}],
|
|
468
|
+
max_tokens: 100,
|
|
469
|
+
stream: false,
|
|
470
|
+
},
|
|
471
|
+
responseType: 'json',
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
t.is(response.statusCode, 200);
|
|
475
|
+
t.is(response.body.object, 'chat.completion');
|
|
476
|
+
t.true(Array.isArray(response.body.choices));
|
|
477
|
+
t.truthy(response.body.choices[0].message.content);
|
|
146
478
|
});
|
|
147
479
|
|