Skip to content

Commit fc96e51

Browse files
yokomotoddicksontsai
authored andcommitted
fix: add support for thinking content blocks (anthropics#28)
## Summary Fixes an issue where `thinking` content blocks in Claude Code responses were not being parsed, resulting in empty `AssistantMessage` content arrays. ## Changes - Added `ThinkingBlock` dataclass to handle thinking content with `thinking` and `signature` fields - Updated client parsing logic in `_internal/client.py` to recognize and create `ThinkingBlock` instances - Added comprehensive test coverage for thinking block functionality ## Before ```python # Claude Code response with thinking block resulted in: AssistantMessage(content=[]) # Empty content! ``` ## After ```python # Now correctly parses to: AssistantMessage(content=[ ThinkingBlock(thinking="...", signature="...") ]) ``` Fixes anthropics#27 --------- Co-authored-by: Dickson Tsai <dickson@anthropic.com> Signed-off-by: Rushil Patel <rpatel@codegen.com>
1 parent 4e8c11a commit fc96e51

File tree

8 files changed

+60
-7
lines changed

8 files changed

+60
-7
lines changed

src/claude_code_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ResultMessage,
2424
SystemMessage,
2525
TextBlock,
26+
ThinkingBlock,
2627
ToolResultBlock,
2728
ToolUseBlock,
2829
UserMessage,
@@ -46,6 +47,7 @@
4647
"Message",
4748
"ClaudeCodeOptions",
4849
"TextBlock",
50+
"ThinkingBlock",
4951
"ToolUseBlock",
5052
"ToolResultBlock",
5153
"ContentBlock",

src/claude_code_sdk/_internal/message_parser.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ResultMessage,
1212
SystemMessage,
1313
TextBlock,
14+
ThinkingBlock,
1415
ToolResultBlock,
1516
ToolUseBlock,
1617
UserMessage,
@@ -53,6 +54,13 @@ def parse_message(data: dict[str, Any]) -> Message:
5354
user_content_blocks.append(
5455
TextBlock(text=block["text"])
5556
)
57+
case "thinking":
58+
user_content_blocks.append(
59+
ThinkingBlock(
60+
thinking=block["thinking"],
61+
signature=block["signature"],
62+
)
63+
)
5664
case "tool_use":
5765
user_content_blocks.append(
5866
ToolUseBlock(
@@ -100,7 +108,9 @@ def parse_message(data: dict[str, Any]) -> Message:
100108
)
101109
)
102110

103-
return AssistantMessage(content=content_blocks)
111+
return AssistantMessage(
112+
content=content_blocks, model=data["message"]["model"]
113+
)
104114
except KeyError as e:
105115
raise MessageParseError(
106116
f"Missing required field in assistant message: {e}", data

src/claude_code_sdk/types.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ class TextBlock:
4747
text: str
4848

4949

50+
@dataclass
51+
class ThinkingBlock:
52+
"""Thinking content block."""
53+
54+
thinking: str
55+
signature: str
56+
57+
5058
@dataclass
5159
class ToolUseBlock:
5260
"""Tool use content block."""
@@ -65,7 +73,7 @@ class ToolResultBlock:
6573
is_error: bool | None = None
6674

6775

68-
ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock
76+
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock
6977

7078

7179
# Message types
@@ -81,6 +89,7 @@ class AssistantMessage:
8189
"""Assistant message with content blocks."""
8290

8391
content: list[ContentBlock]
92+
model: str
8493

8594

8695
@dataclass

tests/test_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ async def _test():
2020
) as mock_process:
2121
# Mock the async generator
2222
async def mock_generator():
23-
yield AssistantMessage(content=[TextBlock(text="4")])
23+
yield AssistantMessage(
24+
content=[TextBlock(text="4")], model="claude-opus-4-1-20250805"
25+
)
2426

2527
mock_process.return_value = mock_generator()
2628

@@ -43,7 +45,10 @@ async def _test():
4345
) as mock_process:
4446

4547
async def mock_generator():
46-
yield AssistantMessage(content=[TextBlock(text="Hello!")])
48+
yield AssistantMessage(
49+
content=[TextBlock(text="Hello!")],
50+
model="claude-opus-4-1-20250805",
51+
)
4752

4853
mock_process.return_value = mock_generator()
4954

