diff --git a/anthropic/anthropic.go b/anthropic/anthropic.go index bd1465792..f4d9a6e23 100644 --- a/anthropic/anthropic.go +++ b/anthropic/anthropic.go @@ -78,12 +78,14 @@ type MessageParam struct { Content any `json:"content"` // string or []ContentBlock } -// ContentBlock represents a content block in a message +// ContentBlock represents a content block in a message. +// Text and Thinking use pointers so they serialize as the field being present (even if empty) +// only when set, which is required for SDK streaming accumulation. type ContentBlock struct { Type string `json:"type"` // text, image, tool_use, tool_result, thinking - // For text blocks (no omitempty - SDK requires field to be present for accumulation) - Text string `json:"text"` + // For text blocks - pointer so field only appears when set (SDK requires it for accumulation) + Text *string `json:"text,omitempty"` // For image blocks Source *ImageSource `json:"source,omitempty"` @@ -98,9 +100,9 @@ type ContentBlock struct { Content any `json:"content,omitempty"` // string or []ContentBlock IsError bool `json:"is_error,omitempty"` - // For thinking blocks (no omitempty - SDK requires field to be present for accumulation) - Thinking string `json:"thinking"` - Signature string `json:"signature,omitempty"` + // For thinking blocks - pointer so field only appears when set (SDK requires it for accumulation) + Thinking *string `json:"thinking,omitempty"` + Signature string `json:"signature,omitempty"` } // ImageSource represents the source of an image @@ -458,14 +460,14 @@ func ToMessagesResponse(id string, r api.ChatResponse) MessagesResponse { if r.Message.Thinking != "" { content = append(content, ContentBlock{ Type: "thinking", - Thinking: r.Message.Thinking, + Thinking: ptr(r.Message.Thinking), }) } if r.Message.Content != "" { content = append(content, ContentBlock{ Type: "text", - Text: r.Message.Content, + Text: ptr(r.Message.Content), }) } @@ -579,7 +581,7 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent { Index: c.contentIndex, ContentBlock: ContentBlock{ Type: "thinking", - Thinking: "", + Thinking: ptr(""), }, }, }) @@ -620,7 +622,7 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent { Index: c.contentIndex, ContentBlock: ContentBlock{ Type: "text", - Text: "", + Text: ptr(""), }, }, }) @@ -760,3 +762,8 @@ func generateID(prefix string) string { func GenerateMessageID() string { return generateID("msg") } + +// ptr returns a pointer to the given string value +func ptr(s string) *string { + return &s +} diff --git a/anthropic/anthropic_test.go b/anthropic/anthropic_test.go index 3cebe6710..8228bd37b 100644 --- a/anthropic/anthropic_test.go +++ b/anthropic/anthropic_test.go @@ -447,7 +447,7 @@ func TestToMessagesResponse_Basic(t *testing.T) { if len(result.Content) != 1 { t.Fatalf("expected 1 content block, got %d", len(result.Content)) } - if result.Content[0].Type != "text" || result.Content[0].Text != "Hello there!" { + if result.Content[0].Type != "text" || result.Content[0].Text == nil || *result.Content[0].Text != "Hello there!" { t.Errorf("unexpected content: %+v", result.Content[0]) } if result.StopReason != "end_turn" { @@ -516,8 +516,8 @@ func TestToMessagesResponse_WithThinking(t *testing.T) { if result.Content[0].Type != "thinking" { t.Errorf("expected first block type 'thinking', got %q", result.Content[0].Type) } - if result.Content[0].Thinking != "Let me think about this..." { - t.Errorf("unexpected thinking content: %q", result.Content[0].Thinking) + if result.Content[0].Thinking == nil || *result.Content[0].Thinking != "Let me think about this..." { + t.Errorf("unexpected thinking content: %v", result.Content[0].Thinking) } if result.Content[1].Type != "text" { t.Errorf("expected second block type 'text', got %q", result.Content[1].Type) @@ -825,7 +825,7 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) { name: "text block includes empty text field", block: ContentBlock{ Type: "text", - Text: "", + Text: ptr(""), }, wantKeys: []string{"type", "text"}, }, @@ -833,7 +833,7 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) { name: "thinking block includes empty thinking field", block: ContentBlock{ Type: "thinking", - Thinking: "", + Thinking: ptr(""), }, wantKeys: []string{"type", "thinking"}, }, @@ -841,7 +841,7 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) { name: "text block with content", block: ContentBlock{ Type: "text", - Text: "hello", + Text: ptr("hello"), }, wantKeys: []string{"type", "text"}, },