diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 86f44b0e15..c0457d7fea 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -181,10 +181,20 @@ def __init__(self, *, completion: ChatCompletion) -> None: class ContentFilterFinishReasonError(OpenAIError): - def __init__(self) -> None: - super().__init__( - f"Could not parse response content as the request was rejected by the content filter", - ) + completion: ChatCompletion + """The completion that caused this error. + + Note: this will *not* be a complete `ChatCompletion` object when streaming as `usage` + will not be included. + """ + + def __init__(self, *, completion: ChatCompletion) -> None: + msg = "Could not parse response content as the request was rejected by the content filter" + if completion.usage: + msg += f" - {completion.usage}" + + super().__init__(msg) + self.completion = completion class InvalidWebhookSignatureError(ValueError): diff --git a/src/openai/lib/_parsing/_completions.py b/src/openai/lib/_parsing/_completions.py index 7a1bded1de..ebea435e93 100644 --- a/src/openai/lib/_parsing/_completions.py +++ b/src/openai/lib/_parsing/_completions.py @@ -100,7 +100,7 @@ def parse_chat_completion( raise LengthFinishReasonError(completion=chat_completion) if choice.finish_reason == "content_filter": - raise ContentFilterFinishReasonError() + raise ContentFilterFinishReasonError(completion=chat_completion) message = choice.message diff --git a/src/openai/lib/streaming/chat/_completions.py b/src/openai/lib/streaming/chat/_completions.py index 5f072cafbd..7ecc70d365 100644 --- a/src/openai/lib/streaming/chat/_completions.py +++ b/src/openai/lib/streaming/chat/_completions.py @@ -431,7 +431,7 @@ def _accumulate_chunk(self, chunk: ChatCompletionChunk) -> ParsedChatCompletionS raise LengthFinishReasonError(completion=completion_snapshot) if choice.finish_reason == "content_filter": - raise ContentFilterFinishReasonError() + raise ContentFilterFinishReasonError(completion=completion_snapshot) if ( choice_snapshot.message.content diff --git a/tests/lib/chat/test_completions.py b/tests/lib/chat/test_completions.py index 85bab4f095..07b458a481 100644 --- a/tests/lib/chat/test_completions.py +++ b/tests/lib/chat/test_completions.py @@ -547,6 +547,39 @@ class Location(BaseModel): ) +@pytest.mark.respx(base_url=base_url) +def test_parse_content_filter_reached(client: OpenAI, respx_mock: MockRouter) -> None: + class Location(BaseModel): + city: str + temperature: float + units: Literal["c", "f"] + + with pytest.raises(openai.ContentFilterFinishReasonError) 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-ABfvvX7eB1KsfeZj8VcF3z7G7SbaA", "object": "chat.completion", "created": 1727346163, "model": "gpt-4o-2024-08-06", "choices": [{"index": 0, "message": {"role": "assistant", "content": null, "refusal": null}, "logprobs": null, "finish_reason": "content_filter"}], "usage": {"prompt_tokens": 79, "completion_tokens": 0, "total_tokens": 79, "completion_tokens_details": {"reasoning_tokens": 0}}, "system_fingerprint": "fp_7568d46099"}' + ), + path="/chat/completions", + mock_client=client, + respx_mock=respx_mock, + ) + + err = exc_info.value + assert err.completion.choices[0].finish_reason == "content_filter" + assert err.completion.usage is not None + assert err.completion.usage.total_tokens == 79 + + @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): diff --git a/tests/lib/chat/test_completions_streaming.py b/tests/lib/chat/test_completions_streaming.py index eb3a0973ac..645fe3a016 100644 --- a/tests/lib/chat/test_completions_streaming.py +++ b/tests/lib/chat/test_completions_streaming.py @@ -396,6 +396,41 @@ class Location(BaseModel): ) +def test_parse_content_filter_reached() -> None: + class Location(BaseModel): + city: str + temperature: float + units: Literal["c", "f"] + + state = ChatCompletionStreamState(response_format=Location) + chunk = ChatCompletionChunk.model_validate( + { + "id": "chatcmpl-ABfvvX7eB1KsfeZj8VcF3z7G7SbaA", + "object": "chat.completion.chunk", + "created": 1727346163, + "model": "gpt-4o-2024-08-06", + "choices": [ + { + "index": 0, + "delta": {"role": "assistant"}, + "logprobs": None, + "finish_reason": "content_filter", + } + ], + } + ) + + list(state.handle_chunk(chunk)) + + with pytest.raises(openai.ContentFilterFinishReasonError) as exc_info: + state.get_final_completion() + + err = exc_info.value + assert err.completion.choices[0].finish_reason == "content_filter" + assert err.completion.usage is None + assert err.completion.choices[0].message.content is None + + @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):