♻️ refactor: formatContentStrings to support AI and System messages (#8528)

* ♻️ refactor: `formatContentStrings` to support AI and System messages

* 📦 chore: bump @librechat/api version to 1.2.7
This commit is contained in:
Danny Avila 2025-07-17 19:19:37 -04:00 committed by GitHub
parent cf59f1ab45
commit 0bf708915b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 58 deletions

2
package-lock.json generated
View file

@ -46467,7 +46467,7 @@
}, },
"packages/api": { "packages/api": {
"name": "@librechat/api", "name": "@librechat/api",
"version": "1.2.6", "version": "1.2.7",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.21.5", "@babel/preset-env": "^7.21.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/api", "name": "@librechat/api",
"version": "1.2.6", "version": "1.2.7",
"type": "commonjs", "type": "commonjs",
"description": "MCP services for LibreChat", "description": "MCP services for LibreChat",
"main": "dist/index.js", "main": "dist/index.js",

View file

@ -100,8 +100,8 @@ describe('formatContentStrings', () => {
}); });
}); });
describe('Non-human messages', () => { describe('AI messages', () => {
it('should not modify AI message content', () => { it('should convert AI message with all text blocks to string', () => {
const messages = [ const messages = [
new AIMessage({ new AIMessage({
content: [ content: [
@ -114,13 +114,32 @@ describe('formatContentStrings', () => {
const result = formatContentStrings(messages); const result = formatContentStrings(messages);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].content).toEqual([ expect(result[0].content).toBe('Hello\nWorld');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello' }, expect(result[0].getType()).toBe('ai');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'World' },
]);
}); });
it('should not modify System message content', () => { it('should not convert AI message with mixed content types', () => {
const messages = [
new AIMessage({
content: [
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here is an image' },
{ type: ContentTypes.TOOL_CALL, tool_call: { name: 'generate_image' } },
],
}),
];
const result = formatContentStrings(messages);
expect(result).toHaveLength(1);
expect(result[0].content).toEqual([
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here is an image' },
{ type: ContentTypes.TOOL_CALL, tool_call: { name: 'generate_image' } },
]);
});
});
describe('System messages', () => {
it('should convert System message with all text blocks to string', () => {
const messages = [ const messages = [
new SystemMessage({ new SystemMessage({
content: [ content: [
@ -133,15 +152,13 @@ describe('formatContentStrings', () => {
const result = formatContentStrings(messages); const result = formatContentStrings(messages);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].content).toEqual([ expect(result[0].content).toBe('System\nMessage');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'System' }, expect(result[0].getType()).toBe('system');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Message' },
]);
}); });
}); });
describe('Mixed message types', () => { describe('Mixed message types', () => {
it('should only process human messages in mixed array', () => { it('should process all valid message types in mixed array', () => {
const messages = [ const messages = [
new HumanMessage({ new HumanMessage({
content: [ content: [
@ -166,18 +183,15 @@ describe('formatContentStrings', () => {
const result = formatContentStrings(messages); const result = formatContentStrings(messages);
expect(result).toHaveLength(3); expect(result).toHaveLength(3);
// Human message should be converted // All messages should be converted
expect(result[0].content).toBe('Human\nMessage'); expect(result[0].content).toBe('Human\nMessage');
// AI message should remain unchanged expect(result[0].getType()).toBe('human');
expect(result[1].content).toEqual([
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'AI' }, expect(result[1].content).toBe('AI\nResponse');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Response' }, expect(result[1].getType()).toBe('ai');
]);
// System message should remain unchanged expect(result[2].content).toBe('System\nPrompt');
expect(result[2].content).toEqual([ expect(result[2].getType()).toBe('system');
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'System' },
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Prompt' },
]);
}); });
}); });
@ -215,24 +229,6 @@ describe('formatContentStrings', () => {
expect(result).toHaveLength(1); expect(result).toHaveLength(1);
expect(result[0].content).toBe('Hello \n World'); expect(result[0].content).toBe('Hello \n World');
}); });
it('should not modify the original messages array', () => {
const messages = [
new HumanMessage({
content: [
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello' },
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'World' },
],
}),
];
const originalContent = [
...(messages[0].content as Array<{ type: string; [key: string]: unknown }>),
];
formatContentStrings(messages);
expect(messages[0].content).toEqual(originalContent);
});
}); });
describe('Real-world scenarios', () => { describe('Real-world scenarios', () => {
@ -277,14 +273,11 @@ describe('formatContentStrings', () => {
// First human message (all text) should be converted // First human message (all text) should be converted
expect(result[0].content).toBe('hi there'); expect(result[0].content).toBe('hi there');
expect(result[0].getType()).toBe('human');
// AI message should remain unchanged // AI message (all text) should now also be converted
expect(result[1].content).toEqual([ expect(result[1].content).toBe('Hi Danny! How can I help you today?');
{ expect(result[1].getType()).toBe('ai');
type: 'text',
text: 'Hi Danny! How can I help you today?',
},
]);
// Third message (mixed content) should remain unchanged // Third message (mixed content) should remain unchanged
expect(result[2].content).toEqual([ expect(result[2].content).toEqual([
@ -302,7 +295,7 @@ describe('formatContentStrings', () => {
]); ]);
}); });
it('should handle human messages with tool calls', () => { it('should handle messages with tool calls', () => {
const messages = [ const messages = [
new HumanMessage({ new HumanMessage({
content: [ content: [
@ -313,11 +306,20 @@ describe('formatContentStrings', () => {
}, },
], ],
}), }),
new AIMessage({
content: [
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'I will calculate that for you' },
{
type: ContentTypes.TOOL_CALL,
tool_call: { name: 'calculator', args: '{"a": 1, "b": 2}' },
},
],
}),
]; ];
const result = formatContentStrings(messages); const result = formatContentStrings(messages);
expect(result).toHaveLength(1); expect(result).toHaveLength(2);
// Should not convert because not all blocks are text // Should not convert because not all blocks are text
expect(result[0].content).toEqual([ expect(result[0].content).toEqual([
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Please use the calculator' }, { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Please use the calculator' },
@ -326,6 +328,13 @@ describe('formatContentStrings', () => {
tool_call: { name: 'calculator', args: '{"a": 1, "b": 2}' }, tool_call: { name: 'calculator', args: '{"a": 1, "b": 2}' },
}, },
]); ]);
expect(result[1].content).toEqual([
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'I will calculate that for you' },
{
type: ContentTypes.TOOL_CALL,
tool_call: { name: 'calculator', args: '{"a": 1, "b": 2}' },
},
]);
}); });
}); });
}); });

View file

@ -1,5 +1,4 @@
import { ContentTypes } from 'librechat-data-provider'; import { ContentTypes } from 'librechat-data-provider';
import { HumanMessage } from '@langchain/core/messages';
import type { BaseMessage } from '@langchain/core/messages'; import type { BaseMessage } from '@langchain/core/messages';
/** /**
@ -13,10 +12,10 @@ export const formatContentStrings = (payload: Array<BaseMessage>): Array<BaseMes
for (const message of payload) { for (const message of payload) {
const messageType = message.getType(); const messageType = message.getType();
const isHumanMessage = messageType === 'human'; const isValidMessage =
messageType === 'human' || messageType === 'ai' || messageType === 'system';
// Skip non-human messages - add them as-is if (!isValidMessage) {
if (!isHumanMessage) {
result.push(message); result.push(message);
continue; continue;
} }
@ -50,8 +49,8 @@ export const formatContentStrings = (payload: Array<BaseMessage>): Array<BaseMes
return acc; return acc;
}, ''); }, '');
const clonedMessage = new HumanMessage(content.trim()); message.content = content.trim();
result.push(clonedMessage); result.push(message);
} }
return result; return result;