Skip to content

Commit 539753a

Browse files
arvinxxclaude
andauthored
🐛 fix(model-runtime): handle null content in anthropic message builder (lobehub#11756)
* 🐛 fix(model-runtime): handle null content in anthropic message builder Fix TypeError when building Anthropic messages with null content: - Handle assistant messages with tool_calls but null content - Handle tool messages with null or empty string content - Use '<empty_content>' placeholder for null/empty content Add 3 test cases covering the null content scenarios. Closes: LOBE-4201, LOBE-2715 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 🐛 fix(model-runtime): handle array content in tool messages Tool messages may have array content, not just string. Use buildArrayContent to properly process array content in tool results. Add test case for tool message with array content. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 🐛 fix(model-runtime): filter out null/empty content in assistant messages When assistant message has tool_calls but null/empty content, filter out the empty text block instead of using placeholder. Only tool_use blocks remain in the content array. Add test case for empty string content scenario. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✅ test(model-runtime): add test case for tool message with image content Add test case to verify tool message with array content containing both text and image is correctly processed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ✅ test(model-runtime): add tests for orphan tool message with null/empty content Add test cases for tool messages without corresponding assistant tool_call when content is null or empty string. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fdc8f95 commit 539753a

File tree

2 files changed

+388
-5
lines changed

2 files changed

+388
-5
lines changed

packages/model-runtime/src/core/contextBuilders/anthropic.test.ts

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,62 @@ describe('anthropicHelpers', () => {
281281
const result = await buildAnthropicMessage(message);
282282
expect(result).toBeUndefined();
283283
});
284+
285+
it('should handle assistant message with tool_calls but null content', async () => {
286+
const message: OpenAIChatMessage = {
287+
content: null as any,
288+
role: 'assistant',
289+
tool_calls: [
290+
{
291+
id: 'call1',
292+
type: 'function',
293+
function: {
294+
name: 'search_people',
295+
arguments: '{"location":"Singapore"}',
296+
},
297+
},
298+
],
299+
};
300+
const result = await buildAnthropicMessage(message);
301+
expect(result!.role).toBe('assistant');
302+
// null content should be filtered out, only tool_use remains
303+
expect(result!.content).toEqual([
304+
{
305+
id: 'call1',
306+
input: { location: 'Singapore' },
307+
name: 'search_people',
308+
type: 'tool_use',
309+
},
310+
]);
311+
});
312+
313+
it('should handle assistant message with tool_calls but empty string content', async () => {
314+
const message: OpenAIChatMessage = {
315+
content: '',
316+
role: 'assistant',
317+
tool_calls: [
318+
{
319+
id: 'call1',
320+
type: 'function',
321+
function: {
322+
name: 'search_people',
323+
arguments: '{"location":"Singapore"}',
324+
},
325+
},
326+
],
327+
};
328+
const result = await buildAnthropicMessage(message);
329+
expect(result!.role).toBe('assistant');
330+
// empty string content should be filtered out, only tool_use remains
331+
expect(result!.content).toEqual([
332+
{
333+
id: 'call1',
334+
input: { location: 'Singapore' },
335+
name: 'search_people',
336+
type: 'tool_use',
337+
},
338+
]);
339+
});
284340
});
285341

286342
describe('buildAnthropicMessages', () => {
@@ -526,6 +582,320 @@ describe('anthropicHelpers', () => {
526582
]);
527583
});
528584

