@aj-archipelago/cortex 1.3.32 → 1.3.34

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 (34) hide show
  1. package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +1 -1
  2. package/lib/encodeCache.js +22 -10
  3. package/lib/pathwayTools.js +10 -3
  4. package/lib/requestExecutor.js +1 -1
  5. package/lib/util.js +136 -1
  6. package/package.json +2 -2
  7. package/pathways/system/entity/memory/sys_memory_manager.js +2 -1
  8. package/pathways/system/entity/sys_entity_continue.js +10 -2
  9. package/pathways/system/entity/sys_entity_start.js +12 -10
  10. package/pathways/system/entity/sys_router_tool.js +2 -2
  11. package/server/chunker.js +23 -3
  12. package/server/pathwayResolver.js +2 -5
  13. package/server/plugins/claude3VertexPlugin.js +2 -3
  14. package/server/plugins/cohereGeneratePlugin.js +1 -1
  15. package/server/plugins/gemini15ChatPlugin.js +1 -1
  16. package/server/plugins/geminiChatPlugin.js +1 -1
  17. package/server/plugins/localModelPlugin.js +1 -1
  18. package/server/plugins/modelPlugin.js +332 -77
  19. package/server/plugins/openAiChatPlugin.js +1 -1
  20. package/server/plugins/openAiCompletionPlugin.js +1 -1
  21. package/server/plugins/palmChatPlugin.js +1 -1
  22. package/server/plugins/palmCodeCompletionPlugin.js +1 -1
  23. package/server/plugins/palmCompletionPlugin.js +1 -1
  24. package/tests/chunkfunction.test.js +9 -6
  25. package/tests/claude3VertexPlugin.test.js +81 -3
  26. package/tests/data/largecontent.txt +1 -0
  27. package/tests/data/mixedcontent.txt +1 -0
  28. package/tests/encodeCache.test.js +47 -14
  29. package/tests/modelPlugin.test.js +21 -0
  30. package/tests/multimodal_conversion.test.js +1 -1
  31. package/tests/subscription.test.js +7 -1
  32. package/tests/tokenHandlingTests.test.js +587 -0
  33. package/tests/truncateMessages.test.js +404 -46
  34. package/tests/util.test.js +146 -0
@@ -9,90 +9,448 @@ const { config, pathway, modelName, model } = mockPathwayResolverString;
9
9
  const modelPlugin = new ModelPlugin(pathway, model);
10
10
 
11
11
  const generateMessage = (role, content) => ({ role, content });
12
+ const generateStructuredMessage = (role, content) => ({ role, content: [{ type: 'text', text: content }] });
12
13
 
13
14
  test('truncateMessagesToTargetLength: should not modify messages if already within target length', (t) => {
14
15
  const messages = [
15
16
  generateMessage('user', 'Hello, how are you?'),
16
17
  generateMessage('assistant', 'I am doing well, thank you!'),
17
18
  ];
18
- const targetTokenLength = encode(modelPlugin.messagesToChatML(messages, false)).length;
19
+ const targetTokenLength = modelPlugin.countMessagesTokens(messages);
19
20
 
20
21
  const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
21
22
  t.deepEqual(result, messages);
22
23
  });
23
24
 
