Skip to content

Commit ff0fbe3

Browse files
GDaamnDouweM
andauthored
Add support for previous_response_id from Responses API (#2756)
Co-authored-by: Douwe Maan <[email protected]>
1 parent 773e1be commit ff0fbe3

File tree

6 files changed

+442
-0
lines changed

6 files changed

+442
-0
lines changed

docs/models/openai.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,56 @@ As of 7:48 AM on Wednesday, April 2, 2025, in Tokyo, Japan, the weather is cloud
143143

144144
You can learn more about the differences between the Responses API and Chat Completions API in the [OpenAI API docs](https://platform.openai.com/docs/guides/responses-vs-chat-completions).
145145

146+
#### Referencing earlier responses
147+
148+
The Responses API supports referencing earlier model responses in a new request using a `previous_response_id` parameter, to ensure the full [conversation state](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#passing-context-from-the-previous-response) including [reasoning items](https://platform.openai.com/docs/guides/reasoning#keeping-reasoning-items-in-context) are kept in context. This is available through the `openai_previous_response_id` field in
149+
[`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings].
150+
151+
```python
152+
from pydantic_ai import Agent
153+
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
154+
155+
model = OpenAIResponsesModel('gpt-5')
156+
agent = Agent(model=model)
157+
158+
result = agent.run_sync('The secret is 1234')
159+
model_settings = OpenAIResponsesModelSettings(
160+
openai_previous_response_id=result.all_messages()[-1].provider_response_id
161+
)
162+
result = agent.run_sync('What is the secret code?', model_settings=model_settings)
163+
print(result.output)
164+
#> 1234
165+
```
166+
167+
By passing the `provider_response_id` from an earlier run, you can allow the model to build on its own prior reasoning without needing to resend the full message history.
168+
169+
##### Automatically referencing earlier responses
170+
171+
When the `openai_previous_response_id` field is set to `'auto'`, Pydantic AI will automatically select the most recent `provider_response_id` from message history and omit messages that came before it, letting the OpenAI API leverage server-side history instead for improved efficiency.
172+
173+
```python
174+
from pydantic_ai import Agent
175+
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
176+
177+
model = OpenAIResponsesModel('gpt-5')
178+
agent = Agent(model=model)
179+
180+
result1 = agent.run_sync('Tell me a joke.')
181+
print(result1.output)
182+
#> Did you hear about the toothpaste scandal? They called it Colgate.
183+
184+
# When set to 'auto', the most recent provider_response_id
185+
# and messages after it are sent as request.
186+
model_settings = OpenAIResponsesModelSettings(openai_previous_response_id='auto')
187+
result2 = agent.run_sync(
188+
'Explain?',
189+
message_history=result1.new_messages(),
190+
model_settings=model_settings
191+
)
192+
print(result2.output)
193+
#> This is an excellent joke invented by Samuel Colvin, it needs no explanation.
194+
```
195+
146196
## OpenAI-compatible Models
147197

148198
Many providers and models are compatible with the OpenAI API, and can be used with `OpenAIChatModel` in Pydantic AI.

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,17 @@ class OpenAIResponsesModelSettings(OpenAIChatModelSettings, total=False):
222222
`medium`, and `high`.
223223
"""
224224

225+
openai_previous_response_id: Literal['auto'] | str
226+
"""The ID of a previous response from the model to use as the starting point for a continued conversation.
227+
228+
When set to `'auto'`, the request automatically uses the most recent
229+
`provider_response_id` from the message history and omits earlier messages.
230+
231+
This enables the model to use server-side conversation state and faithfully reference previous reasoning.
232+
See the [OpenAI Responses API documentation](https://platform.openai.com/docs/guides/reasoning#keeping-reasoning-items-in-context)
233+
for more information.
234+
"""
235+
225236

226237
@dataclass(init=False)
227238
class OpenAIChatModel(Model):
@@ -977,6 +988,10 @@ async def _responses_create(
977988
else:
978989
tool_choice = 'auto'
979990

991+
previous_response_id = model_settings.get('openai_previous_response_id')
992+
if previous_response_id == 'auto':
993+
previous_response_id, messages = self._get_previous_response_id_and_new_messages(messages)
994+
980995
instructions, openai_messages = await self._map_messages(messages, model_settings)
981996
reasoning = self._get_reasoning(model_settings)
982997

@@ -1027,6 +1042,7 @@ async def _responses_create(
10271042
truncation=model_settings.get('openai_truncation', NOT_GIVEN),
10281043
timeout=model_settings.get('timeout', NOT_GIVEN),
10291044
service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
1045+
previous_response_id=previous_response_id,
10301046
reasoning=reasoning,
10311047
user=model_settings.get('openai_user', NOT_GIVEN),
10321048
text=text or NOT_GIVEN,
@@ -1092,6 +1108,28 @@ def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam
10921108
),
10931109
}
10941110

1111+
def _get_previous_response_id_and_new_messages(
1112+
self, messages: list[ModelMessage]
1113+
) -> tuple[str | None, list[ModelMessage]]:
1114+
# When `openai_previous_response_id` is set to 'auto', the most recent
1115+
# `provider_response_id` from the message history is selected and all
1116+
# earlier messages are omitted. This allows the OpenAI SDK to reuse
1117+
# server-side history for efficiency. The returned tuple contains the
1118+
# `previous_response_id` (if found) and the trimmed list of messages.
1119+
previous_response_id = None
1120+
trimmed_messages: list[ModelMessage] = []
1121+
for m in reversed(messages):
1122+
if isinstance(m, ModelResponse) and m.provider_name == self.system:
1123+
previous_response_id = m.provider_response_id
1124+
break
1125+
else:
1126+
trimmed_messages.append(m)
1127+
1128+
if previous_response_id and trimmed_messages:
1129+
return previous_response_id, list(reversed(trimmed_messages))
1130+
else:
1131+
return None, messages
1132+
10951133
async def _map_messages( # noqa: C901
10961134
self, messages: list[ModelMessage], model_settings: OpenAIResponsesModelSettings
10971135
) -> tuple[str | NotGiven, list[responses.ResponseInputItemParam]]:
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-type:
11+
- application/json
12+
host:
13+
- api.openai.com
14+
method: POST
15+
parsed_body:
16+
input:
17+
- content: The secret key is sesame
18+
role: user
19+
instructions: ''
20+
model: gpt-5
21+
text:
22+
format:
23+
type: text
24+
uri: https://api.openai.com/v1/responses
25+
response:
26+
headers:
27+
content-type:
28+
- application/json
29+
parsed_body:
30+
created_at: 1743075629
31+
error: null
32+
id: resp_1234
33+
incomplete_details: null
34+
instructions: ''
35+
max_output_tokens: null
36+
metadata: {}
37+
model: gpt-5
38+
object: response
39+
output:
40+
- content:
41+
- annotations: []
42+
text: "Open sesame! What would you like to unlock?"
43+
type: output_text
44+
id: msg_test_previous_response_id
45+
role: assistant
46+
status: completed
47+
type: message
48+
parallel_tool_calls: true
49+
previous_response_id: null
50+
reasoning: null
51+
status: complete
52+
status_details: null
53+
tool_calls: null
54+
total_tokens: 15
55+
usage:
56+
input_tokens: 10
57+
input_tokens_details:
58+
cached_tokens: 0
59+
output_tokens: 1
60+
output_tokens_details:
61+
reasoning_tokens: 0
62+
total_tokens: 11
63+
status:
64+
code: 200
65+
message: OK
66+
- request:
67+
headers:
68+
accept:
69+
- application/json
70+
accept-encoding:
71+
- gzip, deflate
72+
connection:
73+
- keep-alive
74+
content-type:
75+
- application/json
76+
host:
77+
- api.openai.com
78+
method: POST
79+
parsed_body:
80+
input:
81+
- content: What is the secret key again?
82+
role: user
83+
instructions: ''
84+
model: gpt-5
85+
text:
86+
format:
87+
type: text
88+
previous_response_id: resp_1234
89+
uri: https://api.openai.com/v1/responses
90+
response:
91+
headers:
92+
content-type:
93+
- application/json
94+
parsed_body:
95+
created_at: 1743075630
96+
error: null
97+
id: resp_5678
98+
incomplete_details: null
99+
instructions: ''
100+
max_output_tokens: null
101+
metadata: {}
102+
model: gpt-5
103+
object: response
104+
output:
105+
- content:
106+
- annotations: []
107+
text: "sesame"
108+
type: output_text
109+
id: msg_test_previous_response_id
110+
role: assistant
111+
status: completed
112+
type: message
113+
parallel_tool_calls: true
114+
previous_response_id: resp_1234
115+
reasoning: null
116+
status: complete
117+
status_details: null
118+
tool_calls: null
119+
total_tokens: 15
120+
usage:
121+
input_tokens: 10
122+
input_tokens_details:
123+
cached_tokens: 0
124+
output_tokens: 1
125+
output_tokens_details:
126+
reasoning_tokens: 0
127+
total_tokens: 11
128+
status:
129+
code: 200
130+
message: OK
131+
version: 1
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-type:
11+
- application/json
12+
host:
13+
- api.openai.com
14+
method: POST
15+
parsed_body:
16+
input:
17+
- content: What is the first secret key?
18+
role: user
19+
instructions: ''
20+
model: gpt-5
21+
text:
22+
format:
23+
type: text
24+
previous_response_id: resp_68b9bda81f5c8197a5a51a20a9f4150a000497db2a4c777b
25+
uri: https://api.openai.com/v1/responses
26+
response:
27+
headers:
28+
content-type:
29+
- application/json
30+
parsed_body:
31+
created_at: 1743075630
32+
error: null
33+
id: resp_a4168b9bda81f5c8197a5a51a20a9f4150a000497db2a4c5
34+
incomplete_details: null
35+
instructions: ''
36+
max_output_tokens: null
37+
metadata: {}
38+
model: gpt-5
39+
object: response
40+
output:
41+
- content:
42+
- annotations: []
43+
text: "sesame"
44+
type: output_text
45+
id: msg_test_previous_response_id_auto
46+
role: assistant
47+
status: completed
48+
type: message
49+
parallel_tool_calls: true
50+
previous_response_id: resp_68b9bda81f5c8197a5a51a20a9f4150a000497db2a4c777b
51+
reasoning: null
52+
status: complete
53+
status_details: null
54+
tool_calls: null
55+
total_tokens: 15
56+
usage:
57+
input_tokens: 10
58+
input_tokens_details:
59+
cached_tokens: 0
60+
output_tokens: 1
61+
output_tokens_details:
62+
reasoning_tokens: 0
63+
total_tokens: 11
64+
status:
65+
code: 200
66+
message: OK
67+
version: 1

0 commit comments

Comments
 (0)