Repository: ollama/ollama
Pull request: anthropic: close thinking block before tool_use when no text in between

## Summary

Fixes #14816.

When a model emits a `thinking` block followed directly by a `tool_use` block (no `text` block in between), the `StreamConverter.Process()` method was reusing `contentIndex=0` for the `tool_use` `content_block_start`. This caused downstream clients to receive `content_block_start` events with duplicate indices, leading to "Content block not found" errors.

---

## Root Cause

`StreamConverter` tracks `thinkingStarted`, `thinkingDone`, `textStarted`, and `contentIndex` to sequence content blocks. The existing logic correctly closes a thinking block when **text content** arrives:

```go
// anthropic.go lines 810–820 (before fix)
if r.Message.Content != "" {
    if c.thinkingStarted && !c.thinkingDone {
        c.thinkingDone = true
        // emit content_block_stop at contentIndex
        c.contentIndex++
    }
    // ... emit text block at new contentIndex
}
```

However, the tool call loop only checked `textStarted` before opening a `tool_use` block:

```go
// anthropic.go line 854 (before fix) — missing thinking check
for _, tc := range r.Message.ToolCalls {
    if c.textStarted {
        // close text block and increment index
    }
    // emit tool_use at contentIndex — WRONG: still 0 if thinking never ended
}
```

When the model response is `thinking + tool_use` with no `text`, `thinkingDone` was never set and `contentIndex` was never incremented, so the `tool_use` `content_block_start` reused index 0 — the same index as the still-open thinking block.

The bug matches exactly the packet trace in the issue:
```
content_block_start index=0 type=thinking
content_block_delta index=0 thinking_delta
content_block_start index=0 type=tool_use   ← BUG: reuses index 0
content_block_stop  index=0
content_block_stop  index=1                  ← no matching start for index 1
```

---

## Solution

In the tool call loop, close any still-open thinking block before opening the `tool_use` block, mirroring the existing `textStarted` close logic:

```go
// Close thinking block if still open (thinking → tool_use without text)
if c.thinkingStarted && !c.thinkingDone {
    c.thinkingDone = true
    events = append(events, content_block_stop at contentIndex)
    c.contentIndex++
}
```

After the fix the event sequence is correct:
```
content_block_start index=0 type=thinking
content_block_delta index=0 thinking_delta
content_block_stop  index=0   ← thinking closed correctly
content_block_start index=1 type=tool_use   ← tool at next index
content_block_delta index=1 input_json_delta
content_block_stop  index=1
```

---

## Testing

Added `TestStreamConverter_ThinkingDirectlyFollowedByToolCall` in `anthropic/anthropic_test.go` that:
1. Sends a thinking-only chunk (no text, no tool calls)
2. Follows with a tool-call-only chunk (no text)
3. Verifies the thinking block is closed at index 0
4. Verifies the tool_use block opens at index 1
5. Verifies all delta/stop events use the correct index

```
go test ./anthropic/... -run TestStreamConverter
```

All existing tests continue to pass.

---

## Checklist

- [x] Fixes the root cause (not just a symptom)
- [x] New test covers the exact failing scenario from the issue
- [x] All existing tests pass (`go test ./anthropic/...`)
- [x] No unrelated changes
- [x] Code style matches surrounding code

Changed files:
filename: anthropic/anthropic.go
patch: @@ -852,6 +852,19 @@ func (c *StreamConverter) Process(r api.ChatResponse) []StreamEvent {
 			continue
 		}
 
