diff --git a/src/anthropic/_base_client.py b/src/anthropic/_base_client.py index f7d910e4..fbfba66f 100644 --- a/src/anthropic/_base_client.py +++ b/src/anthropic/_base_client.py @@ -531,9 +531,17 @@ def _process_response( # in the response, e.g. application/json; charset=utf-8 content_type, *_ = response.headers.get("content-type").split(";") if content_type != "application/json": - raise ValueError( - f"Expected Content-Type response header to be `application/json` but received {content_type} instead." - ) + if self._strict_response_validation: + raise exceptions.APIResponseValidationError( + response=response, + request=response.request, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore data = response.json() return self._process_response_data(data=data, cast_to=cast_to, response=response) diff --git a/src/anthropic/_base_exceptions.py b/src/anthropic/_base_exceptions.py index aac00103..e2ba6aa4 100644 --- a/src/anthropic/_base_exceptions.py +++ b/src/anthropic/_base_exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing_extensions import Literal from httpx import Request, Response @@ -17,8 +19,8 @@ class APIResponseValidationError(APIError): response: Response status_code: int - def __init__(self, request: Request, response: Response) -> None: - super().__init__("Data returned by API invalid for expected schema.", request) + def __init__(self, request: Request, response: Response, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", request) self.response = response self.status_code = response.status_code diff --git a/tests/test_client.py b/tests/test_client.py index 55db3be0..d6fae54c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,7 +12,7 @@ import pytest from respx import MockRouter -from anthropic import Anthropic, AsyncAnthropic +from anthropic import Anthropic, AsyncAnthropic, APIResponseValidationError from anthropic._types import Omit from anthropic._models import BaseModel, FinalRequestOptions from anthropic._streaming import Stream, AsyncStream @@ -383,6 +383,23 @@ class Model(BaseModel): response = self.client.post("/foo", cast_to=Model, stream=True) assert isinstance(response, Stream) + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = Anthropic(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + client = Anthropic(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + class TestAsyncAnthropic: client = AsyncAnthropic(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -740,3 +757,21 @@ class Model(BaseModel): response = await self.client.post("/foo", cast_to=Model, stream=True) assert isinstance(response, AsyncStream) + + @pytest.mark.respx(base_url=base_url) + @pytest.mark.asyncio + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncAnthropic(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + client = AsyncAnthropic(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable]