15
15
16
16
from collections .abc import Hashable
17
17
from dataclasses import dataclass , field , replace
18
- from typing import Any , Literal , Union , overload
18
+ from typing import Any , Union
19
19
20
20
from pydantic_ai ._thinking_part import END_THINK_TAG , START_THINK_TAG
21
21
from pydantic_ai .exceptions import UnexpectedModelBehavior
@@ -67,23 +67,6 @@ def get_parts(self) -> list[ModelResponsePart]:
67
67
"""
68
68
return [p for p in self ._parts if not isinstance (p , ToolCallPartDelta )]
69
69
70
- @overload
71
- def handle_text_delta (
72
- self ,
73
- * ,
74
- vendor_part_id : VendorId | None ,
75
- content : str ,
76
- ) -> ModelResponseStreamEvent : ...
77
-
78
- @overload
79
- def handle_text_delta (
80
- self ,
81
- * ,
82
- vendor_part_id : VendorId ,
83
- content : str ,
84
- extract_think_tags : Literal [True ],
85
- ) -> ModelResponseStreamEvent | None : ...
86
-
87
70
def handle_text_delta (
88
71
self ,
89
72
* ,
@@ -105,7 +88,9 @@ def handle_text_delta(
105
88
extract_think_tags: Whether to extract `<think>` tags from the text content and handle them as thinking parts.
106
89
107
90
Returns:
108
- A `PartStartEvent` if a new part was created, or a `PartDeltaEvent` if an existing part was updated.
91
+ - A `PartStartEvent` if a new part was created.
92
+ - A `PartDeltaEvent` if an existing part was updated.
93
+ - `None` if no new event is emitted (e.g., the first text part was all whitespace).
109
94
110
95
Raises:
111
96
UnexpectedModelBehavior: If attempting to apply text content to a part that is not a TextPart.
@@ -144,6 +129,12 @@ def handle_text_delta(
144
129
return self .handle_thinking_delta (vendor_part_id = vendor_part_id , content = '' )
145
130
146
131
if existing_text_part_and_index is None :
132
+ # If the first text delta is all whitespace, don't emit a new part yet.
133
+ # This is a workaround for models that emit `<think>\n</think>\n\n` ahead of tool calls (e.g. Ollama + Qwen3),
134
+ # which we don't want to end up treating as a final result.
135
+ if content .isspace ():
136
+ return None
137
+
147
138
# There is no existing text part that should be updated, so create a new one
148
139
new_part_index = len (self ._parts )
149
140
part = TextPart (content = content )
0 commit comments