mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 15:53:27 +02:00
anthropic: fix empty inputs in content blocks (#15105)
* anthropic: fix empty inputs in content blocks
When we switched to `api.ToolCallFunctionArguments`, `omitempty` stopped
doing what we were relying on it for before. This would cause non-tool
content blocks to have an `"input": {}` field, which doesn't match our
old behavior.
* use omitzero instead
This commit is contained in:
@@ -123,7 +123,7 @@ type ContentBlock struct {
|
||||
// For tool_use and server_tool_use blocks
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Input api.ToolCallFunctionArguments `json:"input,omitempty"`
|
||||
Input api.ToolCallFunctionArguments `json:"input,omitzero"`
|
||||
|
||||
// For tool_result and web_search_tool_result blocks
|
||||
ToolUseID string `json:"tool_use_id,omitempty"`
|
||||
@@ -868,7 +868,6 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
||||
slog.Error("failed to marshal tool arguments", "error", err, "tool_id", tc.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
events = append(events, StreamEvent{
|
||||
Event: "content_block_start",
|
||||
Data: ContentBlockStartEvent{
|
||||
@@ -878,7 +877,7 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
|
||||
Type: "tool_use",
|
||||
ID: tc.ID,
|
||||
Name: tc.Function.Name,
|
||||
Input: api.ToolCallFunctionArguments{},
|
||||
Input: api.NewToolCallFunctionArguments(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1072,6 +1072,57 @@ func TestContentBlockJSON_EmptyFieldsPresent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentBlockJSON_NonToolBlocksDoNotIncludeInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
block ContentBlock
|
||||
}{
|
||||
{
|
||||
name: "text block",
|
||||
block: ContentBlock{
|
||||
Type: "text",
|
||||
Text: ptr("hello"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "thinking block",
|
||||
block: ContentBlock{
|
||||
Type: "thinking",
|
||||
Thinking: ptr("let me think"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image block",
|
||||
block: ContentBlock{
|
||||
Type: "image",
|
||||
Source: &ImageSource{
|
||||
Type: "base64",
|
||||
MediaType: "image/png",
|
||||
Data: testImage,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(tt.block)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := result["input"]; ok {
|
||||
t.Fatalf("unexpected input field in non-tool block JSON: %s", string(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
||||
t.Run("text block start includes empty text", func(t *testing.T) {
|
||||
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||
@@ -1092,7 +1143,9 @@ func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
||||
// Marshal and verify the text field is present
|
||||
data, _ := json.Marshal(start)
|
||||
var result map[string]any
|
||||
json.Unmarshal(data, &result)
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal content_block_start JSON: %v", err)
|
||||
}
|
||||
cb := result["content_block"].(map[string]any)
|
||||
if _, ok := cb["text"]; !ok {
|
||||
t.Error("content_block_start for text should include 'text' field")
|
||||
@@ -1139,6 +1192,64 @@ func TestStreamConverter_ContentBlockStartIncludesEmptyFields(t *testing.T) {
|
||||
t.Error("expected thinking content_block_start event")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tool_use block start includes empty input object", func(t *testing.T) {
|
||||
conv := NewStreamConverter("msg_123", "test-model", 0)
|
||||
|
||||
resp := api.ChatResponse{
|
||||
Model: "test-model",
|
||||
Message: api.Message{
|
||||
Role: "assistant",
|
||||
ToolCalls: []api.ToolCall{
|
||||
{
|
||||
ID: "call_123",
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: makeArgs("location", "Paris"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
events := conv.Process(resp)
|
||||
|
||||
var foundToolStart bool
|
||||
for _, e := range events {
|
||||
if e.Event == "content_block_start" {
|
||||
if start, ok := e.Data.(ContentBlockStartEvent); ok {
|
||||
if start.ContentBlock.Type == "tool_use" {
|
||||
foundToolStart = true
|
||||
if start.ContentBlock.Input.Len() != 0 {
|
||||
t.Errorf("expected empty input object, got len=%d", start.ContentBlock.Input.Len())
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(start)
|
||||
var result map[string]any
|
||||
json.Unmarshal(data, &result)
|
||||
cb := result["content_block"].(map[string]any)
|
||||
input, ok := cb["input"]
|
||||
if !ok {
|
||||
t.Error("content_block_start for tool_use should include 'input' field")
|
||||
continue
|
||||
}
|
||||
inputMap, ok := input.(map[string]any)
|
||||
if !ok {
|
||||
t.Errorf("input field should be an object, got %T", input)
|
||||
continue
|
||||
}
|
||||
if len(inputMap) != 0 {
|
||||
t.Errorf("expected empty input object in content_block_start, got %v", inputMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundToolStart {
|
||||
t.Error("expected tool_use content_block_start event")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEstimateTokens_SimpleMessage(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user