Skip to content

Commit 8ab9357

Browse files
committed
fix: handoff from reasoning model to non-reasoning
Be more selective/surgical on the reasoning message preservation. When we handoff from Claude 4 Sonnet Thinking to non-thinking agent, we get errors because non-thinking models expects no thinking blocks in the request. This fixes this edge case by only preserving blocks when reasoning effort is not None.
1 parent 543d192 commit 8ab9357

File tree

3 files changed

+19
-3
lines changed

3 files changed

+19
-3
lines changed

src/agents/extensions/models/litellm_model.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,15 @@ async def _fetch_response(
257257
stream: bool = False,
258258
prompt: Any | None = None,
259259
) -> litellm.types.utils.ModelResponse | tuple[Response, AsyncStream[ChatCompletionChunk]]:
260-
converted_messages = Converter.items_to_messages(input)
260+
# Preserve reasoning messages for tool calls when reasoning is on
261+
# This is needed for models like Claude 4 Sonnet/Opus which support interleaved thinking
262+
preserve_reasoning_message = (
263+
model_settings.reasoning is not None and model_settings.reasoning.effort is not None
264+
)
265+
266+
converted_messages = Converter.items_to_messages(
267+
input, preserve_reasoning_message=preserve_reasoning_message
268+
)
261269

262270
if system_instructions:
263271
converted_messages.insert(

src/agents/models/chatcmpl_converter.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,18 @@ def extract_all_content(
315315
def items_to_messages(
316316
cls,
317317
items: str | Iterable[TResponseInputItem],
318+
preserve_reasoning_message: bool = False,
318319
) -> list[ChatCompletionMessageParam]:
319320
"""
320321
Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
321322
323+
Args:
324+
items: A string or iterable of response input items to convert
325+
preserve_reasoning_message: Whether to preserve reasoning messages (thinking blocks)
326+
in tool calls for reasoning models like Claude 4 Sonnet/Opus which support
327+
interleaved thinking. When True, thinking blocks are reconstructed and
328+
included in assistant messages with tool calls.
329+
322330
Rules:
323331
- EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
324332
- EasyInputMessage or InputMessage (role=system) => ChatCompletionSystemMessageParam
@@ -512,7 +520,7 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
512520
content_items = reasoning_item.get("content", [])
513521
signature = reasoning_item.get("encrypted_content")
514522

515-
if content_items:
523+
if content_items and preserve_reasoning_message:
516524
# Reconstruct thinking blocks from content and signature
517525
pending_thinking_blocks = []
518526
for content_item in content_items:

tests/test_anthropic_thinking_blocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def test_anthropic_thinking_blocks_with_tool_calls():
176176
else:
177177
items_as_dicts.append(cast(dict[str, Any], item))
178178

179-
messages = Converter.items_to_messages(items_as_dicts) # type: ignore[arg-type]
179+
messages = Converter.items_to_messages(items_as_dicts, preserve_reasoning_message=True) # type: ignore[arg-type]
180180

181181
# Find the assistant message with tool calls
182182
assistant_messages = [

0 commit comments

Comments
 (0)