Skip to content

Commit 8a46192

Browse files
authored
Don't send item IDs to Responses API for non-reasoning models (#2950)
1 parent db3c136 commit 8a46192

File tree

4 files changed

+368
-30
lines changed

4 files changed

+368
-30
lines changed

docs/thinking.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ The [`OpenAIResponsesModel`][pydantic_ai.models.openai.OpenAIResponsesModel] can
1717
To enable this functionality, you need to set the `openai_reasoning_effort` and `openai_reasoning_summary` fields in the
1818
[`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings].
1919

20-
By default, reasoning IDs from the message history are sent to the model, which can result in errors like `"Item 'rs_123' of type 'reasoning' was provided without its required following item."`
20+
By default, the unique IDs of reasoning, text, and function call parts from the message history are sent to the model, which can result in errors like `"Item 'rs_123' of type 'reasoning' was provided without its required following item."`
2121
if the message history you're sending does not match exactly what was received from the Responses API in a previous response, for example if you're using a [history processor](message-history.md#processing-message-history).
22-
To disable this, you can set the `openai_send_reasoning_ids` field on [`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings].
22+
To disable this, you can set the `openai_send_reasoning_ids` field on [`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings] to `False`.
2323

2424
```python {title="openai_thinking_part.py"}
2525
from pydantic_ai import Agent

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ class OpenAIResponsesModelSettings(OpenAIChatModelSettings, total=False):
195195
"""
196196

197197
openai_send_reasoning_ids: bool
198-
"""Whether to send reasoning IDs from the message history to the model. Enabled by default.
198+
"""Whether to send the unique IDs of reasoning, text, and function call parts from the message history to the model. Enabled by default for reasoning models.
199199
200200
This can result in errors like `"Item 'rs_123' of type 'reasoning' was provided without its required following item."`
201201
if the message history you're sending does not match exactly what was received from the Responses API in a previous response,
@@ -1134,6 +1134,11 @@ async def _map_messages( # noqa: C901
11341134
self, messages: list[ModelMessage], model_settings: OpenAIResponsesModelSettings
11351135
) -> tuple[str | NotGiven, list[responses.ResponseInputItemParam]]:
11361136
"""Just maps a `pydantic_ai.Message` to a `openai.types.responses.ResponseInputParam`."""
1137+
profile = OpenAIModelProfile.from_profile(self.profile)
1138+
send_item_ids = model_settings.get(
1139+
'openai_send_reasoning_ids', profile.openai_supports_encrypted_reasoning_content
1140+
)
1141+
11371142
openai_messages: list[responses.ResponseInputItemParam] = []
11381143
for message in messages:
11391144
if isinstance(message, ModelRequest):
@@ -1169,15 +1174,17 @@ async def _map_messages( # noqa: C901
11691174
else:
11701175
assert_never(part)
11711176
elif isinstance(message, ModelResponse):
1177+
send_item_ids = send_item_ids and message.provider_name == self.system
1178+
11721179
message_item: responses.ResponseOutputMessageParam | None = None
11731180
reasoning_item: responses.ResponseReasoningItemParam | None = None
11741181
for item in message.parts:
11751182
if isinstance(item, TextPart):
1176-
if item.id and message.provider_name == self.system:
1183+
if item.id and send_item_ids:
11771184
if message_item is None or message_item['id'] != item.id: # pragma: no branch
11781185
message_item = responses.ResponseOutputMessageParam(
11791186
role='assistant',
1180-
id=item.id or _utils.generate_tool_call_id(),
1187+
id=item.id,
11811188
content=[],
11821189
type='message',
11831190
status='completed',
@@ -1195,23 +1202,28 @@ async def _map_messages( # noqa: C901
11951202
responses.EasyInputMessageParam(role='assistant', content=item.content)
11961203
)
11971204
elif isinstance(item, ToolCallPart):
1198-
openai_messages.append(self._map_tool_call(item))
1205+
call_id = _guard_tool_call_id(t=item)
1206+
call_id, id = _split_combined_tool_call_id(call_id)
1207+
1208+
param = responses.ResponseFunctionToolCallParam(
1209+
name=item.tool_name,
1210+
arguments=item.args_as_json_str(),
1211+
call_id=call_id,
1212+
type='function_call',
1213+
)
1214+
if id and send_item_ids: # pragma: no branch
1215+
param['id'] = id
1216+
openai_messages.append(param)
11991217
elif isinstance(item, BuiltinToolCallPart | BuiltinToolReturnPart):
12001218
# We don't currently track built-in tool calls from OpenAI
12011219
pass
12021220
elif isinstance(item, ThinkingPart):
1203-
if (
1204-
item.id
1205-
and message.provider_name == self.system
1206-
and model_settings.get('openai_send_reasoning_ids', True)
1207-
):
1221+
if item.id and send_item_ids:
12081222
signature: str | None = None
12091223
if (
12101224
item.signature
12111225
and item.provider_name == self.system
1212-
and OpenAIModelProfile.from_profile(
1213-
self.profile
1214-
).openai_supports_encrypted_reasoning_content
1226+
and profile.openai_supports_encrypted_reasoning_content
12151227
):
12161228
signature = item.signature
12171229

@@ -1234,7 +1246,7 @@ async def _map_messages( # noqa: C901
12341246
Summary(text=item.content, type='summary_text'),
12351247
]
12361248
else:
1237-
start_tag, end_tag = self.profile.thinking_tags
1249+
start_tag, end_tag = profile.thinking_tags
12381250
openai_messages.append(
12391251
responses.EasyInputMessageParam(
12401252
role='assistant', content='\n'.join([start_tag, item.content, end_tag])
@@ -1247,21 +1259,6 @@ async def _map_messages( # noqa: C901
12471259
instructions = self._get_instructions(messages) or NOT_GIVEN
12481260
return instructions, openai_messages
12491261

1250-
@staticmethod
1251-
def _map_tool_call(t: ToolCallPart) -> responses.ResponseFunctionToolCallParam:
1252-
call_id = _guard_tool_call_id(t=t)
1253-
call_id, id = _split_combined_tool_call_id(call_id)
1254-
1255-
param = responses.ResponseFunctionToolCallParam(
1256-
name=t.tool_name,
1257-
arguments=t.args_as_json_str(),
1258-
call_id=call_id,
1259-
type='function_call',
1260-
)
1261-
if id: # pragma: no branch
1262-
param['id'] = id
1263-
return param
1264-
12651262
def _map_json_schema(self, o: OutputObjectDefinition) -> responses.ResponseFormatTextJSONSchemaConfigParam:
12661263
response_format_param: responses.ResponseFormatTextJSONSchemaConfigParam = {
12671264
'type': 'json_schema',
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '319'
12+
content-type:
13+
- application/json
14+
host:
15+
- api.openai.com
16+
method: POST
17+
parsed_body:
18+
input:
19+
- content: What is the meaning of life?
20+
role: user
21+
model: gpt-4.1
22+
previous_response_id: null
23+
stream: false
24+
tool_choice: auto
25+
tools:
26+
- description: null
27+
name: get_meaning_of_life
28+
parameters:
29+
additionalProperties: false
30+
properties: {}
31+
type: object
32+
strict: false
33+
type: function
34+
uri: https://api.openai.com/v1/responses
35+
response:
36+
headers:
37+
alt-svc:
38+
- h3=":443"; ma=86400
39+
connection:
40+
- keep-alive
41+
content-length:
42+
- '1582'
43+
content-type:
44+
- application/json
45+
openai-organization:
46+
- pydantic-28gund
47+
openai-processing-ms:
48+
- '1079'
49+
openai-project:
50+
- proj_dKobscVY9YJxeEaDJen54e3d
51+
openai-version:
52+
- '2020-10-01'
53+
strict-transport-security:
54+
- max-age=31536000; includeSubDomains; preload
55+
transfer-encoding:
56+
- chunked
57+
parsed_body:
58+
background: false
59+
billing:
60+
payer: developer
61+
created_at: 1758220197
62+
error: null
63+
id: resp_68cc4fa5603481958e2143685133fe530548824120ffcf74
64+
incomplete_details: null
65+
instructions: null
66+
max_output_tokens: null
67+
max_tool_calls: null
68+
metadata: {}
69+
model: gpt-4.1-2025-04-14
70+
object: response
71+
output:
72+
- arguments: '{}'
73+
call_id: call_3WCunBU7lCG1HHaLmnnRJn8I
74+
id: fc_68cc4fa649ac8195b0c6c239cd2c14470548824120ffcf74
75+
name: get_meaning_of_life
76+
status: completed
77+
type: function_call
78+
parallel_tool_calls: true
79+
previous_response_id: null
80+
prompt_cache_key: null
81+
reasoning:
82+
effort: null
83+
summary: null
84+
safety_identifier: null
85+
service_tier: default
86+
status: completed
87+
store: true
88+
temperature: 1.0
89+
text:
90+
format:
91+
type: text
92+
verbosity: medium
93+
tool_choice: auto
94+
tools:
95+
- description: null
96+
name: get_meaning_of_life
97+
parameters:
98+
additionalProperties: false
99+
properties: {}
100+
type: object
101+
strict: false
102+
type: function
103+
top_logprobs: 0
104+
top_p: 1.0
105+
truncation: disabled
106+
usage:
107+
input_tokens: 36
108+
input_tokens_details:
109+
cached_tokens: 0
110+
output_tokens: 15
111+
output_tokens_details:
112+
reasoning_tokens: 0
113+
total_tokens: 51
114+
user: null
115+
status:
116+
code: 200
117+
message: OK
118+
- request:
119+
headers:
120+
accept:
121+
- application/json
122+
accept-encoding:
123+
- gzip, deflate
124+
connection:
125+
- keep-alive
126+
content-length:
127+
- '520'
128+
content-type:
129+
- application/json
130+
cookie:
131+
- __cf_bm=YI5In51XbQNZpt_mBtyMc6O0Pci8G.hkbqZFEWO.3.k-1758220198-1.0.1.1-p4O2urDd35HLxqyMaRXCKX4sikzWXueCzd2uwA2ParH1KuPQ0ERMIioAUS0Uxwl_q2RTDvgwus77kTI_np9kW2zRrsCQvIEetlQmf8IuH5E;
132+
_cfuvid=DfgGpWxEPtt9KaLYpGJqErHQ81Ow1nzWn1Ap_w4r.NM-1758220198467-0.0.1.1-604800000
133+
host:
134+
- api.openai.com
135+
method: POST
136+
parsed_body:
137+
input:
138+
- content: What is the meaning of life?
139+
role: user
140+
- arguments: '{}'
141+
call_id: call_3WCunBU7lCG1HHaLmnnRJn8I
142+
name: get_meaning_of_life
143+
type: function_call
144+
- call_id: call_3WCunBU7lCG1HHaLmnnRJn8I
145+
output: '42'
146+
type: function_call_output
147+
model: gpt-4.1
148+
previous_response_id: null
149+
stream: false
150+
tool_choice: auto
151+
tools:
152+
- description: null
153+
name: get_meaning_of_life
154+
parameters:
155+
additionalProperties: false
156+
properties: {}
157+
type: object
158+
strict: false
159+
type: function
160+
uri: https://api.openai.com/v1/responses
161+
response:
162+
headers:
163+
alt-svc:
164+
- h3=":443"; ma=86400
165+
connection:
166+
- keep-alive
167+
content-length:
168+
- '1908'
169+
content-type:
170+
- application/json
171+
openai-organization:
172+
- pydantic-28gund
173+
openai-processing-ms:
174+
- '1327'
175+
openai-project:
176+
- proj_dKobscVY9YJxeEaDJen54e3d
177+
openai-version:
178+
- '2020-10-01'
179+
strict-transport-security:
180+
- max-age=31536000; includeSubDomains; preload
181+
transfer-encoding:
182+
- chunked
183+
parsed_body:
184+
background: false
185+
billing:
186+
payer: developer
187+
created_at: 1758220198
188+
error: null
189+
id: resp_68cc4fa6a8a881a187b0fe1603057bff0307c6d4d2ee5985
190+
incomplete_details: null
191+
instructions: null
192+
max_output_tokens: null
193+
max_tool_calls: null
194+
metadata: {}
195+
model: gpt-4.1-2025-04-14
196+
object: response
197+
output:
198+
- content:
199+
- annotations: []
200+
logprobs: []
201+
text: |-
202+
The meaning of life, according to popular culture and famously in Douglas Adams' "The Hitchhiker's Guide to the Galaxy," is 42!
203+
204+
If you're looking for a deeper or philosophical answer, let me know your perspective or context, and I can elaborate further.
205+
type: output_text
206+
id: msg_68cc4fa7693081a184ff6f32e5209ab00307c6d4d2ee5985
207+
role: assistant
208+
status: completed
209+
type: message
210+
parallel_tool_calls: true
211+
previous_response_id: null
212+
prompt_cache_key: null
213+
reasoning:
214+
effort: null
215+
summary: null
216+
safety_identifier: null
217+
service_tier: default
218+
status: completed
219+
store: true
220+
temperature: 1.0
221+
text:
222+
format:
223+
type: text
224+
verbosity: medium
225+
tool_choice: auto
226+
tools:
227+
- description: null
228+
name: get_meaning_of_life
229+
parameters:
230+
additionalProperties: false
231+
properties: {}
232+
type: object
233+
strict: false
234+
type: function
235+
top_logprobs: 0
236+
top_p: 1.0
237+
truncation: disabled
238+
usage:
239+
input_tokens: 61
240+
input_tokens_details:
241+
cached_tokens: 0
242+
output_tokens: 56
243+
output_tokens_details:
244+
reasoning_tokens: 0
245+
total_tokens: 117
246+
user: null
247+
status:
248+
code: 200
249+
message: OK
250+
version: 1

0 commit comments

Comments
 (0)