Skip to content

Commit 3530dcb

Browse files
authored
Store OpenAI Responses text part ID to prevent error with reasoning (#2882)
1 parent b4b10ef commit 3530dcb

File tree

13 files changed

+297
-235
lines changed

13 files changed

+297
-235
lines changed

pydantic_ai_slim/pydantic_ai/_parts_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def handle_text_delta(
7171
*,
7272
vendor_part_id: VendorId | None,
7373
content: str,
74+
id: str | None = None,
7475
thinking_tags: tuple[str, str] | None = None,
7576
ignore_leading_whitespace: bool = False,
7677
) -> ModelResponseStreamEvent | None:
@@ -85,6 +86,7 @@ def handle_text_delta(
8586
of text. If None, a new part will be created unless the latest part is already
8687
a TextPart.
8788
content: The text content to append to the appropriate TextPart.
89+
id: An optional id for the text part.
8890
thinking_tags: If provided, will handle content between the thinking tags as thinking parts.
8991
ignore_leading_whitespace: If True, will ignore leading whitespace in the content.
9092
@@ -137,7 +139,7 @@ def handle_text_delta(
137139

138140
# There is no existing text part that should be updated, so create a new one
139141
new_part_index = len(self._parts)
140-
part = TextPart(content=content)
142+
part = TextPart(content=content, id=id)
141143
if vendor_part_id is not None:
142144
self._vendor_id_to_part_index[vendor_part_id] = new_part_index
143145
self._parts.append(part)

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,9 @@ class TextPart:
870870

871871
_: KW_ONLY
872872

873+
id: str | None = None
874+
"""An optional identifier of the text part."""
875+
873876
part_kind: Literal['text'] = 'text'
874877
"""Part type identifier, this is available on all parts as a discriminator."""
875878

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ def _process_response(self, response: responses.Response) -> ModelResponse:
886886
elif isinstance(item, responses.ResponseOutputMessage):
887887
for content in item.content:
888888
if isinstance(content, responses.ResponseOutputText): # pragma: no branch
889-
items.append(TextPart(content.text))
889+
items.append(TextPart(content.text, id=item.id))
890890
elif isinstance(item, responses.ResponseFunctionToolCall):
891891
items.append(
892892
ToolCallPart(item.name, item.arguments, tool_call_id=_combine_tool_call_ids(item.call_id, item.id))
@@ -1122,10 +1122,31 @@ async def _map_messages( # noqa: C901
11221122
else:
11231123
assert_never(part)
11241124
elif isinstance(message, ModelResponse):
1125+
message_item: responses.ResponseOutputMessageParam | None = None
11251126
reasoning_item: responses.ResponseReasoningItemParam | None = None
11261127
for item in message.parts:
11271128
if isinstance(item, TextPart):
1128-
openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content))
1129+
if item.id and item.id.startswith('msg_'):
1130+
if message_item is None or message_item['id'] != item.id: # pragma: no branch
1131+
message_item = responses.ResponseOutputMessageParam(
1132+
role='assistant',
1133+
id=item.id or _utils.generate_tool_call_id(),
1134+
content=[],
1135+
type='message',
1136+
status='completed',
1137+
)
1138+
openai_messages.append(message_item)
1139+
1140+
message_item['content'] = [
1141+
*message_item['content'],
1142+
responses.ResponseOutputTextParam(
1143+
text=item.content, type='output_text', annotations=[]
1144+
),
1145+
]
1146+
else:
1147+
openai_messages.append(
1148+
responses.EasyInputMessageParam(role='assistant', content=item.content)
1149+
)
11291150
elif isinstance(item, ToolCallPart):
11301151
openai_messages.append(self._map_tool_call(item))
11311152
elif isinstance(item, BuiltinToolCallPart | BuiltinToolReturnPart):
@@ -1436,7 +1457,9 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
14361457
pass # there's nothing we need to do here
14371458

14381459
elif isinstance(chunk, responses.ResponseTextDeltaEvent):
1439-
maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=chunk.item_id, content=chunk.delta)
1460+
maybe_event = self._parts_manager.handle_text_delta(
1461+
vendor_part_id=chunk.item_id, content=chunk.delta, id=chunk.item_id
1462+
)
14401463
if maybe_event is not None: # pragma: no branch
14411464
yield maybe_event
14421465

tests/models/cassettes/test_model_names/test_known_model_names.yaml

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -99,53 +99,19 @@ interactions:
9999
alt-svc:
100100
- h3=":443"; ma=86400
101101
content-length:
102-
- '762'
102+
- '99'
103103
content-type:
104104
- application/json
105105
referrer-policy:
106106
- strict-origin-when-cross-origin
107107
strict-transport-security:
108108
- max-age=3600; includeSubDomains
109109
parsed_body:
110-
data:
111-
- created: 0
112-
id: qwen-3-235b-a22b-thinking-2507
113-
object: model
114-
owned_by: Cerebras
115-
- created: 0
116-
id: llama-3.3-70b
117-
object: model
118-
owned_by: Cerebras
119-
- created: 0
120-
id: qwen-3-235b-a22b-instruct-2507
121-
object: model
122-
owned_by: Cerebras
123-
- created: 0
124-
id: llama-4-scout-17b-16e-instruct
125-
object: model
126-
owned_by: Cerebras
127-
- created: 0
128-
id: gpt-oss-120b
129-
object: model
130-
owned_by: Cerebras
131-
- created: 0
132-
id: qwen-3-coder-480b
133-
object: model
134-
owned_by: Cerebras
135-
- created: 0
136-
id: llama3.1-8b
137-
object: model
138-
owned_by: Cerebras
139-
- created: 0
140-
id: llama-4-maverick-17b-128e-instruct
141-
object: model
142-
owned_by: Cerebras
143-
- created: 0
144-
id: qwen-3-32b
145-
object: model
146-
owned_by: Cerebras
147-
object: list
110+
code: wrong_api_key
111+
message: Wrong API Key
112+
param: api_key
113+
type: invalid_request_error
148114
status:
149-
code: 200
150-
message: OK
115+
code: 401
116+
message: Unauthorized
151117
version: 1

0 commit comments

Comments
 (0)