Skip to content

Commit e27e9a5

Browse files
authored
Merge pull request #1288 from Weaxs/main
support merge ChatCompletionContentPart && add filterEmptyMessages
2 parents 2578d8c + d2bc46c commit e27e9a5

2 files changed

Lines changed: 245 additions & 8 deletions

File tree

packages/core/src/core/openaiContentGenerator/converter.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,4 +542,206 @@ describe('OpenAIContentConverter', () => {
542542
expect(original).toEqual(originalCopy);
543543
});
544544
});
545+
546+
describe('mergeConsecutiveAssistantMessages', () => {
547+
it('should merge two consecutive assistant messages with string content', () => {
548+
const request: GenerateContentParameters = {
549+
model: 'models/test',
550+
contents: [
551+
{
552+
role: 'model',
553+
parts: [{ text: 'First part' }],
554+
},
555+
{
556+
role: 'model',
557+
parts: [{ text: 'Second part' }],
558+
},
559+
],
560+
};
561+
562+
const messages = converter.convertGeminiRequestToOpenAI(request);
563+
564+
expect(messages).toHaveLength(1);
565+
expect(messages[0].role).toBe('assistant');
566+
const content = messages[0]
567+
.content as OpenAI.Chat.ChatCompletionContentPart[];
568+
expect(content).toHaveLength(2);
569+
expect(content[0]).toEqual({ type: 'text', text: 'First part' });
570+
expect(content[1]).toEqual({ type: 'text', text: 'Second part' });
571+
});
572+
573+
it('should merge multiple consecutive assistant messages', () => {
574+
const request: GenerateContentParameters = {
575+
model: 'models/test',
576+
contents: [
577+
{
578+
role: 'model',
579+
parts: [{ text: 'Part 1' }],
580+
},
581+
{
582+
role: 'model',
583+
parts: [{ text: 'Part 2' }],
584+
},
585+
{
586+
role: 'model',
587+
parts: [{ text: 'Part 3' }],
588+
},
589+
],
590+
};
591+
592+
const messages = converter.convertGeminiRequestToOpenAI(request);
593+
594+
expect(messages).toHaveLength(1);
595+
expect(messages[0].role).toBe('assistant');
596+
const content = messages[0]
597+
.content as OpenAI.Chat.ChatCompletionContentPart[];
598+
expect(content).toHaveLength(3);
599+
});
600+
601+
it('should merge tool_calls from consecutive assistant messages', () => {
602+
const request: GenerateContentParameters = {
603+
model: 'models/test',
604+
contents: [
605+
{
606+
role: 'model',
607+
parts: [
608+
{
609+
functionCall: {
610+
id: 'call_1',
611+
name: 'tool_1',
612+
args: {},
613+
},
614+
},
615+
],
616+
},
617+
{
618+
role: 'user',
619+
parts: [
620+
{
621+
functionResponse: {
622+
id: 'call_1',
623+
name: 'tool_1',
624+
response: { output: 'result_1' },
625+
},
626+
},
627+
],
628+
},
629+
{
630+
role: 'model',
631+
parts: [
632+
{
633+
functionCall: {
634+
id: 'call_2',
635+
name: 'tool_2',
636+
args: {},
637+
},
638+
},
639+
],
640+
},
641+
{
642+
role: 'user',
643+
parts: [
644+
{
645+
functionResponse: {
646+
id: 'call_2',
647+
name: 'tool_2',
648+
response: { output: 'result_2' },
649+
},
650+
},
651+
],
652+
},
653+
],
654+
};
655+
656+
const messages = converter.convertGeminiRequestToOpenAI(request);
657+
658+
// Should have: assistant (tool_call_1), tool (result_1), assistant (tool_call_2), tool (result_2)
659+
expect(messages).toHaveLength(4);
660+
expect(messages[0].role).toBe('assistant');
661+
expect(messages[1].role).toBe('tool');
662+
expect(messages[2].role).toBe('assistant');
663+
expect(messages[3].role).toBe('tool');
664+
});
665+
666+
it('should not merge assistant messages separated by user messages', () => {
667+
const request: GenerateContentParameters = {
668+
model: 'models/test',
669+
contents: [
670+
{
671+
role: 'model',
672+
parts: [{ text: 'First assistant' }],
673+
},
674+
{
675+
role: 'user',
676+
parts: [{ text: 'User message' }],
677+
},
678+
{
679+
role: 'model',
680+
parts: [{ text: 'Second assistant' }],
681+
},
682+
],
683+
};
684+
685+
const messages = converter.convertGeminiRequestToOpenAI(request);
686+
687+
expect(messages).toHaveLength(3);
688+
expect(messages[0].role).toBe('assistant');
689+
expect(messages[1].role).toBe('user');
690+
expect(messages[2].role).toBe('assistant');
691+
});
692+
693+
it('should handle merging when one message has array content and another has string', () => {
694+
const request: GenerateContentParameters = {
695+
model: 'models/test',
696+
contents: [
697+
{
698+
role: 'model',
699+
parts: [{ text: 'Text part' }],
700+
},
701+
{
702+
role: 'model',
703+
parts: [{ text: 'Another text' }],
704+
},
705+
],
706+
};
707+
708+
const messages = converter.convertGeminiRequestToOpenAI(request);
709+
710+
expect(messages).toHaveLength(1);
711+
const content = messages[0]
712+
.content as OpenAI.Chat.ChatCompletionContentPart[];
713+
expect(Array.isArray(content)).toBe(true);
714+
expect(content).toHaveLength(2);
715+
});
716+
717+
it('should merge empty content correctly', () => {
718+
const request: GenerateContentParameters = {
719+
model: 'models/test',
720+
contents: [
721+
{
722+
role: 'model',
723+
parts: [{ text: 'First' }],
724+
},
725+
{
726+
role: 'model',
727+
parts: [],
728+
},
729+
{
730+
role: 'model',
731+
parts: [{ text: 'Second' }],
732+
},
733+
],
734+
};
735+
736+
const messages = converter.convertGeminiRequestToOpenAI(request);
737+
738+
// Empty messages should be filtered out
739+
expect(messages).toHaveLength(1);
740+
const content = messages[0]
741+
.content as OpenAI.Chat.ChatCompletionContentPart[];
742+
expect(content).toHaveLength(2);
743+
expect(content[0]).toEqual({ type: 'text', text: 'First' });
744+
expect(content[1]).toEqual({ type: 'text', text: 'Second' });
745+
});
746+
});
545747
});

