anthropic: use pointer types for Text and Thinking fields
Use *string instead of string for Text and Thinking fields in ContentBlock
so that omitempty works correctly:
- nil pointer: field omitted from JSON (for blocks that don't use it)
- ptr(""): field present as "" (for SDK streaming accumulation)
- ptr("content"): field present with content
This keeps the JSON output clean (text blocks don't have thinking field,
thinking blocks don't have text field) while still satisfying SDK
requirements for field presence during streaming.
This commit is contained in:
parent
6188e90aab
commit
fa42204da8
|
|
@ -78,12 +78,14 @@ type MessageParam struct {
|
||||||
Content any `json:"content"` // string or []ContentBlock
|
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 ContentBlock struct {
|
||||||
Type string `json:"type"` // text, image, tool_use, tool_result, thinking
|
Type string `json:"type"` // text, image, tool_use, tool_result, thinking
|
||||||
|
|
||||||
// For text blocks (no omitempty - SDK requires field to be present for accumulation)
|
// For text blocks - pointer so field only appears when set (SDK requires it for accumulation)
|
||||||
Text string `json:"text"`
|
Text *string `json:"text,omitempty"`
|
||||||
|
|
||||||
// For image blocks
|
// For image blocks
|
||||||
Source *ImageSource `json:"source,omitempty"`
|
Source *ImageSource `json:"source,omitempty"`
|
||||||
|
|
@ -98,9 +100,9 @@ type ContentBlock struct {
|
||||||
Content any `json:"content,omitempty"` // string or []ContentBlock
|
Content any `json:"content,omitempty"` // string or []ContentBlock
|
||||||
IsError bool `json:"is_error,omitempty"`
|
IsError bool `json:"is_error,omitempty"`
|
||||||
|
|
||||||
// For thinking blocks (no omitempty - SDK requires field to be present for accumulation)
|
// For thinking blocks - pointer so field only appears when set (SDK requires it for accumulation)
|
||||||
Thinking string `json:"thinking"`
|
Thinking *string `json:"thinking,omitempty"`
|
||||||
Signature string `json:"signature,omitempty"`
|
Signature string `json:"signature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageSource represents the source of an image
|
// ImageSource represents the source of an image
|
||||||
|
|
@ -458,14 +460,14 @@ func ToMessagesResponse(id string, r api.ChatResponse) MessagesResponse {
|
||||||
if r.Message.Thinking != "" {
|
if r.Message.Thinking != "" {
|
||||||
content = append(content, ContentBlock{
|
content = append(content, ContentBlock{
|
||||||
Type: "thinking",
|
Type: "thinking",
|
||||||
Thinking: r.Message.Thinking,
|
Thinking: ptr(r.Message.Thinking),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Message.Content != "" {
|
if r.Message.Content != "" {
|
||||||
content = append(content, ContentBlock{
|
content = append(content, ContentBlock{
|
||||||
Type: "text",
|
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,
|
Index: c.contentIndex,
|
||||||
ContentBlock: ContentBlock{
|
ContentBlock: ContentBlock{
|
||||||
Type: "thinking",
|
Type: "thinking",
|
||||||
Thinking: "",
|
Thinking: ptr(""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -620,7 +622,7 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
||||||
Index: c.contentIndex,
|
Index: c.contentIndex,
|
||||||
ContentBlock: ContentBlock{
|
ContentBlock: ContentBlock{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: "",
|
Text: ptr(""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -760,3 +762,8 @@ func generateID(prefix string) string {
|
||||||
func GenerateMessageID() string {
|
func GenerateMessageID() string {
|
||||||
return generateID("msg")
|
return generateID("msg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ptr returns a pointer to the given string value
|
||||||
|
func ptr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -447,7 +447,7 @@ func TestToMessagesResponse_Basic(t *testing.T) {
|
||||||
if len(result.Content) != 1 {
|
if len(result.Content) != 1 {
|
||||||
t.Fatalf("expected 1 content block, got %d", len(result.Content))
|
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])
|
t.Errorf("unexpected content: %+v", result.Content[0])
|
||||||
}
|
}
|
||||||
if result.StopReason != "end_turn" {
|
if result.StopReason != "end_turn" {
|
||||||
|
|
@ -516,8 +516,8 @@ func TestToMessagesResponse_WithThinking(t *testing.T) {
|
||||||
if result.Content[0].Type != "thinking" {
|
if result.Content[0].Type != "thinking" {
|
||||||
t.Errorf("expected first block type 'thinking', got %q", result.Content[0].Type)
|
t.Errorf("expected first block type 'thinking', got %q", result.Content[0].Type)
|
||||||
}
|
}
|
||||||
if result.Content[0].Thinking != "Let me think about this..." {
|
if result.Content[0].Thinking == nil || *result.Content[0].Thinking != "Let me think about this..." {
|
||||||
t.Errorf("unexpected thinking content: %q", result.Content[0].Thinking)
|
t.Errorf("unexpected thinking content: %v", result.Content[0].Thinking)
|
||||||
}
|
}
|
||||||
if result.Content[1].Type != "text" {
|
if result.Content[1].Type != "text" {
|
||||||
t.Errorf("expected second block type 'text', got %q", result.Content[1].Type)
|
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",
|
name: "text block includes empty text field",
|
||||||
block: ContentBlock{
|
block: ContentBlock{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: "",
|
Text: ptr(""),
|
||||||
},
|
},
|
||||||
wantKeys: []string{"type", "text"},
|
wantKeys: []string{"type", "text"},
|
||||||
},
|
},
|
||||||
|
|
@ -833,7 +833,7 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) {
|
||||||
name: "thinking block includes empty thinking field",
|
name: "thinking block includes empty thinking field",
|
||||||
block: ContentBlock{
|
block: ContentBlock{
|
||||||
Type: "thinking",
|
Type: "thinking",
|
||||||
Thinking: "",
|
Thinking: ptr(""),
|
||||||
},
|
},
|
||||||
wantKeys: []string{"type", "thinking"},
|
wantKeys: []string{"type", "thinking"},
|
||||||
},
|
},
|
||||||
|
|
@ -841,7 +841,7 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) {
|
||||||
name: "text block with content",
|
name: "text block with content",
|
||||||
block: ContentBlock{
|
block: ContentBlock{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
Text: "hello",
|
Text: ptr("hello"),
|
||||||
},
|
},
|
||||||
wantKeys: []string{"type", "text"},
|
wantKeys: []string{"type", "text"},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue