@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.
Files changed (44) hide show
  1. package/README.md +64 -0
  2. package/config.js +26 -1
  3. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +2 -2
  4. package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +9 -4
  5. package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +1 -0
  6. package/lib/util.js +5 -25
  7. package/package.json +5 -2
  8. package/pathways/system/entity/memory/shared/sys_memory_helpers.js +228 -0
  9. package/pathways/system/entity/memory/sys_memory_format.js +30 -0
  10. package/pathways/system/entity/memory/sys_memory_manager.js +85 -27
  11. package/pathways/system/entity/memory/sys_memory_process.js +154 -0
  12. package/pathways/system/entity/memory/sys_memory_required.js +4 -2
  13. package/pathways/system/entity/memory/sys_memory_topic.js +22 -0
  14. package/pathways/system/entity/memory/sys_memory_update.js +50 -150
  15. package/pathways/system/entity/memory/sys_read_memory.js +67 -69
  16. package/pathways/system/entity/memory/sys_save_memory.js +1 -1
  17. package/pathways/system/entity/memory/sys_search_memory.js +1 -1
  18. package/pathways/system/entity/sys_entity_start.js +9 -6
  19. package/pathways/system/entity/sys_generator_image.js +5 -41
  20. package/pathways/system/entity/sys_generator_memory.js +3 -1
  21. package/pathways/system/entity/sys_generator_reasoning.js +1 -1
  22. package/pathways/system/entity/sys_router_tool.js +3 -4
  23. package/pathways/system/rest_streaming/sys_claude_35_sonnet.js +1 -1
  24. package/pathways/system/rest_streaming/sys_claude_3_haiku.js +1 -1
  25. package/pathways/system/rest_streaming/sys_google_gemini_chat.js +1 -1
  26. package/pathways/system/rest_streaming/sys_ollama_chat.js +21 -0
  27. package/pathways/system/rest_streaming/sys_ollama_completion.js +14 -0
  28. package/pathways/system/rest_streaming/sys_openai_chat_o1.js +1 -1
  29. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +1 -1
  30. package/pathways/transcribe_gemini.js +525 -0
  31. package/server/modelExecutor.js +8 -0
  32. package/server/pathwayResolver.js +13 -8
  33. package/server/plugins/claude3VertexPlugin.js +150 -18
  34. package/server/plugins/gemini15ChatPlugin.js +90 -1
  35. package/server/plugins/gemini15VisionPlugin.js +16 -3
  36. package/server/plugins/modelPlugin.js +12 -9
  37. package/server/plugins/ollamaChatPlugin.js +158 -0
  38. package/server/plugins/ollamaCompletionPlugin.js +147 -0
  39. package/server/rest.js +70 -8
  40. package/tests/claude3VertexToolConversion.test.js +411 -0
  41. package/tests/memoryfunction.test.js +560 -46
  42. package/tests/multimodal_conversion.test.js +169 -0
  43. package/tests/openai_api.test.js +332 -0
  44. 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
+ });
@@ -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