+		// Close thinking block if still open (thinking → tool_use without text in between)
+		if c.thinkingStarted && !c.thinkingDone {
+			c.thinkingDone = true
+			events = append(events, StreamEvent{
+				Event: "content_block_stop",
+				Data: ContentBlockStopEvent{
+					Type:  "content_block_stop",
+					Index: c.contentIndex,
+				},
+			})
+			c.contentIndex++
+		}
+
 		if c.textStarted {
 			events = append(events, StreamEvent{
 				Event: "content_block_stop",

filename: anthropic/anthropic_test.go
patch: @@ -799,6 +799,107 @@ func TestStreamConverter_WithToolCalls(t *testing.T) {
 	}
 }
 
+// TestStreamConverter_ThinkingDirectlyFollowedByToolCall verifies that when a
+// model emits a thinking block followed directly by a tool_use block (with no
+// text block in between), the streaming converter correctly closes the thinking
+// block and increments the content index before opening the tool_use block.
+// Previously, the converter reused contentIndex=0 for the tool_use block,
+// which caused "Content block not found" errors in clients. See #14816.
+func TestStreamConverter_ThinkingDirectlyFollowedByToolCall(t *testing.T) {
+	conv := NewStreamConverter("msg_123", "test-model", 0)
+
+	// First chunk: thinking content (no text)
+	resp1 := api.ChatResponse{
+		Model: "test-model",
+		Message: api.Message{
+			Role:     "assistant",
+			Thinking: "I should call the tool.",
+		},
+	}
+	events1 := conv.Process(resp1)
+
+	// Should have: message_start, content_block_start(thinking), content_block_delta(thinking)
+	if len(events1) < 3 {
+		t.Fatalf("expected at least 3 events for thinking chunk, got %d", len(events1))
+	}
+	if events1[0].Event != "message_start" {
+		t.Errorf("expected first event 'message_start', got %q", events1[0].Event)
+	}
+	thinkingStart, ok := events1[1].Data.(ContentBlockStartEvent)
+	if !ok || thinkingStart.ContentBlock.Type != "thinking" {
+		t.Errorf("expected content_block_start(thinking) as second event, got %+v", events1[1])
+	}
+	if thinkingStart.Index != 0 {
+		t.Errorf("expected thinking block at index 0, got %d", thinkingStart.Index)
+	}
+
+	// Second chunk: tool call (no text between thinking and tool)
+	resp2 := api.ChatResponse{
+		Model: "test-model",
+		Message: api.Message{
+			Role: "assistant",
+			ToolCalls: []api.ToolCall{
+				{
+					ID: "call_abc",
+					Function: api.ToolCallFunction{
+						Name:      "ask_user",
+						Arguments: testArgs(map[string]any{"question": "cats or dogs?"}),
+					},
+				},
+			},
+		},
+		Done:       true,
+		DoneReason: "stop",
+		Metrics:    api.Metrics{PromptEvalCount: 10, EvalCount: 5},
+	}
+	events2 := conv.Process(resp2)
+
+	// Expect: content_block_stop(index=0), content_block_start(tool_use, index=1),
+	//         content_block_delta(input_json_delta, index=1), content_block_stop(index=1),
+	//         message_delta, message_stop
+	var thinkingStop, toolStart, toolDelta, toolStop *StreamEvent
+	for i := range events2 {
+		e := &events2[i]
+		switch e.Event {
+		case "content_block_stop":
+			if stop, ok := e.Data.(ContentBlockStopEvent); ok {
+				if stop.Index == 0 && thinkingStop == nil {
+					thinkingStop = e
+				} else if stop.Index == 1 {
+					toolStop = e
+				}
+			}
+		case "content_block_start":
+			if start, ok := e.Data.(ContentBlockStartEvent); ok && start.ContentBlock.Type == "tool_use" {
+				toolStart = e
+			}
+		case "content_block_delta":
+			if delta, ok := e.Data.(ContentBlockDeltaEvent); ok && delta.Delta.Type == "input_json_delta" {
+				toolDelta = e
+			}
+		}
+	}
+
+	if thinkingStop == nil {
+		t.Error("expected content_block_stop for thinking block (index 0)")
+	}
+	if toolStart == nil {
+		t.Fatal("expected content_block_start for tool_use block")
+	}
+	if start, ok := toolStart.Data.(ContentBlockStartEvent); !ok || start.Index != 1 {
+		t.Errorf("expected tool_use block at index 1, got %+v", toolStart.Data)
+	}
+	if toolDelta == nil {
+		t.Fatal("expected input_json_delta event for tool call")
+	}
+	if delta, ok := toolDelta.Data.(ContentBlockDeltaEvent); !ok || delta.Index != 1 {
+		t.Errorf("expected tool delta at index 1, got %+v", toolDelta.Data)
+	}
+	if toolStop == nil {
+		t.Error("expected content_block_stop for tool_use block (index 1)")
+	}
+}
+
 func TestStreamConverter_ToolCallWithUnmarshalableArgs(t *testing.T) {
 	// Test that unmarshalable arguments (like channels) are handled gracefully
 	// and don't cause a panic or corrupt stream