Fix responses.parse() memory leak from runtime generic schema rebuilds#3092
Fix responses.parse() memory leak from runtime generic schema rebuilds#3092erhan1209 wants to merge 1 commit intoopenai:mainfrom
Conversation
## 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`
| construct_type_unchecked( | ||
| type_=ParsedResponseOutputText, | ||
| value={ | ||
| **item.to_dict(), |
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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.
Changes being requested
Fix issue
#3084inresponses.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.pyto the non-parameterized classes:ParsedResponseOutputTextParsedResponseOutputMessageParsedResponseThe 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 intoconstruct_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