@@ -83,6 +88,7 @@ async def mock_receive():
8388
"message": {
8489
"role": "assistant",
8590
"content": [{"type": "text", "text": "Done"}],
91+
"model": "claude-opus-4-1-20250805",
8692
},
8793
}
8894
yield {

tests/test_integration.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async def mock_receive():
3838
"message": {
3939
"role": "assistant",
4040
"content": [{"type": "text", "text": "2 + 2 equals 4"}],
41+
"model": "claude-opus-4-1-20250805",
4142
},
4243
}
4344
yield {
@@ -103,6 +104,7 @@ async def mock_receive():
103104
"input": {"file_path": "/test.txt"},
104105
},
105106
],
107+
"model": "claude-opus-4-1-20250805",
106108
},
107109
}
108110
yield {
@@ -179,6 +181,7 @@ async def mock_receive():
179181
"text": "Continuing from previous conversation",
180182
}
181183
],
184+
"model": "claude-opus-4-1-20250805",
182185
},
183186
}
184187

tests/test_message_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def test_parse_valid_assistant_message(self):
142142
"name": "Read",
143143
"input": {"file_path": "/test.txt"},
144144
},
145-
]
145+
],
146+
"model": "claude-opus-4-1-20250805",
146147
},
147148
}
148149
message = parse_message(data)

tests/test_streaming_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ async def mock_receive():
187187
"message": {
188188
"role": "assistant",
189189
"content": [{"type": "text", "text": "Hello!"}],
190+
"model": "claude-opus-4-1-20250805",
190191
},
191192
}
192193
yield {
@@ -229,6 +230,7 @@ async def mock_receive():
229230
"message": {
230231
"role": "assistant",
231232
"content": [{"type": "text", "text": "Answer"}],
233+
"model": "claude-opus-4-1-20250805",
232234
},
233235
}
234236
yield {
@@ -250,6 +252,7 @@ async def mock_receive():
250252
{"type": "text", "text": "Should not see this"}
251253
],
252254
},
255+
"model": "claude-opus-4-1-20250805",
253256
}
254257

255258
mock_transport.receive_messages = mock_receive
@@ -335,6 +338,7 @@ async def mock_receive():
335338
"message": {
336339
"role": "assistant",
337340
"content": [{"type": "text", "text": "Response 1"}],
341+
"model": "claude-opus-4-1-20250805",
338342
},
339343
}
340344
await asyncio.sleep(0.1)
@@ -531,13 +535,15 @@ async def mock_receive():
531535
"message": {
532536
"role": "assistant",
533537
"content": [{"type": "text", "text": "Hello"}],
538+
"model": "claude-opus-4-1-20250805",
534539
},
535540
}
536541
yield {
537542
"type": "assistant",
538543
"message": {
539544
"role": "assistant",
540545
"content": [{"type": "text", "text": "World"}],
546+
"model": "claude-opus-4-1-20250805",
541547
},
542548
}
543549
yield {

tests/test_types.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
ClaudeCodeOptions,
66
ResultMessage,
77
)
8-
from claude_code_sdk.types import TextBlock, ToolResultBlock, ToolUseBlock, UserMessage
8+
from claude_code_sdk.types import (
9+
TextBlock,
10+
ThinkingBlock,
11+
ToolResultBlock,
12+
ToolUseBlock,
13+
UserMessage,
14+
)
915

1016

1117
class TestMessageTypes:
@@ -19,10 +25,20 @@ def test_user_message_creation(self):
1925
def test_assistant_message_with_text(self):
2026
"""Test creating an AssistantMessage with text content."""
2127
text_block = TextBlock(text="Hello, human!")
22-
msg = AssistantMessage(content=[text_block])
28+
msg = AssistantMessage(content=[text_block], model="claude-opus-4-1-20250805")
2329
assert len(msg.content) == 1
2430
assert msg.content[0].text == "Hello, human!"
2531

32+
def test_assistant_message_with_thinking(self):
33+
"""Test creating an AssistantMessage with thinking content."""
34+
thinking_block = ThinkingBlock(thinking="I'm thinking...", signature="sig-123")
35+
msg = AssistantMessage(
36+
content=[thinking_block], model="claude-opus-4-1-20250805"
37+
)
38+
assert len(msg.content) == 1
39+
assert msg.content[0].thinking == "I'm thinking..."
40+
assert msg.content[0].signature == "sig-123"
41+
2642
def test_tool_use_block(self):
2743
"""Test creating a ToolUseBlock."""
2844
block = ToolUseBlock(

0 commit comments

Comments
 (0)