feat: add OpenAI Conversations API state support via OpenAIResponsesModelSettings.openai_conversation_id#5224
Conversation
|
@corytomlinson Thanks, plan looks good! Can you please generate code from it + verify that the implementation works for you (including |
|
@corytomlinson Thanks Cory! At the same time, I'm working on #5251. Any opinion on how they should interact? Should the OpenAI |
|
Good question. My instinct is to keep them separate by default. I see So I would not automatically stamp OpenAI's One thing that may be worth adding once #5251 is in: |
|
Following up on the #5251 question: OpenAI conversation IDs remain provider-specific, and |
|
Adding some evidence gathered during user testing. Below is a Logfire trace showing 2-turns where the first turn makes the request to create the durable conversation object in the
And here are the corresponding logs from OpenAI which validate the object and all referenced
There are no signs of any duplicate context in either Logfire or OpenAI which confirms intended behavior:
Not directly in scope for this feature, but I have also validated that server side compaction works when sending
Going to move this PR to ready for review. @DouweM please let me know if further work is necessary. |
There was a problem hiding this comment.
Pull request overview
Adds first-class support for OpenAI Responses API durable conversation state (conversation) in OpenAIResponsesModel, including automatic reuse and safe history trimming, plus tests/cassettes and documentation.
Changes:
- Introduces
OpenAIResponsesModelSettings.openai_conversation_id('auto'or concreteconv_...) and enforces mutual exclusivity withopenai_previous_response_id. - Sends
conversationon/responsesrequests, persists returned IDs inModelResponse.provider_details['conversation_id'], and trims already-stored history for matching OpenAI conversations (including streaming). - Adds integration tests + VCR cassettes, updates cassette header filtering, and documents durable conversation usage.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/models/test_openai_responses.py | Adds conversation-state integration tests and a helper to create/delete conversations. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_tool_call_continuation.yaml | Records tool-call continuation behavior when using conversation. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_streaming_provider_details.yaml | Records streaming events including conversation metadata. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_preserves_mismatched_history.yaml | Records behavior ensuring mismatched history isn’t incorrectly trimmed. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_explicit_and_auto.yaml | Records explicit conversation ID + 'auto' reuse behavior. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_auto_without_history.yaml | Records 'auto' behavior when no prior conversation exists. |
| tests/models/cassettes/test_openai_responses/test_openai_conversation_id_auto_respects_pydantic_ai_conversation_id.yaml | Records 'auto' scoping to the active Pydantic AI conversation_id. |
| tests/json_body_serializer.py | Filters additional OpenAI-related headers from recorded cassettes. |
| pydantic_ai_slim/pydantic_ai/models/openai.py | Implements openai_conversation_id, request mapping, response metadata capture, and history trimming logic. |
| docs/models/openai.md | Documents durable conversations and updates compaction compatibility notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @asynccontextmanager | ||
| async def _openai_conversation(openai_api_key: str) -> AsyncIterator[tuple[AsyncOpenAI, str]]: | ||
| async with AsyncOpenAI(api_key=openai_api_key) as async_client: | ||
| conversation = await async_client.conversations.create() | ||
| try: | ||
| yield async_client, conversation.id | ||
| finally: | ||
| await async_client.conversations.delete(conversation.id) |
There was a problem hiding this comment.
Good catch. Fixed in 55147b3 by quoting the AsyncOpenAI return annotation so the module can still import when the optional OpenAI dependency is unavailable. I also verified this with an import check that blocks openai, plus the focused openai_conversation_id cassette tests.
OpenAIResponsesModelSettings.openai_conversation_id
|
@corytomlinson Thank you Cory! |



Closes #5222.
Summary
OpenAIResponsesModelSettings.openai_conversation_idwith concrete conversation IDs and'auto'.conversationparameter, stores returned conversation IDs inModelResponse.provider_details['conversation_id'], and trims already-stored history for matching same-provider conversations.openai_previous_response_idandopenai_conversation_id, covers streaming metadata, and documents durable Conversations API usage.Update after #5251
After #5251 added generic Pydantic AI
conversation_idsupport, this PR keeps the OpenAI Conversations API ID provider-specific inModelResponse.provider_details['conversation_id'].openai_conversation_id='auto'now scopes reuse to the active Pydantic AI conversation when message-levelconversation_idvalues are available. This preventsconversation_id='new'from accidentally continuing the previous OpenAI server-side conversation, while explicitopenai_conversation_id='<id>'still allows deliberate reuse.Test plan
uv run pytest tests/models/test_openai_responses.py -k openai_conversation_id --record-mode=noneuv run pytest tests/test_examples.py -k 'docs/models/openai.md'uv run coverage run -m pytest tests/models/test_openai_responses.py -k openai_conversation_id --record-mode=noneuv run ruff check pydantic_ai_slim/pydantic_ai/models/openai.py tests/models/test_openai_responses.pyuv run ruff format --check pydantic_ai_slim/pydantic_ai/models/openai.py tests/models/test_openai_responses.pyChecklist