Skip to content

Commit b4b10ef

Browse files
authored
Don't lose Azure OpenAI Responses encrypted_content if no summary was included (#2874)
1 parent ec87153 commit b4b10ef

File tree

2 files changed

+83
-16
lines changed

2 files changed

+83
-16
lines changed

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -859,18 +859,28 @@ def _process_response(self, response: responses.Response) -> ModelResponse:
859859
for item in response.output:
860860
if isinstance(item, responses.ResponseReasoningItem):
861861
signature = item.encrypted_content
862-
for summary in item.summary:
863-
# We use the same id for all summaries so that we can merge them on the round trip.
864-
# We only need to store the signature once.
862+
if item.summary:
863+
for summary in item.summary:
864+
# We use the same id for all summaries so that we can merge them on the round trip.
865+
items.append(
866+
ThinkingPart(
867+
content=summary.text,
868+
id=item.id,
869+
signature=signature,
870+
provider_name=self.system if signature else None,
871+
)
872+
)
873+
# We only need to store the signature once.
874+
signature = None
875+
elif signature:
865876
items.append(
866877
ThinkingPart(
867-
content=summary.text,
878+
content='',
868879
id=item.id,
869880
signature=signature,
870-
provider_name=self.system if signature else None,
881+
provider_name=self.system,
871882
)
872883
)
873-
signature = None
874884
# NOTE: We don't currently handle the raw CoT from gpt-oss `reasoning_text`: https://cookbook.openai.com/articles/gpt-oss/handle-raw-cot
875885
# If you need this, please file an issue.
876886
elif isinstance(item, responses.ResponseOutputMessage):
@@ -1122,20 +1132,20 @@ async def _map_messages( # noqa: C901
11221132
# We don't currently track built-in tool calls from OpenAI
11231133
pass
11241134
elif isinstance(item, ThinkingPart):
1125-
if reasoning_item is not None and item.id == reasoning_item['id']:
1135+
if reasoning_item is None or reasoning_item['id'] != item.id:
1136+
reasoning_item = responses.ResponseReasoningItemParam(
1137+
id=item.id or _utils.generate_tool_call_id(),
1138+
summary=[],
1139+
encrypted_content=item.signature if item.provider_name == self.system else None,
1140+
type='reasoning',
1141+
)
1142+
openai_messages.append(reasoning_item)
1143+
1144+
if item.content:
11261145
reasoning_item['summary'] = [
11271146
*reasoning_item['summary'],
11281147
Summary(text=item.content, type='summary_text'),
11291148
]
1130-
continue
1131-
1132-
reasoning_item = responses.ResponseReasoningItemParam(
1133-
id=item.id or _utils.generate_tool_call_id(),
1134-
summary=[Summary(text=item.content, type='summary_text')],
1135-
encrypted_content=item.signature if item.provider_name == self.system else None,
1136-
type='reasoning',
1137-
)
1138-
openai_messages.append(reasoning_item)
11391149
else:
11401150
assert_never(item)
11411151
else:

tests/models/test_openai_responses.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
with try_import() as imports_successful:
4040
from openai.types.responses.response_output_message import Content, ResponseOutputMessage, ResponseOutputText
41+
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
4142
from openai.types.responses.response_usage import ResponseUsage
4243

4344
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
@@ -1404,3 +1405,59 @@ def update_plan(plan: str) -> str:
14041405
),
14051406
]
14061407
)
1408+
1409+
1410+
async def test_openai_responses_thinking_without_summary(allow_model_requests: None):
1411+
c = response_message(
1412+
[
1413+
ResponseReasoningItem(
1414+
id='reasoning',
1415+
summary=[],
1416+
type='reasoning',
1417+
encrypted_content='123',
1418+
),
1419+
ResponseOutputMessage(
1420+
id='text',
1421+
content=cast(list[Content], [ResponseOutputText(text='4', type='output_text', annotations=[])]),
1422+
role='assistant',
1423+
status='completed',
1424+
type='message',
1425+
),
1426+
],
1427+
)
1428+
mock_client = MockOpenAIResponses.create_mock(c)
1429+
model = OpenAIResponsesModel('gpt-5', provider=OpenAIProvider(openai_client=mock_client))
1430+
1431+
agent = Agent(model=model)
1432+
result = await agent.run('What is 2+2?')
1433+
assert result.all_messages() == snapshot(
1434+
[
1435+
ModelRequest(
1436+
parts=[
1437+
UserPromptPart(
1438+
content='What is 2+2?',
1439+
timestamp=IsDatetime(),
1440+
)
1441+
]
1442+
),
1443+
ModelResponse(
1444+
parts=[
1445+
ThinkingPart(content='', id='reasoning', signature='123', provider_name='openai'),
1446+
TextPart(content='4'),
1447+
],
1448+
model_name='gpt-4o-123',
1449+
timestamp=IsDatetime(),
1450+
provider_name='openai',
1451+
provider_response_id='123',
1452+
),
1453+
]
1454+
)
1455+
1456+
_, openai_messages = await model._map_messages(result.all_messages()) # type: ignore[reportPrivateUsage]
1457+
assert openai_messages == snapshot(
1458+
[
1459+
{'role': 'user', 'content': 'What is 2+2?'},
1460+
{'id': 'reasoning', 'summary': [], 'encrypted_content': '123', 'type': 'reasoning'},
1461+
{'role': 'assistant', 'content': '4'},
1462+
]
1463+
)

0 commit comments

Comments
 (0)