585+
it('should handle tool message with null content', async () => {
586+
const messages: OpenAIChatMessage[] = [
587+
{
588+
content: '搜索人员',
589+
role: 'user',
590+
},
591+
{
592+
content: '正在搜索...',
593+
role: 'assistant',
594+
tool_calls: [
595+
{
596+
function: {
597+
arguments: '{"location": "Singapore"}',
598+
name: 'search_people',
599+
},
600+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
601+
type: 'function',
602+
},
603+
],
604+
},
605+
{
606+
content: null as any,
607+
name: 'search_people',
608+
role: 'tool',
609+
tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
610+
},
611+
];
612+
613+
const contents = await buildAnthropicMessages(messages);
614+
615+
expect(contents).toEqual([
616+
{ content: '搜索人员', role: 'user' },
617+
{
618+
content: [
619+
{ text: '正在搜索...', type: 'text' },
620+
{
621+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
622+
input: { location: 'Singapore' },
623+
name: 'search_people',
624+
type: 'tool_use',
625+
},
626+
],
627+
role: 'assistant',
628+
},
629+
{
630+
content: [
631+
{
632+
content: [{ text: '<empty_content>', type: 'text' }],
633+
tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
634+
type: 'tool_result',
635+
},
636+
],
637+
role: 'user',
638+
},
639+
]);
640+
});
641+
642+
it('should handle tool message with empty string content', async () => {
643+
const messages: OpenAIChatMessage[] = [
644+
{
645+
content: '搜索人员',
646+
role: 'user',
647+
},
648+
{
649+
content: '正在搜索...',
650+
role: 'assistant',
651+
tool_calls: [
652+
{
653+
function: {
654+
arguments: '{"location": "Singapore"}',
655+
name: 'search_people',
656+
},
657+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
658+
type: 'function',
659+
},
660+
],
661+
},
662+
{
663+
content: '',
664+
name: 'search_people',
665+
role: 'tool',
666+
tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
667+
},
668+
];
669+
670+
const contents = await buildAnthropicMessages(messages);
671+
672+
expect(contents).toEqual([
673+
{ content: '搜索人员', role: 'user' },
674+
{
675+
content: [
676+
{ text: '正在搜索...', type: 'text' },
677+
{
678+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
679+
input: { location: 'Singapore' },
680+
name: 'search_people',
681+
type: 'tool_use',
682+
},
683+
],
684+
role: 'assistant',
685+
},
686+
{
687+
content: [
688+
{
689+
content: [{ text: '<empty_content>', type: 'text' }],
690+
tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
691+
type: 'tool_result',
692+
},
693+
],
694+
role: 'user',
695+
},
696+
]);
697+
});
698+
699+
it('should handle tool message with array content', async () => {
700+
const messages: OpenAIChatMessage[] = [
701+
{
702+
content: '搜索人员',
703+
role: 'user',
704+
},
705+
{
706+
content: '正在搜索...',
707+
role: 'assistant',
708+
tool_calls: [
709+
{
710+
function: {
711+
arguments: '{"location": "Singapore"}',
712+
name: 'search_people',
713+
},
714+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
715+
type: 'function',
716+
},
717+
],
718+
},
719+
{
720+
content: [
721+
{ type: 'text', text: 'Found 5 candidates' },
722+
{ type: 'text', text: 'Result details here' },
723+
] as any,
724+
name: 'search_people',
725+
role: 'tool',
726+
tool_call_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
727+
},
728+
];
729+
730+
const contents = await buildAnthropicMessages(messages);
731+
732+
expect(contents).toEqual([
733+
{ content: '搜索人员', role: 'user' },
734+
{
735+
content: [
736+
{ text: '正在搜索...', type: 'text' },
737+
{
738+
id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
739+
input: { location: 'Singapore' },
740+
name: 'search_people',
741+
type: 'tool_use',
742+
},
743+
],
744+
role: 'assistant',
745+
},
746+
{
747+
content: [
748+
{
749+
content: [
750+
{ type: 'text', text: 'Found 5 candidates' },
751+
{ type: 'text', text: 'Result details here' },
752+
],
753+
tool_use_id: 'toolu_01CnXPcBEqsGGbvRriem3Rth',
754+
type: 'tool_result',
755+
},
756+
],
757+
role: 'user',
758+
},
759+
]);
760+
});
761+
762+
it('should handle tool message with array content containing image', async () => {
763+
vi.mocked(parseDataUri).mockReturnValueOnce({
764+
mimeType: 'image/png',
765+
base64: 'screenshotBase64Data',
766+
type: 'base64',
767+
});
768+
769+
const messages: OpenAIChatMessage[] = [
770+
{
771+
content: '截图分析',
772+
role: 'user',
773+
},
774+
{
775+
content: '正在截图...',
776+
role: 'assistant',
777+
tool_calls: [
778+
{
779+
function: {
780+
arguments: '{"url": "https://example.com"}',
781+
name: 'screenshot',
782+
},
783+
id: 'toolu_screenshot_123',
784+
type: 'function',
785+
},
786+
],
787+
},
788+
{
789+
content: [
790+
{ type: 'text', text: 'Screenshot captured' },
791+
{
792+
type: 'image_url',
793+
image_url: { url: 'data:image/png;base64,screenshotBase64Data' },
794+
},
795+
] as any,
796+
name: 'screenshot',
797+
role: 'tool',
798+
tool_call_id: 'toolu_screenshot_123',
799+
},
800+
];
801+
802+
const contents = await buildAnthropicMessages(messages);
803+
804+
expect(contents).toEqual([
805+
{ content: '截图分析', role: 'user' },
806+
{
807+
content: [
808+
{ text: '正在截图...', type: 'text' },
809+
{
810+
id: 'toolu_screenshot_123',
811+
input: { url: 'https://example.com' },
812+
name: 'screenshot',
813+
type: 'tool_use',
814+
},
815+
],
816+
role: 'assistant',
817+
},
818+
{
819+
content: [
820+
{
821+
content: [
822+
{ type: 'text', text: 'Screenshot captured' },
823+
{
824+
type: 'image',
825+
source: {
826+
type: 'base64',
827+
media_type: 'image/png',
828+
data: 'screenshotBase64Data',
829+
},
830+
},
831+
],
832+
tool_use_id: 'toolu_screenshot_123',
833+
type: 'tool_result',
834+
},
835+
],
836+
role: 'user',
837+
},
838+
]);
839+
});
840+
841+
it('should handle orphan tool message with null content', async () => {
842+
// Tool message without corresponding assistant tool_call
843+
const messages: OpenAIChatMessage[] = [
844+
{
845+
content: null as any,
846+
name: 'some_tool',
847+
role: 'tool',
848+
tool_call_id: 'orphan_tool_call_id',
849+
},
850+
{
851+
content: 'Continue',
852+
role: 'user',
853+
},
854+
];
855+
856+
const contents = await buildAnthropicMessages(messages);
857+
858+
expect(contents).toEqual([
859+
{
860+
content: '<empty_content>',
861+
role: 'user',
862+
},
863+
{
864+
content: 'Continue',
865+
role: 'user',
866+
},
867+
]);
868+
});
869+
870+
it('should handle orphan tool message with empty string content', async () => {
871+
// Tool message without corresponding assistant tool_call
872+
const messages: OpenAIChatMessage[] = [
873+
{
874+
content: '',
875+
name: 'some_tool',
876+
role: 'tool',
877+
tool_call_id: 'orphan_tool_call_id',
878+
},
879+
{
880+
content: 'Continue',
881+
role: 'user',
882+
},
883+
];
884+
885+
const contents = await buildAnthropicMessages(messages);
886+
887+
expect(contents).toEqual([
888+
{
889+
content: '<empty_content>',
890+
role: 'user',
891+
},
892+
{
893+
content: 'Continue',
894+
role: 'user',
895+
},
896+
]);
897+
});
898+
529899
it('should work well starting with tool message', async () => {
530900
const messages: OpenAIChatMessage[] = [
531901
{

0 commit comments

Comments
 (0)