@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.
- package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +1 -1
- package/lib/encodeCache.js +22 -10
- package/lib/pathwayTools.js +10 -3
- package/lib/requestExecutor.js +1 -1
- package/lib/util.js +136 -1
- package/package.json +2 -2
- package/pathways/system/entity/memory/sys_memory_manager.js +2 -1
- package/pathways/system/entity/sys_entity_continue.js +10 -2
- package/pathways/system/entity/sys_entity_start.js +12 -10
- package/pathways/system/entity/sys_router_tool.js +2 -2
- package/server/chunker.js +23 -3
- package/server/pathwayResolver.js +2 -5
- package/server/plugins/claude3VertexPlugin.js +2 -3
- package/server/plugins/cohereGeneratePlugin.js +1 -1
- package/server/plugins/gemini15ChatPlugin.js +1 -1
- package/server/plugins/geminiChatPlugin.js +1 -1
- package/server/plugins/localModelPlugin.js +1 -1
- package/server/plugins/modelPlugin.js +332 -77
- package/server/plugins/openAiChatPlugin.js +1 -1
- package/server/plugins/openAiCompletionPlugin.js +1 -1
- package/server/plugins/palmChatPlugin.js +1 -1
- package/server/plugins/palmCodeCompletionPlugin.js +1 -1
- package/server/plugins/palmCompletionPlugin.js +1 -1
- package/tests/chunkfunction.test.js +9 -6
- package/tests/claude3VertexPlugin.test.js +81 -3
- package/tests/data/largecontent.txt +1 -0
- package/tests/data/mixedcontent.txt +1 -0
- package/tests/encodeCache.test.js +47 -14
- package/tests/modelPlugin.test.js +21 -0
- package/tests/multimodal_conversion.test.js +1 -1
- package/tests/subscription.test.js +7 -1
- package/tests/tokenHandlingTests.test.js +587 -0
- package/tests/truncateMessages.test.js +404 -46
- 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 =
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
91
|
+
test('truncateMessagesToTargetLength: should not add truncation markers to messages that fit completely', (t) => {
|
|
49
92
|
const messages = [
|
|
50
|
-
generateMessage('
|
|
51
|
-
generateMessage('
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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('
|
|
63
|
-
generateMessage('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
173
|
+
test('truncateMessagesToTargetLength: should handle structured messages with maxMessageTokenLength=0', (t) => {
|
|
82
174
|
const messages = [
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
});
|