Skip to content

Commit 2e05a5c

Browse files
committed
add support for previous_response_id from Responses API
1 parent 01b6e51 commit 2e05a5c

File tree

6 files changed

+280
-0
lines changed

6 files changed

+280
-0
lines changed

docs/models/openai.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ 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+
The Responses API also supports referencing earlier model responses in a new request. This is available through the `openai_previous_response_id` field in
147+
[`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings].
148+
149+
```python
150+
from pydantic_ai import Agent
151+
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
152+
153+
model = OpenAIResponsesModel('gpt-4o')
154+
agent = Agent(model=model)
155+
156+
result = agent.run_sync('The secret is 1234')
157+
model_settings = OpenAIResponsesModelSettings(
158+
openai_previous_response_id=result.all_messages()[-1].provider_response_id
159+
)
160+
result = agent.run_sync('What is the secret code?', model_settings=model_settings)
161+
print(result.output)
162+
#> 1234
163+
```
164+
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.
165+
146166
## OpenAI-compatible Models
147167

148168
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ class OpenAIResponsesModelSettings(OpenAIChatModelSettings, total=False):
190190
`medium`, and `high`.
191191
"""
192192

193+
openai_previous_response_id: str
194+
"""The identifier of the most recent response to include in the API request.
195+
196+
This enables the model to reference previous reasoning traces.
197+
See the [OpenAI Responses API documentation](https://platform.openai.com/docs/guides/responses) for more information.
198+
"""
199+
193200

194201
@dataclass(init=False)
195202
class OpenAIChatModel(Model):
@@ -887,6 +894,18 @@ async def _responses_create(
887894
else:
888895
tool_choice = 'auto'
889896

897+
previous_response_id: str | None = None
898+
for message in reversed(messages):
899+
# Instead of sending the full message history, get provider_response_id
900+
# (openai-compatible) from the latest matching ModelResponse and
901+
# pass it to the next ModelRequest as previous_response_id to preserve context.
902+
# Since the full history isn't needed, only the latest message is kept.
903+
if isinstance(message, ModelResponse) and message.model_name:
904+
if self._model_name in message.model_name:
905+
previous_response_id = message.provider_response_id
906+
messages = [messages[-1]]
907+
break
908+
890909
instructions, openai_messages = await self._map_messages(messages)
891910
reasoning = self._get_reasoning(model_settings)
892911

@@ -932,6 +951,8 @@ async def _responses_create(
932951
truncation=model_settings.get('openai_truncation', NOT_GIVEN),
933952
timeout=model_settings.get('timeout', NOT_GIVEN),
934953
service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
954+
previous_response_id=previous_response_id
955+
or model_settings.get('openai_previous_response_id', NOT_GIVEN),
935956
reasoning=reasoning,
936957
user=model_settings.get('openai_user', NOT_GIVEN),
937958
text=text or NOT_GIVEN,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 secret key again?
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: 1743075630
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: "sesame"
43+
type: output_text
44+
id: msg_test_invalid_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+
version: 1
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

tests/models/test_openai_responses.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,44 @@ async def test_openai_responses_verbosity(allow_model_requests: None, openai_api
10821082
assert result.output == snapshot('4')
10831083

10841084

1085+
@pytest.mark.vcr()
1086+
async def test_openai_previous_response_id(allow_model_requests: None, openai_api_key: str):
1087+
"""Test if previous responses are detected via previous_response_id in settings"""
1088+
model = OpenAIResponsesModel('gpt-5', provider=OpenAIProvider(api_key=openai_api_key))
1089+
agent = Agent(model=model)
1090+
result = await agent.run('The secret key is sesame')
1091+
settings = OpenAIResponsesModelSettings(openai_previous_response_id=result.all_messages()[-1].provider_response_id) # type: ignore
1092+
result = await agent.run('What is the secret code?', model_settings=settings)
1093+
assert result.output == snapshot('sesame')
1094+
1095+
1096+
@pytest.mark.vcr()
1097+
async def test_openai_invalid_previous_response_id(allow_model_requests: None, openai_api_key: str):
1098+
"""Test if invalid previous response id is ignored when history contains non-OpenAI responses"""
1099+
history = [
1100+
ModelRequest(
1101+
parts=[
1102+
UserPromptPart(
1103+
content='The secret key is sesame',
1104+
),
1105+
],
1106+
),
1107+
ModelResponse(
1108+
parts=[
1109+
TextPart(content='Open sesame! What would you like to unlock?'),
1110+
],
1111+
model_name='claude-3-5-sonnet-latest',
1112+
provider_name='anthropic',
1113+
provider_response_id='msg_1234',
1114+
),
1115+
]
1116+
1117+
model = OpenAIResponsesModel('gpt-5', provider=OpenAIProvider(api_key=openai_api_key))
1118+
agent = Agent(model=model)
1119+
result = await agent.run('What is the secret code?', message_history=history)
1120+
assert result.output == snapshot('sesame')
1121+
1122+
10851123
async def test_openai_responses_usage_without_tokens_details(allow_model_requests: None):
10861124
c = response_message(
10871125
[

tests/test_examples.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,10 @@ async def model_logic( # noqa: C901
620620
return ModelResponse(parts=list(response))
621621
else:
622622
return ModelResponse(parts=[response])
623+
elif m.content == 'The secret is 1234':
624+
return ModelResponse(parts=[TextPart('The secret is safe with me')])
625+
elif m.content == 'What is the secret code?':
626+
return ModelResponse(parts=[TextPart('1234')])
623627

624628
elif isinstance(m, ToolReturnPart) and m.tool_name == 'roulette_wheel':
625629
win = m.content == 'winner'

0 commit comments

Comments
 (0)