From 54af90d7ba382a0fa1d1c95e0f307effb0bbbd79 Mon Sep 17 00:00:00 2001 From: Mukunda Katta Date: Wed, 15 Apr 2026 00:42:22 -0700 Subject: [PATCH] fix(parsing): drop TextFormatT parameterization in parse_response (#3084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3084. parse_response() called construct_type_unchecked with three types parameterized by a free TypeVar (ParsedResponseOutputText[TextFormatT], ParsedResponseOutputMessage[TextFormatT], ParsedResponse[TextFormatT]). Pydantic cannot resolve a free TypeVar, so model_rebuild(raise_errors=False) returns False on every invocation. MockCoreSchema._built_memo only caches the rebuilt schema when model_rebuild succeeds; when it fails, the cache is never set and a new Rust-backed SchemaValidator/SchemaSerializer is allocated on every parse_response() call. Result: heavy pydantic-core objects leak without bound whenever AsyncResponses.parse() is called in a long-lived process. This is observable as a flame-graph spike (see #3084 for the screenshot). Fix (per issue author's diagnosis): drop the [TextFormatT] parameterization at the runtime type_= argument. At runtime Python's generics are erased anyway — the constructed object's type is identical either way. Only the pydantic schema rebuild path differs: with the non-parameterized generic, model_rebuild succeeds and the schema is cached in _built_memo. Tests: all 5 existing tests in tests/lib/responses/test_responses.py continue to pass (verified locally). No behavior change for callers. Signed-off-by: Mukunda Katta --- src/openai/lib/_parsing/_responses.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/openai/lib/_parsing/_responses.py b/src/openai/lib/_parsing/_responses.py index 8853a0749f..07f454e0c6 100644 --- a/src/openai/lib/_parsing/_responses.py +++ b/src/openai/lib/_parsing/_responses.py @@ -68,7 +68,11 @@ def parse_response( content_list.append( construct_type_unchecked( - type_=ParsedResponseOutputText[TextFormatT], + # Drop the TextFormatT parameterization: pydantic cannot resolve a free + # TypeVar so model_rebuild() always returns False, which means + # MockCoreSchema._built_memo is never populated and a new Rust-backed + # SchemaValidator is allocated on every call. See issue #3084. + type_=ParsedResponseOutputText, value={ **item.to_dict(), "parsed": parse_text(item.text, text_format=text_format), @@ -78,7 +82,8 @@ def parse_response( output_list.append( construct_type_unchecked( - type_=ParsedResponseOutputMessage[TextFormatT], + # See note above: non-parameterized generic keeps the schema cache hot. + type_=ParsedResponseOutputMessage, value={ **output.to_dict(), "content": content_list, @@ -130,7 +135,10 @@ def parse_response( output_list.append(output) return construct_type_unchecked( - type_=ParsedResponse[TextFormatT], + # See note above: non-parameterized generic keeps the schema cache hot. + # At runtime Python's generics are erased, so the constructed object's type + # is identical either way — only the pydantic schema rebuild path differs. + type_=ParsedResponse, value={ **response.to_dict(), "output": output_list,