Skip to content

Fix responses.parse() memory leak from runtime generic schema rebuilds#3092

Open
erhan1209 wants to merge 1 commit intoopenai:mainfrom
erhan1209:fix/responses-parse-memory-leak-3084
Open

Fix responses.parse() memory leak from runtime generic schema rebuilds#3092
erhan1209 wants to merge 1 commit intoopenai:mainfrom
erhan1209:fix/responses-parse-memory-leak-3084

Conversation

@erhan1209
Copy link
Copy Markdown

  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Fix issue #3084 in responses.parse().

The parser was constructing parameterized generic response models at runtime:

  • ParsedResponseOutputText[TextFormatT]
  • ParsedResponseOutputMessage[TextFormatT]
  • ParsedResponse[TextFormatT]

With Pydantic v2, feeding unresolved generics into runtime construction can trigger repeated schema rebuilds instead of stable reuse, which can cause memory growth in long-running processes using responses.parse().

This change switches runtime construction in src/openai/lib/_parsing/_responses.py to the non-parameterized classes:

  • ParsedResponseOutputText
  • ParsedResponseOutputMessage
  • ParsedResponse

The generic return typing is preserved with cast(...), so the static typing behavior remains unchanged.

A regression test was added to verify that parse_response() only passes non-parameterized runtime response types into construct_type_unchecked.

Additional context & links

Fixes #3084

Verification:

  • $env:PYTHONPATH='src'; python -m pytest -q tests/lib/responses/test_responses.py -n 0
  • $env:PYTHONPATH='src'; python -m pytest -q tests/test_models.py tests/test_response.py -n 0

## Summary
This fixes issue `openai#3084`.

`responses.parse()` was constructing parameterized generic response models at runtime:
- `ParsedResponseOutputText[TextFormatT]`
- `ParsedResponseOutputMessage[TextFormatT]`
- `ParsedResponse[TextFormatT]`

Those unresolved generics can cause Pydantic to repeatedly rebuild schemas instead of reusing them, which leads to memory growth in long-running processes.

## What changed
- Switched runtime construction in `src/openai/lib/_parsing/_responses.py` to use the non-parameterized classes:
  - `ParsedResponseOutputText`
  - `ParsedResponseOutputMessage`
  - `ParsedResponse`
- Kept the generic return typing via `cast(...)`, so the public type experience stays the same.
- Added a regression test to verify `parse_response()` only passes non-parameterized runtime response types into `construct_type_unchecked`.

## Why this is safe
The generic parameter is only needed for static typing here. At runtime, the parsed response models already use non-parameterized field definitions outside `TYPE_CHECKING`, so constructing the base classes preserves behavior while avoiding repeated schema rebuilds.

## Verification
Ran:
- `$env:PYTHONPATH='src'; python -m pytest -q tests/lib/responses/test_responses.py -n 0`
- `$env:PYTHONPATH='src'; python -m pytest -q tests/test_models.py tests/test_response.py -n 0`
@erhan1209 erhan1209 requested a review from a team as a code owner April 15, 2026 14:27
Comment thread src/openai/lib/_parsing/_responses.py
@erhan1209 erhan1209 requested a review from afurm April 15, 2026 16:00
construct_type_unchecked(
type_=ParsedResponseOutputText,
value={
**item.to_dict(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using cast(Any, ...) to bypass construct_type_unchecked's type parameterization check discards Pydantic's validation at the call site. If value contains an unexpected field or wrong type, Pydantic will now silently accept it instead of raising a ValidationError. Consider adding an explicit model validation step after construction to preserve runtime safety:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing your concern, but I don’t think cast(...) changes the runtime behavior here.

cast(...) is only for static typing; the runtime construction path is still construct_type_unchecked(...), which this code was already using before this patch. The runtime change in this PR is only switching the type_= target from parameterized generics like ParsedResponseOutputText[TextFormatT] to the corresponding non-parameterized runtime classes, to avoid repeated schema rebuilds.

If your concern is with the use of construct_type_unchecked(...) more generally, or with the cast-based typing shape, I’m happy to adjust the implementation to express the same runtime behavior more clearly.

@erhan1209 erhan1209 requested a review from afurm April 15, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async Responses Structured Outputs Memory leak

2 participants