packages/core/src/core/openaiContentGenerator/converter.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,12 +1120,44 @@ export class OpenAIContentConverter {
11201120
// If the last message is also an assistant message, merge them
11211121
if (lastMessage.role === 'assistant') {
11221122
// Combine content
1123-
const combinedContent = [
1124-
typeof lastMessage.content === 'string' ? lastMessage.content : '',
1125-
typeof message.content === 'string' ? message.content : '',
1126-
]
1127-
.filter(Boolean)
1128-
.join('');
1123+
const lastContent = lastMessage.content;
1124+
const currentContent = message.content;
1125+
1126+
// Determine if we should use array format (if either content is an array)
1127+
const useArrayFormat =
1128+
Array.isArray(lastContent) || Array.isArray(currentContent);
1129+
1130+
let combinedContent:
1131+
| string
1132+
| OpenAI.Chat.ChatCompletionContentPart[]
1133+
| null;
1134+
1135+
if (useArrayFormat) {
1136+
// Convert both to array format and merge
1137+
const lastParts = Array.isArray(lastContent)
1138+
? lastContent
1139+
: typeof lastContent === 'string' && lastContent
1140+
? [{ type: 'text' as const, text: lastContent }]
1141+
: [];
1142+
1143+
const currentParts = Array.isArray(currentContent)
1144+
? currentContent
1145+
: typeof currentContent === 'string' && currentContent
1146+
? [{ type: 'text' as const, text: currentContent }]
1147+
: [];
1148+
1149+
combinedContent = [
1150+
...lastParts,
1151+
...currentParts,
1152+
] as OpenAI.Chat.ChatCompletionContentPart[];
1153+
} else {
1154+
// Both are strings or null, merge as strings
1155+
const lastText = typeof lastContent === 'string' ? lastContent : '';
1156+
const currentText =
1157+
typeof currentContent === 'string' ? currentContent : '';
1158+
const mergedText = [lastText, currentText].filter(Boolean).join('');
1159+
combinedContent = mergedText || null;
1160+
}
11291161

11301162
// Combine tool calls
11311163
const lastToolCalls =
@@ -1137,14 +1169,17 @@ export class OpenAIContentConverter {
11371169
// Update the last message with combined data
11381170
(
11391171
lastMessage as OpenAI.Chat.ChatCompletionMessageParam & {
1140-
content: string | null;
1172+
content: string | OpenAI.Chat.ChatCompletionContentPart[] | null;
11411173
tool_calls?: OpenAI.Chat.ChatCompletionMessageToolCall[];
11421174
}
11431175
).content = combinedContent || null;
11441176
if (combinedToolCalls.length > 0) {
11451177
(
11461178
lastMessage as OpenAI.Chat.ChatCompletionMessageParam & {
1147-
content: string | null;
1179+
content:
1180+
| string
1181+
| OpenAI.Chat.ChatCompletionContentPart[]
1182+
| null;
11481183
tool_calls?: OpenAI.Chat.ChatCompletionMessageToolCall[];
11491184
}
11501185
).tool_calls = combinedToolCalls;

0 commit comments

Comments
 (0)