24
- test('truncateMessagesToTargetLength: should remove messages from the front until target length is reached', (t) => {
25
- const messages = [
26
- generateMessage('user', 'Hello, how are you?'),
27
- generateMessage('assistant', 'I am doing well, thank you!'),
28
- generateMessage('user', 'What is your favorite color?'),
29
- ];
30
- const targetTokenLength = encode(modelPlugin.messagesToChatML(messages.slice(1), false)).length;
25
+ test('truncateMessagesToTargetLength: should prioritize final user message', (t) => {
26
+ const messages = [
27
+ generateMessage('system', 'System message'),
28
+ generateMessage('user', 'First user message'),
29
+ generateMessage('assistant', 'Assistant response'),
30
+ generateMessage('user', 'Final important question that should be preserved'),
31
+ ];
32
+
33
+ // Set target length to only fit the final user message plus the minimum safety margin
34
+ const finalUserMsg = messages[messages.length - 1];
35
+ const targetTokenLength = modelPlugin.countMessagesTokens([finalUserMsg]) * 1.1;
36
+
37
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
38
+ t.is(result.length, 1, 'Should only keep final user message');
39
+ t.is(result[0].role, 'user', 'Should be a user message');
40
+ t.is(result[0].content, finalUserMsg.content, 'Should preserve final user message content');
41
+ });
31
42
 
32
- const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
33
- t.deepEqual(result, messages.slice(1));
43
+ test('truncateMessagesToTargetLength: should prioritize final user message with tight constraints', (t) => {
44
+ const messages = [
45
+ generateMessage('system', 'System message content that is very long and exceeds the target token length'),
46
+ generateMessage('user', 'Hello, how are you?'),
47
+ generateMessage('assistant', 'I am fine, thank you.'),
48
+ generateMessage('user', 'Final user message'),
49
+ ];
50
+
51
+ // Very tight token constraint
52
+ const targetTokenLength = 15;
53
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
54
+
55
+ // Should prioritize final user message
56
+ t.is(result.length, 1, 'Should keep only the final user message with tight constraints');
57
+ t.is(result[0].role, 'user', 'Should keep the user message');
58
+ t.is(result[0].content.length <= messages[3].content.length, true, 'User message may be truncated');
34
59
  });
35
60
 
36
- test('truncateMessagesToTargetLength: should skip system messages', (t) => {
37
- const messages = [
38
- generateMessage('system', 'System message 1'),
39
- generateMessage('user', 'Hello, how are you?'),
40
- generateMessage('assistant', 'I am doing well, thank you!'),
41
- ];
42
- const targetTokenLength = encode(modelPlugin.messagesToChatML([messages[0], ...messages.slice(2)], false)).length;
61
+ test('truncateMessagesToTargetLength: should add truncation markers to shortened messages', (t) => {
62
+ // Create a very long message that will definitely be truncated
63
+ const longContent = 'a'.repeat(1000);
43
64
 
44
- const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
45
- t.deepEqual(result, [messages[0], ...messages.slice(2)]);
65
+ const messages = [
66
+ generateMessage('system', 'System message: ' + longContent),
67
+ generateMessage('user', 'Final user message: ' + longContent),
68
+ ];
69
+
70
+ // Set a target token length that will force heavy truncation
71
+ const targetTokenLength = 20;
72
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
73
+
74
+ // Verify truncation markers are added
75
+ const expectedMarker = "[...]";
76
+
77
+ // Check if at least one message has the truncation marker
78
+ const hasMarker = result.some(msg => msg.content.includes(expectedMarker));
79
+ t.true(hasMarker, 'At least one message should have truncation marker');
80
+
81
+ // Verify individual messages
82
+ result.forEach(msg => {
83
+ // Only verify messages that were actually truncated
84
+ if (msg.content.length < 1000) {
85
+ t.true(msg.content.includes(expectedMarker),
86
+ `Truncated ${msg.role} message should include truncation marker`);
87
+ }
88
+ });
46
89
  });
47
90
 
48
- test('truncateMessagesToTargetLength: should truncate messages to fit target length', (t) => {
91
+ test('truncateMessagesToTargetLength: should not add truncation markers to messages that fit completely', (t) => {
49
92
  const messages = [
50
- generateMessage('user', 'Hello, how are you?'),
51
- generateMessage('assistant', 'I am doing well, thank you!'),
93
+ generateMessage('system', 'Short system message'),
94
+ generateMessage('user', 'Short user message'),
95
+ generateMessage('assistant', 'Short assistant message'),
96
+ generateMessage('user', 'Another short user message'),
52
97
  ];
53
- const targetTokenLength = encode(modelPlugin.messagesToChatML(messages, false)).length - 4;
54
98
 
99
+ // Set a target token length that allows all messages to fit
100
+ const targetTokenLength = encode(modelPlugin.messagesToChatML(messages, false)).length;
55
101
  const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
56
- t.true(result.every((message, index) => message.content.length <= messages[index].content.length));
57
- t.true(encode(modelPlugin.messagesToChatML(result, false)).length <= targetTokenLength);
102
+
103
+ // Verify no truncation markers are added
104
+ const expectedMarker = "[...]";
105
+
106
+ // None of the messages should have the truncation marker
107
+ const hasMarker = result.some(msg => msg.content.includes(expectedMarker));
108
+ t.false(hasMarker, 'No message should have a truncation marker when all fit completely');
109
+
110
+ // Verify content is unchanged
111
+ result.forEach((msg, index) => {
112
+ t.is(msg.content, messages[index].content,
113
+ `${msg.role} message content should be unchanged`);
114
+ });
58
115
  });
59
116
 
60
- test('truncateMessagesToTargetLength: should remove messages entirely if they need to be empty to fit target length', (t) => {
117
+ test('truncateMessagesToTargetLength: should handle extreme token constraints with markers', (t) => {
118
+ // Create a very long message that will definitely be truncated
119
+ const longContent = 'a'.repeat(1000);
120
+
61
121
  const messages = [
62
- generateMessage('user', 'Hello, how are you?'),
63
- generateMessage('assistant', 'I am doing well, thank you!'),
122
+ generateMessage('system', 'System message: ' + longContent),
123
+ generateMessage('user', 'Final user message: ' + longContent),
64
124
  ];
65
- const targetTokenLength = encode(modelPlugin.messagesToChatML(messages.slice(1), false)).length;
66
125
 
126
+ // Extremely tight token constraint
127
+ const targetTokenLength = 30;
67
128
  const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
68
- t.deepEqual(result, messages.slice(1));
129
+
130
+ // Verify result
131
+ t.true(result.length > 0, 'Should have at least one message');
132
+
133
+ // The kept message should have the truncation marker
134
+ const expectedMarker = "[...]";
135
+ t.true(result[0].content.includes(expectedMarker),
136
+ 'Extremely truncated message should include truncation marker');
69
137
  });
70
138
 
71
- test('truncateMessagesToTargetLength: should return an empty array if target length is 0', (t) => {
139
+ test('truncateMessagesToTargetLength: should maintain message order', (t) => {
140
+ const messages = [
141
+ generateMessage('system', 'System message'),
142
+ generateMessage('user', 'First user message'),
143
+ generateMessage('assistant', 'Assistant response'),
144
+ generateMessage('user', 'Second user message'),
145
+ generateMessage('assistant', 'Second assistant response'),
146
+ generateMessage('user', 'Final user message'),
147
+ ];
148
+
149
+ // Set target length to fit all messages
150
+ const targetTokenLength = encode(modelPlugin.messagesToChatML(messages, false)).length;
151
+
152
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
153
+ t.deepEqual(result.map(m => m.role), messages.map(m => m.role), 'Message order should be preserved');
154
+ });
155
+
156
+ test('truncateMessagesToTargetLength: should return messages with [...] if target length is 0', (t) => {
72
157
  const messages = [
73
158
  generateMessage('user', 'Hello, how are you?'),
74
159
  generateMessage('assistant', 'I am doing well, thank you!'),
75
160
  ];
76
161
 
77
- const result = modelPlugin.truncateMessagesToTargetLength(messages, 0);
78
- t.deepEqual(result, []);
162
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, null, 0);
163
+
164
+ // Should return all messages but with [...] content
165
+ t.is(result.length, messages.length, 'Should return all messages');
166
+
167
+ // Each message should be truncated to just the marker
168
+ result.forEach(msg => {
169
+ t.is(msg.content, '[...]', 'Message content should be just the truncation marker');
170
+ });
79
171
  });
80
172
 
81
- test('truncateMessagesToTargetLength: should not remove system messages even if they are too long', (t) => {
173
+ test('truncateMessagesToTargetLength: should handle structured messages with maxMessageTokenLength=0', (t) => {
82
174
  const messages = [
83
- generateMessage('user', 'Hello, how are you?'),
84
- generateMessage('system', 'System message content that is very long and exceeds the target token length'),
85
- generateMessage('assistant', 'I am fine, thank you.'),
175
+ generateStructuredMessage('user', 'Hello, how are you?'),
176
+ generateStructuredMessage('assistant', 'I am doing well, thank you!'),
86
177
  ];
87
178
 
88
- const targetTokenLength = 20;
89
- const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength);
179
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, null, 0);
180
+
181
+ // Should return all messages but with [...] content
182
+ t.is(result.length, messages.length, 'Should return all structured messages');
183
+
184
+ // Each message should be truncated to just a single content item with the marker
185
+ result.forEach(msg => {
186
+ t.true(Array.isArray(msg.content), 'Content should still be an array');
187
+ t.is(msg.content.length, 1, 'Should have exactly one content item');
188
+ t.is(msg.content[0].type, 'text', 'Content item should be of type text');
189
+ t.is(msg.content[0].text, '[...]', 'Content text should be just the truncation marker');
190
+ });
191
+ });
192
+
193
+ // New tests for maxMessageTokenLength
194
+
195
+ test('truncateMessagesToTargetLength: should respect maxMessageTokenLength constraint', (t) => {
196
+ // Create messages with different lengths
197
+ const longContent = 'a'.repeat(1000);
198
+
199
+ const messages = [
200
+ generateMessage('user', 'Short first message'),
201
+ generateMessage('assistant', longContent),
202
+ generateMessage('user', 'Short final message'),
203
+ ];
204
+
205
+ // Set a target that would fit all messages normally
206
+ const targetTokenLength = modelPlugin.countMessagesTokens(messages) + 100;
207
+
208
+ // Calculate tokens in the assistant message
209
+ const assistantMsgTokens = modelPlugin.countMessagesTokens([messages[1]]);
210
+
211
+ // Set maxMessageTokenLength to be less than the assistant message length
212
+ const maxMessageTokenLength = Math.floor(assistantMsgTokens * 0.3);
213
+
214
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
215
+
216
+ // All messages should be present
217
+ t.is(result.length, 3, 'All messages should be preserved');
218
+
219
+ // Only the long message should be truncated
220
+ t.is(result[0].content, messages[0].content, 'First message should be unchanged');
221
+ t.is(result[2].content, messages[2].content, 'Last message should be unchanged');
222
+
223
+ // The assistant message should be truncated
224
+ t.true(result[1].content.length < longContent.length, 'Long message should be truncated');
225
+ t.true(result[1].content.includes('[...]'), 'Truncated message should have marker');
226
+
227
+ // Calculate tokens in the truncated message
228
+ const truncatedMsgTokens = modelPlugin.countMessagesTokens([result[1]]);
229
+
230
+ // Allow small buffer for truncation marker
231
+ t.true(truncatedMsgTokens <= maxMessageTokenLength + 10,
232
+ `Truncated message (${truncatedMsgTokens} tokens) should not exceed maxMessageTokenLength (${maxMessageTokenLength} tokens) by more than buffer`);
233
+ });
234
+
235
+ test('truncateMessagesToTargetLength: should handle very small maxMessageTokenLength', (t) => {
236
+ const messages = [
237
+ generateMessage('system', 'System message that will definitely need to be truncated to fit the maxMessageTokenLength'),
238
+ generateMessage('user', 'This is a user message that will need to be heavily truncated to fit the maxMessageTokenLength'),
239
+ ];
240
+
241
+ // Set a large target token length
242
+ const targetTokenLength = 1000;
243
+
244
+ // But set a very small maxMessageTokenLength
245
+ const maxMessageTokenLength = 5;
246
+
247
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
248
+
249
+ // All messages should be present but truncated
250
+ t.is(result.length, 2, 'Both messages should be present');
251
+
252
+ // Both messages should be truncated to fit the maxMessageTokenLength
253
+ result.forEach(msg => {
254
+ const msgTokens = modelPlugin.safeGetEncodedLength(msg.content);
255
+ t.true(msgTokens <= maxMessageTokenLength + 5,
256
+ `Message (${msgTokens} tokens) should not exceed maxMessageTokenLength (${maxMessageTokenLength}) by more than buffer`);
257
+ t.true(msg.content.includes('[...]'), 'Truncated message should have marker');
258
+ });
259
+ });
260
+
261
+ test('truncateMessagesToTargetLength: should handle both constraints together', (t) => {
262
+ const longContent = 'a'.repeat(500);
263
+
264
+ const messages = [
265
+ generateMessage('system', 'System: ' + longContent),
266
+ generateMessage('user', 'User: ' + longContent),
267
+ generateMessage('assistant', 'Assistant: ' + longContent),
268
+ generateMessage('user', 'Final: ' + longContent),
269
+ ];
270
+
271
+ // Set a moderate target token length
272
+ const targetTokenLength = 300;
273
+
274
+ // And a moderate maxMessageTokenLength
275
+ const maxMessageTokenLength = 100;
276
+
277
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
278
+
279
+ // We should have some messages, but not necessarily all
280
+ t.true(result.length > 0 && result.length <= messages.length, 'Should have some messages');
281
+
282
+ // Total token count should be below target
283
+ const totalTokens = modelPlugin.countMessagesTokens(result);
284
+ t.true(totalTokens <= targetTokenLength,
285
+ `Total tokens (${totalTokens}) should not exceed target length (${targetTokenLength})`);
286
+
287
+ // Each message should respect maxMessageTokenLength
288
+ result.forEach(msg => {
289
+ const msgTokens = modelPlugin.countMessagesTokens([msg]);
290
+ t.true(msgTokens <= maxMessageTokenLength + 10,
291
+ `Message (${msgTokens} tokens) should not exceed maxMessageTokenLength (${maxMessageTokenLength}) by more than buffer`);
292
+ });
293
+ });
90
294
 
91
- const systemMessage = result.find((message) => message.role === 'system');
92
- t.truthy(systemMessage, 'System message should not be removed');
93
- t.is(
94
- systemMessage.content,
95
- 'System message content that is very long and exceeds the target token length',
96
- 'System message content should not be altered'
97
- );
295
+ test('truncateMessagesToTargetLength: maxMessageTokenLength should not affect unchanged messages', (t) => {
296
+ const messages = [
297
+ generateMessage('system', 'Short system message'),
298
+ generateMessage('user', 'Short user message'),
299
+ ];
300
+
301
+ // Calculate tokens in each message
302
+ const systemMsgTokens = modelPlugin.countMessagesTokens([messages[0]]);
303
+ const userMsgTokens = modelPlugin.countMessagesTokens([messages[1]]);
304
+
305
+ // Set maxMessageTokenLength above individual message sizes but below their sum
306
+ const maxMessageTokenLength = Math.max(systemMsgTokens, userMsgTokens) + 10;
307
+
308
+ // Set target length to fit all messages
309
+ const targetTokenLength = modelPlugin.countMessagesTokens(messages) + 20;
310
+
311
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
312
+
313
+ // All messages should be unchanged
314
+ t.is(result.length, 2, 'Both messages should be present');
315
+ t.is(result[0].content, messages[0].content, 'First message should be unchanged');
316
+ t.is(result[1].content, messages[1].content, 'Second message should be unchanged');
317
+
318
+ // No truncation markers
319
+ const hasMarker = result.some(msg => msg.content.includes('[...]'));
320
+ t.false(hasMarker, 'No message should have truncation marker');
321
+ });
322
+
323
+ test('truncateMessagesToTargetLength: should truncate long messages with maxMessageTokenLength', t => {
324
+ const longText = 'A'.repeat(6000);
325
+ const messages = [
326
+ generateMessage('user', longText),
327
+ generateMessage('assistant', 'Response'),
328
+ generateMessage('user', 'Short message')
329
+ ];
330
+
331
+ const shortMsgTokens = modelPlugin.countMessagesTokens([{ role: 'user', content: 'Short message' }]);
332
+ const maxMessageTokenLength = shortMsgTokens * 2; // Just enough to force truncation of long messages
333
+
334
+ // Large target to ensure only maxMessageTokenLength constraint is active
335
+ const targetTokenLength = 10000;
336
+
337
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
338
+
339
+ // Check that long message was truncated
340
+ const longMsgTokens = modelPlugin.countMessagesTokens([result[0]]);
341
+ t.true(longMsgTokens <= maxMessageTokenLength + 10,
342
+ `Long message (${longMsgTokens} tokens) should be truncated to near maxMessageTokenLength (${maxMessageTokenLength})`);
343
+ t.true(result[0].content.includes('[...]'), 'Truncated message should have truncation marker');
344
+
345
+ // Short messages should be unchanged
346
+ t.is(result[1].content, 'Response');
347
+ t.is(result[2].content, 'Short message');
348
+ });
349
+
350
+ test('truncateMessagesToTargetLength: should not truncate image content with maxMessageTokenLength', t => {
351
+ const longText = 'A'.repeat(6000);
352
+ const imageContent = { type: 'image_url', url: 'image.jpg' };
353
+ const longTextContent = { type: 'text', text: longText };
354
+ const messages = [
355
+ generateMessage('user', [imageContent, longTextContent]),
356
+ generateMessage('assistant', 'I see an image')
357
+ ];
358
+
359
+ // Calculate tokens for image + some text
360
+ const imageTokens = 100; // Estimate from countMessagesTokens
361
+ const maxMessageTokenLength = imageTokens + 50; // Enough for image but not all text
362
+
363
+ // Large target to ensure only maxMessageTokenLength constraint is active
364
+ const targetTokenLength = 10000;
365
+
366
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
367
+
368
+ // Image should be preserved
369
+ t.deepEqual(result[0].content[0], imageContent, 'Image content should be preserved');
370
+
371
+ // Text should be truncated
372
+ t.true(result[0].content[1].text.length < longText.length, 'Text content should be truncated');
373
+ t.true(result[0].content[1].text.includes('[...]'), 'Truncated text should have marker');
374
+
375
+ // Check overall message length
376
+ const msgTokens = modelPlugin.countMessagesTokens([result[0]]);
377
+ t.true(msgTokens <= maxMessageTokenLength + 10,
378
+ `Message tokens (${msgTokens}) should not exceed maxMessageTokenLength (${maxMessageTokenLength}) by more than buffer`);
379
+ });
380
+
381
+ test('truncateMessagesToTargetLength: should truncate array content with maxMessageTokenLength', t => {
382
+ const longText1 = 'A'.repeat(3000);
383
+ const longText2 = 'B'.repeat(3000);
384
+ const longTextContent1 = { type: 'text', text: longText1 };
385
+ const longTextContent2 = { type: 'text', text: longText2 };
386
+ const messages = [
387
+ generateMessage('user', [longTextContent1, longTextContent2]),
388
+ generateMessage('assistant', 'Response')
389
+ ];
390
+
391
+ // Set a moderate maxMessageTokenLength
392
+ const maxMessageTokenLength = 200;
393
+
394
+ // Large target to ensure only maxMessageTokenLength constraint is active
395
+ const targetTokenLength = 10000;
396
+
397
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
398
+
399
+ // Check that message was truncated
400
+ const msgTokens = modelPlugin.countMessagesTokens([result[0]]);
401
+ t.true(msgTokens <= maxMessageTokenLength + 10,
402
+ `Message tokens (${msgTokens}) should not exceed maxMessageTokenLength (${maxMessageTokenLength}) by more than buffer`);
403
+
404
+ // At least one of the text items should be truncated
405
+ const hasMarker = result[0].content.some(item =>
406
+ typeof item === 'string' && item.includes('[...]') ||
407
+ item.type === 'text' && item.text.includes('[...]'));
408
+ t.true(hasMarker, 'At least one content item should have truncation marker');
409
+ });
410
+
411
+ test('truncateMessagesToTargetLength: should handle mixed message types with maxMessageTokenLength', t => {
412
+ const longText = 'A'.repeat(10000);
413
+ const shortText = 'Short message';
414
+ const imageContent = { type: 'image_url', url: 'image.jpg' };
415
+ const longTextContent = { type: 'text', text: longText };
416
+ const shortTextContent = { type: 'text', text: shortText };
417
+
418
+ const messages = [
419
+ generateMessage('user', shortText),
420
+ generateMessage('assistant', longText),
421
+ generateMessage('user', [shortTextContent, imageContent, longTextContent]),
422
+ generateMessage('system', longText)
423
+ ];
424
+
425
+ // Calculate reasonable maxMessageTokenLength
426
+ const shortMsgTokens = modelPlugin.countMessagesTokens([{ role: 'user', content: shortText }]);
427
+ const maxMessageTokenLength = 200; // Force truncation of long messages
428
+
429
+ // Large target to ensure only maxMessageTokenLength constraint is active
430
+ const targetTokenLength = 10000;
431
+
432
+ const result = modelPlugin.truncateMessagesToTargetLength(messages, targetTokenLength, maxMessageTokenLength);
433
+
434
+ // Short message should be unchanged
435
+ t.is(result[0].content, shortText, 'Short message should be unchanged');
436
+
437
+ // Long text messages should be truncated
438
+ t.true(result[1].content.length < longText.length, 'Long text message should be truncated');
439
+ t.true(result[1].content.includes('[...]'), 'Truncated message should have marker');
440
+ t.true(result[3].content.length < longText.length, 'Long system message should be truncated');
441
+
442
+ // Check multimodal message
443
+ t.deepEqual(result[2].content[1], imageContent, 'Image should be preserved');
444
+ if (typeof result[2].content[1] === 'string') {
445
+ t.true(result[2].content[1].length < longText.length, 'Text in multimodal message should be truncated');
446
+ } else if (result[2].content[1] && result[2].content[1].type === 'text') {
447
+ t.true(result[2].content[1].text.length < longText.length, 'Text in multimodal message should be truncated');
448
+ }
449
+
450
+ // All messages should respect maxMessageTokenLength
451
+ result.forEach((msg, i) => {
452
+ const msgTokens = modelPlugin.countMessagesTokens([msg]);
453
+ t.true(msgTokens <= maxMessageTokenLength + 10,
454
+ `Message ${i} tokens (${msgTokens}) should not exceed maxMessageTokenLength (${maxMessageTokenLength}) by more than buffer`);
455
+ });
98
456
  });
@@ -0,0 +1,146 @@
1
+ // util.test.js
2
+ // Tests for utility functions in cortex/lib/util.js
3
+
4
+ import test from 'ava';
5
+ import { removeOldImageAndFileContent } from '../lib/util.js';
6
+
7
+ // Test removeOldImageAndFileContent function
8
+
9
+ test('removeOldImageAndFileContent should return original chat history if empty', t => {
10
+ const chatHistory = [];
11
+ const result = removeOldImageAndFileContent(chatHistory);
12
+ t.deepEqual(result, chatHistory);
13
+ });
14
+
15
+ test('removeOldImageAndFileContent should return original chat history if null or undefined', t => {
16
+ t.deepEqual(removeOldImageAndFileContent(null), null);
17
+ t.deepEqual(removeOldImageAndFileContent(undefined), undefined);
18
+ });
19
+
20
+ test('removeOldImageAndFileContent should not modify chat history without image or file content', t => {
21
+ const chatHistory = [
22
+ { role: 'user', content: 'Hello' },
23
+ { role: 'assistant', content: 'Hi there!' },
24
+ { role: 'user', content: 'How are you?' }
25
+ ];
26
+ const result = removeOldImageAndFileContent(chatHistory);
27
+ t.deepEqual(result, chatHistory);
28
+ });
29
+
30
+ test('removeOldImageAndFileContent should keep only the last user message with image content', t => {
31
+ const chatHistory = [
32
+ { role: 'user', content: [{ type: 'image_url', url: 'image1.jpg' }, 'Text 1'] },
33
+ { role: 'assistant', content: 'I see image 1' },
34
+ { role: 'user', content: [{ type: 'image_url', url: 'image2.jpg' }, 'Text 2'] },
35
+ { role: 'assistant', content: 'I see image 2' }
36
+ ];
37
+
38
+ const expected = [
39
+ { role: 'user', content: ['Text 1'] },
40
+ { role: 'assistant', content: 'I see image 1' },
41
+ { role: 'user', content: [{ type: 'image_url', url: 'image2.jpg' }, 'Text 2'] },
42
+ { role: 'assistant', content: 'I see image 2' }
43
+ ];
44
+
45
+ const result = removeOldImageAndFileContent(chatHistory);
46
+ t.deepEqual(result, expected);
47
+ });
48
+
49
+ test('removeOldImageAndFileContent should handle string JSON content', t => {
50
+ const chatHistory = [
51
+ { role: 'user', content: JSON.stringify({ type: 'image_url', url: 'image1.jpg' }) },
52
+ { role: 'assistant', content: 'I see image 1' },
53
+ { role: 'user', content: JSON.stringify({ type: 'image_url', url: 'image2.jpg' }) },
54
+ { role: 'assistant', content: 'I see image 2' }
55
+ ];
56
+
57
+ const expected = [
58
+ { role: 'user', content: '' },
59
+ { role: 'assistant', content: 'I see image 1' },
60
+ { role: 'user', content: JSON.stringify({ type: 'image_url', url: 'image2.jpg' }) },
61
+ { role: 'assistant', content: 'I see image 2' }
62
+ ];
63
+
64
+ const result = removeOldImageAndFileContent(chatHistory);
65
+ t.deepEqual(result, expected);
66
+ });
67
+
68
+ test('removeOldImageAndFileContent should handle object content', t => {
69
+ const chatHistory = [
70
+ { role: 'user', content: { type: 'image_url', url: 'image1.jpg' } },
71
+ { role: 'assistant', content: 'I see image 1' },
72
+ { role: 'user', content: { type: 'image_url', url: 'image2.jpg' } },
73
+ { role: 'assistant', content: 'I see image 2' }
74
+ ];
75
+
76
+ const expected = [
77
+ { role: 'user', content: '' },
78
+ { role: 'assistant', content: 'I see image 1' },
79
+ { role: 'user', content: { type: 'image_url', url: 'image2.jpg' } },
80
+ { role: 'assistant', content: 'I see image 2' }
81
+ ];
82
+
83
+ const result = removeOldImageAndFileContent(chatHistory);
84
+ t.deepEqual(result, expected);
85
+ });
86
+
87
+ test('removeOldImageAndFileContent should handle file content', t => {
88
+ const chatHistory = [
89
+ { role: 'user', content: { type: 'file', url: 'document1.pdf' } },
90
+ { role: 'assistant', content: 'I see document 1' },
91
+ { role: 'user', content: { type: 'file', url: 'document2.pdf' } },
92
+ { role: 'assistant', content: 'I see document 2' }
93
+ ];
94
+
95
+ const expected = [
96
+ { role: 'user', content: '' },
97
+ { role: 'assistant', content: 'I see document 1' },
98
+ { role: 'user', content: { type: 'file', url: 'document2.pdf' } },
99
+ { role: 'assistant', content: 'I see document 2' }
100
+ ];
101
+
102
+ const result = removeOldImageAndFileContent(chatHistory);
103
+ t.deepEqual(result, expected);
104
+ });
105
+
106
+ test('removeOldImageAndFileContent should only process user messages', t => {
107
+ const chatHistory = [
108
+ { role: 'user', content: { type: 'image_url', url: 'image1.jpg' } },
109
+ { role: 'assistant', content: { type: 'image_url', url: 'response1.jpg' } },
110
+ { role: 'user', content: { type: 'image_url', url: 'image2.jpg' } },
111
+ { role: 'assistant', content: { type: 'image_url', url: 'response2.jpg' } }
112
+ ];
113
+
114
+ const expected = [
115
+ { role: 'user', content: '' },
116
+ { role: 'assistant', content: { type: 'image_url', url: 'response1.jpg' } },
117
+ { role: 'user', content: { type: 'image_url', url: 'image2.jpg' } },
118
+ { role: 'assistant', content: { type: 'image_url', url: 'response2.jpg' } }
119
+ ];
120
+
121
+ const result = removeOldImageAndFileContent(chatHistory);
122
+ t.deepEqual(result, expected);
123
+ });
124
+
125
+ test('removeOldImageAndFileContent should handle mixed content types', t => {
126
+ const chatHistory = [
127
+ { role: 'user', content: [{ type: 'image_url', url: 'image1.jpg' }, 'Text 1'] },
128
+ { role: 'assistant', content: 'I see image 1' },
129
+ { role: 'user', content: 'Just text' },
130
+ { role: 'assistant', content: 'I see text' },
131
+ { role: 'user', content: [{ type: 'file', url: 'document.pdf' }, 'Text 2'] },
132
+ { role: 'assistant', content: 'I see document' }
133
+ ];
134
+
135
+ const expected = [
136
+ { role: 'user', content: ['Text 1'] },
137
+ { role: 'assistant', content: 'I see image 1' },
138
+ { role: 'user', content: 'Just text' },
139
+ { role: 'assistant', content: 'I see text' },
140
+ { role: 'user', content: [{ type: 'file', url: 'document.pdf' }, 'Text 2'] },
141
+ { role: 'assistant', content: 'I see document' }
142
+ ];
143
+
144
+ const result = removeOldImageAndFileContent(chatHistory);
145
+ t.deepEqual(result, expected);
146
+ });