Repository: ollama/ollama
Pull request: 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.

Changed files:
filename: anthropic/anthropic.go
patch: @@ -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(),
 				},
 			},
 		})

filename: anthropic/anthropic_test.go
patch: @@ -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) {