From e74abe2dab63f9192bec3929aebc6f32ae73a17d Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 14:22:41 +0100 Subject: [PATCH 01/10] fix: wrap ValidationError in parse() with helpful error context When beta.chat.completions.parse() receives malformed or truncated JSON from the API, pydantic.ValidationError was raised directly without any context. Now catches pydantic.ValidationError and json.JSONDecodeError in _parse_content() and wraps them in a new ContentFormatError that includes the raw content string for debugging. Fixes #1763 --- src/openai/__init__.py | 2 + src/openai/_exceptions.py | 20 +++++++++ src/openai/lib/_parsing/_completions.py | 17 +++++--- tests/lib/chat/test_completions.py | 58 +++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/openai/__init__.py b/src/openai/__init__.py index b2093ada68..60e181efb2 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -24,6 +24,7 @@ APITimeoutError, BadRequestError, APIConnectionError, + ContentFormatError, AuthenticationError, InternalServerError, PermissionDeniedError, @@ -65,6 +66,7 @@ "InternalServerError", "LengthFinishReasonError", "ContentFilterFinishReasonError", + "ContentFormatError", "InvalidWebhookSignatureError", "Timeout", "RequestOptions", diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 09016dfedb..2db7e3df5e 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -24,6 +24,7 @@ "InternalServerError", "LengthFinishReasonError", "ContentFilterFinishReasonError", + "ContentFormatError", "InvalidWebhookSignatureError", ] @@ -157,5 +158,24 @@ def __init__(self) -> None: ) +class ContentFormatError(OpenAIError): + """Raised when the API returns content that cannot be parsed into the expected response format. + + This typically happens when the model returns malformed or truncated JSON that + does not match the expected pydantic model schema. + """ + + raw_content: str + """The raw content string returned by the API that failed to parse.""" + + def __init__(self, *, raw_content: str, error: Exception) -> None: + super().__init__( + f"Could not parse response content as the response did not match the expected format: {error}. " + f"Raw content: {raw_content!r}" + ) + self.raw_content = raw_content + self.__cause__ = error + + class InvalidWebhookSignatureError(ValueError): """Raised when a webhook signature is invalid, meaning the computed signature does not match the expected signature.""" diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 7a1bded1de..87b85468ec 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -25,7 +25,7 @@ ChatCompletionFunctionToolParam, completion_create_params, ) -from ..._exceptions import LengthFinishReasonError, ContentFilterFinishReasonError +from ..._exceptions import ContentFormatError, LengthFinishReasonError, ContentFilterFinishReasonError from ...types.shared_params import FunctionDefinition from ...types.chat.completion_create_params import ResponseFormat as ResponseFormatParam from ...types.chat.chat_completion_message_function_tool_call import Function @@ -241,14 +241,17 @@ def is_parseable_tool(input_tool: ChatCompletionToolUnionParam) -> bool: def _parse_content(response_format: type[ResponseFormatT], content: str) -> ResponseFormatT: - if is_basemodel_type(response_format): - return cast(ResponseFormatT, model_parse_json(response_format, content)) + try: + if is_basemodel_type(response_format): + return cast(ResponseFormatT, model_parse_json(response_format, content)) - if is_dataclass_like_type(response_format): - if PYDANTIC_V1: - raise TypeError(f"Non BaseModel types are only supported with Pydantic v2 - {response_format}") + if is_dataclass_like_type(response_format): + if PYDANTIC_V1: + raise TypeError(f"Non BaseModel types are only supported with Pydantic v2 - {response_format}") - return pydantic.TypeAdapter(response_format).validate_json(content) + return pydantic.TypeAdapter(response_format).validate_json(content) + except (pydantic.ValidationError, json.JSONDecodeError) as exc: + raise ContentFormatError(raw_content=content, error=exc) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}") diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index 85bab4f095..23d05bbe92 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -547,6 +547,64 @@ class Location(BaseModel): ) +@pytest.mark.respx(base_url=base_url) +def test_parse_malformed_json_content(client: OpenAI, respx_mock: MockRouter) -> None: + class Location(BaseModel): + city: str + temperature: float + units: Literal["c", "f"] + + with pytest.raises(openai.ContentFormatError) as exc_info: + make_snapshot_request( + lambda c: c.chat.completions.parse( + model="gpt-4o-2024-08-06", + messages=[ + { + "role": "user", + "content": "What's the weather like in SF?", + }, + ], + response_format=Location, + ), + content_snapshot=snapshot( + '{"id": "chatcmpl-truncated", "object": "chat.completion", "created": 1727346163, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"city\\": \\"San", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 79, "completion_tokens": 5, "total_tokens": 84}, "system_fingerprint": "fp_test"}' + ), + path="/chat/completions", + mock_client=client, + respx_mock=respx_mock, + ) + assert '{"city": "San' in exc_info.value.raw_content + + +@pytest.mark.respx(base_url=base_url) +def test_parse_invalid_json_schema(client: OpenAI, respx_mock: MockRouter) -> None: + class Location(BaseModel): + city: str + temperature: float + units: Literal["c", "f"] + + with pytest.raises(openai.ContentFormatError) as exc_info: + make_snapshot_request( + lambda c: c.chat.completions.parse( + model="gpt-4o-2024-08-06", + messages=[ + { + "role": "user", + "content": "What's the weather like in SF?", + }, + ], + response_format=Location, + ), + content_snapshot=snapshot( + '{"id": "chatcmpl-badschema", "object": "chat.completion", "created": 1727346163, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": "{\\"city\\": \\"San Francisco\\"}", "refusal": null}, "logprobs": null, "finish_reason": "stop"}], "usage": {"prompt_tokens": 79, "completion_tokens": 10, "total_tokens": 89}, "system_fingerprint": "fp_test"}' + ), + path="/chat/completions", + mock_client=client, + respx_mock=respx_mock, + ) + assert exc_info.value.raw_content == '{"city": "San Francisco"}' + + @pytest.mark.respx(base_url=base_url) def test_parse_pydantic_model_refusal(client: OpenAI, respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch) -> None: class Location(BaseModel): From 81f3e5b6e14903017804650aa381feb3e2a0307c Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 14:40:12 +0100 Subject: [PATCH 02/10] fix: address review feedback on ContentFormatError - Truncate raw_content in error message to 500 chars to prevent unbounded messages and reduce sensitive data exposure - Remove redundant self.__cause__ assignment (raise...from sets it); store error on dedicated .error attribute instead - Update docstring to cover both Pydantic models and dataclass-like types validated via TypeAdapter Refs: #1763 --- src/openai/_exceptions.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 2db7e3df5e..59a189b69e 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -162,19 +162,25 @@ class ContentFormatError(OpenAIError): """Raised when the API returns content that cannot be parsed into the expected response format. This typically happens when the model returns malformed or truncated JSON that - does not match the expected pydantic model schema. + does not match the expected schema for the response type (for example, a Pydantic + model or dataclass validated via ``pydantic.TypeAdapter``). """ raw_content: str """The raw content string returned by the API that failed to parse.""" + error: Exception + """The underlying parsing exception.""" + def __init__(self, *, raw_content: str, error: Exception) -> None: + max_preview = 500 + preview = raw_content if len(raw_content) <= max_preview else raw_content[:max_preview] + "... (truncated)" super().__init__( f"Could not parse response content as the response did not match the expected format: {error}. " - f"Raw content: {raw_content!r}" + f"Raw content: {preview!r}" ) self.raw_content = raw_content - self.__cause__ = error + self.error = error class InvalidWebhookSignatureError(ValueError): From 24438b0cb6246ab203967b3df3de4acbcea16106 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 16:25:44 +0100 Subject: [PATCH 03/10] fix(parsing): enrich ContentFormatError context safely Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/openai/_exceptions.py | 39 +++++++++++++++++++++---- src/openai/lib/_parsing/_completions.py | 2 +- tests/lib/chat/test_completions.py | 4 +++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 59a189b69e..72c962eefe 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING, Any, Optional, cast from typing_extensions import Literal @@ -169,19 +170,47 @@ class ContentFormatError(OpenAIError): raw_content: str """The raw content string returned by the API that failed to parse.""" + expected_response_format: str | None + """Best-effort name of the expected response format type.""" + error: Exception """The underlying parsing exception.""" - def __init__(self, *, raw_content: str, error: Exception) -> None: - max_preview = 500 - preview = raw_content if len(raw_content) <= max_preview else raw_content[:max_preview] + "... (truncated)" + def __init__(self, *, raw_content: str, error: Exception, response_format: object | None = None) -> None: + expected_response_format = _response_format_name(response_format) + expected_details = ( + f" Expected response format: {expected_response_format}." + if expected_response_format is not None + else "" + ) super().__init__( - f"Could not parse response content as the response did not match the expected format: {error}. " - f"Raw content: {preview!r}" + f"Could not parse response content as the response did not match the expected format." + f"{expected_details} Validation error: {_format_parse_error(error)}." ) self.raw_content = raw_content + self.expected_response_format = expected_response_format self.error = error +def _response_format_name(response_format: object | None) -> str | None: + if response_format is None: + return None + return cast(str, getattr(response_format, "__name__", None) or getattr(response_format, "__qualname__", None) or repr(response_format)) + + +def _format_parse_error(error: Exception) -> str: + if isinstance(error, json.JSONDecodeError): + return f"{error.msg} (line {error.lineno}, column {error.colno})" + + errors_fn = getattr(error, "errors", None) + if callable(errors_fn): + try: + return repr(errors_fn(include_input=False)) + except TypeError: + return repr(errors_fn()) + + return str(error) + + class InvalidWebhookSignatureError(ValueError): """Raised when a webhook signature is invalid, meaning the computed signature does not match the expected signature.""" diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 87b85468ec..9d7483f235 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -251,7 +251,7 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp return pydantic.TypeAdapter(response_format).validate_json(content) except (pydantic.ValidationError, json.JSONDecodeError) as exc: - raise ContentFormatError(raw_content=content, error=exc) from exc + raise ContentFormatError(raw_content=content, error=exc, response_format=response_format) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}") diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index 23d05bbe92..3848d52447 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -574,6 +574,8 @@ class Location(BaseModel): respx_mock=respx_mock, ) assert '{"city": "San' in exc_info.value.raw_content + assert exc_info.value.expected_response_format == "Location" + assert '{"city": "San' not in str(exc_info.value) @pytest.mark.respx(base_url=base_url) @@ -603,6 +605,8 @@ class Location(BaseModel): respx_mock=respx_mock, ) assert exc_info.value.raw_content == '{"city": "San Francisco"}' + assert exc_info.value.expected_response_format == "Location" + assert '{"city": "San Francisco"}' not in str(exc_info.value) @pytest.mark.respx(base_url=base_url) From 4290c9a10d06b6f20b4d8df3e7eb01b7b30f8831 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 17:24:26 +0100 Subject: [PATCH 04/10] fix(review): truncate raw_content in ContentFormatError message Include truncated raw_content (first 500 chars) in exception message for better debugging while avoiding unbounded output. Full content remains accessible via the raw_content attribute. Update tests to reflect the new message format and add truncation coverage test. Refs: #2917 --- src/openai/_exceptions.py | 15 ++++++++++----- tests/lib/chat/test_completions.py | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 72c962eefe..255fc165d2 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -179,13 +179,13 @@ class ContentFormatError(OpenAIError): def __init__(self, *, raw_content: str, error: Exception, response_format: object | None = None) -> None: expected_response_format = _response_format_name(response_format) expected_details = ( - f" Expected response format: {expected_response_format}." - if expected_response_format is not None - else "" + f" Expected response format: {expected_response_format}." if expected_response_format is not None else "" ) + truncated_content = raw_content[:500] + "..." if len(raw_content) > 500 else raw_content super().__init__( f"Could not parse response content as the response did not match the expected format." - f"{expected_details} Validation error: {_format_parse_error(error)}." + f"{expected_details} Raw content: {truncated_content!r}." + f" Validation error: {_format_parse_error(error)}." ) self.raw_content = raw_content self.expected_response_format = expected_response_format @@ -195,7 +195,12 @@ def __init__(self, *, raw_content: str, error: Exception, response_format: objec def _response_format_name(response_format: object | None) -> str | None: if response_format is None: return None - return cast(str, getattr(response_format, "__name__", None) or getattr(response_format, "__qualname__", None) or repr(response_format)) + return cast( + str, + getattr(response_format, "__name__", None) + or getattr(response_format, "__qualname__", None) + or repr(response_format), + ) def _format_parse_error(error: Exception) -> str: diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index 3848d52447..8614e81274 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -575,7 +575,7 @@ class Location(BaseModel): ) assert '{"city": "San' in exc_info.value.raw_content assert exc_info.value.expected_response_format == "Location" - assert '{"city": "San' not in str(exc_info.value) + assert "Raw content:" in str(exc_info.value) @pytest.mark.respx(base_url=base_url) @@ -606,7 +606,21 @@ class Location(BaseModel): ) assert exc_info.value.raw_content == '{"city": "San Francisco"}' assert exc_info.value.expected_response_format == "Location" - assert '{"city": "San Francisco"}' not in str(exc_info.value) + assert "Raw content:" in str(exc_info.value) + + +def test_content_format_error_truncates_raw_content() -> None: + """Verify raw_content is truncated in the exception message for very large payloads.""" + long_content = "x" * 1000 + err = openai.ContentFormatError(raw_content=long_content, error=ValueError("bad"), response_format=None) + msg = str(err) + # Full raw_content is preserved on the attribute + assert len(err.raw_content) == 1000 + # Message should contain truncated version (500 chars + "...") + assert "xxx..." in msg + assert len(long_content) > 500 # sanity + # Should not contain the full 1000-char string in the message + assert long_content not in msg @pytest.mark.respx(base_url=base_url) From 06e4eda70813177967fa39d1c62215fa39295273 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 17:45:53 +0100 Subject: [PATCH 05/10] fix: apply code review suggestions Apply reviewer code suggestions from PR review. --- src/openai/_exceptions.py | 13 +++++++++++-- src/openai/lib/_parsing/_completions.py | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 255fc165d2..5fe7d1877d 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -169,9 +169,18 @@ class ContentFormatError(OpenAIError): raw_content: str """The raw content string returned by the API that failed to parse.""" + # Avoid including the full raw_content in the exception message to prevent + # leaking large or sensitive responses into logs/tracebacks. A truncated + # preview is included instead; the full content is available via the + # `raw_content` attribute for debugging purposes. + preview = raw_content + max_preview_length = 200 + if len(preview) > max_preview_length: + preview = preview[:max_preview_length] + "... [truncated]" - expected_response_format: str | None - """Best-effort name of the expected response format type.""" + super().__init__( + f"Could not parse response content as the response did not match the expected format: {error}. " + f"Raw content preview: {preview!r}" error: Exception """The underlying parsing exception.""" diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 9d7483f235..ec202c3942 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -251,7 +251,8 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp return pydantic.TypeAdapter(response_format).validate_json(content) except (pydantic.ValidationError, json.JSONDecodeError) as exc: - raise ContentFormatError(raw_content=content, error=exc, response_format=response_format) from exc + expected_type = getattr(response_format, "__qualname__", getattr(response_format, "__name__", repr(response_format))) + raise ContentFormatError(raw_content=content, error=exc, expected_type=expected_type) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}") From ab7f5f415eb282dee92cc383c7e9b61f1a182360 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 18:26:32 +0100 Subject: [PATCH 06/10] fix: apply code review suggestions --- src/openai/_exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 5fe7d1877d..0b75068811 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -162,7 +162,8 @@ def __init__(self) -> None: class ContentFormatError(OpenAIError): """Raised when the API returns content that cannot be parsed into the expected response format. - This typically happens when the model returns malformed or truncated JSON that + does not match the expected schema for the response type (for example, a Pydantic + model or dataclass validated via `pydantic.TypeAdapter`). does not match the expected schema for the response type (for example, a Pydantic model or dataclass validated via ``pydantic.TypeAdapter``). """ From b20d3a8dcf3330e8f119ad0e59823c3c7cf2bc12 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Mon, 2 Mar 2026 18:29:03 +0100 Subject: [PATCH 07/10] fix: apply code review suggestions Refs: #2917 --- src/openai/_exceptions.py | 12 ------------ src/openai/lib/_parsing/_completions.py | 3 +-- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 0b75068811..188ca71920 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -170,18 +170,6 @@ class ContentFormatError(OpenAIError): raw_content: str """The raw content string returned by the API that failed to parse.""" - # Avoid including the full raw_content in the exception message to prevent - # leaking large or sensitive responses into logs/tracebacks. A truncated - # preview is included instead; the full content is available via the - # `raw_content` attribute for debugging purposes. - preview = raw_content - max_preview_length = 200 - if len(preview) > max_preview_length: - preview = preview[:max_preview_length] + "... [truncated]" - - super().__init__( - f"Could not parse response content as the response did not match the expected format: {error}. " - f"Raw content preview: {preview!r}" error: Exception """The underlying parsing exception.""" diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index ec202c3942..9d7483f235 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -251,8 +251,7 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp return pydantic.TypeAdapter(response_format).validate_json(content) except (pydantic.ValidationError, json.JSONDecodeError) as exc: - expected_type = getattr(response_format, "__qualname__", getattr(response_format, "__name__", repr(response_format))) - raise ContentFormatError(raw_content=content, error=exc, expected_type=expected_type) from exc + raise ContentFormatError(raw_content=content, error=exc, response_format=response_format) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}") From cc9b1c3f7129188141dfcc79bb9f5a4f17654a02 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Mon, 2 Mar 2026 18:30:02 +0100 Subject: [PATCH 08/10] fix: clean up ContentFormatError docstring Refs: #2917 --- src/openai/_exceptions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 188ca71920..b1487aa205 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -162,10 +162,9 @@ def __init__(self) -> None: class ContentFormatError(OpenAIError): """Raised when the API returns content that cannot be parsed into the expected response format. + This typically happens when the model returns malformed or truncated JSON that does not match the expected schema for the response type (for example, a Pydantic model or dataclass validated via `pydantic.TypeAdapter`). - does not match the expected schema for the response type (for example, a Pydantic - model or dataclass validated via ``pydantic.TypeAdapter``). """ raw_content: str From 48b3532205e2d629bb663612191e66da7c26f903 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 2 Mar 2026 19:40:26 +0100 Subject: [PATCH 09/10] fix: apply code review suggestions --- src/openai/_exceptions.py | 13 +++++++++++-- src/openai/lib/_parsing/_completions.py | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index b1487aa205..74ac675c8c 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -169,9 +169,18 @@ class ContentFormatError(OpenAIError): raw_content: str """The raw content string returned by the API that failed to parse.""" + # Avoid including the full raw_content in the exception message to prevent + # leaking large or sensitive responses into logs/tracebacks. A truncated + # preview is included instead; the full content is available via the + # `raw_content` attribute for debugging purposes. + preview = raw_content + max_preview_length = 200 + if len(preview) > max_preview_length: + preview = preview[:max_preview_length] + "... [truncated]" - error: Exception - """The underlying parsing exception.""" + super().__init__( + f"Could not parse response content as the response did not match the expected format: {error}. " + f"Raw content preview: {preview!r}" def __init__(self, *, raw_content: str, error: Exception, response_format: object | None = None) -> None: expected_response_format = _response_format_name(response_format) diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 9d7483f235..ec202c3942 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -251,7 +251,8 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp return pydantic.TypeAdapter(response_format).validate_json(content) except (pydantic.ValidationError, json.JSONDecodeError) as exc: - raise ContentFormatError(raw_content=content, error=exc, response_format=response_format) from exc + expected_type = getattr(response_format, "__qualname__", getattr(response_format, "__name__", repr(response_format))) + raise ContentFormatError(raw_content=content, error=exc, expected_type=expected_type) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}") From 78ef382848c51e272044b498beaee588accb0521 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Mon, 2 Mar 2026 21:01:00 +0100 Subject: [PATCH 10/10] fix: resolve ContentFormatError syntax error and align expected_type/response_format parameter - Remove dangling code block from _exceptions.py ContentFormatError class that caused IndentationError - Fix _completions.py to pass response_format= instead of expected_type= to ContentFormatError --- src/openai/_exceptions.py | 12 ------------ src/openai/lib/_parsing/_completions.py | 3 +-- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 74ac675c8c..0712ed54f2 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -169,18 +169,6 @@ class ContentFormatError(OpenAIError): raw_content: str """The raw content string returned by the API that failed to parse.""" - # Avoid including the full raw_content in the exception message to prevent - # leaking large or sensitive responses into logs/tracebacks. A truncated - # preview is included instead; the full content is available via the - # `raw_content` attribute for debugging purposes. - preview = raw_content - max_preview_length = 200 - if len(preview) > max_preview_length: - preview = preview[:max_preview_length] + "... [truncated]" - - super().__init__( - f"Could not parse response content as the response did not match the expected format: {error}. " - f"Raw content preview: {preview!r}" def __init__(self, *, raw_content: str, error: Exception, response_format: object | None = None) -> None: expected_response_format = _response_format_name(response_format) diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index ec202c3942..9d7483f235 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -251,8 +251,7 @@ def _parse_content(response_format: type[ResponseFormatT], content: str) -> Resp return pydantic.TypeAdapter(response_format).validate_json(content) except (pydantic.ValidationError, json.JSONDecodeError) as exc: - expected_type = getattr(response_format, "__qualname__", getattr(response_format, "__name__", repr(response_format))) - raise ContentFormatError(raw_content=content, error=exc, expected_type=expected_type) from exc + raise ContentFormatError(raw_content=content, error=exc, response_format=response_format) from exc raise TypeError(f"Unable to automatically parse response format type {response_format}")