2020from pydantic_ai .exceptions import UnexpectedModelBehavior
2121from pydantic_ai .messages import (
2222 BuiltinToolCallPart ,
23+ BuiltinToolReturnPart ,
24+ FilePart ,
2325 ModelResponsePart ,
2426 ModelResponseStreamEvent ,
2527 PartDeltaEvent ,
@@ -344,6 +346,10 @@ def _handle_text_delta_with_thinking_tags(
344346 part_index = self ._vendor_id_to_part_index .get (vendor_part_id )
345347 existing_part = self ._parts [part_index ] if part_index is not None else None
346348
349+ # Strip leading whitespace if enabled and no existing part
350+ if ignore_leading_whitespace and not buffered and not existing_part :
351+ content = content .lstrip ()
352+
347353 # If a TextPart has already been created for this vendor_part_id, disable thinking tag detection
348354 if existing_part is not None and isinstance (existing_part , TextPart ):
349355 combined_content = buffered + content
@@ -367,12 +373,11 @@ def _handle_text_delta_with_thinking_tags(
367373 )
368374
369375 # Check for text before thinking tag - if so, treat entire combined content as text
376+ # this covers cases like `pre<think>` or `pre<thi`
370377 if segments and segments [0 ][0 ] == 'text' :
371378 text_content = segments [0 ][1 ]
372- if ignore_leading_whitespace and text_content .isspace ():
373- text_content = ''
374379
375- if text_content :
380+ if text_content : # praga: no cover - line was always true
376381 combined_content = buffered + content
377382 self ._thinking_tag_buffer .pop (vendor_part_id , None )
378383 yield from self ._emit_text_part (
@@ -392,7 +397,7 @@ def _handle_text_delta_with_thinking_tags(
392397 and i + 1 < len (segments )
393398 and segments [i + 1 ][0 ] == 'start_tag'
394399 )
395- if not skip_whitespace_before_tag :
400+ if not skip_whitespace_before_tag : # praga: no cover - line was always true (this is probably dead code, will remove after double checking)
396401 yield from self ._emit_text_part (
397402 vendor_part_id = vendor_part_id ,
398403 content = segment_content ,
@@ -445,6 +450,7 @@ def _emit_text_part(
445450 latest_part = self ._parts [part_index ]
446451 if isinstance (latest_part , TextPart ):
447452 existing_text_part_and_index = latest_part , part_index
453+ # else: existing_text_part_and_index remains None
448454 else :
449455 part_index = self ._vendor_id_to_part_index .get (vendor_part_id )
450456 if part_index is not None :
@@ -453,6 +459,7 @@ def _emit_text_part(
453459 existing_text_part_and_index = existing_part , part_index
454460 else :
455461 raise UnexpectedModelBehavior (f'Cannot apply a text delta to { existing_part = } ' )
462+ # else: existing_text_part_and_index remains None
456463
457464 if existing_text_part_and_index is None :
458465 new_part_index = len (self ._parts )
@@ -467,7 +474,9 @@ def _emit_text_part(
467474 part_delta = TextPartDelta (content_delta = content )
468475 updated_text_part = part_delta .apply (existing_text_part )
469476 self ._parts [part_index ] = updated_text_part
470- if part_index not in self ._started_part_indices :
477+ if (
478+ part_index not in self ._started_part_indices
479+ ): # pragma: no cover - TextPart should have already emitted PartStartEvent when created
471480 self ._started_part_indices .add (part_index )
472481 yield PartStartEvent (index = part_index , part = updated_text_part )
473482 else :
@@ -516,6 +525,8 @@ def handle_thinking_delta(
516525 raise UnexpectedModelBehavior (
517526 'Cannot create ThinkingPart after TextPart: thinking must come before text in response'
518527 )
528+ else : # pragma: no cover - `handle_thinking_delta` should never be called when vendor_part_id is None the latest part is not a ThinkingPart or TextPart
529+ raise UnexpectedModelBehavior (f'Cannot apply a thinking delta to { latest_part = } ' )
519530 else :
520531 # Otherwise, attempt to look up an existing ThinkingPart by vendor_part_id
521532 part_index = self ._vendor_id_to_part_index .get (vendor_part_id )
@@ -684,10 +695,7 @@ def handle_tool_call_part(
684695 return PartStartEvent (index = new_part_index , part = new_part )
685696
686697 def handle_part (
687- self ,
688- * ,
689- vendor_part_id : Hashable | None ,
690- part : ModelResponsePart ,
698+ self , * , vendor_part_id : Hashable | None , part : BuiltinToolCallPart | BuiltinToolReturnPart | FilePart
691699 ) -> ModelResponseStreamEvent :
692700 """Create or overwrite a ModelResponsePart.
693701
0 commit comments